Add Code Insights independent MVP (#19600)

* Add sandboxes directory for MVPs

* Change code insights imports in consumers

* Add new API context approach for mocking data in MVP
This commit is contained in:
Vova Kulikov 2021-04-14 13:04:19 +03:00 committed by GitHub
parent 6d2604a09d
commit 7dfc4416f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 675 additions and 187 deletions

View File

@ -7,6 +7,7 @@
- **eslint-plugin-sourcegraph**: Not published package with custom ESLint rules for Sourcegraph. Isn't intended for reuse by other repositories in the Sourcegraph org.
- **extension-api**: The Sourcegraph extension API types for the _Sourcegraph extensions_. Published as `sourcegraph`.
- **extension-api-types**: The Sourcegraph extension API types for _client applications_ that embed Sourcegraph extensions and need to communicate with them. Published as `@sourcegraph/extension-api-types`.
- **sandboxes**: All demos-mvp (minimum viable product) for the Sourcegraph web application.
- **shared**: Contains common TypeScript/React/SCSS client code shared between the browser extension and the web app. Everything in this package is code-host agnostic.
- **branded**: Contains React components and implements the visual design language we use across our web app and e.g. in the options menu of the browser extension. Over time, components from `shared` and `branded` packages should be moved into the `wildcard` package.
- **wildcard**: Package that encapsulates storybook configuration and contains our Wildcard design system components. If we're using a component in two or more different areas (e.g. `web-app` and `browser-extension`) then it should live in the `wildcard` package. Otherwise the components should be better colocated with the code where they're actually used.

View File

@ -0,0 +1,13 @@
// @ts-check
const baseConfig = require('../../../.eslintrc.js')
module.exports = {
extends: '../../../.eslintrc.js',
parserOptions: {
...baseConfig.parserOptions,
project: [__dirname + '/tsconfig.json'],
},
rules: {},
overrides: baseConfig.overrides,
}

View File

@ -0,0 +1,3 @@
{
"extends": ["@sourcegraph/stylelint-config"]
}

View File

@ -0,0 +1,9 @@
// @ts-check
/** @type {import('@babel/core').TransformOptions} */
const config = {
extends: '../../../babel.config.js',
plugins: ['react-refresh/babel'],
}
module.exports = config

View File

@ -0,0 +1,13 @@
{
"private": true,
"name": "@sourcegraph/code-insights-demo",
"version": "0.0.1",
"description": "Sourcegraph code insight mvp",
"sideEffects": false,
"license": "Apache-2.0",
"scripts": {
"eslint": "eslint --cache '**/*.[jt]s?(x)'",
"stylelint": "stylelint 'src/**/*.scss' --quiet",
"serve": "webpack serve --hot"
}
}

View File

@ -0,0 +1,50 @@
import { createBrowserHistory } from 'history'
import React, { ReactElement, useState } from 'react'
import { render } from 'react-dom'
import { BrowserRouter } from 'react-router-dom'
import { setLinkComponent } from '@sourcegraph/shared/src/components/Link'
import { SearchPatternType } from '@sourcegraph/shared/src/graphql-operations'
import { EMPTY_SETTINGS_CASCADE } from '@sourcegraph/shared/src/settings/settings'
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { RouterLinkOrAnchor } from '@sourcegraph/web/src/components/RouterLinkOrAnchor'
import { InsightsApiContext, InsightsPage } from '@sourcegraph/web/src/insights'
import { MockInsightsApi } from './mock-api'
import '@sourcegraph/web/src/SourcegraphWebApp.scss'
const history = createBrowserHistory()
const mockAPI = new MockInsightsApi()
setLinkComponent(RouterLinkOrAnchor)
export function App(): ReactElement {
const [patternType, setPatterType] = useState(SearchPatternType.literal)
const [caseSensitive, setCaseSensitive] = useState(false)
return (
<BrowserRouter>
<InsightsApiContext.Provider value={mockAPI}>
<InsightsPage
versionContext={undefined}
telemetryService={NOOP_TELEMETRY_SERVICE}
copyQueryButton={false}
caseSensitive={caseSensitive}
setCaseSensitivity={setCaseSensitive}
setPatternType={setPatterType}
patternType={patternType}
settingsCascade={EMPTY_SETTINGS_CASCADE}
globbing={false}
location={history.location}
history={history}
/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
// @ts-ignore
extensionsController={null}
/>
</InsightsApiContext.Provider>
</BrowserRouter>
)
}
render(<App />, document.querySelector('#root'))

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Code insights magic developer environment</title>
</head>
<body class="theme-light ">
<div id="root"></div>
</body>
</html>

View File

@ -0,0 +1,160 @@
import { Observable, of } from 'rxjs'
import { InsightsAPI } from '@sourcegraph/web/src/insights/core/backend/insights-api'
import {
ViewInsightProviderResult,
ViewInsightProviderSourceType,
} from '@sourcegraph/web/src/insights/core/backend/types'
export const MOCK_VIEWS = [
{
id: 'searchInsights.searchInsights.insight.graphQLTypesMigration.insightsPage',
view: {
title: 'Migration to new GraphQL TS types',
content: [
{
chart: 'line' as const,
data: [
{
date: 1595624400000,
'Imports of old GQL.* types': 259,
'Imports of new graphql-operations types': 7,
},
{
date: 1599253200000,
'Imports of old GQL.* types': 190,
'Imports of new graphql-operations types': 191,
},
{
date: 1602882000000,
'Imports of old GQL.* types': 182,
'Imports of new graphql-operations types': 210,
},
{
date: 1606510800000,
'Imports of old GQL.* types': 179,
'Imports of new graphql-operations types': 256,
},
{
date: 1610139600000,
'Imports of old GQL.* types': 139,
'Imports of new graphql-operations types': 335,
},
{
date: 1613768400000,
'Imports of old GQL.* types': 139,
'Imports of new graphql-operations types': 352,
},
{
date: 1617397200000,
'Imports of old GQL.* types': 139,
'Imports of new graphql-operations types': 362,
},
],
series: [
{
dataKey: 'Imports of old GQL.* types',
name: 'Imports of old GQL.* types',
stroke: 'var(--oc-red-7)',
linkURLs: [
'https://sourcegraph.com/search?q=repo%3A%5Egithub%5C.com%2Fsourcegraph%2Fsourcegraph%24+type%3Adiff+after%3A2020-06-13T00%3A00%3A00%2B03%3A00+before%3A2020-07-25T00%3A00%3A00%2B03%3A00+patternType%3Aregex+case%3Ayes+%5C*%5Csas%5CsGQL',
'https://sourcegraph.com/search?q=repo%3A%5Egithub%5C.com%2Fsourcegraph%2Fsourcegraph%24+type%3Adiff+after%3A2020-07-25T00%3A00%3A00%2B03%3A00+before%3A2020-09-05T00%3A00%3A00%2B03%3A00+patternType%3Aregex+case%3Ayes+%5C*%5Csas%5CsGQL',
'https://sourcegraph.com/search?q=repo%3A%5Egithub%5C.com%2Fsourcegraph%2Fsourcegraph%24+type%3Adiff+after%3A2020-09-05T00%3A00%3A00%2B03%3A00+before%3A2020-10-17T00%3A00%3A00%2B03%3A00+patternType%3Aregex+case%3Ayes+%5C*%5Csas%5CsGQL',
'https://sourcegraph.com/search?q=repo%3A%5Egithub%5C.com%2Fsourcegraph%2Fsourcegraph%24+type%3Adiff+after%3A2020-10-17T00%3A00%3A00%2B03%3A00+before%3A2020-11-28T00%3A00%3A00%2B03%3A00+patternType%3Aregex+case%3Ayes+%5C*%5Csas%5CsGQL',
'https://sourcegraph.com/search?q=repo%3A%5Egithub%5C.com%2Fsourcegraph%2Fsourcegraph%24+type%3Adiff+after%3A2020-11-28T00%3A00%3A00%2B03%3A00+before%3A2021-01-09T00%3A00%3A00%2B03%3A00+patternType%3Aregex+case%3Ayes+%5C*%5Csas%5CsGQL',
'https://sourcegraph.com/search?q=repo%3A%5Egithub%5C.com%2Fsourcegraph%2Fsourcegraph%24+type%3Adiff+after%3A2021-01-09T00%3A00%3A00%2B03%3A00+before%3A2021-02-20T00%3A00%3A00%2B03%3A00+patternType%3Aregex+case%3Ayes+%5C*%5Csas%5CsGQL',
'https://sourcegraph.com/search?q=repo%3A%5Egithub%5C.com%2Fsourcegraph%2Fsourcegraph%24+type%3Adiff+after%3A2021-02-20T00%3A00%3A00%2B03%3A00+before%3A2021-04-03T00%3A00%3A00%2B03%3A00+patternType%3Aregex+case%3Ayes+%5C*%5Csas%5CsGQL',
],
},
{
dataKey: 'Imports of new graphql-operations types',
name: 'Imports of new graphql-operations types',
stroke: 'var(--oc-blue-7)',
linkURLs: [
'https://sourcegraph.com/search?q=repo%3A%5Egithub%5C.com%2Fsourcegraph%2Fsourcegraph%24+type%3Adiff+after%3A2020-06-13T00%3A00%3A00%2B03%3A00+before%3A2020-07-25T00%3A00%3A00%2B03%3A00+patternType%3Aregexp+case%3Ayes+%2Fgraphql-operations%27',
'https://sourcegraph.com/search?q=repo%3A%5Egithub%5C.com%2Fsourcegraph%2Fsourcegraph%24+type%3Adiff+after%3A2020-07-25T00%3A00%3A00%2B03%3A00+before%3A2020-09-05T00%3A00%3A00%2B03%3A00+patternType%3Aregexp+case%3Ayes+%2Fgraphql-operations%27',
'https://sourcegraph.com/search?q=repo%3A%5Egithub%5C.com%2Fsourcegraph%2Fsourcegraph%24+type%3Adiff+after%3A2020-09-05T00%3A00%3A00%2B03%3A00+before%3A2020-10-17T00%3A00%3A00%2B03%3A00+patternType%3Aregexp+case%3Ayes+%2Fgraphql-operations%27',
'https://sourcegraph.com/search?q=repo%3A%5Egithub%5C.com%2Fsourcegraph%2Fsourcegraph%24+type%3Adiff+after%3A2020-10-17T00%3A00%3A00%2B03%3A00+before%3A2020-11-28T00%3A00%3A00%2B03%3A00+patternType%3Aregexp+case%3Ayes+%2Fgraphql-operations%27',
'https://sourcegraph.com/search?q=repo%3A%5Egithub%5C.com%2Fsourcegraph%2Fsourcegraph%24+type%3Adiff+after%3A2020-11-28T00%3A00%3A00%2B03%3A00+before%3A2021-01-09T00%3A00%3A00%2B03%3A00+patternType%3Aregexp+case%3Ayes+%2Fgraphql-operations%27',
'https://sourcegraph.com/search?q=repo%3A%5Egithub%5C.com%2Fsourcegraph%2Fsourcegraph%24+type%3Adiff+after%3A2021-01-09T00%3A00%3A00%2B03%3A00+before%3A2021-02-20T00%3A00%3A00%2B03%3A00+patternType%3Aregexp+case%3Ayes+%2Fgraphql-operations%27',
'https://sourcegraph.com/search?q=repo%3A%5Egithub%5C.com%2Fsourcegraph%2Fsourcegraph%24+type%3Adiff+after%3A2021-02-20T00%3A00%3A00%2B03%3A00+before%3A2021-04-03T00%3A00%3A00%2B03%3A00+patternType%3Aregexp+case%3Ayes+%2Fgraphql-operations%27',
],
},
],
xAxis: { dataKey: 'date', type: 'number', scale: 'time' },
},
],
},
source: ViewInsightProviderSourceType.Extension,
},
{
id: 'codeStatsInsights.languages.insightsPage',
view: {
title: 'Language usage',
content: [
{
chart: 'pie',
pies: [
{
data: [
{
name: 'Go',
totalLines: 363432,
fill: '#00ADD8',
linkURL:
'https://sourcegraph.com/stats?q=repo%3A%5Egithub%5C.com%2Fsourcegraph%2Fsourcegraph%24',
},
{
name: 'HTML',
totalLines: 224961,
fill: '#e34c26',
linkURL:
'https://sourcegraph.com/stats?q=repo%3A%5Egithub%5C.com%2Fsourcegraph%2Fsourcegraph%24',
},
{
name: 'TypeScript',
totalLines: 155381,
fill: '#2b7489',
linkURL:
'https://sourcegraph.com/stats?q=repo%3A%5Egithub%5C.com%2Fsourcegraph%2Fsourcegraph%24',
},
{
name: 'Markdown',
totalLines: 46675,
fill: '#083fa1',
linkURL:
'https://sourcegraph.com/stats?q=repo%3A%5Egithub%5C.com%2Fsourcegraph%2Fsourcegraph%24',
},
{
name: 'YAML',
totalLines: 25412,
fill: '#cb171e',
linkURL:
'https://sourcegraph.com/stats?q=repo%3A%5Egithub%5C.com%2Fsourcegraph%2Fsourcegraph%24',
},
{
name: 'Other',
totalLines: 56846,
fill: 'gray',
linkURL:
'https://sourcegraph.com/stats?q=repo%3A%5Egithub%5C.com%2Fsourcegraph%2Fsourcegraph%24',
},
],
dataKey: 'totalLines',
nameKey: 'name',
fillKey: 'fill',
linkURLKey: 'linkURL',
},
],
},
],
},
source: 'Extension',
},
] as ViewInsightProviderResult[]
export class MockInsightsApi implements InsightsAPI {
public getCombinedViews = (): Observable<ViewInsightProviderResult[]> => of(MOCK_VIEWS)
public getInsightCombinedViews = (): Observable<ViewInsightProviderResult[]> => this.getCombinedViews()
}

View File

@ -0,0 +1,28 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"baseUrl": ".",
"paths": {
"*": ["src/types/*", "../../shared/src/types/*", "*"],
},
"jsx": "react",
"rootDir": ".",
"outDir": "out",
"plugins": [
{
"name": "ts-graphql-plugin",
"schema": "../../../cmd/frontend/graphqlbackend/schema.graphql",
"tag": "gql",
},
],
},
"references": [
{ "path": "../../web" },
{ "path": "../../shared" },
{ "path": "../../branded" },
{ "path": "../../../schema" },
],
"include": ["**/*", ".*", "./src/**/*.json"],
"exclude": ["../../node_modules", "./node_modules", "./out", "src/end-to-end", "src/regression", "src/integration"],
}

