UNPKG

@finos/legend-application-pure-ide

Version:
334 lines 23.4 kB
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; /** * Copyright (c) 2020-present, Goldman Sachs * * 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 { observer } from 'mobx-react-lite'; import { getTestTreeNodeStatus, TestResultType, TestSuiteStatus, } from '../../stores/TestRunnerState.js'; import { TestFailureResult, TestSuccessResult, } from '../../server/models/Test.js'; import { flowResult } from 'mobx'; import { clsx, ResizablePanel, ResizablePanelGroup, ResizablePanelSplitter, BlankPanelContent, PanelLoadingIndicator, TreeView, ProgressBar, QuestionCircleIcon, CheckCircleIcon, ExclamationCircleIcon, TimesCircleIcon, CircleNotchIcon, ChevronDownIcon, ChevronRightIcon, ExpandIcon, CompressIcon, BanIcon, PlayIcon, PlusIcon, ResizablePanelSplitterLine, GoToFileIcon, SubjectIcon, ViewHeadlineIcon, TimesIcon, WordWrapIcon, Panel, PanelContent, PanelHeader, PanelHeaderActions, PanelHeaderActionItem, } from '@finos/legend-art'; import { guaranteeNonNullable, isNonNullable, noop, } from '@finos/legend-shared'; import { useApplicationStore } from '@finos/legend-application'; import { usePureIDEStore } from '../PureIDEStoreProvider.js'; import { FileCoordinate } from '../../server/models/File.js'; import { ELEMENT_PATH_DELIMITER } from '@finos/legend-graph'; import { useEffect, useRef, useState } from 'react'; import { editor as monacoEditorAPI, languages as monacoLanguagesAPI, } from 'monaco-editor'; import { CODE_EDITOR_LANGUAGE, CODE_EDITOR_THEME, getBaseCodeEditorOptions, } from '@finos/legend-code-editor'; import { disposeCodeEditor } from '@finos/legend-lego/code-editor'; const TestTreeNodeContainer = observer((props) => { const { node, level, stepPaddingInRem, onNodeSelect, innerProps } = props; const { testRunnerState, onNodeOpen, onNodeExpand, onNodeCompress, renderNodeLabel, } = innerProps; const ideStore = usePureIDEStore(); const applicationStore = useApplicationStore(); const testResultInfo = testRunnerState.testResultInfo; const isExpandable = !node.data.type; // NOTE: the quirky thing here is since we make the node container an `observer`, effectively, we wrap `memo` // around this component, so since we use `isSelected = node.isSelected`, changing selection will not trigger // a re-render, hence, we have to make it observes the currently selected node to derive its `isSelected` state const isSelected = node.id === testRunnerState.selectedTestId; const nodeTestStatus = testResultInfo ? getTestTreeNodeStatus(node, testResultInfo) : undefined; let nodeIcon; switch (nodeTestStatus) { case TestResultType.PASSED: { nodeIcon = (_jsx("div", { className: "test-runner-panel__explorer__package-tree__status test-runner-panel__explorer__package-tree__status--passed", children: _jsx(CheckCircleIcon, {}) })); break; } case TestResultType.FAILED: { nodeIcon = (_jsx("div", { className: "test-runner-panel__explorer__package-tree__status test-runner-panel__explorer__package-tree__status--failed", children: _jsx(ExclamationCircleIcon, {}) })); break; } case TestResultType.ERROR: { nodeIcon = (_jsx("div", { className: "test-runner-panel__explorer__package-tree__status test-runner-panel__explorer__package-tree__status--error", children: _jsx(TimesCircleIcon, {}) })); break; } case TestResultType.RUNNING: { nodeIcon = (_jsx("div", { className: "test-runner-panel__explorer__package-tree__status test-runner-panel__explorer__package-tree__status--running", children: _jsx(CircleNotchIcon, {}) })); break; } default: { nodeIcon = _jsx(QuestionCircleIcon, {}); break; } } const toggleExpansion = () => { if (node.isOpen) { onNodeCompress(node); } else { onNodeExpand(node); } }; const selectNode = (event) => { event.stopPropagation(); event.preventDefault(); onNodeSelect?.(node); if (isExpandable) { toggleExpansion(); } else { onNodeOpen(node); } }; const onDoubleClick = () => { if (isExpandable) { toggleExpansion(); } else { flowResult(ideStore.loadFile(node.data.li_attr.file, new FileCoordinate(node.data.li_attr.file, Number.parseInt(node.data.li_attr.line, 10), Number.parseInt(node.data.li_attr.column, 10)))).catch(applicationStore.alertUnhandledError); } }; return (_jsxs("div", { className: clsx('tree-view__node__container explorer__package-tree__node__container', { 'explorer__package-tree__node__container--selected': isSelected }), onClick: selectNode, onDoubleClick: onDoubleClick, style: { paddingLeft: `${level * (stepPaddingInRem ?? 1)}rem`, display: 'flex', }, children: [_jsxs("div", { className: "tree-view__node__icon explorer__package-tree__node__icon", children: [_jsx("div", { className: "explorer__package-tree__node__icon__expand", onClick: (event) => { event.stopPropagation(); toggleExpansion(); }, children: !isExpandable ? (_jsx("div", {})) : node.isOpen ? (_jsx(ChevronDownIcon, {})) : (_jsx(ChevronRightIcon, {})) }), _jsx("div", { className: "explorer__package-tree__node__icon__type", children: nodeIcon })] }), _jsx("button", { className: "tree-view__node__label explorer__package-tree__node__label", tabIndex: -1, children: renderNodeLabel?.(node) ?? node.label })] })); }); const TestRunnerList = observer((props) => { const { testRunnerState } = props; const treeData = testRunnerState.getTreeData(); const onNodeOpen = (node) => testRunnerState.setSelectedTestId(node.id); const renderNodeLabel = (node) => { let path = node.id.split('__')[0]; if (!path) { return node.label; } const parts = path.split('_'); path = parts.slice(1, parts.length).join(ELEMENT_PATH_DELIMITER); return (_jsxs("div", { className: "test-runner-list__item__label", children: [_jsx("div", { className: "test-runner-list__item__label__name", children: node.label }), _jsx("div", { className: "test-runner-list__item__label__path", children: path })] })); }; return (_jsx("div", { className: "explorer__content", children: Array.from(testRunnerState.allTests.keys()) .map((id) => treeData.nodes.get(id)) .filter(isNonNullable) .map((node) => (_jsx(TestTreeNodeContainer, { node: node, level: 0, onNodeSelect: noop(), innerProps: { testRunnerState, onNodeOpen, renderNodeLabel, onNodeExpand: noop(), onNodeCompress: noop(), } }, node.id))) })); }); const TestRunnerTree = observer((props) => { const { testRunnerState } = props; const treeData = testRunnerState.getTreeData(); const isEmptyTree = treeData.nodes.size === 0; const onNodeOpen = (node) => testRunnerState.setSelectedTestId(node.id); const onNodeExpand = (node) => { node.isOpen = true; testRunnerState.refreshTree(); }; const onNodeCompress = (node) => { node.isOpen = false; testRunnerState.refreshTree(); }; const getChildNodes = (node) => { if (node.isLoading || !node.childrenIds) { return []; } return node.childrenIds .map((childId) => treeData.nodes.get(childId)) .filter(isNonNullable); }; return (_jsxs("div", { className: "explorer__content", children: [isEmptyTree && _jsx(BlankPanelContent, { children: "No tests found" }), !isEmptyTree && (_jsx(TreeView, { components: { TreeNodeContainer: TestTreeNodeContainer, }, treeData: treeData, onNodeSelect: noop(), getChildNodes: getChildNodes, innerProps: { testRunnerState, onNodeOpen, onNodeExpand, onNodeCompress, } }))] })); }); // NOTE: we need the global match hence, including /g in the regexp const TEST_ERROR_LOCATION_PATTERN = /(?<path>resource:(?<path_sourceId>\/?(?:\w+\/)*\w+(?:\.\w+)*) (?:line:(?<path_line>\d+)) (?:column:(?<path_column>\d+)))/g; const TestResultConsole = (props) => { const { wrapText, result } = props; const ideStore = usePureIDEStore(); const applicationStore = useApplicationStore(); const [editor, setEditor] = useState(); const locationLinkProviderDisposer = useRef(undefined); const textInputRef = useRef(null); useEffect(() => { if (!editor && textInputRef.current) { const element = textInputRef.current; const newEditor = monacoEditorAPI.create(element, { ...getBaseCodeEditorOptions(), fontSize: 12, extraEditorClassName: 'monaco-editor--small-font', readOnly: true, glyphMargin: false, folding: false, lineNumbers: 'off', lineDecorationsWidth: 10, lineNumbersMinChars: 0, minimap: { enabled: false, }, guides: { bracketPairs: false, bracketPairsHorizontal: false, highlightActiveBracketPair: false, indentation: false, highlightActiveIndentation: false, }, renderLineHighlight: 'none', theme: CODE_EDITOR_THEME.DEFAULT_DARK, language: CODE_EDITOR_LANGUAGE.TEXT, }); setEditor(newEditor); } }, [applicationStore, editor]); if (editor) { locationLinkProviderDisposer.current?.dispose(); locationLinkProviderDisposer.current = monacoLanguagesAPI.registerLinkProvider(CODE_EDITOR_LANGUAGE.TEXT, { provideLinks: (model) => { const links = []; for (let i = 1; i <= model.getLineCount(); ++i) { Array.from(model.getLineContent(i).matchAll(TEST_ERROR_LOCATION_PATTERN)).forEach((match) => { if (match.groups?.path && match.groups.path_sourceId && match.groups.path_column && match.groups.path_line) { links.push({ range: { startLineNumber: i, startColumn: match.index + 1, endLineNumber: i, endColumn: match.index + 1 + match.groups.path.length, }, tooltip: 'Click to go to location', sourceId: match.groups.path_sourceId, line: match.groups.path_line, column: match.groups.path_column, }); } }); } return { links, }; }, // NOTE: this is a hacky way to customize the behavior of clicking on a link // there is no good solution right now to intercept this cleanly and prevent the default behavior // this will produce a warning in the console since link resolved is not navigatable by monaco-editor resolveLink: (link) => { const locationLink = link; flowResult(ideStore.loadFile(locationLink.sourceId, new FileCoordinate(locationLink.sourceId, Number.parseInt(locationLink.line, 10), Number.parseInt(locationLink.column, 10)))).catch(ideStore.applicationStore.alertUnhandledError); return undefined; }, }); } useEffect(() => { if (editor) { const value = result instanceof TestSuccessResult ? 'Test passed!' : result instanceof TestFailureResult ? result.error.text : 'Running...'; editor.setValue(value); // color text based on test result/status if (result instanceof TestSuccessResult || result instanceof TestFailureResult) { editor.createDecorationsCollection([ { range: { startLineNumber: 1, startColumn: 1, endLineNumber: Number.MAX_SAFE_INTEGER, endColumn: Number.MAX_SAFE_INTEGER, }, options: { inlineClassName: result instanceof TestSuccessResult ? 'test-runner-panel__result__content--success' : 'test-runner-panel__result__content--failure', }, }, ]); } } }, [editor, result]); useEffect(() => { if (editor) { editor.updateOptions({ wordWrap: wrapText ? 'on' : 'off', }); } }, [editor, wrapText]); // dispose editor useEffect(() => () => { if (editor) { disposeCodeEditor(editor); locationLinkProviderDisposer.current?.dispose(); } }, [editor]); return (_jsx("div", { className: "code-editor__container", children: _jsx("div", { className: "code-editor__body", ref: textInputRef }) })); }; const TestResultViewer = observer((props) => { const { testRunnerState, selectedTestId, testResultInfo } = props; const ideStore = usePureIDEStore(); const applicationStore = useApplicationStore(); const [wrapText, setWrapText] = useState(false); const result = testResultInfo.results.get(selectedTestId); const testInfo = guaranteeNonNullable(testRunnerState.allTests.get(selectedTestId), `Can't find info for test with ID '${selectedTestId}'`); const goToFile = () => { flowResult(ideStore.loadFile(testInfo.li_attr.file, new FileCoordinate(testInfo.li_attr.file, Number.parseInt(testInfo.li_attr.line, 10), Number.parseInt(testInfo.li_attr.column, 10)))).catch(applicationStore.alertUnhandledError); }; return (_jsxs(Panel, { children: [_jsx(PanelHeader, { title: testInfo.text, children: _jsxs(PanelHeaderActions, { children: [_jsx(PanelHeaderActionItem, { className: clsx({ 'panel__header__action--active': wrapText, }), onClick: () => setWrapText(!wrapText), title: "Toggle Text Wrap", children: _jsx(WordWrapIcon, { className: "test-runner-panel__result__header__icon--text-wrap" }) }), _jsx(PanelHeaderActionItem, { title: "Open File", onClick: goToFile, children: _jsx(GoToFileIcon, {}) })] }) }), _jsx(PanelContent, { className: "test-runner-panel__result", children: _jsx(TestResultConsole, { result: result, wrapText: wrapText }) })] })); }); const TestRunnerResultDisplay = observer((props) => { const { testRunnerState } = props; const ideStore = usePureIDEStore(); const applicationStore = useApplicationStore(); const numberOfTests = testRunnerState.testExecutionResult.count; const pctAdapter = ideStore.PCTAdapters.find((adapter) => adapter.func === testRunnerState.testExecutionResult.pctAdapter); const testResultInfo = testRunnerState.testResultInfo; const overallResult = testResultInfo?.suiteStatus ?? TestSuiteStatus.NONE; const runPercentage = testResultInfo?.runPercentage ?? 0; const collapseTree = () => testRunnerState.collapseTree(); const expandTree = () => testRunnerState.expandTree(); const runSuite = () => { flowResult(testRunnerState.rerunTestSuite()).catch(applicationStore.alertUnhandledError); }; const cancelTestRun = () => { flowResult(testRunnerState.cancelTestRun()).catch(applicationStore.alertUnhandledError); }; const toggleViewMode = () => testRunnerState.setViewAsList(!testRunnerState.viewAsList); const removeTestResult = () => { flowResult(testRunnerState.cancelTestRun()) .catch(applicationStore.alertUnhandledError) .finally(() => { ideStore.setTestRunnerState(undefined); }); }; return (_jsx("div", { className: "test-runner-panel__content", children: _jsxs(ResizablePanelGroup, { orientation: "vertical", children: [_jsx(ResizablePanel, { minSize: 400, children: _jsxs("div", { className: "panel test-runner-panel__explorer", children: [_jsx(PanelLoadingIndicator, { isLoading: testRunnerState.treeBuildingState.isInProgress }), _jsxs("div", { className: "panel__header", children: [_jsx("div", { className: "panel__header__title", children: _jsxs("div", { className: "panel__header__title__content test-runner-panel__explorer__report", children: [_jsxs("div", { className: "test-runner-panel__explorer__report__overview", children: [_jsxs("div", { className: "test-runner-panel__explorer__report__overview__stat test-runner-panel__explorer__report__overview__stat--total", children: [numberOfTests, " total"] }), _jsxs("div", { className: "test-runner-panel__explorer__report__overview__stat test-runner-panel__explorer__report__overview__stat--passed", children: [testResultInfo?.passed ?? 0, " ", _jsx(CheckCircleIcon, {})] }), _jsxs("div", { className: "test-runner-panel__explorer__report__overview__stat test-runner-panel__explorer__report__overview__stat--failed", children: [testResultInfo?.failed ?? 0, " ", _jsx(ExclamationCircleIcon, {})] }), _jsxs("div", { className: "test-runner-panel__explorer__report__overview__stat test-runner-panel__explorer__report__overview__stat--error", children: [testResultInfo?.error ?? 0, " ", _jsx(TimesCircleIcon, {})] })] }), testResultInfo && (_jsxs("div", { className: "test-runner-panel__explorer__report__time", children: [testResultInfo.time, "ms"] })), pctAdapter !== undefined && (_jsx("div", { className: "test-runner-panel__explorer__report__pct", children: `PCT: ${pctAdapter.name}` }))] }) }), _jsxs("div", { className: "panel__header__actions", children: [_jsx("button", { className: "panel__header__action", onClick: toggleViewMode, title: testRunnerState.viewAsList ? 'View As Tree' : 'View As List', children: testRunnerState.viewAsList ? (_jsx(SubjectIcon, { className: "test-runner-panel__icon--tree-view" })) : (_jsx(ViewHeadlineIcon, { className: "test-runner-panel__icon--list-view" })) }), _jsx("button", { className: "panel__header__action", onClick: expandTree, title: "Expand All", children: _jsx(ExpandIcon, {}) }), _jsx("button", { className: "panel__header__action", onClick: collapseTree, title: "Collapse All", children: _jsx(CompressIcon, {}) }), _jsx("button", { className: "panel__header__action", tabIndex: -1, disabled: !ideStore.testRunState.isInProgress, onClick: cancelTestRun, title: "Stop", children: _jsx(BanIcon, {}) }), _jsx("button", { className: "panel__header__action", tabIndex: -1, onClick: runSuite, disabled: ideStore.testRunState.isInProgress, title: "Run Suite", children: _jsx(PlayIcon, {}) }), _jsx("button", { className: "panel__header__action", tabIndex: -1, onClick: removeTestResult, title: "Reset", children: _jsx(TimesIcon, {}) })] })] }), _jsx("div", { className: "test-runner-panel__header__status", children: _jsx(ProgressBar, { className: `test-runner-panel__progress-bar test-runner-panel__progress-bar--${overallResult.toLowerCase()}`, classes: { bar: `test-runner-panel__progress-bar__bar test-runner-panel__progress-bar__bar--${overallResult.toLowerCase()}`, }, variant: "determinate", value: runPercentage }) }), _jsx("div", { className: "panel__content", children: testRunnerState.treeData && (_jsxs(_Fragment, { children: [!testRunnerState.viewAsList && (_jsx(TestRunnerTree, { testRunnerState: testRunnerState })), testRunnerState.viewAsList && (_jsx(TestRunnerList, { testRunnerState: testRunnerState }))] })) })] }) }), _jsx(ResizablePanelSplitter, { children: _jsx(ResizablePanelSplitterLine, { color: ideStore.panelGroupDisplayState.isMaximized ? 'transparent' : 'var(--color-dark-grey-250)' }) }), _jsxs(ResizablePanel, { minSize: 400, children: [testRunnerState.selectedTestId && !testResultInfo && _jsx("div", {}), testRunnerState.selectedTestId && testResultInfo && (_jsx(TestResultViewer, { testRunnerState: testRunnerState, selectedTestId: testRunnerState.selectedTestId, testResultInfo: testResultInfo })), !testRunnerState.selectedTestId && (_jsxs("div", { className: "panel", children: [_jsx("div", { className: "panel__header" }), _jsx("div", { className: "panel__content", children: _jsx(BlankPanelContent, { children: "No test selected" }) })] })), _jsx("div", {})] })] }) })); }); export const TestRunnerPanel = observer(() => { const ideStore = usePureIDEStore(); const testRunnerState = ideStore.testRunnerState; return (_jsxs("div", { className: "test-runner-panel", children: [!testRunnerState && (_jsx(BlankPanelContent, { children: _jsx("div", { className: "panel-group__splash-screen", children: _jsxs("div", { className: "panel-group__splash-screen__content", children: [_jsxs("div", { className: "panel-group__splash-screen__content__item", children: [_jsx("div", { className: "panel-group__splash-screen__content__item__label", children: "Run full test suite" }), _jsx("div", { className: "panel-group__splash-screen__content__item__hot-keys", children: _jsx("div", { className: "hotkey__key", children: "F10" }) })] }), _jsxs("div", { className: "panel-group__splash-screen__content__item", children: [_jsx("div", { className: "panel-group__splash-screen__content__item__label", children: "Run relevant tests only" }), _jsxs("div", { className: "panel-group__splash-screen__content__item__hot-keys", children: [_jsx("div", { className: "hotkey__key", children: "Shift" }), _jsx("div", { className: "hotkey__plus", children: _jsx(PlusIcon, {}) }), _jsx("div", { className: "hotkey__key", children: "F10" })] })] })] }) }) })), testRunnerState && (_jsx(TestRunnerResultDisplay, { testRunnerState: testRunnerState }))] })); }); //# sourceMappingURL=TestRunnerPanel.js.map