mirror of
https://github.com/harness/drone.git
synced 2025-05-01 13:11:27 +00:00
Pipeline Checks refactoring and bug fixes (#515)
This commit is contained in:
parent
b29d2765b0
commit
1572618093
@ -50,7 +50,7 @@
|
||||
"@uiw/codemirror-extensions-hyper-link": "^4.19.9",
|
||||
"@uiw/codemirror-themes-all": "^4.19.9",
|
||||
"@uiw/react-markdown-preview": "^4.1.12",
|
||||
"anser": "2.0.1",
|
||||
"anser": "^2.1.1",
|
||||
"classnames": "^2.2.6",
|
||||
"clipboard-copy": "^3.1.0",
|
||||
"diff2html": "3.4.22",
|
||||
@ -85,11 +85,10 @@
|
||||
"rehype-video": "^1.2.2",
|
||||
"restful-react": "15.6.0",
|
||||
"webpack-retry-chunk-load-plugin": "^3.1.0",
|
||||
"xterm": "^5.1.0",
|
||||
"xterm-addon-canvas": "^0.3.0",
|
||||
"xterm-addon-fit": "^0.7.0",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-addon-fit": "^0.8.0",
|
||||
"xterm-addon-search": "^0.11.0",
|
||||
"xterm-addon-web-links": "^0.8.0",
|
||||
"xterm-addon-web-links": "^0.9.0",
|
||||
"yaml": "^1.10.0",
|
||||
"yup": "^0.29.1"
|
||||
},
|
||||
|
@ -4,6 +4,9 @@
|
||||
@include vars;
|
||||
|
||||
&.fullPage {
|
||||
--font-family: Inter, sans-serif;
|
||||
--font-family-mono: Roboto Mono, monospace;
|
||||
|
||||
height: var(--page-height);
|
||||
}
|
||||
|
||||
|
@ -1,65 +1,48 @@
|
||||
import React, { useEffect, useMemo, useRef } from 'react'
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import { Container } from '@harnessio/uicore'
|
||||
import { Terminal } from 'xterm'
|
||||
import { FitAddon } from 'xterm-addon-fit'
|
||||
import { CanvasAddon } from 'xterm-addon-canvas'
|
||||
import { SearchAddon } from 'xterm-addon-search'
|
||||
import { WebLinksAddon } from 'xterm-addon-web-links'
|
||||
import 'xterm/css/xterm.css'
|
||||
import { useEventListener } from 'hooks/useEventListener'
|
||||
|
||||
const DEFAULT_SCROLLBACK_LINES = 100000
|
||||
|
||||
export type TermRefs = { term: Terminal; fitAddon: FitAddon } | undefined
|
||||
export type TermRefs = { term: Terminal; fitAddon: FitAddon }
|
||||
|
||||
export interface LogViewerProps {
|
||||
/** Search text */
|
||||
searchText?: string
|
||||
|
||||
/** Number of scrollback lines */
|
||||
scrollbackLines?: number
|
||||
|
||||
/** Log content as string */
|
||||
search?: string
|
||||
content: string
|
||||
|
||||
termRefs?: React.MutableRefObject<TermRefs>
|
||||
|
||||
termRefs?: React.MutableRefObject<TermRefs | undefined>
|
||||
autoHeight?: boolean
|
||||
}
|
||||
|
||||
export const LogViewer: React.FC<LogViewerProps> = ({ scrollbackLines, content, termRefs, autoHeight }) => {
|
||||
const LogTerminal: React.FC<LogViewerProps> = ({ content, termRefs, autoHeight }) => {
|
||||
const ref = useRef<HTMLDivElement | null>(null)
|
||||
const lines = useMemo(() => content.split(/\r?\n/), [content])
|
||||
const term = useRef<{ term: Terminal; fitAddon: FitAddon }>()
|
||||
const term = useRef<TermRefs>()
|
||||
|
||||
useEffect(() => {
|
||||
if (!term.current) {
|
||||
const _term = new Terminal({
|
||||
cursorBlink: true,
|
||||
cursorStyle: 'block',
|
||||
allowTransparency: true,
|
||||
disableStdin: true,
|
||||
scrollback: scrollbackLines || DEFAULT_SCROLLBACK_LINES,
|
||||
theme: {
|
||||
background: 'transparent'
|
||||
}
|
||||
tabStopWidth: 2,
|
||||
scrollOnUserInput: false,
|
||||
smoothScrollDuration: 0,
|
||||
scrollback: 10000
|
||||
})
|
||||
|
||||
const searchAddon = new SearchAddon()
|
||||
const fitAddon = new FitAddon()
|
||||
const webLinksAddon = new WebLinksAddon()
|
||||
|
||||
_term.loadAddon(searchAddon)
|
||||
_term.loadAddon(fitAddon)
|
||||
_term.loadAddon(webLinksAddon)
|
||||
_term.loadAddon(new CanvasAddon())
|
||||
|
||||
_term.open(ref?.current as HTMLDivElement)
|
||||
|
||||
fitAddon.fit()
|
||||
searchAddon.activate(_term)
|
||||
|
||||
_term.write('\x1b[?25l') // disable cursor
|
||||
// disable cursor
|
||||
_term.write('\x1b[?25l')
|
||||
|
||||
term.current = { term: _term, fitAddon }
|
||||
|
||||
@ -67,24 +50,38 @@ export const LogViewer: React.FC<LogViewerProps> = ({ scrollbackLines, content,
|
||||
termRefs.current = term.current
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (term.current) {
|
||||
if (termRefs) {
|
||||
termRefs.current = undefined
|
||||
}
|
||||
setTimeout(() => term.current?.term.dispose(), 1000)
|
||||
}
|
||||
}
|
||||
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
useEffect(() => {
|
||||
term.current?.term?.clear()
|
||||
const lines = content.split(/\r?\n/)
|
||||
|
||||
lines.forEach(line => term.current?.term?.writeln(line))
|
||||
|
||||
if (autoHeight) {
|
||||
term.current?.term?.resize(term.current?.term?.cols, lines.length + 1)
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
term.current?.term.scrollToTop()
|
||||
}, 0)
|
||||
|
||||
return () => {
|
||||
term.current?.term?.clear()
|
||||
}
|
||||
}, [lines, autoHeight])
|
||||
}, [content, autoHeight])
|
||||
|
||||
useEventListener('resize', () => {
|
||||
term.current?.fitAddon?.fit()
|
||||
})
|
||||
useEventListener('resize', () => term.current?.fitAddon?.fit())
|
||||
|
||||
return <Container ref={ref} width="100%" height={autoHeight ? 'auto' : '100%'} />
|
||||
}
|
||||
|
||||
export const LogViewer = React.memo(LogTerminal)
|
||||
|
@ -3,12 +3,12 @@ import { useEffect } from 'react'
|
||||
import type { GetDataError } from 'restful-react'
|
||||
import { getErrorMessage } from 'utils/Utils'
|
||||
|
||||
export function useShowRequestError(error: GetDataError<Unknown> | null) {
|
||||
export function useShowRequestError(error: GetDataError<Unknown> | null, timeout?: number) {
|
||||
const { showError } = useToaster()
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
showError(getErrorMessage(error))
|
||||
showError(getErrorMessage(error), timeout)
|
||||
}
|
||||
}, [error, showError])
|
||||
}, [error, showError, timeout])
|
||||
}
|
||||
|
154
web/src/pages/PullRequest/Checks/CheckPipelineStages.tsx
Normal file
154
web/src/pages/PullRequest/Checks/CheckPipelineStages.tsx
Normal file
@ -0,0 +1,154 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useGet } from 'restful-react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import { Color } from '@harnessio/design-system'
|
||||
import { Icon } from '@harnessio/icons'
|
||||
import { Container, Layout, Text } from '@harnessio/uicore'
|
||||
import cx from 'classnames'
|
||||
import { Falsy, Match, Truthy } from 'react-jsx-match'
|
||||
import { useAppContext } from 'AppContext'
|
||||
import { ExecutionState, ExecutionStatus } from 'components/ExecutionStatus/ExecutionStatus'
|
||||
import { useQueryParams } from 'hooks/useQueryParams'
|
||||
import { useShowRequestError } from 'hooks/useShowRequestError'
|
||||
import type { TypesExecution, TypesStage } from 'services/code'
|
||||
import type { GitInfoProps } from 'utils/GitUtils'
|
||||
import { ButtonRoleProps, PullRequestSection } from 'utils/Utils'
|
||||
import { findDefaultExecution } from './ChecksUtils'
|
||||
import css from './Checks.module.scss'
|
||||
|
||||
interface CheckPipelineStagesProps extends Pick<GitInfoProps, 'repoMetadata' | 'pullRequestMetadata'> {
|
||||
pipelineName: string
|
||||
executionNumber: string
|
||||
expanded?: boolean
|
||||
onSelectStage: (stage: TypesStage) => void
|
||||
}
|
||||
|
||||
export const CheckPipelineStages: React.FC<CheckPipelineStagesProps> = ({
|
||||
pipelineName,
|
||||
executionNumber,
|
||||
expanded,
|
||||
repoMetadata,
|
||||
pullRequestMetadata,
|
||||
onSelectStage
|
||||
}) => {
|
||||
const { data, error, loading, refetch } = useGet<TypesExecution>({
|
||||
path: `/api/v1/repos/${repoMetadata?.path}/+/pipelines/${pipelineName}/executions/${executionNumber}`,
|
||||
lazy: true
|
||||
})
|
||||
const [execution, setExecution] = useState<TypesExecution>()
|
||||
const { uid, stageId } = useQueryParams<{ pullRequestId: string; uid: string; stageId: string }>()
|
||||
const stages = useMemo(() => execution?.stages || [], [execution])
|
||||
const history = useHistory()
|
||||
const { routes } = useAppContext()
|
||||
|
||||
useShowRequestError(error, 0)
|
||||
|
||||
useEffect(() => {
|
||||
let timeoutId = 0
|
||||
|
||||
if (repoMetadata && expanded) {
|
||||
if (!execution && !error) {
|
||||
refetch()
|
||||
} else {
|
||||
if (
|
||||
!error &&
|
||||
stages.find(({ status }) => status === ExecutionState.PENDING || status === ExecutionState.RUNNING)
|
||||
) {
|
||||
timeoutId = window.setTimeout(refetch, POLLING_INTERVAL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(timeoutId)
|
||||
}
|
||||
}, [repoMetadata, expanded, execution, refetch, error, stages])
|
||||
const selectStage = useCallback(
|
||||
(stage: TypesStage) => {
|
||||
history.replace(
|
||||
routes.toCODEPullRequest({
|
||||
repoPath: repoMetadata.path as string,
|
||||
pullRequestId: String(pullRequestMetadata.number),
|
||||
pullRequestSection: PullRequestSection.CHECKS
|
||||
}) + `?uid=${pipelineName}${`&stageId=${stage.name}`}`
|
||||
)
|
||||
onSelectStage(stage)
|
||||
},
|
||||
[history, onSelectStage, pipelineName, pullRequestMetadata.number, repoMetadata.path, routes]
|
||||
)
|
||||
const stageRef = useRef<TypesStage>()
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
setExecution(data)
|
||||
}
|
||||
}, [data])
|
||||
|
||||
useEffect(() => {
|
||||
if (!expanded) {
|
||||
setExecution(undefined)
|
||||
}
|
||||
}, [expanded])
|
||||
|
||||
useEffect(() => {
|
||||
if (stages.length) {
|
||||
if (uid === pipelineName) {
|
||||
// Pre-select stage if no stage is selected in the url
|
||||
if (!stageId) {
|
||||
selectStage(findDefaultExecution(stages) as TypesStage)
|
||||
} else {
|
||||
// If a stage is selected in url, find if it's matched
|
||||
// with a stage from polling data and update selected
|
||||
// stage accordingly to make sure parents has the latest
|
||||
// stage data (status, time, etc...)
|
||||
const _stage = stages.find(stg => stg.name === stageId && stageRef.current !== stg)
|
||||
|
||||
if (_stage) {
|
||||
stageRef.current = _stage
|
||||
selectStage(_stage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [selectStage, pipelineName, stageId, stages, uid])
|
||||
|
||||
return (
|
||||
<Container className={cx(css.pipelineStages, { [css.hidden]: !expanded || error })}>
|
||||
<Match expr={loading && !execution}>
|
||||
<Truthy>
|
||||
<Container className={css.spinner}>
|
||||
<Icon name="steps-spinner" size={16} />
|
||||
</Container>
|
||||
</Truthy>
|
||||
<Falsy>
|
||||
<>
|
||||
{stages.map(stage => (
|
||||
<Layout.Horizontal
|
||||
spacing="small"
|
||||
key={stage.name}
|
||||
className={cx(css.subMenu, { [css.selected]: pipelineName === uid && stage.name === stageId })}
|
||||
{...ButtonRoleProps}
|
||||
onClick={() => {
|
||||
// ALways send back the latest stage
|
||||
selectStage(stages.find(stg => stg === stage.name) || stage)
|
||||
}}>
|
||||
<ExecutionStatus
|
||||
className={cx(css.status, css.noShrink)}
|
||||
status={stage.status as ExecutionState}
|
||||
iconSize={16}
|
||||
noBackground
|
||||
iconOnly
|
||||
/>
|
||||
<Text color={Color.GREY_800} className={css.text}>
|
||||
{stage.name}
|
||||
</Text>
|
||||
</Layout.Horizontal>
|
||||
))}
|
||||
</>
|
||||
</Falsy>
|
||||
</Match>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const POLLING_INTERVAL = 2000
|
198
web/src/pages/PullRequest/Checks/CheckPipelineSteps.tsx
Normal file
198
web/src/pages/PullRequest/Checks/CheckPipelineSteps.tsx
Normal file
@ -0,0 +1,198 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Render } from 'react-jsx-match'
|
||||
import { NavArrowRight } from 'iconoir-react'
|
||||
import cx from 'classnames'
|
||||
import { useGet } from 'restful-react'
|
||||
import Anser from 'anser'
|
||||
import { Container, Layout, Text, FlexExpander, Utils, useToaster } from '@harnessio/uicore'
|
||||
import { Icon } from '@harnessio/icons'
|
||||
import { Color, FontVariation } from '@harnessio/design-system'
|
||||
import { ButtonRoleProps, getErrorMessage, timeDistance } from 'utils/Utils'
|
||||
import type { GitInfoProps } from 'utils/GitUtils'
|
||||
import type { LivelogLine, TypesStage, TypesStep } from 'services/code'
|
||||
import { ExecutionState, ExecutionStatus } from 'components/ExecutionStatus/ExecutionStatus'
|
||||
import { useShowRequestError } from 'hooks/useShowRequestError'
|
||||
import css from './Checks.module.scss'
|
||||
|
||||
interface CheckPipelineStepsProps extends Pick<GitInfoProps, 'repoMetadata' | 'pullRequestMetadata'> {
|
||||
pipelineName: string
|
||||
executionNumber: string
|
||||
stage: TypesStage
|
||||
}
|
||||
|
||||
export const CheckPipelineSteps: React.FC<CheckPipelineStepsProps> = ({
|
||||
repoMetadata,
|
||||
pullRequestMetadata,
|
||||
pipelineName,
|
||||
stage,
|
||||
executionNumber
|
||||
}) => {
|
||||
return (
|
||||
<Container className={cx(css.pipelineSteps)}>
|
||||
{stage.steps?.map(step => (
|
||||
<CheckPipelineStep
|
||||
key={pipelineName + stage.name + executionNumber + step.name + step.started}
|
||||
pipelineName={pipelineName}
|
||||
executionNumber={executionNumber}
|
||||
repoMetadata={repoMetadata}
|
||||
pullRequestMetadata={pullRequestMetadata}
|
||||
stage={stage}
|
||||
step={step}
|
||||
/>
|
||||
))}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const CheckPipelineStep: React.FC<CheckPipelineStepsProps & { step: TypesStep }> = ({
|
||||
pipelineName,
|
||||
executionNumber,
|
||||
stage,
|
||||
repoMetadata,
|
||||
step
|
||||
}) => {
|
||||
const { showError } = useToaster()
|
||||
const eventSourceRef = useRef<EventSource | null>(null)
|
||||
const isRunning = useMemo(() => step.status === ExecutionState.RUNNING, [step])
|
||||
const [expanded, setExpanded] = useState(
|
||||
isRunning || step.status === ExecutionState.ERROR || step.status === ExecutionState.FAILURE
|
||||
)
|
||||
const stepLogPath = useMemo(
|
||||
() =>
|
||||
`/api/v1/repos/${repoMetadata?.path}/+/pipelines/${pipelineName}/executions/${executionNumber}/logs/${stage.number}/${step.number}`,
|
||||
[executionNumber, pipelineName, repoMetadata?.path, stage.number, step.number]
|
||||
)
|
||||
const lazy =
|
||||
!expanded || isRunning || step.status === ExecutionState.PENDING || step.status === ExecutionState.SKIPPED
|
||||
const {
|
||||
data: logs,
|
||||
error,
|
||||
loading,
|
||||
refetch
|
||||
} = useGet<LivelogLine[]>({
|
||||
path: stepLogPath,
|
||||
lazy: true
|
||||
})
|
||||
const [isStreamingDone, setIsStreamingDone] = useState(false)
|
||||
const containerRef = useRef<HTMLDivElement | null>(null)
|
||||
const [autoCollapse, setAutoCollapse] = useState(false)
|
||||
const closeEventStream = useCallback(() => {
|
||||
eventSourceRef.current?.close()
|
||||
eventSourceRef.current = null
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (expanded && isRunning) {
|
||||
setAutoCollapse(false)
|
||||
|
||||
if (containerRef.current) {
|
||||
containerRef.current.textContent = ''
|
||||
}
|
||||
eventSourceRef.current = new EventSource(`${stepLogPath}/stream`)
|
||||
|
||||
eventSourceRef.current.onmessage = event => {
|
||||
try {
|
||||
const scrollParent = containerRef.current?.closest(`.${css.pipelineSteps}`) as HTMLDivElement
|
||||
const autoScroll =
|
||||
scrollParent && scrollParent.scrollTop === scrollParent.scrollHeight - scrollParent.offsetHeight
|
||||
|
||||
const element = createLogLineElement((JSON.parse(event.data) as LivelogLine).out)
|
||||
containerRef.current?.appendChild(element)
|
||||
|
||||
if (autoScroll) {
|
||||
scrollParent.scrollTop = scrollParent.scrollHeight
|
||||
}
|
||||
} catch (exception) {
|
||||
showError(getErrorMessage(exception))
|
||||
closeEventStream()
|
||||
}
|
||||
}
|
||||
|
||||
eventSourceRef.current.onerror = () => {
|
||||
setIsStreamingDone(true)
|
||||
setAutoCollapse(true)
|
||||
closeEventStream()
|
||||
}
|
||||
} else {
|
||||
closeEventStream()
|
||||
}
|
||||
|
||||
return closeEventStream
|
||||
}, [expanded, isRunning, showError, stepLogPath, step.status, closeEventStream])
|
||||
|
||||
useEffect(() => {
|
||||
if (!lazy && !error && (!isStreamingDone || !isRunning) && expanded) {
|
||||
refetch()
|
||||
}
|
||||
}, [lazy, error, refetch, isStreamingDone, expanded, isRunning])
|
||||
|
||||
useEffect(() => {
|
||||
if (autoCollapse && expanded && step.status === ExecutionState.SUCCESS) {
|
||||
setAutoCollapse(false)
|
||||
setExpanded(false)
|
||||
}
|
||||
}, [autoCollapse, expanded, step.status])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isRunning && logs?.length) {
|
||||
logs.forEach(_log => {
|
||||
const element = createLogLineElement(_log.out)
|
||||
containerRef.current?.appendChild(element)
|
||||
})
|
||||
}
|
||||
}, [isRunning, logs])
|
||||
|
||||
useShowRequestError(error, 0)
|
||||
|
||||
return (
|
||||
<Container key={step.number}>
|
||||
<Layout.Horizontal
|
||||
spacing="small"
|
||||
className={cx(css.stepHeader, { [css.expanded]: expanded, [css.selected]: expanded })}
|
||||
{...ButtonRoleProps}
|
||||
onClick={() => {
|
||||
setExpanded(!expanded)
|
||||
}}>
|
||||
<NavArrowRight
|
||||
color={Utils.getRealCSSColor(Color.GREY_500)}
|
||||
className={cx(css.noShrink, css.chevron)}
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
|
||||
<ExecutionStatus
|
||||
className={cx(css.status, css.noShrink)}
|
||||
status={step.status as ExecutionState}
|
||||
iconSize={16}
|
||||
noBackground
|
||||
iconOnly
|
||||
/>
|
||||
|
||||
<Text className={css.name} lineClamp={1}>
|
||||
{step.name}
|
||||
</Text>
|
||||
|
||||
<FlexExpander />
|
||||
|
||||
<Render when={loading}>
|
||||
<Icon name="steps-spinner" size={16} />
|
||||
</Render>
|
||||
|
||||
<Render when={step.started && step.stopped}>
|
||||
<Text color={Color.GREY_300} font={{ variation: FontVariation.SMALL }} className={css.noShrink}>
|
||||
{timeDistance(step.started, step.stopped)}
|
||||
</Text>
|
||||
</Render>
|
||||
</Layout.Horizontal>
|
||||
<Render when={expanded}>
|
||||
<Container className={css.stepLogViewer} ref={containerRef}></Container>
|
||||
</Render>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const createLogLineElement = (line = '') => {
|
||||
const element = document.createElement('pre')
|
||||
element.className = css.consoleLine
|
||||
element.innerHTML = Anser.ansiToHtml(line.replace(/\r?\n$/, ''))
|
||||
return element
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
@import 'src/utils/utils';
|
||||
|
||||
.main {
|
||||
--stage-title-height: 54px;
|
||||
--stage-detail-section-height: 48px;
|
||||
@ -194,6 +196,9 @@
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 10px 20px 0 !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
@ -248,6 +253,19 @@
|
||||
}
|
||||
|
||||
.stepLogViewer {
|
||||
padding-left: 20px;
|
||||
padding: 15px 36px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.consoleLine {
|
||||
color: var(--white);
|
||||
|
||||
@include mono-font;
|
||||
|
||||
word-wrap: break-word !important;
|
||||
white-space: pre-wrap !important;
|
||||
cursor: text;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
// This is an auto-generated file
|
||||
export declare const active: string
|
||||
export declare const chevron: string
|
||||
export declare const consoleLine: string
|
||||
export declare const content: string
|
||||
export declare const expanded: string
|
||||
export declare const header: string
|
||||
|
@ -1,25 +1,14 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Falsy, Match, Render, Truthy } from 'react-jsx-match'
|
||||
import { CheckCircle, NavArrowRight } from 'iconoir-react'
|
||||
import { get } from 'lodash-es'
|
||||
import { get, sortBy } from 'lodash-es'
|
||||
import cx from 'classnames'
|
||||
import { useGet } from 'restful-react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import {
|
||||
Container,
|
||||
Layout,
|
||||
Text,
|
||||
FlexExpander,
|
||||
Utils,
|
||||
Button,
|
||||
ButtonVariation,
|
||||
ButtonSize,
|
||||
useToaster
|
||||
} from '@harnessio/uicore'
|
||||
import { Container, Layout, Text, FlexExpander, Utils, Button, ButtonVariation, ButtonSize } from '@harnessio/uicore'
|
||||
import { Icon } from '@harnessio/icons'
|
||||
import { Color, FontVariation } from '@harnessio/design-system'
|
||||
import { LogViewer, TermRefs } from 'components/LogViewer/LogViewer'
|
||||
import { ButtonRoleProps, getErrorMessage, PullRequestCheckType, PullRequestSection, timeDistance } from 'utils/Utils'
|
||||
import { ButtonRoleProps, PullRequestCheckType, PullRequestSection, timeDistance } from 'utils/Utils'
|
||||
import type { GitInfoProps } from 'utils/GitUtils'
|
||||
import { useAppContext } from 'AppContext'
|
||||
import { useQueryParams } from 'hooks/useQueryParams'
|
||||
@ -27,17 +16,18 @@ import { useStrings } from 'framework/strings'
|
||||
import { Split } from 'components/Split/Split'
|
||||
import { MarkdownViewer } from 'components/MarkdownViewer/MarkdownViewer'
|
||||
import type { PRChecksDecisionResult } from 'hooks/usePRChecksDecision'
|
||||
import type { LivelogLine, TypesCheck, TypesExecution, TypesStage, TypesStep } from 'services/code'
|
||||
import type { TypesCheck, TypesStage } from 'services/code'
|
||||
import { ExecutionState, ExecutionStatus } from 'components/ExecutionStatus/ExecutionStatus'
|
||||
import { useShowRequestError } from 'hooks/useShowRequestError'
|
||||
// import drone from './drone.svg'
|
||||
import { CheckPipelineStages } from './CheckPipelineStages'
|
||||
import { findDefaultExecution } from './ChecksUtils'
|
||||
import { CheckPipelineSteps } from './CheckPipelineSteps'
|
||||
import css from './Checks.module.scss'
|
||||
|
||||
interface ChecksProps extends Pick<GitInfoProps, 'repoMetadata' | 'pullRequestMetadata'> {
|
||||
prChecksDecisionResult?: PRChecksDecisionResult
|
||||
}
|
||||
|
||||
export const Checks: React.FC<ChecksProps> = props => {
|
||||
export const Checks: React.FC<ChecksProps> = ({ repoMetadata, pullRequestMetadata, prChecksDecisionResult }) => {
|
||||
const { getString } = useStrings()
|
||||
const history = useHistory()
|
||||
const { routes } = useAppContext()
|
||||
@ -56,32 +46,34 @@ export const Checks: React.FC<ChecksProps> = props => {
|
||||
const executionLink = useMemo(() => {
|
||||
if (selectedStage) {
|
||||
return routes.toCODEExecution({
|
||||
repoPath: props.repoMetadata?.path as string,
|
||||
repoPath: repoMetadata?.path as string,
|
||||
pipeline: selectedItemData?.uid as string,
|
||||
execution: get(selectedItemData, 'payload.data.execution_number', '')
|
||||
})
|
||||
} else {
|
||||
return selectedItemData?.link
|
||||
}
|
||||
}, [props.repoMetadata?.path, routes, selectedItemData, selectedStage])
|
||||
}, [repoMetadata?.path, routes, selectedItemData, selectedStage])
|
||||
|
||||
if (!props.prChecksDecisionResult) {
|
||||
if (!prChecksDecisionResult) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className={css.main}>
|
||||
<Match expr={props.prChecksDecisionResult?.overallStatus}>
|
||||
<Match expr={prChecksDecisionResult?.overallStatus}>
|
||||
<Truthy>
|
||||
<Split
|
||||
split="vertical"
|
||||
size="calc(100% - 400px)"
|
||||
minSize={800}
|
||||
maxSize="calc(100% - 900px)"
|
||||
size={'calc(100% - 400px)'}
|
||||
minSize={'calc(100% - 300px)'}
|
||||
maxSize={'calc(100% - 600px)'}
|
||||
onDragFinished={onSplitPaneResized}
|
||||
primary="second">
|
||||
<ChecksMenu
|
||||
{...props}
|
||||
repoMetadata={repoMetadata}
|
||||
pullRequestMetadata={pullRequestMetadata}
|
||||
prChecksDecisionResult={prChecksDecisionResult}
|
||||
onDataItemChanged={data => {
|
||||
setTimeout(() => setSelectedItemData(data), 0)
|
||||
}}
|
||||
@ -138,10 +130,12 @@ export const Checks: React.FC<ChecksProps> = props => {
|
||||
<Container className={css.terminalContainer}>
|
||||
<Match expr={selectedStage}>
|
||||
<Truthy>
|
||||
<PipelineSteps
|
||||
itemData={selectedItemData as TypesCheck}
|
||||
repoMetadata={props.repoMetadata}
|
||||
<CheckPipelineSteps
|
||||
repoMetadata={repoMetadata}
|
||||
pullRequestMetadata={pullRequestMetadata}
|
||||
pipelineName={selectedItemData?.uid as string}
|
||||
stage={selectedStage as TypesStage}
|
||||
executionNumber={get(selectedItemData, 'payload.data.execution_number', '')}
|
||||
/>
|
||||
</Truthy>
|
||||
<Falsy>
|
||||
@ -181,6 +175,7 @@ const ChecksMenu: React.FC<ChecksMenuProps> = ({
|
||||
const { uid } = useQueryParams<{ uid: string }>()
|
||||
const [selectedUID, setSelectedUID] = React.useState<string | undefined>()
|
||||
const [selectedStage, setSelectedStage] = useState<TypesStage | null>(null)
|
||||
|
||||
useMemo(() => {
|
||||
if (selectedUID) {
|
||||
const selectedDataItem = prChecksDecisionResult?.data?.find(item => item.uid === selectedUID)
|
||||
@ -196,14 +191,7 @@ const ChecksMenu: React.FC<ChecksMenuProps> = ({
|
||||
setSelectedUID(uid)
|
||||
}
|
||||
} else {
|
||||
// Find and set a default selected item. Order: Error, Failure, Running, Success, Pending
|
||||
const defaultSelectedItem =
|
||||
prChecksDecisionResult?.data?.find(({ status }) => status === ExecutionState.ERROR) ||
|
||||
prChecksDecisionResult?.data?.find(({ status }) => status === ExecutionState.FAILURE) ||
|
||||
prChecksDecisionResult?.data?.find(({ status }) => status === ExecutionState.RUNNING) ||
|
||||
prChecksDecisionResult?.data?.find(({ status }) => status === ExecutionState.SUCCESS) ||
|
||||
prChecksDecisionResult?.data?.find(({ status }) => status === ExecutionState.PENDING) ||
|
||||
prChecksDecisionResult?.data?.[0]
|
||||
const defaultSelectedItem = findDefaultExecution(prChecksDecisionResult?.data)
|
||||
|
||||
if (defaultSelectedItem) {
|
||||
onDataItemChanged(defaultSelectedItem)
|
||||
@ -231,7 +219,7 @@ const ChecksMenu: React.FC<ChecksMenuProps> = ({
|
||||
|
||||
return (
|
||||
<Container className={css.menu}>
|
||||
{prChecksDecisionResult?.data?.map(itemData => (
|
||||
{sortBy(prChecksDecisionResult?.data || [], ['uid'])?.map(itemData => (
|
||||
<CheckMenuItem
|
||||
repoMetadata={repoMetadata}
|
||||
pullRequestMetadata={pullRequestMetadata}
|
||||
@ -253,6 +241,10 @@ const ChecksMenu: React.FC<ChecksMenuProps> = ({
|
||||
}) + `?uid=${itemData.uid}${stage ? `&stageId=${stage.name}` : ''}`
|
||||
)
|
||||
}}
|
||||
setSelectedStage={stage => {
|
||||
setSelectedStage(stage)
|
||||
setSelectedStageFromProps(stage)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Container>
|
||||
@ -264,6 +256,7 @@ interface CheckMenuItemProps extends ChecksProps {
|
||||
isSelected?: boolean
|
||||
itemData: TypesCheck
|
||||
onClick: (stage?: TypesStage) => void
|
||||
setSelectedStage: (stage: TypesStage | null) => void
|
||||
}
|
||||
|
||||
const CheckMenuItem: React.FC<CheckMenuItemProps> = ({
|
||||
@ -271,7 +264,9 @@ const CheckMenuItem: React.FC<CheckMenuItemProps> = ({
|
||||
isSelected = false,
|
||||
itemData,
|
||||
onClick,
|
||||
repoMetadata
|
||||
repoMetadata,
|
||||
pullRequestMetadata,
|
||||
setSelectedStage
|
||||
}) => {
|
||||
const [expanded, setExpanded] = useState(isSelected)
|
||||
|
||||
@ -297,7 +292,6 @@ const CheckMenuItem: React.FC<CheckMenuItemProps> = ({
|
||||
<Match expr={isPipeline}>
|
||||
<Truthy>
|
||||
<Icon name="pipeline" size={20} />
|
||||
{/* <img src={drone} width={20} height={20} /> */}
|
||||
</Truthy>
|
||||
<Falsy>
|
||||
<CheckCircle color={Utils.getRealCSSColor(Color.GREY_500)} className={css.noShrink} />
|
||||
@ -332,255 +326,15 @@ const CheckMenuItem: React.FC<CheckMenuItemProps> = ({
|
||||
</Layout.Horizontal>
|
||||
|
||||
<Render when={isPipeline}>
|
||||
<PipelineStages itemData={itemData} expanded={expanded} repoMetadata={repoMetadata} onClick={onClick} />
|
||||
</Render>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const PipelineStages: React.FC<
|
||||
Pick<CheckMenuItemProps, 'repoMetadata' | 'itemData' | 'onClick'> & { expanded: boolean }
|
||||
> = ({ itemData, expanded, repoMetadata, onClick }) => {
|
||||
const {
|
||||
data: execution,
|
||||
error,
|
||||
loading,
|
||||
refetch
|
||||
} = useGet<TypesExecution>({
|
||||
path: `/api/v1/repos/${repoMetadata?.path}/+/pipelines/${itemData.uid}/executions/${get(
|
||||
itemData,
|
||||
'payload.data.execution_number'
|
||||
)}`,
|
||||
lazy: true
|
||||
})
|
||||
const { uid, stageId } = useQueryParams<{ uid: string; stageId: string }>()
|
||||
const stages = useMemo(() => execution?.stages, [execution])
|
||||
const [selectedStageName, setSelectedStageName] = useState<string>('')
|
||||
|
||||
useShowRequestError(error)
|
||||
|
||||
useEffect(() => {
|
||||
let timeoutId = 0
|
||||
|
||||
if (repoMetadata && expanded) {
|
||||
if (!execution) {
|
||||
refetch()
|
||||
} else {
|
||||
if (
|
||||
!error &&
|
||||
stages?.find(({ status }) => status === ExecutionState.PENDING || status === ExecutionState.RUNNING)
|
||||
) {
|
||||
timeoutId = window.setTimeout(refetch, PIPELINE_STAGE_POLLING_INTERVAL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(timeoutId)
|
||||
}
|
||||
}, [repoMetadata, expanded, execution, refetch, error, stages])
|
||||
|
||||
useEffect(() => {
|
||||
if (stages?.length) {
|
||||
if (uid === itemData.uid && !selectedStageName) {
|
||||
if (!stageId) {
|
||||
setSelectedStageName(stages[0].name as string)
|
||||
onClick(stages[0])
|
||||
} else {
|
||||
const _stage = stages.find(({ name }) => name === stageId)
|
||||
if (_stage) {
|
||||
setSelectedStageName(_stage.name as string)
|
||||
onClick(_stage)
|
||||
}
|
||||
}
|
||||
} else if (uid !== itemData.uid && selectedStageName) {
|
||||
setSelectedStageName('')
|
||||
}
|
||||
}
|
||||
}, [stages, selectedStageName, setSelectedStageName, onClick, stageId, uid, itemData.uid])
|
||||
|
||||
useEffect(() => {
|
||||
if (stageId && selectedStageName && selectedStageName !== stageId) {
|
||||
setSelectedStageName('')
|
||||
}
|
||||
}, [stageId, selectedStageName])
|
||||
|
||||
return (
|
||||
<Container className={cx(css.pipelineStages, { [css.hidden]: !expanded })}>
|
||||
<Match expr={loading && !execution}>
|
||||
<Truthy>
|
||||
<Container className={css.spinner}>
|
||||
<Icon name="steps-spinner" size={16} />
|
||||
</Container>
|
||||
</Truthy>
|
||||
<Falsy>
|
||||
<>
|
||||
{stages?.map(stage => (
|
||||
<Layout.Horizontal
|
||||
spacing="small"
|
||||
key={stage.name}
|
||||
className={cx(css.subMenu, { [css.selected]: stage.name === selectedStageName })}
|
||||
{...ButtonRoleProps}
|
||||
onClick={() => {
|
||||
onClick(stage)
|
||||
setSelectedStageName(stage.name as string)
|
||||
}}>
|
||||
<ExecutionStatus
|
||||
className={cx(css.status, css.noShrink)}
|
||||
status={stage.status as ExecutionState}
|
||||
iconSize={16}
|
||||
noBackground
|
||||
iconOnly
|
||||
/>
|
||||
<Text color={Color.GREY_800} className={css.text}>
|
||||
{stage.name}
|
||||
</Text>
|
||||
</Layout.Horizontal>
|
||||
))}
|
||||
</>
|
||||
</Falsy>
|
||||
</Match>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const PipelineSteps: React.FC<Pick<CheckMenuItemProps, 'repoMetadata' | 'itemData'> & { stage: TypesStage }> = ({
|
||||
itemData,
|
||||
stage,
|
||||
repoMetadata
|
||||
}) => {
|
||||
return (
|
||||
<Container className={cx(css.pipelineSteps)}>
|
||||
{stage.steps?.map(step => (
|
||||
<PipelineStep
|
||||
key={(itemData.uid + ((stage.name as string) + step.name)) as string}
|
||||
itemData={itemData}
|
||||
<CheckPipelineStages
|
||||
pipelineName={itemData.uid as string}
|
||||
executionNumber={get(itemData, 'payload.data.execution_number', '')}
|
||||
expanded={expanded}
|
||||
repoMetadata={repoMetadata}
|
||||
stage={stage}
|
||||
step={step}
|
||||
pullRequestMetadata={pullRequestMetadata}
|
||||
onSelectStage={setSelectedStage}
|
||||
/>
|
||||
))}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const PipelineStep: React.FC<
|
||||
Pick<CheckMenuItemProps, 'repoMetadata' | 'itemData'> & { stage: TypesStage; step: TypesStep }
|
||||
> = ({ itemData, stage, repoMetadata, step }) => {
|
||||
const { showError } = useToaster()
|
||||
const eventSourceRef = useRef<EventSource | null>(null)
|
||||
const [streamingLogs, setStreamingLogs] = useState<LivelogLine[]>([])
|
||||
const isRunning = useMemo(() => step.status === ExecutionState.RUNNING, [step])
|
||||
const [expanded, setExpanded] = useState(
|
||||
isRunning || step.status === ExecutionState.ERROR || step.status === ExecutionState.FAILURE
|
||||
)
|
||||
const stepLogPath = useMemo(
|
||||
() =>
|
||||
`/api/v1/repos/${repoMetadata?.path}/+/pipelines/${itemData.uid}/executions/${get(
|
||||
itemData,
|
||||
'payload.data.execution_number'
|
||||
)}/logs/${stage.number}/${step.number}`,
|
||||
[itemData, repoMetadata?.path, stage.number, step.number]
|
||||
)
|
||||
const lazy =
|
||||
!expanded || isRunning || step.status === ExecutionState.PENDING || step.status === ExecutionState.SKIPPED
|
||||
const {
|
||||
data: logs,
|
||||
error,
|
||||
loading
|
||||
} = useGet<LivelogLine[]>({
|
||||
path: stepLogPath,
|
||||
lazy
|
||||
})
|
||||
const logContent = useMemo(
|
||||
() => ((isRunning ? streamingLogs : logs) || []).map(log => log.out || '').join(''),
|
||||
[streamingLogs, logs, isRunning]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (isRunning) {
|
||||
if (eventSourceRef.current) {
|
||||
eventSourceRef.current.close()
|
||||
}
|
||||
|
||||
setStreamingLogs([])
|
||||
eventSourceRef.current = new EventSource(`${stepLogPath}/stream`)
|
||||
|
||||
eventSourceRef.current.onmessage = event => {
|
||||
try {
|
||||
setStreamingLogs(existingLogs => [...existingLogs, JSON.parse(event.data)])
|
||||
} catch (exception) {
|
||||
showError(getErrorMessage(exception))
|
||||
eventSourceRef.current?.close()
|
||||
eventSourceRef.current = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
setStreamingLogs([])
|
||||
eventSourceRef.current?.close()
|
||||
}
|
||||
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
useShowRequestError(error)
|
||||
|
||||
return (
|
||||
<Container key={step.number}>
|
||||
<Layout.Horizontal
|
||||
spacing="small"
|
||||
className={cx(css.stepHeader, { [css.expanded]: expanded, [css.selected]: expanded })}
|
||||
{...ButtonRoleProps}
|
||||
onClick={() => {
|
||||
setExpanded(!expanded)
|
||||
}}>
|
||||
<NavArrowRight
|
||||
color={Utils.getRealCSSColor(Color.GREY_500)}
|
||||
className={cx(css.noShrink, css.chevron)}
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
|
||||
<ExecutionStatus
|
||||
className={cx(css.status, css.noShrink)}
|
||||
status={step.status as ExecutionState}
|
||||
iconSize={16}
|
||||
noBackground
|
||||
iconOnly
|
||||
/>
|
||||
|
||||
<Text className={css.name} lineClamp={1}>
|
||||
{step.name}
|
||||
</Text>
|
||||
|
||||
<FlexExpander />
|
||||
|
||||
<Render when={loading}>
|
||||
<Icon name="steps-spinner" size={16} />
|
||||
</Render>
|
||||
|
||||
<Render when={step.started && step.stopped}>
|
||||
<Text color={Color.GREY_300} font={{ variation: FontVariation.SMALL }} className={css.noShrink}>
|
||||
{timeDistance(step.started, step.stopped)}
|
||||
</Text>
|
||||
</Render>
|
||||
</Layout.Horizontal>
|
||||
<Render when={expanded}>
|
||||
<Container className={css.stepLogViewer}>
|
||||
<Match expr={isRunning}>
|
||||
<Truthy>
|
||||
{/* Streaming puts too much pressure on xtermjs and cause incorrect row calculation. Using key to force React to create new instance every time there is new data */}
|
||||
{[streamingLogs.length].map(len => (
|
||||
<LogViewer key={len} content={logContent} autoHeight />
|
||||
))}
|
||||
</Truthy>
|
||||
<Falsy>
|
||||
<LogViewer content={logContent} autoHeight />
|
||||
</Falsy>
|
||||
</Match>
|
||||
</Container>
|
||||
</Render>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const PIPELINE_STAGE_POLLING_INTERVAL = 5000
|
||||
|
15
web/src/pages/PullRequest/Checks/ChecksUtils.ts
Normal file
15
web/src/pages/PullRequest/Checks/ChecksUtils.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { ExecutionState } from 'components/ExecutionStatus/ExecutionStatus'
|
||||
import type { EnumCheckStatus } from 'services/code'
|
||||
|
||||
type CheckType = { status: EnumCheckStatus }[]
|
||||
|
||||
export function findDefaultExecution<T>(collection: Iterable<T> | null | undefined) {
|
||||
return (collection as CheckType)?.length
|
||||
? (((collection as CheckType).find(({ status }) => status === ExecutionState.ERROR) ||
|
||||
(collection as CheckType).find(({ status }) => status === ExecutionState.FAILURE) ||
|
||||
(collection as CheckType).find(({ status }) => status === ExecutionState.RUNNING) ||
|
||||
(collection as CheckType).find(({ status }) => status === ExecutionState.SUCCESS) ||
|
||||
(collection as CheckType).find(({ status }) => status === ExecutionState.PENDING) ||
|
||||
(collection as CheckType)[0]) as T)
|
||||
: null
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g opacity="0.9">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.7141 7.77147C13.7141 8.50462 13.5719 9.21745 13.3235 9.87395C13.2779 9.9945 13.124 10.0248 13.0323 9.93419L11.2087 8.13261C11.1326 8.05744 11.1199 7.94005 11.1695 7.84529C11.3672 7.46734 11.4661 7.03501 11.4661 6.5829C11.492 5.1101 10.2776 3.89569 8.77893 3.89569C8.30313 3.89569 7.86706 4.0149 7.48816 4.21847C7.39489 4.26857 7.27878 4.25661 7.20342 4.18223L5.58012 2.58001C5.46572 2.4671 5.50071 2.27474 5.65076 2.21712C6.31081 1.96367 7.01208 1.82861 7.77123 1.82861C11.0527 1.82861 13.7141 4.48998 13.7141 7.77147ZM4.33148 2.93518C4.43742 2.86053 4.58114 2.87604 4.67279 2.96769L6.51749 4.81239C6.599 4.8939 6.60614 5.02281 6.54217 5.1187C6.25383 5.55093 6.09172 6.05957 6.09172 6.60874C6.09172 8.10737 7.30613 9.32178 8.80477 9.32178C9.35394 9.32178 9.86258 9.15967 10.2948 8.87134C10.3907 8.80737 10.5196 8.8145 10.6011 8.89601L12.5979 10.8928C12.6908 10.9857 12.7053 11.1321 12.6276 11.2382C11.5311 12.7361 9.78238 13.7143 7.77123 13.7143C4.48974 13.7143 1.82837 11.053 1.82837 7.77147C1.82837 5.75853 2.80833 4.0085 4.33148 2.93518ZM8.80477 8.28824C9.81247 8.28824 10.6135 7.48725 10.6135 6.47955C10.6135 5.47184 9.81247 4.67085 8.80477 4.67085C7.79706 4.67085 6.99607 5.47184 6.99607 6.47955C6.99607 7.48725 7.79706 8.28824 8.80477 8.28824Z" fill="url(#paint0_linear_4015_405182)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_4015_405182" x1="-2.01082" y1="5.6678" x2="5.66756" y2="17.5535" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#73DFE7"/>
|
||||
<stop offset="1" stop-color="#0095F7"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.7 KiB |
@ -1,14 +1,12 @@
|
||||
$code-editor-font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono',
|
||||
monospace;
|
||||
|
||||
@mixin mono-font {
|
||||
font-family: Menlo, Monaco, 'Courier New', monospace var(--font-family-mono) !important;
|
||||
font-weight: normal !important;
|
||||
font-family: var(--font-family-mono) !important;
|
||||
font-size: 13px !important;
|
||||
font-weight: normal !important;
|
||||
font-kerning: none;
|
||||
letter-spacing: 0;
|
||||
line-height: 20px;
|
||||
font-feature-settings: 'liga' 0, 'calt' 0;
|
||||
font-variation-settings: normal;
|
||||
line-height: 18px;
|
||||
letter-spacing: 0px;
|
||||
}
|
||||
|
||||
@mixin markdown-font {
|
||||
|
@ -2273,10 +2273,10 @@ ajv@^8.0.0, ajv@^8.0.1, ajv@^8.9.0:
|
||||
require-from-string "^2.0.2"
|
||||
uri-js "^4.2.2"
|
||||
|
||||
anser@2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/anser/-/anser-2.0.1.tgz#8d9069291fee18306ffaf2e364a690dcc8ed78ad"
|
||||
integrity sha512-4g5Np4CVD3c5c/36Mj0jllEA5bQcuXF0dqakZcuHGeubBzw93EAhwRuQCzgFm4/ZwvyBMzFdtn9BcihOjnxIdQ==
|
||||
anser@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.npmjs.org/anser/-/anser-2.1.1.tgz#8afae28d345424c82de89cc0e4d1348eb0c5af7c"
|
||||
integrity sha512-nqLm4HxOTpeLOxcmB3QWmV5TcDFhW9y/fyQ+hivtDFcK4OQ+pQ5fzPnXHM1Mfcm0VkLtvVi1TCPr++Qy0Q/3EQ==
|
||||
|
||||
ansi-align@^2.0.0:
|
||||
version "2.0.0"
|
||||
@ -11410,30 +11410,25 @@ xmlchars@^2.2.0:
|
||||
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
|
||||
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
|
||||
|
||||
xterm-addon-canvas@^0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/xterm-addon-canvas/-/xterm-addon-canvas-0.3.0.tgz#8cfb5a13297f4a31a12870c1119af2c139392b50"
|
||||
integrity sha512-2deF4ev6T+NjgSM56H+jcAWz4k5viEoaBtuDEyfo5Qdh1r7HOvNzLC45HSeegdH38qmEcL9XIt0KXyOINpSFRA==
|
||||
|
||||
xterm-addon-fit@^0.7.0:
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.7.0.tgz#b8ade6d96e63b47443862088f6670b49fb752c6a"
|
||||
integrity sha512-tQgHGoHqRTgeROPnvmtEJywLKoC/V9eNs4bLLz7iyJr1aW/QFzRwfd3MGiJ6odJd9xEfxcW36/xRU47JkD5NKQ==
|
||||
xterm-addon-fit@^0.8.0:
|
||||
version "0.8.0"
|
||||
resolved "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.8.0.tgz#48ca99015385141918f955ca7819e85f3691d35f"
|
||||
integrity sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw==
|
||||
|
||||
xterm-addon-search@^0.11.0:
|
||||
version "0.11.0"
|
||||
resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.11.0.tgz#2a00ff7f9848f6140e7c4d1782486b0b18b06e0d"
|
||||
integrity sha512-6U4uHXcQ7G5igsdaGqrJ9ehm7vep24bXqWxuy3AnIosXF2Z5uy2MvmYRyTGNembIqPV/x1YhBQ7uShtuqBHhOQ==
|
||||
|
||||
xterm-addon-web-links@^0.8.0:
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.8.0.tgz#2cb1d57129271022569208578b0bf4774e7e6ea9"
|
||||
integrity sha512-J4tKngmIu20ytX9SEJjAP3UGksah7iALqBtfTwT9ZnmFHVplCumYQsUJfKuS+JwMhjsjH61YXfndenLNvjRrEw==
|
||||
xterm-addon-web-links@^0.9.0:
|
||||
version "0.9.0"
|
||||
resolved "https://registry.npmjs.org/xterm-addon-web-links/-/xterm-addon-web-links-0.9.0.tgz#c65b18588d1f613e703eb6feb7f129e7ff1c63e7"
|
||||
integrity sha512-LIzi4jBbPlrKMZF3ihoyqayWyTXAwGfu4yprz1aK2p71e9UKXN6RRzVONR0L+Zd+Ik5tPVI9bwp9e8fDTQh49Q==
|
||||
|
||||
xterm@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.1.0.tgz#3e160d60e6801c864b55adf19171c49d2ff2b4fc"
|
||||
integrity sha512-LovENH4WDzpwynj+OTkLyZgJPeDom9Gra4DMlGAgz6pZhIDCQ+YuO7yfwanY+gVbn/mmZIStNOnVRU/ikQuAEQ==
|
||||
xterm@^5.3.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz#867daf9cc826f3d45b5377320aabd996cb0fce46"
|
||||
integrity sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==
|
||||
|
||||
"y18n@^3.2.1 || ^4.0.0", y18n@^4.0.0:
|
||||
version "4.0.3"
|
||||
|
Loading…
x
Reference in New Issue
Block a user