View File

@ -0,0 +1,47 @@
const path = require('path')
const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: path.resolve(__dirname, './src/demo.tsx'),
output: {
path: path.resolve(__dirname, 'dist'), // Note: Physical files are only output by the production build task `npm run build`.
publicPath: '/',
filename: '[name].bundle.js',
},
target: 'web', // necessary per https://webpack.github.io/docs/testing.html#compile-and-test
mode: 'development',
devtool: 'source-map',
resolve: {
alias: { react: require.resolve('react') },
extensions: ['.ts', '.tsx', '.js', '.jsx'],
},
module: {
rules: [
{
test: /\.(ts|js)x?$/,
exclude: /node_modules/,
use: ['babel-loader'],
},
{
test: /\.(scss)$/i,
use: [
'style-loader',
{
loader: 'css-loader',
},
'sass-loader',
],
},
],
},
plugins: [
new ReactRefreshPlugin({
overlay: false,
}),
new HtmlWebpackPlugin({
template: './src/index.html',
}),
],
}

View File

@ -121,6 +121,7 @@ body,
@import './user/UserAvatar';
@import './user/area/UserArea';
@import './org/OrgsArea';
@import 'insights/pages/InsightsPage';
@import './components/Badge';
@import './components/Collapsible';
@import './components/DismissibleAlert';

