Code Insights: Create insight example section (#30300)

* Restructure view components for future reuses

* Implement code insights example layouts

* Add storybook story

* Update sg config yaml URL var
This commit is contained in:
Vova Kulikov 2022-01-28 17:15:56 +03:00 committed by GitHub
parent 3e3b168840
commit 6f2203f5fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 290 additions and 51 deletions

View File

@ -0,0 +1,50 @@
.section {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.card {
flex: 1;
flex-basis: 25rem;
}
.chart {
min-height: 15rem;
flex-grow: 1;
width: 100%;
position: relative;
flex-basis: 18rem;
}
.chart-content {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
.legend {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-top: 1rem;
&--horizontal {
flex-direction: column;
}
}
.code {
background-color: var(--gray-01);
padding: 0.25rem 0.5rem;
width: 100%;
display: inline-block;
border-radius: var(--border-radius);
}
.capture-group {
display: flex;
height: 100%;
}

View File

@ -0,0 +1,13 @@
import { Meta } from '@storybook/react'
import React from 'react'
import { WebStory } from '../../../../components/WebStory'
import { CodeInsightsExamples } from './CodeInsightsExamples'
export default {
title: 'web/insights/getting-started/CodeInsightExamples',
decorators: [story => <WebStory>{() => story()}</WebStory>],
} as Meta
export const StandardExample = () => <CodeInsightsExamples />

View File

@ -0,0 +1,155 @@
import { ParentSize } from '@visx/responsive'
import classNames from 'classnames'
import React from 'react'
import { LineChartContent, LineChartContent as LineChartContentType, LineChartSeries } from 'sourcegraph'
import * as View from '../../../../views'
import { LegendBlock, LegendItem } from '../../../../views'
import {
getLineStroke,
LineChart,
} from '../../../../views/components/view/content/chart-view-content/charts/line/components/LineChartContent'
import styles from './CodeInsightsExamples.module.scss'
export const CodeInsightsExamples: React.FunctionComponent = () => (
<section className={styles.section}>
<CodeInsightSearchExample className={styles.card} />
<CodeInsightCaptureExample className={styles.card} />
</section>
)
interface ExampleCardProps {
className?: string
}
interface SeriesWithQuery extends LineChartSeries<any> {
query: string
}
type Content = Omit<LineChartContentType<any, string>, 'chart' | 'series'> & { series: SeriesWithQuery[] }
const SEARCH_INSIGHT_EXAMPLES_DATA: Content = {
data: [
{ x: 1588965700286 - 4 * 24 * 60 * 60 * 1000, a: null, b: null },
{ x: 1588965700286 - 3 * 24 * 60 * 60 * 1000, a: null, b: null },
{ x: 1588965700286 - 2 * 24 * 60 * 60 * 1000, a: 94, b: 200 },
{ x: 1588965700286 - 1.5 * 24 * 60 * 60 * 1000, a: 134, b: null },
{ x: 1588965700286 - 1.3 * 24 * 60 * 60 * 1000, a: null, b: 150 },
{ x: 1588965700286 - 1 * 24 * 60 * 60 * 1000, a: 134, b: 190 },
{ x: 1588965700286, a: 123, b: 170 },
],
series: [
{
dataKey: 'a',
name: 'A metric',
stroke: 'var(--blue)',
query: 'file:README archived:no fork:no',
},
{
dataKey: 'b',
name: 'B metric',
stroke: 'var(--warning)',
query: '-file:README archived:no fork:no',
},
],
xAxis: {
dataKey: 'x',
scale: 'time',
type: 'number',
},
}
export const CodeInsightSearchExample: React.FunctionComponent<ExampleCardProps> = props => {
const { className } = props
return (
<View.Root
title="Repos with READMEs / without READMEs"
subtitle={<InlineCodeBlock className="mt-1">All repositories</InlineCodeBlock>}
className={classNames(className)}
>
<div className={styles.chart}>
<ParentSize>
{({ width, height }) => (
<LineChart {...SEARCH_INSIGHT_EXAMPLES_DATA} width={width} height={height} />
)}
</ParentSize>
</div>
<LegendBlock className={styles.legend}>
{SEARCH_INSIGHT_EXAMPLES_DATA.series.map(line => (
<LegendItem key={line.dataKey.toString()} color={getLineStroke<any>(line)}>
<span className="flex-shrink-0 mr-2">{line.name}</span>
<InlineCodeBlock>{line.query}</InlineCodeBlock>
</LegendItem>
))}
</LegendBlock>
</View.Root>
)
}
const CAPTURE_INSIGHT_EXAMPLES_DATA: LineChartContent<any, string> = {
chart: 'line' as const,
data: [
{ x: 1588965700286 - 6 * 24 * 60 * 60 * 1000, a: 20, b: 200 },
{ x: 1588965700286 - 5 * 24 * 60 * 60 * 1000, a: 40, b: 177 },
{ x: 1588965700286 - 4 * 24 * 60 * 60 * 1000, a: 110, b: 150 },
{ x: 1588965700286 - 3 * 24 * 60 * 60 * 1000, a: 105, b: 165 },
{ x: 1588965700286 - 2 * 24 * 60 * 60 * 1000, a: 160, b: 100 },
{ x: 1588965700286 - 1 * 24 * 60 * 60 * 1000, a: 184, b: 85 },
{ x: 1588965700286, a: 200, b: 50 },
],
series: [
{
dataKey: 'a',
name: 'Go 1.11',
stroke: 'var(--oc-indigo-7)',
},
{
dataKey: 'b',
name: 'Go 1.12',
stroke: 'var(--oc-orange-7)',
},
],
xAxis: {
dataKey: 'x',
scale: 'time' as const,
type: 'number',
},
}
export const CodeInsightCaptureExample: React.FunctionComponent<ExampleCardProps> = props => {
const { className } = props
return (
<View.Root
title="Node.js versions (present or most popular)"
subtitle={<InlineCodeBlock className="mt-1">repo:github.com/awesomeOrg/examplerepo</InlineCodeBlock>}
className={classNames(className)}
>
<div className={styles.captureGroup}>
<div className={styles.chart}>
<ParentSize className={styles.chartContent}>
{({ width, height }) => (
<LineChart {...CAPTURE_INSIGHT_EXAMPLES_DATA} width={width} height={height} />
)}
</ParentSize>
</div>
<LegendBlock className={styles.legendHorizontal}>
{CAPTURE_INSIGHT_EXAMPLES_DATA.series.map(line => (
<LegendItem key={line.dataKey.toString()} color={getLineStroke<any>(line)}>
{line.name}
</LegendItem>
))}
</LegendBlock>
</div>
<InlineCodeBlock className="mt-2">nvm install ([0-9]+\.[0-9]+) archived:no fork:no</InlineCodeBlock>
</View.Root>
)
}
const InlineCodeBlock: React.FunctionComponent<React.HTMLAttributes<HTMLElement>> = props => (
<code className={classNames(styles.code, props.className)}>{props.children}</code>
)

View File

@ -14,7 +14,7 @@ type ViewCardElementProps = React.DetailedHTMLProps<Omit<React.HTMLAttributes<HT
export interface ViewCardProps extends ViewCardElementProps {
title?: string
subtitle?: string
subtitle?: ReactNode
innerRef?: React.Ref<HTMLElement>
/**
@ -35,7 +35,6 @@ export const View: React.FunctionComponent<PropsWithChildren<ViewCardProps>> = p
<Card
as="section"
{...otherProps}
/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */
tabIndex={0}
ref={innerRef}
className={classNames(otherProps.className, styles.view)}

View File

@ -1,9 +1,8 @@
import { ParentSize } from '@visx/responsive'
import { EventEmitterProvider } from '@visx/xychart'
import classNames from 'classnames'
import React, { ReactElement, useContext } from 'react'
import { getLineStroke, LineChartContent, LineChartContentProps } from './components/LineChartContent'
import { getLineStroke, LineChart as LineChartContent, LineChartContentProps } from './components/LineChartContent'
import { ScrollBox } from './components/scroll-box/ScrollBox'
import { MINIMAL_HORIZONTAL_LAYOUT_WIDTH, MINIMAL_SERIES_FOR_ASIDE_LEGEND } from './constants'
import { LineChartLayoutOrientation, LineChartSettingsContext } from './line-chart-settings-provider'
@ -34,39 +33,51 @@ export function LineChart<Datum extends object>(props: LineChartProps<Datum>): R
hasViewManySeries && hasEnoughXSpace
return (
<EventEmitterProvider>
<div
aria-label="Line chart"
/* eslint-disable-next-line react/forbid-dom-props */
style={hasChartParentFixedSize ? { width, height } : undefined}
className={classNames(styles.lineChart, { [styles.lineChartHorizontal]: isHorizontal })}
>
{/*
In case if we have a legend to render we have to have responsive container for chart
just to calculate right sizes for chart content = rootContainerSizes - legendSizes
*/}
<ParentSize className={styles.contentParentSize} data-line-chart-size-root="">
{({ width, height }) => <LineChartContent {...otherProps} width={width} height={height} />}
</ParentSize>
<div
aria-label="Line chart"
/* eslint-disable-next-line react/forbid-dom-props */
style={hasChartParentFixedSize ? { width, height } : undefined}
className={classNames(styles.lineChart, { [styles.lineChartHorizontal]: isHorizontal })}
>
{/*
In case if we have a legend to render we have to have responsive container for chart
just to calculate right sizes for chart content = rootContainerSizes - legendSizes
*/}
<ParentSize className={styles.contentParentSize} data-line-chart-size-root="">
{({ width, height }) => <LineChartContent {...otherProps} width={width} height={height} />}
</ParentSize>
<ScrollBox
aria-hidden={true}
className={classNames(styles.legend, { [styles.legendHorizontal]: isHorizontal })}
>
<ul className={classNames(styles.legendList, { [styles.legendListHorizontal]: isHorizontal })}>
{props.series.map(line => (
<li key={line.dataKey.toString()} className={styles.legendItem}>
<div
/* eslint-disable-next-line react/forbid-dom-props */
style={{ backgroundColor: getLineStroke(line) }}
className={styles.legendMark}
/>
{line.name}
</li>
))}
</ul>
</ScrollBox>
</div>
</EventEmitterProvider>
<ScrollBox
aria-hidden={true}
className={classNames(styles.legend, { [styles.legendHorizontal]: isHorizontal })}
>
<LegendBlock className={classNames({ [styles.legendListHorizontal]: isHorizontal })}>
{props.series.map(line => (
<LegendItem key={line.dataKey.toString()} color={getLineStroke(line)}>
{line.name}
</LegendItem>
))}
</LegendBlock>
</ScrollBox>
</div>
)
}
export const LegendBlock: React.FunctionComponent<React.HTMLAttributes<HTMLUListElement>> = props => (
<ul className={classNames(styles.legendList, props.className)}>{props.children}</ul>
)
interface LegendItem {
color: string
}
export const LegendItem: React.FunctionComponent<LegendItem> = props => (
<li className={styles.legendItem}>
<div
/* eslint-disable-next-line react/forbid-dom-props */
style={{ backgroundColor: props.color }}
className={styles.legendMark}
/>
{props.children}
</li>
)

View File

@ -1,10 +1,4 @@
.content {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
&--with-cursor {
cursor: pointer;
}

View File

@ -1,7 +1,16 @@
import { curveLinear } from '@visx/curve'
import { GridRows } from '@visx/grid'
import { Group } from '@visx/group'
import { Axis, DataProvider, GlyphSeries, LineSeries, Tooltip, TooltipProvider, XYChart } from '@visx/xychart'
import {
Axis,
DataProvider,
GlyphSeries,
LineSeries,
Tooltip,
TooltipProvider,
XYChart,
EventEmitterProvider,
} from '@visx/xychart'
import { RenderTooltipParams } from '@visx/xychart/lib/components/Tooltip'
import { XYCHART_EVENT_SOURCE } from '@visx/xychart/lib/constants'
import isValidNumber from '@visx/xychart/lib/typeguards/isValidNumber'
@ -9,7 +18,7 @@ import { EventHandlerParams } from '@visx/xychart/lib/types'
import classNames from 'classnames'
import React, { ReactElement, useCallback, useMemo, useState, MouseEvent, useRef } from 'react'
import { noop } from 'rxjs'
import { LineChartContent as LineChartContentType } from 'sourcegraph'
import { LineChartContent as LineChartContentType, LineChartSeries } from 'sourcegraph'
import { DEFAULT_LINE_STROKE } from '../constants'
import { generateAccessors } from '../helpers/generate-accessors'
@ -43,9 +52,8 @@ const SCALES_CONFIG = {
}
// Line color accessor
export const getLineStroke = <Datum extends object>(
line: LineChartContentType<Datum, keyof Datum>['series'][number]
): string => line?.stroke ?? DEFAULT_LINE_STROKE
export const getLineStroke = <Datum extends object>(line: LineChartSeries<Datum>): string =>
line?.stroke ?? DEFAULT_LINE_STROKE
const stopPropagation = (event: React.MouseEvent): void => event.stopPropagation()
@ -70,6 +78,14 @@ export interface LineChartContentProps<Datum extends object>
onDatumLinkClick?: (event: React.MouseEvent) => void
}
export function LineChart<Datum extends object>(props: LineChartContentProps<Datum>): ReactElement {
return (
<EventEmitterProvider>
<LineChartContent {...props} />
</EventEmitterProvider>
)
}
/**
* Displays line chart content - line chart, tooltip, active point
*/
@ -220,7 +236,7 @@ export function LineChartContent<Datum extends object>(props: LineChartContentPr
const hoveredDatumLink = hoveredDatum
? hoveredDatumLinks[+hoveredDatum.datum.x] ?? hoveredDatumLinks[hoveredDatum.index]
: null
const rootClasses = classNames(styles.content, { [styles.contentWithCursor]: !!hoveredDatumLink })
const rootClasses = classNames({ [styles.contentWithCursor]: !!hoveredDatumLink })
return (
<div className={classNames(rootClasses, 'percy-inactive-element')} data-testid="line-chart__content">

View File

@ -1,3 +1,4 @@
export { LineChart } from './LineChart'
export { LineChart, LegendBlock, LegendItem } from './LineChart'
export { LineChartSettingsContext, LineChartLayoutOrientation } from './line-chart-settings-provider'
export { EMPTY_DATA_POINT_VALUE, DEFAULT_LINE_STROKE } from './constants'
export { getLineStroke } from './components/LineChartContent'

View File

@ -317,7 +317,7 @@ commands:
install: yarn --no-progress
env:
WEBPACK_SERVE_INDEX: true
SOURCEGRAPH_API_URL: https://sourcegraph.com
SOURCEGRAPH_API_URL: https://k8s.sgdev.org
web-standalone-http-prod:
cmd: yarn workspace @sourcegraph/web serve:prod