Add live preview chart to lang stats creation UI (#20856)

* Add onChange API for useForm hook
* Add live preview for the lang stats creation UI

Co-authored-by: Valery Bugakov <skymk1@gmail.com>
This commit is contained in:
Vova Kulikov 2021-05-14 00:20:21 +03:00 committed by GitHub
parent 6841dfd765
commit 264f0b3fa3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 579 additions and 60 deletions

View File

@ -0,0 +1,63 @@
import linguistLanguages from 'linguist-languages'
import { escapeRegExp, partition, sum } from 'lodash'
import { defer } from 'rxjs'
import { map, retry } from 'rxjs/operators'
import { PieChartContent } from 'sourcegraph'
import { fetchLangStatsInsight } from '../requests/fetch-lang-stats-insight'
import { LangStatsInsightsSettings } from '../types'
const isLinguistLanguage = (language: string): language is keyof typeof linguistLanguages =>
Object.prototype.hasOwnProperty.call(linguistLanguages, language)
/**
* This logic is a simplified copy of fetch logic of lang-stats-based code insight extension.
* In order to have live preview for creation UI we had to copy this logic from
* extension.
* See https://github.com/sourcegraph/sourcegraph-code-stats-insights/blob/master/src/code-stats-insights.ts
* */
export async function getLangStatsInsightContent(settings: LangStatsInsightsSettings): Promise<PieChartContent<any>> {
const { repository, threshold = 0.03 } = settings
const query = `repo:^${escapeRegExp(repository)}`
const stats = await defer(() => fetchLangStatsInsight(query))
.pipe(
// The search may timeout, but a retry is then likely faster because caches are warm
retry(3),
map(data => data.search!.stats)
)
.toPromise()
if (stats.languages.length === 0) {
throw new Error("We couldn't find the language statistics, try changing the repository.")
}
const totalLines = sum(stats.languages.map(language => language.totalLines))
const linkURL = new URL('/stats', window.location.origin)
linkURL.searchParams.set('q', query)
const [notOther, other] = partition(stats.languages, language => language.totalLines / totalLines >= threshold)
return {
chart: 'pie' as const,
pies: [
{
data: [
...notOther,
{
name: 'Other',
totalLines: sum(other.map(language => language.totalLines)),
},
].map(language => ({
...language,
fill: (isLinguistLanguage(language.name) && linguistLanguages[language.name].color) || 'gray',
linkURL: linkURL.href,
})),
dataKey: 'totalLines',
nameKey: 'name',
fillKey: 'fill',
linkURLKey: 'linkURL',
},
],
}
}

View File

@ -1,6 +1,7 @@
import { of, throwError } from 'rxjs'
import { getCombinedViews, getInsightCombinedViews } from './api/get-combined-views'
import { getLangStatsInsightContent } from './api/get-lang-stats-insight-content'
import { getSearchInsightContent } from './api/get-search-insight-content'
import { getSubjectSettings, updateSubjectSettings } from './api/subject-settings'
import { ApiService } from './types'
@ -14,6 +15,7 @@ export const createInsightAPI = (): ApiService => ({
getSubjectSettings,
updateSubjectSettings,
getSearchInsightContent,
getLangStatsInsightContent,
})
/**
@ -26,5 +28,6 @@ export const createMockInsightAPI = (overrideRequests: Partial<ApiService>): Api
getSubjectSettings: () => throwError(new Error('Implement getSubjectSettings handler first')),
updateSubjectSettings: () => throwError(new Error('Implement getSubjectSettings handler first')),
getSearchInsightContent: () => Promise.reject(new Error('Implement getSubjectSettings handler first')),
getLangStatsInsightContent: () => Promise.reject(new Error('Implement getLangStatsInsightContent handler first')),
...overrideRequests,
})

View File

@ -0,0 +1,28 @@
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'
import { dataOrThrowErrors, gql } from '@sourcegraph/shared/src/graphql/graphql'
import { requestGraphQL } from '../../../../backend/graphql'
import { LangStatsInsightContentResult, LangStatsInsightContentVariables } from '../../../../graphql-operations'
export function fetchLangStatsInsight(query: string): Observable<LangStatsInsightContentResult> {
return requestGraphQL<LangStatsInsightContentResult, LangStatsInsightContentVariables>(
gql`
query LangStatsInsightContent($query: String!) {
search(query: $query) {
results {
limitHit
}
stats {
languages {
name
totalLines
}
}
}
}
`,
{ query }
).pipe(map(dataOrThrowErrors))
}

View File

@ -28,6 +28,13 @@ export interface SearchInsightSettings {
repositories: string[]
}
export interface LangStatsInsightsSettings {
/** URL of git repository from which statistics will be collected */
repository: string
/** The threshold below which a language is counted as part of 'Other' */
threshold: number
}
export interface DataSeries {
name: string
stroke: string
@ -38,14 +45,18 @@ export interface ApiService {
getCombinedViews: (
getExtensionsInsights: () => Observable<ViewProviderResult[]>
) => Observable<ViewInsightProviderResult[]>
getInsightCombinedViews: (
extensionApi: Promise<Remote<FlatExtensionHostAPI>>
) => Observable<ViewInsightProviderResult[]>
getSubjectSettings: (id: string) => Observable<SubjectSettingsResult>
updateSubjectSettings: (
context: Pick<PlatformContext, 'updateSettings'>,
subjectId: string,
content: string
) => Observable<void>
getSearchInsightContent: (insight: SearchInsightSettings) => Promise<sourcegraph.LineChartContent<any, string>>
getLangStatsInsightContent: (insight: LangStatsInsightsSettings) => Promise<sourcegraph.PieChartContent<any>>
}

View File

@ -1,5 +1,5 @@
@import 'wildcard/src/global-styles/breakpoints';
.creation-page {
max-width: $viewport-md;
max-width: $viewport-lg;
}

View File

@ -6,7 +6,10 @@ import { EMPTY_SETTINGS_CASCADE } from '@sourcegraph/shared/src/settings/setting
import { WebStory } from '../../../../components/WebStory'
import { authUser } from '../../../../search/panels/utils'
import { InsightsApiContext } from '../../../core/backend/api-provider'
import { createMockInsightAPI } from '../../../core/backend/insights-api'
import { getRandomLangStatsMock } from './components/live-preview-chart/live-preview-mock-data'
import { LangStatsInsightCreationPage, LangStatsInsightCreationPageProps } from './LangStatsInsightCreationPage'
const { add } = storiesOf('web/insights/CreateLangStatsInsightPageProps', module)
@ -24,13 +27,27 @@ const PLATFORM_CONTEXT: LangStatsInsightCreationPageProps['platformContext'] = {
},
}
function sleep(delay: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, delay))
}
const mockAPI = createMockInsightAPI({
getLangStatsInsightContent: async () => {
await sleep(2000)
return getRandomLangStatsMock()
},
})
const history = createMemoryHistory()
add('Page', () => (
<LangStatsInsightCreationPage
history={history}
platformContext={PLATFORM_CONTEXT}
settingsCascade={EMPTY_SETTINGS_CASCADE}
authenticatedUser={authUser}
/>
<InsightsApiContext.Provider value={mockAPI}>
<LangStatsInsightCreationPage
history={history}
platformContext={PLATFORM_CONTEXT}
settingsCascade={EMPTY_SETTINGS_CASCADE}
authenticatedUser={authUser}
/>
</InsightsApiContext.Provider>
))