View File

@ -32,7 +32,7 @@ import { ExtensionAreaRoute } from './extensions/extension/ExtensionArea'
import { ExtensionAreaHeaderNavItem } from './extensions/extension/ExtensionAreaHeader'
import { ExtensionsAreaRoute } from './extensions/ExtensionsArea'
import { ExtensionsAreaHeaderActionButton } from './extensions/ExtensionsAreaHeader'
import { logCodeInsightsChanges } from './insights/analytics'
import { logCodeInsightsChanges } from './insights'
import { KeyboardShortcutsProps } from './keyboardShortcuts/keyboardShortcuts'
import { Layout, LayoutProps } from './Layout'
import { updateUserSessionStores } from './marketing/util'

View File

@ -1,116 +0,0 @@
import { combineLatest, Observable, of } from 'rxjs'
import { catchError, map } from 'rxjs/operators'
import { LineChartContent } from 'sourcegraph'
import { ViewProviderResult } from '@sourcegraph/shared/src/api/extension/extensionHostApi'
import { dataOrThrowErrors, gql } from '@sourcegraph/shared/src/graphql/graphql'
import { asError } from '@sourcegraph/shared/src/util/errors'
import { requestGraphQL } from '../backend/graphql'
import { InsightsResult, InsightFields } from '../graphql-operations'
const insightFieldsFragment = gql`
fragment InsightFields on Insight {
title
description
series {
label
points {
dateTime
value
}
}
}
`
function fetchBackendInsights(): Observable<InsightFields[]> {
return requestGraphQL<InsightsResult>(gql`
query Insights {
insights {
nodes {
...InsightFields
}
}
}
${insightFieldsFragment}
`).pipe(
map(dataOrThrowErrors),
map(data => data.insights?.nodes ?? [])
)
}
export enum ViewInsightProviderSourceType {
Backend = 'Backend',
Extension = 'Extension',
}
export interface ViewInsightProviderResult extends ViewProviderResult {
/** The source of view provider to distinguish between data from extension and data from backend */
source: ViewInsightProviderSourceType
}
export function getCombinedViews(
getExtensionsInsights: () => Observable<ViewProviderResult[]>
): Observable<ViewInsightProviderResult[]> {
return combineLatest([
getExtensionsInsights().pipe(
map(extensionInsights =>
extensionInsights.map(insight => ({ ...insight, source: ViewInsightProviderSourceType.Extension }))
)
),
fetchBackendInsights().pipe(
map(backendInsights =>
backendInsights.map(
(insight, index): ViewInsightProviderResult => ({
id: `Backend insight ${index + 1}`,
view: {
title: insight.title,
subtitle: insight.description,
content: [backendInsightToViewContent(insight)],
},
source: ViewInsightProviderSourceType.Backend,
})
)
),
catchError(error =>
of<ViewInsightProviderResult[]>([
{
id: 'Backend insight',
view: asError(error),
source: ViewInsightProviderSourceType.Backend,
},
])
)
),
]).pipe(map(([extensionViews, backendInsights]) => [...backendInsights, ...extensionViews]))
}
function backendInsightToViewContent(
insight: InsightFields
): LineChartContent<{ dateTime: number; [seriesKey: string]: number }, 'dateTime'> {
const dataByXValue = new Map<string, { dateTime: number; [seriesKey: string]: number }>()
for (const [seriesIndex, series] of insight.series.entries()) {
for (const point of series.points) {
let dataObject = dataByXValue.get(point.dateTime)
if (!dataObject) {
dataObject = {
dateTime: Date.parse(point.dateTime),
}
dataByXValue.set(point.dateTime, dataObject)
}
dataObject[`series${seriesIndex}`] = point.value
}
}
return {
chart: 'line',
data: [...dataByXValue.values()],
series: insight.series.map((series, index) => ({
name: series.label,
dataKey: `series${index}`,
})),
xAxis: {
dataKey: 'dateTime',
scale: 'time',
type: 'number',
},
}
}

