From c5da67e71aefb5d0ebc3c3882a35399497f159b0 Mon Sep 17 00:00:00 2001 From: katjuell Date: Thu, 28 Apr 2022 16:20:56 -0400 Subject: [PATCH] Add code insights page & components (#122) * Add code insights page and related components * Fix styles on code insights charts components * Set height on tab carousel and clean up some styles --- package.json | 13 + src/components/Blockquote.tsx | 46 -- src/components/Carousels/CustomCarousel.tsx | 8 +- .../CodeInsightsExamples.module.scss | 70 ++ .../CodeInsights/CodeInsightsExamples.tsx | 97 +++ .../CodeInsightsQueryBlock.module.scss | 11 + .../components/CodeInsightsQueryBlock.tsx | 9 + .../components/card/Card.module.scss | 22 + .../CodeInsights/components/card/Card.tsx | 16 + .../legend/CodeInsightLegend.module.scss | 31 + .../components/legend/CodeInsightLegend.tsx | 26 + .../components/line-chart/LineChart.tsx | 389 +++++++++++ .../line-chart/LineChartContent.module.scss | 65 ++ .../components/GlyphContent.module.scss | 17 + .../line-chart/components/GlyphContent.tsx | 114 ++++ .../line-chart/components/MaybeLink.tsx | 17 + .../line-chart/components/TickComponent.tsx | 52 ++ .../TooltipContent.module.scss | 45 ++ .../tooltip-content/TooltipContent.tsx | 95 +++ .../tooltip-content/get-list-window.ts | 52 ++ .../components/line-chart/constants.ts | 59 ++ .../line-chart/helpers/generate-accessors.ts | 36 ++ .../line-chart/helpers/get-min-max.ts | 21 + .../helpers/get-processed-chart-data.ts | 104 +++ .../line-chart/helpers/get-y-axis-width.ts | 18 + .../line-chart/helpers/get-y-ticks.ts | 56 ++ .../line-chart/helpers/use-event-emitters.ts | 56 ++ .../line-chart/helpers/use-scales.ts | 116 ++++ .../line-chart-settings-provider.ts | 49 ++ .../components/line-chart/types.ts | 66 ++ .../components/view/View.module.scss | 50 ++ .../CodeInsights/components/view/View.tsx | 49 ++ src/components/CodeInsights/constants.ts | 14 + src/components/CodeInsights/mock-data.tsx | 289 +++++++++ src/components/CodeInsights/types.ts | 32 + src/components/TabCarousel.tsx | 64 ++ src/components/TemplateCodeBlock.tsx | 25 + src/components/index.ts | 4 +- .../nutanix-fixed-log4j-with-sourcegraph.tsx | 4 +- src/pages/code-insights.tsx | 610 ++++++++++++++++++ src/styles/pages/_code-insights.scss | 23 +- tsconfig.json | 1 + yarn.lock | 512 ++++++++++++++- 43 files changed, 3375 insertions(+), 78 deletions(-) create mode 100644 src/components/CodeInsights/CodeInsightsExamples.module.scss create mode 100644 src/components/CodeInsights/CodeInsightsExamples.tsx create mode 100644 src/components/CodeInsights/components/CodeInsightsQueryBlock.module.scss create mode 100644 src/components/CodeInsights/components/CodeInsightsQueryBlock.tsx create mode 100644 src/components/CodeInsights/components/card/Card.module.scss create mode 100644 src/components/CodeInsights/components/card/Card.tsx create mode 100644 src/components/CodeInsights/components/legend/CodeInsightLegend.module.scss create mode 100644 src/components/CodeInsights/components/legend/CodeInsightLegend.tsx create mode 100644 src/components/CodeInsights/components/line-chart/LineChart.tsx create mode 100644 src/components/CodeInsights/components/line-chart/LineChartContent.module.scss create mode 100644 src/components/CodeInsights/components/line-chart/components/GlyphContent.module.scss create mode 100644 src/components/CodeInsights/components/line-chart/components/GlyphContent.tsx create mode 100644 src/components/CodeInsights/components/line-chart/components/MaybeLink.tsx create mode 100644 src/components/CodeInsights/components/line-chart/components/TickComponent.tsx create mode 100644 src/components/CodeInsights/components/line-chart/components/tooltip-content/TooltipContent.module.scss create mode 100644 src/components/CodeInsights/components/line-chart/components/tooltip-content/TooltipContent.tsx create mode 100644 src/components/CodeInsights/components/line-chart/components/tooltip-content/get-list-window.ts create mode 100644 src/components/CodeInsights/components/line-chart/constants.ts create mode 100644 src/components/CodeInsights/components/line-chart/helpers/generate-accessors.ts create mode 100644 src/components/CodeInsights/components/line-chart/helpers/get-min-max.ts create mode 100644 src/components/CodeInsights/components/line-chart/helpers/get-processed-chart-data.ts create mode 100644 src/components/CodeInsights/components/line-chart/helpers/get-y-axis-width.ts create mode 100644 src/components/CodeInsights/components/line-chart/helpers/get-y-ticks.ts create mode 100644 src/components/CodeInsights/components/line-chart/helpers/use-event-emitters.ts create mode 100644 src/components/CodeInsights/components/line-chart/helpers/use-scales.ts create mode 100644 src/components/CodeInsights/components/line-chart/line-chart-settings-provider.ts create mode 100644 src/components/CodeInsights/components/line-chart/types.ts create mode 100644 src/components/CodeInsights/components/view/View.module.scss create mode 100644 src/components/CodeInsights/components/view/View.tsx create mode 100644 src/components/CodeInsights/constants.ts create mode 100644 src/components/CodeInsights/mock-data.tsx create mode 100644 src/components/CodeInsights/types.ts create mode 100644 src/components/TabCarousel.tsx create mode 100644 src/components/TemplateCodeBlock.tsx create mode 100644 src/pages/code-insights.tsx diff --git a/package.json b/package.json index dc0e6593a66..3c7c9714172 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "lint": "yarn eslint && yarn typecheck", "typecheck": "tsc -p .", "eslint": "eslint '**/*.{ts,tsx}'", + "eslint:fix": "yarn eslint --fix", "ci": "yarn install --immutable --immutable-cache --check-cache", "prettier": "prettier '**/{*.{js?(on),ts?(x),md,scss},.*.js?(on)}' --write --list-different", "prettier:check": "yarn -s prettier --write=false" @@ -39,6 +40,12 @@ } }, "dependencies": { + "@visx/grid": "^2.6.0", + "@visx/group": "^2.1.0", + "@visx/responsive": "^2.8.0", + "@visx/scale": "^2.2.2", + "@visx/text": "^2.3.0", + "@visx/xychart": "^2.9.0", "gray-matter": "^4.0.3", "lodash": "^4.17.21", "next": "^12.1.0", @@ -48,11 +55,17 @@ "rehype-autolink-headings": "^6.1.1", "rehype-slug": "^5.0.1", "remark-gfm": "^3.0.1", + "ts-loader": "^9.2.8", + "d3-format": "^3.1.0", + "d3-time-format": "^4.1.0", + "rxjs": "^7.5.5", "ts-node": "^10.7.0" }, "devDependencies": { "@sourcegraph/eslint-config": "^0.27.0", "@sourcegraph/prettierrc": "^3.0.3", + "@types/d3-format": "^3.0.1", + "@types/d3-time-format": "^4.0.0", "@types/lodash": "^4.14.180", "@types/node": "^17.0.23", "@types/react": "^17.0.43", diff --git a/src/components/Blockquote.tsx b/src/components/Blockquote.tsx index 55a330dd8cf..6c639704e5b 100644 --- a/src/components/Blockquote.tsx +++ b/src/components/Blockquote.tsx @@ -118,49 +118,3 @@ export const BlockquoteWithBorder: FunctionComponent<{ )} ) - -export const Blockquote: FunctionComponent<{ - quote: string - headline?: string - author?: string | ReactFragment - logoHref?: string - logoImage?: string - logoAlt?: string - linkText?: string - link?: string - bold?: boolean -}> = ({ quote, headline, author, logoHref, logoImage, linkText, link, logoAlt, bold }) => ( - <> - {headline &&

{headline}

} -
-

“{quote}”

