From 2803ac6a2d7892fe6ffe69f6e17616cd33b27677 Mon Sep 17 00:00:00 2001 From: Alan Greene Date: Sun, 20 Oct 2024 17:56:22 +0100 Subject: [PATCH] Improve log display Add support for toggling display of timestamps in the logs, so the user no longer has to navigate away to the settings page to do this. This is available on both PipelineRun and TaskRun pages. We always request timestamps and just show / hide them depending on the user's preference. This is persisted to browser localStorage as with the toggle on the settings page. Add support for detecting GitHub Actions workflow command-style log levels in log output. This provides an improved user experience as it allows for filtering logs to hide unwanted noise, e.g. debug logs, by default. We may consider allowing the format to be customised in a future release depending on user feedback. Refactor the styles so `LogFormat` now correctly owns most of the styling of the log content, with `Log` only responsible for additional styling of the container. Refactor use of the `LogsToolbar` component to allow for customisable use by third-party consumers of the Dashboard components. This means they can much more easily take advantage of the new features, such as toggling timestamps and log levels, without having to reimplement the menu and related code themselves. Eliminate redundant use of `split` and `join` calls when processing the logs, improving performance. `LogFormat` now receives an array of log line objects, pre-parsed into the new structure with the `timestamp`, `level` (optional), and `message` fields. Where a multiline log is encountered, the timestamp of the first line is reused for subsequent lines in that log. Fix issue where in some cases a blank line did not reserve vertical space, leading to cramped display of logs. Now each line is guaranteed to occupy a minimum height, ensuring blank lines output in the logs to aid in readability are preserved in the UI. Update `FormattedDate` to add support for displaying seconds, as this is quite important in the log context. Default to `false` for this setting so existing date / timestamps in other parts of the UI are unaffected. The full raw timestamp as received in the logs in displayed in a tooltip on hover. Update unit tests to reflect the new and changed components and behaviours. Update common PipelineRun E2E to exercise the new log toolbar and validate the log content is rendered as expected. Add new stories to cover the new functionality. Update existing stories to demonstrate use of the new functionality in context. Update Carbon: - resolve issue with Plex Mono font Some glyphs weren't included in the Plex version packaged with previous Carbon releases, resulting in broken formatting for some log content, e.g. using box characters to print tables. In `@carbon/react` 1.71.0 the Plex version has been updated, as well as changing how it's consumed. Instead of a single package with all of the font variants, they're now published as separate packages per font family. Add the `$use-per-family-plex` flag to our config to use these new packages. The custom `$font-path` is still required for compatibility with Vite. - resolve issue with duplicate onChange events from MenuItemSelectable - resolve issue with duplicate onChange events when clearing a ComboBox - document the log viewer feature, the new log format, and the existing external logs support Notes: - colours of the log level badges are based on the colours of the Carbon `Tag` component, with their opacity reduced so they're not as intense due to the potentially large number of them that could be displayed in the logs. These all meet minimum colour contrast ratio required for WCAG 2.0 level AA (i.e. > 4.5:1). - the default log level is 'info' if no log level is explicitly provided in the logs, however we only display the badge when the log level is explicitly set. This avoids unnecessary and unwanted noise / clutter in the logs when not using the new log format. - highlight and hover state included to highlight log lines, aiding in consuming the content, especially with longer log lines where the log level badge may not be adjacent to the content being read. A future update to the log viewer will add support for line wrapping but this is out of scope for this particular change. --- .eslintrc.cjs | 5 +- docs/logs.md | 79 +++++++ package-lock.json | 6 +- .../ActionableNotification.jsx | 4 +- .../FormattedDate/FormattedDate.jsx | 5 +- .../FormattedDate/FormattedDate.stories.js | 6 + .../FormattedDate/FormattedDate.test.jsx | 14 +- .../components/src/components/Log/Log.jsx | 83 ++++++-- .../src/components/Log/Log.stories.jsx | 49 ++++- .../components/src/components/Log/_Log.scss | 18 +- .../Log/samples/timestamps_log_levels.txt | 160 +++++++++++++++ .../src/components/LogFormat/LogFormat.jsx | 54 +++-- .../components/LogFormat/LogFormat.stories.js | 96 +++++++-- .../components/LogFormat/LogFormat.test.jsx | 194 +++++++++++++----- .../src/components/LogFormat/_LogFormat.scss | 70 ++++++- .../components/LogsToolbar/LogsToolbar.jsx | 161 ++++++++++++--- .../LogsToolbar/LogsToolbar.stories.jsx | 121 +++++++++++ .../components/PipelineRun/PipelineRun.jsx | 13 +- .../PipelineRun/PipelineRun.stories.jsx | 92 +++++++++ .../e2e/cypress/e2e/common/pipelinerun.cy.js | 16 ++ src/api/index.js | 4 +- src/api/utils.js | 28 +++ src/api/utils.test.js | 18 -- src/containers/LogsToolbar/LogsToolbar.jsx | 54 +++++ .../LogsToolbar/LogsToolbar.test.jsx | 66 ++++++ .../containers/LogsToolbar/index.js | 17 +- src/containers/PipelineRun/PipelineRun.jsx | 62 ++++-- src/containers/Settings/Settings.jsx | 20 -- src/containers/Settings/Settings.test.jsx | 15 -- src/containers/TaskRun/TaskRun.jsx | 74 +++++-- src/containers/index.js | 1 + src/nls/messages_de.json | 6 + src/nls/messages_en.json | 8 +- src/nls/messages_es.json | 6 + src/nls/messages_fr.json | 6 + src/nls/messages_it.json | 6 + src/nls/messages_ja.json | 6 + src/nls/messages_ko.json | 6 + src/nls/messages_pt.json | 6 + src/nls/messages_zh-Hans.json | 6 + src/nls/messages_zh-Hant.json | 6 + src/scss/_carbon.scss | 3 +- src/utils/{index.jsx => index.js} | 33 +-- src/utils/index.test.js | 52 +---- 44 files changed, 1416 insertions(+), 339 deletions(-) create mode 100644 docs/logs.md create mode 100644 packages/components/src/components/Log/samples/timestamps_log_levels.txt create mode 100644 packages/components/src/components/LogsToolbar/LogsToolbar.stories.jsx create mode 100644 src/containers/LogsToolbar/LogsToolbar.jsx create mode 100644 src/containers/LogsToolbar/LogsToolbar.test.jsx rename packages/components/src/components/LogsToolbar/LogsToolbar.stories.js => src/containers/LogsToolbar/index.js (67%) rename src/utils/{index.jsx => index.js} (87%) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index e10661858..bb3c322c1 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -65,7 +65,10 @@ module.exports = { 'import/no-extraneous-dependencies': 'off', 'import/no-named-as-default': 'off', 'import/no-named-as-default-member': 'off', - 'import/no-unresolved': ['error', { ignore: ['\\.svg\\?react$'] }], + 'import/no-unresolved': [ + 'error', + { ignore: ['\\.svg\\?react$', '\\.txt\\?raw$'] } + ], 'import/prefer-default-export': 'off', 'jsx-a11y/anchor-is-valid': 'off', 'no-case-declarations': 'off', diff --git a/docs/logs.md b/docs/logs.md new file mode 100644 index 000000000..81cf4b03a --- /dev/null +++ b/docs/logs.md @@ -0,0 +1,79 @@ + + +# Tekton Dashboard log viewer + +This guide describes the features and functionality of the log viewer provided by the Tekton Dashboard on the `TaskRun` and `PipelineRun` details pages. + +## Basic functionality + +The Tekton Dashboard log viewer supports ANSI colour codes and text styles, and will automatically detect URLs in log content and render them as clickable links opening in a new window. + +## Toolbar + +The toolbar diplayed in the log viewer includes a number of additional features, including: + +- maximize: increase the area available to the log viewer by hiding the task list and run header. This allows the user to eliminate distractions from other parts of the app and focus on the log content. +- open in new window: open the raw logs in a separate browser window. This provides an unmodified and unprocessed view of the logs, without any of the added features provided by the log viewer. +- download: download the raw logs as a text file. +- user preferences: these are persisted to browser local storage and applied to all logs in the app. See the following sections for more details. + +### Timestamps + +The Dashboard will always request logs with timestamps from the Kubernetes pod logs API, and show / hide the timestamps in the log viewer based on the user's preference. This can be toggled from the settings menu in the toolbar at the top of the log viewer. + +The timestamps are localised based on users' browser settings, with the raw timestamp value received from the API provided as a tooltip on hover. + +In releases prior to Tekton Dashboard v0.54, the timestamp preference was found on the Settings page, and governed whether or not timestamps were requested from the Kubernetes API server. + +### Log levels + +The log viewer parses log lines to detect the associated log level and decorate them accordingly to help with log consumability. The format supported is described below. + +``` + :::: +``` + +- `timestamp` is provided by the Kubernetes API server +- `level` is one of `error`, `warning`, `notice`, `info`, `debug` + - `debug` logs are hidden by default + - any log line without an explicit `level` is considered as `info`, but will not display the log level badge to avoid redundancy in the UI where users are not using the supported log format +- `message` is any other content on the line, and may contain ANSI codes for formatting, etc. + +For example, the following snippet would output a log line at the `warning` level: + +```sh +echo '::warning::Something that may require attention but is non-blocking…' +``` + +The displayed log levels can be changed via the settings menu in the toolbar at the top of the log viewer. + +## Logs persistence + +By default, Tekton Dashboard loads the logs from the Kubernetes API server, using the pod logs API. However, it also supports loading logs from an external source when the container logs or the pods associated with the `TaskRuns` are no longer available on the cluster. + +This functionality is described in detail, along with a full walk-through of an example configuration, in [Tekton Dashboard walk-through - Logs persistence](./walkthrough/walkthrough-logs.md). + +It can be enabled by providing the `--external-logs` flag to the installer script, or configured directly in the Dashboard deployment's args. + +When configured, the Dashboard will first attempt to load pod logs normally, and if they're unavailable will fallback to the provided external logs service by making a `GET` request to the provided endpoint with the following format: + +``` +GET ///?startTime=&completionTime= +``` + +- `namespace`: the namespace containing the run +- `podName`: the name of the `Pod` resource associated with the selected `TaskRun` +- `container`: the name of the container associated with the selected `step` +- `stepStartTime`: the start time of the step container +- `stepCompletionTime`: the completion time of the step container + +If the start / completion times are unavailable their respective query parameters will be omitted from the request. + +--- + +Except as otherwise noted, the content of this page is licensed under the [Creative Commons Attribution 4.0 License](https://creativecommons.org/licenses/by/4.0/). Code samples are licensed under the [Apache 2.0 License](https://www.apache.org/licenses/LICENSE-2.0). diff --git a/package-lock.json b/package-lock.json index 247790136..d5cd30ab3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4931,9 +4931,9 @@ "integrity": "sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA==" }, "node_modules/cross-spawn": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", - "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/packages/components/src/components/ActionableNotification/ActionableNotification.jsx b/packages/components/src/components/ActionableNotification/ActionableNotification.jsx index 7d795d33a..7245b90f2 100644 --- a/packages/components/src/components/ActionableNotification/ActionableNotification.jsx +++ b/packages/components/src/components/ActionableNotification/ActionableNotification.jsx @@ -18,9 +18,7 @@ import { export default function ActionableNotification(props) { return ( - + ); diff --git a/packages/components/src/components/FormattedDate/FormattedDate.jsx b/packages/components/src/components/FormattedDate/FormattedDate.jsx index dffab487f..7f9c91e3d 100644 --- a/packages/components/src/components/FormattedDate/FormattedDate.jsx +++ b/packages/components/src/components/FormattedDate/FormattedDate.jsx @@ -16,6 +16,7 @@ import { FormattedDate, FormattedRelativeTime, useIntl } from 'react-intl'; const FormattedDateWrapper = ({ date, formatTooltip = formattedDate => formattedDate, + includeSeconds = false, relative }) => { const intl = useIntl(); @@ -47,6 +48,7 @@ const FormattedDateWrapper = ({ year={yearFormat} hour="numeric" minute="numeric" + {...(includeSeconds ? { second: 'numeric' } : null)} /> ); } @@ -56,7 +58,8 @@ const FormattedDateWrapper = ({ month: 'long', year: 'numeric', hour: 'numeric', - minute: 'numeric' + minute: 'numeric', + ...(includeSeconds ? { second: 'numeric' } : null) }); formattedDate = formatTooltip(formattedDate); return {content}; diff --git a/packages/components/src/components/FormattedDate/FormattedDate.stories.js b/packages/components/src/components/FormattedDate/FormattedDate.stories.js index 255e93068..e95660a2b 100644 --- a/packages/components/src/components/FormattedDate/FormattedDate.stories.js +++ b/packages/components/src/components/FormattedDate/FormattedDate.stories.js @@ -33,3 +33,9 @@ export const Relative = { }; export const Absolute = {}; + +export const Seconds = { + args: { + includeSeconds: true + } +}; diff --git a/packages/components/src/components/FormattedDate/FormattedDate.test.jsx b/packages/components/src/components/FormattedDate/FormattedDate.test.jsx index 3a6e9950c..dbddb9b70 100644 --- a/packages/components/src/components/FormattedDate/FormattedDate.test.jsx +++ b/packages/components/src/components/FormattedDate/FormattedDate.test.jsx @@ -21,8 +21,18 @@ describe('FormattedDate', () => { }); it('handles absolute date formatting', () => { - const { queryByText } = render(); - expect(queryByText(/Dec 1, 2019/i)).toBeTruthy(); + const { queryByText } = render( + + ); + expect(queryByText(/Dec 1, 2019, 12:13/i)).toBeTruthy(); + expect(queryByText(/:14/i)).toBeFalsy(); + }); + + it('handles absolute date formatting with seconds', () => { + const { queryByText } = render( + + ); + expect(queryByText(/Dec 1, 2019, 12:13:14/i)).toBeTruthy(); }); it('handles absolute date formatting for current year', () => { diff --git a/packages/components/src/components/Log/Log.jsx b/packages/components/src/components/Log/Log.jsx index b3348b63b..9c0b27d4a 100644 --- a/packages/components/src/components/Log/Log.jsx +++ b/packages/components/src/components/Log/Log.jsx @@ -27,19 +27,16 @@ import { import DotSpinner from '../DotSpinner'; import LogFormat from '../LogFormat'; -const LogLine = ({ data, index, style }) => ( -
- {`${data[index]}\n`} -
-); - -const itemSize = 15; // This should be kept in sync with the line-height in SCSS +const itemSize = 16; // This should be kept in sync with the line-height in SCSS const defaultHeight = itemSize * 100 + itemSize / 2; +const logFormatRegex = + /^((?\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3,9}Z)\s?)?(::(?error|warning|info|notice|debug)::)?(?.*)?$/s; + export class LogContainer extends Component { constructor(props) { super(props); - this.state = { loading: true }; + this.state = { loading: true, logs: [] }; this.logRef = createRef(); this.textRef = createRef(); } @@ -244,7 +241,27 @@ export class LogContainer extends Component { }; getLogList = () => { - const { stepStatus, intl } = this.props; + const { + intl, + logLevels, + parseLogLine = line => { + if (!line?.length) { + return { message: line }; + } + + const { + groups: { level, message, timestamp } + } = logFormatRegex.exec(line); + return { + level, + message, + timestamp + }; + }, + showLevels, + showTimestamps, + stepStatus + } = this.props; const { reason } = (stepStatus && stepStatus.terminated) || {}; const { logs = [ @@ -255,8 +272,35 @@ export class LogContainer extends Component { ] } = this.state; - if (logs.length < 20000) { - return {logs.join('\n')}; + let previousTimestamp; + const parsedLogs = logs.reduce((acc, line) => { + const parsedLogLine = parseLogLine(line); + if (!parsedLogLine.timestamp) { + // multiline log, use same timestamp as previous line + parsedLogLine.timestamp = previousTimestamp; + } else { + previousTimestamp = parsedLogLine.timestamp; + } + + if ( + !logLevels || + // we treat lines with no log level as if they specified 'info' + // but we don't display a default level for these lines to avoid + // unnecessary noise for users not using the expected log format + (!parsedLogLine.level && logLevels.info) || + logLevels[parsedLogLine.level] + ) { + acc.push(parsedLogLine); + } + return acc; + }, []); + if (parsedLogs.length < 20_000) { + return ( + + ); } const height = reason @@ -266,12 +310,19 @@ export class LogContainer extends Component { return ( - {LogLine} + {({ data, index, style }) => ( +
+ +
+ )}
); }; @@ -333,7 +384,7 @@ export class LogContainer extends Component { logs += decoder.decode(value, { stream: !done }); this.setState({ loading: false, - logs: logs.split('\n') + logs: logs.split(/\r?\n/) }); } else { this.setState({ @@ -376,7 +427,7 @@ export class LogContainer extends Component { } else { this.setState({ loading: false, - logs: logs ? logs.split('\n') : undefined + logs: logs ? logs.split(/\r?\n/) : undefined }); if (continuePolling) { clearTimeout(this.timer); diff --git a/packages/components/src/components/Log/Log.stories.jsx b/packages/components/src/components/Log/Log.stories.jsx index 20b0c543a..61cfd3d4b 100644 --- a/packages/components/src/components/Log/Log.stories.jsx +++ b/packages/components/src/components/Log/Log.stories.jsx @@ -11,14 +11,16 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { useArgs } from '@storybook/preview-api'; + import Log from './Log'; import LogsToolbar from '../LogsToolbar'; const ansiLog = '\n=== demo-pipeline-run-1-build-skaffold-app-2mrdg-pod-59e217: build-step-git-source-skaffold-git-ml8j4 ===\n{"level":"info","ts":1553865693.943092,"logger":"fallback-logger","caller":"git-init/main.go:100","msg":"Successfully cloned https://github.com/GoogleContainerTools/skaffold @ \\"master\\" in path \\"/workspace\\""}\n\n=== demo-pipeline-run-1-build-skaffold-app-2mrdg-pod-59e217: build-step-build-and-push ===\n\u001b[36mINFO\u001b[0m[0000] Downloading base image golang:1.10.1-alpine3.7\n2019/03/29 13:21:34 No matching credentials were found, falling back on anonymous\n\u001b[36mINFO\u001b[0m[0001] Executing 0 build triggers\n\u001b[36mINFO\u001b[0m[0001] Unpacking rootfs as cmd RUN go build -o /app . requires it.\n\u001b[36mINFO\u001b[0m[0010] Taking snapshot of full filesystem...\n\u001b[36mINFO\u001b[0m[0015] Using files from context: [/workspace/examples/microservices/leeroy-app/app.go]\n\u001b[36mINFO\u001b[0m[0015] COPY app.go .\n\u001b[36mINFO\u001b[0m[0015] Taking snapshot of files...\n\u001b[36mINFO\u001b[0m[0015] RUN go build -o /app .\n\u001b[36mINFO\u001b[0m[0015] cmd: /bin/sh\n\u001b[36mINFO\u001b[0m[0015] args: [-c go build -o /app .]\n\u001b[36mINFO\u001b[0m[0016] Taking snapshot of full filesystem...\n\u001b[36mINFO\u001b[0m[0036] CMD ["./app"]\n\u001b[36mINFO\u001b[0m[0036] COPY --from=builder /app .\n\u001b[36mINFO\u001b[0m[0036] Taking snapshot of files...\nerror pushing image: failed to push to destination gcr.io/christiewilson-catfactory/leeroy-app:latest: Get https://gcr.io/v2/token?scope=repository%3Achristiewilson-catfactory%2Fleeroy-app%3Apush%2Cpull\u0026scope=repository%3Alibrary%2Falpine%3Apull\u0026service=gcr.io exit status 1\n\n=== demo-pipeline-run-1-build-skaffold-app-2mrdg-pod-59e217: nop ===\nBuild successful\n\r\r\n'; -const long = Array.from({ length: 60000 }, (v, i) => `Line ${i + 1}\n`).join( - '' +const long = Array.from({ length: 60000 }, (v, i) => `Line ${i + 1}`).join( + '\n' ); const performanceTest = Array.from( @@ -30,7 +32,7 @@ export default { component: Log, decorators: [ Story => ( -
+
) @@ -85,6 +87,8 @@ export const ANSICodes = { export const Windowed = { args: { fetchLogs: () => long, + showLevels: true, + showTimestamps: true, stepStatus: { terminated: { reason: 'Completed', exitCode: 0 } } } }; @@ -92,6 +96,8 @@ export const Windowed = { export const Performance = { args: { fetchLogs: () => performanceTest, + showLevels: true, + showTimestamps: true, stepStatus: { terminated: { reason: 'Completed', exitCode: 0 } } }, name: 'performance test (<20,000 lines with ANSI)' @@ -109,8 +115,39 @@ export const Skipped = { export const Toolbar = { args: { - fetchLogs: () => 'A log message', - stepStatus: { terminated: { reason: 'Completed', exitCode: 0 } }, - toolbar: + fetchLogs: async () => + (await import('./samples/timestamps_log_levels.txt?raw')).default, + logLevels: { + error: true, + warning: true, + info: true, + notice: true, + debug: false + }, + showLevels: true, + showTimestamps: false, + stepStatus: { terminated: { reason: 'Completed', exitCode: 0 } } + }, + render: args => { + const [, updateArgs] = useArgs(); + return ( + + updateArgs({ logLevels: { ...args.logLevels, ...logLevel } }) + } + onToggleShowTimestamps={showTimestamps => + updateArgs({ showTimestamps }) + } + showTimestamps={args.showTimestamps} + url="/step/log/url" + /> + } + /> + ); } }; diff --git a/packages/components/src/components/Log/_Log.scss b/packages/components/src/components/Log/_Log.scss index 6c125260c..3bbe1c777 100644 --- a/packages/components/src/components/Log/_Log.scss +++ b/packages/components/src/components/Log/_Log.scss @@ -16,17 +16,19 @@ limitations under the License. @use '@carbon/react/scss/type' as *; pre.tkn--log { + --tkn-log-inline-padding: 1.6rem; position: relative; padding-block-start: 2rem; padding-block-end: 1.3rem; - padding-inline: 1.6rem; + padding-inline: var(--tkn-log-inline-padding); @include font-family('mono'); + // @include type-style('code-01'); font-size: 0.75rem; @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx) { font-size: 0.6875rem; } - line-height: 0.95rem; // Update the react-window List itemSize if changing this + line-height: 1rem; // Update the react-window List itemSize if changing this overflow: hidden; background-color: $background; color: $text-primary; @@ -49,12 +51,16 @@ pre.tkn--log { code { white-space: pre; overflow-wrap: normal; + display: block; + inline-size: auto; + min-inline-size: max-content; } .#{$prefix}--btn-set { position: absolute; inset-block-start: 0; - inset-inline-end: 0; + inset-inline-end: var(--tkn-log-inline-padding); + align-items: center; } .button-container { @@ -63,7 +69,7 @@ pre.tkn--log { inset-block-start: 3.125rem; //equals the maximum between padding-block-start of pre.tkn--log and between the page header height inset-block-end: 0; inset-inline-end: 0; - inline-size: 1.6rem; //equals the padding-inline-end of pre.tkn--log + inline-size: var(--tkn-log-inline-padding); } #log-scroll-to-start-btn, #log-scroll-to-end-btn { @@ -83,6 +89,7 @@ pre.tkn--log { inset-block-end: var(--tkn-scroll-button-bottom); } + .#{$prefix}--btn--ghost, .#{$prefix}--copy-btn { inline-size: 2rem; block-size: 2rem; @@ -103,7 +110,8 @@ pre.tkn--log { } .tkn--log-trailer { - font-family: ibm-plex-sans, sans-serif; + font-family: 'IBM Plex Sans', sans-serif; + // @include type-style('helper-text-01'); font-weight: bold; &[data-status='Completed'] { diff --git a/packages/components/src/components/Log/samples/timestamps_log_levels.txt b/packages/components/src/components/Log/samples/timestamps_log_levels.txt new file mode 100644 index 000000000..581404241 --- /dev/null +++ b/packages/components/src/components/Log/samples/timestamps_log_levels.txt @@ -0,0 +1,160 @@ +2024-11-14T14:10:53.354144861Z ::info::APP_REPO referred as https://github.com/example-org/example-app +2024-11-14T14:10:56.300268594Z ::debug::[get_repo_params:30] | get_repo_name called for https://github.com/example-org/example-app. Repository Name identified as example-app +2024-11-14T14:10:56.307088791Z ::debug::[get_repo_params:18] | get_repo_owner called for https://github.com/example-org/example-app. Repository Owner identified as example-org +2024-11-14T14:10:56.700290228Z ::debug::[setup_pr:87] | INCIDENT_ISSUES_URL https://github.com/example-org/example-compliance-issues +2024-11-14T14:10:56.815017386Z ::debug::[get_repo_params:212] | Unable to locate repository parameters for key https://github.com/example-org/example-compliance-issues in the cache. Attempt to fetch repository parameters. +2024-11-14T14:10:56.819937688Z ::debug::[get_repo_params:39] | get_repo_server_name called for https://github.com/example-org/example-compliance-issues. Repository Server Name identified as github.com +2024-11-14T14:10:56.869171436Z ::debug::[get_repo_params:89] | get_absolute_scm_type called for https://github.com/example-org/example-compliance-issues. SCM Type identified as github +2024-11-14T14:10:56.869719012Z ::debug::[get_repo_params:201] | get_api_url called for https://github.com/example-org/example-compliance-issues. Reading token from environment property https://github.com/api/v3. +2024-11-14T14:10:56.921468475Z ::debug::[get_repo_params:89] | get_absolute_scm_type called for https://github.com/example-org/example-compliance-issues. SCM Type identified as github +2024-11-14T14:10:56.921926910Z ::debug::[get_repo_params:97] | get_absolute_scm_type called for https://github.com/example-org/example-compliance-issues. SCM Type identified as github +2024-11-14T14:10:56.930606377Z ::debug::[get_repo_params:18] | get_repo_owner called for https://github.com/example-org/example-compliance-issues. Repository Owner identified as PIYUSH-Mundra +2024-11-14T14:10:56.937633991Z ::debug::[get_repo_params:30] | get_repo_name called for https://github.com/example-org/example-compliance-issues. Repository Name identified as example-compliance-issues +2024-11-14T14:10:56.938340279Z ::debug::[get_repo_params:218] | Update the cache for key https://github.com/example-org/example-compliance-issues. +2024-11-14T14:10:57.053269000Z ::debug::[get_repo_params:223] | get_repo_params called for https://github.com/example-org/example-compliance-issues. +2024-11-14T14:10:57.060958330Z ::debug::[get_repo_params:30] | get_repo_name called for https://github.com/example-org/example-compliance-issues. Repository Name identified as example-compliance-issues +2024-11-14T14:10:57.065382263Z ::debug::[get_repo_params:18] | get_repo_owner called for https://github.com/example-org/example-compliance-issues. Repository Owner identified as PIYUSH-Mundra +2024-11-14T14:10:57.144848440Z ::debug::[get_repo_params:141] | get_repo_specific_token called for https://github.com/example-org/example-compliance-issues. Reading token from environment property +2024-11-14T14:10:57.145289008Z ::debug::[get_repo_params:153] | get_repo_token called for https://github.com/example-org/example-compliance-issues. Reading token from environment property . +2024-11-14T14:10:57.145311781Z ::debug::[get_repo_params:162] | Unable to retreive repo_token from environment property. Attempt to fetch Personal Access Token. +2024-11-14T14:10:57.177880037Z ::debug::[get_repo_params:116] | get_repo_pat called for https://github.com/example-org/example-compliance-issues. Auth Type identified as oauth +2024-11-14T14:10:57.178355945Z ::debug::[get_repo_params:167] | Unable to retreive Personal Access Token. Attempt to fetch token from Toolchain Broker. +2024-11-14T14:10:57.211621179Z ::debug::[get_repo_params:89] | get_absolute_scm_type called for https://github.com/example-org/example-compliance-issues. SCM Type identified as github +2024-11-14T14:10:57.891095096Z ::debug::[get_credentials_v2:187] | Fetch Git Token for SCM Type: github, SCM ID: integrated, Repository URL: https://github.com/example-org/example-compliance-issues. +2024-11-14T14:10:57.935248942Z ::info::Fetch git token for https://github.com/example-org/example-inventory.git +2024-11-14T14:10:59.051060095Z ::info::Fetching IAM access token... +2024-11-14T14:11:00.302592151Z ::info::Fetch token from toolchain broker. Query URL: https://otc-github-consolidated-broker.us-south.devops.cloud.ibm.com/github/token?git_id=integrated&toolchain_id=85f5ae2c-e4bb-43aa-96db-92aa8f54368c&service_instance_id=40478e15-8b70-40f0-93c5-932c2b958b17&repo_url=https://github.com/example-org/example-inventory.git. +2024-11-14T14:11:02.921082406Z ::debug::[get_credentials_v2:184] | Successfully retreived token for https://github.com/example-org/example-inventory.git and updated the cache. +2024-11-14T14:11:11.213717482Z ::debug::[get_repo_params:30] | get_repo_name called for https://github.com/example-org/example-app. Repository Name identified as example-app +2024-11-14T14:11:11.224234563Z ::debug::[get_repo_params:18] | get_repo_owner called for https://github.com/example-org/example-app. Repository Owner identified as PIYUSH-Mundra +2024-11-14T14:11:11.333949964Z ::debug::[get_repo_params:141] | get_repo_specific_token called for https://github.com/example-org/example-app. Reading token from environment property +2024-11-14T14:11:11.335344502Z ::debug::[get_repo_params:153] | get_repo_token called for https://github.com/example-org/example-app. Reading token from environment property . +2024-11-14T14:11:11.335771106Z ::debug::[get_repo_params:162] | Unable to retreive repo_token from environment property. Attempt to fetch Personal Access Token. +2024-11-14T14:11:11.380632436Z ::debug::[get_repo_params:116] | get_repo_pat called for https://github.com/example-org/example-app. Auth Type identified as oauth +2024-11-14T14:11:11.381059828Z ::debug::[get_repo_params:167] | Unable to retreive Personal Access Token. Attempt to fetch token from Toolchain Broker. +2024-11-14T14:11:11.444320839Z ::debug::[get_repo_params:89] | get_absolute_scm_type called for https://github.com/example-org/example-app. SCM Type identified as github +2024-11-14T14:11:12.149847003Z ::debug::[get_credentials_v2:187] | Fetch Git Token for SCM Type: github, SCM ID: integrated, Repository URL: https://github.com/example-org/example-app. +2024-11-14T14:11:12.207642299Z ::info::Fetch git token for https://github.com/example-org/example-inventory.git +2024-11-14T14:11:12.313844262Z ::debug::[get_credentials_v2:106] | Returning git token for https://github.com/example-org/example-inventory.git as found in cache. +2024-11-14T14:11:12.640204642Z ::debug::[get_repo_params:89] | get_absolute_scm_type called for https://github.com/example-org/example-app. SCM Type identified as github +2024-11-14T14:11:12.640848952Z ::debug::[get_repo_params:97] | get_absolute_scm_type called for https://github.com/example-org/example-app. SCM Type identified as github +2024-11-14T14:11:17.217179370Z INFO: notifications are turned off . +2024-11-14T14:11:17.324996212Z ::info::Cloning application repository of type=github repo=https://github.com/example-org/example-app:revert-4-test/test-3 branch= commit= +2024-11-14T14:11:17.831509554Z ::info::Cloning Repository: https://github.com/example-org/example-app, Branch: master, Commit: at Path: with USE_SUBMODULES flag as: 0 +2024-11-14T14:11:19.025893202Z ::debug::[clone_repo:118] | Branch master exists in https://github.com/example-org/example-app +2024-11-14T14:11:19.033207058Z ::debug::[clone_repo:143] | Attempting to clone with retry using git clone -q -b master https://****:****@github.com/example-org/example-app +2024-11-14T14:11:26.947198505Z ::debug::[clone_repo:185] | Origin: +2024-11-14T14:11:26.958713232Z ::info::Successfully Cloned Repository: https://github.com/example-org/example-app, Branch: master, Commit: f16e5a66fa78262078d3590b75a242f311da4224 at Path: example-app. +2024-11-14T14:11:29.826267156Z From https://github.com/example-org/example-app +2024-11-14T14:11:29.826317883Z * [new ref] refs/pull/5/head -> temp_revert-4-test_test-3_temp +2024-11-14T14:11:29.994799234Z Switched to branch 'temp_revert-4-test_test-3_temp' +2024-11-14T14:11:30.041818627Z HEAD is now at b00b89b Revert "chore: update readme" +2024-11-14T14:11:30.069444019Z Switched to branch 'master' +2024-11-14T14:11:30.072838820Z Your branch is up to date with 'origin/master'. +2024-11-14T14:11:30.145860476Z Updating f16e5a6..b00b89b +2024-11-14T14:11:30.145891405Z Fast-forward +2024-11-14T14:11:30.167249857Z README.md | 3 --- +2024-11-14T14:11:30.167288759Z 1 file changed, 3 deletions(-) +2024-11-14T14:11:30.168330372Z ::debug::[merge_pr_branch:53] | git merge of pull request with id 5 and commit b00b89b53ead4344a55d003b1f5698183e6128ab to master succeeded. +2024-11-14T14:11:30.567714946Z ::debug::[get_repo_params:30] | get_repo_name called for https://github.com/example-org/example-app. Repository Name identified as example-app +2024-11-14T14:11:30.572994605Z ::debug::[get_repo_params:18] | get_repo_owner called for https://github.com/example-org/example-app. Repository Owner identified as PIYUSH-Mundra +2024-11-14T14:11:30.669224685Z ::debug::[get_repo_params:141] | get_repo_specific_token called for https://github.com/example-org/example-app. Reading token from environment property +2024-11-14T14:11:30.669846206Z ::debug::[get_repo_params:153] | get_repo_token called for https://github.com/example-org/example-app. Reading token from environment property . +2024-11-14T14:11:30.670304272Z ::debug::[get_repo_params:162] | Unable to retreive repo_token from environment property. Attempt to fetch Personal Access Token. +2024-11-14T14:11:30.718736005Z ::debug::[get_repo_params:116] | get_repo_pat called for https://github.com/example-org/example-app. Auth Type identified as oauth +2024-11-14T14:11:30.720588941Z ::debug::[get_repo_params:167] | Unable to retreive Personal Access Token. Attempt to fetch token from Toolchain Broker. +2024-11-14T14:11:30.769049756Z ::debug::[get_repo_params:89] | get_absolute_scm_type called for https://github.com/example-org/example-app. SCM Type identified as github +2024-11-14T14:11:31.563713080Z ::debug::[get_credentials_v2:187] | Fetch Git Token for SCM Type: github, SCM ID: integrated, Repository URL: https://github.com/example-org/example-app. +2024-11-14T14:11:31.612290227Z ::info::Fetch git token for https://github.com/example-org/example-inventory.git +2024-11-14T14:11:31.699130184Z ::debug::[get_credentials_v2:106] | Returning git token for https://github.com/example-org/example-inventory.git as found in cache. +2024-11-14T14:11:32.516948769Z ::info::Cloning Repository: https://github.com/example-org/example-app, Branch: master, Commit: at Path: one-pipeline-config-repo with USE_SUBMODULES flag as: 1 +2024-11-14T14:11:33.636651210Z ::debug::[clone_repo:118] | Branch master exists in https://github.com/example-org/example-app +2024-11-14T14:11:33.645486961Z ::debug::[clone_repo:143] | Attempting to clone with retry using git clone -q -b master https://****:****@github.com/example-org/example-app one-pipeline-config-repo +2024-11-14T14:11:37.349997961Z ::debug::[clone_repo:185] | Origin: +2024-11-14T14:11:37.367120803Z ::info::Successfully Cloned Repository: https://github.com/example-org/example-app, Branch: master, Commit: f16e5a66fa78262078d3590b75a242f311da4224 at Path: one-pipeline-config-repo. +2024-11-14T14:11:42.498866720Z +2024-11-14T14:11:42.498915345Z +2024-11-14T14:11:42.498922840Z YAML linting of pipeline configuration passed +2024-11-14T14:11:42.498926150Z +2024-11-14T14:11:42.498929514Z +2024-11-14T14:11:42.498934511Z List of custom stages within pipeline configuration missing implementation. For these stages pipeline will pick the default implementations. : +2024-11-14T14:11:42.498941481Z - 'detect-secrets' +2024-11-14T14:11:42.498945057Z - 'compliance-checks' +2024-11-14T14:11:42.498948957Z - 'pr-finish' +2024-11-14T14:11:42.498953257Z For more information on the stages that can be skipped refer to the doc link below +2024-11-14T14:11:42.498957672Z https://cloud.ibm.com/docs/devsecops?topic=devsecops-cd-devsecops-pr-pipeline +2024-11-14T14:11:42.498961928Z +2024-11-14T14:11:42.498964925Z +2024-11-14T14:11:43.342596128Z ::debug::[set-commit-status:48] | repository: https://github.com/example-org/example-app +2024-11-14T14:11:43.342933970Z ::debug::[set-commit-status:60] | commit-sha: b00b89b53ead4344a55d003b1f5698183e6128ab +2024-11-14T14:11:43.342946722Z ::debug::[set-commit-status:72] | state: pending +2024-11-14T14:11:43.342952600Z ::debug::[set-commit-status:85] | description: Running unit tests... +2024-11-14T14:11:43.343224450Z ::debug::[set-commit-status:97] | context: tekton/code-unit-tests +2024-11-14T14:11:43.343242477Z ::debug::[set-commit-status:109] | task-name: code-unit-tests +2024-11-14T14:11:43.343247262Z ::debug::[set-commit-status:121] | step-name: run-stage +2024-11-14T14:11:43.343251080Z +2024-11-14T14:11:43.448849244Z ::debug::[get_repo_params:212] | Unable to locate repository parameters for key https://github.com/example-org/example-app in the cache. Attempt to fetch repository parameters. +2024-11-14T14:11:43.453672514Z ::debug::[get_repo_params:39] | get_repo_server_name called for https://github.com/example-org/example-app. Repository Server Name identified as github.com +2024-11-14T14:11:43.496797303Z ::debug::[get_repo_params:89] | get_absolute_scm_type called for https://github.com/example-org/example-app. SCM Type identified as github +2024-11-14T14:11:43.497560996Z ::debug::[get_repo_params:201] | get_api_url called for https://github.com/example-org/example-app. Reading token from environment property https://github.com/api/v3. +2024-11-14T14:11:43.529001605Z ::debug::[get_repo_params:89] | get_absolute_scm_type called for https://github.com/example-org/example-app. SCM Type identified as github +2024-11-14T14:11:43.529338994Z ::debug::[get_repo_params:97] | get_absolute_scm_type called for https://github.com/example-org/example-app. SCM Type identified as github +2024-11-14T14:11:43.533299101Z ::debug::[get_repo_params:18] | get_repo_owner called for https://github.com/example-org/example-app. Repository Owner identified as PIYUSH-Mundra +2024-11-14T14:11:43.540457519Z ::debug::[get_repo_params:30] | get_repo_name called for https://github.com/example-org/example-app. Repository Name identified as example-app +2024-11-14T14:11:43.540474134Z ::debug::[get_repo_params:218] | Update the cache for key https://github.com/example-org/example-app. +2024-11-14T14:11:43.623629967Z ::debug::[get_repo_params:223] | get_repo_params called for https://github.com/example-org/example-app. +2024-11-14T14:11:43.631172957Z ::debug::[get_repo_params:30] | get_repo_name called for https://github.com/example-org/example-app. Repository Name identified as example-app +2024-11-14T14:11:43.635020828Z ::debug::[get_repo_params:18] | get_repo_owner called for https://github.com/example-org/example-app. Repository Owner identified as PIYUSH-Mundra +2024-11-14T14:11:43.719595703Z ::debug::[get_repo_params:141] | get_repo_specific_token called for https://github.com/example-org/example-app. Reading token from environment property +2024-11-14T14:11:43.719784194Z ::debug::[get_repo_params:153] | get_repo_token called for https://github.com/example-org/example-app. Reading token from environment property . +2024-11-14T14:11:43.719789683Z ::debug::[get_repo_params:162] | Unable to retreive repo_token from environment property. Attempt to fetch Personal Access Token. +2024-11-14T14:11:43.750695266Z ::debug::[get_repo_params:116] | get_repo_pat called for https://github.com/example-org/example-app. Auth Type identified as oauth +2024-11-14T14:11:43.750947704Z ::debug::[get_repo_params:167] | Unable to retreive Personal Access Token. Attempt to fetch token from Toolchain Broker. +2024-11-14T14:11:43.782406568Z ::debug::[get_repo_params:89] | get_absolute_scm_type called for https://github.com/example-org/example-app. SCM Type identified as github +2024-11-14T14:11:44.375617191Z ::debug::[get_credentials_v2:187] | Fetch Git Token for SCM Type: github, SCM ID: integrated, Repository URL: https://github.com/example-org/example-app. +2024-11-14T14:11:44.415251742Z ::info::Fetch git token for https://github.com/example-org/example-inventory.git +2024-11-14T14:11:44.507177971Z ::debug::[get_credentials_v2:106] | Returning git token for https://github.com/example-org/example-inventory.git as found in cache. +2024-11-14T14:11:44.825013004Z ::debug::[set-commit-status:219] | Calling set-commit-status with params %s: --state=pending --targetURL=https://cloud.ibm.com/devops/pipelines/tekton/a3fe08ad-d5de-4cd2-98b0-f5c5219a33cd/runs/5f3949cf-de85-415c-a6cc-74a336bbe206/code-unit-tests/run-stage?env_id=ibm:yp:us-south --context=tekton/code-unit-tests --description=Running unit tests... --git-provider=github --git-token-path=/workspace/app/secrets/app-token --git-api-url=https://github.com/api/v3 +2024-11-14T14:11:47.398894541Z ::info::set-commit-status for context tekton/code-unit-tests and commit with b00b89b53ead4344a55d003b1f5698183e6128ab in https://github.com/example-org/example-app to pending +2024-11-14T14:11:50.294887769Z ::info::Validating pipeline config +2024-11-14T14:11:51.257435452Z ::debug::[validate_pipeline:102] | Pipeline definitions are being referenced from branch with name chore/private-compliance-baseimage-3.59.5_commons-1.20.6 +2024-11-14T14:11:51.432935896Z ::debug::[validate_pipeline:157] | get_current_tag_branch retreived metadata for pipeline definitions. BRANCH_TAG_VALUE: chore/private-compliance-baseimage-3.59.5_commons-1.20.6, IS_BRANCH_OR_TAG: branch +2024-11-14T14:11:51.781884301Z ::debug::[validate_pipeline:364] | Successfully retrieved the metadata for pipeline definitions. Proceeding with pipeline definition validation. +2024-11-14T14:11:51.781910560Z ::debug::[validate_pipeline:296] | Validate Pipeline Definition for branch with value chore/private-compliance-baseimage-3.59.5_commons-1.20.6 and GIT SCM Provider github with production branches. +2024-11-14T14:11:51.781916139Z ::debug::[validate_pipeline:278] | Validating pipeline definition source chore/private-compliance-baseimage-3.59.5_commons-1.20.6 against list of stable branches chore/private-compliance-baseimage-3.59.5_commons-1.20.6 v10 v10-preprocessor v9. +2024-11-14T14:11:51.781922526Z ::warning::[validate_pipeline:280] | Your pipeline is currently running against pipeline definition branch: chore/private-compliance-baseimage-3.59.5_commons-1.20.6. It is advised to set theses branches: chore/private-compliance-baseimage-3.59.5_commons-1.20.6 v10 v10-preprocessor v9 . +2024-11-14T14:11:53.458588489Z INFO: notifications are turned off . +2024-11-14T14:11:54.166495164Z ::info::Getting compliance versions... +2024-11-14T14:11:54.490186086Z ::debug::[validate_pipeline:157] | get_current_tag_branch retreived metadata for pipeline definitions. BRANCH_TAG_VALUE: , IS_BRANCH_OR_TAG: +2024-11-14T14:11:54.833949298Z ::info::Core BaseImage Version: 3.59.5 +2024-11-14T14:11:54.833995892Z ::info::Commons Version: 1.20.6 +2024-11-14T14:11:54.834000728Z ::info::Pipeline Definition Source: branch +2024-11-14T14:11:54.836379443Z ::info::Pipeline Definition Source Value: chore/private-compliance-baseimage-3.59.5_commons-1.20.6 +2024-11-14T14:12:08.065631069Z ::error::Sample error +2024-11-14T14:12:08.065631069Z ::warning::Sample warning +2024-11-14T14:12:08.065631069Z ::notice::Sample notice +2024-11-14T14:12:08.065631069Z ::info::Sample info +2024-11-14T14:12:08.065631069Z Sample with no log level +2024-11-14T14:12:08.065631069Z ::debug::Sample debug +2024-11-14T14:12:08.065631069Z ::info::Details of asset created: +2024-11-14T14:12:11.849912684Z ┌─────┬──────┬────┬─────┐ +2024-11-14T14:12:11.849981080Z │ Key │ Type │ ID │ URL │ +2024-11-14T14:12:11.849987327Z └─────┴──────┴────┴─────┘ +2024-11-14T14:12:11.869437298Z ::info::Details of evidence collected: +2024-11-14T14:12:15.892827575Z ┌─────────────────┬────────────────────┐ +2024-11-14T14:12:15.892883264Z │ Attribute │ Value │ +2024-11-14T14:12:15.892888519Z ├─────────────────┼────────────────────┤ +2024-11-14T14:12:15.892895717Z │ Status │ success │ +2024-11-14T14:12:15.892900191Z ├─────────────────┼────────────────────┤ +2024-11-14T14:12:15.892904785Z │ Tool Type │ jest │ +2024-11-14T14:12:15.892908480Z ├─────────────────┼────────────────────┤ +2024-11-14T14:12:15.892912390Z │ Evidence ID │ - │ +2024-11-14T14:12:15.892916374Z ├─────────────────┼────────────────────┤ +2024-11-14T14:12:15.892920207Z │ Evidence Type │ com.ibm.unit_tests │ +2024-11-14T14:12:15.892924894Z ├─────────────────┼────────────────────┤ +2024-11-14T14:12:15.892930294Z │ Issues │ - │ +2024-11-14T14:12:15.892933984Z ├─────────────────┼────────────────────┤ +2024-11-14T14:12:15.892938649Z │ Attachment URLs │ │ +2024-11-14T14:12:15.892942307Z │ │ │ +2024-11-14T14:12:15.892947043Z └─────────────────┴────────────────────┘ +2024-11-14T14:12:15.989838531Z success \ No newline at end of file diff --git a/packages/components/src/components/LogFormat/LogFormat.jsx b/packages/components/src/components/LogFormat/LogFormat.jsx index 3c868359c..8b980b33e 100644 --- a/packages/components/src/components/LogFormat/LogFormat.jsx +++ b/packages/components/src/components/LogFormat/LogFormat.jsx @@ -11,12 +11,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import PropTypes from 'prop-types'; import tlds from 'tlds'; import LinkifyIt from 'linkify-it'; import { classNames } from '@tektoncd/dashboard-utils'; import { colors } from './defaults'; +import FormattedDate from '../FormattedDate'; const linkifyIt = LinkifyIt().tlds(tlds); @@ -24,6 +24,14 @@ const linkifyIt = LinkifyIt().tlds(tlds); const ansiRegex = /^\u001b([@-_])(.*?)([@-~])/; const characterRegex = /[^]/m; +const getDecoratedLevel = level => { + if (!level) { + return null; + } + + return {level}; +}; + const getXtermColor = commandStack => { if (commandStack.length >= 2 && commandStack[0] === '5') { commandStack.shift(); @@ -84,7 +92,7 @@ const linkify = (str, styleObj, classNameString) => { return elements; }; -const LogFormat = ({ children }) => { +const LogFormat = ({ fields = { message: true }, logs = [] }) => { let properties = { classes: {}, foregroundColor: null, @@ -240,13 +248,14 @@ const LogFormat = ({ children }) => { }; }; - const parse = (ansi, index) => { - if (ansi.length === 0) { + const parse = (log, index) => { + const { level, message = '', timestamp } = log; + if (!message?.length && !timestamp && !level) { return
; } let offset = 0; - while (offset !== ansi.length) { - const str = ansi.substring(offset); + while (offset !== message.length) { + const str = message.substring(offset); const controlSequence = str.match(ansiRegex); if (controlSequence) { offset += controlSequence.index + controlSequence[0].length; @@ -270,21 +279,36 @@ const LogFormat = ({ children }) => { ) ); } - return
{line}
; + + return ( +
+ {fields.timestamp && ( + + timestamp} + includeSeconds + /> + + )} + {fields.level && getDecoratedLevel(level)} + {line} +
+ ); }; - const convert = ansi => - ansi.split(/\r?\n/).map((part, index) => { + const convert = () => + logs.map((part, index) => { text = ''; line = []; return parse(part, index); }); - - return {convert(children)}; -}; - -LogFormat.propTypes = { - children: PropTypes.string.isRequired + return {convert()}; }; export default LogFormat; diff --git a/packages/components/src/components/LogFormat/LogFormat.stories.js b/packages/components/src/components/LogFormat/LogFormat.stories.js index 02d9adbab..48a0f030e 100644 --- a/packages/components/src/components/LogFormat/LogFormat.stories.js +++ b/packages/components/src/components/LogFormat/LogFormat.stories.js @@ -14,26 +14,29 @@ limitations under the License. import LogFormat from './LogFormat'; const ansiColors = (() => { - let text = ''; + const logs = []; // 16 named 'system' colors [30, 90, 40, 100].forEach(seq => { + let line = ''; for (let i = 0; i < 8; i += 1) { - text += `\u001b[${seq + i}m${i} \u001b[0m`; + line += `\u001b[${seq + i}m${i} \u001b[0m`; } - text += '\n'; + logs.push(line); }); - text += '\n'; + logs.push(''); // 256-colors [38, 48].forEach(seq => { + let line = ''; for (let i = 0; i < 256; i += 1) { - text += `\u001b[${seq};5;${i}m${i} \u001b[0m`; + line += `\u001b[${seq};5;${i}m${i} \u001b[0m`; if ((i + 1) % 6 === 4) { - text += '\n'; + logs.push(line); + line = ''; } } - text += '\n'; + logs.push(''); }); - return text; + return logs.map(message => ({ message })); })(); const ansiTextStyles = (() => { @@ -45,11 +48,10 @@ const ansiTextStyles = (() => { cross: 9 }; - let text = ''; - Object.entries(textStyles).forEach(([key, value]) => { - text += `\u001b[${value}m${key}\u001b[0m\n`; - }); - return text; + const logs = Object.entries(textStyles).map(([key, value]) => ({ + message: `\u001b[${value}m${key}\u001b[0m` + })); + return logs; })(); export default { @@ -64,19 +66,19 @@ export default { export const Colors = { args: { - children: ansiColors + logs: ansiColors } }; export const TextStyles = { args: { - children: ansiTextStyles + logs: ansiTextStyles } }; export const URLDetection = { args: { - children: ` + logs: ` + curl https://raw.githubusercontent.com/tektoncd/pipeline/master/tekton/koparse/koparse.py --output /usr/bin/koparse.py % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed @@ -86,5 +88,67 @@ export const URLDetection = { + IMAGES=(gcr.io/tekton-releases/github.com/tektoncd/dashboard/cmd/dashboard) + BUILT_IMAGES=($(/usr/bin/koparse.py --path /workspace/output/bucket-for-dashboard/latest/tekton-dashboard-release.yaml --base gcr.io/tekton-releases/github.com/tektoncd/dashboard --images \${IMAGES[@]})) ` + .split('\n') + .map(message => ({ message })) + } +}; + +export const LogLevelsAndTimestamps = { + args: { + fields: { + level: true, + timestamp: true + }, + logs: [ + { + timestamp: '2024-11-14T14:10:53.354144861Z', + level: 'info', + message: 'Cloning repo' + }, + { + timestamp: '2024-11-14T14:10:56.300268594Z', + level: 'debug', + message: + '[get_repo_params:30] | get_repo_name called for https://github.com/example-org/example-app. Repository Name identified as example-app' + }, + { + timestamp: '2024-11-14T14:10:56.307088791Z', + level: 'debug', + message: + '[get_repo_params:18] | get_repo_owner called for https://github.com/example-org/example-app. Repository Owner identified as example-org' + }, + { + timestamp: '2024-11-14T14:10:56.815017386Z', + level: 'debug', + message: + '[get_repo_params:212] | Unable to locate repository parameters for key https://github.com/example-org/example-app in the cache. Attempt to fetch repository parameters.' + }, + { + timestamp: '2024-11-14T14:10:56.819937688Z', + level: 'debug', + message: + '[get_repo_params:39] | get_repo_server_name called for https://github.com/example-org/example-app. Repository Server Name identified as github.com' + }, + { + timestamp: '2024-11-14T14:10:56.869719012Z', + level: null, + message: 'Sample with no log level' + }, + { + timestamp: '2024-11-14T14:10:56.869719012Z', + level: 'error', + message: 'Sample error' + }, + { + timestamp: '2024-11-14T14:10:56.869719012Z', + level: 'warning', + message: 'Sample warning' + }, + { + timestamp: '2024-11-14T14:10:56.869719012Z', + level: 'notice', + message: 'Sample notice' + } + ] } }; diff --git a/packages/components/src/components/LogFormat/LogFormat.test.jsx b/packages/components/src/components/LogFormat/LogFormat.test.jsx index 04d83bcff..f9a379445 100644 --- a/packages/components/src/components/LogFormat/LogFormat.test.jsx +++ b/packages/components/src/components/LogFormat/LogFormat.test.jsx @@ -14,8 +14,8 @@ limitations under the License. import { render } from '../../utils/test'; import LogFormat from './LogFormat'; -const getElement = (text, query) => { - const { queryByText } = render({text}); +const getElement = (logs, query) => { + const { queryByText } = render(); const queryRegex = new RegExp(query, 'i'); return queryByText(queryRegex); }; @@ -26,82 +26,109 @@ const textClassPrefix = 'tkn--ansi--text-'; describe('LogFormat', () => { it('displays text', () => { - const element = getElement('Hello World', 'Hello World'); + const element = getElement([{ message: 'Hello World' }], 'Hello World'); expect(element).toBeTruthy(); }); it('displays red text', () => { - const element = getElement('\u001b[31mHello World\u001b[0m', 'Hello World'); + const element = getElement( + [{ message: '\u001b[31mHello World\u001b[0m' }], + 'Hello World' + ); expect(element.outerHTML).toBe( `Hello World` ); }); it('displays green text', () => { - const element = getElement('\u001b[32mHello World\u001b[0m', 'Hello World'); + const element = getElement( + [{ message: '\u001b[32mHello World\u001b[0m' }], + 'Hello World' + ); expect(element.outerHTML).toBe( `Hello World` ); }); it('displays yellow text', () => { - const element = getElement('\u001b[33mHello World\u001b[0m', 'Hello World'); + const element = getElement( + [{ message: '\u001b[33mHello World\u001b[0m' }], + 'Hello World' + ); expect(element.outerHTML).toBe( `Hello World` ); }); it('displays blue text', () => { - const element = getElement('\u001b[34mHello World\u001b[0m', 'Hello World'); + const element = getElement( + [{ message: '\u001b[34mHello World\u001b[0m' }], + 'Hello World' + ); expect(element.outerHTML).toBe( `Hello World` ); }); it('displays a magenta background', () => { - const element = getElement('\u001b[45mHello World', 'Hello World'); + const element = getElement( + [{ message: '\u001b[45mHello World' }], + 'Hello World' + ); expect(element.outerHTML).toBe( `Hello World` ); }); it('displays a cyan background', () => { - const element = getElement('\u001b[46mHello World\u001b[0m', 'Hello World'); + const element = getElement( + [{ message: '\u001b[46mHello World\u001b[0m' }], + 'Hello World' + ); expect(element.outerHTML).toBe( `Hello World` ); }); it('displays a white background', () => { - const element = getElement('\u001b[47mHello World', 'Hello World'); + const element = getElement( + [{ message: '\u001b[47mHello World' }], + 'Hello World' + ); expect(element.outerHTML).toBe( `Hello World` ); }); it('displays cyan text without a trailing reset', () => { - const element = getElement('\u001b[36mHello', 'Hello'); + const element = getElement([{ message: '\u001b[36mHello' }], 'Hello'); expect(element.outerHTML).toBe( `Hello` ); }); it('displays red text on a blue background', () => { - const element = getElement('\u001b[31;44mHello', 'Hello'); + const element = getElement([{ message: '\u001b[31;44mHello' }], 'Hello'); expect(element.outerHTML).toBe( `Hello` ); }); it('resets colors after red text on blue background', () => { - const element = getElement('\u001b[31;44mHello\u001b[0m world', 'world'); + const element = getElement( + [{ message: '\u001b[31;44mHello\u001b[0m world' }], + 'world' + ); expect(element.innerHTML).toBe( `Hello world` ); }); it('performs a color change from red/blue to yellow/blue', () => { - const element = getElement('\u001b[31;44mHello\u001b[33m world', 'Hello'); + const element = getElement( + [{ message: '\u001b[31;44mHello\u001b[33m world' }], + 'Hello' + ); expect(element.outerHTML).toBe( `Hello` ); @@ -112,7 +139,7 @@ describe('LogFormat', () => { it('performs color change from red/blue to yellow/green', () => { const element = getElement( - '\u001b[31;44mHello\u001b[33;42m world', + [{ message: '\u001b[31;44mHello\u001b[33;42m world' }], 'Hello' ); expect(element.outerHTML).toBe( @@ -124,110 +151,137 @@ describe('LogFormat', () => { }); it('ignores unsupported codes', () => { - const element = getElement('\u001b[51mHello\u001b[0m', 'Hello'); + const element = getElement( + [{ message: '\u001b[51mHello\u001b[0m' }], + 'Hello' + ); expect(element.innerHTML).toBe('Hello'); }); it('displays bright red text', () => { - const element = getElement('\u001b[91mHello\u001b[0m', 'Hello'); + const element = getElement( + [{ message: '\u001b[91mHello\u001b[0m' }], + 'Hello' + ); expect(element.outerHTML).toBe( `Hello` ); }); it('displays bright red background', () => { - const element = getElement('\u001b[101mHello\u001b[0m', 'Hello'); + const element = getElement( + [{ message: '\u001b[101mHello\u001b[0m' }], + 'Hello' + ); expect(element.outerHTML).toBe( `Hello` ); }); it('displays a xterm color as a background', () => { - const element = getElement('\u001b[48;5;200mHello', 'Hello'); + const element = getElement([{ message: '\u001b[48;5;200mHello' }], 'Hello'); expect(element.outerHTML).toBe( 'Hello' ); }); it('displays a xterm color as a foreground', () => { - const element = getElement('\u001b[38;5;100mHello', 'Hello'); + const element = getElement([{ message: '\u001b[38;5;100mHello' }], 'Hello'); expect(element.outerHTML).toBe( 'Hello' ); }); it('displays bold text', () => { - const element = getElement('\u001b[1mHello', 'Hello'); + const element = getElement([{ message: '\u001b[1mHello' }], 'Hello'); expect(element.outerHTML).toBe( `Hello` ); }); it('can reset bold text (bold off)', () => { - const element = getElement('\u001b[1mHello\u001b[21m world', 'world'); + const element = getElement( + [{ message: '\u001b[1mHello\u001b[21m world' }], + 'world' + ); expect(element.innerHTML).toBe( `Hello world` ); }); it('can reset bold text (normal color / intensity)', () => { - const element = getElement('\u001b[1mHello\u001b[22m world', 'world'); + const element = getElement( + [{ message: '\u001b[1mHello\u001b[22m world' }], + 'world' + ); expect(element.innerHTML).toBe( `Hello world` ); }); it('displays italic text', () => { - const element = getElement('\u001b[3mHello', 'Hello'); + const element = getElement([{ message: '\u001b[3mHello' }], 'Hello'); expect(element.outerHTML).toBe( `Hello` ); }); it('can reset italic text', () => { - const element = getElement('\u001b[3mHello\u001b[23m world', 'world'); + const element = getElement( + [{ message: '\u001b[3mHello\u001b[23m world' }], + 'world' + ); expect(element.innerHTML).toBe( `Hello world` ); }); it('displays underline text', () => { - const element = getElement('\u001b[4mHello', 'Hello'); + const element = getElement([{ message: '\u001b[4mHello' }], 'Hello'); expect(element.outerHTML).toBe( `Hello` ); }); it('can resets underline text', () => { - const element = getElement('\u001b[4mHello\u001b[24m world', 'world'); + const element = getElement( + [{ message: '\u001b[4mHello\u001b[24m world' }], + 'world' + ); expect(element.innerHTML).toBe( `Hello world` ); }); it('displays concealed text', () => { - const element = getElement('\u001b[8mHello', 'Hello'); + const element = getElement([{ message: '\u001b[8mHello' }], 'Hello'); expect(element.outerHTML).toBe( `Hello` ); }); it('can resets concealed text', () => { - const element = getElement('\u001b[8mHello\u001b[28m world', 'world'); + const element = getElement( + [{ message: '\u001b[8mHello\u001b[28m world' }], + 'world' + ); expect(element.innerHTML).toBe( `Hello world` ); }); it('displays crossed text', () => { - const element = getElement('\u001b[9mHello', 'Hello'); + const element = getElement([{ message: '\u001b[9mHello' }], 'Hello'); expect(element.outerHTML).toBe( `Hello` ); }); it('can reset crossed-out text', () => { - const element = getElement('\u001b[9mHello\u001b[29m world', 'world'); + const element = getElement( + [{ message: '\u001b[9mHello\u001b[29m world' }], + 'world' + ); expect(element.innerHTML).toBe( `Hello world` ); @@ -235,7 +289,12 @@ describe('LogFormat', () => { it('displays links', () => { const element = getElement( - 'A dashboard for Tekton! https://github.com/tektoncd/dashboard', + [ + { + message: + 'A dashboard for Tekton! https://github.com/tektoncd/dashboard' + } + ], 'https://github.com/tektoncd/dashboard' ); expect(element.outerHTML).toBe( @@ -245,7 +304,12 @@ describe('LogFormat', () => { it('displays links with styles', () => { const element = getElement( - 'A dashboard for Tekton!\u001b[9m\u001b[48;5;194mhttps://github.com/tektoncd/dashboard', + [ + { + message: + 'A dashboard for Tekton!\u001b[9m\u001b[48;5;194mhttps://github.com/tektoncd/dashboard' + } + ], 'https://github.com/tektoncd/dashboard' ); expect(element.outerHTML).toBe( @@ -253,30 +317,62 @@ describe('LogFormat', () => { ); }); - it('converts new lines as line breaks', () => { - const text = 'Hello\n\nWorld'; - const { container } = render({text}); + it('handles empty lines', () => { + const logs = [{ message: 'Hello' }, { message: '' }, { message: 'World' }]; + const { container } = render(); expect(container.childNodes[0].innerHTML).toBe( - '
Hello

World
' + '
Hello

World
' ); }); - it('separates text by new lines', () => { - const text = - 'Hello World\nA dashboard for Tekton! https://github.com/tektoncd/dashboard\nTekon is cool!'; - const { container } = render({text}); + it('handles multiple lines', () => { + const logs = [ + { + message: 'Hello World' + }, + { + message: 'A dashboard for Tekton! https://github.com/tektoncd/dashboard' + }, + { message: 'Tekton is cool!' } + ]; + const { container } = render(); expect(container.childNodes[0].childNodes).toHaveLength(3); }); - it('separates text by new lines and carriage returns', () => { - const text = '\r \n \r \n\r \n'; - const { container } = render({text}); - expect(container.childNodes[0].childNodes).toHaveLength(4); - }); - it('handles consecutive carriage returns without error', () => { - const text = '\r\r'; - const { container } = render({text}); + const logs = [{ message: '\r\r' }]; + const { container } = render(); expect(container.childNodes[0].childNodes).toHaveLength(1); }); + + it('handles timestamps', () => { + const timestamp = '2024-11-20 12:13:14'; + const logs = [{ timestamp, message: 'Hello' }]; + const { container, rerender } = render( + + ); + expect(container.childNodes[0].innerHTML).toBe( + '
Hello
' + ); + rerender(); + expect(container.childNodes[0].innerHTML).toMatch( + // uses raw timestamp from input as title attribute + // contains formatted timestamp for display + // accept anything (`.*`) for test purposes as it may be localised + // and we're more concerned with the structure here + new RegExp( + `
.*Hello
` + ) + ); + }); + + it('handles levels', () => { + const logs = [{ level: 'debug', message: 'Hello' }]; + const { queryByText, rerender } = render( + + ); + expect(queryByText('debug')).toBeFalsy(); + rerender(); + expect(queryByText('debug')).toBeTruthy(); + }); }); diff --git a/packages/components/src/components/LogFormat/_LogFormat.scss b/packages/components/src/components/LogFormat/_LogFormat.scss index cea05a8d4..8a793f6cd 100644 --- a/packages/components/src/components/LogFormat/_LogFormat.scss +++ b/packages/components/src/components/LogFormat/_LogFormat.scss @@ -1,5 +1,5 @@ /* -Copyright 2020 The Tekton Authors +Copyright 2020-2024 The Tekton 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 @@ -68,3 +68,71 @@ $colors: ( text-decoration: line-through; } } + +.tkn--log-line { + min-block-size: 1rem; // explicitly set height to hold space for empty log line when timestamps disabled + transition: 0s background-color; + &:hover { + background-color: #ffffff10; + transition-delay: 0.5s; + } + + // set default colours for the log level badge, this will be used for 'info' + --tkn-log-level-badge-background: var(--cds-tag-background-gray); + --tkn-log-level-badge-color: var(--cds-text-primary); + + &.tkn--log-level--error { + --tkn-log-level-badge-background: rgb(from var(--cds-tag-background-red) r g b / 80%); + --tkn-log-level-badge-color: var(--cds-tag-color-red); + + background-color: rgb(from var(--cds-support-error-inverse) r g b / 15%); + + &:hover { + background-color: rgb(from var(--cds-support-error-inverse) r g b / 30%); + } + } + + &.tkn--log-level--warning { + --tkn-log-level-badge-background: rgb(from var(--cds-support-warning) r g b / 35%); + + background-color: rgb(from var(--cds-support-warning-inverse) r g b / 15%); + + &:hover { + background-color: rgb(from var(--cds-support-warning-inverse) r g b / 30%); + } + } + + &.tkn--log-level--notice { + --tkn-log-level-badge-background: var(--cds-tag-background-teal); + --tkn-log-level-badge-color: var(--cds-tag-color-teal); + } + + &.tkn--log-level--debug { + --tkn-log-level-badge-background: rgb(from var(--cds-tag-background-purple) r g b / 60%); + --tkn-log-level-badge-color: var(--cds-tag-color-purple); + } + + .tkn--log-line--timestamp { + color: var(--cds-text-helper); + font-weight: 300; + } + + .tkn--log-line--level { + display: inline-block; + padding-inline: 4px; + background-color: var(--tkn-log-level-badge-background); + color: var(--tkn-log-level-badge-color); + } +} + +// only add margin after timestamp if there's at least one log line with a +// visible timestamp so we don't add unnecessary space when 'Show timestamps' is +// enabled but for some reason there are no timestamps to show (maybe external +// logs that didn't capture timestamps at runtime) +.tkn--log:has(.tkn--log-line--timestamp:not(:empty)) .tkn--log-line--timestamp { + margin-inline-end: 0.5rem; +} + +.tkn--log-line--level { + margin-inline-end: 0.5rem; +} diff --git a/packages/components/src/components/LogsToolbar/LogsToolbar.jsx b/packages/components/src/components/LogsToolbar/LogsToolbar.jsx index 089bdda8f..a30843578 100644 --- a/packages/components/src/components/LogsToolbar/LogsToolbar.jsx +++ b/packages/components/src/components/LogsToolbar/LogsToolbar.jsx @@ -12,18 +12,41 @@ limitations under the License. */ /* istanbul ignore file */ import { useIntl } from 'react-intl'; -import { Download, Launch, Maximize, Minimize } from '@carbon/react/icons'; -import { usePrefix } from '@carbon/react'; +import { + Download, + Launch, + Maximize, + Minimize, + Settings +} from '@carbon/react/icons'; +import { + unstable_FeatureFlags as FeatureFlags, + MenuItemDivider, + MenuItemGroup, + MenuItemSelectable, + OverflowMenu, + usePrefix +} from '@carbon/react'; -const LogsToolbar = ({ isMaximized, name, toggleMaximized, url }) => { +const LogsToolbar = ({ + isMaximized, + name, + logLevels, + onToggleShowTimestamps, + onToggleLogLevel, + onToggleMaximized, + showTimestamps, + url +}) => { const carbonPrefix = usePrefix(); const intl = useIntl(); + return (
- {toggleMaximized ? ( + {onToggleMaximized ? ( ) : null} - - - - {intl.formatMessage({ - id: 'dashboard.logs.launchButtonTooltip', - defaultMessage: 'Open logs in a new window' - })} - - - - - - - {intl.formatMessage({ - id: 'dashboard.logs.downloadButtonTooltip', - defaultMessage: 'Download logs' + {url ? ( + <> + <a + className={`${carbonPrefix}--btn ${carbonPrefix}--btn--icon-only ${carbonPrefix}--copy-btn`} + href={url} + target="_blank" + rel="noopener noreferrer" + > + <Launch> + <title> + {intl.formatMessage({ + id: 'dashboard.logs.launchButtonTooltip', + defaultMessage: 'Open logs in a new window' + })} + + + + + + + {intl.formatMessage({ + id: 'dashboard.logs.downloadButtonTooltip', + defaultMessage: 'Download logs' + })} + + + + + ) : null} + + + - - + onChange={onToggleShowTimestamps} + selected={showTimestamps} + /> + {logLevels && onToggleLogLevel ? ( + <> + + + onToggleLogLevel({ error })} + selected={logLevels.error} + /> + onToggleLogLevel({ warning })} + selected={logLevels.warning} + /> + onToggleLogLevel({ notice })} + selected={logLevels.notice} + /> + onToggleLogLevel({ info })} + selected={logLevels.info} + /> + onToggleLogLevel({ debug })} + selected={logLevels.debug} + /> + + + ) : null} + +
); }; diff --git a/packages/components/src/components/LogsToolbar/LogsToolbar.stories.jsx b/packages/components/src/components/LogsToolbar/LogsToolbar.stories.jsx new file mode 100644 index 000000000..2825226ba --- /dev/null +++ b/packages/components/src/components/LogsToolbar/LogsToolbar.stories.jsx @@ -0,0 +1,121 @@ +/* +Copyright 2019-2024 The Tekton 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 { useArgs } from '@storybook/preview-api'; + +import LogsToolbar from './LogsToolbar'; + +export default { + component: LogsToolbar, + decorators: [ + Story => ( +
+        
+      
+ ) + ], + title: 'LogsToolbar' +}; + +export const Default = { + args: { + showTimestamps: false + }, + render: args => { + const [, updateArgs] = useArgs(); + + return ( + + updateArgs({ showTimestamps }) + } + /> + ); + } +}; + +export const WithLogLevels = { + args: { + ...Default.args, + logLevels: { + error: true, + warning: true, + info: true, + notice: true, + debug: false + } + }, + render: args => { + const [, updateArgs] = useArgs(); + + return ( + + updateArgs({ logLevels: { ...args.logLevels, ...logLevel } }) + } + onToggleShowTimestamps={showTimestamps => + updateArgs({ showTimestamps }) + } + /> + ); + } +}; + +export const WithMaximize = { + args: { + ...WithLogLevels.args, + isMaximized: false + }, + render: args => { + const [, updateArgs] = useArgs(); + + return ( + + updateArgs({ logLevels: { ...args.logLevels, ...logLevel } }) + } + onToggleMaximized={() => updateArgs({ isMaximized: !args.isMaximized })} + onToggleShowTimestamps={showTimestamps => + updateArgs({ showTimestamps }) + } + /> + ); + } +}; + +export const WithURL = { + args: { + ...WithMaximize.args, + name: 'some_filename.txt', + url: '/some/logs/url' + }, + render: args => { + const [, updateArgs] = useArgs(); + + return ( + + updateArgs({ logLevels: { ...args.logLevels, ...logLevel } }) + } + onToggleMaximized={() => updateArgs({ isMaximized: !args.isMaximized })} + onToggleShowTimestamps={showTimestamps => + updateArgs({ showTimestamps }) + } + /> + ); + } +}; diff --git a/packages/components/src/components/PipelineRun/PipelineRun.jsx b/packages/components/src/components/PipelineRun/PipelineRun.jsx index c74f6b3c3..ea335202a 100644 --- a/packages/components/src/components/PipelineRun/PipelineRun.jsx +++ b/packages/components/src/components/PipelineRun/PipelineRun.jsx @@ -51,6 +51,7 @@ export default /* istanbul ignore next */ function PipelineRun({ handleTaskSelected = /* istanbul ignore next */ () => {}, icon, loading, + logLevels, maximizedLogsContainer, onRetryChange, onViewChange = /* istanbul ignore next */ () => {}, @@ -63,6 +64,8 @@ export default /* istanbul ignore next */ function PipelineRun({ selectedStepId = null, selectedTaskId = null, selectedTaskRunName, + showLogLevels, + showLogTimestamps, taskRuns, tasks, triggerHeader, @@ -88,7 +91,7 @@ export default /* istanbul ignore next */ function PipelineRun({ ); } - function toggleLogsMaximized() { + function onToggleLogsMaximized() { setIsLogsMaximized(prevIsLogsMaximized => !prevIsLogsMaximized); } @@ -110,19 +113,23 @@ export default /* istanbul ignore next */ function PipelineRun({ stepStatus && getLogsToolbar({ isMaximized: isLogsMaximized, + onToggleMaximized: + !!maximizedLogsContainer && onToggleLogsMaximized, stepStatus, - taskRun, - toggleMaximized: !!maximizedLogsContainer && toggleLogsMaximized + taskRun }) } fetchLogs={() => fetchLogs(stepName, stepStatus, taskRun)} forcePolling={forceLogPolling} key={`${selectedTaskId}:${selectedStepId}:${selectedRetry}`} + logLevels={logLevels} pollingInterval={pollingInterval} stepStatus={stepStatus} isLogsMaximized={isLogsMaximized} enableLogAutoScroll={enableLogAutoScroll} enableLogScrollButtons={enableLogScrollButtons} + showLevels={showLogLevels} + showTimestamps={showLogTimestamps} /> ); diff --git a/packages/components/src/components/PipelineRun/PipelineRun.stories.jsx b/packages/components/src/components/PipelineRun/PipelineRun.stories.jsx index 7b1efef3e..da5aa41c7 100644 --- a/packages/components/src/components/PipelineRun/PipelineRun.stories.jsx +++ b/packages/components/src/components/PipelineRun/PipelineRun.stories.jsx @@ -15,6 +15,7 @@ import { useArgs } from '@storybook/preview-api'; import { labels as labelConstants } from '@tektoncd/dashboard-utils'; import PipelineRun from '.'; +import LogsToolbar from '../LogsToolbar'; const task = { metadata: { @@ -197,6 +198,38 @@ export default { title: 'PipelineRun' }; +const logsWithTimestampsAndLevels = `2024-11-14T14:10:53.354144861Z::info::Cloning repo +2024-11-14T14:10:56.300268594Z::debug::[get_repo_params:30] | get_repo_name called for https://github.com/example/app. Repository Name identified as app +2024-11-14T14:10:56.307088791Z::debug::[get_repo_params:18] | get_repo_owner called for https://github.com/example/app. Repository Owner identified as example +2024-11-14T14:10:56.815017386Z::debug::[get_repo_params:212] | Unable to locate repository parameters for key https://github.com/example/app in the cache. Attempt to fetch repository parameters. +2024-11-14T14:10:56.819937688Z::debug::[get_repo_params:39] | get_repo_server_name called for https://github.com/example/app. Repository Server Name identified as github.com +2024-11-14T14:10:56.869719012Z Sample with no log level +2024-11-14T14:10:56.869719012Z::error::Sample error +2024-11-14T14:10:56.869719012Z::warning::Sample warning +2024-11-14T14:10:56.869719012Z::notice::Sample notice +2024-11-14T14:11:08.065631069Z ::info::Details of asset created: +2024-11-14T14:11:11.849912684Z ┌─────┬──────┬────┬─────┐ +2024-11-14T14:11:11.849981080Z │ Key │ Type │ ID │ URL │ +2024-11-14T14:11:11.849987327Z └─────┴──────┴────┴─────┘ +2024-11-14T14:11:11.869437298Z ::info::Details of evidence collected: +2024-11-14T14:11:15.892827575Z ┌─────────────────┬────────────────────┐ +2024-11-14T14:11:15.892883264Z │ Attribute │ Value │ +2024-11-14T14:11:15.892888519Z ├─────────────────┼────────────────────┤ +2024-11-14T14:11:15.892895717Z │ Status │ \x1B[32msuccess\x1B[39m │ +2024-11-14T14:11:15.892900191Z ├─────────────────┼────────────────────┤ +2024-11-14T14:11:15.892904785Z │ Tool Type │ jest │ +2024-11-14T14:11:15.892908480Z ├─────────────────┼────────────────────┤ +2024-11-14T14:11:15.892912390Z │ Evidence ID │ - │ +2024-11-14T14:11:15.892916374Z ├─────────────────┼────────────────────┤ +2024-11-14T14:11:15.892920207Z │ Evidence Type │ com.ibm.unit_tests │ +2024-11-14T14:11:15.892924894Z ├─────────────────┼────────────────────┤ +2024-11-14T14:11:15.892930294Z │ Issues │ - │ +2024-11-14T14:11:15.892933984Z ├─────────────────┼────────────────────┤ +2024-11-14T14:11:15.892938649Z │ Attachment URLs │ │ +2024-11-14T14:11:15.892942307Z │ │ │ +2024-11-14T14:11:15.892947043Z └─────────────────┴────────────────────┘ +2024-11-14T14:11:15.989838531Z success`; + export const Default = args => { const [, updateArgs] = useArgs(); @@ -320,12 +353,71 @@ export const WithPodDetails = args => { } } }} + selectedTaskId="task1" taskRuns={[taskRun]} tasks={[task]} + view="pod" /> ); }; +export const LogsWithTimestampsAndLevels = { + args: { + fetchLogs: () => logsWithTimestampsAndLevels, + logLevels: { + error: true, + warning: true, + notice: true, + info: true, + debug: false + }, + pipelineRun: pipelineRunWithMinimalStatus, + selectedStepId: 'build', + selectedTaskId: task.metadata.name, + showLogLevels: true, + showLogTimestamps: true, + taskRuns: [taskRun], + tasks: [task] + }, + render: args => { + const [, updateArgs] = useArgs(); + + return ( + ( + + updateArgs({ logLevels: { ...args.logLevels, ...level } }) + } + onToggleShowTimestamps={showLogTimestamps => + updateArgs({ showLogTimestamps }) + } + showTimestamps={args.showLogTimestamps} + /> + )} + handleTaskSelected={({ + selectedStepId: stepId, + selectedTaskId: taskId + }) => { + updateArgs({ selectedStepId: stepId, selectedTaskId: taskId }); + }} + onViewChange={selectedView => updateArgs({ view: selectedView })} + pipelineRun={pipelineRun} + taskRuns={[ + taskRun, + taskRunWithWarning, + taskRunSkipped, + taskRunWithSkippedStep + ]} + tasks={[task]} + /> + ); + } +}; + export const Empty = {}; export const Error = { args: { error: 'Internal server error' } }; diff --git a/packages/e2e/cypress/e2e/common/pipelinerun.cy.js b/packages/e2e/cypress/e2e/common/pipelinerun.cy.js index 9a8cbec14..f93ecc1e2 100644 --- a/packages/e2e/cypress/e2e/common/pipelinerun.cy.js +++ b/packages/e2e/cypress/e2e/common/pipelinerun.cy.js @@ -41,6 +41,7 @@ spec: script: | #!/bin/ash echo "Hello World!" + echo "::debug::This line should be hidden by default" `; cy.applyResource(pipelineRun); @@ -52,5 +53,20 @@ spec: cy.get('header[class="tkn--pipeline-run-header"]') .find('span[class="tkn--status-label"]', { timeout: 15000 }) .should('have.text', 'Succeeded'); + + cy.contains('.tkn--log', 'Hello World!'); + cy.contains('.tkn--log', '2024').should('not.exist'); + cy.get('.tkn--log-settings-menu button').click(); + cy.contains('Show timestamps').click(); + cy.contains( + // title starts with date formatted as 'yyyy-MM-dd' + `.tkn--log [title^="${new Date().toISOString().substring(0, 10)}"]`, + // something that looks like part of a timestamp: `mm:ss` + /\d{2}:\d{2}/ + ); + cy.contains('.tkn--log', 'hidden by default').should('not.exist'); + cy.get('.tkn--log-settings-menu button').click(); + cy.contains('Debug').click(); + cy.contains('.tkn--log', 'hidden by default'); }); }); diff --git a/src/api/index.js b/src/api/index.js index 8e2d6e3be..8e235c6e0 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -22,7 +22,6 @@ import { apiRoot, getKubeAPI, getTektonPipelinesAPIVersion, - isLogTimestampsEnabled, tektonAPIGroup, useCollection, useResource @@ -157,11 +156,10 @@ export function getExternalLogURL({ } export function getPodLogURL({ container, name, namespace, follow }) { - const timestamps = isLogTimestampsEnabled(); const queryParams = { ...(container && { container }), ...(follow && { follow }), - ...(timestamps && { timestamps }) + timestamps: true }; const uri = `${getKubeAPI({ group: 'core', diff --git a/src/api/utils.js b/src/api/utils.js index c22e9572e..97cc469f8 100644 --- a/src/api/utils.js +++ b/src/api/utils.js @@ -320,6 +320,34 @@ export function setLogTimestampsEnabled(enabled) { localStorage.setItem('tkn-logs-timestamps', enabled); } +export function getLogLevels() { + let logLevels = localStorage.getItem('tkn-logs-levels'); + if (logLevels) { + try { + logLevels = JSON.parse(logLevels); + } catch (e) { + // we'll fallback to a default config below + logLevels = null; + } + } + + if (!logLevels) { + logLevels = { + error: true, + warning: true, + info: true, + notice: true, + debug: false + }; + } + + return logLevels; +} + +export function setLogLevels(levels) { + localStorage.setItem('tkn-logs-levels', JSON.stringify(levels)); +} + export function removeSystemAnnotations(resource) { Object.keys(resource.metadata.annotations).forEach(annotation => { if (annotation.startsWith('tekton.dev/')) { diff --git a/src/api/utils.test.js b/src/api/utils.test.js index f80f42d6c..4187ae146 100644 --- a/src/api/utils.test.js +++ b/src/api/utils.test.js @@ -377,24 +377,6 @@ describe('isLogTimestampsEnabled', () => { }); }); -describe('isLogTimestampsEnabled', () => { - afterEach(() => { - localStorage.removeItem('tkn-logs-timestamps'); - }); - - it('handles valid values', () => { - localStorage.setItem('tkn-logs-timestamps', true); - expect(utils.isLogTimestampsEnabled()).toBe(true); - localStorage.setItem('tkn-logs-timestamps', false); - expect(utils.isLogTimestampsEnabled()).toBe(false); - }); - - it('handles invalid values', () => { - localStorage.setItem('tkn-logs-timestamps', 'foo'); - expect(utils.isLogTimestampsEnabled()).toBe(false); - }); -}); - describe('setLogTimestampsEnabled', () => { afterEach(() => { localStorage.removeItem('tkn-logs-timestamps'); diff --git a/src/containers/LogsToolbar/LogsToolbar.jsx b/src/containers/LogsToolbar/LogsToolbar.jsx new file mode 100644 index 000000000..7cf7bafa9 --- /dev/null +++ b/src/containers/LogsToolbar/LogsToolbar.jsx @@ -0,0 +1,54 @@ +/* +Copyright 2020-2024 The Tekton 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 { LogsToolbar } from '@tektoncd/dashboard-components'; + +import { getExternalLogURL, getPodLogURL } from '../../api'; + +export default function LogsToolbarContainer({ + externalLogsURL, + isMaximized, + isUsingExternalLogs, + logLevels, + onToggleLogLevel, + onToggleMaximized, + onToggleShowTimestamps, + showTimestamps, + stepStatus, + taskRun +}) { + const { container } = stepStatus; + const { namespace } = taskRun.metadata; + const { podName } = taskRun.status; + + const logURL = isUsingExternalLogs + ? getExternalLogURL({ container, externalLogsURL, namespace, podName }) + : getPodLogURL({ + container, + name: podName, + namespace + }); + + return ( + + ); +} diff --git a/src/containers/LogsToolbar/LogsToolbar.test.jsx b/src/containers/LogsToolbar/LogsToolbar.test.jsx new file mode 100644 index 000000000..510ac0813 --- /dev/null +++ b/src/containers/LogsToolbar/LogsToolbar.test.jsx @@ -0,0 +1,66 @@ +/* +Copyright 2020-2024 The Tekton 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 * as API from '../../api'; +import { render } from '../../utils/test'; + +import LogsToolbarContainer from './LogsToolbar'; + +describe('getLogsToolbar', () => { + it('should handle pod logs (default)', () => { + const container = 'fake_container'; + const namespace = 'fake_namespace'; + const podName = 'fake_podname'; + const stepStatus = { container }; + const taskRun = { metadata: { namespace }, status: { podName } }; + vi.spyOn(API, 'getPodLogURL'); + vi.spyOn(API, 'getExternalLogURL'); + + render(); + + expect(API.getExternalLogURL).not.toHaveBeenCalled(); + expect(API.getPodLogURL).toHaveBeenCalledWith({ + container, + name: podName, + namespace + }); + }); + + it('should handle external logs', () => { + const container = 'fake_container'; + const externalLogsURL = 'fake_externalLogsURL'; + const namespace = 'fake_namespace'; + const podName = 'fake_podname'; + const stepStatus = { container }; + const taskRun = { metadata: { namespace }, status: { podName } }; + vi.spyOn(API, 'getPodLogURL'); + vi.spyOn(API, 'getExternalLogURL'); + + render( + + ); + + expect(API.getPodLogURL).not.toHaveBeenCalled(); + expect(API.getExternalLogURL).toHaveBeenCalledWith({ + container, + externalLogsURL, + namespace, + podName + }); + }); +}); diff --git a/packages/components/src/components/LogsToolbar/LogsToolbar.stories.js b/src/containers/LogsToolbar/index.js similarity index 67% rename from packages/components/src/components/LogsToolbar/LogsToolbar.stories.js rename to src/containers/LogsToolbar/index.js index 5b26c9b76..128b61b5b 100644 --- a/packages/components/src/components/LogsToolbar/LogsToolbar.stories.js +++ b/src/containers/LogsToolbar/index.js @@ -1,5 +1,5 @@ /* -Copyright 2019-2024 The Tekton Authors +Copyright 2024 The Tekton 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 @@ -10,17 +10,6 @@ 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. */ +/* istanbul ignore file */ -import LogsToolbar from './LogsToolbar'; - -export default { - component: LogsToolbar, - title: 'LogsToolbar' -}; - -export const Default = { - args: { - name: 'some_filename.txt', - url: '/some/logs/url' - } -}; +export { default } from './LogsToolbar'; diff --git a/src/containers/PipelineRun/PipelineRun.jsx b/src/containers/PipelineRun/PipelineRun.jsx index 303e75f38..6c9bf715a 100644 --- a/src/containers/PipelineRun/PipelineRun.jsx +++ b/src/containers/PipelineRun/PipelineRun.jsx @@ -33,6 +33,7 @@ import { InlineNotification, RadioTile, TileGroup } from '@carbon/react'; import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'; import { useIntl } from 'react-intl'; +import LogsToolbar from '../LogsToolbar'; import { cancelPipelineRun, deletePipelineRun, @@ -49,21 +50,49 @@ import { useTaskRuns, useTasks } from '../../api'; -import { - getLogsRetriever, - getLogsToolbar, - getViewChangeHandler -} from '../../utils'; +import { getLogsRetriever, getViewChangeHandler } from '../../utils'; import NotFound from '../NotFound'; +import { + getLogLevels, + isLogTimestampsEnabled, + setLogLevels, + setLogTimestampsEnabled +} from '../../api/utils'; const { PIPELINE_TASK, RETRY, STEP, TASK_RUN_NAME, VIEW } = queryParamConstants; -export /* istanbul ignore next */ function PipelineRunContainer() { +export /* istanbul ignore next */ function PipelineRunContainer({ + // we may consider customisation of the log format in future + showLogLevels = true +}) { const intl = useIntl(); const location = useLocation(); const navigate = useNavigate(); const params = useParams(); + const [logLevels, setLogLevelsState] = useState(getLogLevels()); + const [showTimestamps, setShowTimestamps] = useState( + isLogTimestampsEnabled() + ); + + function onToggleLogLevel(logLevel) { + setLogLevelsState(levels => { + const newLevels = { ...levels, ...logLevel }; + // if (!Object.values(newLevels).filter(Boolean).length) { + // // TODO: logs - notification or allow? + // alert('must have at least 1 log level enabled'); + // return levels; + // } + setLogLevels(newLevels); + return newLevels; + }); + } + + function onToggleShowTimestamps(show) { + setShowTimestamps(show); + setLogTimestampsEnabled(show); + } + const { name, namespace } = params; const queryParams = new URLSearchParams(location.search); @@ -531,13 +560,18 @@ export /* istanbul ignore next */ function PipelineRunContainer() { })} handleTaskSelected={handleTaskSelected} loading={isLoading} - getLogsToolbar={toolbarParams => - getLogsToolbar({ - ...toolbarParams, - externalLogsURL, - isUsingExternalLogs - }) - } + logLevels={logLevels} + getLogsToolbar={toolbarProps => ( + + )} maximizedLogsContainer={maximizedLogsContainer.current} onRetryChange={retry => { if (Number.isInteger(retry)) { @@ -562,6 +596,8 @@ export /* istanbul ignore next */ function PipelineRunContainer() { selectedStepId={currentSelectedStepId} selectedTaskId={selectedTaskId} selectedTaskRunName={currentTaskRunName} + showLogLevels={showLogLevels} + showLogTimestamps={showTimestamps} taskRuns={taskRuns} tasks={tasks.concat(clusterTasks)} view={view} diff --git a/src/containers/Settings/Settings.jsx b/src/containers/Settings/Settings.jsx index ad342413f..ad51e9118 100644 --- a/src/containers/Settings/Settings.jsx +++ b/src/containers/Settings/Settings.jsx @@ -22,9 +22,7 @@ import { import { getTheme, setTheme } from '../../utils'; import { - isLogTimestampsEnabled, isPipelinesV1ResourcesEnabled, - setLogTimestampsEnabled, setPipelinesV1ResourcesEnabled } from '../../api/utils'; @@ -79,24 +77,6 @@ export function Settings() { - setLogTimestampsEnabled(checked)} - /> - { expect(Utils.setTheme).toHaveBeenCalledWith('dark'); }); - it('should render the log timestamp settings correctly', () => { - vi.spyOn(APIUtils, 'isLogTimestampsEnabled').mockImplementation(() => true); - vi.spyOn(APIUtils, 'setLogTimestampsEnabled'); - - const { getByRole } = render(); - - const logTimestampToggle = getByRole('switch', { - name: /show log timestamps/i - }); - expect(logTimestampToggle).toBeTruthy(); - expect(within(logTimestampToggle.parentNode).getByText('On')).toBeTruthy(); - fireEvent.click(logTimestampToggle); - expect(APIUtils.setLogTimestampsEnabled).toHaveBeenCalledWith(false); - }); - it('should render the v1 API settings correctly', () => { vi.spyOn(APIUtils, 'isPipelinesV1ResourcesEnabled').mockImplementation( () => true diff --git a/src/containers/TaskRun/TaskRun.jsx b/src/containers/TaskRun/TaskRun.jsx index 6c4094dca..57cf547b4 100644 --- a/src/containers/TaskRun/TaskRun.jsx +++ b/src/containers/TaskRun/TaskRun.jsx @@ -37,11 +37,8 @@ import { useTitleSync } from '@tektoncd/dashboard-utils'; -import { - getLogsRetriever, - getLogsToolbar, - getViewChangeHandler -} from '../../utils'; +import LogsToolbar from '../LogsToolbar'; +import { getLogsRetriever, getViewChangeHandler } from '../../utils'; import { cancelTaskRun, deleteTaskRun, @@ -56,15 +53,47 @@ import { useTaskRun } from '../../api'; import NotFound from '../NotFound'; +import { + getLogLevels, + isLogTimestampsEnabled, + setLogLevels, + setLogTimestampsEnabled +} from '../../api/utils'; const { STEP, RETRY, TASK_RUN_DETAILS, VIEW } = queryParamConstants; -export function TaskRunContainer() { +export function TaskRunContainer({ + // we may consider customisation of the log format in future + showLogLevels = true +}) { const intl = useIntl(); const location = useLocation(); const navigate = useNavigate(); const params = useParams(); + const [logLevels, setLogLevelsState] = useState(getLogLevels()); + const [showTimestamps, setShowTimestamps] = useState( + isLogTimestampsEnabled() + ); + + function onToggleLogLevel(logLevel) { + setLogLevelsState(levels => { + const newLevels = { ...levels, ...logLevel }; + // if (!Object.values(newLevels).filter(Boolean).length) { + // // TODO: logs - notification or allow? + // alert('must have at least 1 log level enabled'); + // return levels; + // } + setLogLevels(newLevels); + return newLevels; + }); + } + + function onToggleShowTimestamps(show) { + setShowTimestamps(show); + setLogTimestampsEnabled(show); + } + const { name, namespace: namespaceParam } = params; const queryParams = new URLSearchParams(location.search); @@ -134,7 +163,7 @@ export function TaskRunContainer() { { enabled: !!podName && view === 'pod' } ); - function toggleLogsMaximized() { + function onToggleLogsMaximized() { setIsLogsMaximized(state => !state); } @@ -158,20 +187,31 @@ export function TaskRunContainer() { : null)} > logsRetriever(stepName, stepStatus, run)} - key={`${stepName}:${currentRetry}`} - stepStatus={stepStatus} isLogsMaximized={isLogsMaximized} enableLogAutoScroll enableLogScrollButtons + key={`${stepName}:${currentRetry}`} + logLevels={logLevels} + showLevels={showLogLevels} + showTimestamps={showTimestamps} + stepStatus={stepStatus} + toolbar={ + + } /> ); diff --git a/src/containers/index.js b/src/containers/index.js index 5dacdc021..79a369bdf 100644 --- a/src/containers/index.js +++ b/src/containers/index.js @@ -30,6 +30,7 @@ export { default as LabelFilter } from './LabelFilter'; export { default as ListPageLayout } from './ListPageLayout'; export { default as LoadingShell } from './LoadingShell'; export { default as LogoutButton } from './LogoutButton'; +export { default as LogsToolbar } from './LogsToolbar'; export { default as NamespacesDropdown } from './NamespacesDropdown'; export { default as NotFound } from './NotFound'; export { default as PipelineRun } from './PipelineRun'; diff --git a/src/nls/messages_de.json b/src/nls/messages_de.json index 6b9f57148..7f8c89c11 100644 --- a/src/nls/messages_de.json +++ b/src/nls/messages_de.json @@ -149,6 +149,12 @@ "dashboard.logo.tooltip": "", "dashboard.logs.downloadButtonTooltip": "", "dashboard.logs.launchButtonTooltip": "", + "dashboard.logs.logLevels.debug": "", + "dashboard.logs.logLevels.error": "", + "dashboard.logs.logLevels.info": "", + "dashboard.logs.logLevels.label": "", + "dashboard.logs.logLevels.notice": "", + "dashboard.logs.logLevels.warning": "", "dashboard.logs.maximize": "", "dashboard.logs.pending": "", "dashboard.logs.restore": "", diff --git a/src/nls/messages_en.json b/src/nls/messages_en.json index 50d636874..a7c756513 100644 --- a/src/nls/messages_en.json +++ b/src/nls/messages_en.json @@ -149,12 +149,18 @@ "dashboard.logo.tooltip": "Meow", "dashboard.logs.downloadButtonTooltip": "Download logs", "dashboard.logs.launchButtonTooltip": "Open logs in a new window", + "dashboard.logs.logLevels.debug": "Debug", + "dashboard.logs.logLevels.error": "Error", + "dashboard.logs.logLevels.info": "Info (default)", + "dashboard.logs.logLevels.label": "Log levels", + "dashboard.logs.logLevels.notice": "Notice", + "dashboard.logs.logLevels.warning": "Warning", "dashboard.logs.maximize": "Maximize", "dashboard.logs.pending": "Final logs pending", "dashboard.logs.restore": "Return to default", "dashboard.logs.scrollToBottom": "Scroll to end of logs", "dashboard.logs.scrollToTop": "Scroll to start of logs", - "dashboard.logs.showTimestamps.label": "Show log timestamps", + "dashboard.logs.showTimestamps.label": "Show timestamps", "dashboard.metadata.dateCreated": "Date created:", "dashboard.metadata.labels": "Labels:", "dashboard.metadata.namespace": "Namespace:", diff --git a/src/nls/messages_es.json b/src/nls/messages_es.json index 75bd5f972..4652ed237 100644 --- a/src/nls/messages_es.json +++ b/src/nls/messages_es.json @@ -149,6 +149,12 @@ "dashboard.logo.tooltip": "", "dashboard.logs.downloadButtonTooltip": "", "dashboard.logs.launchButtonTooltip": "", + "dashboard.logs.logLevels.debug": "", + "dashboard.logs.logLevels.error": "", + "dashboard.logs.logLevels.info": "", + "dashboard.logs.logLevels.label": "", + "dashboard.logs.logLevels.notice": "", + "dashboard.logs.logLevels.warning": "", "dashboard.logs.maximize": "", "dashboard.logs.pending": "", "dashboard.logs.restore": "", diff --git a/src/nls/messages_fr.json b/src/nls/messages_fr.json index b3d4046d5..520e4e57c 100644 --- a/src/nls/messages_fr.json +++ b/src/nls/messages_fr.json @@ -149,6 +149,12 @@ "dashboard.logo.tooltip": "", "dashboard.logs.downloadButtonTooltip": "", "dashboard.logs.launchButtonTooltip": "", + "dashboard.logs.logLevels.debug": "", + "dashboard.logs.logLevels.error": "", + "dashboard.logs.logLevels.info": "", + "dashboard.logs.logLevels.label": "", + "dashboard.logs.logLevels.notice": "", + "dashboard.logs.logLevels.warning": "", "dashboard.logs.maximize": "", "dashboard.logs.pending": "", "dashboard.logs.restore": "", diff --git a/src/nls/messages_it.json b/src/nls/messages_it.json index a591aca33..bae5c6379 100644 --- a/src/nls/messages_it.json +++ b/src/nls/messages_it.json @@ -149,6 +149,12 @@ "dashboard.logo.tooltip": "", "dashboard.logs.downloadButtonTooltip": "", "dashboard.logs.launchButtonTooltip": "", + "dashboard.logs.logLevels.debug": "", + "dashboard.logs.logLevels.error": "", + "dashboard.logs.logLevels.info": "", + "dashboard.logs.logLevels.label": "", + "dashboard.logs.logLevels.notice": "", + "dashboard.logs.logLevels.warning": "", "dashboard.logs.maximize": "", "dashboard.logs.pending": "", "dashboard.logs.restore": "", diff --git a/src/nls/messages_ja.json b/src/nls/messages_ja.json index 0b40b3095..83fb389b7 100644 --- a/src/nls/messages_ja.json +++ b/src/nls/messages_ja.json @@ -149,6 +149,12 @@ "dashboard.logo.tooltip": "ニャー", "dashboard.logs.downloadButtonTooltip": "ログをダウンロード", "dashboard.logs.launchButtonTooltip": "新しいウィンドウでログを開く", + "dashboard.logs.logLevels.debug": "", + "dashboard.logs.logLevels.error": "", + "dashboard.logs.logLevels.info": "", + "dashboard.logs.logLevels.label": "", + "dashboard.logs.logLevels.notice": "", + "dashboard.logs.logLevels.warning": "", "dashboard.logs.maximize": "最大化", "dashboard.logs.pending": "", "dashboard.logs.restore": "デフォルトに戻す", diff --git a/src/nls/messages_ko.json b/src/nls/messages_ko.json index 5e519bfc9..ffe20b4ec 100644 --- a/src/nls/messages_ko.json +++ b/src/nls/messages_ko.json @@ -149,6 +149,12 @@ "dashboard.logo.tooltip": "야옹", "dashboard.logs.downloadButtonTooltip": "로그 다운로드", "dashboard.logs.launchButtonTooltip": "새 창에서 로그 열기", + "dashboard.logs.logLevels.debug": "", + "dashboard.logs.logLevels.error": "", + "dashboard.logs.logLevels.info": "", + "dashboard.logs.logLevels.label": "", + "dashboard.logs.logLevels.notice": "", + "dashboard.logs.logLevels.warning": "", "dashboard.logs.maximize": "최대화", "dashboard.logs.pending": "보류 중인 최종 로그", "dashboard.logs.restore": "기본값으로 돌아가기", diff --git a/src/nls/messages_pt.json b/src/nls/messages_pt.json index babfcffb3..c6198b6c7 100644 --- a/src/nls/messages_pt.json +++ b/src/nls/messages_pt.json @@ -149,6 +149,12 @@ "dashboard.logo.tooltip": "", "dashboard.logs.downloadButtonTooltip": "", "dashboard.logs.launchButtonTooltip": "", + "dashboard.logs.logLevels.debug": "", + "dashboard.logs.logLevels.error": "", + "dashboard.logs.logLevels.info": "", + "dashboard.logs.logLevels.label": "", + "dashboard.logs.logLevels.notice": "", + "dashboard.logs.logLevels.warning": "", "dashboard.logs.maximize": "", "dashboard.logs.pending": "", "dashboard.logs.restore": "", diff --git a/src/nls/messages_zh-Hans.json b/src/nls/messages_zh-Hans.json index 13612239d..b3411660b 100644 --- a/src/nls/messages_zh-Hans.json +++ b/src/nls/messages_zh-Hans.json @@ -149,6 +149,12 @@ "dashboard.logo.tooltip": "Meow", "dashboard.logs.downloadButtonTooltip": "下载日志", "dashboard.logs.launchButtonTooltip": "在新窗口中打开日志", + "dashboard.logs.logLevels.debug": "", + "dashboard.logs.logLevels.error": "", + "dashboard.logs.logLevels.info": "", + "dashboard.logs.logLevels.label": "", + "dashboard.logs.logLevels.notice": "", + "dashboard.logs.logLevels.warning": "", "dashboard.logs.maximize": "最大化", "dashboard.logs.pending": "", "dashboard.logs.restore": "", diff --git a/src/nls/messages_zh-Hant.json b/src/nls/messages_zh-Hant.json index 7dd0f66d9..f12dea08e 100644 --- a/src/nls/messages_zh-Hant.json +++ b/src/nls/messages_zh-Hant.json @@ -149,6 +149,12 @@ "dashboard.logo.tooltip": "", "dashboard.logs.downloadButtonTooltip": "", "dashboard.logs.launchButtonTooltip": "", + "dashboard.logs.logLevels.debug": "", + "dashboard.logs.logLevels.error": "", + "dashboard.logs.logLevels.info": "", + "dashboard.logs.logLevels.label": "", + "dashboard.logs.logLevels.notice": "", + "dashboard.logs.logLevels.warning": "", "dashboard.logs.maximize": "", "dashboard.logs.pending": "", "dashboard.logs.restore": "", diff --git a/src/scss/_carbon.scss b/src/scss/_carbon.scss index 8fc1f5e9f..bad69f1bb 100644 --- a/src/scss/_carbon.scss +++ b/src/scss/_carbon.scss @@ -12,7 +12,8 @@ limitations under the License. */ @use '@carbon/react/scss/config' with ( - $font-path: '@ibm/plex' + $font-path: '@ibm/plex', + $use-per-family-plex: true ); @use '@carbon/react/scss/reset'; diff --git a/src/utils/index.jsx b/src/utils/index.js similarity index 87% rename from src/utils/index.jsx rename to src/utils/index.js index 6d3867d63..f8f084542 100644 --- a/src/utils/index.jsx +++ b/src/utils/index.js @@ -11,9 +11,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { LogsToolbar } from '@tektoncd/dashboard-components'; +import { getExternalLogURL, getPodLog } from '../api'; -import { getExternalLogURL, getPodLog, getPodLogURL } from '../api'; import { get } from '../api/comms'; const buildLocales = import.meta.env.VITE_LOCALES_BUILD.split(','); @@ -150,36 +149,6 @@ export function getViewChangeHandler({ location, navigate }) { }; } -export function getLogsToolbar({ - externalLogsURL, - isMaximized, - isUsingExternalLogs, - stepStatus, - taskRun, - toggleMaximized -}) { - const { container } = stepStatus; - const { namespace } = taskRun.metadata; - const { podName } = taskRun.status; - - const logURL = isUsingExternalLogs - ? getExternalLogURL({ container, externalLogsURL, namespace, podName }) - : getPodLogURL({ - container, - name: podName, - namespace - }); - - return ( - - ); -} - export function formatLocale(locale) { switch (locale) { case 'zh': diff --git a/src/utils/index.test.js b/src/utils/index.test.js index e368fc48a..d438d0876 100644 --- a/src/utils/index.test.js +++ b/src/utils/index.test.js @@ -1,5 +1,5 @@ /* -Copyright 2019-2023 The Tekton Authors +Copyright 2019-2024 The Tekton 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 @@ -19,7 +19,6 @@ import { followLogs, getLocale, getLogsRetriever, - getLogsToolbar, getTheme, getViewChangeHandler, I18N_DEV_KEY, @@ -293,55 +292,6 @@ it('getViewChangeHandler', () => { ); }); -describe('getLogsToolbar', () => { - it('should handle pod logs (default)', () => { - const container = 'fake_container'; - const namespace = 'fake_namespace'; - const podName = 'fake_podname'; - const stepStatus = { container }; - const taskRun = { metadata: { namespace }, status: { podName } }; - vi.spyOn(API, 'getPodLogURL'); - vi.spyOn(API, 'getExternalLogURL'); - - const logsToolbar = getLogsToolbar({ stepStatus, taskRun }); - - expect(API.getExternalLogURL).not.toHaveBeenCalled(); - expect(API.getPodLogURL).toHaveBeenCalledWith({ - container, - name: podName, - namespace - }); - expect(logsToolbar).toBeTruthy(); - }); - - it('should handle external logs', () => { - const container = 'fake_container'; - const externalLogsURL = 'fake_externalLogsURL'; - const namespace = 'fake_namespace'; - const podName = 'fake_podname'; - const stepStatus = { container }; - const taskRun = { metadata: { namespace }, status: { podName } }; - vi.spyOn(API, 'getPodLogURL'); - vi.spyOn(API, 'getExternalLogURL'); - - const logsToolbar = getLogsToolbar({ - externalLogsURL, - isUsingExternalLogs: true, - stepStatus, - taskRun - }); - - expect(API.getPodLogURL).not.toHaveBeenCalled(); - expect(API.getExternalLogURL).toHaveBeenCalledWith({ - container, - externalLogsURL, - namespace, - podName - }); - expect(logsToolbar).toBeTruthy(); - }); -}); - describe('getLocale', () => { it('handles exact matches for supported locales', () => { const locale = 'en';