diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 8d24b419d7..0d17d30461 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -62,6 +62,8 @@ "react-syntax-highlighter": "^15.4.3", "react-text-truncate": "^0.16.0", "react-use": "^17.2.4", + "react-virtualized-auto-sizer": "^1.0.6", + "react-window": "^1.8.6", "remark-gfm": "^2.0.0", "zen-observable": "^0.8.15" }, @@ -89,6 +91,8 @@ "@types/node": "^14.14.32", "@types/react-helmet": "^6.1.0", "@types/react-syntax-highlighter": "^13.5.2", + "@types/react-virtualized-auto-sizer": "^1.0.1", + "@types/react-window": "^1.8.5", "@types/zen-observable": "^0.8.0" }, "files": [ diff --git a/packages/core-components/src/components/LogViewer/LogViewer.stories.tsx b/packages/core-components/src/components/LogViewer/LogViewer.stories.tsx new file mode 100644 index 0000000000..b7a56e1a7b --- /dev/null +++ b/packages/core-components/src/components/LogViewer/LogViewer.stories.tsx @@ -0,0 +1,81 @@ +/* + * Copyright 2020 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. + */ + +import React from 'react'; +import { LogViewer } from './LogViewer'; + +export default { + title: 'Data Display/LogViewer', + component: LogViewer, +}; + +const exampleLog = `Starting up task with 3 steps +Beginning step Fetch Skeleton + Template +info: Fetching template content from remote URL {"timestamp":"2021-12-03T15:47:11.625Z"} +info: Listing files and directories in template {"timestamp":"2021-12-03T15:47:12.797Z"} +info: Processing 33 template files/directories with input values {"component_id":"srnthsrthntrhsn","description":"rnthsrtnhssrthnrsthn","destination":{"host":"github.com","owner":"rtshnsrtmhrstmh","repo":"srtmhsrtmhrsthms"},"owner":"rstnhrstnhsrthn","timestamp":"2021-12-03T15:47:12.801Z"} +info: Writing file .editorconfig to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.816Z"} +info: Writing file .eslintignore to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.818Z"} +info: Writing file .eslintrc.js to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.820Z"} +info: Writing directory .github/ to template output path. {"timestamp":"2021-12-03T15:47:12.823Z"} +info: Writing file .gitignore to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.824Z"} +info: Writing file README.md to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.827Z"} +info: Writing file babel.config.js to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.829Z"} +info: Writing file catalog-info.yaml to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.831Z"} +info: Writing directory docs/ to template output path. {"timestamp":"2021-12-03T15:47:12.834Z"} +info: Writing file jest.config.js to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.836Z"} +info: Writing file mkdocs.yml to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.838Z"} +info: Writing file next-env.d.ts to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.841Z"} +info: Writing file next.config.js to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.844Z"} +info: Writing file package.json to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.845Z"} +info: Writing file prettier.config.js to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.848Z"} +info: Writing directory public/ to template output path. {"timestamp":"2021-12-03T15:47:12.849Z"} +info: Writing directory src/ to template output path. {"timestamp":"2021-12-03T15:47:12.850Z"} +info: Writing file tsconfig.json to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.851Z"} +info: Writing directory .github/workflows/ to template output path. {"timestamp":"2021-12-03T15:47:12.853Z"} +info: Writing file docs/index.md to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.854Z"} +info: Writing directory public/static/ to template output path. {"timestamp":"2021-12-03T15:47:12.857Z"} +info: Writing directory src/__tests__/ to template output path. {"timestamp":"2021-12-03T15:47:12.858Z"} +info: Writing directory src/components/ to template output path. {"timestamp":"2021-12-03T15:47:12.858Z"} +info: Writing directory src/pages/ to template output path. {"timestamp":"2021-12-03T15:47:12.859Z"} +info: Copying file/directory .github/workflows/build.yml without processing. {"timestamp":"2021-12-03T15:47:12.859Z"} +info: Writing file .github/workflows/build.yml to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.860Z"} +info: Writing file public/static/fonts.css to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.861Z"} +info: Writing file src/components/Header.tsx to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.863Z"} +info: Writing file src/__tests__/index.test.tsx to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.865Z"} +info: Writing file src/pages/_app.tsx to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.868Z"} +info: Writing file src/pages/_document.tsx to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.871Z"} +info: Writing directory src/pages/api/ to template output path. {"timestamp":"2021-12-03T15:47:12.873Z"} +info: Writing file src/pages/index.tsx to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.874Z"} +info: Writing file src/pages/api/ping.ts to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.877Z"} +info: Template result written to /var/folders/k6/9s7hd6w17115xlgwnsp0wsbr0000gn/T/5c9f8584-fded-4741-b6ef-46d94ff2cbdb {"timestamp":"2021-12-03T15:47:12.878Z"} +Finished step Fetch Skeleton + Template +Beginning step Publish +HttpError: Not Found + at /Users/patriko/dev/backstage/node_modules/@octokit/request/dist-node/index.js:86:21 + at runMicrotasks () + at processTicksAndRejections (internal/process/task_queues.js:95:5) + at async Object.handler (webpack-internal:///../../plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts:156:20) + at async HandlebarsWorkflowRunner.execute (webpack-internal:///../../plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts:254:11) + at async TaskWorker.runOneTask (webpack-internal:///../../plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts:110:13) + at async eval (webpack-internal:///../../plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts:100:9) +Run completed with status: failed`; + +export const ExampleLogViewer = () => ( +
+ +
+); diff --git a/packages/core-components/src/components/LogViewer/LogViewer.tsx b/packages/core-components/src/components/LogViewer/LogViewer.tsx new file mode 100644 index 0000000000..21d42a1862 --- /dev/null +++ b/packages/core-components/src/components/LogViewer/LogViewer.tsx @@ -0,0 +1,196 @@ +/* + * Copyright 2021 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. + */ + +import { makeStyles } from '@material-ui/core/styles'; +import React, { useMemo } from 'react'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { FixedSizeList } from 'react-window'; +import { AnsiProcessor } from './AnsiProcessor'; +import startCase from 'lodash/startCase'; +import * as colors from '@material-ui/core/colors'; + +export interface LogViewerProps { + text: string; + noLineNumbers?: boolean; +} + +export type AnsiColor = + | 'black' + | 'red' + | 'green' + | 'yellow' + | 'blue' + | 'magenta' + | 'cyan' + | 'white' + | 'grey'; + +export interface ChunkModifiers { + foreground?: AnsiColor; + background?: AnsiColor; + bold?: boolean; + italic?: boolean; + underline?: boolean; +} + +const useStyles = makeStyles(theme => ({ + root: { + fontFamily: '"Monaco", monospace', + fontSize: theme.typography.fontSize, + background: theme.palette.background.paper, + }, + line: { + whiteSpace: 'pre', + }, + lineNumber: { + display: 'inline-block', + textAlign: 'end', + width: 60, + marginRight: theme.spacing(1), + }, + modifierBold: { + fontWeight: theme.typography.fontWeightBold, + }, + modifierItalic: { + fontStyle: 'italic', + }, + modifierUnderline: { + textDecoration: 'underline', + }, + modifierForegroundBlack: { + color: colors.common.black, + }, + modifierForegroundRed: { + color: colors.red[500], + }, + modifierForegroundGreen: { + color: colors.green[500], + }, + modifierForegroundYellow: { + color: colors.yellow[500], + }, + modifierForegroundBlue: { + color: colors.blue[500], + }, + modifierForegroundMagenta: { + color: colors.purple[500], + }, + modifierForegroundCyan: { + color: colors.cyan[500], + }, + modifierForegroundWhite: { + color: colors.common.white, + }, + modifierForegroundGrey: { + color: colors.grey[500], + }, + modifierBackgroundBlack: { + color: colors.common.black, + }, + modifierBackgroundRed: { + color: colors.red[500], + }, + modifierBackgroundGreen: { + color: colors.green[500], + }, + modifierBackgroundYellow: { + color: colors.yellow[500], + }, + modifierBackgroundBlue: { + color: colors.blue[500], + }, + modifierBackgroundMagenta: { + color: colors.purple[500], + }, + modifierBackgroundCyan: { + color: colors.cyan[500], + }, + modifierBackgroundWhite: { + color: colors.common.white, + }, + modifierBackgroundGrey: { + color: colors.grey[500], + }, +})); + +function getModifierClasses( + classes: ReturnType, + modifiers: ChunkModifiers, +) { + const classNames = new Array(); + if (modifiers.bold) { + classNames.push(classes.modifierBold); + } + if (modifiers.italic) { + classNames.push(classes.modifierItalic); + } + if (modifiers.underline) { + classNames.push(classes.modifierUnderline); + } + if (modifiers.foreground) { + const key = `modifierForeground${startCase( + modifiers.foreground, + )}` as keyof typeof classes; + classNames.push(classes[key]); + } + if (modifiers.background) { + const key = `modifierBackground${startCase( + modifiers.background, + )}` as keyof typeof classes; + classNames.push(classes[key]); + } + return classNames.join(' '); +} + +export function LogViewer(props: LogViewerProps) { + const { noLineNumbers } = props; + const classes = useStyles(); + + // The processor keeps state that optimizes appending to the text + const processor = useMemo(() => new AnsiProcessor(), []); + const lines = processor.process(props.text); + + return ( + + {({ height, width }) => ( + + {({ index, style, data }) => ( +
+ {!noLineNumbers && ( + {index + 1} + )} + {data[index].map(({ text, modifiers }, i) => ( + + {text} + + ))} +
+ )} +
+ )} +
+ ); +} diff --git a/packages/core-components/src/components/LogViewer/index.ts b/packages/core-components/src/components/LogViewer/index.ts new file mode 100644 index 0000000000..839f34f81e --- /dev/null +++ b/packages/core-components/src/components/LogViewer/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2021 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 { LogViewer } from './LogViewer'; +export type { LogViewerProps } from './LogViewer'; diff --git a/packages/core-components/src/components/index.ts b/packages/core-components/src/components/index.ts index 3c9376a894..473fab8444 100644 --- a/packages/core-components/src/components/index.ts +++ b/packages/core-components/src/components/index.ts @@ -30,6 +30,7 @@ export * from './HeaderIconLinkRow'; export * from './HorizontalScrollGrid'; export * from './Lifecycle'; export * from './Link'; +export * from './LogViewer'; export * from './MarkdownContent'; export * from './OAuthRequestDialog'; export * from './OverflowTooltip'; diff --git a/yarn.lock b/yarn.lock index 5c43e126d4..a71567ffe8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6974,13 +6974,6 @@ dependencies: "@types/node" "*" -"@types/ansi-regex@^5.0.0": - version "5.0.0" - resolved "https://registry.npmjs.org/@types/ansi-regex/-/ansi-regex-5.0.0.tgz#569a5189a92cc46d63fb2ad91e6b130f33d999c1" - integrity sha512-SQafVL3pXFh/5qq/nN6p5858g//zSVzcb8JzCLtoVxm8YNPggMQfEIm7aaTNysxpw1S+lFTaW8kv+aR0/CEhCA== - dependencies: - ansi-regex "*" - "@types/archiver@^5.1.0": version "5.3.0" resolved "https://registry.npmjs.org/@types/archiver/-/archiver-5.3.0.tgz#2b34ba56d4d7102d256b922c7e91e09eab79db6f" @@ -9242,11 +9235,6 @@ ansi-html@0.0.7, ansi-html@^0.0.7: resolved "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" integrity sha1-gTWEAhliqenm/QOflA0S9WynhZ4= -ansi-regex@*, ansi-regex@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" - integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== - ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" @@ -9267,6 +9255,11 @@ ansi-regex@^5.0.0, ansi-regex@^5.0.1: resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== +ansi-regex@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" + integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== + ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"