Skip to content
This repository has been archived by the owner on Sep 30, 2024. It is now read-only.

Commit

Permalink
Code Insights: Refactor creation UI lang stats preview (#33553)
Browse files Browse the repository at this point in the history
* Add insight card abstraction

* Adjust lang stats live preview chart

* Fix default code insights backend ts types
  • Loading branch information
vovakulikov authored Apr 8, 2022
1 parent 9e96bbb commit 668bc01
Show file tree
Hide file tree
Showing 20 changed files with 525 additions and 95 deletions.
16 changes: 12 additions & 4 deletions client/web/src/charts/components/pie-chart/PieChart.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { ReactElement, useMemo, useState } from 'react'
import React, { ReactElement, SVGProps, useMemo, useState } from 'react'

import { Group } from '@visx/group'
import Pie, { PieArcDatum } from '@visx/shape/lib/shapes/Pie'
Expand All @@ -25,7 +25,7 @@ function getSubtitle<Datum>(arc: PieArcDatum<Datum>, total: number): string {
return `${((100 * arc.value) / total).toFixed(2)}%`
}

export interface PieChartProps<Datum> extends CategoricalLikeChart<Datum> {
export interface PieChartProps<Datum> extends CategoricalLikeChart<Datum>, SVGProps<SVGSVGElement> {
width: number
height: number
padding?: typeof DEFAULT_PADDING
Expand All @@ -40,8 +40,10 @@ export function PieChart<Datum>(props: PieChartProps<Datum>): ReactElement | nul
getDatumValue,
getDatumName,
getDatumColor,
getDatumLink,
getDatumLink = noop,
onDatumLinkClick = noop,
className,
...attributes
} = props

// We have to track which arc is hovered to change order of rendering.
Expand Down Expand Up @@ -76,7 +78,13 @@ export function PieChart<Datum>(props: PieChartProps<Datum>): ReactElement | nul
}

return (
<svg aria-label="Pie chart" width={width} height={height} className={styles.svg}>
<svg
{...attributes}
aria-label="Pie chart"
width={width}
height={height}
className={classNames(styles.svg, className)}
>
<Group top={centerY + padding.top} left={centerX + padding.left}>
<Pie data={sortedData} pieValue={getDatumValue} outerRadius={radius} cornerRadius={3}>
{pie => {
Expand Down
2 changes: 1 addition & 1 deletion client/web/src/charts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface CategoricalLikeChart<Datum> {
getDatumValue: (datum: Datum) => number
getDatumName: (datum: Datum) => string
getDatumColor: (datum: Datum) => string | undefined
getDatumLink: (datum: Datum) => string | undefined
getDatumLink?: (datum: Datum) => string | undefined | void
onDatumLinkClick?: (event: React.MouseEvent) => void
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
.view {
display: flex;
flex-direction: column;
padding: 1rem 1rem;
cursor: default;
outline: none;

&:focus,
&:focus-within {
box-shadow: var(--focus-box-shadow);
}
}

.header {
margin-bottom: 0.75rem;
}

.header-content {
display: flex;
}

.title {
// Truncation for multiple lines
// stylelint-disable-next-line value-no-vendor-prefix
display: -webkit-box;
overflow: hidden;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
margin-bottom: 0.25rem;
flex-grow: 1;
}

.action {
align-self: start;
display: flex;
align-items: center;
// Move icons in action panel visually closer to card border
margin-right: -0.5rem;
margin-top: -0.25rem;
}

.subtitle {
flex-basis: 100%;
}

.error-boundary {
padding-top: 0;
}

// Content states

.loading-content {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import React from 'react'

import { Meta, Story } from '@storybook/react'
import { noop } from 'lodash'
import DotsVerticalIcon from 'mdi-react/DotsVerticalIcon'
import FilterOutlineIcon from 'mdi-react/FilterOutlineIcon'

import { ErrorAlert } from '@sourcegraph/branded/src/components/alerts'
import { Button, Menu, MenuButton, MenuItem, MenuList } from '@sourcegraph/wildcard'

import { getLineColor, LegendItem, LegendList, ParentSize, Series } from '../../../../../charts'
import { WebStory } from '../../../../../components/WebStory'
import { SeriesChart } from '../chart'
import { SeriesBasedChartTypes } from '../types'

import * as Card from './InsightCard'

export default {
title: 'web/insights/shared-components',
decorators: [story => <WebStory>{() => story()}</WebStory>],
} as Meta

export const InsightCardShowcase: Story = () => (
<main style={{ display: 'flex', flexWrap: 'wrap', gap: '1rem' }}>
<section>
<h2>Empty view</h2>
<Card.Root style={{ width: '400px', height: '400px' }}>
<Card.Header title="Empty card" />
</Card.Root>
</section>

<section>
<h2>View with loading content</h2>
<Card.Root style={{ width: '400px', height: '400px' }}>
<Card.Header title="Loading insight card" subtitle="View with loading content example" />
<Card.Loading>Loading insight</Card.Loading>
</Card.Root>
</section>

<section>
<h2>View with error-like content</h2>
<Card.Root style={{ width: '400px', height: '400px' }}>
<Card.Header title="Loading insight card" subtitle="View with errored content example" />
<ErrorAlert error={new Error("We couldn't find code insight")} />
</Card.Root>
</section>

<section>
<h2>Card with banner content (resizing state)</h2>
<Card.Root style={{ width: '400px', height: '400px' }}>
<Card.Header title="Resizing insight card" subtitle="Resizing insight card" />
<Card.Banner>Resizing</Card.Banner>
</Card.Root>
</section>

<section>
<h2>Card with insight chart</h2>
<InsightCardWithChart />
</section>

<section>
<h2>View with context action item</h2>
<Card.Root style={{ width: 400, height: 400 }}>
<Card.Header
title="Chart view and looooooong loooooooooooooooong name of insight card block"
subtitle="Subtitle chart description"
>
<Button variant="icon" className="p-1">
<FilterOutlineIcon size="1rem" />
</Button>
<Menu>
<MenuButton variant="icon" className="p-1">
<DotsVerticalIcon size={16} />
</MenuButton>
<MenuList>
<MenuItem onSelect={noop}>Create</MenuItem>
<MenuItem onSelect={noop}>Update</MenuItem>
<MenuItem onSelect={noop}>Delete</MenuItem>
</MenuList>
</Menu>
</Card.Header>
</Card.Root>
</section>
</main>
)

interface StandardDatum {
a: number | null
b: number | null
c: number | null
x: number
}

const DATA: StandardDatum[] = [
{
x: 1588965700286 - 4 * 24 * 60 * 60 * 1000,
a: 4000,
b: 15000,
c: 5000,
},
{
x: 1588965700286 - 3 * 24 * 60 * 60 * 1000,
a: 4000,
b: 26000,
c: 5000,
},
{
x: 1588965700286 - 2 * 24 * 60 * 60 * 1000,
a: 5600,
b: 20000,
c: 5000,
},
{
x: 1588965700286 - 24 * 60 * 60 * 1000,
a: 9800,
b: 19000,
c: 5000,
},
{
x: 1588965700286,
a: 6000,
b: 17000,
c: 5000,
},
]

const SERIES: Series<StandardDatum>[] = [
{
dataKey: 'a',
name: 'A metric',
color: 'var(--blue)',
},
{
dataKey: 'b',
name: 'B metric',
color: 'var(--orange)',
},
{
dataKey: 'c',
name: 'C metric',
color: 'var(--indigo)',
},
]

const getXValue = (datum: { x: number }) => new Date(datum.x)

function InsightCardWithChart() {
return (
<Card.Root style={{ width: '400px', height: '400px' }}>
<Card.Header title="Insight with chart" subtitle="CSS migration insight chart">
<Button variant="icon" className="p-1">
<FilterOutlineIcon size="1rem" />
</Button>
<Menu>
<MenuButton variant="icon" className="p-1">
<DotsVerticalIcon size={16} />
</MenuButton>
<MenuList>
<MenuItem onSelect={noop}>Create</MenuItem>
<MenuItem onSelect={noop}>Update</MenuItem>
<MenuItem onSelect={noop}>Delete</MenuItem>
</MenuList>
</Menu>
</Card.Header>
<ParentSize>
{parent => (
<SeriesChart
type={SeriesBasedChartTypes.Line}
data={DATA}
series={SERIES}
getXValue={getXValue}
width={parent.width}
height={parent.height}
/>
)}
</ParentSize>
<LegendList className="mt-3">
{SERIES.map(line => (
<LegendItem key={line.dataKey.toString()} color={getLineColor(line)} name={line.name} />
))}
</LegendList>
</Card.Root>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import React, { forwardRef, HTMLAttributes, ReactNode } from 'react'

import classNames from 'classnames'
import { useLocation } from 'react-router-dom'

import { Card, ForwardReferenceComponent, LoadingSpinner } from '@sourcegraph/wildcard'

import { ErrorBoundary } from '../../../../../components/ErrorBoundary'

import styles from './InsightCard.module.scss'

const InsightCard = forwardRef((props, reference) => {
const { title, children, className, as = 'section', ...otherProps } = props

return (
<Card as={as} tabIndex={0} ref={reference} {...otherProps} className={classNames(className, styles.view)}>
<ErrorBoundary location={useLocation()} className={styles.errorBoundary}>
{children}
</ErrorBoundary>
</Card>
)
}) as ForwardReferenceComponent<'section'>

interface InsightCardTitleProps {
title: string
subtitle?: string

/**
* It's primarily conceived as a slot for card actions (like filter buttons) that render
* element right after header text at the right top corner of the card.
*/
children?: ReactNode
}

const InsightCardHeader = forwardRef((props, reference) => {
const { as: Component = 'header', title, subtitle, className, children, ...attributes } = props

return (
<Component {...attributes} ref={reference} className={classNames(styles.header, className)}>
<div className={styles.headerContent}>
<h4 title={title} className={styles.title}>
{title}
</h4>

{children && (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
<div
// View component usually is rendered within a view grid component. To suppress
// bad click that lead to card DnD events in view grid we stop event bubbling for
// clicks.
onClick={event => event.stopPropagation()}
onMouseDown={event => event.stopPropagation()}
className={styles.action}
>
{children}
</div>
)}
</div>

{subtitle && <div className={styles.subtitle}>{subtitle}</div>}
</Component>
)
}) as ForwardReferenceComponent<'header', InsightCardTitleProps>

const InsightCardLoading: React.FunctionComponent = props => (
<InsightCardBanner>
<LoadingSpinner />
{props.children}
</InsightCardBanner>
)

const InsightCardBanner: React.FunctionComponent<HTMLAttributes<HTMLDivElement>> = props => (
<div {...props} className={classNames(styles.loadingContent, props.className)}>
{props.children}
</div>
)

const Root = InsightCard
const Header = InsightCardHeader
const Loading = InsightCardLoading
const Banner = InsightCardBanner

export {
InsightCard,
InsightCardHeader,
InsightCardLoading,
InsightCardBanner,
// * as Card imports
Root,
Header,
Loading,
Banner,
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import React from 'react'

import { Meta, Story } from '@storybook/react'

import { WebStory } from '../../../../../components/WebStory'
import { CategoricalBasedChartTypes } from '../types'
import { WebStory } from '../../../../../../components/WebStory'
import { CategoricalBasedChartTypes } from '../../types'

import { CategoricalChart } from './CategoricalChart'

Expand Down
Loading

0 comments on commit 668bc01

Please sign in to comment.