diff --git a/.changeset/stupid-eagles-shave.md b/.changeset/stupid-eagles-shave.md new file mode 100644 index 0000000000..9bd88290ee --- /dev/null +++ b/.changeset/stupid-eagles-shave.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-code-coverage': patch +--- + +Updated layout to improve contrasts and consistency with other plugins diff --git a/plugins/code-coverage/dev/__fixtures__/coverage-for-entity.json b/plugins/code-coverage/dev/__fixtures__/coverage-for-entity.json new file mode 100644 index 0000000000..c2f4725818 --- /dev/null +++ b/plugins/code-coverage/dev/__fixtures__/coverage-for-entity.json @@ -0,0 +1,174 @@ +{ + "metadata": { + "vcs": { + "type": "github", + "location": "https://github.com/backstage/backstage/tree/main/" + }, + "generationTime": 1694413579498 + }, + "entity": { + "name": "backstage", + "namespace": "default", + "kind": "Component" + }, + "files": [ + { + "filename": "src/index.js", + "lineHits": { + "1": 1, + "2": 1, + "3": 1, + "4": 13, + "5": 13, + "6": 3, + "7": 13, + "8": 3, + "9": 13, + "10": 3, + "11": 13, + "12": 3, + "13": 13, + "14": 1, + "15": 13, + "16": 13 + }, + "branchHits": { + "3": { + "covered": 1, + "available": 1, + "missed": 0 + }, + "5": { + "covered": 1, + "available": 1, + "missed": 0 + }, + "7": { + "covered": 1, + "available": 1, + "missed": 0 + }, + "9": { + "covered": 1, + "available": 1, + "missed": 0 + }, + "11": { + "covered": 1, + "available": 1, + "missed": 0 + }, + "13": { + "covered": 1, + "available": 1, + "missed": 0 + } + } + }, + { + "filename": "src/math.js", + "lineHits": { + "1": 1, + "2": 3, + "3": 2, + "4": 2, + "5": 1, + "6": 1, + "7": 1, + "8": 1, + "9": 3, + "10": 2, + "11": 2, + "12": 1, + "13": 1, + "14": 1, + "15": 1, + "16": 3, + "17": 2, + "18": 2, + "19": 1, + "20": 1, + "21": 1, + "22": 1, + "23": 3, + "24": 2, + "25": 3, + "26": 0, + "27": 0, + "28": 1, + "29": 1 + }, + "branchHits": { + "2": { + "covered": 3, + "available": 3, + "missed": 0 + }, + "5": { + "covered": 1, + "available": 1, + "missed": 0 + }, + "8": { + "covered": 1, + "available": 1, + "missed": 0 + }, + "9": { + "covered": 2, + "available": 2, + "missed": 0 + }, + "12": { + "covered": 1, + "available": 1, + "missed": 0 + }, + "15": { + "covered": 1, + "available": 1, + "missed": 0 + }, + "16": { + "covered": 2, + "available": 2, + "missed": 0 + }, + "19": { + "covered": 1, + "available": 1, + "missed": 0 + }, + "22": { + "covered": 1, + "available": 1, + "missed": 0 + }, + "23": { + "covered": 2, + "available": 2, + "missed": 0 + }, + "25": { + "covered": 1, + "available": 2, + "missed": 1 + } + } + } + ], + "aggregate": { + "line": { + "available": 45, + "covered": 43, + "missed": 2, + "percentage": 95.56 + }, + "branch": { + "available": 23, + "covered": 22, + "missed": 1, + "percentage": 95.65 + } + } +} diff --git a/plugins/code-coverage/dev/__fixtures__/coverage-history-for-entity.json b/plugins/code-coverage/dev/__fixtures__/coverage-history-for-entity.json new file mode 100644 index 0000000000..26b96875e5 --- /dev/null +++ b/plugins/code-coverage/dev/__fixtures__/coverage-history-for-entity.json @@ -0,0 +1,84 @@ +{ + "entity": { + "name": "backstage", + "kind": "component", + "namespace": "default" + }, + "history": [ + { + "timestamp": 1694413579498, + "branch": { + "available": 23, + "covered": 22, + "missed": 1, + "percentage": 95.65 + }, + "line": { + "available": 45, + "covered": 43, + "missed": 2, + "percentage": 95.56 + } + }, + { + "timestamp": 1694178927072, + "branch": { + "available": 23, + "covered": 22, + "missed": 1, + "percentage": 95.65 + }, + "line": { + "available": 45, + "covered": 43, + "missed": 2, + "percentage": 95.56 + } + }, + { + "timestamp": 1694178830777, + "branch": { + "available": 19, + "covered": 18, + "missed": 1, + "percentage": 94.74 + }, + "line": { + "available": 45, + "covered": 43, + "missed": 2, + "percentage": 95.56 + } + }, + { + "timestamp": 1694178708402, + "branch": { + "available": 15, + "covered": 10, + "missed": 5, + "percentage": 66.67 + }, + "line": { + "available": 45, + "covered": 36, + "missed": 9, + "percentage": 80 + } + }, + { + "timestamp": 1694178270486, + "branch": { + "available": 10, + "covered": 5, + "missed": 5, + "percentage": 50 + }, + "line": { + "available": 45, + "covered": 26, + "missed": 19, + "percentage": 57.78 + } + } + ] +} diff --git a/plugins/code-coverage/dev/__fixtures__/file-content-index-js.txt b/plugins/code-coverage/dev/__fixtures__/file-content-index-js.txt new file mode 100644 index 0000000000..34df5226d5 --- /dev/null +++ b/plugins/code-coverage/dev/__fixtures__/file-content-index-js.txt @@ -0,0 +1,16 @@ +import { add, multiply, subtract, divide } from "./math"; + +export default (a, b, operator) => { + switch(operator) { + case '+': + return add(a, b); + case '-': + return subtract(a, b); + case '*': + return multiply(a, b); + case '/': + return divide(a, b); + default: + throw new Error('Invalid operator'); + } +}; diff --git a/plugins/code-coverage/dev/__fixtures__/file-content-math-js.txt b/plugins/code-coverage/dev/__fixtures__/file-content-math-js.txt new file mode 100644 index 0000000000..05f43438f5 --- /dev/null +++ b/plugins/code-coverage/dev/__fixtures__/file-content-math-js.txt @@ -0,0 +1,29 @@ +export function add(a, b) { + if (isNaN(a) || isNaN(b)) { + throw new Error('Invalid input'); + } + return a + b; +} + +export function subtract(a, b) { + if (isNaN(a) || isNaN(b)) { + throw new Error('Invalid input'); + } + return a - b; +} + +export function multiply(a, b) { + if (isNaN(a) || isNaN(b)) { + throw new Error('Invalid input'); + } + return a * b; +} + +export function divide(a, b) { + if (isNaN(a) || isNaN(b)) { + throw new Error('Invalid input'); + } else if (b === 0) { + throw new Error('Division by zero'); + } + return a / b; +} diff --git a/plugins/code-coverage/dev/__fixtures__/get-file-content-from-entity.ts b/plugins/code-coverage/dev/__fixtures__/get-file-content-from-entity.ts new file mode 100644 index 0000000000..9fea0997f7 --- /dev/null +++ b/plugins/code-coverage/dev/__fixtures__/get-file-content-from-entity.ts @@ -0,0 +1,64 @@ +/* + * Copyright 2023 The Backstage Authors + * + * 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. + */ +export default { + 'src/index.js': `import { add, multiply, subtract, divide } from \"./math\"; + +export default (a, b, operator) => { + switch(operator) { + case '+': + return add(a, b); + case '-': + return subtract(a, b); + case '*': + return multiply(a, b); + case '/': + return divide(a, b); + default: + throw new Error('Invalid operator'); + } +}; + `, + 'src/math.js': `export function add(a, b) { + if (isNaN(a) || isNaN(b)) { + throw new Error('Invalid input'); + } + return a + b; +} + +export function subtract(a, b) { + if (isNaN(a) || isNaN(b)) { + throw new Error('Invalid input'); + } + return a - b; +} + +export function multiply(a, b) { + if (isNaN(a) || isNaN(b)) { + throw new Error('Invalid input'); + } + return a * b; +} + +export function divide(a, b) { + if (isNaN(a) || isNaN(b)) { + throw new Error('Invalid input'); + } else if (b === 0) { + throw new Error('Division by zero'); + } + return a / b; +} + `, +}; diff --git a/plugins/code-coverage/dev/index.tsx b/plugins/code-coverage/dev/index.tsx index f364c2a0d1..e0c1ef7616 100644 --- a/plugins/code-coverage/dev/index.tsx +++ b/plugins/code-coverage/dev/index.tsx @@ -16,11 +16,65 @@ import React from 'react'; import { createDevApp } from '@backstage/dev-utils'; import { codeCoveragePlugin, EntityCodeCoverageContent } from '../src/plugin'; +import { CompoundEntityRef, Entity } from '@backstage/catalog-model'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; +import { codeCoverageApiRef, CodeCoverageApi } from '../src/api'; +import coverageForEntity from './__fixtures__/coverage-for-entity.json'; +import coverageHistoryForEntity from './__fixtures__/coverage-history-for-entity.json'; +import fileContentFromEntity from './__fixtures__/get-file-content-from-entity'; + +const mockEntity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'backstage', + description: 'backstage.io', + annotations: { + 'backstage.io/code-coverage': 'enabled', + }, + }, + spec: { + lifecycle: 'production', + type: 'website', + owner: 'user:guest', + }, +}; + +const mockCodeCoverageApi: CodeCoverageApi = { + async getCoverageForEntity(_entity: CompoundEntityRef) { + return coverageForEntity as any; + }, + async getFileContentFromEntity(_entity: CompoundEntityRef, filePath: string) { + switch (filePath) { + case 'src/index.js': + return fileContentFromEntity['src/index.js']; + case 'src/math.js': + return fileContentFromEntity['src/math.js']; + default: + return ''; + } + }, + async getCoverageHistoryForEntity( + _entity: CompoundEntityRef, + _limit?: number, + ) { + return coverageHistoryForEntity; + }, +}; createDevApp() + .registerApi({ + api: codeCoverageApiRef, + deps: {}, + factory: () => mockCodeCoverageApi, + }) .registerPlugin(codeCoveragePlugin) .addPage({ - element: , + element: ( + + + + ), title: 'Root Page', }) .render(); diff --git a/plugins/code-coverage/src/components/FileExplorer/CodeRow.tsx b/plugins/code-coverage/src/components/FileExplorer/CodeRow.tsx index c7dee55f2b..20f0548f16 100644 --- a/plugins/code-coverage/src/components/FileExplorer/CodeRow.tsx +++ b/plugins/code-coverage/src/components/FileExplorer/CodeRow.tsx @@ -41,9 +41,11 @@ const useStyles = makeStyles(theme => ({ }, hitCountRoundedRectangle: { backgroundColor: `${theme.palette.success.main}`, + color: `${theme.palette.success.contrastText}`, }, notHitCountRoundedRectangle: { backgroundColor: `${theme.palette.error.main}`, + color: `${theme.palette.error.contrastText}`, }, codeLine: { paddingLeft: `${theme.spacing(1)}`, @@ -52,9 +54,11 @@ const useStyles = makeStyles(theme => ({ }, hitCodeLine: { backgroundColor: `${theme.palette.success.main}`, + color: `${theme.palette.success.contrastText}`, }, notHitCodeLine: { backgroundColor: `${theme.palette.error.main}`, + color: `${theme.palette.error.contrastText}`, }, })); diff --git a/plugins/code-coverage/src/components/FileExplorer/FileExplorer.tsx b/plugins/code-coverage/src/components/FileExplorer/FileExplorer.tsx index 3d41a1ff64..4017ba8264 100644 --- a/plugins/code-coverage/src/components/FileExplorer/FileExplorer.tsx +++ b/plugins/code-coverage/src/components/FileExplorer/FileExplorer.tsx @@ -15,15 +15,9 @@ */ import { useEntity } from '@backstage/plugin-catalog-react'; -import { - Box, - Card, - CardContent, - CardHeader, - Modal, - Tooltip, -} from '@material-ui/core'; -import DescriptionIcon from '@material-ui/icons/Description'; +import { Box, Modal, makeStyles } from '@material-ui/core'; +import FolderIcon from '@material-ui/icons/Folder'; +import FileOutlinedIcon from '@material-ui/icons/InsertDriveFileOutlined'; import { Alert } from '@material-ui/lab'; import React, { Fragment, useEffect, useState } from 'react'; import useAsync from 'react-use/lib/useAsync'; @@ -38,6 +32,19 @@ import { } from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; +const useStyles = makeStyles(theme => ({ + container: { + marginTop: theme.spacing(2), + }, + icon: { + marginRight: theme.spacing(1), + }, + link: { + color: theme.palette.primary.main, + cursor: 'pointer', + }, +})); + type FileStructureObject = Record; type CoverageTableRow = { @@ -143,6 +150,7 @@ export const getObjectsAtPath = ( }; export const FileExplorer = () => { + const styles = useStyles(); const { entity } = useEntity(); const [curData, setCurData] = useState(); const [tableData, setTableData] = useState(); @@ -188,6 +196,7 @@ export const FileExplorer = () => { const moveUpIntoPath = (idx: number) => { const path = curPath.split('/').slice(0, idx + 1); + setCurFile(''); setCurPath(path.join('/')); setTableData(getObjectsAtPath(curData, path.slice(1))); }; @@ -197,43 +206,32 @@ export const FileExplorer = () => { title: 'Path', type: 'string', field: 'path', - render: (row: CoverageTableRow) => { - if (row.files?.length) { - return ( -
{ - setCurPath(`${curPath}/${row.path}`); - moveDownIntoPath(row.path); - }} - onClick={() => { - setCurPath(`${curPath}/${row.path}`); - moveDownIntoPath(row.path); - }} - > - {row.path} -
- ); - } - - return ( - - {row.path} - - { - setCurFile(`${curPath.slice(1)}/${row.path}`); - setModalOpen(true); - }} - /> - - - ); - }, + render: (row: CoverageTableRow) => ( + { + if (row.files?.length) { + setCurPath(`${curPath}/${row.path}`); + moveDownIntoPath(row.path); + } else { + setCurFile(`${curPath.slice(1)}/${row.path}`); + setModalOpen(true); + } + }} + > + {row.files?.length > 0 && ( + + )} + {row.files?.length === 0 && ( + + )} + {row.path} + + ), }, { title: 'Coverage', @@ -264,42 +262,50 @@ export const FileExplorer = () => { } return ( - - - - - {pathArray.map((pathElement, idx) => ( - -
moveUpIntoPath(idx)} - onClick={() => moveUpIntoPath(idx)} - > - {pathElement || 'root'} -
-
{'\u00A0/\u00A0'}
-
- ))} -
- No files found} - data={tableData || []} - columns={columns} - /> - event.stopPropagation()} - onClose={() => setModalOpen(false)} - style={{ overflow: 'scroll' }} - > - - - - + +
No files found} + data={tableData || []} + columns={columns} + title={ + <> + Explore Files + + {pathArray.map((pathElement, idx) => ( + +
moveUpIntoPath(idx)} + onClick={() => moveUpIntoPath(idx)} + > + {pathElement || 'root'} +
+ {idx !== lastPathElementIndex &&
{'\u00A0/\u00A0'}
} +
+ ))} +
+ + } + /> + event.stopPropagation()} + onClose={() => setModalOpen(false)} + style={{ overflow: 'scroll' }} + > + + + ); }; diff --git a/plugins/code-coverage/src/components/FileExplorer/Highlighter.ts b/plugins/code-coverage/src/components/FileExplorer/Highlighter.ts index d97d147da3..465e43f6e4 100644 --- a/plugins/code-coverage/src/components/FileExplorer/Highlighter.ts +++ b/plugins/code-coverage/src/components/FileExplorer/Highlighter.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import 'highlight.js/styles/atom-one-dark.css'; -import highlight from 'highlight.js'; +import 'highlight.js/styles/mono-blue.css'; +import { highlight } from 'highlight.js'; /* * Given a file extension, repo name, and array of code lines, return a Promise resolving @@ -30,7 +30,6 @@ import highlight from 'highlight.js'; */ export const highlightLines = (fileExtension: string, lines: Array) => { const formattedLines: Array = []; - let state: CompiledMode | Language | undefined; let fileformat = fileExtension; if (fileExtension === 'm') { fileformat = 'objectivec'; @@ -46,8 +45,10 @@ export const highlightLines = (fileExtension: string, lines: Array) => { } lines.forEach(line => { - const result = highlight.highlight(fileformat, line, true, state); - state = result.top; + const result = highlight(line, { + language: fileformat, + ignoreIllegals: true, + }); formattedLines.push(result.value); }); return formattedLines;