View File

@ -17,9 +17,9 @@ import { InsightsApiContext } from '../../../core/backend/api-provider'
import { InsightTypeSuffix } from '../../../core/types'
import {
LangStatsInsightCreationForm,
LangStatsInsightCreationFormProps,
} from './components/lang-stats-insight-creation-form/LangStatsInsightCreationForm'
LangStatsInsightCreationContent,
LangStatsInsightCreationContentProps,
} from './components/lang-stats-insight-creation-content/LangStatsInsightCreationContent'
import styles from './LangStatsInsightCreationPage.module.scss'
const DEFAULT_FINAL_SETTINGS = {}
@ -45,7 +45,7 @@ export const LangStatsInsightCreationPage: React.FunctionComponent<LangStatsInsi
const { history, authenticatedUser, settingsCascade, platformContext } = props
const { getSubjectSettings, updateSubjectSettings } = useContext(InsightsApiContext)
const handleSubmit = useCallback<LangStatsInsightCreationFormProps['onSubmit']>(
const handleSubmit = useCallback<LangStatsInsightCreationContentProps['onSubmit']>(
async values => {
if (!authenticatedUser) {
return
@ -103,7 +103,7 @@ export const LangStatsInsightCreationPage: React.FunctionComponent<LangStatsInsi
}
return (
<Page className={classnames(styles.creationPage, 'col-8')}>
<Page className={classnames(styles.creationPage, 'col-10')}>
<PageTitle title="Create new code insight" />
<div className="mb-5">
@ -121,7 +121,7 @@ export const LangStatsInsightCreationPage: React.FunctionComponent<LangStatsInsi
</p>
</div>
<LangStatsInsightCreationForm
<LangStatsInsightCreationContent
className="pb-5"
settings={settingsCascade.final ?? DEFAULT_FINAL_SETTINGS}
onSubmit={handleSubmit}

View File

@ -0,0 +1,38 @@
@import 'wildcard/src/global-styles/breakpoints';
.content {
display: flex;
flex-wrap: wrap;
&__form {
order: 1;
flex-grow: 1;
flex-basis: 25rem;
}
&__live-preview {
order: 2;
position: sticky;
top: 1rem;
padding-left: 1rem;
flex-basis: 25rem;
flex-shrink: 0;
height: 20rem;
}
@media (--md-breakpoint-down) {
&__live-preview {
order: 1;
flex-basis: 100%;
padding-left: 0;
margin-bottom: 2rem;
position: static;
}
&__form {
order: 2;
}
}
}

View File

@ -0,0 +1,84 @@
import classnames from 'classnames'
import React from 'react'
import { noop } from 'rxjs'
import { Settings } from '@sourcegraph/shared/src/settings/settings'
import { useField } from '../../../../../components/form/hooks/useField'
import { SubmissionErrors, useForm } from '../../../../../components/form/hooks/useForm'
import { useTitleValidator } from '../../../../../components/form/hooks/useTitleValidator'
import { createRequiredValidator } from '../../../../../components/form/validators'
import { InsightTypeSuffix } from '../../../../../core/types'
import { LangStatsCreationFormFields } from '../../types'
import { LangStatsInsightCreationForm } from '../lang-stats-insight-creation-form/LangStatsInsightCreationForm'
import { LangStatsInsightLivePreview } from '../live-preview-chart/LangStatsInsightLivePreview'
import styles from './LangStatsInsightCreationContent.module.scss'
const repositoriesFieldValidator = createRequiredValidator('Repositories is a required field for code insight.')
const thresholdFieldValidator = createRequiredValidator('Threshold is a required field for code insight.')
const INITIAL_VALUES: LangStatsCreationFormFields = {
repository: '',
title: '',
threshold: 3,
visibility: 'personal',
}
export interface LangStatsInsightCreationContentProps {
/** Final settings cascade. Used for title field validation. */
settings?: Settings | null
/** Initial value for all form fields. */
initialValues?: LangStatsCreationFormFields
/** Custom class name for root form element. */
className?: string
/** Submit handler for form element. */
onSubmit: (values: LangStatsCreationFormFields) => SubmissionErrors | Promise<SubmissionErrors> | void
/** Cancel handler. */
onCancel?: () => void
}
export const LangStatsInsightCreationContent: React.FunctionComponent<LangStatsInsightCreationContentProps> = props => {
const { settings, initialValues = INITIAL_VALUES, className, onSubmit, onCancel = noop } = props
const { handleSubmit, formAPI, ref } = useForm<LangStatsCreationFormFields>({
initialValues,
onSubmit,
})
// We can't have two or more insights with the same name, since we rely on name as on id of insights.
const titleValidator = useTitleValidator({ settings, insightType: InsightTypeSuffix.langStats })
const repository = useField('repository', formAPI, repositoriesFieldValidator)
const title = useField('title', formAPI, titleValidator)
const threshold = useField('threshold', formAPI, thresholdFieldValidator)
const visibility = useField('visibility', formAPI)
// If some fields that needed to run live preview are invalid
// we should disabled live chart preview
const allFieldsForPreviewAreValid = repository.meta.validState === 'VALID' && threshold.meta.validState === 'VALID'
return (
<div className={classnames(styles.content, className)}>
<LangStatsInsightCreationForm
innerRef={ref}
handleSubmit={handleSubmit}
submitErrors={formAPI.submitErrors}
submitting={formAPI.submitting}
title={title}
repository={repository}
threshold={threshold}
visibility={visibility}
onCancel={onCancel}
className={styles.contentForm}
/>
<LangStatsInsightLivePreview
repository={repository.meta.value}
threshold={threshold.meta.value}
disabled={!allFieldsForPreviewAreValid}
className={styles.contentLivePreview}
/>
</div>
)
}

View File

@ -1,65 +1,50 @@
import classnames from 'classnames'
import React from 'react'
import { Settings } from '@sourcegraph/shared/src/settings/settings'
import React, { FormEventHandler, RefObject } from 'react'
import { ErrorAlert } from '../../../../../../components/alerts'
import { LoaderButton } from '../../../../../../components/LoaderButton'
import { FormGroup } from '../../../../../components/form/form-group/FormGroup'
import { FormInput } from '../../../../../components/form/form-input/FormInput'
import { FormRadioInput } from '../../../../../components/form/form-radio-input/FormRadioInput'
import { useField } from '../../../../../components/form/hooks/useField'
import { FORM_ERROR, SubmissionErrors, useForm } from '../../../../../components/form/hooks/useForm'
import { useTitleValidator } from '../../../../../components/form/hooks/useTitleValidator'
import { createRequiredValidator } from '../../../../../components/form/validators'
import { InsightTypeSuffix } from '../../../../../core/types'
import { useFieldAPI } from '../../../../../components/form/hooks/useField'
import { FORM_ERROR, SubmissionErrors } from '../../../../../components/form/hooks/useForm'
import { LangStatsCreationFormFields } from '../../types'
import styles from './LangStatsInsightCreationForm.module.scss'
const repositoriesFieldValidator = createRequiredValidator('Repositories is a required field for code insight.')
const thresholdFieldValidator = createRequiredValidator('Threshold is a required field for code insight.')
export interface LangStatsInsightCreationFormProps {
settings: Settings | null
innerRef: RefObject<any>
handleSubmit: FormEventHandler
submitErrors: SubmissionErrors
submitting: boolean
className?: string
onSubmit: (values: LangStatsCreationFormFields) => SubmissionErrors | Promise<SubmissionErrors> | void
title: useFieldAPI<LangStatsCreationFormFields['title']>
repository: useFieldAPI<LangStatsCreationFormFields['repository']>
threshold: useFieldAPI<LangStatsCreationFormFields['threshold']>
visibility: useFieldAPI<LangStatsCreationFormFields['visibility']>
onCancel: () => void
}
export interface LangStatsCreationFormFields {
title: string
repository: string
threshold: number
visibility: 'personal' | 'organization'
}
const INITIAL_VALUES: LangStatsCreationFormFields = {
repository: '',
title: '',
threshold: 3,
visibility: 'personal',
}
export const LangStatsInsightCreationForm: React.FunctionComponent<LangStatsInsightCreationFormProps> = props => {
const { className, onSubmit, onCancel, settings } = props
const { handleSubmit, formAPI, ref } = useForm<LangStatsCreationFormFields>({
initialValues: INITIAL_VALUES,
onSubmit,
})
// We can't have two or more insights with the same name, since we rely on name as on id of insights.
const titleValidator = useTitleValidator({ settings, insightType: InsightTypeSuffix.langStats })
const repository = useField('repository', formAPI, repositoriesFieldValidator)
const title = useField('title', formAPI, titleValidator)
const threshold = useField('threshold', formAPI, thresholdFieldValidator)
const visibility = useField('visibility', formAPI)
const {
innerRef,
handleSubmit,
submitErrors,
submitting,
className,
title,
repository,
threshold,
visibility,
onCancel,
} = props
return (
// eslint-disable-next-line react/forbid-elements
<form
ref={ref}
ref={innerRef}
noValidate={true}
className={classnames(className, 'd-flex flex-column')}
onSubmit={handleSubmit}
@ -134,14 +119,14 @@ export const LangStatsInsightCreationForm: React.FunctionComponent<LangStatsInsi
<hr className={styles.formSeparator} />
<div>
{formAPI.submitErrors?.[FORM_ERROR] && <ErrorAlert error={formAPI.submitErrors[FORM_ERROR]} />}
{submitErrors?.[FORM_ERROR] && <ErrorAlert error={submitErrors[FORM_ERROR]} />}
<LoaderButton
alwaysShowLabel={true}
loading={formAPI.submitting}
label={formAPI.submitting ? 'Submitting' : 'Create code insight'}
loading={submitting}
label={submitting ? 'Submitting' : 'Create code insight'}
type="submit"
disabled={formAPI.submitting}
disabled={submitting}
className="btn btn-primary mr-2"
/>

View File

@ -0,0 +1,55 @@
.live-preview {
display: flex;
flex-direction: column;
&__update-button {
display: flex;
align-items: center;
margin-bottom: 0.5rem;
flex-grow: 0;
align-self: flex-start;
}
&__chart-container {
position: relative;
width: 100%;
flex-grow: 1;
}
&__chart {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
&__chart--with-mock-data {
filter: blur(5px);
pointer-events: none;
// In order to turn off any interactions with chart like
// tooltip or chart shutter for user cursor we have to
// override pointer events. Since visx charts add pointer events
// by html attribute we have to use important statement.
:global(.visx-group) {
pointer-events: none !important;
}
}
&__update-button-icon {
margin-left: 0.5rem;
}
&__loading-chart-info {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
&__loader {
background-color: var(--color-bg-2);
height: 100%;
}
}

View File

@ -0,0 +1,97 @@
import classnames from 'classnames'
import RefreshIcon from 'mdi-react/RefreshIcon'
import React, { useMemo } from 'react'
import { useHistory } from 'react-router-dom'
import { LoadingSpinner } from '@sourcegraph/react-loading-spinner'
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { isErrorLike } from '@sourcegraph/shared/src/util/errors'
import { ErrorAlert } from '../../../../../../components/alerts'
import { ChartViewContent } from '../../../../../../views/ChartViewContent/ChartViewContent'
import { useLangStatsPreviewContent } from './hooks/use-lang-stats-preview-content'
import styles from './LangStatsInsightLivePreview.module.scss'
import { DEFAULT_PREVIEW_MOCK } from './live-preview-mock-data'
export interface LangStatsInsightLivePreviewProps {
/** Custom className for the root element of live preview. */
className?: string
/** List of repositories for insights. */
repository: string
/** Step value for cut off other small values. */
threshold: number
/**
* Disable prop to disable live preview.
* Used in a consumer of this component when some required fields
* for live preview are invalid.
* */
disabled?: boolean
}
/**
* Displays live preview chart for creation UI with latest insights settings
* from creation UI form.
* */
export const LangStatsInsightLivePreview: React.FunctionComponent<LangStatsInsightLivePreviewProps> = props => {
const { repository = '', threshold, disabled = false, className } = props
const history = useHistory()
const previewSetting = useMemo(
() => ({
repository: repository.trim(),
threshold: threshold / 100,
}),
[repository, threshold]
)
const { loading, dataOrError, update } = useLangStatsPreviewContent({ disabled, previewSetting })
return (
<div className={classnames(styles.livePreview, className)}>
<button
type="button"
disabled={disabled}
className={classnames('btn btn-secondary', styles.livePreviewUpdateButton)}
onClick={update}
>
Update live preview
<RefreshIcon size="1rem" className={styles.livePreviewUpdateButtonIcon} />
</button>
{loading && (
<div
className={classnames(
styles.livePreviewLoader,
'flex-grow-1 d-flex flex-column align-items-center justify-content-center'
)}
>
<LoadingSpinner /> Loading code insight
</div>
)}
{isErrorLike(dataOrError) && <ErrorAlert className="m-0" error={dataOrError} />}
{!loading && !isErrorLike(dataOrError) && (
<div className={styles.livePreviewChartContainer}>
<ChartViewContent
className={classnames(styles.livePreviewChart, {
[styles.livePreviewChartWithMockData]: !dataOrError,
})}
history={history}
viewID="search-insight-live-preview"
telemetryService={NOOP_TELEMETRY_SERVICE}
content={dataOrError ?? DEFAULT_PREVIEW_MOCK}
/>
{!dataOrError && (
<p className={styles.livePreviewLoadingChartInfo}>
Here youll see your insights chart preview
</p>
)}
</div>
)}
</div>
)
}

View File

@ -0,0 +1,67 @@
import { useContext, useEffect, useState } from 'react'
import { PieChartContent } from 'sourcegraph'
import { asError } from '@sourcegraph/shared/src/util/errors'
import { useDebounce } from '@sourcegraph/wildcard/src'
import { InsightsApiContext } from '../../../../../../core/backend/api-provider'
export interface UseLangStatsPreviewContentProps {
/** Prop to turn off fetching and reset data for live preview chart. */
disabled: boolean
/** Settings which needed to fetch data for live preview. */
previewSetting: {
repository: string
threshold: number
}
}
export interface UseLangStatsPreviewContentAPI {
loading: boolean
dataOrError: PieChartContent<any> | Error | undefined
update: () => void
}
/**
* Unified logic for fetching data for live preview chart of lang stats insight.
* */
export function useLangStatsPreviewContent(props: UseLangStatsPreviewContentProps): UseLangStatsPreviewContentAPI {
const { disabled, previewSetting } = props
const { getLangStatsInsightContent } = useContext(InsightsApiContext)
const [loading, setLoading] = useState<boolean>(false)
const [dataOrError, setDataOrError] = useState<PieChartContent<any> | Error | undefined>()
// Synthetic deps to trigger dry run for fetching live preview data
const [lastPreviewVersion, setLastPreviewVersion] = useState(0)
const liveDebouncedSettings = useDebounce(previewSetting, 500)
useEffect(() => {
let hasRequestCanceled = false
setLoading(true)
setDataOrError(undefined)
if (disabled) {
setLoading(false)
return
}
getLangStatsInsightContent(liveDebouncedSettings)
.then(data => !hasRequestCanceled && setDataOrError(data))
.catch(error => !hasRequestCanceled && setDataOrError(asError(error)))
.finally(() => !hasRequestCanceled && setLoading(false))
return () => {
hasRequestCanceled = true
}
}, [disabled, lastPreviewVersion, getLangStatsInsightContent, liveDebouncedSettings])
return {
loading,
dataOrError,
update: () => setLastPreviewVersion(count => count + 1),
}
}

View File

@ -0,0 +1,59 @@
import { random } from 'lodash'
import { PieChartContent } from 'sourcegraph'
export const DEFAULT_PREVIEW_MOCK: PieChartContent<any> = {
chart: 'pie' as const,
pies: [
{
dataKey: 'value',
nameKey: 'name',
fillKey: 'fill',
linkURLKey: 'linkURL',
data: [
{
name: 'Covered',
value: 0.3,
fill: 'var(--oc-grape-7)',
linkURL: '#Covered',
},
{
name: 'Not covered',
value: 0.7,
fill: 'var(--oc-orange-7)',
linkURL: '#Not_covered',
},
],
},
],
}
export function getRandomLangStatsMock(): PieChartContent<any> {
const randomFirstPieValue = random(0, 0.6)
const randomSecondPieValue = 1 - randomFirstPieValue
return {
chart: 'pie' as const,
pies: [
{
dataKey: 'value',
nameKey: 'name',
fillKey: 'fill',
linkURLKey: 'linkURL',
data: [
{
name: 'JavaScript',
value: randomFirstPieValue,
fill: 'var(--oc-grape-7)',
linkURL: '#Covered',
},
{
name: 'Typescript',
value: randomSecondPieValue,
fill: 'var(--oc-orange-7)',
linkURL: '#Not_covered',
},
],
},
],
}
}

View File

@ -0,0 +1,6 @@
export interface LangStatsCreationFormFields {
title: string
repository: string
threshold: number
visibility: 'personal' | 'organization'
}

View File

@ -141,7 +141,7 @@ const config = {
...(shouldAnalyze ? [new BundleAnalyzerPlugin()] : []),
],
resolve: {
extensions: ['.mjs', '.ts', '.tsx', '.js'],
extensions: ['.mjs', '.ts', '.tsx', '.js', '.json'],
mainFields: ['es2015', 'module', 'browser', 'main'],
alias: {
// react-visibility-sensor's main field points to a UMD bundle instead of ESM

View File

@ -337,6 +337,7 @@
"is-absolute-url": "^3.0.3",
"iterare": "^1.2.1",
"js-cookie": "^2.2.1",
"linguist-languages": "^7.14.0",
"lodash": "^4.17.20",
"marked": "^1.2.7",
"mdi-react": "^7.3.0",

View File

@ -14900,6 +14900,11 @@ lines-and-columns@^1.1.6:
resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
linguist-languages@^7.14.0:
version "7.14.0"
resolved "https://registry.npmjs.org/linguist-languages/-/linguist-languages-7.14.0.tgz#62a6bdbe1ef80d0715d16c0bb8e953ee93c95dfc"
integrity sha512-VqnUYHOSqRqAGnIl+7SCnFxK+sX0x7LXe5qn0TG6t9SViofQgN7272PLCFZ/lgkT7tAO5CA/2pCsZGlGvGhfWA==
linkify-it@^2.0.0:
version "2.0.3"
resolved "https://registry.npmjs.org/linkify-it/-/linkify-it-2.0.3.tgz#d94a4648f9b1c179d64fa97291268bdb6ce9434f"