View File

@ -1,8 +1,7 @@
import React from 'react'
import { LinkWithIcon } from '../components/LinkWithIcon'
import { InsightsIcon } from './icon'
import { LinkWithIcon } from '../../../components/LinkWithIcon'
import { InsightsIcon } from '../Icons'
export const InsightsNavItem: React.FunctionComponent = () => (
<LinkWithIcon

View File

@ -1,8 +1,8 @@
@import '../../views/ViewContent';
@import '../../../views/ViewContent';
@import 'react-grid-layout/css/styles';
@import 'react-resizable/css/styles';
.view-grid {
.insights-view-grid {
.react-resizable-handle {
background: none;
cursor: nwse-resize;

View File

@ -9,16 +9,16 @@ import { LoadingSpinner } from '@sourcegraph/react-loading-spinner'
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { isErrorLike } from '@sourcegraph/shared/src/util/errors'
import { ErrorAlert } from '../../components/alerts'
import { ErrorBoundary } from '../../components/ErrorBoundary'
import { ViewInsightProviderResult, ViewInsightProviderSourceType } from '../../insights/backend'
import { ViewContent, ViewContentProps } from '../../views/ViewContent'
import { ErrorAlert } from '../../../components/alerts'
import { ErrorBoundary } from '../../../components/ErrorBoundary'
import { ViewContent, ViewContentProps } from '../../../views/ViewContent'
import { ViewInsightProviderResult, ViewInsightProviderSourceType } from '../../core/backend/types'
// TODO use a method to get width that also triggers when file explorer is closed
// (WidthProvider only listens to window resize events)
const ResponsiveGridLayout = WidthProvider(Responsive)
export interface ViewGridProps
export interface InsightsViewGridProps
extends Omit<ViewContentProps, 'viewContent' | 'viewID' | 'containerClassName'>,
TelemetryProps {
views: ViewInsightProviderResult[]
@ -97,7 +97,7 @@ const getInsightViewIcon = (source: ViewInsightProviderSourceType): MdiReactIcon
}
}
export const ViewGrid: React.FunctionComponent<ViewGridProps> = props => {
export const InsightsViewGrid: React.FunctionComponent<InsightsViewGridProps> = props => {
const onResizeOrDragStart: ReactGridLayout.ItemCallback = useCallback(
(_layout, item) => {
try {
@ -110,7 +110,7 @@ export const ViewGrid: React.FunctionComponent<ViewGridProps> = props => {
)
return (
<div className={classNames(props.className, 'view-grid')}>
<div className={classNames(props.className, 'insights-view-grid')}>
<ResponsiveGridLayout
breakpoints={breakpoints}
layouts={viewsToReactGridLayouts(props.views)}
@ -123,7 +123,7 @@ export const ViewGrid: React.FunctionComponent<ViewGridProps> = props => {
onDragStart={onResizeOrDragStart}
>
{props.views.map(({ id, view, source }) => (
<div key={id} className={classNames('card view-grid__item')}>
<div key={id} className={classNames('card insights-view-grid__item')}>
<ErrorBoundary
location={props.location}
extraContext={
@ -156,14 +156,16 @@ export const ViewGrid: React.FunctionComponent<ViewGridProps> = props => {
</>
) : (
<>
<h3 className="view-grid__view-title">{view.title}</h3>
{view.subtitle && <div className="view-grid__view-subtitle">{view.subtitle}</div>}
<h3 className="insights-view-grid__view-title">{view.title}</h3>
{view.subtitle && (
<div className="insights-view-grid__view-subtitle">{view.subtitle}</div>
)}
<ViewContent
{...props}
settingsCascade={props.settingsCascade}
viewContent={view.content}
viewID={id}
containerClassName="view-grid__item"
containerClassName="insights-view-grid__item"
/>
</>
)}

View File

@ -0,0 +1,4 @@
export { InsightsIcon } from './Icons'
export { InsightsNavItem } from './InsightsNavLink/InsightsNavLink'
export { InsightsViewGrid } from './InsightsViewGrid/InsightsViewGrid'
export type { InsightsViewGridProps } from './InsightsViewGrid/InsightsViewGrid'

View File

@ -0,0 +1,6 @@
import React from 'react'
import { InsightsAPI } from './insights-api'
import { ApiService } from './types'
export const InsightsApiContext = React.createContext<ApiService>(new InsightsAPI())

View File

@ -0,0 +1,60 @@
import { Remote } from 'comlink'
import { combineLatest, from, Observable, of } from 'rxjs'
import { catchError, map, switchMap } from 'rxjs/operators'
import { wrapRemoteObservable } from '@sourcegraph/shared/src/api/client/api/common'
import { FlatExtensionHostAPI } from '@sourcegraph/shared/src/api/contract'
import { ViewProviderResult } from '@sourcegraph/shared/src/api/extension/extensionHostApi'
import { asError } from '@sourcegraph/shared/src/util/errors'
import { fetchBackendInsights } from './requests/fetch-backend-insights'
import { ApiService, ViewInsightProviderResult, ViewInsightProviderSourceType } from './types'
import { createViewContent } from './utils/create-view-content'
/** Main API service to get data for code insights */
export class InsightsAPI implements ApiService {
/** Get combined (backend and extensions) code insights */
public getCombinedViews = (
getExtensionsInsights: () => Observable<ViewProviderResult[]>
): Observable<ViewInsightProviderResult[]> =>
combineLatest([
getExtensionsInsights().pipe(
map(extensionInsights =>
extensionInsights.map(insight => ({ ...insight, source: ViewInsightProviderSourceType.Extension }))
)
),
fetchBackendInsights().pipe(
map(backendInsights =>
backendInsights.map(
(insight, index): ViewInsightProviderResult => ({
id: `Backend insight ${index + 1}`,
view: {
title: insight.title,
subtitle: insight.description,
content: [createViewContent(insight)],
},
source: ViewInsightProviderSourceType.Backend,
})
)
),
catchError(error =>
of<ViewInsightProviderResult[]>([
{
id: 'Backend insight',
view: asError(error),
source: ViewInsightProviderSourceType.Backend,
},
])
)
),
]).pipe(map(([extensionViews, backendInsights]) => [...backendInsights, ...extensionViews]))
public getInsightCombinedViews = (
extensionApi: Promise<Remote<FlatExtensionHostAPI>>
): Observable<ViewInsightProviderResult[]> =>
this.getCombinedViews(() =>
from(extensionApi).pipe(
switchMap(extensionHostAPI => wrapRemoteObservable(extensionHostAPI.getInsightsViews({})))
)
)
}

View File

@ -0,0 +1,36 @@
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'
import { dataOrThrowErrors, gql } from '@sourcegraph/shared/src/graphql/graphql'
import { requestGraphQL } from '../../../../backend/graphql'
import { InsightFields, InsightsResult } from '../../../../graphql-operations'
const insightFieldsFragment = gql`
fragment InsightFields on Insight {
title
description
series {
label
points {
dateTime
value
}
}
}
`
export function fetchBackendInsights(): Observable<InsightFields[]> {
return requestGraphQL<InsightsResult>(gql`
query Insights {
insights {
nodes {
...InsightFields
}
}
}
${insightFieldsFragment}
`).pipe(
map(dataOrThrowErrors),
map(data => data.insights?.nodes ?? [])
)
}

View File

@ -0,0 +1,24 @@
import { Remote } from 'comlink'
import { Observable } from 'rxjs'
import { FlatExtensionHostAPI } from '@sourcegraph/shared/src/api/contract'
import { ViewProviderResult } from '@sourcegraph/shared/src/api/extension/extensionHostApi'
export enum ViewInsightProviderSourceType {
Backend = 'Backend',
Extension = 'Extension',
}
export interface ViewInsightProviderResult extends ViewProviderResult {
/** The source of view provider to distinguish between data from extension and data from backend */
source: ViewInsightProviderSourceType
}
export interface ApiService {
getCombinedViews: (
getExtensionsInsights: () => Observable<ViewProviderResult[]>
) => Observable<ViewInsightProviderResult[]>
getInsightCombinedViews: (
extensionApi: Promise<Remote<FlatExtensionHostAPI>>
) => Observable<ViewInsightProviderResult[]>
}

View File

@ -0,0 +1,34 @@
import { LineChartContent } from 'sourcegraph'
import { InsightFields } from '../../../../graphql-operations'
export function createViewContent(
insight: InsightFields
): LineChartContent<{ dateTime: number; [seriesKey: string]: number }, 'dateTime'> {
const dataByXValue = new Map<string, { dateTime: number; [seriesKey: string]: number }>()
for (const [seriesIndex, series] of insight.series.entries()) {
for (const point of series.points) {
let dataObject = dataByXValue.get(point.dateTime)
if (!dataObject) {
dataObject = {
dateTime: Date.parse(point.dateTime),
}
dataByXValue.set(point.dateTime, dataObject)
}
dataObject[`series${seriesIndex}`] = point.value
}
}
return {
chart: 'line',
data: [...dataByXValue.values()],
series: insight.series.map((series, index) => ({
name: series.label,
dataKey: `series${index}`,
})),
xAxis: {
dataKey: 'dateTime',
scale: 'time',
type: 'number',
},
}
}

View File

@ -0,0 +1,10 @@
// Pages exports
export { InsightsPage } from './pages/InsightsPage'
// Core insights exports
export { InsightsApiContext } from './core/backend/api-provider'
export * from './core/analytics'
// Public Insights components
export { InsightsViewGrid } from './components'
export type { InsightsViewGridProps } from './components'

View File

@ -0,0 +1 @@
@import '../components/InsightsViewGrid/InsightsViewGrid';

View File

@ -1,37 +1,29 @@
import GearIcon from 'mdi-react/GearIcon'
import PlusIcon from 'mdi-react/PlusIcon'
import React, { useCallback, useEffect, useMemo } from 'react'
import { from } from 'rxjs'
import { switchMap } from 'rxjs/operators'
import React, { useCallback, useEffect, useMemo, useContext } from 'react'
import { LoadingSpinner } from '@sourcegraph/react-loading-spinner'
import { wrapRemoteObservable } from '@sourcegraph/shared/src/api/client/api/common'
import { Link } from '@sourcegraph/shared/src/components/Link'
import { ExtensionsControllerProps } from '@sourcegraph/shared/src/extensions/controller'
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { useObservable } from '@sourcegraph/shared/src/util/useObservable'
import { FeedbackBadge } from '../components/FeedbackBadge'
import { Page } from '../components/Page'
import { PageHeader } from '../components/PageHeader'
import { ViewGrid, ViewGridProps } from '../repo/tree/ViewGrid'
import { FeedbackBadge } from '../../components/FeedbackBadge'
import { Page } from '../../components/Page'
import { PageHeader } from '../../components/PageHeader'
import { InsightsIcon, InsightsViewGrid, InsightsViewGridProps } from '../components'
import { InsightsApiContext } from '../core/backend/api-provider'
import { getCombinedViews } from './backend'
import { InsightsIcon } from './icon'
interface InsightsPageProps extends ExtensionsControllerProps, Omit<ViewGridProps, 'views'>, TelemetryProps {}
interface InsightsPageProps extends ExtensionsControllerProps, Omit<InsightsViewGridProps, 'views'>, TelemetryProps {}
export const InsightsPage: React.FunctionComponent<InsightsPageProps> = props => {
const { getInsightCombinedViews } = useContext(InsightsApiContext)
const views = useObservable(
useMemo(
() =>
getCombinedViews(() =>
from(props.extensionsController.extHostAPI).pipe(
switchMap(extensionHostAPI => wrapRemoteObservable(extensionHostAPI.getInsightsViews({})))
)
),
[props.extensionsController]
)
useMemo(() => getInsightCombinedViews(props.extensionsController?.extHostAPI), [
props.extensionsController,
getInsightCombinedViews,
])
)
useEffect(() => {
@ -73,7 +65,7 @@ export const InsightsPage: React.FunctionComponent<InsightsPageProps> = props =>
<LoadingSpinner className="my-4" />
</div>
) : (
<ViewGrid {...props} views={views} />
<InsightsViewGrid {...props} views={views} />
)}
</Page>
</div>

View File

@ -4,7 +4,7 @@ import React from 'react'
import { BatchChangesNavItem } from '../batches/BatchChangesNavItem'
import { CodeMonitoringNavItem } from '../code-monitoring/CodeMonitoringNavItem'
import { WebStory } from '../components/WebStory'
import { InsightsNavItem } from '../insights/InsightsNavLink'
import { InsightsNavItem } from '../insights/components/InsightsNavLink/InsightsNavLink'
import { MenuNavItem } from './MenuNavItem'

View File

@ -18,7 +18,7 @@ import { CodeMonitoringProps } from '../code-monitoring'
import { CodeMonitoringNavItem } from '../code-monitoring/CodeMonitoringNavItem'
import { LinkWithIcon } from '../components/LinkWithIcon'
import { WebActionsNavItems, WebCommandListPopoverButton } from '../components/shared'
import { InsightsNavItem } from '../insights/InsightsNavLink'
import { InsightsNavItem } from '../insights/components/InsightsNavLink/InsightsNavLink'
import {
KeyboardShortcutsProps,
KEYBOARD_SHORTCUT_SHOW_COMMAND_PALETTE,

View File

@ -1,5 +1,5 @@
@import '../../search/input/SearchButton';
@import './ViewGrid';
@import '../../insights/components/InsightsViewGrid/InsightsViewGrid';
@import './TreeEntriesSection';
.tree-page {

View File

@ -8,7 +8,7 @@ import SourceCommitIcon from 'mdi-react/SourceCommitIcon'
import SourceRepositoryIcon from 'mdi-react/SourceRepositoryIcon'
import TagIcon from 'mdi-react/TagIcon'
import UserIcon from 'mdi-react/UserIcon'
import React, { useState, useMemo, useCallback, useEffect } from 'react'
import React, { useState, useMemo, useCallback, useEffect, useContext } from 'react'
import { Link, Redirect } from 'react-router-dom'
import { Observable, EMPTY, from } from 'rxjs'
import { catchError, map, switchMap } from 'rxjs/operators'
@ -42,7 +42,7 @@ import { BreadcrumbSetters } from '../../components/Breadcrumbs'
import { FilteredConnection } from '../../components/FilteredConnection'
import { PageTitle } from '../../components/PageTitle'
import { GitCommitFields, Scalars, TreePageRepositoryFields } from '../../graphql-operations'
import { getCombinedViews } from '../../insights/backend'
import { InsightsApiContext, InsightsViewGrid } from '../../insights'
import { Settings } from '../../schema/settings.schema'
import { PatternTypeProps, CaseSensitivityProps, CopyQueryButtonProps, SearchContextProps } from '../../search'
import { basename } from '../../util/path'
@ -52,7 +52,6 @@ import { gitCommitFragment } from '../commits/RepositoryCommitsPage'
import { FilePathBreadcrumbs } from '../FilePathBreadcrumbs'
import { TreeEntriesSection } from './TreeEntriesSection'
import { ViewGrid } from './ViewGrid'
const fetchTreeCommits = memoizeObservable(
(args: {
@ -263,6 +262,8 @@ export const TreePage: React.FunctionComponent<Props> = ({
[props.extensionsController]
)
)
const { getCombinedViews } = useContext(InsightsApiContext)
const views = useObservable(
useMemo(
() =>
@ -287,7 +288,7 @@ export const TreePage: React.FunctionComponent<Props> = ({
)
)
: EMPTY,
[showCodeInsights, workspaceUri, uri, props.extensionsController]
[getCombinedViews, showCodeInsights, workspaceUri, uri, props.extensionsController]
)
)
@ -419,7 +420,7 @@ export const TreePage: React.FunctionComponent<Props> = ({
)}
</header>
{views && (
<ViewGrid
<InsightsViewGrid
{...props}
className="tree-page__section"
views={views}

View File

@ -191,7 +191,7 @@ export const routes: readonly LayoutRouteProps<any>[] = [
{
path: '/insights',
exact: true,
render: lazyComponent(() => import('./insights/InsightsPage'), 'InsightsPage'),
render: lazyComponent(() => import('./insights/pages/InsightsPage'), 'InsightsPage'),
condition: props =>
!isErrorLike(props.settingsCascade.final) &&
!!props.settingsCascade.final?.experimentalFeatures?.codeInsights &&

View File

@ -1,6 +1,6 @@
import classNames from 'classnames'
import * as H from 'history'
import React, { useCallback, useEffect, useMemo } from 'react'
import React, { useCallback, useContext, useEffect, useMemo } from 'react'
import { EMPTY, from } from 'rxjs'
import { switchMap } from 'rxjs/operators'
@ -30,9 +30,8 @@ import {
import { AuthenticatedUser } from '../../auth'
import { BrandLogo } from '../../components/branding/BrandLogo'
import { SyntaxHighlightedSearchQuery } from '../../components/SyntaxHighlightedSearchQuery'
import { getCombinedViews } from '../../insights/backend'
import { InsightsApiContext, InsightsViewGrid } from '../../insights'
import { KeyboardShortcutsProps } from '../../keyboardShortcuts/keyboardShortcuts'
import { ViewGrid } from '../../repo/tree/ViewGrid'
import { repogroupList, homepageLanguageList } from '../../repogroups/HomepageConfig'
import { Settings } from '../../schema/settings.schema'
import { VersionContext } from '../../schema/site.schema'
@ -97,6 +96,7 @@ export const SearchPage: React.FunctionComponent<SearchPageProps> = props => {
!!props.settingsCascade.final?.experimentalFeatures?.codeInsights &&
props.settingsCascade.final['insights.displayLocation.homepage'] !== false
const { getCombinedViews } = useContext(InsightsApiContext)
const views = useObservable(
useMemo(
() =>
@ -107,7 +107,7 @@ export const SearchPage: React.FunctionComponent<SearchPageProps> = props => {
)
)
: EMPTY,
[showCodeInsights, props.extensionsController]
[getCombinedViews, showCodeInsights, props.extensionsController]
)
)
return (
@ -121,7 +121,7 @@ export const SearchPage: React.FunctionComponent<SearchPageProps> = props => {
})}
>
<SearchPageInput {...props} source="home" />
{views && <ViewGrid {...props} className="mt-5" views={views} />}
{views && <InsightsViewGrid {...props} className="mt-5" views={views} />}
</div>
{props.isSourcegraphDotCom &&
props.showRepogroupHomepage &&

View File

@ -48,6 +48,7 @@ export const ViewContent: React.FunctionComponent<ViewContentProps> = ({
}) => {
// Track user intent to interact with extension-contributed views
const viewContentReference = useRef<HTMLDivElement>(null)
useEffect(() => {
let viewContentElement = viewContentReference.current
@ -70,7 +71,7 @@ export const ViewContent: React.FunctionComponent<ViewContentProps> = ({
}
// If containerClassName is specified, the element with this class is the element
// that embodies the view in the eyes of the user. e.g. ViewGrid
// that embodies the view in the eyes of the user. e.g. InsightsViewGrid
if (containerClassName) {
viewContentElement = viewContentElement?.closest(`.${containerClassName}`) as HTMLDivElement
}

View File

@ -20,6 +20,7 @@
"graphql-lint": "graphql-schema-linter cmd/frontend/graphqlbackend/schema.graphql",
"build-web": "yarn workspace @sourcegraph/web run build",
"watch-web": "yarn workspace @sourcegraph/web run watch",
"code-insights-demo": "yarn workspace @sourcegraph/code-insights-demo run serve",
"generate": "gulp generate",
"watch-generate": "gulp watchGenerate",
"test": "jest --testPathIgnorePatterns end-to-end regression integration storybook",
@ -76,7 +77,8 @@
},
"workspaces": {
"packages": [
"client/*"
"client/*",
"client/sandboxes/*"
]
},
"devDependencies": {
@ -97,6 +99,7 @@
"@mermaid-js/mermaid-cli": "^8.7.0",
"@octokit/rest": "^16.36.0",
"@percy/puppeteer": "^1.1.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
"@pollyjs/adapter": "^5.0.0",
"@pollyjs/core": "^5.1.0",
"@pollyjs/persister-fs": "^5.0.0",
@ -133,6 +136,8 @@
"@types/d3-scale": "2.2.0",
"@types/d3-selection": "1.4.1",
"@types/d3-shape": "1.3.2",
"@types/d3-format": "^2.0.0",
"@types/d3-time-format": "^3.0.0",
"@types/enzyme": "3.10.8",
"@types/expect": "24.3.0",
"@types/fancy-log": "1.3.1",
@ -243,8 +248,9 @@
"raw-loader": "^4.0.2",
"react-docgen-typescript-webpack-plugin": "^1.1.0",
"react-hot-loader": "^4.13.0",
"react-spring": "^9.0.0",
"react-refresh": "^0.10.0",
"react-test-renderer": "^16.14.0",
"react-spring": "^9.0.0",
"sass": "^1.32.4",
"sass-loader": "^10.1.0",
"shelljs": "^0.8.4",
@ -270,6 +276,7 @@
"web-ext": "^4.2.0",
"webpack": "^4.44.2",
"webpack-bundle-analyzer": "^3.9.0",
"webpack-cli": "^4.6.0",
"webpack-dev-server": "^3.11.1",
"worker-loader": "^3.0.8",
"yarn-deduplicate": "^3.1.0"
@ -287,8 +294,6 @@
"@sourcegraph/extension-api-classes": "^1.1.0",
"@sourcegraph/react-loading-spinner": "0.0.7",
"@sqs/jsonc-parser": "^1.0.3",
"@types/d3-format": "^2.0.0",
"@types/d3-time-format": "^3.0.0",
"@visx/annotation": "^1.7.2",
"@visx/axis": "^1.7.0",
"@visx/glyph": "^1.7.0",

119
yarn.lock
View File

@ -1367,6 +1367,11 @@
enabled "2.0.x"
kuler "^2.0.0"
"@discoveryjs/json-ext@^0.5.0":
version "0.5.2"
resolved "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.2.tgz#8f03a22a04de437254e8ce8cc84ba39689288752"
integrity sha512-HyYEUDeIj5rRQU2Hk5HTB2uHsbRQpF70nvMhVzi+VJR0X+xNEhjPui4/kBf3VeH/wqD28PT4sVOm8qqLjBrSZg==
"@dsherret/to-absolute-glob@^2.0.2":
version "2.0.2"
resolved "https://registry.npmjs.org/@dsherret/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz#1f6475dc8bd974cea07a2daf3864b317b1dd332c"
@ -2408,7 +2413,7 @@
dependencies:
esquery "^1.0.1"
"@pmmmwh/react-refresh-webpack-plugin@^0.4.2":
"@pmmmwh/react-refresh-webpack-plugin@^0.4.2", "@pmmmwh/react-refresh-webpack-plugin@^0.4.3":
version "0.4.3"
resolved "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.4.3.tgz#1eec460596d200c0236bf195b078a5d1df89b766"
integrity sha512-br5Qwvh8D2OQqSXpd1g/xqXKnK0r+Jz6qVKBbWmpUcrbGOxUrf39V5oZ1876084CGn18uMdR5uvPqBv9UqtBjQ==
@ -5232,6 +5237,23 @@
"@webassemblyjs/wast-parser" "1.9.0"
"@xtuc/long" "4.2.2"
"@webpack-cli/configtest@^1.0.2":
version "1.0.2"
resolved "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.0.2.tgz#2a20812bfb3a2ebb0b27ee26a52eeb3e3f000836"
integrity sha512-3OBzV2fBGZ5TBfdW50cha1lHDVf9vlvRXnjpVbJBa20pSZQaSkMJZiwA8V2vD9ogyeXn8nU5s5A6mHyf5jhMzA==
"@webpack-cli/info@^1.2.3":
version "1.2.3"
resolved "https://registry.npmjs.org/@webpack-cli/info/-/info-1.2.3.tgz#ef819d10ace2976b6d134c7c823a3e79ee31a92c"
integrity sha512-lLek3/T7u40lTqzCGpC6CAbY6+vXhdhmwFRxZLMnRm6/sIF/7qMpT8MocXCRQfz0JAh63wpbXLMnsQ5162WS7Q==
dependencies:
envinfo "^7.7.3"
"@webpack-cli/serve@^1.3.1":
version "1.3.1"
resolved "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.3.1.tgz#911d1b3ff4a843304b9c3bacf67bb34672418441"
integrity sha512-0qXvpeYO6vaNoRBI52/UsbcaBydJCggoBBnIo/ovQQdn6fug0BgwsjorV1hVS7fMqGVTZGcVxv8334gjmbj5hw==
"@webpack-contrib/schema-utils@^1.0.0-beta.0":
version "1.0.0-beta.0"
resolved "https://registry.npmjs.org/@webpack-contrib/schema-utils/-/schema-utils-1.0.0-beta.0.tgz#bf9638c9464d177b48209e84209e23bee2eb4f65"
@ -5562,7 +5584,7 @@ ansi-align@^3.0.0:
dependencies:
string-width "^3.0.0"
ansi-colors@4.1.1:
ansi-colors@4.1.1, ansi-colors@^4.1.1:
version "4.1.1"
resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==
@ -5574,7 +5596,7 @@ ansi-colors@^1.0.1:
dependencies:
ansi-wrap "^0.1.0"
ansi-colors@^3.0.0, ansi-colors@^3.2.1:
ansi-colors@^3.0.0:
version "3.2.3"
resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz#57d35b8686e851e2cc04c403f1c00203976a1813"
integrity sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==
@ -7874,6 +7896,15 @@ clone-buffer@^1.0.0:
resolved "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58"
integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg=
clone-deep@^4.0.1:
version "4.0.1"
resolved "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387"
integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==
dependencies:
is-plain-object "^2.0.4"
kind-of "^6.0.2"
shallow-clone "^3.0.0"
clone-regexp@^2.1.0:
version "2.2.0"
resolved "https://registry.npmjs.org/clone-regexp/-/clone-regexp-2.2.0.tgz#7d65e00885cd8796405c35a737e7a86b7429e36f"
@ -8103,6 +8134,11 @@ commander@^6.0.0, commander@^6.1.0:
resolved "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
commander@^7.0.0:
version "7.2.0"
resolved "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
comment-parser@^0.7.6:
version "0.7.6"
resolved "https://registry.npmjs.org/comment-parser/-/comment-parser-0.7.6.tgz#0e743a53c8e646c899a1323db31f6cd337b10f12"
@ -9825,12 +9861,12 @@ enhanced-resolve@^4.3.0:
memory-fs "^0.5.0"
tapable "^1.0.0"
enquirer@^2.3.5:
version "2.3.5"
resolved "https://registry.npmjs.org/enquirer/-/enquirer-2.3.5.tgz#3ab2b838df0a9d8ab9e7dff235b0e8712ef92381"
integrity sha512-BNT1C08P9XD0vNg3J475yIUG+mVdp9T6towYFHUv897X0KoHBjB1shyrNmhmtHWKP17iSWgo7Gqh7BBuzLZMSA==
enquirer@^2.3.5, enquirer@^2.3.6:
version "2.3.6"
resolved "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d"
integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==
dependencies:
ansi-colors "^3.2.1"
ansi-colors "^4.1.1"
entities@^1.1.1, entities@~1.1.1:
version "1.1.2"
@ -9850,6 +9886,11 @@ env-ci@^5.0.2:
execa "^4.0.0"
java-properties "^1.0.0"
envinfo@^7.7.3:
version "7.8.1"
resolved "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475"
integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==
enzyme-adapter-react-16@^1.15.4:
version "1.15.4"
resolved "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.4.tgz#328a782365a363ecb424f99283c4833dd92c0f21"
@ -13290,10 +13331,10 @@ interpret@^1.0.0, interpret@^1.1.0:
resolved "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296"
integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==
interpret@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/interpret/-/interpret-2.0.0.tgz#b783ffac0b8371503e9ab39561df223286aa5433"
integrity sha512-e0/LknJ8wpMMhTiWcjivB+ESwIuvHnBSlBbmP/pSb8CQJldoj1p2qv7xGZ/+BtbTziYRFSz8OsvdbiX45LtYQA==
interpret@^2.0.0, interpret@^2.2.0:
version "2.2.0"
resolved "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9"
integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==
invariant@2.2.4, invariant@^2.2.1, invariant@^2.2.3, invariant@^2.2.4:
version "2.2.4"
@ -18983,6 +19024,11 @@ react-popper@^2.2.4:
react-fast-compare "^3.0.1"
warning "^4.0.2"
react-refresh@^0.10.0:
version "0.10.0"
resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.10.0.tgz#2f536c9660c0b9b1d500684d9e52a65e7404f7e3"
integrity sha512-PgidR3wST3dDYKr6b4pJoqQFpPGNKDSCDx4cZoshjXipw3LzO7mG1My2pwEzz2JVkF+inx3xRpDeQLFQGH/hsQ==
react-refresh@^0.8.3:
version "0.8.3"
resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
@ -19380,6 +19426,13 @@ rechoir@^0.6.2:
dependencies:
resolve "^1.1.6"
rechoir@^0.7.0:
version "0.7.0"
resolved "https://registry.npmjs.org/rechoir/-/rechoir-0.7.0.tgz#32650fd52c21ab252aa5d65b19310441c7e03aca"
integrity sha512-ADsDEH2bvbjltXEP+hTIAmeFekTFK0V2BTxMkok6qILyAJEXV0AFfoWcAq4yfll5VdIMd/RVXq0lR+wQi5ZU3Q==
dependencies:
resolve "^1.9.0"
recursive-readdir@2.2.2, recursive-readdir@^2.0.0:
version "2.2.2"
resolved "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f"
@ -19909,7 +19962,7 @@ resolve@1.1.7:
resolved "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=
resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.8.1:
resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.8.1, resolve@^1.9.0:
version "1.20.0"
resolved "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
@ -20408,6 +20461,13 @@ sha.js@^2.4.0, sha.js@^2.4.8, sha.js@~2.4.4:
inherits "^2.0.1"
safe-buffer "^5.0.1"
shallow-clone@^3.0.0:
version "3.0.1"
resolved "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"
integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==
dependencies:
kind-of "^6.0.2"
shallowequal@1.1.0, shallowequal@^1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8"
@ -23155,6 +23215,26 @@ webpack-bundle-analyzer@^3.9.0:
opener "^1.5.1"
ws "^6.0.0"
webpack-cli@^4.6.0:
version "4.6.0"
resolved "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.6.0.tgz#27ae86bfaec0cf393fcfd58abdc5a229ad32fd16"
integrity sha512-9YV+qTcGMjQFiY7Nb1kmnupvb1x40lfpj8pwdO/bom+sQiP4OBMKjHq29YQrlDWDPZO9r/qWaRRywKaRDKqBTA==
dependencies:
"@discoveryjs/json-ext" "^0.5.0"
"@webpack-cli/configtest" "^1.0.2"
"@webpack-cli/info" "^1.2.3"
"@webpack-cli/serve" "^1.3.1"
colorette "^1.2.1"
commander "^7.0.0"
enquirer "^2.3.6"
execa "^5.0.0"
fastest-levenshtein "^1.0.12"
import-local "^3.0.2"
interpret "^2.2.0"
rechoir "^0.7.0"
v8-compile-cache "^2.2.0"
webpack-merge "^5.7.3"
webpack-dev-middleware@^3.7.0, webpack-dev-middleware@^3.7.2:
version "3.7.2"
resolved "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz#0019c3db716e3fa5cecbf64f2ab88a74bab331f3"
@ -23238,6 +23318,14 @@ webpack-log@^2.0.0:
ansi-colors "^3.0.0"
uuid "^3.3.2"
webpack-merge@^5.7.3:
version "5.7.3"
resolved "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.7.3.tgz#2a0754e1877a25a8bbab3d2475ca70a052708213"
integrity sha512-6/JUQv0ELQ1igjGDzHkXbVDRxkfA57Zw7PfiupdLFJYrgFqY5ZP8xxbpp2lU3EPwYx89ht5Z/aDkD40hFCm5AA==
dependencies:
clone-deep "^4.0.1"
wildcard "^2.0.0"
webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3:
version "1.4.3"
resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933"
@ -23405,6 +23493,11 @@ widest-line@^3.1.0:
dependencies:
string-width "^4.0.0"
wildcard@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec"
integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==
windows-release@^3.1.0:
version "3.2.0"
resolved "https://registry.npmjs.org/windows-release/-/windows-release-3.2.0.tgz#8122dad5afc303d833422380680a79cdfa91785f"