@finos/legend-application-pure-ide
Version:
Legend Pure IDE application core
334 lines • 23.4 kB
JavaScript
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