diff --git a/.storybook/Container.scss b/.storybook/Container.scss index 3efe68e8b..b10c91aef 100644 --- a/.storybook/Container.scss +++ b/.storybook/Container.scss @@ -1,5 +1,5 @@ /* -Copyright 2019-2024 The Tekton Authors +Copyright 2019-2026 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 @@ -28,3 +28,9 @@ limitations under the License. background-color: inherit !important; } } + +[role="main"] > .tkn--task-logs { + .cds--accordion__heading { + inset-block-start: 0; // demonstrate the sticky header behaviour when displayed in isolation + } +} diff --git a/packages/components/src/components/TaskTree/_TaskTree.scss b/packages/components/src/components/AccordionItem/AccordionItem.jsx similarity index 62% rename from packages/components/src/components/TaskTree/_TaskTree.scss rename to packages/components/src/components/AccordionItem/AccordionItem.jsx index 126b47f04..5ab0f1763 100644 --- a/packages/components/src/components/TaskTree/_TaskTree.scss +++ b/packages/components/src/components/AccordionItem/AccordionItem.jsx @@ -1,5 +1,5 @@ /* -Copyright 2019-2024 The Tekton Authors +Copyright 2025-2026 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 @@ -11,7 +11,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -.tkn--task-tree { - inline-size: 21%; - min-inline-size: 15rem; +import { AccordionItem as CarbonAccordionItem } from '@carbon/react'; + +function AccordionItem({ children, open, ...rest }) { + return ( + + {open ? children : null} + + ); } + +export default AccordionItem; diff --git a/packages/components/src/components/AccordionItem/AccordionItem.stories.jsx b/packages/components/src/components/AccordionItem/AccordionItem.stories.jsx new file mode 100644 index 000000000..76803e0e5 --- /dev/null +++ b/packages/components/src/components/AccordionItem/AccordionItem.stories.jsx @@ -0,0 +1,75 @@ +/* +Copyright 2026 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 { Accordion } from '@carbon/react'; + +import AccordionItem from './AccordionItem'; + +export default { + component: AccordionItem, + title: 'AccordionItem' +}; + +export const Closed = { + args: { + open: false, + title: 'Closed Accordion Item', + children:
This content is not rendered when closed
+ }, + decorators: [ + Story => ( + + + + ) + ] +}; + +export const Open = { + args: { + open: true, + title: 'Open Accordion Item', + children: ( +
+

This content is rendered when open

+

+ The AccordionItem optimizes rendering by only rendering children when + open +

+
+ ) + }, + decorators: [ + Story => ( + + + + ) + ] +}; + +export const MultipleItems = { + render: () => ( + + +

Content of first item

+
+ +

Content of second item (not rendered)

+
+ +

Content of third item

+
+
+ ) +}; diff --git a/packages/components/src/components/AccordionItem/AccordionItem.test.jsx b/packages/components/src/components/AccordionItem/AccordionItem.test.jsx new file mode 100644 index 000000000..4cfd5246c --- /dev/null +++ b/packages/components/src/components/AccordionItem/AccordionItem.test.jsx @@ -0,0 +1,51 @@ +/* +Copyright 2026 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 { render } from '../../utils/test'; + +import AccordionItem from './AccordionItem'; + +describe('AccordionItem', () => { + it('should render children when open is true', () => { + const { queryByText } = render( + +
Test Content
+
+ ); + expect(queryByText('Test Content')).toBeTruthy(); + }); + + it('should not render children when open is false', () => { + const { queryByText } = render( + +
Test Content
+
+ ); + expect(queryByText('Test Content')).toBeFalsy(); + }); + + it('should pass through other props to CarbonAccordionItem', () => { + const { container } = render( + +
Test Content
+
+ ); + const accordionItem = container.querySelector('.custom-class'); + expect(accordionItem).toBeTruthy(); + }); +}); diff --git a/packages/components/src/components/TaskTree/index.js b/packages/components/src/components/AccordionItem/index.js similarity index 87% rename from packages/components/src/components/TaskTree/index.js rename to packages/components/src/components/AccordionItem/index.js index 1655a5b0c..665a5e1ad 100644 --- a/packages/components/src/components/TaskTree/index.js +++ b/packages/components/src/components/AccordionItem/index.js @@ -1,5 +1,5 @@ /* -Copyright 2019-2021 The Tekton Authors +Copyright 2026 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 @@ -12,4 +12,4 @@ limitations under the License. */ /* istanbul ignore file */ -export { default } from './TaskTree'; +export { default } from './AccordionItem'; diff --git a/packages/components/src/components/DetailsHeader/_DetailsHeader.scss b/packages/components/src/components/DetailsHeader/_DetailsHeader.scss index ca1a7ad71..df994919e 100644 --- a/packages/components/src/components/DetailsHeader/_DetailsHeader.scss +++ b/packages/components/src/components/DetailsHeader/_DetailsHeader.scss @@ -42,12 +42,6 @@ header.tkn--step-details-header { .#{$prefix}--btn-set { margin-inline: 0.5rem; align-self: center; - align-items: baseline; - - .cds--btn--icon-only { - inline-size: 2rem; - block-size: 2rem; - } } .tkn--run-details-name { diff --git a/packages/components/src/components/Log/_Log.scss b/packages/components/src/components/Log/_Log.scss index 4f8168594..75c2ae35f 100644 --- a/packages/components/src/components/Log/_Log.scss +++ b/packages/components/src/components/Log/_Log.scss @@ -1,5 +1,5 @@ /* -Copyright 2019-2025 The Tekton Authors +Copyright 2019-2026 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 @@ -72,37 +72,7 @@ pre.tkn--log { .#{$prefix}--btn-set { position: absolute; inset-block-start: 0; - inset-inline-end: var(--tkn-log-inline-padding); - align-items: center; - } - - .button-container { - position: absolute; - clip-path: inset(0); // ensure the children with position:fixed are not shown outside this element. - 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: var(--tkn-log-inline-padding); - } - - .#{$prefix}--btn--ghost, - .#{$prefix}--copy-btn { - inline-size: 2rem; - block-size: 2rem; - min-block-size: 2rem; - background-color: $background; - - &:hover { - background-color: $layer-hover; - } - - &:focus { - outline-color: white; - } - - svg { - fill: $icon-primary; - } + inset-inline-end: 0.5rem; // var(--tkn-log-inline-padding); } .tkn--log-trailer { diff --git a/packages/components/src/components/LogsToolbar/LogsToolbar.jsx b/packages/components/src/components/LogsToolbar/LogsToolbar.jsx index 3df54b781..bf6538e33 100644 --- a/packages/components/src/components/LogsToolbar/LogsToolbar.jsx +++ b/packages/components/src/components/LogsToolbar/LogsToolbar.jsx @@ -1,5 +1,5 @@ /* -Copyright 2020-2025 The Tekton Authors +Copyright 2020-2026 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 @@ -12,98 +12,51 @@ limitations under the License. */ /* istanbul ignore file */ import { useIntl } from 'react-intl'; +import { Maximize, Minimize, Settings } from '@carbon/react/icons'; import { - Download, - Launch, - Maximize, - Minimize, - Settings -} from '@carbon/react/icons'; -import { + Button, + ButtonSet, Checkbox, CheckboxGroup, Popover, - PopoverContent, - usePrefix + PopoverContent } from '@carbon/react'; import { useState } from 'react'; const LogsToolbar = ({ id, isMaximized, - name, logLevels, onToggleShowTimestamps, onToggleLogLevel, onToggleMaximized, - showTimestamps, - url + showTimestamps }) => { - const carbonPrefix = usePrefix(); const intl = useIntl(); const [isSettingsOpen, setIsSettingsOpen] = useState(false); return ( -
+ {onToggleMaximized ? ( - - ) : null} - {url ? ( - <> - - - - {intl.formatMessage({ - id: 'dashboard.logs.launchButtonTooltip', - defaultMessage: 'Open logs in a new window' - })} - - - - - - - {intl.formatMessage({ - id: 'dashboard.logs.downloadButtonTooltip', - defaultMessage: 'Download logs' - })} - - - - + }) + } + kind="ghost" + onClick={onToggleMaximized} + renderIcon={isMaximized ? Minimize : Maximize} + size="sm" + /> ) : null} - + renderIcon={Settings} + size="sm" + /> -
+ ); }; diff --git a/packages/components/src/components/LogsToolbar/LogsToolbar.stories.jsx b/packages/components/src/components/LogsToolbar/LogsToolbar.stories.jsx index d0daec77f..7f51445c3 100644 --- a/packages/components/src/components/LogsToolbar/LogsToolbar.stories.jsx +++ b/packages/components/src/components/LogsToolbar/LogsToolbar.stories.jsx @@ -1,5 +1,5 @@ /* -Copyright 2019-2025 The Tekton Authors +Copyright 2019-2026 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 @@ -97,27 +97,3 @@ export const WithMaximize = { ); } }; - -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 ef35ce7fd..13978c76d 100644 --- a/packages/components/src/components/PipelineRun/PipelineRun.jsx +++ b/packages/components/src/components/PipelineRun/PipelineRun.jsx @@ -11,25 +11,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Fragment, useState } from 'react'; +import { useState } from 'react'; import { InlineNotification, SkeletonText, TabsVertical } from '@carbon/react'; import { useIntl } from 'react-intl'; import { getErrorMessage, getStatus, - getStepDefinition, - getStepStatus, labels as labelConstants } from '@tektoncd/dashboard-utils'; import Log from '../Log'; -import Portal from '../Portal'; import RunHeader from '../RunHeader'; -import StepDetails from '../StepDetails'; -import TaskRunDetails from '../TaskRunDetails'; import TaskRunTabPanels from '../TaskRunTabPanels'; import TaskRunTabs from '../TaskRunTabs'; -import TaskTree from '../TaskTree'; function getPipelineTask({ pipeline, pipelineRun, selectedTaskId, taskRun }) { const memberOf = taskRun?.metadata?.labels?.[labelConstants.MEMBER_OF]; @@ -48,17 +42,16 @@ export default /* istanbul ignore next */ function PipelineRun({ duration, enableLogAutoScroll, enableLogScrollButtons, - enableTabLayout, error, fetchLogs, forceLogPolling, getLogsToolbar, + getStepLogToolbar, handlePipelineRunInfo = () => {}, handleTaskSelected = /* istanbul ignore next */ () => {}, ignoredSidecars = {}, loading, logLevels, - maximizedLogsContainer, onRetryChange, onViewChange = /* istanbul ignore next */ () => {}, pipeline, @@ -79,7 +72,7 @@ export default /* istanbul ignore next */ function PipelineRun({ view = null }) { const intl = useIntl(); - const [isLogsMaximized, setIsLogsMaximized] = useState(false); + const [isLogsMaximized] = useState(false); const [isTaskRunMaximized, setIsTaskRunMaximized] = useState(false); const [expandedSteps, setExpandedSteps] = useState(() => selectedStepId ? { [selectedStepId]: true } : {} @@ -106,60 +99,35 @@ export default /* istanbul ignore next */ function PipelineRun({ return status === 'False' && reason !== 'Cancelled'; } - function onToggleLogsMaximized() { - setIsLogsMaximized(prevIsLogsMaximized => !prevIsLogsMaximized); - } - function onToggleTaskRunMaximized() { setIsTaskRunMaximized(prevIsTaskRunMaximized => !prevIsTaskRunMaximized); } - function getLogContainer({ - disableLogsToolbar, - isSidecar, - stepName, - stepStatus, - taskRun - }) { + function getLogContainer({ isSidecar, stepName, stepStatus, taskRun }) { if ((!selectedStepId && !stepName) || !stepStatus) { return null; } - const LogsRoot = - isLogsMaximized && maximizedLogsContainer ? Portal : Fragment; - return ( - - fetchLogs({ stepName, stepStatus, taskRun })} - forcePolling={forceLogPolling} - isLogsMaximized={isLogsMaximized} - isSidecar={isSidecar} - key={`${selectedTaskId}:${stepName}:${selectedRetry}`} - logLevels={logLevels} - pollingInterval={pollingInterval} - showLevels={showLogLevels} - showTimestamps={showLogTimestamps} - stepStatus={stepStatus} - toolbar={ - !disableLogsToolbar && - getLogsToolbar && - stepStatus && - getLogsToolbar({ - id: `${selectedTaskId}-${stepName}-${selectedRetry}-logs-toolbar`, - isMaximized: isLogsMaximized, - onToggleMaximized: - !!maximizedLogsContainer && onToggleLogsMaximized, - stepStatus, - taskRun - }) - } - /> - + fetchLogs({ stepName, stepStatus, taskRun })} + forcePolling={forceLogPolling} + isLogsMaximized={isLogsMaximized} + isSidecar={isSidecar} + key={`${selectedTaskId}:${stepName}:${selectedRetry}`} + logLevels={logLevels} + pollingInterval={pollingInterval} + showLevels={showLogLevels} + showTimestamps={showLogTimestamps} + stepStatus={stepStatus} + toolbar={ + getStepLogToolbar && + stepStatus && + getStepLogToolbar({ stepStatus, taskRun }) + } + /> ); } @@ -338,33 +306,12 @@ export default /* istanbul ignore next */ function PipelineRun({ tasks?.find(t => t.metadata.name === taskRun.spec.taskRef.name)) || {}; - const stepStatus = getStepStatus({ - selectedStepId, - taskRun - }); - - let definition; - let logContainer; - if (!enableTabLayout) { - definition = getStepDefinition({ - selectedStepId, - task, - taskRun - }); - - logContainer = getLogContainer({ - stepName: selectedStepId, - stepStatus, - taskRun - }); - } - const skippedTasks = pipelineRun.status?.skippedTasks || []; const skippedTask = skippedTasks.find( skipped => skipped.name === selectedTaskId ); - if (enableTabLayout && !selectedTaskId) { + if (!selectedTaskId) { const selectedTaskRun = taskRunsToUse[0]; const { labels = {} } = selectedTaskRun?.metadata || {}; const { [labelConstants.PIPELINE_TASK]: pipelineTaskName } = labels; @@ -399,93 +346,55 @@ export default /* istanbul ignore next */ function PipelineRun({ {(taskRunsToUse.length > 0 || preTaskRun) && (
- {enableTabLayout ? ( - { + if (preTaskRun && newSelectedIndex === 0) { + onTaskSelected({}); + return; + } + + const selectedTaskRun = + taskRunsToUse[newSelectedIndex - preTaskRunOffset]; + const { labels } = selectedTaskRun.metadata; + const { [labelConstants.PIPELINE_TASK]: pipelineTaskName } = + labels; + + setExpandedSteps({}); + onTaskSelected({ + selectedTaskId: pipelineTaskName, + taskRunName: selectedTaskRun.metadata?.name + }); + }} + > + + { - if (preTaskRun && newSelectedIndex === 0) { - onTaskSelected({}); - return; - } - - const selectedTaskRun = - taskRunsToUse[newSelectedIndex - preTaskRunOffset]; - const { labels } = selectedTaskRun.metadata; - const { [labelConstants.PIPELINE_TASK]: pipelineTaskName } = - labels; - - setExpandedSteps({}); - onTaskSelected({ - selectedTaskId: pipelineTaskName, - taskRunName: selectedTaskRun.metadata?.name - }); - }} - > - - - - ) : ( - <> - - {(selectedStepId && ( - - )) || - (selectedTaskId && ( - - ))} - - )} + selectedRetry={selectedRetry} + selectedTaskId={selectedTaskId} + selectedStepId={selectedStepId} + skippedTask={skippedTask} + task={task} + taskRun={taskRun} + taskRuns={taskRunsToUse} + view={view} + /> +
)} diff --git a/packages/components/src/components/PipelineRun/PipelineRun.stories.jsx b/packages/components/src/components/PipelineRun/PipelineRun.stories.jsx index 20519603c..66387f79f 100644 --- a/packages/components/src/components/PipelineRun/PipelineRun.stories.jsx +++ b/packages/components/src/components/PipelineRun/PipelineRun.stories.jsx @@ -1,5 +1,5 @@ /* -Copyright 2019-2025 The Tekton Authors +Copyright 2019-2026 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 @@ -231,7 +231,6 @@ const pipelineRunWithMinimalStatus = { export default { args: { - enableTabLayout: false, selectedRetry: '', selectedStepId: undefined, selectedTaskId: undefined, diff --git a/packages/components/src/components/PipelineRun/PipelineRun.test.jsx b/packages/components/src/components/PipelineRun/PipelineRun.test.jsx index d94c23fb5..01e416c4f 100644 --- a/packages/components/src/components/PipelineRun/PipelineRun.test.jsx +++ b/packages/components/src/components/PipelineRun/PipelineRun.test.jsx @@ -1,5 +1,5 @@ /* -Copyright 2019-2024 The Tekton Authors +Copyright 2019-2026 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 @@ -30,11 +30,15 @@ it('PipelineRunContainer handles init step failures', async () => { const initStepName = 'my-failed-init-step'; const pipelineRunName = 'fake_pipelineRunName'; const taskRunName = 'fake_taskRunName'; + const pipelineTaskName = 'fake_pipelineTaskName'; + const handleTaskSelected = vi.fn(); const taskRun = { metadata: { name: taskRunName, - labels: {}, + labels: { + 'tekton.dev/pipelineTask': pipelineTaskName + }, uid: '41deea80-53bc-4500-a20e-3b18549e23ab' }, spec: { @@ -60,15 +64,22 @@ it('PipelineRunContainer handles init step failures', async () => { } }; - const { getByText } = render( + render( {}} + handleTaskSelected={handleTaskSelected} pipelineRun={pipelineRun} taskRuns={[taskRun]} tasks={[]} /> ); - await waitFor(() => getByText(initStepName)); + await waitFor(() => + expect(handleTaskSelected).toHaveBeenCalledWith({ + selectedRetry: undefined, + selectedStepId: undefined, + selectedTaskId: pipelineTaskName, + taskRunName: undefined + }) + ); }); it('PipelineRunContainer handles init step failures for retry', async () => { @@ -78,7 +89,9 @@ it('PipelineRunContainer handles init step failures for retry', async () => { const taskRun = { metadata: { - labels: {}, + labels: { + 'tekton.dev/pipelineTask': 'task-1' + }, name: taskRunName, uid: 'b4402feb-69fe-4a5a-a0c2-5e390aa58894' }, @@ -115,7 +128,6 @@ it('PipelineRunContainer handles init step failures for retry', async () => { ] } }; - const pipelineRun = { metadata: { name: pipelineRunName @@ -123,7 +135,8 @@ it('PipelineRunContainer handles init step failures for retry', async () => { status: { childReferences: [ { - name: taskRunName + name: taskRunName, + pipelineTaskName: 'task-1' } ] } @@ -132,21 +145,21 @@ it('PipelineRunContainer handles init step failures for retry', async () => { class TestWrapper extends ReactComponent { state = { selectedStepId: null, - selectedTaskId: null + selectedTaskId: 'task-1' }; - handleTaskSelected = ({ selectedStepId, selectedTaskId }) => { - this.setState({ selectedStepId, selectedTaskId }); + handleTaskSelected = ({ selectedTaskId }) => { + this.setState({ + selectedTaskId: selectedTaskId ?? this.state.selectedTaskId + }); }; render() { const { children: Component } = this.props; - const { selectedStepId, selectedTaskId } = this.state; return ( ); } @@ -154,12 +167,11 @@ it('PipelineRunContainer handles init step failures for retry', async () => { const { getByText } = render( - {({ handleTaskSelected, selectedStepId, selectedTaskId }) => ( + {({ handleTaskSelected, selectedTaskId }) => ( {}} handleTaskSelected={handleTaskSelected} pipelineRun={pipelineRun} - selectedStepId={selectedStepId} + selectedRetry="0" selectedTaskId={selectedTaskId} taskRuns={[taskRun]} tasks={[]} @@ -167,6 +179,5 @@ it('PipelineRunContainer handles init step failures for retry', async () => { )} ); - await waitFor(() => getByText(/(retry 1)/)); - await waitFor(() => getByText(initStepName, { selector: '.tkn--step-name' })); + await waitFor(() => getByText('task-1')); }); diff --git a/packages/components/src/components/Step/Step.jsx b/packages/components/src/components/Step/Step.jsx deleted file mode 100644 index 2a705f1ff..000000000 --- a/packages/components/src/components/Step/Step.jsx +++ /dev/null @@ -1,127 +0,0 @@ -/* -Copyright 2019-2025 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 { useIntl } from 'react-intl'; -import { Pending as DefaultIcon } from '@carbon/react/icons'; - -import StatusIcon from '../StatusIcon'; - -export default function Step({ - exitCode, - id, - onSelect, - reason, - selected, - status, - stepName = 'unknown', - terminationReason -}) { - const intl = useIntl(); - - function handleClick(event) { - event.preventDefault(); - onSelect(id); - } - - function getStatusLabel() { - if (terminationReason === 'Skipped') { - return intl.formatMessage({ - id: 'dashboard.taskRun.status.skipped', - defaultMessage: 'Skipped' - }); - } - if ( - status === 'cancelled' || - (status === 'terminated' && - (reason === 'TaskRunCancelled' || reason === 'TaskRunTimeout')) - ) { - return intl.formatMessage({ - id: 'dashboard.taskRun.status.cancelled', - defaultMessage: 'Cancelled' - }); - } - - if (status === 'running') { - return intl.formatMessage({ - id: 'dashboard.taskRun.status.running', - defaultMessage: 'Running' - }); - } - - if (status === 'terminated') { - if (reason === 'Completed') { - return exitCode !== 0 - ? intl.formatMessage( - { - id: 'dashboard.taskRun.status.succeeded.warning', - defaultMessage: 'Completed with exit code {exitCode}' - }, - { exitCode } - ) - : intl.formatMessage({ - id: 'dashboard.taskRun.status.succeeded', - defaultMessage: 'Completed' - }); - } - return intl.formatMessage({ - id: 'dashboard.taskRun.status.failed', - defaultMessage: 'Failed' - }); - } - - if (status === 'waiting') { - return intl.formatMessage({ - id: 'dashboard.taskRun.status.waiting', - defaultMessage: 'Waiting' - }); - } - - return intl.formatMessage({ - id: 'dashboard.taskRun.status.notRun', - defaultMessage: 'Not run' - }); - } - - const statusLabel = getStatusLabel(); - - return ( -
  • - e.key === 'Enter' && handleClick(e)} - role="button" - > - } - hasWarning={exitCode !== 0} - reason={reason} - status={status} - terminationReason={terminationReason} - title={statusLabel} - type="inverse" - /> - - {stepName} - - -
  • - ); -} diff --git a/packages/components/src/components/Step/Step.stories.js b/packages/components/src/components/Step/Step.stories.js deleted file mode 100644 index 6fe032753..000000000 --- a/packages/components/src/components/Step/Step.stories.js +++ /dev/null @@ -1,53 +0,0 @@ -/* -Copyright 2019-2025 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 { action } from 'storybook/actions'; - -import Step from './Step'; - -export default { - args: { - exitCode: 0, - onSelect: action('selected'), - stepName: 'build' - }, - component: Step, - title: 'Step' -}; - -export const Default = {}; -export const Selected = { args: { selected: true } }; -export const Waiting = { args: { status: 'waiting' } }; -export const Running = { args: { status: 'running' } }; - -export const Completed = { - args: { reason: 'Completed', status: 'terminated' } -}; - -export const CompletedWithWarning = { - args: { - ...Completed.args, - exitCode: 1 - }, - name: 'Completed with warning' -}; - -export const Skipped = { - args: { - reason: 'Completed', - status: 'terminated', - terminationReason: 'Skipped' - } -}; - -export const Error = { args: { reason: 'Error', status: 'terminated' } }; diff --git a/packages/components/src/components/Step/Step.test.jsx b/packages/components/src/components/Step/Step.test.jsx deleted file mode 100644 index c7cfdbe48..000000000 --- a/packages/components/src/components/Step/Step.test.jsx +++ /dev/null @@ -1,99 +0,0 @@ -/* -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 { fireEvent } from '@testing-library/react'; -import Step from './Step'; -import { render } from '../../utils/test'; - -it('Step renders default content', () => { - const { queryByText } = render(); - - expect(queryByText(/Unknown/i)).toBeTruthy(); -}); - -it('Step renders the provided content', () => { - const stepName = 'build'; - const { queryByText } = render(); - expect(queryByText(/build/i)).toBeTruthy(); -}); - -it('Step renders selected state', () => { - render(); -}); - -it('Step renders waiting state', () => { - const { queryByText } = render(); - expect(queryByText(/waiting/i)).toBeTruthy(); -}); - -it('Step renders running state', () => { - const { queryByText } = render(); - expect(queryByText(/running/i)).toBeTruthy(); -}); - -it('Step renders cancelled state', () => { - const { queryByText } = render(); - expect(queryByText(/Cancelled/i)).toBeTruthy(); -}); - -it('Step renders cancelled state for TaskRunCancelled', () => { - const { queryByText } = render( - - ); - expect(queryByText(/Cancelled/i)).toBeTruthy(); -}); - -it('Step renders cancelled state for TaskRunTimeout', () => { - const { queryByText } = render( - - ); - expect(queryByText(/Cancelled/i)).toBeTruthy(); -}); - -it('Step renders completed state', () => { - const { queryByText } = render( - - ); - expect(queryByText(/Completed/i)).toBeTruthy(); -}); - -it('Step renders completed with warning state', () => { - const { queryByText } = render( - - ); - expect(queryByText(/Completed with exit code 1/i)).toBeTruthy(); -}); - -it('Step renders skipped state', () => { - const { queryByText } = render( - - ); - expect(queryByText(/Skipped/i)).toBeTruthy(); -}); - -it('Step renders error state', () => { - const { queryByText } = render(); - expect(queryByText(/Failed/i)).toBeTruthy(); -}); - -it('Step handles click event', () => { - const onSelect = vi.fn(); - const { getByText } = render(); - fireEvent.click(getByText(/build/i)); - expect(onSelect).toHaveBeenCalledTimes(1); -}); diff --git a/packages/components/src/components/Step/_Step.scss b/packages/components/src/components/Step/_Step.scss index 1b17a505a..2d7eb27f4 100644 --- a/packages/components/src/components/Step/_Step.scss +++ b/packages/components/src/components/Step/_Step.scss @@ -1,5 +1,5 @@ /* -Copyright 2019-2024 The Tekton Authors +Copyright 2019-2026 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 @@ -12,42 +12,8 @@ limitations under the License. */ @use '@carbon/react/scss/theme' as *; -@use '@carbon/react/scss/type' as *; .tkn--step { - list-style-type: none; - - &:hover > .tkn--step-link, - &:hover > .tkn--step-link:hover, - &[data-selected] > .tkn--step-link:hover { - background-color: $layer-hover; - text-decoration: none; - } - - &:hover > .tkn--step-link, - > .tkn--step-link:hover { - border-inline-start-color: $layer-hover; - } - - &[data-selected] > .tkn--step-link { - border-inline-start: 3px solid $border-interactive; - } - - > .tkn--step-link { - cursor: pointer; - display: flex; - align-items: baseline; - position: relative; - padding-block: 0; - padding-inline-start: 0.75rem; - padding-inline-end: 1rem; - line-height: 2.2rem; - font-size: 0.78rem; - letter-spacing: 0.02rem; - text-decoration: none; - border-inline-start: 3px solid $layer; - } - .tkn--status-icon { flex-shrink: 0; align-self: center; diff --git a/packages/components/src/components/StepDetails/StepDetails.jsx b/packages/components/src/components/StepDetails/StepDetails.jsx deleted file mode 100644 index 3d5b1e1e4..000000000 --- a/packages/components/src/components/StepDetails/StepDetails.jsx +++ /dev/null @@ -1,113 +0,0 @@ -/* -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 PropTypes from 'prop-types'; -import { useIntl } from 'react-intl'; -import { getStatus, getStepStatusReason } from '@tektoncd/dashboard-utils'; -import { Tab, TabList, TabPanel, TabPanels, Tabs } from '@carbon/react'; - -import DetailsHeader from '../DetailsHeader'; -import Log from '../Log'; -import StepDefinition from '../StepDefinition'; - -const tabs = ['logs', 'details']; - -const defaults = { - onViewChange: /* istanbul ignore next */ () => {}, - taskRun: {} -}; - -const StepDetails = ({ - definition, - logContainer, - onViewChange = defaults.onViewChange, - skippedTask, - stepName, - stepStatus, - taskRun = defaults.taskRun, - view -}) => { - const intl = useIntl(); - const { exitCode, reason, status } = getStepStatusReason(stepStatus); - const statusValue = - getStatus(taskRun).reason === 'TaskRunCancelled' && status !== 'terminated' - ? 'cancelled' - : status; - - let selectedTabIndex = tabs.indexOf(view); - if (selectedTabIndex === -1) { - selectedTabIndex = 0; - } - - return ( -
    - - onViewChange(tabs[event.selectedIndex])} - selectedIndex={selectedTabIndex} - > - - - {intl.formatMessage({ - id: 'dashboard.taskRun.logs', - defaultMessage: 'Logs' - })} - - - {intl.formatMessage({ - id: 'dashboard.resource.detailsTab', - defaultMessage: 'Details' - })} - - - - - {selectedTabIndex === 0 && skippedTask ? ( - - intl.formatMessage({ - id: 'dashboard.taskRun.logs.skipped', - defaultMessage: - 'This step did not run as the task was skipped. See status for more details.' - }) - } - /> - ) : ( - logContainer - )} - - - {selectedTabIndex === 1 && ( - - )} - - - -
    - ); -}; - -StepDetails.propTypes = { - onViewChange: PropTypes.func, - taskRun: PropTypes.shape({}) -}; - -export default StepDetails; diff --git a/packages/components/src/components/StepDetails/StepDetails.stories.jsx b/packages/components/src/components/StepDetails/StepDetails.stories.jsx deleted file mode 100644 index b1f22a12c..000000000 --- a/packages/components/src/components/StepDetails/StepDetails.stories.jsx +++ /dev/null @@ -1,119 +0,0 @@ -/* -Copyright 2019-2025 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 Log from '../Log'; -import StepDetails from './StepDetails'; - -function getStepStatus({ exitCode = 0, terminationReason } = {}) { - return { terminated: { exitCode, reason: 'Completed' }, terminationReason }; -} - -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'; - -function getLogContainer({ - exitCode = 0, - logContent = ansiLog, - terminationReason -} = {}) { - return ( - logContent} - stepStatus={getStepStatus({ exitCode, terminationReason })} - /> - ); -} - -export default { - args: { - definition: 'this will show the Task.spec or TaskRun.spec.taskSpec', - stepName: 'build', - taskRun: {} - }, - component: StepDetails, - subcomponents: { Log }, - title: 'StepDetails' -}; - -export const Default = { - args: { - logContainer: getLogContainer(), - stepStatus: getStepStatus() - }, - render: args => { - const [, updateArgs] = useArgs(); - return ( - updateArgs({ view: selectedView })} - /> - ); - } -}; - -export const WithWarning = { - args: { - logContainer: getLogContainer({ exitCode: 1 }), - stepStatus: getStepStatus({ exitCode: 1 }) - }, - render: args => { - const [, updateArgs] = useArgs(); - return ( - updateArgs({ view: selectedView })} - /> - ); - } -}; - -export const SkippedTask = { - args: { - logContainer: getLogContainer(), - skippedTask: {} - }, - render: args => { - const [, updateArgs] = useArgs(); - return ( - updateArgs({ view: selectedView })} - /> - ); - } -}; - -export const SkippedStep = { - args: { - logContainer: getLogContainer({ - logContent: - 'Step was skipped due to when expressions were evaluated to false.', - terminationReason: 'Skipped' - }), - stepStatus: { - terminated: { exitCode: 0, reason: 'Completed' }, - terminationReason: 'Skipped' - } - }, - render: args => { - const [, updateArgs] = useArgs(); - return ( - updateArgs({ view: selectedView })} - /> - ); - } -}; diff --git a/packages/components/src/components/StepDetails/StepDetails.test.jsx b/packages/components/src/components/StepDetails/StepDetails.test.jsx deleted file mode 100644 index 7cef08880..000000000 --- a/packages/components/src/components/StepDetails/StepDetails.test.jsx +++ /dev/null @@ -1,64 +0,0 @@ -/* -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 { fireEvent, waitFor } from '@testing-library/react'; -import { renderWithRouter } from '../../utils/test'; - -import StepDetails from './StepDetails'; - -describe('StepDetails', () => { - it('renders', () => { - renderWithRouter(); - }); - - it('renders terminated state', () => { - renderWithRouter( - - ); - }); - - it('renders cancelled state', () => { - renderWithRouter( - - ); - }); - - it('renders with selected view', () => { - const { getByText } = renderWithRouter( - - ); - - fireEvent.click(getByText(/logs/i)); - }); - - it('renders skipped Task state', async () => { - const { getByText } = renderWithRouter( - - ); - - await waitFor(() => getByText(/task was skipped/i)); - }); -}); diff --git a/packages/components/src/components/StepLogToolbar/StepLogToolbar.jsx b/packages/components/src/components/StepLogToolbar/StepLogToolbar.jsx new file mode 100644 index 000000000..34d59b725 --- /dev/null +++ b/packages/components/src/components/StepLogToolbar/StepLogToolbar.jsx @@ -0,0 +1,55 @@ +/* +Copyright 2020-2026 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. +*/ +/* istanbul ignore file */ + +import { useIntl } from 'react-intl'; +import { Download, Launch } from '@carbon/react/icons'; +import { Button, ButtonSet } from '@carbon/react'; + +const StepLogToolbar = ({ name, url }) => { + const intl = useIntl(); + + return ( + +