sarif-web-component/components/RunCard.renderCell.tsx

191 строка
7.1 KiB
TypeScript

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import './RunCard.renderCell.scss'
import * as React from 'react'
import {Fragment} from 'react'
import * as ReactMarkdown from 'react-markdown'
import {Result} from 'sarif'
import {Hi} from './Hi'
import {tryOr, tryLink} from './try'
import {Rule, More, ResultOrRuleOrMore} from './Viewer.Types'
import {Snippet} from './Snippet'
import {css} from 'azure-devops-ui/Util'
import {Link} from 'azure-devops-ui/Link'
import {ObservableLike} from 'azure-devops-ui/Core/Observable'
import {Status, Statuses, StatusSize} from "azure-devops-ui/Status"
import {PillSize, Pill} from 'azure-devops-ui/Pill'
import {ISimpleTableCell, TableCell} from 'azure-devops-ui/Table'
import {ExpandableTreeCell, ITreeColumn} from 'azure-devops-ui/TreeEx'
import {ITreeItemEx, ITreeItem} from 'azure-devops-ui/Utilities/TreeItemProvider'
import {Icon, IconSize} from 'azure-devops-ui/Icon'
import { renderPathCell } from './RunCard.renderPathCell'
import { renderActionsCell } from './RunCard.renderActionsCell'
import { getRepoUri } from './getRepoUri'
const colspan = 99 // No easy way to parameterize this, however extra does not hurt, so using an arbitrarily large value.
export function renderCell<T extends ISimpleTableCell>(
rowIndex: number,
columnIndex: number,
treeColumn: ITreeColumn<T>,
treeItem: ITreeItemEx<T>): JSX.Element {
const data = ObservableLike.getValue(treeItem.underlyingItem.data)
const commonProps = {
className: treeColumn.className,
columnIndex,
treeItem,
treeColumn,
}
// ROW AGE
const isAge = (item => item.isAge) as (item: any) => item is { name: string, treeItem: ITreeItem<ResultOrRuleOrMore> }
if (isAge(data)) {
const age = data
return columnIndex === 0
? ExpandableTreeCell({
children: <div className="swcRowRule">{/* Div for flow layout. */}
{age.name}
<Pill size={PillSize.compact}>{age.treeItem.childItemsAll.length}</Pill>
</div>,
colspan,
...commonProps,
})
: null
}
// ROW RULE
const isRule = (item => item.isRule) as (item: any) => item is Rule
if (isRule(data)) {
const rule = data
return columnIndex === 0
? ExpandableTreeCell({
children: <div className="swcRowRule">{/* Div for flow layout. */}
{tryLink(() => rule.helpUri, <Hi>{rule.id || rule.guid}</Hi>)}
{tryOr(() => rule.name && <>: <Hi>{rule.name}</Hi></>)}
{tryOr(() => rule.relationships.map((rel, i) => {
const taxon = rule.run.taxonomies[rel.target.toolComponent.index].taxa[rel.target.index]
return <Fragment key={rel.target.id}>{i > 0 ? ',' : ''} {tryLink(() => taxon.helpUri, taxon.name)}</Fragment>
}))}
<Pill size={PillSize.compact}>{rule.treeItem.childItemsAll.length}</Pill>
</div>,
colspan,
...commonProps,
})
: null
}
// ROW RESULT
const capitalize = str => `${str[0].toUpperCase()}${str.slice(1)}`
const isResult = (item => item.message !== undefined) as (item: any) => item is Result
if (isResult(data)) {
const result = data
const status = {
none: result.kind === 'pass' ? Statuses.Success : Statuses.Queued,
note: Statuses.Information,
error: Statuses.Failed,
}[result.level] || Statuses.Warning
return columnIndex === 0
// ExpandableTreeCell (td div.bolt-table-cell-content.flex-row.flex-center TreeExpand children)
// calls SimpleTableCell - adds an extra div
// calls TableCell
? ExpandableTreeCell({ // As close to Table#TwoLineTableCell (which calls TableCell) as possible.
children: <>
<Status {...status} className="bolt-table-two-line-cell-icon flex-noshrink bolt-table-status-icon" size={StatusSize.m} ariaLabel={result.level || 'warning'} />
{renderPathCell(result)}
</>,
...commonProps,
})
: TableCell({ // Don't want SimpleTableCell as it has flex row.
children: (() => {
const rule = result._rule
switch (treeColumn.id) {
case 'Actions':
return <> {renderActionsCell(result)} </>
case 'Details':
const messageFromRule = result._rule?.messageStrings?.[result.message.id ?? -1] ?? result.message;
const formattedMessage = format(messageFromRule.text || result.message?.text, result.message.arguments) ?? '';
const formattedMarkdown = format(messageFromRule.markdown || result.message?.markdown, result.message.arguments); // No '—', leave undefined if empty.
return <>
{formattedMarkdown
? <div className="swcMarkDown">
<ReactMarkdown source={formattedMarkdown}
renderers={{ link: ({href, children}) => <a href={href} target="_blank">{children}</a> }} />
</div> // Div to cancel out containers display flex row.
: <span style={{ whiteSpace: 'pre-line' }}><Hi>{renderMessageWithEmbeddedLinks(result, formattedMessage)}</Hi></span> || ''}
<Snippet ploc={result.locations?.[0]?.physicalLocation} />
</>
case 'Rule':
return <>
{tryLink(() => rule.helpUri, <Hi>{rule.id || rule.guid}</Hi>)}
{tryOr(() => rule.name && <>: <Hi>{rule.name}</Hi></>)}
</>
case 'Baseline':
return <Hi>{result.baselineState && capitalize(result.baselineState) || 'New'}</Hi>
case 'Bug':
return tryOr(() => <Link href={result.workItemUris[0]} target="_blank">
<Icon iconName="LadybugSolid" size={IconSize.medium} style={{ color: '#E81123' }} />
</Link>)
case 'Age':
return <Hi>{result.sla}</Hi>
case 'FirstObserved':
return <Hi>{result.firstDetection.toLocaleDateString()}</Hi>
}
})(),
className: css(treeColumn.className, 'font-size'),
columnIndex,
})
}
// ROW MORE
const isMore = (item => item.onClick !== undefined) as (item: any) => item is More
if (isMore(data)) {
return columnIndex === 0
? ExpandableTreeCell({
children: <Link onClick={data.onClick} tabIndex={-1}>Show All</Link>,
colspan,
...commonProps
})
: null
}
return null
}
// Replace [text](relatedIndex) with <a href />
function renderMessageWithEmbeddedLinks(result: Result, message: string) {
const rxLink = /\[([^\]]*)\]\(([^\)]+)\)/ // Matches [text](id). Similar to below, but with an extra grouping around the id part.
return message.match(rxLink)
? message
.split(/(\[[^\]]*\]\([^\)]+\))/g)
.map((item, i) => {
if (i % 2 === 0) return item
const [_, text, id] = item.match(rxLink)
const href = (() => {
if (isNaN(id as any)) return id // `id` is a URI string
// Else `id` is a number
// TODO: search other location collections
// RelatedLocations is typically [{ id: 1, ...}, { id: 2, ...}]
const physicalLocation = result.relatedLocations?.find(location => location.id === +id)?.physicalLocation
return getRepoUri(physicalLocation?.artifactLocation?.uri, result.run, physicalLocation?.region)
})()
return href
? <a key={i} href={href} target="_blank">{text}</a>
: text
})
: message
}
// Borrowed from sarif-vscode-extension.
function format(template: string | undefined, args?: string[]) {
if (!template) return undefined;
if (!args) return template;
return template.replace(/{(\d+)}/g, (_, group) => args[group]);
}