mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 20:31:48 +00:00
Code Insights: Migrate dashboard chart cards to new chart and card components API (#33799)
* Create InsightCard component (abstraction for building insight card for the dashboard page) * Migrate BuiltIn (runtime insight) card component * Improve type safety over data fetching state * Revamp backend insight api methods * Move locked chart view to insight folder * Update some of @visx packages
This commit is contained in:
parent
1407ed3a31
commit
fa2e2a7709
@ -110,7 +110,15 @@ const PlainChart = () => {
|
||||
},
|
||||
]
|
||||
|
||||
return <LineChart width={400} height={400} data={DATA} series={SERIES} getXValue={getXValue} />
|
||||
return (
|
||||
<div style={{ width: 400, height: 400 }}>
|
||||
<ParentSize className="flex-1">
|
||||
{({ width, height }) => (
|
||||
<LineChart width={width} height={height} data={DATA} series={SERIES} getXValue={getXValue} />
|
||||
)}
|
||||
</ParentSize>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const PlainStackedChart = () => {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { ReactElement, useMemo, useRef, useState, SVGProps } from 'react'
|
||||
import React, { ReactElement, useMemo, useState, SVGProps } from 'react'
|
||||
|
||||
import { curveLinear } from '@visx/curve'
|
||||
import { Group } from '@visx/group'
|
||||
@ -51,8 +51,8 @@ export function LineChart<D>(props: LineChartContentProps<D>): ReactElement | nu
|
||||
} = props
|
||||
|
||||
const [activePoint, setActivePoint] = useState<Point<D> & { element?: Element }>()
|
||||
const yAxisReference = useRef<SVGGElement>(null)
|
||||
const xAxisReference = useRef<SVGGElement>(null)
|
||||
const [yAxisElement, setYAxisElement] = useState<SVGGElement | null>(null)
|
||||
const [xAxisReference, setXAxisElement] = useState<SVGGElement | null>(null)
|
||||
|
||||
const { width, height, margin } = useMemo(
|
||||
() =>
|
||||
@ -62,11 +62,11 @@ export function LineChart<D>(props: LineChartContentProps<D>): ReactElement | nu
|
||||
margin: {
|
||||
top: 10,
|
||||
right: 20,
|
||||
left: yAxisReference.current?.getBoundingClientRect().width,
|
||||
bottom: xAxisReference.current?.getBoundingClientRect().height,
|
||||
left: yAxisElement?.getBoundingClientRect().width,
|
||||
bottom: xAxisReference?.getBoundingClientRect().height,
|
||||
},
|
||||
}),
|
||||
[outerWidth, outerHeight]
|
||||
[yAxisElement, xAxisReference, outerWidth, outerHeight]
|
||||
)
|
||||
|
||||
const dataSeries = useMemo(() => getSeriesData({ data, series, stacked, getXValue }), [
|
||||
@ -147,7 +147,7 @@ export function LineChart<D>(props: LineChartContentProps<D>): ReactElement | nu
|
||||
{...handlers}
|
||||
>
|
||||
<AxisLeft
|
||||
ref={yAxisReference}
|
||||
ref={setYAxisElement}
|
||||
scale={yScale}
|
||||
width={width}
|
||||
height={height}
|
||||
@ -155,7 +155,7 @@ export function LineChart<D>(props: LineChartContentProps<D>): ReactElement | nu
|
||||
left={margin.left}
|
||||
/>
|
||||
|
||||
<AxisBottom ref={xAxisReference} scale={xScale} top={margin.top + height} width={width} />
|
||||
<AxisBottom ref={setXAxisElement} scale={xScale} top={margin.top + height} width={width} />
|
||||
|
||||
<NonActiveBackground
|
||||
data={data}
|
||||
|
||||
@ -1,21 +1,27 @@
|
||||
import React from 'react'
|
||||
|
||||
import { bottomTickLabelProps } from '@visx/axis/lib/axis/AxisBottom'
|
||||
import { leftTickLabelProps } from '@visx/axis/lib/axis/AxisLeft'
|
||||
import { TickLabelProps, TickRendererProps } from '@visx/axis/lib/types'
|
||||
import { TickLabelProps, TickRendererProps } from '@visx/axis'
|
||||
import { Group } from '@visx/group'
|
||||
import { Text } from '@visx/text'
|
||||
import { TextProps } from '@visx/text/lib/Text'
|
||||
import { Text, TextProps } from '@visx/text'
|
||||
|
||||
import { formatXLabel } from '../../utils'
|
||||
|
||||
export const getTickYProps: TickLabelProps<number> = (value, index, values): Partial<TextProps> => ({
|
||||
...leftTickLabelProps(),
|
||||
dx: '-0.25em',
|
||||
dy: '0.25em',
|
||||
fill: '#222',
|
||||
fontFamily: 'Arial',
|
||||
fontSize: 10,
|
||||
textAnchor: 'end',
|
||||
'aria-label': `Tick axis ${index + 1} of ${values.length}. Value: ${value}`,
|
||||
})
|
||||
|
||||
export const getTickXProps: TickLabelProps<Date> = (value, index, values): Partial<TextProps> => ({
|
||||
...bottomTickLabelProps(),
|
||||
dy: '0.25em',
|
||||
fill: '#222',
|
||||
fontFamily: 'Arial',
|
||||
fontSize: 10,
|
||||
textAnchor: 'middle',
|
||||
'aria-label': `Tick axis ${index + 1} of ${values.length}. Value: ${formatXLabel(value)}`,
|
||||
})
|
||||
|
||||
@ -26,14 +32,14 @@ export const Tick: React.FunctionComponent<TickRendererProps> = props => {
|
||||
const { formattedValue, ...tickLabelProps } = props
|
||||
|
||||
// Hack with Group + Text (aria hidden)
|
||||
// Because the Text component renders text inside of svg element and text element with tspan
|
||||
// Because the Text component renders text inside svg element and text element with tspan
|
||||
// this makes another nested group for a11y tree. To avoid "group - end group"
|
||||
// phrase in voice over we hide nested children from a11y tree and put explicit aria-label
|
||||
// on the parent Group element with role text
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/aria-role
|
||||
<Group role="text" aria-label={tickLabelProps['aria-label']}>
|
||||
<Text aria-hidden={true} {...tickLabelProps}>
|
||||
<Text aria-hidden={true} {...(tickLabelProps as TextProps)}>
|
||||
{formattedValue}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import React, { LiHTMLAttributes } from 'react'
|
||||
|
||||
import classNames from 'classnames'
|
||||
|
||||
@ -18,18 +18,18 @@ export const LegendList: React.FunctionComponent<LegendListProps> = props => {
|
||||
)
|
||||
}
|
||||
|
||||
interface LegendItemProps {
|
||||
interface LegendItemProps extends LiHTMLAttributes<HTMLLIElement> {
|
||||
color: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export const LegendItem: React.FunctionComponent<LegendItemProps> = props => (
|
||||
<li className={styles.legendItem}>
|
||||
export const LegendItem: React.FunctionComponent<LegendItemProps> = ({ color, name, className, ...attributes }) => (
|
||||
<li {...attributes} className={classNames(styles.legendItem, className)}>
|
||||
<div
|
||||
/* eslint-disable-next-line react/forbid-dom-props */
|
||||
style={{ backgroundColor: props.color }}
|
||||
style={{ backgroundColor: color }}
|
||||
className={styles.legendMark}
|
||||
/>
|
||||
{props.name}
|
||||
{name}
|
||||
</li>
|
||||
)
|
||||
|
||||
@ -20,19 +20,19 @@ export function generatePointsField<Datum>(input: PointsFieldInput<Datum>): Poin
|
||||
return dataSeries.flatMap(series => {
|
||||
const { data, dataKey, getLinkURL = NULL_LINK } = series
|
||||
|
||||
return (data as SeriesDatum<Datum>[]).filter(isDatumWithValidNumber).map((data, index) => {
|
||||
const datumValue = getDatumValue(data)
|
||||
return (data as SeriesDatum<Datum>[]).filter(isDatumWithValidNumber).map((datum, index) => {
|
||||
const datumValue = getDatumValue(datum)
|
||||
|
||||
return {
|
||||
id: `${dataKey as string}-${index}`,
|
||||
seriesKey: dataKey as string,
|
||||
value: datumValue,
|
||||
y: yScale(datumValue),
|
||||
x: xScale(data.x),
|
||||
time: data.x,
|
||||
x: xScale(datum.x),
|
||||
time: datum.x,
|
||||
color: series.color ?? 'green',
|
||||
linkUrl: getLinkURL(data.datum, index),
|
||||
datum: data.datum,
|
||||
linkUrl: getLinkURL(datum.datum, index),
|
||||
datum: datum.datum,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,22 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import classNames from 'classnames'
|
||||
|
||||
import styles from './AlertOverlay.module.scss'
|
||||
|
||||
export interface AlertOverlayProps {
|
||||
title: string
|
||||
description: string
|
||||
icon?: React.ReactNode
|
||||
}
|
||||
|
||||
export const AlertOverlay: React.FunctionComponent<AlertOverlayProps> = ({ title, description, icon }) => (
|
||||
<>
|
||||
<div className={classNames('position-absolute w-100 h-100', styles.gradient)} />
|
||||
<div className="position-absolute d-flex flex-column justify-content-center align-items-center w-100 h-100">
|
||||
{icon && <div className={styles.icon}>{icon}</div>}
|
||||
<h4 className={styles.title}>{title}</h4>
|
||||
<small className={styles.description}>{description}</small>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
@ -2,7 +2,8 @@ import React from 'react'
|
||||
|
||||
import { Page } from '../../../../components/Page'
|
||||
import { useUiFeatures } from '../../hooks/use-ui-features'
|
||||
import { CodeInsightsLimitAccessBanner } from '../limit-access-banner/CodeInsightsLimitAccessBanner'
|
||||
|
||||
import { CodeInsightsLimitAccessBanner } from './limit-access-banner/CodeInsightsLimitAccessBanner'
|
||||
|
||||
interface CodeInsightsPageProps extends React.HTMLAttributes<HTMLDivElement> {}
|
||||
|
||||
|
||||
@ -1,98 +0,0 @@
|
||||
import { getCollisions, Position } from '@reach/popover'
|
||||
|
||||
const DEFAULT_PADDING = 6
|
||||
|
||||
/**
|
||||
* Custom popover position calculator. Returns position objects (top,left,right,bottom) styles
|
||||
* with values such that the target and the popover element have the same right borders.
|
||||
*
|
||||
* <pre>
|
||||
* ------------ | Target | --------
|
||||
* ----|*****************| --------
|
||||
* ----|*****************| --------
|
||||
* ----|*** Popover *****| --------
|
||||
* ----|*****************| --------
|
||||
* ----|*****************| --------
|
||||
* </pre>
|
||||
*
|
||||
* @param targetRectangle - bounding client rect of the target element
|
||||
* @param popoverRectangle - bounding client rect of the pop-over element. All calculation props
|
||||
* that are returned from this function will be applied to this element.
|
||||
*/
|
||||
export const positionBottomRight: Position = (targetRectangle, popoverRectangle) => {
|
||||
if (!targetRectangle || !popoverRectangle) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const { directionUp } = getCollisions(targetRectangle, popoverRectangle)
|
||||
|
||||
return {
|
||||
left: `${targetRectangle.right - popoverRectangle.width + window.scrollX}px`,
|
||||
top: directionUp
|
||||
? `${targetRectangle.top - popoverRectangle.height + window.scrollY - DEFAULT_PADDING}px`
|
||||
: `${targetRectangle.top + targetRectangle.height + window.scrollY + DEFAULT_PADDING}px`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom position calculator with flip logic.
|
||||
*
|
||||
* <pre>
|
||||
* In case if it's enough space at right
|
||||
* --| Target ||*****************|--
|
||||
* ------------|*****************|--
|
||||
* ------------|**** Popover ****|--
|
||||
* ------------|*****************|--
|
||||
* ------------|*****************|--
|
||||
*
|
||||
* In other case if it's enough space at left side
|
||||
* --|*****************|| Target |
|
||||
* --|*****************|
|
||||
* --|**** Popover ****|
|
||||
* --|*****************|
|
||||
* --|*****************|
|
||||
*
|
||||
* And as a fallback plan place it below the target
|
||||
* ------------ | Target | --------
|
||||
* ----|*****************| --------
|
||||
* ----|*****************| --------
|
||||
* ----|*** Popover *****| --------
|
||||
* ----|*****************| --------
|
||||
* ----|*****************| --------
|
||||
* </pre>
|
||||
*
|
||||
* @param targetRectangle - bounding client rect of the target element
|
||||
* @param popoverRectangle - bounding client rect of the pop-over element. All calculation props
|
||||
* that are returned from this function will be applied to this element
|
||||
*/
|
||||
export const flipRightPosition: Position = (targetRectangle, popoverRectangle) => {
|
||||
if (!targetRectangle || !popoverRectangle) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const isEnoughSpaceLeft = targetRectangle.left - popoverRectangle.width > 0
|
||||
const isEnoughSpaceRight = window.innerWidth > targetRectangle.right + popoverRectangle.width
|
||||
|
||||
const { directionUp } = getCollisions(targetRectangle, popoverRectangle)
|
||||
|
||||
if (isEnoughSpaceRight) {
|
||||
return {
|
||||
left: `${targetRectangle.right + window.scrollX + DEFAULT_PADDING}px`,
|
||||
top: `${targetRectangle.top + window.scrollY - 4}px`,
|
||||
}
|
||||
}
|
||||
|
||||
if (isEnoughSpaceLeft) {
|
||||
return {
|
||||
left: `${targetRectangle.left - popoverRectangle.width + window.scrollX - DEFAULT_PADDING}px`,
|
||||
top: `${targetRectangle.top + window.scrollY - 4}px`,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
left: `${targetRectangle.right - popoverRectangle.width + window.scrollX}px`,
|
||||
top: directionUp
|
||||
? `${targetRectangle.top - popoverRectangle.height + window.scrollY - DEFAULT_PADDING}px`
|
||||
: `${targetRectangle.top + targetRectangle.height + window.scrollY + DEFAULT_PADDING}px`,
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,7 @@
|
||||
|
||||
.insight-card {
|
||||
min-height: 20rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chart-block {
|
||||
|
||||
@ -1,2 +1 @@
|
||||
export { CodeInsightsIcon } from '../../../insights/Icons'
|
||||
export { InsightsNavItem } from './insights-nav-link/InsightsNavLink'
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import { LinkWithIcon } from '../../../../components/LinkWithIcon'
|
||||
import { CodeInsightsIcon } from '../../../../insights/Icons'
|
||||
|
||||
export const InsightsNavItem: React.FunctionComponent = () => (
|
||||
<LinkWithIcon
|
||||
to="/insights"
|
||||
text="Insights"
|
||||
icon={CodeInsightsIcon}
|
||||
className="nav-link text-decoration-none"
|
||||
activeClassName="active"
|
||||
/>
|
||||
)
|
||||
@ -1,17 +1,13 @@
|
||||
import React from 'react'
|
||||
|
||||
import { Meta } from '@storybook/react'
|
||||
import { of } from 'rxjs'
|
||||
import { Observable, of } from 'rxjs'
|
||||
|
||||
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
|
||||
import { WebStory } from '../../../../components/WebStory'
|
||||
import {
|
||||
LINE_CHART_TESTS_CASES_EXAMPLE,
|
||||
LINE_CHART_WITH_HUGE_NUMBER_OF_LINES,
|
||||
LINE_CHART_WITH_MANY_LINES,
|
||||
} from '../../../../views/mocks/charts-content'
|
||||
import { CodeInsightsBackendStoryMock } from '../../CodeInsightsBackendStoryMock'
|
||||
import { BackendInsightData, SeriesChartContent } from '../../core'
|
||||
import { BackendInsight, Insight, InsightExecutionType, InsightType, isCaptureGroupInsight } from '../../core/types'
|
||||
|
||||
import { SmartInsightsViewGrid } from './SmartInsightsViewGrid'
|
||||
@ -137,26 +133,267 @@ const insightsWithManyLines: Insight[] = [
|
||||
},
|
||||
]
|
||||
|
||||
interface HugeLinesDatum {
|
||||
x: number
|
||||
a: number
|
||||
b: number
|
||||
c: number
|
||||
d: number
|
||||
e: number | null
|
||||
f: number
|
||||
g: number
|
||||
h: number
|
||||
i: number
|
||||
j: number
|
||||
k: number
|
||||
l: number
|
||||
m: number
|
||||
n: number
|
||||
o: number
|
||||
p: number
|
||||
q: number
|
||||
r: number
|
||||
s: number
|
||||
t: number
|
||||
}
|
||||
|
||||
const LINE_CHART_WITH_HUGE_NUMBER_OF_LINES: SeriesChartContent<HugeLinesDatum> = {
|
||||
data: [
|
||||
{
|
||||
x: 1588965700286 - 4 * 24 * 60 * 60 * 1000,
|
||||
a: 4000,
|
||||
b: 15000,
|
||||
c: 12000,
|
||||
d: 11000,
|
||||
e: 11000,
|
||||
f: 13000,
|
||||
g: 5000,
|
||||
h: 5000,
|
||||
i: 5000,
|
||||
j: 7000,
|
||||
k: 10000,
|
||||
l: 8000,
|
||||
m: 3900,
|
||||
n: 3000,
|
||||
o: 4000,
|
||||
p: 5000,
|
||||
q: 4500,
|
||||
r: 5000,
|
||||
s: 5500,
|
||||
t: 6000,
|
||||
},
|
||||
{
|
||||
x: 1588965700286 - 3 * 24 * 60 * 60 * 1000,
|
||||
a: 4000,
|
||||
b: 17000,
|
||||
c: 14000,
|
||||
d: 11000,
|
||||
e: 11000,
|
||||
f: 5000,
|
||||
g: 5000,
|
||||
h: 6000,
|
||||
i: 5500,
|
||||
j: 7200,
|
||||
k: 8000,
|
||||
l: 7800,
|
||||
m: 4000,
|
||||
n: 3000,
|
||||
o: 4500,
|
||||
p: 5500,
|
||||
q: 5500,
|
||||
r: 6000,
|
||||
s: 7500,
|
||||
t: 5000,
|
||||
},
|
||||
{
|
||||
x: 1588965700286 - 2 * 24 * 60 * 60 * 1000,
|
||||
a: 5600,
|
||||
b: 20000,
|
||||
c: 15000,
|
||||
d: 13000,
|
||||
e: null,
|
||||
f: 23000,
|
||||
g: 8000,
|
||||
h: 7000,
|
||||
i: 4500,
|
||||
j: 11000,
|
||||
k: 10000,
|
||||
l: 9000,
|
||||
m: 5000,
|
||||
n: 3000,
|
||||
o: 4000,
|
||||
p: 5000,
|
||||
q: 4500,
|
||||
r: 5000,
|
||||
s: 5500,
|
||||
t: 6000,
|
||||
},
|
||||
{
|
||||
x: 1588965700286 - 1 * 24 * 60 * 60 * 1000,
|
||||
a: 9800,
|
||||
b: 19000,
|
||||
c: 9000,
|
||||
d: 8000,
|
||||
e: null,
|
||||
f: 13000,
|
||||
g: 5000,
|
||||
h: 6000,
|
||||
i: 5500,
|
||||
j: 7200,
|
||||
k: 8000,
|
||||
l: 7800,
|
||||
m: 4000,
|
||||
n: 4000,
|
||||
o: 5000,
|
||||
p: 4000,
|
||||
q: 7500,
|
||||
r: 8000,
|
||||
s: 8500,
|
||||
t: 4000,
|
||||
},
|
||||
{
|
||||
x: 1588965700286,
|
||||
a: 12300,
|
||||
b: 17000,
|
||||
c: 8000,
|
||||
d: 8500,
|
||||
e: null,
|
||||
f: 16000,
|
||||
g: 9000,
|
||||
h: 8000,
|
||||
i: 5500,
|
||||
j: 12000,
|
||||
k: 11000,
|
||||
l: 10000,
|
||||
m: 6000,
|
||||
n: 6000,
|
||||
o: 7000,
|
||||
p: 8000,
|
||||
q: 6500,
|
||||
r: 9000,
|
||||
s: 10500,
|
||||
t: 16000,
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
dataKey: 'a',
|
||||
name: 'React functional components',
|
||||
color: 'var(--green)',
|
||||
},
|
||||
{
|
||||
dataKey: 'b',
|
||||
name: 'Class components',
|
||||
color: 'var(--orange)',
|
||||
},
|
||||
{ dataKey: 'c', name: 'useTheme adoption', color: 'var(--blue)' },
|
||||
{ dataKey: 'd', name: 'Class without CSS modules', color: 'var(--purple)' },
|
||||
{ dataKey: 'e', name: '1.11', color: 'var(--oc-grape-7)' },
|
||||
{ dataKey: 'f', name: 'Functional components without CSS modules', color: 'var(--oc-red-7)' },
|
||||
{ dataKey: 'g', name: '1.12', color: 'var(--pink)' },
|
||||
{ dataKey: 'h', name: '1.13', color: 'var(--oc-violet-7)' },
|
||||
{ dataKey: 'i', name: '1.14', color: 'var(--indigo)' },
|
||||
{ dataKey: 'm', name: '1.15', color: 'var(--cyan)' },
|
||||
{ dataKey: 'j', name: '1.16', color: 'var(--teal)' },
|
||||
{ dataKey: 'k', name: '1.17', color: 'var(--oc-lime-7)' },
|
||||
{ dataKey: 'l', name: '1.18', color: 'var(--yellow)' },
|
||||
{ dataKey: 'n', name: '1.19', color: 'var(--oc-lime-7)' },
|
||||
{ dataKey: 'o', name: '1.20', color: 'var(--oc-pink-7)' },
|
||||
{ dataKey: 'p', name: '1.21', color: 'var(--oc-red-7)' },
|
||||
{ dataKey: 'q', name: '1.22', color: 'var(--oc-blue-7)' },
|
||||
{ dataKey: 'r', name: '1.23', color: 'var(--oc-grape-7)' },
|
||||
{ dataKey: 's', name: '1.24', color: 'var(--oc-green-7)' },
|
||||
{ dataKey: 't', name: '1.25', color: 'var(--oc-cyan-7)' },
|
||||
],
|
||||
getXValue: datum => new Date(datum.x),
|
||||
}
|
||||
|
||||
interface ManyLinesDatum {
|
||||
x: number
|
||||
a: number
|
||||
b: number
|
||||
c: number
|
||||
d: number
|
||||
f: number
|
||||
}
|
||||
|
||||
const LINE_CHART_WITH_MANY_LINES: SeriesChartContent<ManyLinesDatum> = {
|
||||
data: [
|
||||
{ x: 1588965700286 - 4 * 24 * 60 * 60 * 1000, a: 4000, b: 15000, c: 12000, d: 11000, f: 13000 },
|
||||
{ x: 1588965700286 - 3 * 24 * 60 * 60 * 1000, a: 4000, b: 26000, c: 14000, d: 11000, f: 5000 },
|
||||
{ x: 1588965700286 - 2 * 24 * 60 * 60 * 1000, a: 5600, b: 20000, c: 15000, d: 13000, f: 63000 },
|
||||
{ x: 1588965700286 - 1 * 24 * 60 * 60 * 1000, a: 9800, b: 19000, c: 9000, d: 8000, f: 13000 },
|
||||
{ x: 1588965700286, a: 12300, b: 17000, c: 8000, d: 8500, f: 16000 },
|
||||
],
|
||||
series: [
|
||||
{
|
||||
dataKey: 'a',
|
||||
name: 'React functional components',
|
||||
color: 'var(--warning)',
|
||||
},
|
||||
{
|
||||
dataKey: 'b',
|
||||
name: 'Class components',
|
||||
color: 'var(--warning)',
|
||||
},
|
||||
{ dataKey: 'c', name: 'useTheme adoption', color: 'var(--blue)' },
|
||||
{ dataKey: 'd', name: 'Class without CSS modules', color: 'var(--purple)' },
|
||||
{ dataKey: 'f', name: 'Functional components without CSS modules', color: 'var(--green)' },
|
||||
],
|
||||
getXValue: datum => new Date(datum.x),
|
||||
}
|
||||
|
||||
interface TestCasesDatum {
|
||||
x: number
|
||||
a: number
|
||||
b: number
|
||||
c: number
|
||||
d: number
|
||||
f: number
|
||||
}
|
||||
|
||||
const LINE_CHART_TESTS_CASES_EXAMPLE: SeriesChartContent<TestCasesDatum> = {
|
||||
data: [
|
||||
{ x: 1588965700286 - 4 * 24 * 60 * 60 * 1000, a: 4000, b: 15000, c: 12000, d: 11000, f: 13000 },
|
||||
{ x: 1588965700286 - 3 * 24 * 60 * 60 * 1000, a: 4000, b: 26000, c: 14000, d: 11000, f: 5000 },
|
||||
{ x: 1588965700286 - 2 * 24 * 60 * 60 * 1000, a: 5600, b: 20000, c: 15000, d: 13000, f: 63000 },
|
||||
{ x: 1588965700286 - 1 * 24 * 60 * 60 * 1000, a: 9800, b: 19000, c: 9000, d: 8000, f: 13000 },
|
||||
{ x: 1588965700286, a: 12300, b: 17000, c: 8000, d: 8500, f: 16000 },
|
||||
],
|
||||
series: [
|
||||
{
|
||||
dataKey: 'a',
|
||||
name: 'React Test renderer',
|
||||
color: 'var(--blue)',
|
||||
},
|
||||
{
|
||||
dataKey: 'b',
|
||||
name: 'Enzyme',
|
||||
color: 'var(--pink)',
|
||||
},
|
||||
{
|
||||
dataKey: 'c',
|
||||
name: 'React Testing Library',
|
||||
color: 'var(--red)',
|
||||
},
|
||||
],
|
||||
getXValue: datum => new Date(datum.x),
|
||||
}
|
||||
|
||||
const codeInsightsApiWithManyLines = {
|
||||
getBackendInsightData: (insight: BackendInsight) => {
|
||||
getBackendInsightData: (insight: BackendInsight): Observable<BackendInsightData> => {
|
||||
if (isCaptureGroupInsight(insight)) {
|
||||
throw new Error('This demo does not support capture group insight')
|
||||
}
|
||||
|
||||
return of({
|
||||
id: insight.id,
|
||||
view: {
|
||||
title: 'Backend Insight Mock',
|
||||
subtitle: 'Backend insight description text',
|
||||
content: [
|
||||
insight.series.length >= 6
|
||||
? insight.series.length >= 15
|
||||
? LINE_CHART_WITH_HUGE_NUMBER_OF_LINES
|
||||
: LINE_CHART_WITH_MANY_LINES
|
||||
: LINE_CHART_TESTS_CASES_EXAMPLE,
|
||||
],
|
||||
isFetchingHistoricalData: false,
|
||||
},
|
||||
content:
|
||||
insight.series.length >= 6
|
||||
? insight.series.length >= 15
|
||||
? LINE_CHART_WITH_HUGE_NUMBER_OF_LINES
|
||||
: LINE_CHART_WITH_MANY_LINES
|
||||
: LINE_CHART_TESTS_CASES_EXAMPLE,
|
||||
isFetchingHistoricalData: false,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
@ -6,10 +6,10 @@ import { Layout, Layouts } from 'react-grid-layout'
|
||||
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
|
||||
import { ViewGrid } from '../../../../views'
|
||||
import { Insight } from '../../core/types'
|
||||
import { Insight } from '../../core'
|
||||
import { getTrackingTypeByInsightType } from '../../pings'
|
||||
|
||||
import { SmartInsight } from './components/smart-insight/SmartInsight'
|
||||
import { SmartInsight } from './components/SmartInsight'
|
||||
import { insightLayoutGenerator, recalculateGridLayout } from './utils/grid-layout-generator'
|
||||
|
||||
interface SmartInsightsViewGridProps extends TelemetryProps {
|
||||
|
||||
@ -0,0 +1,138 @@
|
||||
import React, { Ref, useContext, useMemo, useRef, useState } from 'react'
|
||||
|
||||
import { useMergeRefs } from 'use-callback-ref'
|
||||
|
||||
import { ErrorAlert } from '@sourcegraph/branded/src/components/alerts'
|
||||
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { useDeepMemo } from '@sourcegraph/wildcard'
|
||||
|
||||
import { ParentSize } from '../../../../../charts'
|
||||
import { CodeInsightsBackendContext, LangStatsInsight, SearchRuntimeBasedInsight } from '../../../core'
|
||||
import { InsightContentType } from '../../../core/types/insight/common'
|
||||
import { useDeleteInsight } from '../../../hooks/use-delete-insight'
|
||||
import { LazyQueryStatus } from '../../../hooks/use-parallel-requests/use-parallel-request'
|
||||
import { useRemoveInsightFromDashboard } from '../../../hooks/use-remove-insight'
|
||||
import { DashboardInsightsContext } from '../../../pages/dashboards/dashboard-page/components/dashboards-content/components/dashboard-inisghts/DashboardInsightsContext'
|
||||
import { getTrackingTypeByInsightType, useCodeInsightViewPings } from '../../../pings'
|
||||
import {
|
||||
CategoricalBasedChartTypes,
|
||||
CategoricalChart,
|
||||
InsightCard,
|
||||
InsightCardBanner,
|
||||
InsightCardHeader,
|
||||
InsightCardLegend,
|
||||
InsightCardLoading,
|
||||
SeriesBasedChartTypes,
|
||||
SeriesChart,
|
||||
} from '../../views'
|
||||
import { useInsightData } from '../hooks/use-insight-data'
|
||||
|
||||
import { InsightContextMenu } from './insight-context-menu/InsightContextMenu'
|
||||
|
||||
interface BuiltInInsightProps extends TelemetryProps, React.HTMLAttributes<HTMLElement> {
|
||||
insight: SearchRuntimeBasedInsight | LangStatsInsight
|
||||
innerRef: Ref<HTMLElement>
|
||||
resizing: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Historically we had a few insights that were worked via extension API
|
||||
* search-based, code-stats insight
|
||||
*
|
||||
* This component renders insight card that works almost like before with extensions
|
||||
* Component sends FE network request to get and process information but does that in
|
||||
* main work thread instead of using Extension API.
|
||||
*/
|
||||
export function BuiltInInsight(props: BuiltInInsightProps): React.ReactElement {
|
||||
const { insight, resizing, telemetryService, innerRef, ...otherProps } = props
|
||||
const { getBuiltInInsightData } = useContext(CodeInsightsBackendContext)
|
||||
const { dashboard } = useContext(DashboardInsightsContext)
|
||||
|
||||
const insightCardReference = useRef<HTMLDivElement>(null)
|
||||
const mergedInsightCardReference = useMergeRefs([insightCardReference, innerRef])
|
||||
|
||||
const cachedInsight = useDeepMemo(insight)
|
||||
|
||||
const { state, isVisible } = useInsightData(
|
||||
useMemo(() => () => getBuiltInInsightData({ insight: cachedInsight }), [getBuiltInInsightData, cachedInsight]),
|
||||
insightCardReference
|
||||
)
|
||||
|
||||
// Visual line chart settings
|
||||
const [zeroYAxisMin, setZeroYAxisMin] = useState(false)
|
||||
const { delete: handleDelete, loading: isDeleting } = useDeleteInsight()
|
||||
const { remove: handleRemove, loading: isRemoving } = useRemoveInsightFromDashboard()
|
||||
|
||||
const { trackDatumClicks, trackMouseLeave, trackMouseEnter } = useCodeInsightViewPings({
|
||||
telemetryService,
|
||||
insightType: getTrackingTypeByInsightType(insight.type),
|
||||
})
|
||||
|
||||
return (
|
||||
<InsightCard
|
||||
{...otherProps}
|
||||
ref={mergedInsightCardReference}
|
||||
data-testid={`insight-card.${insight.id}`}
|
||||
onMouseEnter={trackMouseEnter}
|
||||
onMouseLeave={trackMouseLeave}
|
||||
>
|
||||
<InsightCardHeader title={insight.title}>
|
||||
{isVisible && (
|
||||
<InsightContextMenu
|
||||
insight={insight}
|
||||
dashboard={dashboard}
|
||||
menuButtonClassName="ml-1 d-inline-flex"
|
||||
zeroYAxisMin={zeroYAxisMin}
|
||||
onToggleZeroYAxisMin={() => setZeroYAxisMin(!zeroYAxisMin)}
|
||||
onRemoveFromDashboard={dashboard => handleRemove({ insight, dashboard })}
|
||||
onDelete={() => handleDelete(insight)}
|
||||
/>
|
||||
)}
|
||||
</InsightCardHeader>
|
||||
{resizing ? (
|
||||
<InsightCardBanner>Resizing</InsightCardBanner>
|
||||
) : state.status === LazyQueryStatus.Loading || isDeleting || !isVisible ? (
|
||||
<InsightCardLoading>{isDeleting ? 'Deleting code insight' : 'Loading code insight'}</InsightCardLoading>
|
||||
) : isRemoving ? (
|
||||
<InsightCardLoading>Removing insight from the dashboard</InsightCardLoading>
|
||||
) : state.status === LazyQueryStatus.Error ? (
|
||||
<ErrorAlert error={state.error} />
|
||||
) : (
|
||||
<>
|
||||
<ParentSize>
|
||||
{parent =>
|
||||
state.data.type === InsightContentType.Series ? (
|
||||
<SeriesChart
|
||||
type={SeriesBasedChartTypes.Line}
|
||||
width={parent.width}
|
||||
height={parent.height}
|
||||
zeroYAxisMin={zeroYAxisMin}
|
||||
locked={insight.isFrozen}
|
||||
onDatumClick={trackDatumClicks}
|
||||
{...state.data.content}
|
||||
/>
|
||||
) : (
|
||||
<CategoricalChart
|
||||
type={CategoricalBasedChartTypes.Pie}
|
||||
width={parent.width}
|
||||
height={parent.height}
|
||||
locked={insight.isFrozen}
|
||||
onDatumLinkClick={trackDatumClicks}
|
||||
{...state.data.content}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</ParentSize>
|
||||
{state.data.type === InsightContentType.Series && (
|
||||
<InsightCardLegend series={state.data.content.series} className="mt-3" />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{
|
||||
// Passing children props explicitly to render any top-level content like
|
||||
// resize-handler from the react-grid-layout library
|
||||
isVisible && otherProps.children
|
||||
}
|
||||
</InsightCard>
|
||||
)
|
||||
}
|
||||
@ -2,9 +2,10 @@ import React, { forwardRef } from 'react'
|
||||
|
||||
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
|
||||
import { Insight, isBackendInsight } from '../../../../core/types'
|
||||
import { BackendInsightView } from '../backend-insight/BackendInsight'
|
||||
import { BuiltInInsight } from '../built-in-insight/BuiltInInsight'
|
||||
import { Insight, isBackendInsight } from '../../../core'
|
||||
|
||||
import { BackendInsightView } from './backend-insight/BackendInsight'
|
||||
import { BuiltInInsight } from './BuiltInInsight'
|
||||
|
||||
export interface SmartInsightProps extends TelemetryProps, React.HTMLAttributes<HTMLElement> {
|
||||
insight: Insight
|
||||
@ -1,24 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import classNames from 'classnames'
|
||||
import ProgressWrench from 'mdi-react/ProgressWrenchIcon'
|
||||
|
||||
import { AlertOverlay } from '../../../alert-overlay/AlertOverlay'
|
||||
|
||||
interface BackendAlertOverLayProps {
|
||||
isFetchingHistoricalData?: boolean
|
||||
hasNoData: boolean
|
||||
}
|
||||
export const BackendAlertOverlay: React.FunctionComponent<BackendAlertOverLayProps> = ({
|
||||
isFetchingHistoricalData,
|
||||
hasNoData,
|
||||
}) =>
|
||||
isFetchingHistoricalData ? (
|
||||
<AlertOverlay
|
||||
title="This insight is still being processed"
|
||||
description="Datapoints shown may be undercounted."
|
||||
icon={<ProgressWrench className={classNames('mb-3')} size={33} />}
|
||||
/>
|
||||
) : hasNoData ? (
|
||||
<AlertOverlay title="No data to display" description="We couldn’t find any matches for this insight." />
|
||||
) : null
|
||||
@ -1,14 +1,14 @@
|
||||
import React from 'react'
|
||||
|
||||
import { Meta, Story } from '@storybook/react'
|
||||
import { of, throwError } from 'rxjs'
|
||||
import { Observable, of, throwError } from 'rxjs'
|
||||
import { delay } from 'rxjs/operators'
|
||||
|
||||
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
|
||||
import { WebStory } from '../../../../../../components/WebStory'
|
||||
import { LINE_CHART_CONTENT_MOCK, LINE_CHART_CONTENT_MOCK_EMPTY } from '../../../../../../views/mocks/charts-content'
|
||||
import { CodeInsightsBackendStoryMock } from '../../../../CodeInsightsBackendStoryMock'
|
||||
import { BackendInsightData, SearchBackendBasedInsight, SeriesChartContent } from '../../../../core'
|
||||
import { InsightInProcessError } from '../../../../core/backend/utils/errors'
|
||||
import {
|
||||
BackendInsight as BackendInsightType,
|
||||
@ -16,7 +16,6 @@ import {
|
||||
InsightType,
|
||||
isCaptureGroupInsight,
|
||||
} from '../../../../core/types'
|
||||
import { SearchBackendBasedInsight } from '../../../../core/types/insight/types/search-insight'
|
||||
|
||||
import { BackendInsightView } from './BackendInsight'
|
||||
|
||||
@ -28,24 +27,72 @@ const defaultStory: Meta = {
|
||||
export default defaultStory
|
||||
|
||||
const INSIGHT_CONFIGURATION_MOCK: SearchBackendBasedInsight = {
|
||||
title: 'Mock Backend Insight',
|
||||
series: [],
|
||||
executionType: InsightExecutionType.Backend,
|
||||
type: InsightType.SearchBased,
|
||||
id: 'searchInsights.insight.mock_backend_insight_id',
|
||||
title: 'Backend Insight Mock',
|
||||
series: [],
|
||||
type: InsightType.SearchBased,
|
||||
executionType: InsightExecutionType.Backend,
|
||||
step: { weeks: 2 },
|
||||
filters: { excludeRepoRegexp: '', includeRepoRegexp: '' },
|
||||
dashboardReferenceCount: 0,
|
||||
isFrozen: false,
|
||||
}
|
||||
|
||||
interface BackendInsightDatum {
|
||||
x: number
|
||||
a: number
|
||||
b: number
|
||||
linkA: string
|
||||
}
|
||||
|
||||
const LINE_CHART_CONTENT_MOCK: SeriesChartContent<BackendInsightDatum> = {
|
||||
data: [
|
||||
{ x: 1588965700286 - 4 * 24 * 60 * 60 * 1000, a: 4000, b: 15000, linkA: '#A:1st_data_point' },
|
||||
{ x: 1588965700286 - 3 * 24 * 60 * 60 * 1000, a: 4000, b: 26000, linkA: '#A:2st_data_point' },
|
||||
{ x: 1588965700286 - 2 * 24 * 60 * 60 * 1000, a: 5600, b: 20000, linkA: '#A:3rd_data_point' },
|
||||
{ x: 1588965700286 - 1 * 24 * 60 * 60 * 1000, a: 9800, b: 19000, linkA: '#A:4th_data_point' },
|
||||
{ x: 1588965700286, a: 12300, b: 17000, linkA: '#A:5th_data_point' },
|
||||
],
|
||||
getXValue: datum => new Date(datum.x),
|
||||
series: [
|
||||
{
|
||||
dataKey: 'a',
|
||||
name: 'A metric',
|
||||
color: 'var(--warning)',
|
||||
getLinkURL: datum => datum.linkA,
|
||||
},
|
||||
{
|
||||
dataKey: 'b',
|
||||
name: 'B metric',
|
||||
color: 'var(--warning)',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const LINE_CHART_CONTENT_MOCK_EMPTY: SeriesChartContent<BackendInsightDatum> = {
|
||||
data: [],
|
||||
getXValue: datum => new Date(datum.x),
|
||||
series: [
|
||||
{
|
||||
dataKey: 'a',
|
||||
name: 'A metric',
|
||||
color: 'var(--warning)',
|
||||
},
|
||||
{
|
||||
dataKey: 'b',
|
||||
name: 'B metric',
|
||||
color: 'var(--warning)',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const mockInsightAPI = ({
|
||||
isFetchingHistoricalData = false,
|
||||
delayAmount = 0,
|
||||
throwProcessingError = false,
|
||||
hasData = true,
|
||||
} = {}) => ({
|
||||
getBackendInsightData: (insight: BackendInsightType) => {
|
||||
getBackendInsightData: (insight: BackendInsightType): Observable<BackendInsightData> => {
|
||||
if (isCaptureGroupInsight(insight)) {
|
||||
throw new Error('This demo does not support capture group insight')
|
||||
}
|
||||
@ -55,13 +102,8 @@ const mockInsightAPI = ({
|
||||
}
|
||||
|
||||
return of({
|
||||
id: insight.id,
|
||||
view: {
|
||||
title: 'Backend Insight Mock',
|
||||
subtitle: 'Backend insight description text',
|
||||
content: [hasData ? LINE_CHART_CONTENT_MOCK : LINE_CHART_CONTENT_MOCK_EMPTY],
|
||||
isFetchingHistoricalData,
|
||||
},
|
||||
content: hasData ? LINE_CHART_CONTENT_MOCK : LINE_CHART_CONTENT_MOCK_EMPTY,
|
||||
isFetchingHistoricalData,
|
||||
}).pipe(delay(delayAmount))
|
||||
},
|
||||
})
|
||||
@ -107,5 +149,16 @@ export const BackendInsight: Story = () => (
|
||||
<TestBackendInsight />
|
||||
</CodeInsightsBackendStoryMock>
|
||||
</article>
|
||||
<article className="mt-3">
|
||||
<h2>Locked Card insight</h2>
|
||||
<CodeInsightsBackendStoryMock mocks={mockInsightAPI()}>
|
||||
<BackendInsightView
|
||||
style={{ width: 400, height: 400 }}
|
||||
insight={{ ...INSIGHT_CONFIGURATION_MOCK, isFrozen: true }}
|
||||
telemetryService={NOOP_TELEMETRY_SERVICE}
|
||||
innerRef={() => {}}
|
||||
/>
|
||||
</CodeInsightsBackendStoryMock>
|
||||
</article>
|
||||
</section>
|
||||
)
|
||||
|
||||
@ -3,28 +3,28 @@ import React, { Ref, useCallback, useContext, useRef, useState } from 'react'
|
||||
import classNames from 'classnames'
|
||||
import { useMergeRefs } from 'use-callback-ref'
|
||||
|
||||
import { asError, isErrorLike } from '@sourcegraph/common'
|
||||
import { asError } from '@sourcegraph/common'
|
||||
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { useDebounce, Alert } from '@sourcegraph/wildcard'
|
||||
import { useDebounce, useDeepMemo } from '@sourcegraph/wildcard'
|
||||
|
||||
import * as View from '../../../../../../views'
|
||||
import { LineChartSettingsContext } from '../../../../../../views'
|
||||
import { LockedChart } from '../../../../../../views/components/view/content/chart-view-content/charts/locked/LockedChart'
|
||||
import { CodeInsightsBackendContext, BackendInsight, InsightFilters } from '../../../../core'
|
||||
import { InsightInProcessError } from '../../../../core/backend/utils/errors'
|
||||
import { BackendInsight, CodeInsightsBackendContext, InsightFilters } from '../../../../core'
|
||||
import { useDeleteInsight } from '../../../../hooks/use-delete-insight'
|
||||
import { useDistinctValue } from '../../../../hooks/use-distinct-value'
|
||||
import { LazyQueryStatus } from '../../../../hooks/use-parallel-requests/use-parallel-request'
|
||||
import { useRemoveInsightFromDashboard } from '../../../../hooks/use-remove-insight'
|
||||
import { DashboardInsightsContext } from '../../../../pages/dashboards/dashboard-page/components/dashboards-content/components/dashboard-inisghts/DashboardInsightsContext'
|
||||
import { useCodeInsightViewPings, getTrackingTypeByInsightType } from '../../../../pings'
|
||||
import { getTrackingTypeByInsightType, useCodeInsightViewPings } from '../../../../pings'
|
||||
import { FORM_ERROR, SubmissionErrors } from '../../../form/hooks/useForm'
|
||||
import { InsightCard, InsightCardBanner, InsightCardHeader, InsightCardLoading } from '../../../views'
|
||||
import { useInsightData } from '../../hooks/use-insight-data'
|
||||
import { InsightContextMenu } from '../insight-context-menu/InsightContextMenu'
|
||||
|
||||
import { BackendAlertOverlay } from './BackendAlertOverlay'
|
||||
import { DrillDownFiltersAction } from './components/drill-down-filters-action/DrillDownFiltersPanel'
|
||||
import { DrillDownInsightCreationFormValues } from './components/drill-down-filters-panel/components/drill-down-insight-creation-form/DrillDownInsightCreationForm'
|
||||
import { EMPTY_DRILLDOWN_FILTERS } from './components/drill-down-filters-panel/utils'
|
||||
import {
|
||||
BackendInsightErrorAlert,
|
||||
EMPTY_DRILLDOWN_FILTERS,
|
||||
DrillDownFiltersPopover,
|
||||
DrillDownInsightCreationFormValues,
|
||||
BackendInsightChart,
|
||||
} from './components'
|
||||
|
||||
import styles from './BackendInsight.module.scss'
|
||||
|
||||
@ -54,7 +54,7 @@ export const BackendInsightView: React.FunctionComponent<BackendInsightProps> =
|
||||
// Use deep copy check in case if a setting subject has re-created copy of
|
||||
// the insight config with same structure and values. To avoid insight data
|
||||
// re-fetching.
|
||||
const cachedInsight = useDistinctValue(insight)
|
||||
const cachedInsight = useDeepMemo(insight)
|
||||
|
||||
// Original insight filters values that are stored in setting subject with insight
|
||||
// configuration object, They are updated whenever the user clicks update/save button
|
||||
@ -66,10 +66,10 @@ export const BackendInsightView: React.FunctionComponent<BackendInsightProps> =
|
||||
// filter value in filters fields.
|
||||
const [filters, setFilters] = useState<InsightFilters>(originalInsightFilters)
|
||||
const [isFiltersOpen, setIsFiltersOpen] = useState(false)
|
||||
const debouncedFilters = useDebounce(useDistinctValue<InsightFilters>(filters), 500)
|
||||
const debouncedFilters = useDebounce(useDeepMemo<InsightFilters>(filters), 500)
|
||||
|
||||
// Loading the insight backend data
|
||||
const { data, loading, error, isVisible } = useInsightData(
|
||||
const { state, isVisible } = useInsightData(
|
||||
useCallback(
|
||||
() =>
|
||||
getBackendInsightData({
|
||||
@ -139,14 +139,18 @@ export const BackendInsightView: React.FunctionComponent<BackendInsightProps> =
|
||||
})
|
||||
|
||||
return (
|
||||
<View.Root
|
||||
<InsightCard
|
||||
{...otherProps}
|
||||
title={insight.title}
|
||||
innerRef={mergedInsightCardReference}
|
||||
actions={
|
||||
isVisible && (
|
||||
ref={mergedInsightCardReference}
|
||||
data-testid={`insight-card.${insight.id}`}
|
||||
className={classNames(otherProps.className, { [styles.cardWithFilters]: isFiltersOpen })}
|
||||
onMouseEnter={trackMouseEnter}
|
||||
onMouseLeave={trackMouseLeave}
|
||||
>
|
||||
<InsightCardHeader title={insight.title}>
|
||||
{isVisible && (
|
||||
<>
|
||||
<DrillDownFiltersAction
|
||||
<DrillDownFiltersPopover
|
||||
isOpen={isFiltersOpen}
|
||||
popoverTargetRef={insightCardReference}
|
||||
initialFiltersValue={filters}
|
||||
@ -166,50 +170,25 @@ export const BackendInsightView: React.FunctionComponent<BackendInsightProps> =
|
||||
onDelete={() => handleDelete(insight)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
data-testid={`insight-card.${insight.id}`}
|
||||
className={classNames(otherProps.className, { [styles.cardWithFilters]: isFiltersOpen })}
|
||||
onMouseEnter={trackMouseEnter}
|
||||
onMouseLeave={trackMouseLeave}
|
||||
>
|
||||
)}
|
||||
</InsightCardHeader>
|
||||
|
||||
{resizing ? (
|
||||
<View.Banner>Resizing</View.Banner>
|
||||
) : loading || isDeleting || !isVisible ? (
|
||||
<View.LoadingContent text={isDeleting ? 'Deleting code insight' : 'Loading code insight'} />
|
||||
<InsightCardBanner>Resizing</InsightCardBanner>
|
||||
) : state.status === LazyQueryStatus.Loading || isDeleting || !isVisible ? (
|
||||
<InsightCardLoading>{isDeleting ? 'Deleting code insight' : 'Loading code insight'}</InsightCardLoading>
|
||||
) : isRemoving ? (
|
||||
<View.LoadingContent text="Removing insight from the dashboard" />
|
||||
) : isErrorLike(error) ? (
|
||||
<View.ErrorContent error={error} title={insight.id}>
|
||||
{error instanceof InsightInProcessError ? (
|
||||
<Alert className="m-0" variant="info">
|
||||
{error.message}
|
||||
</Alert>
|
||||
) : null}
|
||||
</View.ErrorContent>
|
||||
) : insight.isFrozen ? (
|
||||
<LockedChart />
|
||||
<InsightCardLoading>Removing insight from the dashboard</InsightCardLoading>
|
||||
) : state.status === LazyQueryStatus.Error ? (
|
||||
<BackendInsightErrorAlert error={state.error} />
|
||||
) : (
|
||||
data && (
|
||||
<LineChartSettingsContext.Provider value={{ zeroYAxisMin }}>
|
||||
<View.Content
|
||||
content={data.view.content}
|
||||
alert={
|
||||
<BackendAlertOverlay
|
||||
hasNoData={!data.view.content.some(({ data }) => data.length > 0)}
|
||||
isFetchingHistoricalData={data.view.isFetchingHistoricalData}
|
||||
/>
|
||||
}
|
||||
onDatumLinkClick={trackDatumClicks}
|
||||
/>
|
||||
</LineChartSettingsContext.Provider>
|
||||
)
|
||||
<BackendInsightChart {...state.data} locked={insight.isFrozen} onDatumClick={trackDatumClicks} />
|
||||
)}
|
||||
{
|
||||
// Passing children props explicitly to render any top-level content like
|
||||
// resize-handler from the react-grid-layout library
|
||||
isVisible && otherProps.children
|
||||
}
|
||||
</View.Root>
|
||||
</InsightCard>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
.gradient {
|
||||
.alert-container {
|
||||
position: relative;
|
||||
|
||||
:global(.theme-dark) & {
|
||||
background: linear-gradient(180deg, rgba(25, 27, 37, 0.6) 0%, #191b25 51.56%, rgba(25, 27, 37, 0.6) 100%);
|
||||
}
|
||||
@ -8,6 +10,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
.alert-content {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--gray-06);
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
import React from 'react'
|
||||
|
||||
import classNames from 'classnames'
|
||||
import ProgressWrench from 'mdi-react/ProgressWrenchIcon'
|
||||
|
||||
import { ErrorAlert } from '@sourcegraph/branded/src/components/alerts'
|
||||
import { ErrorLike } from '@sourcegraph/common'
|
||||
import { Alert } from '@sourcegraph/wildcard'
|
||||
|
||||
import { InsightInProcessError } from '../../../../../../core/backend/utils/errors'
|
||||
|
||||
import styles from './BackendInsightAlerts.module.scss'
|
||||
|
||||
interface BackendAlertOverLayProps {
|
||||
isFetchingHistoricalData: boolean
|
||||
hasNoData: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const BackendAlertOverlay: React.FunctionComponent<BackendAlertOverLayProps> = props => {
|
||||
const { isFetchingHistoricalData, hasNoData, className } = props
|
||||
|
||||
if (isFetchingHistoricalData) {
|
||||
return (
|
||||
<AlertOverlay
|
||||
title="This insight is still being processed"
|
||||
description="Datapoints shown may be undercounted."
|
||||
icon={<ProgressWrench className={classNames('mb-3')} size={33} />}
|
||||
className={className}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (hasNoData) {
|
||||
return (
|
||||
<AlertOverlay
|
||||
title="No data to display"
|
||||
description="We couldn’t find any matches for this insight."
|
||||
className={className}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export interface AlertOverlayProps {
|
||||
title: string
|
||||
description: string
|
||||
icon?: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const AlertOverlay: React.FunctionComponent<AlertOverlayProps> = props => {
|
||||
const { title, description, icon, className } = props
|
||||
|
||||
return (
|
||||
<div className={classNames(className, styles.alertContainer)}>
|
||||
<div className={styles.alertContent}>
|
||||
{icon && <div className={styles.icon}>{icon}</div>}
|
||||
<h4 className={styles.title}>{title}</h4>
|
||||
<small className={styles.description}>{description}</small>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface BackendInsightErrorAlertProps {
|
||||
error: ErrorLike
|
||||
}
|
||||
|
||||
export const BackendInsightErrorAlert: React.FunctionComponent<BackendInsightErrorAlertProps> = props =>
|
||||
props.error instanceof InsightInProcessError ? (
|
||||
<Alert variant="info">{props.error.message}</Alert>
|
||||
) : (
|
||||
<ErrorAlert error={props.error} />
|
||||
)
|
||||
@ -0,0 +1,69 @@
|
||||
.chart {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(10rem, 30%);
|
||||
grid-template-rows: auto 1fr;
|
||||
gap: 0.75rem;
|
||||
grid-template-areas:
|
||||
'chart chart'
|
||||
'chart chart'
|
||||
'legend legend';
|
||||
|
||||
&--horizontal {
|
||||
grid-template-areas:
|
||||
'chart legend'
|
||||
'chart legend';
|
||||
|
||||
.legend-list {
|
||||
flex-wrap: nowrap;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.responsive-container {
|
||||
grid-area: chart;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
.alert-overlay {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.legend-list-container {
|
||||
grid-area: legend;
|
||||
overflow-x: hidden;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.25rem;
|
||||
height: 0.25rem;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 3px;
|
||||
box-shadow: inset 0 0 6px var(--text-muted);
|
||||
}
|
||||
|
||||
@-moz-document url-prefix('') {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--text-muted);
|
||||
}
|
||||
}
|
||||
|
||||
.legend-list-item {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.alert-overlay {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
@ -0,0 +1,102 @@
|
||||
import React from 'react'
|
||||
|
||||
import { ParentSize } from '@visx/responsive'
|
||||
import classNames from 'classnames'
|
||||
import useResizeObserver from 'use-resize-observer'
|
||||
|
||||
import { useDebounce } from '@sourcegraph/wildcard'
|
||||
|
||||
import { getLineColor, LegendItem, LegendList } from '../../../../../../../../charts'
|
||||
import { ScrollBox } from '../../../../../../../../views/components/view/content/chart-view-content/charts/line/components/scroll-box/ScrollBox'
|
||||
import { BackendInsightData } from '../../../../../../core'
|
||||
import { SeriesBasedChartTypes, SeriesChart } from '../../../../../views'
|
||||
import { BackendAlertOverlay } from '../backend-insight-alerts/BackendInsightAlerts'
|
||||
|
||||
import styles from './BackendInsightChart.module.scss'
|
||||
|
||||
/**
|
||||
* 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 you 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 N series
|
||||
*/
|
||||
export const MINIMAL_SERIES_FOR_ASIDE_LEGEND = 3
|
||||
|
||||
interface BackendInsightChartProps<Datum> extends BackendInsightData {
|
||||
locked: boolean
|
||||
className?: string
|
||||
onDatumClick: () => void
|
||||
}
|
||||
|
||||
export function BackendInsightChart<Datum>(props: BackendInsightChartProps<Datum>): React.ReactElement {
|
||||
const { locked, isFetchingHistoricalData, content, className, onDatumClick } = props
|
||||
const { ref, width = 0 } = useDebounce(useResizeObserver(), 100)
|
||||
|
||||
const hasViewManySeries = content.series.length > MINIMAL_SERIES_FOR_ASIDE_LEGEND
|
||||
const hasEnoughXSpace = width >= MINIMAL_HORIZONTAL_LAYOUT_WIDTH
|
||||
|
||||
const isHorizontalMode = hasViewManySeries && hasEnoughXSpace
|
||||
|
||||
return (
|
||||
<div ref={ref} className={classNames(className, styles.chart, { [styles.chartHorizontal]: isHorizontalMode })}>
|
||||
{width && (
|
||||
<>
|
||||
<ParentSize
|
||||
debounceTime={0}
|
||||
enableDebounceLeadingCall={true}
|
||||
className={styles.responsiveContainer}
|
||||
>
|
||||
{parent => (
|
||||
<>
|
||||
<BackendAlertOverlay
|
||||
hasNoData={content.data.length === 0}
|
||||
isFetchingHistoricalData={isFetchingHistoricalData}
|
||||
className={styles.alertOverlay}
|
||||
/>
|
||||
|
||||
<SeriesChart
|
||||
type={SeriesBasedChartTypes.Line}
|
||||
width={parent.width}
|
||||
height={parent.height}
|
||||
locked={locked}
|
||||
onDatumClick={onDatumClick}
|
||||
{...content}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</ParentSize>
|
||||
|
||||
<ScrollBox className={styles.legendListContainer}>
|
||||
<LegendList className={styles.legendList}>
|
||||
{content.series.map(series => (
|
||||
<LegendItem
|
||||
key={series.dataKey as string}
|
||||
color={getLineColor(series)}
|
||||
name={series.name}
|
||||
className={styles.legendListItem}
|
||||
/>
|
||||
))}
|
||||
</LegendList>
|
||||
</ScrollBox>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import { InsightFilters } from '../../../../../../core/types'
|
||||
import { InsightFilters } from '../../../../../../core'
|
||||
import { FormChangeEvent, SubmissionResult } from '../../../../../form/hooks/useForm'
|
||||
|
||||
import {
|
||||
|
||||
@ -5,15 +5,15 @@ import FilterOutlineIcon from 'mdi-react/FilterOutlineIcon'
|
||||
|
||||
import { Button, Popover, PopoverContent, PopoverTrigger, Position } from '@sourcegraph/wildcard'
|
||||
|
||||
import { InsightFilters } from '../../../../../../core/types'
|
||||
import { InsightFilters } from '../../../../../../core'
|
||||
import { SubmissionResult } from '../../../../../form/hooks/useForm'
|
||||
import { hasActiveFilters } from '../drill-down-filters-panel/components/drill-down-filters-form/DrillDownFiltersForm'
|
||||
import { DrillDownInsightCreationFormValues } from '../drill-down-filters-panel/components/drill-down-insight-creation-form/DrillDownInsightCreationForm'
|
||||
import { DrillDownFiltersPanel } from '../drill-down-filters-panel/DrillDownFiltersPanel'
|
||||
|
||||
import styles from './DrillDownFiltersPanel.module.scss'
|
||||
import styles from './DrillDownFiltersPopover.module.scss'
|
||||
|
||||
interface DrillDownFiltersProps {
|
||||
interface DrillDownFiltersPopoverProps {
|
||||
isOpen: boolean
|
||||
initialFiltersValue: InsightFilters
|
||||
originalFiltersValue: InsightFilters
|
||||
@ -28,7 +28,7 @@ interface DrillDownFiltersProps {
|
||||
// the filter panel should not trigger react-grid-layout events.
|
||||
const handleMouseDown: DOMAttributes<HTMLElement>['onMouseDown'] = event => event.stopPropagation()
|
||||
|
||||
export const DrillDownFiltersAction: React.FunctionComponent<DrillDownFiltersProps> = props => {
|
||||
export const DrillDownFiltersPopover: React.FunctionComponent<DrillDownFiltersPopoverProps> = props => {
|
||||
const {
|
||||
isOpen,
|
||||
popoverTargetRef,
|
||||
@ -0,0 +1,8 @@
|
||||
export type { DrillDownInsightCreationFormValues } from './drill-down-filters-panel/components/drill-down-insight-creation-form/DrillDownInsightCreationForm'
|
||||
|
||||
export { DrillDownFiltersPanel } from './drill-down-filters-panel/DrillDownFiltersPanel'
|
||||
export { DrillDownFiltersPopover } from './drill-down-filters-popover/DrillDownFiltersPopover'
|
||||
export { EMPTY_DRILLDOWN_FILTERS } from './drill-down-filters-panel/utils'
|
||||
|
||||
export { BackendInsightErrorAlert, BackendAlertOverlay } from './backend-insight-alerts/BackendInsightAlerts'
|
||||
export { BackendInsightChart } from './backend-insight-chart/BackendInsightChart'
|
||||
@ -1,106 +0,0 @@
|
||||
import React, { Ref, useContext, useMemo, useRef, useState } from 'react'
|
||||
|
||||
import { useMergeRefs } from 'use-callback-ref'
|
||||
|
||||
import { isErrorLike } from '@sourcegraph/common'
|
||||
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
|
||||
import * as View from '../../../../../../views'
|
||||
import { LineChartSettingsContext } from '../../../../../../views'
|
||||
import { CodeInsightsBackendContext, LangStatsInsight, SearchRuntimeBasedInsight } from '../../../../core'
|
||||
import { useDeleteInsight } from '../../../../hooks/use-delete-insight'
|
||||
import { useDistinctValue } from '../../../../hooks/use-distinct-value'
|
||||
import { useRemoveInsightFromDashboard } from '../../../../hooks/use-remove-insight'
|
||||
import { DashboardInsightsContext } from '../../../../pages/dashboards/dashboard-page/components/dashboards-content/components/dashboard-inisghts/DashboardInsightsContext'
|
||||
import { useCodeInsightViewPings, getTrackingTypeByInsightType } from '../../../../pings'
|
||||
import { useInsightData } from '../../hooks/use-insight-data'
|
||||
import { InsightContextMenu } from '../insight-context-menu/InsightContextMenu'
|
||||
|
||||
interface BuiltInInsightProps extends TelemetryProps, React.HTMLAttributes<HTMLElement> {
|
||||
insight: SearchRuntimeBasedInsight | LangStatsInsight
|
||||
innerRef: Ref<HTMLElement>
|
||||
resizing: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Historically we had a few insights that were worked via extension API
|
||||
* search-based, code-stats insight
|
||||
*
|
||||
* This component renders insight card that works almost like before with extensions
|
||||
* Component sends FE network request to get and process information but does that in
|
||||
* main work thread instead of using Extension API.
|
||||
*/
|
||||
export function BuiltInInsight(props: BuiltInInsightProps): React.ReactElement {
|
||||
const { insight, resizing, telemetryService, innerRef, ...otherProps } = props
|
||||
const { getBuiltInInsightData } = useContext(CodeInsightsBackendContext)
|
||||
const { dashboard } = useContext(DashboardInsightsContext)
|
||||
|
||||
const insightCardReference = useRef<HTMLDivElement>(null)
|
||||
const mergedInsightCardReference = useMergeRefs([insightCardReference, innerRef])
|
||||
|
||||
const cachedInsight = useDistinctValue(insight)
|
||||
|
||||
const { data, loading, isVisible } = useInsightData(
|
||||
useMemo(() => () => getBuiltInInsightData({ insight: cachedInsight }), [getBuiltInInsightData, cachedInsight]),
|
||||
insightCardReference
|
||||
)
|
||||
|
||||
// Visual line chart settings
|
||||
const [zeroYAxisMin, setZeroYAxisMin] = useState(false)
|
||||
const { delete: handleDelete, loading: isDeleting } = useDeleteInsight()
|
||||
const { remove: handleRemove, loading: isRemoving } = useRemoveInsightFromDashboard()
|
||||
|
||||
const { trackDatumClicks, trackMouseLeave, trackMouseEnter } = useCodeInsightViewPings({
|
||||
telemetryService,
|
||||
insightType: getTrackingTypeByInsightType(insight.type),
|
||||
})
|
||||
|
||||
return (
|
||||
<View.Root
|
||||
{...otherProps}
|
||||
innerRef={mergedInsightCardReference}
|
||||
title={insight.title}
|
||||
actions={
|
||||
isVisible && (
|
||||
<InsightContextMenu
|
||||
insight={insight}
|
||||
dashboard={dashboard}
|
||||
menuButtonClassName="ml-1 d-inline-flex"
|
||||
zeroYAxisMin={zeroYAxisMin}
|
||||
onToggleZeroYAxisMin={() => setZeroYAxisMin(!zeroYAxisMin)}
|
||||
onRemoveFromDashboard={dashboard => handleRemove({ insight, dashboard })}
|
||||
onDelete={() => handleDelete(insight)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
data-testid={`insight-card.${insight.id}`}
|
||||
onMouseEnter={trackMouseEnter}
|
||||
onMouseLeave={trackMouseLeave}
|
||||
>
|
||||
{resizing ? (
|
||||
<View.Banner>Resizing</View.Banner>
|
||||
) : !data || loading || isDeleting || !isVisible ? (
|
||||
<View.LoadingContent text={isDeleting ? 'Deleting code insight' : 'Loading code insight'} />
|
||||
) : isRemoving ? (
|
||||
<View.LoadingContent text="Removing insight from the dashboard" />
|
||||
) : isErrorLike(data.view) ? (
|
||||
<View.ErrorContent error={data.view} title={insight.id} />
|
||||
) : (
|
||||
data.view && (
|
||||
<LineChartSettingsContext.Provider value={{ zeroYAxisMin }}>
|
||||
<View.Content
|
||||
content={data.view.content}
|
||||
onDatumLinkClick={trackDatumClicks}
|
||||
locked={insight.isFrozen}
|
||||
/>
|
||||
</LineChartSettingsContext.Provider>
|
||||
)
|
||||
)}
|
||||
{
|
||||
// Passing children props explicitly to render any top-level content like
|
||||
// resize-handler from the react-grid-layout library
|
||||
isVisible && otherProps.children
|
||||
}
|
||||
</View.Root>
|
||||
)
|
||||
}
|
||||
@ -6,7 +6,7 @@ import DotsVerticalIcon from 'mdi-react/DotsVerticalIcon'
|
||||
|
||||
import { Link, Menu, MenuButton, MenuDivider, MenuItem, MenuLink, MenuList, Position } from '@sourcegraph/wildcard'
|
||||
|
||||
import { Insight, InsightDashboard, isVirtualDashboard } from '../../../../core/types'
|
||||
import { Insight, InsightDashboard, isVirtualDashboard } from '../../../../core'
|
||||
import { useUiFeatures } from '../../../../hooks/use-ui-features'
|
||||
|
||||
import styles from './InsightContextMenu.module.scss'
|
||||
|
||||
@ -1,19 +1,14 @@
|
||||
import { useContext, useEffect, useState } from 'react'
|
||||
import { RefObject, useContext, useEffect, useState } from 'react'
|
||||
|
||||
import { ObservableInput } from 'rxjs'
|
||||
|
||||
import { ErrorLike } from '@sourcegraph/common'
|
||||
|
||||
import { CodeInsightsBackendContext } from '../../../core/backend/code-insights-backend-context'
|
||||
import { CodeInsightsGqlBackend } from '../../../core/backend/gql-backend/code-insights-gql-backend'
|
||||
import { useLazyParallelRequest } from '../../../hooks/use-parallel-requests/use-parallel-request'
|
||||
import { CodeInsightsBackendContext, CodeInsightsGqlBackend } from '../../../core'
|
||||
import { LazyQueryState, useLazyParallelRequest } from '../../../hooks/use-parallel-requests/use-parallel-request'
|
||||
|
||||
export interface UseInsightDataResult<T> {
|
||||
data: T | undefined
|
||||
error: ErrorLike | undefined
|
||||
loading: boolean
|
||||
isVisible: boolean
|
||||
query: (request: () => ObservableInput<T>) => void
|
||||
state: LazyQueryState<T>
|
||||
}
|
||||
|
||||
/**
|
||||
@ -26,15 +21,15 @@ export interface UseInsightDataResult<T> {
|
||||
*/
|
||||
export function useInsightData<D>(
|
||||
request: () => ObservableInput<D>,
|
||||
reference: React.RefObject<HTMLElement>
|
||||
reference: RefObject<HTMLElement>
|
||||
): UseInsightDataResult<D> {
|
||||
const api = useContext(CodeInsightsBackendContext)
|
||||
const isGqlAPI = api instanceof CodeInsightsGqlBackend
|
||||
|
||||
const { data, loading, error, query } = useLazyParallelRequest<D>()
|
||||
const { state, query } = useLazyParallelRequest<D>()
|
||||
|
||||
// All non GQL API implementations do not support partial loading,
|
||||
// allowing insights fetching for these API whether insights are
|
||||
// allowing insights fetching for this API whether insights are
|
||||
// in a viewport or not.
|
||||
const [isVisible, setVisibility] = useState<boolean>(!isGqlAPI)
|
||||
const [hasIntersected, setHasIntersected] = useState<boolean>(!isGqlAPI)
|
||||
@ -77,5 +72,5 @@ export function useInsightData<D>(
|
||||
return () => observer.unobserve(element)
|
||||
}, [isGqlAPI, reference])
|
||||
|
||||
return { data, loading, error, isVisible, query }
|
||||
return { state, isVisible, query }
|
||||
}
|
||||
|
||||
@ -1,23 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
import { Link, LinkProps } from '@sourcegraph/wildcard'
|
||||
|
||||
export interface LinkWithQueryProps extends Omit<LinkProps, 'to'> {
|
||||
to: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders react router link component with query params preserving between route transitions.
|
||||
*/
|
||||
export const LinkWithQuery: React.FunctionComponent<LinkWithQueryProps> = props => {
|
||||
const { children, to, ...otherProps } = props
|
||||
const { search } = useLocation()
|
||||
|
||||
return (
|
||||
<Link to={to + search} {...otherProps}>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@ -5,6 +5,7 @@ import { useLocation } from 'react-router-dom'
|
||||
|
||||
import { Card, ForwardReferenceComponent, LoadingSpinner } from '@sourcegraph/wildcard'
|
||||
|
||||
import { getLineColor, LegendItem, LegendList, Series } from '../../../../../charts'
|
||||
import { ErrorBoundary } from '../../../../../components/ErrorBoundary'
|
||||
|
||||
import styles from './InsightCard.module.scss'
|
||||
@ -75,19 +76,38 @@ const InsightCardBanner: React.FunctionComponent<HTMLAttributes<HTMLDivElement>>
|
||||
</div>
|
||||
)
|
||||
|
||||
interface InsightCardLegendProps extends React.HTMLAttributes<HTMLUListElement> {
|
||||
series: Series<any>[]
|
||||
}
|
||||
|
||||
const InsightCardLegend: React.FunctionComponent<InsightCardLegendProps> = props => {
|
||||
const { series, ...attributes } = props
|
||||
|
||||
return (
|
||||
<LegendList {...attributes}>
|
||||
{series.map(series => (
|
||||
<LegendItem key={series.dataKey as string} color={getLineColor(series)} name={series.name} />
|
||||
))}
|
||||
</LegendList>
|
||||
)
|
||||
}
|
||||
|
||||
const Root = InsightCard
|
||||
const Header = InsightCardHeader
|
||||
const Loading = InsightCardLoading
|
||||
const Banner = InsightCardBanner
|
||||
const Legends = InsightCardLegend
|
||||
|
||||
export {
|
||||
InsightCard,
|
||||
InsightCardHeader,
|
||||
InsightCardLoading,
|
||||
InsightCardBanner,
|
||||
InsightCardLegend,
|
||||
// * as Card imports
|
||||
Root,
|
||||
Header,
|
||||
Loading,
|
||||
Banner,
|
||||
Legends,
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import React, { SVGProps } from 'react'
|
||||
|
||||
import { CategoricalLikeChart, PieChart } from '../../../../../../charts'
|
||||
import { CategoricalBasedChartTypes } from '../../types'
|
||||
import { LockedChart } from '../locked/LockedChart'
|
||||
|
||||
export interface CategoricalChartProps<Datum>
|
||||
extends CategoricalLikeChart<Datum>,
|
||||
@ -9,10 +10,15 @@ export interface CategoricalChartProps<Datum>
|
||||
type: CategoricalBasedChartTypes
|
||||
width: number
|
||||
height: number
|
||||
locked?: boolean
|
||||
}
|
||||
|
||||
export function CategoricalChart<Datum>(props: CategoricalChartProps<Datum>): React.ReactElement | null {
|
||||
const { type, ...otherProps } = props
|
||||
const { type, locked, ...otherProps } = props
|
||||
|
||||
if (locked) {
|
||||
return <LockedChart />
|
||||
}
|
||||
|
||||
if (type === CategoricalBasedChartTypes.Pie) {
|
||||
return <PieChart {...otherProps} />
|
||||
|
||||
@ -1,2 +1,5 @@
|
||||
export { SeriesChart } from './series/SeriesChart'
|
||||
export { CategoricalChart } from './categorical/CategoricalChart'
|
||||
|
||||
export type { SeriesChartProps } from './series/SeriesChart'
|
||||
export type { CategoricalChartProps } from './categorical/CategoricalChart'
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
margin: 0 2rem;
|
||||
border-top: solid 1px var(--border-color);
|
||||
border-bottom: solid 1px var(--border-color);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--icon-color);
|
||||
}
|
||||
|
||||
.banner {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
.banner span {
|
||||
color: var(--purple-03);
|
||||
text-transform: uppercase;
|
||||
padding-right: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
border-right: solid 1px var(--purple-01);
|
||||
}
|
||||
|
||||
.banner small {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
import React from 'react'
|
||||
|
||||
import classNames from 'classnames'
|
||||
import LockIcon from 'mdi-react/LockOutlineIcon'
|
||||
|
||||
import styles from './LockedChart.module.scss'
|
||||
|
||||
export const LockedChart: React.FunctionComponent<{ className?: string }> = ({ className }) => (
|
||||
<section className={classNames(styles.wrapper, className)}>
|
||||
<LockIcon size={40} />
|
||||
<div className={classNames(styles.banner)}>
|
||||
<span>Limited access</span>
|
||||
<small>Insight locked</small>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
@ -2,15 +2,22 @@ import React, { SVGProps } from 'react'
|
||||
|
||||
import { LineChart, SeriesLikeChart } from '../../../../../../charts'
|
||||
import { SeriesBasedChartTypes } from '../../types'
|
||||
import { LockedChart } from '../locked/LockedChart'
|
||||
|
||||
export interface SeriesChartProps<D> extends SeriesLikeChart<D>, Omit<SVGProps<SVGSVGElement>, 'type'> {
|
||||
type: SeriesBasedChartTypes
|
||||
width: number
|
||||
height: number
|
||||
zeroYAxisMin?: boolean
|
||||
locked?: boolean
|
||||
}
|
||||
|
||||
export function SeriesChart<Datum>(props: SeriesChartProps<Datum>): React.ReactElement {
|
||||
const { type, ...otherProps } = props
|
||||
const { type, locked, ...otherProps } = props
|
||||
|
||||
if (locked) {
|
||||
return <LockedChart />
|
||||
}
|
||||
|
||||
return <LineChart {...otherProps} />
|
||||
}
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
export { InsightCard, InsightCardHeader, InsightCardBanner, InsightCardLoading } from './card/InsightCard'
|
||||
export * from './card/InsightCard'
|
||||
|
||||
export { SeriesChart, CategoricalChart } from './chart'
|
||||
export { CategoricalBasedChartTypes, SeriesBasedChartTypes } from './types'
|
||||
|
||||
export type { SeriesChartProps, CategoricalChartProps } from './chart'
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { Duration } from 'date-fns'
|
||||
import { LineChartContent as LegacyLineChartContent } from 'sourcegraph'
|
||||
|
||||
import { Series } from '../../../../charts'
|
||||
import {
|
||||
@ -12,6 +11,7 @@ import {
|
||||
SearchBackendBasedInsight,
|
||||
SearchRuntimeBasedInsight,
|
||||
} from '../types'
|
||||
import { InsightContentType } from '../types/insight/common'
|
||||
|
||||
export interface CategoricalChartContent<Datum> {
|
||||
data: Datum[]
|
||||
@ -27,6 +27,18 @@ export interface SeriesChartContent<Datum> {
|
||||
getXValue: (datum: Datum) => Date
|
||||
}
|
||||
|
||||
export interface InsightCategoricalContent<Datum> {
|
||||
type: InsightContentType.Categorical
|
||||
content: CategoricalChartContent<Datum>
|
||||
}
|
||||
|
||||
export interface InsightSeriesContent<Datum> {
|
||||
type: InsightContentType.Series
|
||||
content: SeriesChartContent<Datum>
|
||||
}
|
||||
|
||||
export type InsightContent<Datum> = InsightSeriesContent<Datum> | InsightCategoricalContent<Datum>
|
||||
|
||||
export interface DashboardCreateInput {
|
||||
name: string
|
||||
owners: InsightsDashboardOwner[]
|
||||
@ -103,14 +115,14 @@ export interface AccessibleInsightInfo {
|
||||
title: string
|
||||
}
|
||||
|
||||
export interface BackendInsightDatum {
|
||||
dateTime: number
|
||||
[seriesKey: string]: number | string
|
||||
}
|
||||
|
||||
export interface BackendInsightData {
|
||||
id: string
|
||||
view: {
|
||||
title: string
|
||||
subtitle?: string
|
||||
content: LegacyLineChartContent<any, string>[]
|
||||
isFetchingHistoricalData: boolean
|
||||
}
|
||||
content: SeriesChartContent<any>
|
||||
isFetchingHistoricalData: boolean
|
||||
}
|
||||
|
||||
export interface GetBuiltInsightInput {
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import { Observable } from 'rxjs'
|
||||
|
||||
import { ViewProviderResult } from '@sourcegraph/shared/src/api/extension/extensionHostApi'
|
||||
|
||||
import { BackendInsight, Insight, InsightDashboard, InsightsDashboardOwner } from '../types'
|
||||
|
||||
import {
|
||||
@ -25,6 +23,7 @@ import {
|
||||
CategoricalChartContent,
|
||||
SeriesChartContent,
|
||||
UiFeaturesConfig,
|
||||
InsightContent,
|
||||
} from './code-insights-backend-types'
|
||||
|
||||
/**
|
||||
@ -96,7 +95,7 @@ export interface CodeInsightsBackend {
|
||||
* Returns extension like built-in insight that is fetched via frontend
|
||||
* network utils to Sourcegraph search API.
|
||||
*/
|
||||
getBuiltInInsightData: (input: GetBuiltInsightInput) => Observable<ViewProviderResult>
|
||||
getBuiltInInsightData: (input: GetBuiltInsightInput) => Observable<InsightContent<unknown>>
|
||||
|
||||
/**
|
||||
* Returns content for the search based insight live preview chart.
|
||||
|
||||
@ -230,53 +230,14 @@ export class CodeInsightsGqlBackend implements CodeInsightsBackend {
|
||||
|
||||
// Live preview fetchers
|
||||
public getSearchInsightContent = (input: GetSearchInsightContentInput): Promise<SeriesChartContent<any>> =>
|
||||
getSearchInsightContent(input).then(data => {
|
||||
const { data: datumList, series, xAxis } = data
|
||||
|
||||
// TODO: Remove this when the dashboard page has new chart fetchers
|
||||
return {
|
||||
data: datumList,
|
||||
series: series.map(series => ({
|
||||
dataKey: series.dataKey,
|
||||
name: series.name ?? '',
|
||||
color: series.stroke,
|
||||
getLinkURL: datum => series.linkURLs?.[+datum[xAxis.dataKey]] ?? undefined,
|
||||
})),
|
||||
getXValue: datum => new Date(+datum[xAxis.dataKey]),
|
||||
}
|
||||
})
|
||||
getSearchInsightContent(input).then(data => data.content)
|
||||
|
||||
public getLangStatsInsightContent = (
|
||||
input: GetLangStatsInsightContentInput
|
||||
): Promise<CategoricalChartContent<any>> =>
|
||||
getLangStatsInsightContent(input).then(data => {
|
||||
const { data: dataList, dataKey, nameKey, fillKey = '', linkURLKey = '' } = data.pies[0]
|
||||
|
||||
// TODO: Remove this when the dashboard page has new chart fetchers
|
||||
return {
|
||||
data: dataList,
|
||||
getDatumValue: datum => datum[dataKey],
|
||||
getDatumColor: datum => datum[fillKey ?? ''],
|
||||
getDatumName: datum => datum[nameKey],
|
||||
getDatumLink: datum => datum[linkURLKey],
|
||||
}
|
||||
})
|
||||
): Promise<CategoricalChartContent<any>> => getLangStatsInsightContent(input).then(data => data.content)
|
||||
|
||||
public getCaptureInsightContent = (input: CaptureInsightSettings): Promise<SeriesChartContent<any>> =>
|
||||
getCaptureGroupInsightsPreview(this.apolloClient, input).then(data => {
|
||||
const { data: datumList, series, xAxis } = data
|
||||
|
||||
// TODO: Remove this when the dashboard page has new chart fetchers
|
||||
return {
|
||||
data: datumList,
|
||||
series: series.map(series => ({
|
||||
dataKey: series.dataKey,
|
||||
name: series.name ?? '',
|
||||
color: series.stroke,
|
||||
})),
|
||||
getXValue: datum => new Date(+datum[xAxis.dataKey]),
|
||||
}
|
||||
})
|
||||
getCaptureGroupInsightsPreview(this.apolloClient, input)
|
||||
|
||||
// Repositories API
|
||||
public getRepositorySuggestions = getRepositorySuggestions
|
||||
|
||||
@ -12,14 +12,10 @@ export const createBackendInsightData = (insight: BackendInsight, response: Insi
|
||||
const seriesMetadata = getParsedDataSeriesMetadata(insight, seriesData)
|
||||
|
||||
return {
|
||||
id: insight.id,
|
||||
view: {
|
||||
title: insight.title,
|
||||
content: [createLineChartContent(seriesData, seriesMetadata, insight.filters)],
|
||||
isFetchingHistoricalData: seriesData.some(
|
||||
({ status: { pendingJobs, backfillQueuedAt } }) => pendingJobs > 0 || backfillQueuedAt === null
|
||||
),
|
||||
},
|
||||
content: createLineChartContent(seriesData, seriesMetadata, insight.filters),
|
||||
isFetchingHistoricalData: seriesData.some(
|
||||
({ status: { pendingJobs, backfillQueuedAt } }) => pendingJobs > 0 || backfillQueuedAt === null
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,35 +1,19 @@
|
||||
import { Observable, of } from 'rxjs'
|
||||
import { catchError, map, switchMap } from 'rxjs/operators'
|
||||
|
||||
import { asError } from '@sourcegraph/common'
|
||||
import { ViewProviderResult } from '@sourcegraph/shared/src/api/extension/extensionHostApi'
|
||||
import { switchMap } from 'rxjs/operators'
|
||||
|
||||
import { isSearchBasedInsight } from '../../../../types'
|
||||
import { GetBuiltInsightInput } from '../../../code-insights-backend-types'
|
||||
import { GetBuiltInsightInput, InsightContent } from '../../../code-insights-backend-types'
|
||||
|
||||
import { getLangStatsInsightContent } from './get-lang-stats-insight-content'
|
||||
import { getSearchInsightContent } from './get-search-insight-content'
|
||||
|
||||
export function getBuiltInInsight(input: GetBuiltInsightInput): Observable<ViewProviderResult> {
|
||||
export function getBuiltInInsight(input: GetBuiltInsightInput): Observable<InsightContent<any>> {
|
||||
const { insight } = input
|
||||
|
||||
return of(insight).pipe(
|
||||
// TODO Implement declarative fetchers map by insight type
|
||||
switchMap(insight =>
|
||||
isSearchBasedInsight(insight) ? getSearchInsightContent(insight) : getLangStatsInsightContent(insight)
|
||||
),
|
||||
map(data => ({
|
||||
id: insight.id,
|
||||
view: {
|
||||
title: insight.title,
|
||||
content: [data],
|
||||
},
|
||||
})),
|
||||
catchError(error =>
|
||||
of<ViewProviderResult>({
|
||||
id: insight.id,
|
||||
view: asError(error),
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,12 +1,19 @@
|
||||
import { escapeRegExp, partition, sum } from 'lodash'
|
||||
import { defer } from 'rxjs'
|
||||
import { map, retry } from 'rxjs/operators'
|
||||
import { PieChartContent } from 'sourcegraph'
|
||||
|
||||
import { GetLangStatsInsightContentInput } from '../../../code-insights-backend-types'
|
||||
import { InsightContentType } from '../../../../types/insight/common'
|
||||
import { GetLangStatsInsightContentInput, InsightCategoricalContent } from '../../../code-insights-backend-types'
|
||||
|
||||
import { fetchLangStatsInsight } from './utils/fetch-lang-stats-insight'
|
||||
|
||||
interface LangStatsDatum {
|
||||
totalLines: number
|
||||
name: string
|
||||
linkURL: string
|
||||
fill: string
|
||||
}
|
||||
|
||||
const getLangColor = async (language: string): Promise<string> => {
|
||||
const { default: languagesMap } = await import('linguist-languages')
|
||||
|
||||
@ -22,7 +29,7 @@ const getLangColor = async (language: string): Promise<string> => {
|
||||
|
||||
export async function getLangStatsInsightContent(
|
||||
input: GetLangStatsInsightContentInput
|
||||
): Promise<PieChartContent<any>> {
|
||||
): Promise<InsightCategoricalContent<LangStatsDatum>> {
|
||||
return getInsightContent({ ...input, repo: input.repository })
|
||||
}
|
||||
|
||||
@ -31,7 +38,7 @@ interface GetInsightContentInputs extends GetLangStatsInsightContentInput {
|
||||
path?: string
|
||||
}
|
||||
|
||||
async function getInsightContent(inputs: GetInsightContentInputs): Promise<PieChartContent<any>> {
|
||||
async function getInsightContent(inputs: GetInsightContentInputs): Promise<InsightCategoricalContent<LangStatsDatum>> {
|
||||
const { otherThreshold, repo, path } = inputs
|
||||
|
||||
const pathRegexp = path ? `file:^${escapeRegExp(path)}/` : ''
|
||||
@ -66,15 +73,13 @@ async function getInsightContent(inputs: GetInsightContentInputs): Promise<PieCh
|
||||
)
|
||||
|
||||
return {
|
||||
chart: 'pie' as const,
|
||||
pies: [
|
||||
{
|
||||
data,
|
||||
dataKey: 'totalLines',
|
||||
nameKey: 'name',
|
||||
fillKey: 'fill',
|
||||
linkURLKey: 'linkURL',
|
||||
},
|
||||
],
|
||||
type: InsightContentType.Categorical,
|
||||
content: {
|
||||
data,
|
||||
getDatumColor: datum => datum.fill,
|
||||
getDatumLink: datum => datum.linkURL,
|
||||
getDatumName: datum => datum.name,
|
||||
getDatumValue: datum => datum.totalLines,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { formatISO, isAfter, startOfDay, sub, Duration } from 'date-fns'
|
||||
import { Duration, formatISO, isAfter, startOfDay, sub } from 'date-fns'
|
||||
import escapeRegExp from 'lodash/escapeRegExp'
|
||||
import { defer } from 'rxjs'
|
||||
import { retry } from 'rxjs/operators'
|
||||
import type { LineChartContent } from 'sourcegraph'
|
||||
|
||||
import { EMPTY_DATA_POINT_VALUE } from '../../../../../../../views'
|
||||
import { GetSearchInsightContentInput } from '../../../code-insights-backend-types'
|
||||
import { InsightContentType } from '../../../../types/insight/common'
|
||||
import { GetSearchInsightContentInput, InsightSeriesContent } from '../../../code-insights-backend-types'
|
||||
|
||||
import { fetchRawSearchInsightResults, fetchSearchInsightCommits } from './utils/fetch-search-insight'
|
||||
import { queryHasCountFilter } from './utils/query-has-count-filter'
|
||||
@ -23,7 +23,7 @@ interface InsightSeriesData {
|
||||
|
||||
export async function getSearchInsightContent(
|
||||
input: GetSearchInsightContentInput
|
||||
): Promise<LineChartContent<any, string>> {
|
||||
): Promise<InsightSeriesContent<InsightSeriesData>> {
|
||||
return getInsightContent({ ...input, repos: input.repositories })
|
||||
}
|
||||
|
||||
@ -32,7 +32,9 @@ interface GetInsightContentInput extends GetSearchInsightContentInput {
|
||||
path?: string
|
||||
}
|
||||
|
||||
export async function getInsightContent(inputs: GetInsightContentInput): Promise<LineChartContent<any, string>> {
|
||||
export async function getInsightContent(
|
||||
inputs: GetInsightContentInput
|
||||
): Promise<InsightSeriesContent<InsightSeriesData>> {
|
||||
const { series, step, repos, path } = inputs
|
||||
const stepInterval = step || { days: 1 }
|
||||
const pathRegexp = path ? `^${escapeRegExp(path)}/` : undefined
|
||||
@ -116,14 +118,16 @@ export async function getInsightContent(inputs: GetInsightContentInput): Promise
|
||||
}
|
||||
|
||||
return {
|
||||
chart: 'line' as const,
|
||||
data,
|
||||
series: series.map(series => ({
|
||||
dataKey: series.name,
|
||||
name: series.name,
|
||||
stroke: series.stroke,
|
||||
linkURLs: Object.fromEntries(
|
||||
dates.map(date => {
|
||||
type: InsightContentType.Series,
|
||||
content: {
|
||||
data,
|
||||
getXValue: datum => new Date(datum.date),
|
||||
series: series.map(series => ({
|
||||
dataKey: series.name,
|
||||
name: series.name,
|
||||
color: series.stroke,
|
||||
getLinkURL: datum => {
|
||||
const date = datum.date
|
||||
// Link to diff search that explains what new cases were added between two data points
|
||||
const url = new URL('/search', window.location.origin)
|
||||
// Use formatISO instead of toISOString(), because toISOString() always outputs UTC.
|
||||
@ -136,14 +140,9 @@ export async function getInsightContent(inputs: GetInsightContentInput): Promise
|
||||
|
||||
url.searchParams.set('q', diffQuery)
|
||||
|
||||
return [date.getTime(), url.href]
|
||||
})
|
||||
),
|
||||
})),
|
||||
xAxis: {
|
||||
dataKey: 'date' as const,
|
||||
type: 'number' as const,
|
||||
scale: 'time' as const,
|
||||
return url.href
|
||||
},
|
||||
})),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,21 +1,14 @@
|
||||
import { ApolloClient, gql } from '@apollo/client'
|
||||
import { startCase } from 'lodash'
|
||||
import openColor from 'open-color'
|
||||
import { LineChartContent } from 'sourcegraph'
|
||||
|
||||
import {
|
||||
GetCaptureGroupInsightPreviewResult,
|
||||
GetCaptureGroupInsightPreviewVariables,
|
||||
} from '../../../../../../graphql-operations'
|
||||
import { CaptureInsightSettings } from '../../code-insights-backend-types'
|
||||
import { getDataPoints, InsightDataSeriesData } from '../../utils/create-line-chart-content'
|
||||
import { CaptureInsightSettings, SeriesChartContent } from '../../code-insights-backend-types'
|
||||
import { getDataPoints, getLinkKey, InsightDataSeriesData } from '../../utils/create-line-chart-content'
|
||||
import { getStepInterval } from '../utils/get-step-interval'
|
||||
|
||||
import { MAX_NUMBER_OF_SERIES } from './get-backend-insight-data/deserializators'
|
||||
|
||||
const SERIES_COLORS = Object.keys(openColor)
|
||||
.filter(name => name !== 'white' && name !== 'black' && name !== 'gray')
|
||||
.map(name => ({ name: startCase(name), color: `var(--oc-${name}-7)` }))
|
||||
import { DATA_SERIES_COLORS_LIST, MAX_NUMBER_OF_SERIES } from './get-backend-insight-data/deserializators'
|
||||
|
||||
const GET_CAPTURE_GROUP_INSIGHT_PREVIEW_GQL = gql`
|
||||
query GetCaptureGroupInsightPreview($input: SearchInsightLivePreviewInput!) {
|
||||
@ -28,11 +21,15 @@ const GET_CAPTURE_GROUP_INSIGHT_PREVIEW_GQL = gql`
|
||||
}
|
||||
}
|
||||
`
|
||||
export interface CaptureGroupInsightDatum {
|
||||
dateTime: number
|
||||
[seriesKey: string]: number | string
|
||||
}
|
||||
|
||||
export const getCaptureGroupInsightsPreview = (
|
||||
client: ApolloClient<unknown>,
|
||||
input: CaptureInsightSettings
|
||||
): Promise<LineChartContent<any, string>> => {
|
||||
): Promise<SeriesChartContent<CaptureGroupInsightDatum>> => {
|
||||
const [unit, value] = getStepInterval(input.step)
|
||||
|
||||
return client
|
||||
@ -65,19 +62,33 @@ export const getCaptureGroupInsightsPreview = (
|
||||
...series,
|
||||
}))
|
||||
|
||||
// TODO Revisit live preview and dashboard insight resolver methods in order to
|
||||
// improve series data handling and manipulation
|
||||
const seriesMetadata = indexedSeries.map((generatedSeries, index) => ({
|
||||
id: generatedSeries.seriesId,
|
||||
name: generatedSeries.label,
|
||||
query: input.query,
|
||||
stroke: DATA_SERIES_COLORS_LIST[index % DATA_SERIES_COLORS_LIST.length],
|
||||
}))
|
||||
|
||||
const seriesDefinitionMap = Object.fromEntries(
|
||||
seriesMetadata.map(definition => [definition.id, definition])
|
||||
)
|
||||
|
||||
return {
|
||||
chart: 'line',
|
||||
data: getDataPoints(indexedSeries),
|
||||
series: indexedSeries.map((series, index) => ({
|
||||
dataKey: series.seriesId,
|
||||
name: series.label,
|
||||
stroke: SERIES_COLORS[index % SERIES_COLORS.length].color,
|
||||
data: getDataPoints({
|
||||
series: indexedSeries,
|
||||
seriesDefinitionMap,
|
||||
includeRepoRegexp: '',
|
||||
excludeRepoRegexp: '',
|
||||
}),
|
||||
series: indexedSeries.map((line, index) => ({
|
||||
dataKey: line.seriesId,
|
||||
name: line.label,
|
||||
color: DATA_SERIES_COLORS_LIST[index % DATA_SERIES_COLORS_LIST.length],
|
||||
getLinkURL: datum => `${datum[getLinkKey(line.seriesId)]}`,
|
||||
})),
|
||||
xAxis: {
|
||||
dataKey: 'dateTime',
|
||||
scale: 'time',
|
||||
type: 'number',
|
||||
},
|
||||
getXValue: datum => new Date(datum.dateTime),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,17 +1,15 @@
|
||||
import { formatISO } from 'date-fns'
|
||||
import { LineChartContent } from 'sourcegraph'
|
||||
|
||||
import { buildSearchURLQuery } from '@sourcegraph/shared/src/util/url'
|
||||
|
||||
import { Series } from '../../../../../charts'
|
||||
import { InsightDataSeries, SearchPatternType } from '../../../../../graphql-operations'
|
||||
import { semanticSort } from '../../../../../insights/utils/semantic-sort'
|
||||
import { PageRoutes } from '../../../../../routes.constants'
|
||||
import { InsightFilters, SearchBasedInsightSeries } from '../../types'
|
||||
import { BackendInsightDatum, SeriesChartContent } from '../code-insights-backend-types'
|
||||
|
||||
interface SeriesDataset {
|
||||
dateTime: number
|
||||
[seriesKey: string]: number
|
||||
}
|
||||
type SeriesDefinition = Record<string, SearchBasedInsightSeries>
|
||||
|
||||
/**
|
||||
* Minimal input type model for {@link createLineChartContent} function
|
||||
@ -20,8 +18,7 @@ export type InsightDataSeriesData = Pick<InsightDataSeries, 'seriesId' | 'label'
|
||||
|
||||
/**
|
||||
* Generates line chart content for visx chart. Note that this function relies on the fact that
|
||||
* all series are indexed. This generator is used only for GQL api, only there we have indexed series
|
||||
* for setting-based api see {@link createLineChartContent}
|
||||
* all series are indexed.
|
||||
*
|
||||
* @param series - insight series with points data
|
||||
* @param seriesDefinition - insight definition with line settings (color, name, query)
|
||||
@ -31,71 +28,43 @@ export function createLineChartContent(
|
||||
series: InsightDataSeriesData[],
|
||||
seriesDefinition: SearchBasedInsightSeries[] = [],
|
||||
filters?: InsightFilters
|
||||
): LineChartContent<SeriesDataset, 'dateTime'> {
|
||||
const definitionMap = Object.fromEntries<SearchBasedInsightSeries>(
|
||||
): SeriesChartContent<BackendInsightDatum> {
|
||||
const seriesDefinitionMap: SeriesDefinition = Object.fromEntries<SearchBasedInsightSeries>(
|
||||
seriesDefinition.map(definition => [definition.id, definition])
|
||||
)
|
||||
|
||||
const { includeRepoRegexp = '', excludeRepoRegexp = '' } = filters ?? {}
|
||||
|
||||
return {
|
||||
chart: 'line',
|
||||
data: getDataPoints(series),
|
||||
data: getDataPoints({ series, seriesDefinitionMap, excludeRepoRegexp, includeRepoRegexp }),
|
||||
series: series
|
||||
.map(line => ({
|
||||
name: definitionMap[line.seriesId]?.name ?? line.label,
|
||||
.map<Series<BackendInsightDatum>>(line => ({
|
||||
dataKey: line.seriesId,
|
||||
stroke: definitionMap[line.seriesId]?.stroke,
|
||||
linkURLs: Object.fromEntries(
|
||||
[...line.points]
|
||||
.sort((a, b) => Date.parse(a.dateTime) - Date.parse(b.dateTime))
|
||||
.map((point, index, points) => {
|
||||
const previousPoint = points[index - 1]
|
||||
const date = Date.parse(point.dateTime)
|
||||
|
||||
// Use formatISO instead of toISOString(), because toISOString() always outputs UTC.
|
||||
// They mark the same point in time, but using the user's timezone makes the date string
|
||||
// easier to read (else the date component may be off by one day)
|
||||
const after = previousPoint ? formatISO(Date.parse(previousPoint.dateTime)) : ''
|
||||
const before = formatISO(date)
|
||||
|
||||
const includeRepoFilter = includeRepoRegexp ? `repo:${includeRepoRegexp}` : ''
|
||||
const excludeRepoFilter = excludeRepoRegexp ? `-repo:${excludeRepoRegexp}` : ''
|
||||
|
||||
const repoFilter = `${includeRepoFilter} ${excludeRepoFilter}`
|
||||
const afterFilter = after ? `after:${after}` : ''
|
||||
const beforeFilter = `before:${before}`
|
||||
const dateFilters = `${afterFilter} ${beforeFilter}`
|
||||
const diffQuery = `${repoFilter} type:diff ${dateFilters} ${
|
||||
definitionMap[line.seriesId].query
|
||||
}`
|
||||
const searchQueryParameter = buildSearchURLQuery(
|
||||
diffQuery,
|
||||
SearchPatternType.literal,
|
||||
false
|
||||
)
|
||||
|
||||
return [date, `${window.location.origin}${PageRoutes.Search}?${searchQueryParameter}`]
|
||||
})
|
||||
),
|
||||
name: seriesDefinitionMap[line.seriesId]?.name ?? line.label,
|
||||
color: seriesDefinitionMap[line.seriesId]?.stroke,
|
||||
getLinkURL: datum => `${datum[getLinkKey(line.seriesId)]}`,
|
||||
}))
|
||||
.sort((a, b) => semanticSort(a.name, b.name)),
|
||||
xAxis: {
|
||||
dataKey: 'dateTime',
|
||||
scale: 'time',
|
||||
type: 'number',
|
||||
},
|
||||
getXValue: datum => new Date(datum.dateTime),
|
||||
}
|
||||
}
|
||||
|
||||
interface GetDataPointsInput {
|
||||
series: InsightDataSeriesData[]
|
||||
seriesDefinitionMap: SeriesDefinition
|
||||
includeRepoRegexp: string
|
||||
excludeRepoRegexp: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups data series by dateTime (x axis) of each series
|
||||
* Groups data series by dateTime (x-axis) of each series
|
||||
*/
|
||||
export function getDataPoints(series: InsightDataSeriesData[]): SeriesDataset[] {
|
||||
const dataByXValue = new Map<string, SeriesDataset>()
|
||||
export function getDataPoints(input: GetDataPointsInput): BackendInsightDatum[] {
|
||||
const { series, seriesDefinitionMap, includeRepoRegexp, excludeRepoRegexp } = input
|
||||
const dataByXValue = new Map<string, BackendInsightDatum>()
|
||||
|
||||
for (const line of series) {
|
||||
for (const point of line.points) {
|
||||
for (const [index, point] of line.points.entries()) {
|
||||
let dataObject = dataByXValue.get(point.dateTime)
|
||||
if (!dataObject) {
|
||||
dataObject = {
|
||||
@ -106,8 +75,51 @@ export function getDataPoints(series: InsightDataSeriesData[]): SeriesDataset[]
|
||||
dataByXValue.set(point.dateTime, dataObject)
|
||||
}
|
||||
dataObject[line.seriesId] = point.value
|
||||
dataObject[getLinkKey(line.seriesId)] = generateLinkURL({
|
||||
previousPoint: line.points[index - 1],
|
||||
series: seriesDefinitionMap[line.seriesId],
|
||||
point,
|
||||
includeRepoRegexp,
|
||||
excludeRepoRegexp,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return [...dataByXValue.values()]
|
||||
}
|
||||
|
||||
interface GenerateLinkInput {
|
||||
series: SearchBasedInsightSeries
|
||||
previousPoint?: { dateTime: string }
|
||||
point: { dateTime: string }
|
||||
includeRepoRegexp: string
|
||||
excludeRepoRegexp: string
|
||||
}
|
||||
|
||||
function generateLinkURL(input: GenerateLinkInput): string {
|
||||
const { series, point, previousPoint, includeRepoRegexp, excludeRepoRegexp } = input
|
||||
|
||||
const date = Date.parse(point.dateTime)
|
||||
|
||||
// Use formatISO instead of toISOString(), because toISOString() always outputs UTC.
|
||||
// They mark the same point in time, but using the user's timezone makes the date string
|
||||
// easier to read (else the date component may be off by one day)
|
||||
const after = previousPoint ? formatISO(Date.parse(previousPoint.dateTime)) : ''
|
||||
const before = formatISO(date)
|
||||
|
||||
const includeRepoFilter = includeRepoRegexp ? `repo:${includeRepoRegexp}` : ''
|
||||
const excludeRepoFilter = excludeRepoRegexp ? `-repo:${excludeRepoRegexp}` : ''
|
||||
|
||||
const repoFilter = `${includeRepoFilter} ${excludeRepoFilter}`
|
||||
const afterFilter = after ? `after:${after}` : ''
|
||||
const beforeFilter = `before:${before}`
|
||||
const dateFilters = `${afterFilter} ${beforeFilter}`
|
||||
const diffQuery = `${repoFilter} type:diff ${dateFilters} ${series.query}`
|
||||
const searchQueryParameter = buildSearchURLQuery(diffQuery, SearchPatternType.literal, false)
|
||||
|
||||
return `${window.location.origin}${PageRoutes.Search}?${searchQueryParameter}`
|
||||
}
|
||||
|
||||
export function getLinkKey(seriesId: string): string {
|
||||
return `${seriesId}:link`
|
||||
}
|
||||
|
||||
@ -17,6 +17,11 @@ export enum InsightType {
|
||||
CaptureGroup = 'CaptureGroup',
|
||||
}
|
||||
|
||||
export enum InsightContentType {
|
||||
Categorical,
|
||||
Series,
|
||||
}
|
||||
|
||||
export interface InsightFilters {
|
||||
includeRepoRegexp: string
|
||||
excludeRepoRegexp: string
|
||||
|
||||
@ -3,8 +3,7 @@ import { useCallback, useContext, useState } from 'react'
|
||||
import { ErrorLike } from '@sourcegraph/common'
|
||||
|
||||
import { eventLogger } from '../../../tracking/eventLogger'
|
||||
import { CodeInsightsBackendContext } from '../core/backend/code-insights-backend-context'
|
||||
import { Insight } from '../core/types'
|
||||
import { CodeInsightsBackendContext, Insight } from '../core'
|
||||
import { getTrackingTypeByInsightType } from '../pings'
|
||||
|
||||
type DeletionInsight = Pick<Insight, 'id' | 'title' | 'type'>
|
||||
|
||||
@ -3,9 +3,7 @@ import { useEffect, useState } from 'react'
|
||||
import { gql, useApolloClient } from '@apollo/client'
|
||||
|
||||
import { IsCodeInsightsLicensedResult } from '../../../graphql-operations'
|
||||
import { CodeInsightsBackend } from '../core/backend/code-insights-backend'
|
||||
import { CodeInsightsGqlBackend } from '../core/backend/gql-backend/code-insights-gql-backend'
|
||||
import { CodeInsightsGqlBackendLimited } from '../core/backend/lam-backend/code-insights-gql-backend-limited'
|
||||
import { CodeInsightsBackend, CodeInsightsGqlBackend, CodeInsightsGqlBackendLimited } from '../core'
|
||||
|
||||
/**
|
||||
* Returns the full or limited version of the API based on
|
||||
|
||||
@ -30,7 +30,23 @@ export interface FetchResult<T> {
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
const MAX_PARALLEL_QUERIES = 2
|
||||
export enum LazyQueryStatus {
|
||||
Loading,
|
||||
Data,
|
||||
Error,
|
||||
}
|
||||
|
||||
export type LazyQueryState<T> =
|
||||
| { status: LazyQueryStatus.Loading }
|
||||
| { status: LazyQueryStatus.Data; data: T }
|
||||
| { status: LazyQueryStatus.Error; error: ErrorLike }
|
||||
|
||||
export interface LazyQueryResult<T> {
|
||||
state: LazyQueryState<T>
|
||||
query: (request: () => ObservableInput<T>) => Unsubscribable
|
||||
}
|
||||
|
||||
const MAX_PARALLEL_QUERIES = 3
|
||||
|
||||
/**
|
||||
* Parallel requests hooks factory. This factory/function generates special
|
||||
@ -75,7 +91,7 @@ export function createUseParallelRequestsHook<T>({ maxRequests } = { maxRequests
|
||||
takeUntil(cancel),
|
||||
map(payload => ({ payload, onComplete })),
|
||||
// In order to close observable and free up space for other queued requests
|
||||
// in merge map queue. Consider to move this into consumers request calls
|
||||
// in merge map queue. Consider moving this into consumers request calls
|
||||
take(1),
|
||||
catchError(error =>
|
||||
of({
|
||||
@ -144,12 +160,8 @@ export function createUseParallelRequestsHook<T>({ maxRequests } = { maxRequests
|
||||
* This provides query methods that allows to you run your request in parallel with
|
||||
* other request that have been made with useParallelRequests request calls.
|
||||
*/
|
||||
lazyQuery: <D>(): FetchResult<D> & { query: (request: () => ObservableInput<D>) => Unsubscribable } => {
|
||||
const [state, setState] = useState<FetchResult<D>>({
|
||||
data: undefined,
|
||||
error: undefined,
|
||||
loading: true,
|
||||
})
|
||||
lazyQuery: <D>(): LazyQueryResult<D> => {
|
||||
const [state, setState] = useState<LazyQueryState<D>>({ status: LazyQueryStatus.Loading })
|
||||
|
||||
const localRequestPool = useRef<Request<D>[]>([])
|
||||
|
||||
@ -166,7 +178,7 @@ export function createUseParallelRequestsHook<T>({ maxRequests } = { maxRequests
|
||||
const query = useCallback((request: () => ObservableInput<D>) => {
|
||||
const cancelStream = new Subject<boolean>()
|
||||
|
||||
setState({ data: undefined, loading: true, error: undefined })
|
||||
setState({ status: LazyQueryStatus.Loading })
|
||||
|
||||
const event: Request<D> = {
|
||||
request,
|
||||
@ -176,10 +188,10 @@ export function createUseParallelRequestsHook<T>({ maxRequests } = { maxRequests
|
||||
localRequestPool.current = localRequestPool.current.filter(request => request !== event)
|
||||
|
||||
if (isErrorLike(result)) {
|
||||
return setState({ data: undefined, loading: false, error: result })
|
||||
return setState({ status: LazyQueryStatus.Error, error: result })
|
||||
}
|
||||
|
||||
setState({ data: result, loading: false, error: undefined })
|
||||
setState({ status: LazyQueryStatus.Data, data: result })
|
||||
},
|
||||
}
|
||||
|
||||
@ -199,7 +211,7 @@ export function createUseParallelRequestsHook<T>({ maxRequests } = { maxRequests
|
||||
}
|
||||
}, [])
|
||||
|
||||
return { ...state, query }
|
||||
return { state, query }
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,8 +3,7 @@ import { useCallback, useContext, useState } from 'react'
|
||||
import { ErrorLike } from '@sourcegraph/common'
|
||||
|
||||
import { eventLogger } from '../../../tracking/eventLogger'
|
||||
import { CodeInsightsBackendContext } from '../core/backend/code-insights-backend-context'
|
||||
import { Insight, InsightDashboard } from '../core/types'
|
||||
import { CodeInsightsBackendContext, Insight, InsightDashboard } from '../core'
|
||||
import { getTrackingTypeByInsightType } from '../pings'
|
||||
|
||||
interface RemoveInsightInput {
|
||||
|
||||
@ -3,8 +3,7 @@ import { useContext, useMemo } from 'react'
|
||||
import { Observable, of } from 'rxjs'
|
||||
import { map } from 'rxjs/operators'
|
||||
|
||||
import { CodeInsightsBackendContext } from '../core/backend/code-insights-backend-context'
|
||||
import { Insight, InsightDashboard, isSearchBasedInsight } from '../core/types'
|
||||
import { CodeInsightsBackendContext, Insight, InsightDashboard, isSearchBasedInsight } from '../core'
|
||||
import {
|
||||
getDashboardPermissions,
|
||||
getTooltipMessage,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { InsightType } from '../core/types'
|
||||
import { InsightType } from '../core'
|
||||
|
||||
export enum CodeInsightTrackType {
|
||||
SearchBasedInsight = 'SearchBased',
|
||||
|
||||
@ -67,7 +67,7 @@ describe('Code insights page', () => {
|
||||
})
|
||||
|
||||
await driver.page.goto(driver.sourcegraphBaseUrl + '/insights/dashboards/all')
|
||||
await driver.page.waitForSelector('[data-testid="line-chart__content"] svg circle')
|
||||
await driver.page.waitForSelector('svg circle')
|
||||
|
||||
const variables = await testContext.waitForGraphQLRequest(async () => {
|
||||
await driver.page.click('[data-testid="insight-card.001"] [data-testid="InsightContextMenuButton"]')
|
||||
|
||||
@ -70,7 +70,7 @@ describe('Backend insight drill down filters', () => {
|
||||
})
|
||||
|
||||
await driver.page.goto(driver.sourcegraphBaseUrl + '/insights/dashboards/all')
|
||||
await driver.page.waitForSelector('[data-testid="line-chart__content"] svg circle')
|
||||
await driver.page.waitForSelector('svg circle')
|
||||
|
||||
await driver.page.click('button[aria-label="Filters"]')
|
||||
await driver.page.waitForSelector('[role="dialog"][aria-label="Drill-down filters panel"]')
|
||||
@ -147,7 +147,7 @@ describe('Backend insight drill down filters', () => {
|
||||
})
|
||||
|
||||
await driver.page.goto(driver.sourcegraphBaseUrl + '/insights/dashboards/all')
|
||||
await driver.page.waitForSelector('[data-testid="line-chart__content"] svg circle')
|
||||
await driver.page.waitForSelector('svg circle')
|
||||
|
||||
await driver.page.click('button[aria-label="Active filters"]')
|
||||
await driver.page.waitForSelector('[role="dialog"][aria-label="Drill-down filters panel"]')
|
||||
|
||||
@ -281,6 +281,8 @@ export function LineChartContent<Datum extends object>(props: LineChartContentPr
|
||||
orientation="left"
|
||||
tickValues={yTicks}
|
||||
tickFormat={numberFormatter}
|
||||
/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
|
||||
// @ts-ignore
|
||||
tickLabelProps={getTickYProps}
|
||||
tickComponent={Tick}
|
||||
axisLineClassName={classNames(styles.axisLine, styles.axisLineVertical)}
|
||||
@ -295,6 +297,8 @@ export function LineChartContent<Datum extends object>(props: LineChartContentPr
|
||||
tickValues={xScale.ticks(numberOfTicksX)}
|
||||
tickFormat={dateTickFormatter}
|
||||
numTicks={numberOfTicksX}
|
||||
/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
|
||||
// @ts-ignore
|
||||
tickLabelProps={getTickXProps}
|
||||
tickComponent={Tick}
|
||||
tickLength={8}
|
||||
|
||||
@ -3,7 +3,6 @@ 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'
|
||||
|
||||
@ -22,6 +21,10 @@ export const dateTickFormatter = timeFormat('%d %b')
|
||||
// Year + full name of month + full name of week day
|
||||
export const dateLabelFormatter = timeFormat('%d %B %A')
|
||||
|
||||
interface TextProps {
|
||||
'aria-label': string
|
||||
}
|
||||
|
||||
// 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
|
||||
@ -44,7 +47,7 @@ export const Tick: React.FunctionComponent<TickRendererProps> = props => {
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/aria-role
|
||||
<Group role="text" aria-label={tickLabelProps['aria-label']}>
|
||||
<Text aria-hidden={true} x={xPosition} y={yPosition} {...tickLabelProps}>
|
||||
<Text aria-hidden={true} x={xPosition} y={yPosition} {...(tickLabelProps as TextProps)}>
|
||||
{formattedValue}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
@ -374,12 +374,13 @@
|
||||
"@types/svgo": "2.6.0",
|
||||
"@types/vscode": "^1.63.1",
|
||||
"@visx/annotation": "^2.9.0",
|
||||
"@visx/axis": "^1.7.0",
|
||||
"@visx/axis": "^2.6.0",
|
||||
"@visx/glyph": "^1.7.0",
|
||||
"@visx/grid": "^1.7.0",
|
||||
"@visx/group": "^1.7.0",
|
||||
"@visx/grid": "^2.6.0",
|
||||
"@visx/group": "^2.1.0",
|
||||
"@visx/mock-data": "^1.7.0",
|
||||
"@visx/pattern": "^1.7.0",
|
||||
"@visx/responsive": "^2.8.0",
|
||||
"@visx/scale": "^1.7.0",
|
||||
"@visx/xychart": "^1.7.3",
|
||||
"@vscode/codicons": "^0.0.29",
|
||||
|
||||
52
yarn.lock
52
yarn.lock
@ -2793,6 +2793,11 @@
|
||||
resolved "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796"
|
||||
integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==
|
||||
|
||||
"@juggle/resize-observer@^3.3.1":
|
||||
version "3.3.1"
|
||||
resolved "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.3.1.tgz#b50a781709c81e10701004214340f25475a171a0"
|
||||
integrity sha512-zMM9Ds+SawiUkakS7y94Ymqx+S0ORzpG3frZirN3l+UlXUmSUR7hF4wxCVqW+ei94JzV5kt0uXBcoOEAuiydrw==
|
||||
|
||||
"@lezer/common@^0.15.0", "@lezer/common@^0.15.5":
|
||||
version "0.15.11"
|
||||
resolved "https://registry.npmjs.org/@lezer/common/-/common-0.15.11.tgz#965b5067036305f12e8a3efc344076850be1d3a8"
|
||||
@ -6295,7 +6300,7 @@
|
||||
prop-types "^15.5.10"
|
||||
react-use-measure "^2.0.4"
|
||||
|
||||
"@visx/axis@1.7.0", "@visx/axis@^1.7.0":
|
||||
"@visx/axis@1.7.0":
|
||||
version "1.7.0"
|
||||
resolved "https://registry.npmjs.org/@visx/axis/-/axis-1.7.0.tgz#ae3bf46bb508eca9cd61cd7dcf6a32a1facdeeef"
|
||||
integrity sha512-C9XCszH+lMyA73lMalrdloDFI3P8E301KZqfmybU+OpwLtClqsEQmzWWpRmw5xPElgtSLztemsx8SczHFGLKuw==
|
||||
@ -6310,6 +6315,20 @@
|
||||
classnames "^2.2.5"
|
||||
prop-types "^15.6.0"
|
||||
|
||||
"@visx/axis@^2.6.0":
|
||||
version "2.6.0"
|
||||
resolved "https://registry.npmjs.org/@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@1.7.0":
|
||||
version "1.7.0"
|
||||
resolved "https://registry.npmjs.org/@visx/bounds/-/bounds-1.7.0.tgz#cc32aaa5aa8711771b93ec4149ff087225dc0684"
|
||||
@ -6383,7 +6402,7 @@
|
||||
d3-shape "^1.2.0"
|
||||
prop-types "^15.6.2"
|
||||
|
||||
"@visx/grid@1.7.0", "@visx/grid@^1.7.0":
|
||||
"@visx/grid@1.7.0":
|
||||
version "1.7.0"
|
||||
resolved "https://registry.npmjs.org/@visx/grid/-/grid-1.7.0.tgz#b447b09d9b409a5b41ca7e04707e08d015c18dba"
|
||||
integrity sha512-mJQjg67JogLNh5ta8RmOgilX0TPiEJWZ0O0VggFS2onThj1Ild0xZgZxCiGEFLqM8vpfuIQxb4qR2vTe16uPQQ==
|
||||
@ -6398,7 +6417,21 @@
|
||||
classnames "^2.2.5"
|
||||
prop-types "^15.6.2"
|
||||
|
||||
"@visx/group@1.7.0", "@visx/group@^1.7.0":
|
||||
"@visx/grid@^2.6.0":
|
||||
version "2.6.0"
|
||||
resolved "https://registry.npmjs.org/@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@1.7.0":
|
||||
version "1.7.0"
|
||||
resolved "https://registry.npmjs.org/@visx/group/-/group-1.7.0.tgz#e0ef2efbe00ef05326215d65b3d8a2b114df4f35"
|
||||
integrity sha512-rzSXtV0+MHUyK+rwhVSV4qaHdzGi3Me3PRFXJSIAKVfoJIZczOkudUOLy34WvSrRlVyoFvGL7k9U5g8wHyY3nw==
|
||||
@ -6408,7 +6441,7 @@
|
||||
classnames "^2.2.5"
|
||||
prop-types "^15.6.2"
|
||||
|
||||
"@visx/group@2.1.0":
|
||||
"@visx/group@2.1.0", "@visx/group@^2.1.0":
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmjs.org/@visx/group/-/group-2.1.0.tgz#65d4a72feb20dd09443f73cdc12ad5fb339792ef"
|
||||
integrity sha512-bZKa54yVjGYPZZhzYHLz4AVlidSr4ET9B/xmSa7nnictMJWr7e/IuZThB/bMfDQlgdtvhcfTgs+ZluySc5SBUg==
|
||||
@ -6469,6 +6502,17 @@
|
||||
prop-types "^15.6.1"
|
||||
resize-observer-polyfill "1.5.1"
|
||||
|
||||
"@visx/responsive@^2.8.0":
|
||||
version "2.8.0"
|
||||
resolved "https://registry.npmjs.org/@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@1.7.0", "@visx/scale@^1.7.0":
|
||||
version "1.7.0"
|
||||
resolved "https://registry.npmjs.org/@visx/scale/-/scale-1.7.0.tgz#c46daade4492edb9eaec36fd3c87dc776960d6af"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user