- {author &&
— {author}
} -
- {logoImage && logoAlt && ( -
- {logoHref ? ( - - {logoAlt} - - ) : ( - {logoAlt} - )} -
- )} - {linkText && link && link.includes('http') && ( - - {linkText} - - - )} - {linkText && link && !link.includes('http') && ( - - {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} - -

{linkText}

- -
- - )} - -) diff --git a/src/components/Carousels/CustomCarousel.tsx b/src/components/Carousels/CustomCarousel.tsx index 2f81e940766..47839252899 100644 --- a/src/components/Carousels/CustomCarousel.tsx +++ b/src/components/Carousels/CustomCarousel.tsx @@ -47,7 +47,7 @@ export const CustomCarousel: FunctionComponent = props => { ? classNames( carouselMainStyles, currentCarousel.currentItem?.backgroundClass, - 'justify-content-between h-xl-450 h-lg-350 h-md-300 h-sm-300 h-300' + 'justify-content-between h-xl-450 h-lg-250 h-md-250 h-sm-auto h-auto' ) : autoAdvance ? classNames( @@ -125,9 +125,9 @@ export const CustomCarousel: FunctionComponent = props => {
@@ -146,7 +146,7 @@ export const CustomCarousel: FunctionComponent = props => { ))}
-
+
carouselHook.moveCarousel('decrement')} diff --git a/src/components/CodeInsights/CodeInsightsExamples.module.scss b/src/components/CodeInsights/CodeInsightsExamples.module.scss new file mode 100644 index 00000000000..ee1c9f1ef97 --- /dev/null +++ b/src/components/CodeInsights/CodeInsightsExamples.module.scss @@ -0,0 +1,70 @@ +.card { + --border-radius: 3px; + --color-bg-1: #ffffff; + --border-color-2: #dbe2f0; + --primary: #1c7ed6; + --body-bg: #fcfcff; + --border-color: #e2e4e8; + --body-color: #343a4d; + --text-muted: #5e6e8c; + --box-shadow: 0 0.25rem 0.5rem #{rgba(#343a4d, 0.07)}; + + flex: 1; + flex-basis: 25rem; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, + 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; +} + +/* Need to override autogenerated styles. +Also see https://github.com/sourcegraph/about/blob/40fe7625efa20c29b1563ae2a320f07b5afc27dd/website/src/components/code-insights/components/line-chart/LineChartContent.module.scss#L56 +*/ + +.searchChart { + height: 200px !important; +} + +.chart { + min-height: 13rem; + flex-grow: 1; + width: 100%; + position: relative; + flex-basis: 13rem; +} + +.chartContent { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; +} + +.actionLink { + margin-top: -0.25rem; + flex-shrink: 0; +} + +.legend { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin: 1rem 0 0 0; + + &--horizontal { + flex-direction: column; + } +} + +.legendMigrationItem { + min-width: 7.5rem; + font-size: 12px; +} + +.captureGroup { + display: flex; + height: 100%; +} + +.keyword { + color: var(--primary); +} diff --git a/src/components/CodeInsights/CodeInsightsExamples.tsx b/src/components/CodeInsights/CodeInsightsExamples.tsx new file mode 100644 index 00000000000..31d938928ce --- /dev/null +++ b/src/components/CodeInsights/CodeInsightsExamples.tsx @@ -0,0 +1,97 @@ +import React from 'react' + +import { ParentSize } from '@visx/responsive' +import classNames from 'classnames' + +import { CodeInsightsQueryBlock } from './components/CodeInsightsQueryBlock' +import { LegendBlock, LegendItem } from './components/legend/CodeInsightLegend' +import { getLineStroke } from './components/line-chart/constants' +import { LineChart } from './components/line-chart/LineChart' +import { LineChartSeries } from './components/line-chart/types' +import { View } from './components/view/View' +import { CaptureGroupInsightData, CodeInsightExampleType, SearchInsightData } from './types' + +import styles from './CodeInsightsExamples.module.scss' + +export type CodeInsightExampleProps = CodeInsightSearchExampleProps | CodeInsightCaptureExampleProps + +export interface CodeInsightSearchExampleProps { + type: CodeInsightExampleType.Search + data: SearchInsightData + className?: string +} + +export interface CodeInsightCaptureExampleProps { + type: CodeInsightExampleType.Capture + data: CaptureGroupInsightData + className?: string +} + +export const CodeInsightExample: React.FunctionComponent = props => { + const { type } = props + + if (type === CodeInsightExampleType.Search) { + return + } + + return +} + +const CodeInsightSearchExample: React.FunctionComponent = props => { + const { className, data } = props + + return ( + {data.repositories}} + className={classNames(className, styles.card)} + > +
+ + {({ width, height }) => } + +
+ + + {data.series.map(line => ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (line)}> + + {line.name} + + {line.query} + + ))} + +
+ ) +} + +const CodeInsightCaptureExample: React.FunctionComponent = props => { + const { className, data } = props + + return ( + All repositories} + className={classNames(className, styles.card)} + > +
+
+ + {({ width, height }) => } + +
+ + + {data.series.map(line => ( + >(line)}> + {line.name} + + ))} + +
+ {data.query} +
+ ) +} diff --git a/src/components/CodeInsights/components/CodeInsightsQueryBlock.module.scss b/src/components/CodeInsights/components/CodeInsightsQueryBlock.module.scss new file mode 100644 index 00000000000..0aa2e2a4812 --- /dev/null +++ b/src/components/CodeInsights/components/CodeInsightsQueryBlock.module.scss @@ -0,0 +1,11 @@ +.query { + display: inline-block; + width: 100%; + margin: 0; + padding: 0.25rem 0.5rem; + font-size: 11px; + border-radius: 3px; + background-color: #f9fafb; + font-family: sfmono-regular, consolas, menlo, dejavu sans mono, monospace; + word-break: break-word; +} diff --git a/src/components/CodeInsights/components/CodeInsightsQueryBlock.tsx b/src/components/CodeInsights/components/CodeInsightsQueryBlock.tsx new file mode 100644 index 00000000000..ca66b4ad651 --- /dev/null +++ b/src/components/CodeInsights/components/CodeInsightsQueryBlock.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +import classNames from 'classnames' + +import styles from './CodeInsightsQueryBlock.module.scss' + +export const CodeInsightsQueryBlock: React.FunctionComponent> = props => ( + +) diff --git a/src/components/CodeInsights/components/card/Card.module.scss b/src/components/CodeInsights/components/card/Card.module.scss new file mode 100644 index 00000000000..9f7795394ef --- /dev/null +++ b/src/components/CodeInsights/components/card/Card.module.scss @@ -0,0 +1,22 @@ +.card { + --card-bg: var(--color-bg-1); + --card-border-color: var(--border-color-2); + --card-border-radius: var(--border-radius); + + // Added inset box shadow to prevent interactive card jump on hover and focus + --hover-box-shadow: 0 0 0 1px var(--primary) inset; + --card-spacer-y: 0.5rem; + --card-spacer-x: 0.5rem; + + position: relative; + display: flex; + flex-direction: column; + min-width: 0; // See https://github.com/twbs/bootstrap/pull/22740#issuecomment-305868106 + word-wrap: break-word; + background-color: var(--card-bg); + background-clip: border-box; + border-width: 1px; + border-style: solid; + border-color: var(--card-border-color); + border-radius: var(--card-border-radius); +} diff --git a/src/components/CodeInsights/components/card/Card.tsx b/src/components/CodeInsights/components/card/Card.tsx new file mode 100644 index 00000000000..e64776ae1d3 --- /dev/null +++ b/src/components/CodeInsights/components/card/Card.tsx @@ -0,0 +1,16 @@ +import React from 'react' + +import classNames from 'classnames' + +import styles from './Card.module.scss' + +export interface CardProps extends React.HTMLAttributes {} + +/** + * Card Element + */ +export const Card: React.FunctionComponent = ({ children, className, ...attributes }) => ( +
+ {children} +
+) diff --git a/src/components/CodeInsights/components/legend/CodeInsightLegend.module.scss b/src/components/CodeInsights/components/legend/CodeInsightLegend.module.scss new file mode 100644 index 00000000000..97d92b0b4ca --- /dev/null +++ b/src/components/CodeInsights/components/legend/CodeInsightLegend.module.scss @@ -0,0 +1,31 @@ +.legendList { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + + // Reset ul styles (bullets, paddings, margins) + list-style: none; + margin: 0; + padding: 0; + line-height: 1.2; +} + +.legendItem { + display: flex; + align-items: baseline; + font-size: 12px; + + @media only screen and (max-width: 768px) { + max-width: 100%; + flex-wrap: wrap; + } +} + +.legendMark { + align-self: baseline; + width: 0.5rem; + height: 0.5rem; + flex-shrink: 0; + margin-right: 0.25rem; + border-radius: 50%; +} diff --git a/src/components/CodeInsights/components/legend/CodeInsightLegend.tsx b/src/components/CodeInsights/components/legend/CodeInsightLegend.tsx new file mode 100644 index 00000000000..b843a166c1e --- /dev/null +++ b/src/components/CodeInsights/components/legend/CodeInsightLegend.tsx @@ -0,0 +1,26 @@ +import React from 'react' + +import classNames from 'classnames' + +import styles from './CodeInsightLegend.module.scss' + +export const LegendBlock: React.FunctionComponent> = props => ( +
    {props.children}
+) + +interface LegendItemProps extends React.LiHTMLAttributes { + color: string +} + +export const LegendItem: React.FunctionComponent = props => ( +
+
  • +
    + {props.children} +
  • +
    +) diff --git a/src/components/CodeInsights/components/line-chart/LineChart.tsx b/src/components/CodeInsights/components/line-chart/LineChart.tsx new file mode 100644 index 00000000000..5e13d504b02 --- /dev/null +++ b/src/components/CodeInsights/components/line-chart/LineChart.tsx @@ -0,0 +1,389 @@ +import React, { ReactElement, useCallback, useMemo, useState, MouseEvent, useRef } from 'react' + +import { curveLinear } from '@visx/curve' +import { GridRows } from '@visx/grid' +import { Group } from '@visx/group' +import { + Axis, + DataProvider, + GlyphSeries, + LineSeries, + Tooltip, + TooltipProvider, + XYChart, + EventEmitterProvider, +} from '@visx/xychart' +import { RenderTooltipParams } from '@visx/xychart/lib/components/Tooltip' +import { XYCHART_EVENT_SOURCE } from '@visx/xychart/lib/constants' +import isValidNumber from '@visx/xychart/lib/typeguards/isValidNumber' +import { EventHandlerParams } from '@visx/xychart/lib/types' +import classNames from 'classnames' +import { noop } from 'rxjs' + +import { ActiveDatum, GlyphContent } from './components/GlyphContent' +import { dateTickFormatter, numberFormatter, Tick, getTickXProps, getTickYProps } from './components/TickComponent' +import { TooltipContent } from './components/tooltip-content/TooltipContent' +import { getLineStroke } from './constants' +import { generateAccessors } from './helpers/generate-accessors' +import { getProcessedChartData } from './helpers/get-processed-chart-data' +import { getYAxisWidth } from './helpers/get-y-axis-width' +import { getYTicks } from './helpers/get-y-ticks' +import { usePointerEventEmitters } from './helpers/use-event-emitters' +import { useScalesConfiguration, useXScale, useYScale } from './helpers/use-scales' +import { LineChartContent as LineChartContentType, onDatumZoneClick, Point } from './types' + +import styles from './LineChartContent.module.scss' + +// Chart configuration +const WIDTH_PER_TICK = 70 +const MARGIN = { top: 10, left: 0, bottom: 26, right: 20 } +const SCALES_CONFIG = { + x: { + type: 'time' as const, + nice: true, + }, + y: { + type: 'linear' as const, + nice: true, + zero: false, + clamp: false, + }, +} + +const stopPropagation = (event: React.MouseEvent): void => event.stopPropagation() + +export interface LineChartContentProps + extends Omit, 'chart'> { + /** Chart width value in px */ + width: number + + /** Chart height value in px */ + height: number + + /** + * Callback calls every time when a point-zone (zone around point) but not point itself + * on the chart was clicked. + */ + onDatumZoneClick?: onDatumZoneClick + + /** + * Callback calls every time when link-point and only link-point + * on the chart was clicked. + */ + onDatumLinkClick?: (event: React.MouseEvent) => void +} + +export function LineChart(props: LineChartContentProps): ReactElement { + return ( + + + + ) +} + +/** + * Displays line chart content - line chart, tooltip, active point + */ +export function LineChartContent(props: LineChartContentProps): ReactElement { + const { width, height, data, series, xAxis, onDatumZoneClick = noop, onDatumLinkClick = noop } = props + + // XYChart must know how to get the right data from datum object in order to render lines and axes + // Because of that we have to generate map of getters for all kind of data which will be rendered on the chart. + const accessors = useMemo(() => generateAccessors(xAxis, series), [xAxis, series]) + + const scalesConfiguration = useScalesConfiguration({ + data, + accessors, + config: SCALES_CONFIG, + }) + + const innerHeight = height - MARGIN.top - MARGIN.bottom + + const yScale = useYScale({ config: scalesConfiguration.y, height: innerHeight }) + const yTicks = getYTicks(yScale, innerHeight) + const yAxisWidth = getYAxisWidth(yTicks) + + // Calculate inner sizes for chart without padding values + const innerWidth = width - MARGIN.left - MARGIN.right - yAxisWidth + const numberOfTicksX = Math.max(1, Math.floor(innerWidth / WIDTH_PER_TICK)) + + const xScale = useXScale({ + config: scalesConfiguration.x, + width: innerWidth, + accessors, + data, + }) + + const dynamicMargin = { ...MARGIN, left: MARGIN.left + yAxisWidth } + + const { seriesWithData } = useMemo( + () => getProcessedChartData({ accessors, data, series }), + [data, accessors, series] + ) + + // state + const [hoveredDatum, setHoveredDatum] = useState | null>(null) + const [focusedDatum, setFocusedDatum] = useState | null>(null) + + // callbacks + const renderTooltip = useCallback( + (renderProps: RenderTooltipParams) => , + [seriesWithData] + ) + + const handlePointerMove = useCallback( + (event: EventHandlerParams) => { + // If active point hasn't been change we shouldn't call setActiveDatum again + if (hoveredDatum?.index === event.index && hoveredDatum?.key === event.key) { + return + } + + const line = series.find(line => line.dataKey === event.key) + + if (!line) { + setHoveredDatum(null) + return + } + + setHoveredDatum({ + ...event, + line, + }) + }, + [series, hoveredDatum] + ) + + const handlePointerUp = useCallback( + (info: EventHandlerParams) => { + info.event?.persist() + + // According to types from visx/xychart index can be undefined + const activeDatumIndex = hoveredDatum?.index + const line = series.find(line => line.dataKey === info.key) + + if (!info.event || !line || !hoveredDatum?.datum || !isValidNumber(activeDatumIndex)) { + return + } + + onDatumZoneClick({ + originEvent: info.event as MouseEvent, + link: line?.linkURLs?.[+hoveredDatum.datum.x] ?? line?.linkURLs?.[activeDatumIndex], + }) + }, + [series, onDatumZoneClick, hoveredDatum] + ) + + const { + onPointerMove = noop, + onPointerOut = noop, + ...otherHandlers + } = usePointerEventEmitters({ + source: XYCHART_EVENT_SOURCE, + onFocus: true, + onBlur: true, + }) + + // We only need to catch pointerout event on root element - chart + // we can't rely on event propagation here because this leads us to + // unnecessary calls when some child element had lost cursor he fired + // that unnecessary event. So we have to track the pointerout by ourselves. + // This focused ref is kind of a flag to track do we have any event from + // user on chart or not used below in move and out handlers to fire pointerout + // event in right moment and avoid unnecessary onPointerOut calls. + const focused = useRef(false) + + const handleRootPointerMove = useCallback( + (event: React.PointerEvent) => { + // Track user activity over chart + focused.current = true + onPointerMove(event) + }, + [onPointerMove] + ) + + const handleRootPointerOut = useCallback( + (event: React.PointerEvent) => { + event.persist() + + // Some element has lost cursor and fired pointerout event but + // we don't know which element did that root element or some child element within root element + // So we mark this focused state as false = root element is not active and then schedule + // next frame check decide do we need fire callback or know. If child lost cursor then + // but cursor still on chart then we mark this focused state in pointerMove handler above + // and won't fire onPointerOut callback. In case if root element child lost cursor we fire onPointerOut + focused.current = false + + requestAnimationFrame(() => { + if (!focused.current) { + setHoveredDatum(null) + onPointerOut(event) + } + }) + }, + [focused, onPointerOut, setHoveredDatum] + ) + + // Disable all event listeners explicitly to avoid flaky tooltip appearance + const eventEmitters = { + onPointerMove: handleRootPointerMove, + onPointerOut: handleRootPointerOut, + ...otherHandlers, + } + + const hoveredDatumLinks = hoveredDatum?.line?.linkURLs ?? {} + const hoveredDatumLink = hoveredDatum + ? hoveredDatumLinks[+hoveredDatum.datum.x] ?? hoveredDatumLinks[hoveredDatum.index] + : null + const rootClasses = classNames({ [styles.contentWithCursor]: !!hoveredDatumLink }) + + return ( +
    + {/* + Because XYChart wraps itself with context providers in case if consumer didn't add them + But this recursive wrapping leads to problem with event emitter context - double subscription all event + See https://github.com/airbnb/visx/blob/master/packages/visx-xychart/src/components/XYChart.tsx#L128-L138 + If we need override EventEmitter (our case because we have to capture all event by ourselves) we + have to provide DataContext and TooltipContext as well to avoid problem with EmitterContext. + */} + + + + + {/* eslint-disable-next-line jsx-a11y/aria-role */} + + + + + + + + + {/* eslint-disable-next-line jsx-a11y/aria-role */} + + + + + + + {/* Spread size of parent group element by transparent rect with width and height */} + + + {seriesWithData.map((line, index) => ( + + point?.x} + yAccessor={point => point?.y} + stroke={getLineStroke(line)} + curve={curveLinear} + aria-hidden={true} + /> + + point?.x} + yAccessor={point => point?.y} + // Don't have info about line in props. @visx/xychart doesn't expose this information + // Move this arrow function in separate component when API of GlyphSeries will be fixed. + renderGlyph={glyphProps => ( + + )} + /> + + ))} + + + + + + +
    + ) +} diff --git a/src/components/CodeInsights/components/line-chart/LineChartContent.module.scss b/src/components/CodeInsights/components/line-chart/LineChartContent.module.scss new file mode 100644 index 00000000000..42ae60e88f8 --- /dev/null +++ b/src/components/CodeInsights/components/line-chart/LineChartContent.module.scss @@ -0,0 +1,65 @@ +.content { + &--with-cursor { + cursor: pointer; + } +} + +// We are not able to add our own classnames for grid lines elements +// because of that we have to use nested statements below + +.gridLine { + line { + stroke: var(--border-color-2); + stroke-width: 1; + } +} + +.axisLine { + stroke: var(--border-color); + stroke-width: 1; + + &--vertical { + // Hide line axis visually and hide from voice over + stroke-width: 0; + display: none; + } +} + +// We are not able to add our own classnames for tick line and text elements +// because of that we have to use nested statements below + +.axisTick { + // small tick line + line { + stroke: var(--border-color); + stroke-width: 1; + } + + // tick label + text { + fill: var(--text-muted); + font-size: 0.75rem; + font-weight: 400; + } + + &--vertical { + line { + // Hide line ticks visually and hide them from voice over + stroke-width: 0; + display: none; + } + } +} + +/* + Tooltip element from visx package is adding some inline styles by himself. + There is no way to override them from css unless !important statement. +*/ +.tooltip { + box-shadow: var(--box-shadow) !important; + border: 1px solid var(--border-color); + color: var(--body-color) !important; + background: #ffffff !important; + + padding: 0.5rem !important; +} diff --git a/src/components/CodeInsights/components/line-chart/components/GlyphContent.module.scss b/src/components/CodeInsights/components/line-chart/components/GlyphContent.module.scss new file mode 100644 index 00000000000..3457bb71ef7 --- /dev/null +++ b/src/components/CodeInsights/components/line-chart/components/GlyphContent.module.scss @@ -0,0 +1,17 @@ +.glyphLink:focus .glyph { + stroke-width: 3; + // Unfortunately we can't use r styling right now as it's part of SVG 2 spec + // and for now we use js to track focus on link and change radius of line point with focus + // but when svg 2 will have broad browser support js implementation on focus should + // be removed and instead we should control the radius of points using the r attribute styling approach. + // r: 6; +} + +.glyph { + stroke-width: 2; + fill: var(--body-bg); + + &--active { + stroke-width: 3; + } +} diff --git a/src/components/CodeInsights/components/line-chart/components/GlyphContent.tsx b/src/components/CodeInsights/components/line-chart/components/GlyphContent.tsx new file mode 100644 index 00000000000..85700ec83b6 --- /dev/null +++ b/src/components/CodeInsights/components/line-chart/components/GlyphContent.tsx @@ -0,0 +1,114 @@ +import React, { MouseEventHandler, PointerEventHandler, ReactElement } from 'react' + +import { GlyphDot as Glyph } from '@visx/glyph' +import { EventHandlerParams, GlyphProps } from '@visx/xychart/lib/types' +import classNames from 'classnames' + +import { getLineStroke } from '../constants' +import { LineChartSeries, Point } from '../types' + +import { MaybeLink } from './MaybeLink' +import { dateLabelFormatter } from './TickComponent' + +import styles from './GlyphContent.module.scss' + +/** + * Type for active datum state in LineChartContent component. In order to render active state + * for hovered or focused point we need to track active datum to calculate styles for active glyph. + */ +export interface ActiveDatum extends EventHandlerParams { + /** Series of data of active datum */ + line?: LineChartSeries +} + +interface GlyphContentProps extends Omit, 'key' | 'index'> { + /** + * Just because GlyphProps has a bug with types. + * GlyphProps key is an index of current datum and + * GlyphProps index doesn't exist in runtime. + */ + index: string + + /** Hovered point info (datum) to calculate proper styles for particular Glyph */ + hoveredDatum: ActiveDatum | null + + /** Focused point info (datum) to calculate proper styles for particular Glyph */ + focusedDatum: ActiveDatum | null + + /** Line (series) index of current point */ + lineIndex: number + + /** Total number of lines (series) to calculate proper aria-label for glyph content */ + totalNumberOfLines: number + + /** Data of particular line of current glyph (chart point) */ + line: LineChartSeries + + /** Focus handler for glyph (chart point) */ + setFocusedDatum: (datum: ActiveDatum | null) => void + + /** On click handler for root component of glyph content */ + onClick: MouseEventHandler + + /** On pointer up handler for root component of glyph content */ + onPointerUp: PointerEventHandler +} + +/** Displays glyph (chart point) with link */ +export function GlyphContent(props: GlyphContentProps): ReactElement { + const { + index, + line, + hoveredDatum, + focusedDatum, + datum, + lineIndex, + totalNumberOfLines, + x: xCoordinate, + y: yCoordinate, + onPointerUp, + onClick, + setFocusedDatum, + } = props + + const currentDatumIndex = +index + const hovered = hoveredDatum?.index === currentDatumIndex && hoveredDatum.key === line.dataKey + const focused = focusedDatum?.index === currentDatumIndex && focusedDatum.key === line.dataKey + + const linkURL = line.linkURLs?.[+datum.x] ?? line.linkURLs?.[currentDatumIndex] + + const currentDatum = { + key: line.dataKey.toString(), + index: currentDatumIndex, + datum, + } + + const xAxisValue = dateLabelFormatter(new Date(datum.x)) + const yAxisValue = (datum.y as unknown as string) ?? '' + const ariaLabel = `Point ${currentDatumIndex + 1} of line ${ + lineIndex + 1 + } of ${totalNumberOfLines}. X value: ${xAxisValue}. Y value: ${yAxisValue}` + + return ( + linkURL && setFocusedDatum(currentDatum)} + onBlur={() => linkURL && setFocusedDatum(null)} + className={styles.glyphLink} + role={linkURL ? 'link' : 'graphics-dataunit'} + aria-label={ariaLabel} + > + + + ) +} diff --git a/src/components/CodeInsights/components/line-chart/components/MaybeLink.tsx b/src/components/CodeInsights/components/line-chart/components/MaybeLink.tsx new file mode 100644 index 00000000000..d2a4953914d --- /dev/null +++ b/src/components/CodeInsights/components/line-chart/components/MaybeLink.tsx @@ -0,0 +1,17 @@ +import React from 'react' + +import Link from 'next/link' + +interface MaybeLinkProps extends React.AnchorHTMLAttributes { + to?: string +} + +/** Wraps the children in a link if to (link href) prop is passed. */ +export const MaybeLink: React.FunctionComponent = ({ children, to, ...props }) => + to ? ( + + {children} + + ) : ( + (children as React.ReactElement) + ) diff --git a/src/components/CodeInsights/components/line-chart/components/TickComponent.tsx b/src/components/CodeInsights/components/line-chart/components/TickComponent.tsx new file mode 100644 index 00000000000..15eb1622469 --- /dev/null +++ b/src/components/CodeInsights/components/line-chart/components/TickComponent.tsx @@ -0,0 +1,52 @@ +import React from 'react' + +import { TickLabelProps, TickRendererProps } from '@visx/axis/lib/types' +import { Group } from '@visx/group' +import { Text } from '@visx/text' +import { TextProps } from '@visx/text/lib/Text' +import { format } from 'd3-format' +import { timeFormat } from 'd3-time-format' + +// Date formatters +const SI_PREFIX_FORMATTER = format('~s') +export const numberFormatter = (number: number): string => { + if (!Number.isInteger(number)) { + return number.toString() + } + + return SI_PREFIX_FORMATTER(number) +} + +// Number of month day + short name of month +export const dateTickFormatter = timeFormat('%d %b') +// Year + full name of month + full name of week day +export const dateLabelFormatter = timeFormat('%d %B %A') + +// Label props generators for x and y axes. +// We need separate x and y generators because we need formatted value +// depend on for which axis we generate label props +export const getTickYProps: TickLabelProps = (value, index, values): Partial => ({ + 'aria-label': `Tick axis ${index + 1} of ${values.length}. Value: ${value}`, +}) +export const getTickXProps: TickLabelProps = (value, index, values): Partial => ({ + 'aria-label': `Tick axis ${index + 1} of ${values.length}. Value: ${dateLabelFormatter(value)}`, +}) + +/** Tick component displays tick label for each axis line of chart */ +export const Tick: React.FunctionComponent = props => { + const { to, formattedValue, x: xPosition, y: yPosition, ...tickLabelProps } = props + + // Hack with Group + Text (aria hidden) + // Because Text renders text inside another svg element and text with tspan + // that makes another nested group for accessibility tree. To avoid group - end group + // phrases of voice over we hide nested children from a11y tree and put explicit aria-label + // on parent Group element with role text + return ( + // eslint-disable-next-line jsx-a11y/aria-role + + + {formattedValue} + + + ) +} diff --git a/src/components/CodeInsights/components/line-chart/components/tooltip-content/TooltipContent.module.scss b/src/components/CodeInsights/components/line-chart/components/tooltip-content/TooltipContent.module.scss new file mode 100644 index 00000000000..91dcdd62529 --- /dev/null +++ b/src/components/CodeInsights/components/line-chart/components/tooltip-content/TooltipContent.module.scss @@ -0,0 +1,45 @@ +.tooltipList { + list-style: none; + margin: 0; + padding: 0; + + display: flex; + flex-direction: column; + gap: 0.25rem; + min-width: 12rem; + max-width: 20rem; + font-size: 11px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, + 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; +} + +.item { + display: flex; + align-items: baseline; + padding: 0.25rem; + border-radius: 0.25rem; + font-weight: normal; +} + +.legendText { + flex-grow: 1; +} + +.legendValue { + margin-left: 1rem; +} + +.mark { + align-self: baseline; + width: 0.5rem; + height: 0.5rem; + flex-shrink: 0; + margin-right: 0.25rem; + border-radius: 50%; +} + +.dateTime { + font-weight: 600; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, + 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; +} diff --git a/src/components/CodeInsights/components/line-chart/components/tooltip-content/TooltipContent.tsx b/src/components/CodeInsights/components/line-chart/components/tooltip-content/TooltipContent.tsx new file mode 100644 index 00000000000..3ac568395ff --- /dev/null +++ b/src/components/CodeInsights/components/line-chart/components/tooltip-content/TooltipContent.tsx @@ -0,0 +1,95 @@ +import React, { ReactElement, useMemo } from 'react' + +import { RenderTooltipParams } from '@visx/xychart/lib/components/Tooltip' + +import { getLineStroke } from '../../constants' +import { LineChartSeriesWithData, Point } from '../../types' + +import { getListWindow, ListWindow } from './get-list-window' + +import styles from './TooltipContent.module.scss' + +const MAX_ITEMS_IN_TOOLTIP = 10 + +function isDefined(value: T): value is NonNullable { + return value !== undefined && value !== null +} + +export interface TooltipContentProps extends RenderTooltipParams { + /** Dataset of series (lines) on the chart. */ + series: LineChartSeriesWithData[] +} + +/** + * Display tooltip content for XYChart. + * It consists of title - datetime for current x point and list of all nearest y points. + */ +export function TooltipContent(props: TooltipContentProps): ReactElement | null { + const { tooltipData, series } = props + const datum = tooltipData?.nearestDatum?.datum + const nearestSeriesKey = tooltipData?.nearestDatum?.key + + const lines = useMemo & { point: Point }>>(() => { + if (!datum) { + return { window: [], leftRemaining: 0, rightRemaining: 0 } + } + + const sortedSeries = [...series] + .map(line => { + const point = line.data.find(point => +point.x === +datum.x) + + if (!point) { + return + } + + return { ...line, point } + }) + .filter(isDefined) + .sort((lineA, lineB) => (lineB.point.y ?? 0) - (lineA.point?.y ?? 0)) + + // Find index of hovered point + const hoveredSeriesIndex = sortedSeries.findIndex(line => line.dataKey === nearestSeriesKey) + + // Normalize index of hovered point + const centerIndex = hoveredSeriesIndex !== -1 ? hoveredSeriesIndex : Math.floor(sortedSeries.length / 2) + + return getListWindow(sortedSeries, centerIndex, MAX_ITEMS_IN_TOOLTIP) + }, [series, datum, nearestSeriesKey]) + + if (!datum) { + return null + } + + const dateString = new Date(datum.x).toDateString() + + return ( + <> +
    {dateString}
    + +
      + {lines.leftRemaining > 0 &&
    • ... and {lines.leftRemaining} more
    • } + {lines.window.map(line => { + const value = line.point.y + const datumKey = tooltipData?.nearestDatum?.key + + const backgroundColor = datumKey === line.dataKey ? '#eef1f7' : '' + + /* eslint-disable react/forbid-dom-props */ + return ( +
    • +
      + + {line?.name ?? 'unknown series'} + + + {' '} + {value === null || Number.isNaN(value) ? '–' : value}{' '} + +
    • + ) + })} + {lines.rightRemaining > 0 &&
    • ... and {lines.rightRemaining} more
    • } +
    + + ) +} diff --git a/src/components/CodeInsights/components/line-chart/components/tooltip-content/get-list-window.ts b/src/components/CodeInsights/components/line-chart/components/tooltip-content/get-list-window.ts new file mode 100644 index 00000000000..105d3fb6625 --- /dev/null +++ b/src/components/CodeInsights/components/line-chart/components/tooltip-content/get-list-window.ts @@ -0,0 +1,52 @@ +export interface ListWindow { + window: T[] + leftRemaining: number + rightRemaining: number +} + +/** + * Return window (sub-list) around point with {@link index} and with size + * equals to {@link size}. + * + * Example: + * list - 1,2,3,4,5,6,7 index - 3, size - 3 + * result 3,4,5 because 1,2, * 3, 4, 5 * 6, 7 + * remaining left 2 (1,2) remaining right 2 (6, 7) + */ +export function getListWindow(list: T[], index: number, size: number): ListWindow { + if (list.length < size) { + return { window: list, leftRemaining: 0, rightRemaining: 0 } + } + + let left = index + let right = index + const window = [list[index]] + + while (window.length < size) { + const nextLeft = left - 1 + const leftElement = list[nextLeft] + + if (leftElement) { + left-- + window.unshift(leftElement) + } + + const nextRight = right + 1 + const rightElement = list[nextRight] + + if (rightElement) { + right++ + window.push(rightElement) + } + + if (!leftElement && !rightElement) { + break + } + } + + return { + window, + leftRemaining: Math.max(left, 0), + rightRemaining: Math.max(list.length - 1 - right, 0), + } +} diff --git a/src/components/CodeInsights/components/line-chart/constants.ts b/src/components/CodeInsights/components/line-chart/constants.ts new file mode 100644 index 00000000000..a44ac85cc90 --- /dev/null +++ b/src/components/CodeInsights/components/line-chart/constants.ts @@ -0,0 +1,59 @@ +import { LineChartSeries } from './types' + +/** + * Default value for line color in case if we didn't get color for line from content config. + */ +export const DEFAULT_LINE_STROKE = 'var(--gray-07)' + +export const getLineStroke = (line: LineChartSeries): string => + line?.stroke ?? DEFAULT_LINE_STROKE + +/** + * Visx xy-chart supports data series with missing. To show the + * points but not the very beginning of the chart we should use + * this default empty value. See example below points that have + * EMPTY_DATA_POINT_VALUE value haven't been rendered instead of + * that we rendered non active background (``` area) + * + *
    + * ┌──────────────────────────────┐     ┌──────────────────────────────┐
    + * │``````````````                │ 10  │         ````````             │ 10
    + * │``````````````                │     │       ▼ ````````             │
    + * │``````````````              ▼ │ 9   │         ````````           ▼ │ 9
    + * │``````````````                │     │  ▼      ````````             │
    + * │``````````````      ▼         │ 8   │         ````````    ▼        │ 8
    + * │``````````````                │     │         ````````             │
    + * │``````````````          ▼     │ 7   │         ````````       ▼     │ 7
    + * │`````````````` ▼              │     │    ▼    ```````` ▼           │
    + * │``````````````                │ 6   │         ````````             │ 6
    + * │``````````````                │     │         ````````             │
    + * │``````````````                │ 5   │         ````````             │ 5
    + * └──────────────────────────────┘     └──────────────────────────────┘
    + * 
    + */ +export const EMPTY_DATA_POINT_VALUE = null + +/** + * If width of the chart is less than this var width value we should put the legend + * block below the chart block + * + * ``` + * Less than 450px - put legend below Chart block has enough space - render legend aside + * ▲ ▲ + * │ ● ● │ ● ● Item 1 + * │ ● ● │ ● ● ● Item 2 + * │ ● ● ● │ ● ● ● + * │ ● ● │ ● ● + * │ ● │ ● + * │ │ + * └─────────────────▶ └─────────────▶ + * ● Item 1 ● Item 2 + * ``` + */ +export const MINIMAL_HORIZONTAL_LAYOUT_WIDTH = 460 + +/** + * Even if have a big enough width for putting legend aside (see {@link MINIMAL_HORIZONTAL_LAYOUT_WIDTH}) + * we should enable this mode only if line chart has more than 3 series + */ +export const MINIMAL_SERIES_FOR_ASIDE_LEGEND = 3 diff --git a/src/components/CodeInsights/components/line-chart/helpers/generate-accessors.ts b/src/components/CodeInsights/components/line-chart/helpers/generate-accessors.ts new file mode 100644 index 00000000000..8323344b4e4 --- /dev/null +++ b/src/components/CodeInsights/components/line-chart/helpers/generate-accessors.ts @@ -0,0 +1,36 @@ +import { EMPTY_DATA_POINT_VALUE } from '../constants' +import { ChartAxis, Accessors } from '../types' + +/** + * Returns accessors map which allows charts to get right values from datum object + * One accessor for x - time axis and a map for different series of data for y axis + */ +export function generateAccessors( + xAxis: ChartAxis, + series: { dataKey: keyof Datum }[] +): Accessors { + const { dataKey: xDataKey, scale = 'time' } = xAxis + + return { + x: data => + scale === 'time' + ? // as unknown as string quick hack for cast Datum[keyof Datum] to string + // fix that when we will have a value type for LineChartContent generic + new Date(data?.[xDataKey] as unknown as number) + : // In case if we got linear scale we have to operate with numbers + +(data?.[xDataKey] ?? 0), + y: series.reduce((accessors, currentLine) => { + const { dataKey } = currentLine + // as unknown as string quick hack for cast Datum[keyof Datum] to string + // fix that when we will have a value type for LineChartContent generic + const key = dataKey as unknown as keyof Datum + + // If we get EMPTY_DATA_POINT_VALUE we should omit the '+' number casting + accessors[key] = data => + data[dataKey] === EMPTY_DATA_POINT_VALUE ? EMPTY_DATA_POINT_VALUE : +data[dataKey] + + return accessors + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any + }, {} as Record any>), + } +} diff --git a/src/components/CodeInsights/components/line-chart/helpers/get-min-max.ts b/src/components/CodeInsights/components/line-chart/helpers/get-min-max.ts new file mode 100644 index 00000000000..f15805ce5f2 --- /dev/null +++ b/src/components/CodeInsights/components/line-chart/helpers/get-min-max.ts @@ -0,0 +1,21 @@ +import { Accessors } from '../types' + +/** Returns minimal and maximal value from data series */ +export function getMinAndMax( + data: Datum[], + accessors: Accessors +): [number, number] { + const keys = Object.keys(accessors.y) as Key[] + + const resultArray = data.reduce((memo, item) => { + for (const key of keys) { + const accessor = accessors.y[key] + + memo.push(+accessor(item)) + } + + return memo + }, []) + + return [Math.min(...resultArray), Math.max(...resultArray)] +} diff --git a/src/components/CodeInsights/components/line-chart/helpers/get-processed-chart-data.ts b/src/components/CodeInsights/components/line-chart/helpers/get-processed-chart-data.ts new file mode 100644 index 00000000000..d7d670cf862 --- /dev/null +++ b/src/components/CodeInsights/components/line-chart/helpers/get-processed-chart-data.ts @@ -0,0 +1,104 @@ +import { Accessors, Point, LineChartSeriesWithData, LineChartSeries } from '../types' + +interface GetProcessedChartDataProps { + data: Datum[] + + /** + * Accessors map to get (x, y) value from datum objects + */ + accessors: Accessors + + series: LineChartSeries[] +} + +interface ProcessedChartData { + /** + * List of processed data series data for each data series line. + */ + seriesWithData: LineChartSeriesWithData[] + + sortedData: Datum[] +} + +/** + * Processes data list (sort it) and extracts data from the datum object and merge it with + * series (line) data. + * + * Example: + * ``` + * data: [ + * { a: 2, b: 3, x: 2}, + * { a: 4, b: 5, x: 3}, + * { a: null, b: 6, x: 4} + * ] → series: [ + * { name: a, data: [{ y: 2, x: 2 }, { y: 4, x: 3 }], + * { name: b, data: [{ y: 3, x: 2 }, { y: 5, x: 3 }, { y: 6, x: 4 }] + * ] + * ``` + */ +export function getProcessedChartData( + props: GetProcessedChartDataProps +): ProcessedChartData { + const { data, series, accessors } = props + + // In case if we've got unsorted by x (time) axis dataset we have to sort that by ourselves + // otherwise we will get an error in calculation of position for the tooltip + // Details: bisector from d3-array package expects sorted data otherwise he can't calculate + // right index for nearest point on the XYChart. + // See https://github.com/airbnb/visx/blob/master/packages/visx-xychart/src/utils/findNearestDatumSingleDimension.ts#L30 + const sortedData = data.sort((firstDatum, secondDatum) => +accessors.x(firstDatum) - +accessors.x(secondDatum)) + + const seriesWithData = series + // Separate datum object by series lines + .map>(line => ({ + ...line, + // Filter select series data from the datum object and process this points array + data: getFilteredSeriesData( + sortedData.map(datum => ({ + x: accessors.x(datum), + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + y: accessors.y[line.dataKey](datum), + })) + ), + })) + + return { sortedData, seriesWithData } +} + +/** + * Filters series data list, preserves null value at the beginning of the series data list + * and removes null value between the points. + * + * ``` + * Null value ▽ Real point ■ Null value ▽ Real point ■ + * ┌────────────────────────────────────┐ ┌────────────────────────────────────┐ + * │░░░░░░░░░░░░░░░ │ │░░░░░░░░░░░░░░░ │ + * │░░░░░░░░░░░░░░░ │ │░░░░░░░░░░░░░░░ │ + * │░░░░░░░░░░░░░░░ ■ │ │░░░░░░░░░░░░░░░ ■ │ + * │░░░░░░░░░░░░▽░░ ■ │ │░░░░░░░░░░░░▽░░ ■ │ + * │░░░░░░░░░░░░░░░ ▽ │──────▶│░░░░░░░░░░░░░░░ │ + * │░░░░░░▽░░░░░░░░ ■ │ │░░░░░░▽░░░░░░░░ ■ │ + * │░░░░░░░░░░░░░░░ ■ │ │░░░░░░░░░░░░░░░ ■ │ + * │░░░▽░░░░░░░░░░░ │ │░░░▽░░░░░░░░░░░ │ + * │░░░░░░░░░░░░░░░ ▽ │ │░░░░░░░░░░░░░░░ │ + * └────────────────────────────────────┘ └────────────────────────────────────┘ + *``` + * + * @param data - Series data list + */ +function getFilteredSeriesData(data: Point[]): Point[] { + const firstNonNullablePointIndex = Math.max( + data.findIndex(datum => datum.y !== null), + 0 + ) + + // Preserve null values at the beginning of the series data list + // but remove null holes between the points further. + const nullBeginningValues = data.slice(0, firstNonNullablePointIndex) + const pointsWithoutHoles = data + // Get values after null area + .slice(firstNonNullablePointIndex) + .filter(point => point.y !== null) + + return [...nullBeginningValues, ...pointsWithoutHoles] +} diff --git a/src/components/CodeInsights/components/line-chart/helpers/get-y-axis-width.ts b/src/components/CodeInsights/components/line-chart/helpers/get-y-axis-width.ts new file mode 100644 index 00000000000..5a3b65b1e23 --- /dev/null +++ b/src/components/CodeInsights/components/line-chart/helpers/get-y-axis-width.ts @@ -0,0 +1,18 @@ +import { numberFormatter } from '../components/TickComponent' + +const APPROXIMATE_SYMBOL_WIDTH = 11 +const MINIMAL_NUMBER_OF_LABEL_SYMBOLS = 3 + +/** + * Returns width of Y label chart part based on max number of character + * in a longest Y axis label. + * + * @param ticksValues - generated Y labels (ticks) strings - 0.1, 12, 42.2k, 2M + */ +export function getYAxisWidth(ticksValues: number[]): number { + const ticksLengths = ticksValues.map(value => numberFormatter(value).split('').length) + + const maxNumberSymbolsInTicks = Math.max(...ticksLengths, MINIMAL_NUMBER_OF_LABEL_SYMBOLS) + + return maxNumberSymbolsInTicks * APPROXIMATE_SYMBOL_WIDTH +} diff --git a/src/components/CodeInsights/components/line-chart/helpers/get-y-ticks.ts b/src/components/CodeInsights/components/line-chart/helpers/get-y-ticks.ts new file mode 100644 index 00000000000..e01a5fcac56 --- /dev/null +++ b/src/components/CodeInsights/components/line-chart/helpers/get-y-ticks.ts @@ -0,0 +1,56 @@ +import { getTicks } from '@visx/scale' +import { AnyD3Scale } from '@visx/scale/lib/types/Scale' + +const HEIGHT_PER_TICK = 40 + +/** + * Returns list of not formatted (raw) Y axis ticks. + * Example: 1000, 1500, 2000, ... + * + * Number of lines (ticks) is based on chart height value and our expectation + * around label density on the chart (no more than 1 tick in each 40px, see + * HEIGHT_PER_TICK const) + */ +export function getYTicks(scale: AnyD3Scale, height: number): number[] { + // Generate max density ticks (d3 scale generation) + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const ticks: number[] = getTicks(scale) + + if (ticks.length <= 2) { + return ticks + } + + // Calculate desirable number of ticks + const numberTicks = Math.max(1, Math.floor(height / HEIGHT_PER_TICK)) + + let filteredTicks = ticks + + while (filteredTicks.length > numberTicks) { + filteredTicks = getHalvedTicks(filteredTicks) + } + + return filteredTicks +} + +/** + * Cut off half of tick elements from the list based on + * original number of ticks. With odd number of original ticks + * removes all even index ticks with even number removes all + * odd index ticks. + */ +function getHalvedTicks(ticks: number[]): number[] { + const isOriginTickLengthOdd = !(ticks.length % 2) + const filteredTicks = [] + + for (let index = ticks.length; index >= 1; index--) { + if (isOriginTickLengthOdd) { + if (index % 2 === 0) { + filteredTicks.unshift(ticks[index - 1]) + } + } else if (index % 2) { + filteredTicks.unshift(ticks[index - 1]) + } + } + + return filteredTicks +} diff --git a/src/components/CodeInsights/components/line-chart/helpers/use-event-emitters.ts b/src/components/CodeInsights/components/line-chart/helpers/use-event-emitters.ts new file mode 100644 index 00000000000..4efbb5a3755 --- /dev/null +++ b/src/components/CodeInsights/components/line-chart/helpers/use-event-emitters.ts @@ -0,0 +1,56 @@ +import { FocusEventHandler, MouseEventHandler, useCallback } from 'react' + +import { useEventEmitter } from '@visx/xychart' + +interface PointerEventEmitterParameters { + /** Source of the events, e.g., the component name. */ + source: string + onBlur?: boolean + onFocus?: boolean + onPointerMove?: boolean + onPointerOut?: boolean + onPointerUp?: boolean +} + +interface PointerEventEmitterOutput { + onPointerMove?: MouseEventHandler + onFocus?: FocusEventHandler + onBlur?: FocusEventHandler + onPointerOut?: MouseEventHandler + onPointerUp?: MouseEventHandler +} + +/** + * A hook that simplifies creation of handlers for emitting + * pointermove, pointerout, and pointerup events to EventEmitterContext. + */ +export function usePointerEventEmitters({ + source, + onPointerOut = true, + onPointerMove = true, + onPointerUp = true, + onFocus = false, + onBlur = false, +}: PointerEventEmitterParameters): PointerEventEmitterOutput { + const emit = useEventEmitter() + + const emitPointerMove = useCallback( + (event: React.PointerEvent) => emit?.('pointermove', event, source), + [emit, source] + ) + const emitPointerOut = useCallback( + (event: React.PointerEvent) => emit?.('pointerout', event, source), + [emit, source] + ) + const emitPointerUp = useCallback((event: React.PointerEvent) => emit?.('pointerup', event, source), [emit, source]) + const emitFocus = useCallback((event: React.FocusEvent) => emit?.('focus', event, source), [emit, source]) + const emitBlur = useCallback((event: React.FocusEvent) => emit?.('blur', event, source), [emit, source]) + + return { + onPointerMove: onPointerMove ? emitPointerMove : undefined, + onFocus: onFocus ? emitFocus : undefined, + onBlur: onBlur ? emitBlur : undefined, + onPointerOut: onPointerOut ? emitPointerOut : undefined, + onPointerUp: onPointerUp ? emitPointerUp : undefined, + } +} diff --git a/src/components/CodeInsights/components/line-chart/helpers/use-scales.ts b/src/components/CodeInsights/components/line-chart/helpers/use-scales.ts new file mode 100644 index 00000000000..6529c5a7f78 --- /dev/null +++ b/src/components/CodeInsights/components/line-chart/helpers/use-scales.ts @@ -0,0 +1,116 @@ +import { useMemo, useContext } from 'react' + +import { AxisScale, AxisScaleOutput } from '@visx/axis' +import { DefaultOutput, ScaleConfig, scaleLinear, scaleTime } from '@visx/scale' +import { PickScaleConfigWithoutType } from '@visx/scale/lib/types/ScaleConfig' +import { ScaleTime } from 'd3-scale' + +import { LineChartSettingsContext } from '../line-chart-settings-provider' +import { Accessors } from '../types' + +import { getMinAndMax } from './get-min-max' + +interface ScalesConfiguration { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + x: ScaleConfig + // eslint-disable-next-line @typescript-eslint/no-explicit-any + y: ScaleConfig +} + +export interface UseScalesConfiguration { + /** + * D3 scales configuration + * See https://github.com/d3/d3-scale#continuous_domain + */ + config: ScalesConfiguration + + /** + * Dataset with all series (lines) of chart + */ + data: Datum[] + + /** + * Accessors map to get (x, y) value from datum objects + */ + accessors: Accessors +} + +export function useScalesConfiguration(props: UseScalesConfiguration): ScalesConfiguration { + const { config, data, accessors } = props + const { zeroYAxisMin } = useContext(LineChartSettingsContext) + + // Extend origin config with calculated domain with vertical padding + return useMemo(() => { + let [min, max] = getMinAndMax(data, accessors) + + if (zeroYAxisMin) { + min = 0 + } + + // Generate pseudo domain if all values of dataset are equal + ;[min, max] = min === max ? [max - max / 2, max + max / 2] : [min, max] + + return { + ...config, + y: { + ...config.y, + domain: [min, max], + }, + } + }, [data, accessors, zeroYAxisMin, config]) +} + +interface UseYScalesProps { + /** + * D3 scales configuration + * See https://github.com/d3/d3-scale#continuous_domain + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + config: ScaleConfig + height: number +} + +export function useYScale(props: UseYScalesProps): AxisScale { + const { config, height } = props + + return useMemo( + () => + scaleLinear({ + ...(config as PickScaleConfigWithoutType<'linear', DefaultOutput>), + range: [height, 0], + }), + [config, height] + ) +} + +interface UseXScalesProps { + /** + * D3 scales configuration + * See https://github.com/d3/d3-scale#continuous_domain + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + config: ScaleConfig + + /** Accessors map to get value from datum object */ + accessors: Accessors + + /** Chart width in px */ + width: number + + /** Dataset with all series (lines) of chart */ + data: Datum[] +} + +export function useXScale(props: UseXScalesProps): ScaleTime { + const { config, accessors, data, width } = props + + return useMemo( + () => + scaleTime({ + ...config, + range: [0, width], + domain: [accessors.x(data[0]), accessors.x(data[data.length - 1])], + }), + [config, accessors, data, width] + ) +} diff --git a/src/components/CodeInsights/components/line-chart/line-chart-settings-provider.ts b/src/components/CodeInsights/components/line-chart/line-chart-settings-provider.ts new file mode 100644 index 00000000000..0e7ca4555d2 --- /dev/null +++ b/src/components/CodeInsights/components/line-chart/line-chart-settings-provider.ts @@ -0,0 +1,49 @@ +import { createContext } from 'react' + +export enum LineChartLayoutOrientation { + Vertical = 'vertical', + Horizontal = 'horizontal', +} + +interface LineChartSettingsContext { + /** + * Enables Y-label generation from 0 value. + * + * ``` + * With zeroYAxisMin: false With zeroYAxisMin: true + * ▲ ▲ + * 40│ ● ● 45│ + * │ ● ● │ ● ● ● + * 30│ ● ● ● 30│ ● ● ● + * │ ● ● │ ● ● + * 20│ ● 15│ ● + * │ │ + * 10└─────────────────▶ 0 └─────────────────▶ + *``` + */ + zeroYAxisMin: boolean + + /** + * Controls chart legend layout position. If it's property isn't specified + * line chart uses its internal logic about putting legend block which is based + * on chart width and number of lines (series). + * + * ``` + * Vertical (default) Horizontal + * ▲ ▲ + * │ ● ● │ ● ● Item 1 + * │ ● ● │ ● ● ● Item 2 + * │ ● ● ● │ ● ● ● + * │ ● ● │ ● ● + * │ ● │ ● + * │ │ + * └─────────────────▶ └─────────────▶ + * ● Item 1 ● Item 2 + * ``` + */ + layout?: LineChartLayoutOrientation +} + +export const LineChartSettingsContext = createContext({ + zeroYAxisMin: false, +}) diff --git a/src/components/CodeInsights/components/line-chart/types.ts b/src/components/CodeInsights/components/line-chart/types.ts new file mode 100644 index 00000000000..85e9f8843dd --- /dev/null +++ b/src/components/CodeInsights/components/line-chart/types.ts @@ -0,0 +1,66 @@ +import { MouseEvent } from 'react' + +export interface LineChartContent { + chart: 'line' + + /** An array of data objects, with one element for each step on the X axis. */ + data: D[] + + /** The series (lines) of the chart. */ + series: LineChartSeries[] + + xAxis: ChartAxis +} + +export interface LineChartSeries { + /** The key in each data object for the values this line should be calculated from. */ + dataKey: keyof D + + /** The name of the line shown in the legend and tooltip. */ + name?: string + + /** + * The link URLs for each data point. + * A link URL should take the user to more details about the specific data point. + */ + linkURLs?: Record | string[] + + /** The CSS color of the line. */ + stroke?: string +} + +export interface ChartAxis { + /** The key in the data object. */ + dataKey: K + + /** The scale of the axis. */ + scale?: 'time' | 'linear' + + /** The type of the data key. */ + type: 'number' | 'category' +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type YAccessor = (data: Datum) => any + +export interface LineChartSeriesWithData extends LineChartSeries { + data: Point[] +} + +export interface Point { + x: Date | number + y: number | null +} + +/** Accessors map for getting values for x and y axes from datum object */ +export interface Accessors { + x: (d: Datum) => Date | number + y: Record> +} + +export interface DatumZoneClickEvent { + originEvent: MouseEvent + link?: string +} + +export type onDatumZoneClick = (event: DatumZoneClickEvent) => void diff --git a/src/components/CodeInsights/components/view/View.module.scss b/src/components/CodeInsights/components/view/View.module.scss new file mode 100644 index 00000000000..2a90ce4ca9c --- /dev/null +++ b/src/components/CodeInsights/components/view/View.module.scss @@ -0,0 +1,50 @@ +.view { + display: flex; + flex-direction: column; + padding: 1rem 1rem; + cursor: default; + outline: none; + + &:focus, + &:focus-within { + box-shadow: var(--focus-box-shadow); + } +} + +.header { + margin-bottom: 0.75rem; +} + +.headerContent { + display: flex; +} + +.title { + // Truncation for multiple lines + // stylelint-disable-next-line value-no-vendor-prefix + display: -webkit-box; + overflow: hidden; + text-overflow: ellipsis; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + margin-bottom: 0.25rem; + flex-grow: 1; + font-size: 1rem; + font-weight: 600; + letter-spacing: 0.0178571429em; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, + 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; +} + +.action { + align-self: start; + display: flex; + align-items: center; + // Move icons in action panel visually closer to card border + margin-right: -0.5rem; + margin-top: -0.25rem; +} + +.subtitle { + flex-basis: 100%; +} diff --git a/src/components/CodeInsights/components/view/View.tsx b/src/components/CodeInsights/components/view/View.tsx new file mode 100644 index 00000000000..0b7599defdf --- /dev/null +++ b/src/components/CodeInsights/components/view/View.tsx @@ -0,0 +1,49 @@ +import React, { PropsWithChildren, ReactNode } from 'react' + +import classNames from 'classnames' + +import { Card } from '../card/Card' + +import styles from './View.module.scss' + +type ViewCardElementProps = React.DetailedHTMLProps< + Omit, 'contextMenu' | 'title'>, + HTMLElement +> + +export interface ViewCardProps extends ViewCardElementProps { + title?: ReactNode + subtitle?: ReactNode + + /** + * Custom card actions (like filter buttons) that render element right next to three dots + * card context menu. + */ + actions?: ReactNode +} + +export const View: React.FunctionComponent> = props => { + const { title, subtitle, actions, children, ...otherProps } = props + + // In case if we don't have a content for the header component + // we should render nothing + const hasHeader = title || subtitle || actions + + return ( + + {hasHeader && ( +
    +
    +

    {title}

    + +
    {actions}
    +
    + +
    {subtitle}
    +
    + )} + + {children} +
    + ) +} diff --git a/src/components/CodeInsights/constants.ts b/src/components/CodeInsights/constants.ts new file mode 100644 index 00000000000..d265a4897e8 --- /dev/null +++ b/src/components/CodeInsights/constants.ts @@ -0,0 +1,14 @@ +export const DATA_SERIES_COLORS = { + RED: '#f03e3e', + PINK: '#d6336c', + GRAPE: '#ae3ec9', + VIOLET: '#7048e8', + INDIGO: '#4263eb', + BLUE: '#1c7ed6', + CYAN: '#1098ad', + TEAL: '#0ca678', + GREEN: '#37b24d', + LIME: '#74b816', + YELLOW: '#f59f00', + ORANGE: '#f76707', +} diff --git a/src/components/CodeInsights/mock-data.tsx b/src/components/CodeInsights/mock-data.tsx new file mode 100644 index 00000000000..5eca8a7f5a7 --- /dev/null +++ b/src/components/CodeInsights/mock-data.tsx @@ -0,0 +1,289 @@ +import React from 'react' + +import { DATA_SERIES_COLORS } from './constants' +import { CaptureGroupInsightData, SearchInsightData } from './types' + +import styles from './CodeInsightsExamples.module.scss' + +export const LOG_4_J_INCIDENT_INSIGHT: SearchInsightData = { + title: 'Log4j incident response', + repositories: ( + <> + repo:https://github.com/wildcard-org/wc-repo + + ), + data: [ + { x: new Date('August 1, 2021'), a: 0, b: 510 }, + { x: new Date('September 1, 2021'), a: 2, b: 440 }, + { x: new Date('October 1, 2021'), a: 35, b: 445 }, + { x: new Date('November 1, 2021'), a: 120, b: 460 }, + { x: new Date('December 1, 2021'), a: 100, b: 430 }, + { x: new Date('January 1, 2022'), a: 120, b: 410 }, + { x: new Date('February 1, 2022'), a: 1500, b: 200 }, + ], + series: [ + { + dataKey: 'a', + name: 'Updated log4j', + stroke: DATA_SERIES_COLORS.GREEN, + query: ( + <> + lang:gradle org\.apache\.logging\.log4j[\'"] + 2\.(17)(\.[0-9]+) patterntype:regexp{' '} + archived:no fork:no + + ), + }, + { + dataKey: 'b', + name: 'Vulnerable log4j', + stroke: DATA_SERIES_COLORS.RED, + query: ( + <> + lang:gradle org\.apache\.logging\.log4j[\'"] + 2\.(0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16)(\.[0-9]+){' '} + patterntype:regexp{' '} + archived:no fork:no + + ), + }, + ], + xAxis: { + dataKey: 'x', + scale: 'time', + type: 'number', + }, +} + +export const SEARCH_INSIGHT_CSS_MODULES_EXAMPLES_DATA: SearchInsightData = { + title: 'Migration to CSS modules', + repositories: ( + <> + repo:https://github.com/wildcard-org/wc-repo + + ), + data: [ + { x: new Date('May 7, 2021'), a: 88, b: 410 }, + { x: new Date('June 7, 2021'), a: 95, b: 410 }, + { x: new Date('July 7, 2021'), a: 110, b: 315 }, + { x: new Date('August 7, 2021'), a: 160, b: 180 }, + { x: new Date('September 7, 2021'), a: 310, b: 90 }, + { x: new Date('October 7, 2021'), a: 520, b: 45 }, + { x: new Date('November 7, 2021'), a: 700, b: 10 }, + ], + series: [ + { + dataKey: 'a', + name: 'CSS Modules', + stroke: DATA_SERIES_COLORS.GREEN, + query: ( + <> + type:file lang:scss{' '} + file:module.scss{' '} + patterntype:regexp{' '} + archived:no fork:no + + ), + }, + { + dataKey: 'b', + name: 'Global CSS', + stroke: DATA_SERIES_COLORS.RED, + query: ( + <> + type:file lang:scss{' '} + -file:module.scss{' '} + patterntype:regexp{' '} + archived:no fork:no + + ), + }, + ], + xAxis: { + dataKey: 'x', + scale: 'time', + type: 'number', + }, +} + +export const ALPINE_VERSIONS_INSIGHT: CaptureGroupInsightData = { + title: 'Alpine versions over all repos', + repositories: 'All repositories', + query: ( + <> + patterntype:regexp FROM\s+alpine:([\d\.]+){' '} + file:Dockerfile + + ), + data: [ + { x: new Date('May 7, 2021'), a: 100, b: 160, c: 90, d: 75, e: 85, f: 20, g: 150 }, + { x: new Date('June 7, 2021'), a: 90, b: 155, c: 95, d: 85, e: 80, f: 25, g: 155 }, + { x: new Date('July 7, 2021'), a: 85, b: 150, c: 110, d: 90, e: 60, f: 40, g: 165 }, + { x: new Date('August 7, 2021'), a: 85, b: 150, c: 125, d: 80, e: 50, f: 50, g: 165 }, + { x: new Date('September 7, 2021'), a: 70, b: 155, c: 125, d: 75, e: 45, f: 55, g: 160 }, + { x: new Date('October 7, 2021'), a: 50, b: 150, c: 145, d: 70, e: 35, f: 60, g: 155 }, + { x: new Date('November 7, 2021'), a: 35, b: 160, c: 175, d: 75, e: 45, f: 65, g: 145 }, + ], + series: [ + { + dataKey: 'a', + name: '3.1', + stroke: DATA_SERIES_COLORS.INDIGO, + }, + { + dataKey: 'b', + name: '3.5', + stroke: DATA_SERIES_COLORS.RED, + }, + { + dataKey: 'c', + name: '3.15', + stroke: DATA_SERIES_COLORS.GREEN, + }, + { + dataKey: 'd', + name: '3.8', + stroke: DATA_SERIES_COLORS.GRAPE, + }, + { + dataKey: 'e', + name: '3.9', + stroke: DATA_SERIES_COLORS.ORANGE, + }, + { + dataKey: 'f', + name: '3.9.2', + stroke: DATA_SERIES_COLORS.TEAL, + }, + { + dataKey: 'g', + name: '3.14', + stroke: DATA_SERIES_COLORS.PINK, + }, + ], + xAxis: { + dataKey: 'x', + scale: 'time' as const, + type: 'number', + }, +} + +export const DEPRECATED_API_USAGE_BY_TEAM: SearchInsightData = { + title: 'Deprecated API usage by team', + repositories: 'All repositories', + data: [ + { x: new Date('August 1, 2021'), a: 165, b: 125, c: 50 }, + { x: new Date('September 1, 2021'), a: 180, b: 80, c: 70 }, + { x: new Date('October 1, 2021'), a: 125, b: 50, c: 75 }, + { x: new Date('November 1, 2021'), a: 80, b: 70, c: 60 }, + { x: new Date('December 1, 2021'), a: 120, b: 20, c: 55 }, + { x: new Date('January 1, 2022'), a: 140, b: 10, c: 55 }, + { x: new Date('February 1, 2022'), a: 100, b: 10, c: 45 }, + ], + series: [ + { + dataKey: 'a', + name: 'Cloud', + stroke: DATA_SERIES_COLORS.ORANGE, + query: ( + <> + problemAPI file:CloudDirectory{' '} + archived:no fork:no + + ), + }, + { + dataKey: 'b', + name: 'Core App', + stroke: DATA_SERIES_COLORS.CYAN, + query: ( + <> + problemAPI file:CoreDirectory{' '} + archived:no fork:no + + ), + }, + { + dataKey: 'c', + name: 'Extensibility', + stroke: DATA_SERIES_COLORS.GRAPE, + query: ( + <> + problemAPI file:ExtnDirectory{' '} + archived:no fork:no + + ), + }, + ], + xAxis: { + dataKey: 'x', + scale: 'time', + type: 'number', + }, +} + +export const LINTER_OVERRIDES: SearchInsightData = { + title: 'Linter override rules in all repos', + repositories: 'All repositories', + data: [ + { x: new Date('August 1, 2021'), a: 6800 }, + { x: new Date('September 1, 2021'), a: 12000 }, + { x: new Date('October 1, 2021'), a: 3200 }, + { x: new Date('November 1, 2021'), a: 3600 }, + { x: new Date('December 1, 2021'), a: 3000 }, + { x: new Date('January 1, 2022'), a: 3100 }, + { x: new Date('February 1, 2022'), a: 14500 }, + ], + series: [ + { + dataKey: 'a', + name: 'Linter overrides', + stroke: DATA_SERIES_COLORS.RED, + query: ( + <> + file:\.eslintignore .\n{' '} + patternType:regexp{' '} + archived:no fork:no + + ), + }, + ], + xAxis: { + dataKey: 'x', + scale: 'time', + type: 'number', + }, +} + +export const REPOS_WITH_CI_SYSTEM: SearchInsightData = { + title: '# of repos connected to the CI system', + repositories: 'All repositories', + data: [ + { x: new Date('August 1, 2021'), a: 60 }, + { x: new Date('September 1, 2021'), a: 60 }, + { x: new Date('October 1, 2021'), a: 120 }, + { x: new Date('November 1, 2021'), a: 80 }, + { x: new Date('December 1, 2021'), a: 200 }, + { x: new Date('January 1, 2022'), a: 325 }, + { x: new Date('February 1, 2022'), a: 480 }, + ], + series: [ + { + dataKey: 'a', + name: 'Connected repos', + stroke: DATA_SERIES_COLORS.GREEN, + query: ( + <> + file:\.circleci/config.yml{' '} + select:repo{' '} + archived:no fork:no + + ), + }, + ], + xAxis: { + dataKey: 'x', + scale: 'time', + type: 'number', + }, +} diff --git a/src/components/CodeInsights/types.ts b/src/components/CodeInsights/types.ts new file mode 100644 index 00000000000..8b71ef486ee --- /dev/null +++ b/src/components/CodeInsights/types.ts @@ -0,0 +1,32 @@ +import { ReactNode } from 'react' + +import { + LineChartContent as LineChartContentType, + LineChartContent, + LineChartSeries, +} from './components/line-chart/types' + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export interface SeriesWithQuery extends LineChartSeries { + query: ReactNode + name: string +} + +export enum CodeInsightExampleType { + Search, + Capture, +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export interface SearchInsightData extends Omit, 'chart' | 'series'> { + title: ReactNode + repositories: ReactNode + series: SeriesWithQuery[] +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export interface CaptureGroupInsightData extends Omit, 'chart'> { + title: ReactNode + repositories: ReactNode + query: ReactNode +} diff --git a/src/components/TabCarousel.tsx b/src/components/TabCarousel.tsx new file mode 100644 index 00000000000..d745cb58d15 --- /dev/null +++ b/src/components/TabCarousel.tsx @@ -0,0 +1,64 @@ +import { FunctionComponent, ReactNode } from 'react' + +import ArrowLeftIcon from 'mdi-react/ArrowLeftIcon' +import ArrowRightIcon from 'mdi-react/ArrowRightIcon' +import CircleSmallIcon from 'mdi-react/CircleSmallIcon' + +import { useCarousel } from '@hooks' + +import { TemplateCodeBlock } from './TemplateCodeBlock' + +interface TabCarouselProps { + items: Template[] + currentItem?: ReactNode + previousItem?: ReactNode + currentItemIndex?: number + autoAdvance?: boolean +} + +interface Template { + header: string + description: string + queries: ReactNode[] +} + +export const TabCarousel: FunctionComponent = ({ items, autoAdvance }) => { + const carouselHook = useCarousel(items, autoAdvance ?? false) + const carouselItems = carouselHook.carouselItems.items as Template[] + + return ( +
    +
    + {carouselItems.map(item => ( +
    + +
    + ))} +
    + +
    + carouselHook.moveCarousel('decrement')} + color={carouselHook.isAdvancing ? '#D0D0D0' : '#000'} + /> +
    + {carouselItems.map(item => ( + + ))} +
    + carouselHook.moveCarousel()} + color={carouselHook.isAdvancing ? '#000' : '#D0D0D0'} + /> +
    +
    + ) +} diff --git a/src/components/TemplateCodeBlock.tsx b/src/components/TemplateCodeBlock.tsx new file mode 100644 index 00000000000..d7ae3cc76cb --- /dev/null +++ b/src/components/TemplateCodeBlock.tsx @@ -0,0 +1,25 @@ +import { FunctionComponent, ReactNode } from 'react' + +interface Props { + template: Template +} + +interface Template { + header: string + description: string + queries: ReactNode[] +} + +export const TemplateCodeBlock: FunctionComponent = ({ template }) => ( +
    +
    +
    {template.header}
    +

    {template.description}

    + {template.queries.map(query => ( +
    + {query} +
    + ))} +
    +
    +) diff --git a/src/components/index.ts b/src/components/index.ts index 178347fd954..1fa52fc0b13 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -9,7 +9,7 @@ export { EmbeddedHubSpot } from './HubSpot' export { BackButton, BackButtonBold, BackButtonLight } from './BackButton' export { FormLegal } from './FormLegal' export { BlogListItem } from './BlogListItem' -export { Blockquote, BlockquoteWithBorder, BlockquoteWithLogoTop, BlockquoteWithLogoBottom } from './Blockquote' +export { BlockquoteWithBorder, BlockquoteWithLogoTop, BlockquoteWithLogoBottom } from './Blockquote' export { Video } from './Video' export { CustomerLogosSectionAnimated } from './CustomerLogosSectionAnimated' export { CustomerLogosSection } from './CustomerLogosSection' @@ -18,6 +18,8 @@ export { BoardSection } from './BoardSection' export { LeadershipSection } from './LeadershipSection' export { SelfHostedSection } from './SelfHostedSection' export { YouTube } from './YouTube' +export { TabCarousel } from './TabCarousel' +export { TemplateCodeBlock } from './TemplateCodeBlock' // Tracking export { buttonStyle, buttonLocation } from './data/tracking' diff --git a/src/pages/case-studies/nutanix-fixed-log4j-with-sourcegraph.tsx b/src/pages/case-studies/nutanix-fixed-log4j-with-sourcegraph.tsx index e1faea0dd5e..984b16a3afa 100644 --- a/src/pages/case-studies/nutanix-fixed-log4j-with-sourcegraph.tsx +++ b/src/pages/case-studies/nutanix-fixed-log4j-with-sourcegraph.tsx @@ -7,7 +7,7 @@ import { Layout, NewCaseStudyLayout, ContentSection, - Blockquote, + BlockquoteWithLogoBottom, ThreeUpText, UseChallengeSolutionResults, } from '@components' @@ -146,7 +146,7 @@ export const CaseStudy: FunctionComponent = () => ( -
    + ), + headerClass: 'active', + itemClass: 'd-block', + }, + { + backgroundClass: '', + buttonLabel: 'Detect and track versions of languages or packages', + text: , + headerClass: '', + itemClass: 'd-none', + }, + { + backgroundClass: '', + buttonLabel: 'Ensure removal of security vulnerabilities', + text: , + headerClass: '', + itemClass: 'd-none', + }, + { + backgroundClass: '', + buttonLabel: 'Understand code by team', + text: , + headerClass: '', + itemClass: 'd-none', + }, + { + backgroundClass: '', + buttonLabel: 'Track code smells and health', + text: , + headerClass: '', + itemClass: 'd-none', + }, + { + backgroundClass: '', + buttonLabel: 'Visualize configurations and services', + text: , + headerClass: '', + itemClass: 'd-none', + }, +] + +const templates = { + migrations: [ + { + header: 'Global CSS to CSS modules', + description: 'Track migration from global CSS to CSS modules.', + queries: [ + <> + select:file lang:SCSS{' '} + file:module patterntype:regexp{' '} + archived:no fork:no + , + <> + type:file lang:scss + -file:module patterntype:regexp{' '} + archived:no fork:no + , + ], + }, + { + header: 'Python 2 to Python 3', + description: 'How far along is the Python major version migration?', + queries: [ + <> + #!/usr/bin/env python3 archived:no{' '} + fork:no + , + <> + #!/usr/bin/env python2 archived:no{' '} + fork:no + , + ], + }, + { + header: 'React Class to Function Components', + description: "What's the status of migrating to React function components from class components?", + queries: [ + <> + patterntype:regexp const\s\w+:\s(React\.)?FunctionComponent{' '} + archived:no fork:no + , + <> + patterntype:regexp extends\s(React\.)?(Pure)?Component{' '} + archived:no fork:no + , + ], + }, + { + header: 'Config or docs file', + description: 'How many repos contain a config or docs file in a specific directory?', + queries: [ + <> + select:repo file: + docs/*/new_config_filename archived:no{' '} + fork:no + , + ], + }, + ], + versionTracking: [ + { + header: 'Java versions', + description: 'Detect and track which Java versions are present or most popular in your code base.', + queries: [ + <> + {'(.*)'} archived:no{' '} + fork:no + , + ], + }, + { + header: 'All log4j versions', + description: 'Which log4j versions are present, including vulnerable versions?', + queries: [ + <> + lang:gradle org\.apache\.logging\.log4j['"] 2\.([0-9]+)\.{' '} + archived:no fork:no + , + ], + }, + { + header: 'License types in the codebase', + description: 'See the breakdown of licenses from package.json files.', + queries: [ + <> + file:package.json "license":\s"(.*)"{' '} + archived:no fork:no + , + ], + }, + { + header: 'Python versions', + description: "Which python versions are in use or haven't been updated?", + queries: [ + <> + #!/usr/bin/env python([0-9]\.[0-9]+) archived:no{' '} + fork:no + , + ], + }, + ], + security: [ + { + header: 'Vulnerable open source library', + description: + 'Confirm that a vulnerable open source library has been fully removed, or the speed of the deprecation.', + queries: [ + <> + vulnerableLibrary@14.3.9 archived:no{' '} + fork:no + , + ], + }, + { + header: 'How many tests are skipped', + description: 'See how many tests have skip conditions.', + queries: [ + <> + this.skip() patterntype:literal{' '} + archived:no fork:no + , + ], + }, + { + header: 'Vulnerable log4j versions', + description: 'What vulnerable log4j versions are present?', + queries: [ + <> + lang:gradle org\.apache\.logging\.log4j['"] + 2\.(0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16)(\.[0-9]+){' '} + patterntype:regexp archived:no{' '} + fork:no + , + ], + }, + { + header: 'API keys', + description: 'How quickly do we notice and remove API keys when they are committed?', + queries: [ + <> + regexMatchingAPIKey patterntype:regexp{' '} + archived:no fork:no + , + ], + }, + ], + codeHealth: [ + { + header: 'TODOs', + description: 'How many TODOs are in a specific part of the codebase (or all of it)?', + queries: [ + <> + TODO archived:no fork:no + , + ], + }, + { + header: 'Commits with "revert"', + description: 'How frequently are there commits with “revert” in the commit message?', + queries: [ + <> + type:commit revert archived:no{' '} + fork:no + , + ], + }, + { + header: 'Linter override rules', + description: 'How many linter override rules exist?', + queries: [ + <> + file:\.eslintignore .\n{' '} + patterntype:regexp archived:no{' '} + fork:no + , + ], + }, + { + header: 'Deprecated calls', + description: 'How many times are deprecated calls used?', + queries: [ + <> + lang:java @deprecated archived:no{' '} + fork:no + , + ], + }, + ], +} + +const blogListItems = [ + { + title: 'How we migrated entirely to CSS Modules using codemods and Sourcegraph Code Insights', + description: + 'How our Frontend Platform team used codemods to automate a challenging global migration to CSS modules, and Code Insights to track and communicate progress.', + type: 'Blog post', + image: 'https://storage.googleapis.com/sourcegraph-assets/blog/code-insights-ga-blogs/migrating-to-css-modules.png', + href: '/blog/migrating-to-css-modules-with-codemods-and-code-insights', + }, + { + title: 'Announcing Code Insights: analytics for engineering teams to understand and visualize their codebase over time', + description: 'Learn about why we built Code Insights from our CEO.', + type: 'Blog post', + image: 'https://storage.googleapis.com/sourcegraph-assets/blog/code-insights-ga-blogs/announcement-header.png', + href: '/blog/announcing-code-insights', + }, + { + title: 'Dive into documentation', + description: 'Learn everything you need to know about Code Insights.', + type: 'Docs', + image: 'https://storage.googleapis.com/sourcegraph-assets/blog/code-insights-ga-blogs/code-insights-docs.png', + href: 'https://docs.sourcegraph.com/code_insights', + }, +] + +export const CodeInsightsPage: FunctionComponent = () => ( + +
    +
    +
    +
    +
    Code Insights
    +

    + Track what really matters to you and your team. +

    +

    + Transform your code into a queryable database to create customizable, visual dashboards + in seconds. +

    +
    + + {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} + + Request a demo + + +
    +
    +
    +
    + + } + > + +
    +
    +
    +
    +

    + Finally, useful engineering metrics{' '} + + you + {' '} + define. +

    +

    + Forget about inaccurate spreadsheets, manual processes, and missing historical data. You can{' '} + track everything in your codebase, from migrations to code smells, in a + seamless and precise way. Make data-driven decisions using visualizations based on the power and + accuracy of Sourcegraph Code Search. +

    +
    +
    +
    + + +
    + +
    +
    + +
    + +
    +
    +

    Trusted by engineering teams worldwide

    +
    +
    +

    + “As we've grown, so has the need to better track and communicate our progress and + goals across the engineering team and broader company. With Code Insights, our data and + migration tracking is accurate across our entire codebase, and our engineers and managers + can shift out of manual spreadsheets and spend more time working on code.” +

    +
    + — Balázs Tóthfalussy, Engineering Manager, Prezi +
    +
    +
    + + Prezi + +
    +
    +
    +
    + +
    + +
    +

    + Engineering leadership with superpowers +

    +

    + Code Insights provides reliable real-time reporting directly from the codebase, making + engineering leaders and their teams more effective. +

    +
    +
    +
    + +
    +
    +
    Set goals
    +

    Measure goals and progress in your codebase

    +
    +
    +
    +
    + +
    +
    +
    Plan proactively
    +

    + Stay on top of engineering initiatives and catch issues before they escalate +

    +
    +
    +
    +
    +
    +
    + +
    +
    +
    Track ownership & trends
    +

    Tie trends and metrics to owners on the teams

    +
    +
    +
    +
    + +
    +
    +
    Celebrate progress
    +

    Visualize the momentum and motivate your teammates

    +
    +
    +
    +
    +
    +
    + + {/* Demo */} + +

    See Code Insights in action

    +
    +
    + +
    +
    +
    + + {/* Use Cases */} +
    + +

    Popular Code Insights templates

    + + +
    +
    + {templates.migrations.map(template => ( + + ))} +
    +
    +
    + +
    +
    + {templates.versionTracking.map(template => ( + + ))} +
    +
    +
    + +
    +
    + {templates.security.map(template => ( + + ))} +
    +
    +
    + +
    +
    + {templates.codeHealth.map(template => ( + + ))} +
    +
    +
    +
    + +
    +
    + + {/* Use Cases Carousel */} +
    + +

    Popular Code Insights templates

    + + +
    +
    + +
    +
    +
    + +
    +
    + +
    +
    +
    + +
    +
    + +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    + +
    +
    +

    Get started with Code Insights

    +

    + Create a code insight in 60 seconds and get historical data for previously untracked metrics + — data backfills automatically. +

    +
    +
    + + {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} + + Request a demo + + +
    +
    +
    +
    + + +
    +
    +

    Learn more

    +
    + {blogListItems.map(item => ( + + ))} +
    +
    + +) + +export default CodeInsightsPage diff --git a/src/styles/pages/_code-insights.scss b/src/styles/pages/_code-insights.scss index 769066801e3..bd6e24da62b 100644 --- a/src/styles/pages/_code-insights.scss +++ b/src/styles/pages/_code-insights.scss @@ -90,27 +90,8 @@ text-decoration: none; } - .tab-section { - @media only screen and (max-width: 768px) { - display: none; - } - } - - .tab-carousel-section { - @media only screen and (min-width: 768px) { - display: none; - } - } - .tab-carousel { transition: background-color 0.5s ease; - - .d-block, - .d-none { - @media only screen and (max-width: 768px) { - height: 355px; - } - } } .icon-subheader { @@ -150,13 +131,13 @@ font-weight: 600; display: block; - @media only screen and (max-width: 768px) { + @media only screen and (max-width: 992px) { margin: 20px 20px 0 20px; height: 150px; } } - @media only screen and (max-width: 768px) { + @media only screen and (max-width: 992px) { padding: 0; margin: 20px 0; display: none; diff --git a/tsconfig.json b/tsconfig.json index 36586f74fc0..481ce9637ce 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,6 +22,7 @@ "@lib": ["lib"], "@util": ["util"], "@components": ["components"], + "@code-insights/*": ["components/CodeInsights/*"], "@styles/*": ["styles/*"] } }, diff --git a/yarn.lock b/yarn.lock index 2f01aec4e5d..7a459009a05 100644 --- a/yarn.lock +++ b/yarn.lock @@ -300,6 +300,11 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@juggle/resize-observer@^3.3.1": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.3.1.tgz#b50a781709c81e10701004214340f25475a171a0" + integrity sha512-zMM9Ds+SawiUkakS7y94Ymqx+S0ORzpG3frZirN3l+UlXUmSUR7hF4wxCVqW+ei94JzV5kt0uXBcoOEAuiydrw== + "@mdx-js/mdx@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@mdx-js/mdx/-/mdx-2.0.0.tgz#7f270df1e77c46f8338fc32089016b3ea383d023" @@ -636,6 +641,57 @@ resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.2.tgz#ed4e0ad92306a704f9fb132a0cfcf77486dbe2bc" integrity sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig== +"@types/d3-color@^1": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-1.4.2.tgz#944f281d04a0f06e134ea96adbb68303515b2784" + integrity sha512-fYtiVLBYy7VQX+Kx7wU/uOIkGQn8aAEY8oWMoyja3N4dLd8Yf6XgSIR/4yWvMuveNOH5VShnqCgRqqh/UNanBA== + +"@types/d3-format@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-3.0.1.tgz#194f1317a499edd7e58766f96735bdc0216bb89d" + integrity sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg== + +"@types/d3-interpolate@^1.3.1": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-1.4.2.tgz#88902a205f682773a517612299a44699285eed7b" + integrity sha512-ylycts6llFf8yAEs1tXzx2loxxzDZHseuhPokrqKprTQSTcD3JbJI1omZP1rphsELZO3Q+of3ff0ZS7+O6yVzg== + dependencies: + "@types/d3-color" "^1" + +"@types/d3-path@^1", "@types/d3-path@^1.0.8": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-1.0.9.tgz#73526b150d14cd96e701597cbf346cfd1fd4a58c" + integrity sha512-NaIeSIBiFgSC6IGUBjZWcscUJEq7vpVu7KthHN8eieTV9d9MqkSOZLH4chq1PmcKy06PNe3axLeKmRIyxJ+PZQ== + +"@types/d3-scale@^3.3.0": + version "3.3.2" + resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-3.3.2.tgz#18c94e90f4f1c6b1ee14a70f14bfca2bd1c61d06" + integrity sha512-gGqr7x1ost9px3FvIfUMi5XA/F/yAf4UkUDtdQhpH92XCT0Oa7zkkRzY61gPVJq+DxpHn/btouw5ohWkbBsCzQ== + dependencies: + "@types/d3-time" "^2" + +"@types/d3-shape@^1.3.1": + version "1.3.8" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-1.3.8.tgz#c3c15ec7436b4ce24e38de517586850f1fea8e89" + integrity sha512-gqfnMz6Fd5H6GOLYixOZP/xlrMtJms9BaS+6oWxTKHNqPGZ93BkWWupQSCYm6YHqx6h9wjRupuJb90bun6ZaYg== + dependencies: + "@types/d3-path" "^1" + +"@types/d3-time-format@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-4.0.0.tgz#ee7b6e798f8deb2d9640675f8811d0253aaa1946" + integrity sha512-yjfBUe6DJBsDin2BMIulhSHmr5qNR5Pxs17+oW4DoVPyVIXZ+m6bs7j1UVKP08Emv6jRmYrYqxYzO63mQxy1rw== + +"@types/d3-time@^2", "@types/d3-time@^2.0.0": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-2.1.1.tgz#743fdc821c81f86537cbfece07093ac39b4bc342" + integrity sha512-9MVYlmIgmRR31C5b4FVSWtuMmBHh2mOWQYfl7XAYOa8dsnb7iEmUmRSWSFgXFtkjxO65d7hTUHQC+RhR/9IWFg== + +"@types/d3-voronoi@^1.1.9": + version "1.1.9" + resolved "https://registry.yarnpkg.com/@types/d3-voronoi/-/d3-voronoi-1.1.9.tgz#7bbc210818a3a5c5e0bafb051420df206617c9e5" + integrity sha512-DExNQkaHd1F3dFPvGA/Aw2NGyjMln6E9QzsiqOcBgnE+VInYnFBHBBySbZQts6z6xD+5jTfKCP7M4OqMyVjdwQ== + "@types/debug@^4.0.0": version "4.1.7" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82" @@ -711,6 +767,11 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= +"@types/lodash@^4.14.172": + version "4.14.181" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.181.tgz#d1d3740c379fda17ab175165ba04e2d03389385d" + integrity sha512-n3tyKthHJbkiWhDZs3DkhkCzt2MexYHXlX0td5iMplyfwketaOeKboEVBqzceH7juqvEg3q5oUoBFxSLu7zFag== + "@types/lodash@^4.14.180": version "4.14.180" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.180.tgz#4ab7c9ddfc92ec4a887886483bc14c79fb380670" @@ -758,6 +819,13 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== +"@types/react-dom@*": + version "18.0.0" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.0.tgz#b13f8d098e4b0c45df4f1ed123833143b0c71141" + integrity sha512-49897Y0UiCGmxZqpC8Blrf6meL8QUla6eb+BBhn69dTXlmuOlzkfr7HHY/O8J25e1lTUMs+YYxSlVDAaGHCOLg== + dependencies: + "@types/react" "*" + "@types/react-transition-group@^4.4.1": version "4.4.4" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.4.tgz#acd4cceaa2be6b757db61ed7b432e103242d163e" @@ -931,6 +999,225 @@ "@typescript-eslint/types" "5.13.0" eslint-visitor-keys "^3.0.0" +"@visx/annotation@2.9.0": + version "2.9.0" + resolved "https://registry.yarnpkg.com/@visx/annotation/-/annotation-2.9.0.tgz#99c216a3ef9eab8b2d19f38e34d4ab6c9f1e93b0" + integrity sha512-bZiV1I6sDixRP4uPTtKR7fui52GhmkoZzIXGMhQwXtkwrCu/UkMYNvYeRbFYjkMNsjjVD7ULcIteeXcamIOrpw== + dependencies: + "@types/react" "*" + "@visx/drag" "2.6.0" + "@visx/group" "2.1.0" + "@visx/point" "2.6.0" + "@visx/shape" "2.4.0" + "@visx/text" "2.3.0" + classnames "^2.3.1" + prop-types "^15.5.10" + react-use-measure "^2.0.4" + +"@visx/axis@2.6.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@visx/axis/-/axis-2.6.0.tgz#33c7713590818705ae4a10bc6ab6d589bdb9ce9c" + integrity sha512-Ti8AxclK4m/2SHQPGMMsvotTxZGaLQIMB10C9tyHP9JNBCLWKWtq1XTrGxlySffnfhXML80zLYsEdLPumzxFvw== + dependencies: + "@types/react" "*" + "@visx/group" "2.1.0" + "@visx/point" "2.6.0" + "@visx/scale" "2.2.2" + "@visx/shape" "2.4.0" + "@visx/text" "2.3.0" + classnames "^2.3.1" + prop-types "^15.6.0" + +"@visx/bounds@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@visx/bounds/-/bounds-2.1.2.tgz#aa46104d262bc703e73380c69aa5854e5492f77b" + integrity sha512-O80K6PkDH//6xVDP3rSdd+9GNtBUMJKgVXh1ZjW8tAj/rtRq+GiyE17sB3uqV+btNkg0oJiVsFpmoLI50beJDQ== + dependencies: + "@types/react" "*" + "@types/react-dom" "*" + prop-types "^15.5.10" + +"@visx/curve@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@visx/curve/-/curve-2.1.0.tgz#f614bfe3db66df7db7382db7a75ced1506b94602" + integrity sha512-9b6JOnx91gmOQiSPhUOxdsvcnW88fgqfTPKoVgQxidMsD/I3wksixtwo8TR/vtEz2aHzzsEEhlv1qK7Y3yaSDw== + dependencies: + "@types/d3-shape" "^1.3.1" + d3-shape "^1.0.6" + +"@visx/drag@2.6.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@visx/drag/-/drag-2.6.0.tgz#382ad9628c8b978d4ee2cf9cfb4392d8b89f315c" + integrity sha512-/VTNgpKT1i4+O053C3feI6EIEf7ml+PMr6/YDe+nCuqQeypdRtmxvU6QzhKOJ5xrGT+gxaECE5kLgrx94xZ3aw== + dependencies: + "@types/react" "*" + "@visx/event" "2.6.0" + "@visx/point" "2.6.0" + prop-types "^15.5.10" + +"@visx/event@2.6.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@visx/event/-/event-2.6.0.tgz#0718eb1efabd5305cf659a153779c94ba4038996" + integrity sha512-WGp91g82s727g3NAnENF1ppC3ZAlvWg+Y+GG0WFg34NmmOZbvPI/PTOqTqZE3x6B8EUn8NJiMxRjxIMbi+IvRw== + dependencies: + "@types/react" "*" + "@visx/point" "2.6.0" + +"@visx/glyph@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@visx/glyph/-/glyph-2.1.2.tgz#3da1a6280b09b43012256dca65845230d16ef49d" + integrity sha512-HUVpW2mdaWP6zfMIGFUW7xR4eRyvzthic/9f46m+4eX8e6a5tJlgh1gNwrTB5rly1kXtBlyGaDOxKEkrs+6UQA== + dependencies: + "@types/d3-shape" "^1.3.1" + "@types/react" "*" + "@visx/group" "2.1.0" + classnames "^2.3.1" + d3-shape "^1.2.0" + prop-types "^15.6.2" + +"@visx/grid@2.6.0", "@visx/grid@^2.6.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@visx/grid/-/grid-2.6.0.tgz#bb24e88dafd3eba3a867a4327dc9dad32bfda2be" + integrity sha512-LH4yioffUZRT1ic3KSYqhimF6GPZCs3jzv4Xdz5gNVsSbEY4BOQyulxtq8375Z80DpLwK1VQAMxoNdj1EDkxIg== + dependencies: + "@types/react" "*" + "@visx/curve" "2.1.0" + "@visx/group" "2.1.0" + "@visx/point" "2.6.0" + "@visx/scale" "2.2.2" + "@visx/shape" "2.4.0" + classnames "^2.3.1" + prop-types "^15.6.2" + +"@visx/group@2.1.0", "@visx/group@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@visx/group/-/group-2.1.0.tgz#65d4a72feb20dd09443f73cdc12ad5fb339792ef" + integrity sha512-bZKa54yVjGYPZZhzYHLz4AVlidSr4ET9B/xmSa7nnictMJWr7e/IuZThB/bMfDQlgdtvhcfTgs+ZluySc5SBUg== + dependencies: + "@types/react" "*" + classnames "^2.3.1" + prop-types "^15.6.2" + +"@visx/point@2.6.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@visx/point/-/point-2.6.0.tgz#c4316ca409b5b829c5455f07118d8c14a92cc633" + integrity sha512-amBi7yMz4S2VSchlPdliznN41TuES64506ySI22DeKQ+mc1s1+BudlpnY90sM1EIw4xnqbKmrghTTGfy6SVqvQ== + +"@visx/react-spring@2.6.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@visx/react-spring/-/react-spring-2.6.0.tgz#e21c2b38c1303924e6ecd348550ef3574fc214c6" + integrity sha512-HSDxVjsHuptAJ4ho+gDGI9CSduFz9vOSUHEP72wI1lSq5SJ/uvw7sE9BcvLE8zZZT4D83NzFyViuYBqB7M1sVw== + dependencies: + "@types/react" "*" + "@visx/axis" "2.6.0" + "@visx/grid" "2.6.0" + "@visx/scale" "2.2.2" + "@visx/text" "2.3.0" + classnames "^2.3.1" + prop-types "^15.6.2" + +"@visx/responsive@2.8.0", "@visx/responsive@^2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@visx/responsive/-/responsive-2.8.0.tgz#96fac67b77e9573393be780d9657337bef9b72b2" + integrity sha512-/CDL6aWvQARwv1CkNh1bZ6QW7Q982UU3UVESZ5Sffp1s3toygvdCmuDreL8UpDjJp0PQNtoc2alee5VUSCxg3Q== + dependencies: + "@juggle/resize-observer" "^3.3.1" + "@types/lodash" "^4.14.172" + "@types/react" "*" + lodash "^4.17.21" + prop-types "^15.6.1" + +"@visx/scale@2.2.2", "@visx/scale@^2.2.2": + version "2.2.2" + resolved "https://registry.yarnpkg.com/@visx/scale/-/scale-2.2.2.tgz#b8eafabdcf92bb45ab196058fe184772ad80fd25" + integrity sha512-3aDySGUTpe6VykDQmF+g2nz5paFu9iSPTcCOEgkcru0/v5tmGzUdvivy8CkYbr87HN73V/Jc53lGm+kJUQcLBw== + dependencies: + "@types/d3-interpolate" "^1.3.1" + "@types/d3-scale" "^3.3.0" + "@types/d3-time" "^2.0.0" + d3-interpolate "^1.4.0" + d3-scale "^3.3.0" + d3-time "^2.1.1" + +"@visx/shape@2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@visx/shape/-/shape-2.4.0.tgz#d25d6a319c416258e12947d74773edb83051ba03" + integrity sha512-D6XdGCgWi0/0ZKJ5iK8W5gILCKdrbDwnR/e7o6n/favLU0o+ntiI4a9PZBZ5bYS0aFNG7r+miGMcWV/AQfODuA== + dependencies: + "@types/d3-path" "^1.0.8" + "@types/d3-shape" "^1.3.1" + "@types/lodash" "^4.14.172" + "@types/react" "*" + "@visx/curve" "2.1.0" + "@visx/group" "2.1.0" + "@visx/scale" "2.2.2" + classnames "^2.3.1" + d3-path "^1.0.5" + d3-shape "^1.2.0" + lodash "^4.17.21" + prop-types "^15.5.10" + +"@visx/text@2.3.0", "@visx/text@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@visx/text/-/text-2.3.0.tgz#34b66a467685cf1db2380a2673e032f2f874919a" + integrity sha512-5mEcmzWZqbziz6Azv+H6BWrnUbnpygAw4SNzBqF+YiGik5gR/USaBRAs9aKjnoUg6qFe3NZWFOcGpR3BzDR/bQ== + dependencies: + "@types/lodash" "^4.14.172" + "@types/react" "*" + classnames "^2.3.1" + lodash "^4.17.21" + prop-types "^15.7.2" + reduce-css-calc "^1.3.0" + +"@visx/tooltip@2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@visx/tooltip/-/tooltip-2.8.0.tgz#6318286a5eaf84f868fcf005237edb18121c7b36" + integrity sha512-INt5/OSchHr7PphYznTV5jw3YP1X006IwWwmAUacDkHk12hunKjnssdIVskIDQXxlNTho1g44ynl7sc3zGbfQQ== + dependencies: + "@types/react" "*" + "@visx/bounds" "2.1.2" + classnames "^2.3.1" + prop-types "^15.5.10" + react-use-measure "^2.0.4" + +"@visx/voronoi@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@visx/voronoi/-/voronoi-2.1.2.tgz#3fff19d06446fb9e48340f9d6d1ee9d484dd9842" + integrity sha512-fbFLqGzBhpufb+gu9r1mPcb1iwjvbPi0/HdQF4YEm0zgcDvd+wir+3xUonVhTq3FWnlM5WEZ/+x8Jg8l85H47A== + dependencies: + "@types/d3-voronoi" "^1.1.9" + "@types/react" "*" + classnames "^2.3.1" + d3-voronoi "^1.1.2" + prop-types "^15.6.1" + +"@visx/xychart@^2.9.0": + version "2.9.0" + resolved "https://registry.yarnpkg.com/@visx/xychart/-/xychart-2.9.0.tgz#74bbe56de3f3a923aff7d06d73473dc0559503d4" + integrity sha512-WC5qY6GrOwuwwmnwZVZgv8x766P0X18bj8pj1jKKeAHed9Bx1mo/huO8QRQZHZoR5pR/r1/L6QTTem6WxFqeSQ== + dependencies: + "@types/lodash" "^4.14.172" + "@types/react" "*" + "@visx/annotation" "2.9.0" + "@visx/axis" "2.6.0" + "@visx/event" "2.6.0" + "@visx/glyph" "2.1.2" + "@visx/grid" "2.6.0" + "@visx/react-spring" "2.6.0" + "@visx/responsive" "2.8.0" + "@visx/scale" "2.2.2" + "@visx/shape" "2.4.0" + "@visx/text" "2.3.0" + "@visx/tooltip" "2.8.0" + "@visx/voronoi" "2.1.2" + classnames "^2.3.1" + d3-array "^2.6.0" + d3-interpolate-path "2.2.1" + d3-shape "^2.0.0" + lodash "^4.17.21" + mitt "^2.1.0" + prop-types "^15.6.2" + acorn-jsx@^5.0.0, acorn-jsx@^5.3.1: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" @@ -1092,6 +1379,11 @@ bail@^2.0.0: resolved "https://registry.yarnpkg.com/bail/-/bail-2.0.2.tgz#d26f5cd8fe5d6f832a31517b9f7c356040ba6d5d" integrity sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw== +balanced-match@^0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" + integrity sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg= + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -1124,7 +1416,7 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -braces@^3.0.1, braces@~3.0.2: +braces@^3.0.1, braces@^3.0.2, 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== @@ -1328,11 +1620,137 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.11.tgz#d66700c5eacfac1940deb4e3ee5642792d85cd33" integrity sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw== +d3-array@2, d3-array@^2.3.0, d3-array@^2.6.0: + version "2.12.1" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.12.1.tgz#e20b41aafcdffdf5d50928004ececf815a465e81" + integrity sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ== + dependencies: + internmap "^1.0.0" + +"d3-array@2 - 3": + version "3.1.6" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.1.6.tgz#0342c835925826f49b4d16eb7027aec334ffc97d" + integrity sha512-DCbBBNuKOeiR9h04ySRBMW52TFVc91O9wJziuyXw6Ztmy8D3oZbmCkOO3UHKC7ceNJsN2Mavo9+vwV8EAEUXzA== + dependencies: + internmap "1 - 2" + +d3-color@1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.4.1.tgz#c52002bf8846ada4424d55d97982fef26eb3bc8a" + integrity sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q== + +"d3-color@1 - 2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-2.0.0.tgz#8d625cab42ed9b8f601a1760a389f7ea9189d62e" + integrity sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ== + +"d3-format@1 - 2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-2.0.0.tgz#a10bcc0f986c372b729ba447382413aabf5b0767" + integrity sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA== + +d3-format@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641" + integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA== + +d3-interpolate-path@2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/d3-interpolate-path/-/d3-interpolate-path-2.2.1.tgz#fd8ff20a90aff3f380bcd1c15305e7b531e55d07" + integrity sha512-6qLLh/KJVzls0XtMsMpcxhqMhgVEN7VIbR/6YGZe2qlS8KDgyyVB20XcmGnDyB051HcefQXM/Tppa9vcANEA4Q== + +"d3-interpolate@1.2.0 - 2": + version "2.0.1" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-2.0.1.tgz#98be499cfb8a3b94d4ff616900501a64abc91163" + integrity sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ== + dependencies: + d3-color "1 - 2" + +d3-interpolate@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.4.0.tgz#526e79e2d80daa383f9e0c1c1c7dcc0f0583e987" + integrity sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA== + dependencies: + d3-color "1" + +d3-path@1, d3-path@^1.0.5: + version "1.0.9" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf" + integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg== + +"d3-path@1 - 2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-2.0.0.tgz#55d86ac131a0548adae241eebfb56b4582dd09d8" + integrity sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA== + +d3-scale@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-3.3.0.tgz#28c600b29f47e5b9cd2df9749c206727966203f3" + integrity sha512-1JGp44NQCt5d1g+Yy+GeOnZP7xHo0ii8zsQp6PGzd+C1/dl0KGsp9A7Mxwp+1D1o4unbTTxVdU/ZOIEBoeZPbQ== + dependencies: + d3-array "^2.3.0" + d3-format "1 - 2" + d3-interpolate "1.2.0 - 2" + d3-time "^2.1.1" + d3-time-format "2 - 3" + +d3-shape@^1.0.6, d3-shape@^1.2.0: + version "1.3.7" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7" + integrity sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw== + dependencies: + d3-path "1" + +d3-shape@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-2.1.0.tgz#3b6a82ccafbc45de55b57fcf956c584ded3b666f" + integrity sha512-PnjUqfM2PpskbSLTJvAzp2Wv4CZsnAgTfcVRTwW03QR3MkXF8Uo7B1y/lWkAsmbKwuecto++4NlsYcvYpXpTHA== + dependencies: + d3-path "1 - 2" + +"d3-time-format@2 - 3": + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-3.0.0.tgz#df8056c83659e01f20ac5da5fdeae7c08d5f1bb6" + integrity sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag== + dependencies: + d3-time "1 - 2" + +d3-time-format@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a" + integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg== + dependencies: + d3-time "1 - 3" + +"d3-time@1 - 2", d3-time@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-2.1.1.tgz#e9d8a8a88691f4548e68ca085e5ff956724a6682" + integrity sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ== + dependencies: + d3-array "2" + +"d3-time@1 - 3": + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.0.0.tgz#65972cb98ae2d4954ef5c932e8704061335d4975" + integrity sha512-zmV3lRnlaLI08y9IMRXSDshQb5Nj77smnfpnd2LrBa/2K281Jijactokeak14QacHs/kKq0AQ121nidNYlarbQ== + dependencies: + d3-array "2 - 3" + +d3-voronoi@^1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/d3-voronoi/-/d3-voronoi-1.1.4.tgz#dd3c78d7653d2bb359284ae478645d95944c8297" + integrity sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg== + damerau-levenshtein@^1.0.7: version "1.0.8" resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== +debounce@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5" + integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug== + debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -1442,6 +1860,14 @@ emoji-regex@^9.2.2: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== +enhanced-resolve@^5.0.0: + version "5.9.3" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz#44a342c012cbc473254af5cc6ae20ebd0aae5d88" + integrity sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + enquirer@^2.3.5: version "2.3.6" resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" @@ -2096,6 +2522,11 @@ globby@^11.0.3, globby@^11.0.4: merge2 "^1.4.1" slash "^3.0.0" +graceful-fs@^4.2.4: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + gray-matter@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-4.0.3.tgz#e893c064825de73ea1f5f7d88c7a9f7274288798" @@ -2257,6 +2688,16 @@ internal-slot@^1.0.3: has "^1.0.3" side-channel "^1.0.4" +"internmap@1 - 2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" + integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== + +internmap@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/internmap/-/internmap-1.0.1.tgz#0017cc8a3b99605f0302f2b198d272e015e5df95" + integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw== + invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" @@ -2624,6 +3065,11 @@ markdown-table@^3.0.0: resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.2.tgz#9b59eb2c1b22fe71954a65ff512887065a7bb57c" integrity sha512-y8j3a5/DkJCmS5x4dMCQL+OR0+2EAq3DOtio1COSHsmW2BGXnNCK3v12hJt1LrUz5iZH5g0LmuYOjDdI+czghA== +math-expression-evaluator@^1.2.14: + version "1.3.14" + resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.3.14.tgz#0ebeaccf65fea0f6f5a626f88df41814e5fcd9bf" + integrity sha512-M6AMrvq9bO8uL42KvQHPA2/SbAobA0R7gviUmPrcTcGfdwpaLitz4q2Euzx2lP9Oy88vxK3HOrsISgSwKsYS4A== + mdast-util-definitions@^5.0.0: version "5.1.0" resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-5.1.0.tgz#b6d10ef00a3c4cf191e8d9a5fa58d7f4a366f817" @@ -3173,6 +3619,14 @@ micromark@^3.0.0: micromark-util-types "^1.0.1" uvu "^0.5.0" +micromatch@^4.0.0: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + micromatch@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" @@ -3193,6 +3647,11 @@ minimist@^1.2.0, minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +mitt@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mitt/-/mitt-2.1.0.tgz#f740577c23176c6205b121b2973514eade1b2230" + integrity sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg== + mri@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" @@ -3472,7 +3931,7 @@ picocolors@^1.0.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== @@ -3532,7 +3991,7 @@ prop-types-extra@^1.1.0: react-is "^16.3.2" warning "^4.0.0" -prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@^15.5.10, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -3625,6 +4084,13 @@ react-transition-group@^4.4.1: loose-envify "^1.4.0" prop-types "^15.6.2" +react-use-measure@^2.0.4: + version "2.1.1" + resolved "https://registry.yarnpkg.com/react-use-measure/-/react-use-measure-2.1.1.tgz#5824537f4ee01c9469c45d5f7a8446177c6cc4ba" + integrity sha512-nocZhN26cproIiIduswYpV5y5lQpSQS1y/4KuvUCjSKmw7ZWIS/+g3aFnX3WdBkyuGUtTLif3UTqnLLhbDoQig== + dependencies: + debounce "^1.2.1" + react@17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" @@ -3659,6 +4125,22 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +reduce-css-calc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716" + integrity sha1-dHyRTgSWFKTJz7umKYca0dKSdxY= + dependencies: + balanced-match "^0.4.2" + math-expression-evaluator "^1.2.14" + reduce-function-call "^1.0.1" + +reduce-function-call@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/reduce-function-call/-/reduce-function-call-1.0.3.tgz#60350f7fb252c0a67eb10fd4694d16909971300f" + integrity sha512-Hl/tuV2VDgWgCSEeWMLwxLZqX7OK59eU1guxXsRKTAyeYimivsKdtcV4fu3r710tpG5GmDKDhQ0HSZLExnNmyQ== + dependencies: + balanced-match "^1.0.0" + regenerator-runtime@^0.13.4: version "0.13.9" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" @@ -3824,6 +4306,13 @@ rxjs-report-usage@^1.0.4: glob "~7.2.0" prompts "~2.4.2" +rxjs@^7.5.5: + version "7.5.5" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.5.tgz#2ebad89af0f560f460ad5cc4213219e1f7dd4e9f" + integrity sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw== + dependencies: + tslib "^2.1.0" + sade@^1.7.3: version "1.8.1" resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701" @@ -4082,6 +4571,11 @@ table@^6.0.9: string-width "^4.2.3" strip-ansi "^6.0.1" +tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -4104,6 +4598,16 @@ trough@^2.0.0: resolved "https://registry.yarnpkg.com/trough/-/trough-2.1.0.tgz#0f7b511a4fde65a46f18477ab38849b22c554876" integrity sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g== +ts-loader@^9.2.8: + version "9.2.8" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.2.8.tgz#e89aa32fa829c5cad0a1d023d6b3adecd51d5a48" + integrity sha512-gxSak7IHUuRtwKf3FIPSW1VpZcqF9+MBrHOvBp9cjHh+525SjtCIJKVGjRKIAfxBwDGDGCFF00rTfzB1quxdSw== + dependencies: + chalk "^4.1.0" + enhanced-resolve "^5.0.0" + micromatch "^4.0.0" + semver "^7.3.4" + ts-node@^10.7.0: version "10.7.0" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.7.0.tgz#35d503d0fab3e2baa672a0e94f4b40653c2463f5" @@ -4138,7 +4642,7 @@ tslib@^1.8.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0: +tslib@^2.0.0, tslib@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==