/** * Copyright (c) Microsoft Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import * as React from 'react'; import './attachmentsTab.css'; import { ImageDiffView } from '@web/shared/imageDiffView'; import type { MultiTraceModel } from './modelUtil'; import { PlaceholderPanel } from './placeholderPanel'; import type { AfterActionTraceEventAttachment } from '@trace/trace'; import { CodeMirrorWrapper, lineHeight } from '@web/components/codeMirrorWrapper'; import { isTextualMimeType } from '@isomorphic/mimeType'; import { Expandable } from '@web/components/expandable'; import { linkifyText } from '@web/renderUtils'; type Attachment = AfterActionTraceEventAttachment & { traceUrl: string }; type ExpandableAttachmentProps = { attachment: Attachment; }; const ExpandableAttachment: React.FunctionComponent = ({ attachment }) => { const [expanded, setExpanded] = React.useState(false); const [attachmentText, setAttachmentText] = React.useState(null); const [placeholder, setPlaceholder] = React.useState(null); const isTextAttachment = isTextualMimeType(attachment.contentType); const hasContent = !!attachment.sha1 || !!attachment.path; React.useEffect(() => { if (expanded && attachmentText === null && placeholder === null) { setPlaceholder('Loading ...'); fetch(attachmentURL(attachment)).then(response => response.text()).then(text => { setAttachmentText(text); setPlaceholder(null); }).catch(e => { setPlaceholder('Failed to load: ' + e.message); }); } }, [expanded, attachmentText, placeholder, attachment]); const snippetHeight = React.useMemo(() => { const lineCount = attachmentText ? attachmentText.split('\n').length : 0; return Math.min(Math.max(5, lineCount), 20) * lineHeight; }, [attachmentText]); const title = {linkifyText(attachment.name)} {hasContent && download} ; if (!isTextAttachment || !hasContent) return
{title}
; return <> {placeholder && {placeholder}} {expanded && attachmentText !== null &&
} ; }; export const AttachmentsTab: React.FunctionComponent<{ model: MultiTraceModel | undefined, }> = ({ model }) => { const { diffMap, screenshots, attachments } = React.useMemo(() => { const attachments = new Set(); const screenshots = new Set(); for (const action of model?.actions || []) { const traceUrl = action.context.traceUrl; for (const attachment of action.attachments || []) attachments.add({ ...attachment, traceUrl }); } const diffMap = new Map(); for (const attachment of attachments) { if (!attachment.path && !attachment.sha1) continue; const match = attachment.name.match(/^(.*)-(expected|actual|diff)\.png$/); if (match) { const name = match[1]; const type = match[2] as 'expected' | 'actual' | 'diff'; const entry = diffMap.get(name) || { expected: undefined, actual: undefined, diff: undefined }; entry[type] = attachment; diffMap.set(name, entry); attachments.delete(attachment); } else if (attachment.contentType.startsWith('image/')) { screenshots.add(attachment); attachments.delete(attachment); } } return { diffMap, attachments, screenshots }; }, [model]); if (!diffMap.size && !screenshots.size && !attachments.size) return ; return
{[...diffMap.values()].map(({ expected, actual, diff }) => { return <> {expected && actual &&
Image diff
} {expected && actual && } ; })} {screenshots.size ?
Screenshots
: undefined} {[...screenshots.values()].map((a, i) => { const url = attachmentURL(a); return ; })} {attachments.size ?
Attachments
: undefined} {[...attachments.values()].map((a, i) => { return
; })}
; }; function attachmentURL(attachment: Attachment, queryParams: Record = {}) { const params = new URLSearchParams(queryParams); if (attachment.sha1) { params.set('trace', attachment.traceUrl); return 'sha1/' + attachment.sha1 + '?' + params.toString(); } params.set('path', attachment.path!); return 'file?' + params.toString(); } function downloadURL(attachment: Attachment) { const params = { dn: attachment.name } as Record; if (attachment.contentType) params.dct = attachment.contentType; return attachmentURL(attachment, params); } function attachmentKey(attachment: Attachment, index: number) { return index + '-' + (attachment.sha1 ? `sha1-` + attachment.sha1 : `path-` + attachment.path); }