Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ODC-7384: Update the PipelineRun details page to use Tekton Results API to load all the info #13313

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export type MatchLabels = {
export type Selector = {
matchLabels?: MatchLabels;
matchExpressions?: MatchExpression[];
filterByName?: string;
};

type K8sVerb =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,8 @@
"TektonConfigs": "TektonConfigs",
"TektonHub": "TektonHub",
"TektonHubs": "TektonHubs",
"TektonResult": "TektonResult",
"TektonResults": "TektonResults",
"Pipeline {{status}}": "Pipeline {{status}}",
"Pipeline status is {{status}}. View logs.": "Pipeline status is {{status}}. View logs.",
"Pipeline not started. Start pipeline.": "Pipeline not started. Start pipeline.",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import * as React from 'react';
import { Tooltip } from '@patternfly/react-core';
import { useTranslation } from 'react-i18next';
import { useK8sWatchResource } from '@console/dynamic-plugin-sdk/src/utils/k8s/hooks/useK8sWatchResource';
import { DetailsPage, DetailsPageProps } from '@console/internal/components/factory';
import { KebabAction, navFactory, viewYamlComponent } from '@console/internal/components/utils';
import { FirehoseResult, K8sResourceKind, referenceForModel } from '@console/internal/module/k8s';
import * as SignedPipelinerunIcon from '../../images/signed-badge.svg';
import { PipelineRunModel } from '../../models';
import { PipelineRunKind } from '../../types';
import { usePipelineTechPreviewBadge } from '../../utils/hooks';
import { getPipelineRunKebabActions } from '../../utils/pipeline-actions';
Expand All @@ -16,14 +19,17 @@ import { PipelineRunDetails } from './detail-page-tabs/PipelineRunDetails';
import { PipelineRunLogsWithActiveTask } from './detail-page-tabs/PipelineRunLogs';
import TaskRuns from './detail-page-tabs/TaskRuns';
import PipelineRunEvents from './events/PipelineRunEvents';
import { useTRPipelineRuns } from './hooks/useTektonResults';
import PipelineRunParametersForm from './PipelineRunParametersForm';
import { useMenuActionsWithUserAnnotation } from './triggered-by';

const PipelineRunDetailsPage: React.FC<DetailsPageProps> = (props) => {
const { kindObj, match } = props;
const { kindObj, match, namespace, name } = props;
const { t } = useTranslation();
const operatorVersion = usePipelineOperatorVersion(props.namespace);
const [taskRuns] = useTaskRuns(props.namespace);
const [data, setData] = React.useState<FirehoseResult<K8sResourceKind> | PipelineRunKind>();
const [loaded, setLoaded] = React.useState(false);
const operatorVersion = usePipelineOperatorVersion(namespace);
const [taskRuns] = useTaskRuns(namespace);
const menuActions: KebabAction[] = useMenuActionsWithUserAnnotation(
getPipelineRunKebabActions(operatorVersion, taskRuns, true),
);
Expand All @@ -40,9 +46,30 @@ const PipelineRunDetailsPage: React.FC<DetailsPageProps> = (props) => {
) : (
obj?.metadata?.name
);

const [pipelineRun, pipelineRunLoaded] = useK8sWatchResource<FirehoseResult<K8sResourceKind>>({
kind: referenceForModel(PipelineRunModel),
namespace,
name,
isList: false,
});
const [TRPlrs, TRPlrsLoaded] = useTRPipelineRuns(namespace, {
selector: { filterByName: name },
});

React.useEffect(() => {
setData(pipelineRun || TRPlrs[0]);
setLoaded(pipelineRunLoaded || TRPlrsLoaded);
}, [TRPlrs, pipelineRun, pipelineRunLoaded, TRPlrsLoaded]);
Comment on lines +50 to +63
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can extract this logic as a separate hook called usePipelineRun, so that this hook can be used elsewhere in the codebase as well.


return (
<DetailsPage
{...props}
obj={{
data,
loaded,
loadError: undefined,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we pass the actual error object here?

}}
badge={badge}
menuActions={menuActions}
getResourceStatus={pipelineRunStatus}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
import { SemVer } from 'semver';
import { useK8sWatchResource } from '@console/dynamic-plugin-sdk/src/utils/k8s/hooks/useK8sWatchResource';
import { DetailsPage } from '@console/internal/components/factory';
import { referenceForModel } from '@console/internal/module/k8s';
import { PipelineRunModel } from '../../../models';
Expand All @@ -19,6 +20,10 @@ type PipelineRunDetailsPageProps = React.ComponentProps<typeof PipelineRunDetail
const i18nNS = 'public';
const i18nPipelineNS = 'pipelines-plugin';

jest.mock('@console/dynamic-plugin-sdk/src/utils/k8s/hooks/useK8sWatchResource', () => ({
useK8sWatchResource: jest.fn(),
}));

describe('PipelineRunDetailsPage:', () => {
let pipelineRunDetailsPageProps: PipelineRunDetailsPageProps;
let wrapper: ShallowWrapper<PipelineRunDetailsPageProps>;
Expand All @@ -38,6 +43,7 @@ describe('PipelineRunDetailsPage:', () => {
menuActions.mockReturnValue([getPipelineRunKebabActions(new SemVer('1.9.0'), [], true)]);
breadCrumbs.mockReturnValue([{ label: 'PipelineRuns' }, { label: 'PipelineRuns Details' }]);
taskRuns.mockReturnValue([]);
(useK8sWatchResource as jest.Mock).mockReturnValue([[], true]);
wrapper = shallow(<PipelineRunDetailsPage {...pipelineRunDetailsPageProps} />);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import { TFunction } from 'i18next';
import * as _ from 'lodash';
import { withTranslation } from 'react-i18next';
import { RouteComponentProps } from 'react-router';
import { Link } from 'react-router-dom';
import { Firehose, resourcePathFromModel } from '@console/internal/components/utils';
import { PipelineRunModel } from '../../../models';
import { PipelineRunKind, TaskRunKind } from '../../../types';
import { pipelineRunFilterReducer } from '../../../utils/pipeline-filter-reducer';
import { WatchK8sResource } from '@console/dynamic-plugin-sdk';
import { ComputedStatus, PipelineRunKind, PipelineTask, TaskRunKind } from '../../../types';
import { pipelineRunStatus } from '../../../utils/pipeline-filter-reducer';
import { taskRunStatus } from '../../../utils/pipeline-utils';
import { TektonResourceLabel } from '../../pipelines/const';
import { ColoredStatusIcon } from '../../pipelines/detail-page-tabs/pipeline-details/StatusIcon';
import { useTaskRuns } from '../../taskruns/useTaskRuns';
import { useTRTaskRuns } from '../hooks/useTektonResults';
import { ErrorDetailsWithStaticLog } from '../logs/log-snippet-types';
import { getDownloadAllLogsCallback } from '../logs/logs-utils';
import LogsWrapperComponent from '../logs/LogsWrapperComponent';
Expand All @@ -38,8 +38,11 @@ class PipelineRunLogsWithTranslation extends React.Component<
}

componentDidMount() {
const { activeTask, taskRuns } = this.props;
const sortedTaskRuns = this.getSortedTaskRun(taskRuns);
const { activeTask, taskRuns, obj } = this.props;
const sortedTaskRuns = this.getSortedTaskRun(taskRuns, [
...(obj?.status?.pipelineSpec?.tasks || []),
...(obj?.status?.pipelineSpec?.finally || []),
]);
const activeItem = this.getActiveTaskRun(sortedTaskRuns, activeTask);
this.setState({ activeItem });
}
Expand All @@ -48,18 +51,25 @@ class PipelineRunLogsWithTranslation extends React.Component<
UNSAFE_componentWillReceiveProps(nextProps) {
if (this.props.obj !== nextProps.obj || this.props.taskRuns !== nextProps.taskRuns) {
const { activeTask, taskRuns } = this.props;
const sortedTaskRuns = this.getSortedTaskRun(taskRuns);
const sortedTaskRuns = this.getSortedTaskRun(taskRuns, [
...(this.props?.obj?.status?.pipelineSpec?.tasks || []),
...(this.props?.obj?.status?.pipelineSpec?.finally || []),
]);
const activeItem = this.getActiveTaskRun(sortedTaskRuns, activeTask);
this.state.navUntouched && this.setState({ activeItem });
}
}

getActiveTaskRun = (taskRuns: string[], activeTask: string): string =>
activeTask
? taskRuns.find((taskRun) => taskRun.includes(activeTask))
: taskRuns[taskRuns.length - 1];
getActiveTaskRun = (taskRuns: TaskRunKind[], activeTask: string): string => {
const activeTaskRun = activeTask
? taskRuns.find((taskRun) => taskRun.metadata.name.includes(activeTask))
: taskRuns.find((taskRun) => taskRunStatus(taskRun) === ComputedStatus.Failed) ||
taskRuns[taskRuns.length - 1];

getSortedTaskRun = (tRuns: TaskRunKind[]): string[] => {
return activeTaskRun?.metadata.name;
};

getSortedTaskRun = (tRuns: TaskRunKind[], tasks: PipelineTask[]): TaskRunKind[] => {
const taskRuns = tRuns?.sort((a, b) => {
if (_.get(a, ['status', 'completionTime'], false)) {
return b.status?.completionTime &&
Expand All @@ -72,7 +82,15 @@ class PipelineRunLogsWithTranslation extends React.Component<
? 1
: -1;
});
return taskRuns?.map((tr) => tr?.metadata?.name) || [];

const pipelineTaskNames = tasks?.map((t) => t?.name);
return (
taskRuns?.sort(
(c, d) =>
pipelineTaskNames?.indexOf(c?.metadata?.labels?.[TektonResourceLabel.pipelineTask]) -
pipelineTaskNames?.indexOf(d?.metadata?.labels?.[TektonResourceLabel.pipelineTask]),
) || []
);
};

onNavSelect = (item) => {
Expand All @@ -85,76 +103,62 @@ class PipelineRunLogsWithTranslation extends React.Component<
render() {
const { obj, t, taskRuns: tRuns } = this.props;
const { activeItem } = this.state;
const taskRuns = this.getSortedTaskRun(tRuns);
const taskRunFromYaml = tRuns?.reduce((acc, value) => {
acc[value?.metadata?.name] = value;
return acc;
}, {});
const logDetails = getPLRLogSnippet(obj, tRuns) as ErrorDetailsWithStaticLog;
const taskRunNames = this.getSortedTaskRun(tRuns, [
...(obj?.status?.pipelineSpec?.tasks || []),
...(obj?.status?.pipelineSpec?.finally || []),
])?.map((tRun) => tRun.metadata.name);

const taskCount = taskRuns.length;
const logDetails = getPLRLogSnippet(obj, tRuns) as ErrorDetailsWithStaticLog;
const pipelineStatus = pipelineRunStatus(obj);
const taskCount = taskRunNames.length;
const downloadAllCallback =
taskCount > 1
? getDownloadAllLogsCallback(
taskRuns,
taskRunFromYaml,
taskRunNames,
tRuns,
obj.metadata?.namespace,
obj.metadata?.name,
)
: undefined;
const podName = taskRunFromYaml?.[activeItem]?.status?.podName;
const taskName =
taskRunFromYaml?.[activeItem]?.metadata?.labels?.[TektonResourceLabel.pipelineTask] || '-';
const resources = taskCount > 0 &&
podName && [
{
name: podName,
kind: 'Pod',
namespace: obj.metadata.namespace,
prop: `obj`,
isList: false,
},
];
const path = `${resourcePathFromModel(
PipelineRunModel,
obj.metadata.name,
obj.metadata.namespace,
)}/logs/`;
const activeTaskRun = tRuns.find((taskRun) => taskRun.metadata.name === activeItem);
const podName = activeTaskRun?.status?.podName;
const taskName = activeTaskRun?.metadata?.labels?.[TektonResourceLabel.pipelineTask] || '-';
const pipelineRunFinished = pipelineStatus !== ComputedStatus.Running;
const resources: WatchK8sResource = taskCount > 0 &&
podName && {
name: podName,
kind: 'Pod',
namespace: obj.metadata.namespace,
isList: false,
};
const waitingForPods = !!(activeItem && !resources);

const selectedItemRef = (item: HTMLSpanElement) => {
if (item?.scrollIntoView) {
item.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
};
return (
<div className="odc-pipeline-run-logs">
<div className="odc-pipeline-run-logs__tasklist" data-test-id="logs-tasklist">
{taskCount > 0 ? (
<Nav onSelect={this.onNavSelect} theme="light">
<NavList className="odc-pipeline-run-logs__nav">
{taskRuns.map((task) => {
{taskRunNames.map((taskRunName) => {
const taskRun = tRuns.find((tRun) => tRun.metadata.name === taskRunName);
return (
<NavItem
key={task}
itemId={task}
isActive={activeItem === task}
key={taskRunName}
itemId={taskRunName}
isActive={activeItem === taskRunName}
className="odc-pipeline-run-logs__navitem"
>
<Link
to={
path +
taskRunFromYaml?.[task]?.metadata?.labels?.[
TektonResourceLabel.pipelineTask
] || '-'
}
>
<ColoredStatusIcon
status={pipelineRunFilterReducer(
obj?.status?.taskRuns
? _.get(obj, ['status', 'taskRuns', task])
: tRuns?.find((tr) => tr?.metadata?.name === task),
)}
/>
<span ref={activeItem === taskRunName ? selectedItemRef : undefined}>
<ColoredStatusIcon status={taskRunStatus(taskRun)} />
<span className="odc-pipeline-run-logs__namespan">
{taskRunFromYaml[task]?.metadata?.labels?.[
TektonResourceLabel.pipelineTask
] || '-'}
{taskRun?.metadata?.labels?.[TektonResourceLabel.pipelineTask] || '-'}
</span>
</Link>
</span>
</NavItem>
);
})}
Expand All @@ -168,21 +172,21 @@ class PipelineRunLogsWithTranslation extends React.Component<
</div>
<div className="odc-pipeline-run-logs__container">
{activeItem && resources ? (
<Firehose key={activeItem} resources={resources}>
<LogsWrapperComponent
taskName={taskName}
downloadAllLabel={t('pipelines-plugin~Download all task logs')}
onDownloadAll={downloadAllCallback}
/>
</Firehose>
<LogsWrapperComponent
resource={resources}
taskName={taskName}
downloadAllLabel={t('pipelines-plugin~Download all task logs')}
onDownloadAll={downloadAllCallback}
taskRun={activeTaskRun}
/>
) : (
<div className="odc-pipeline-run-logs__log">
<div className="odc-pipeline-run-logs__logtext">
{_.get(
obj,
['status', 'conditions', 0, 'message'],
t('pipelines-plugin~No logs found'),
)}
<div className="odc-pipeline-run-logs__logtext" data-test-id="task-logs-error">
{waitingForPods && !pipelineRunFinished && `Waiting for ${taskName} task to start `}
{!resources &&
pipelineRunFinished &&
!obj.status &&
t('pipelines-plugin~No logs found')}
{logDetails && (
<div className="odc-pipeline-run-logs__logsnippet">
{logDetails.staticMessage}
Expand All @@ -208,11 +212,22 @@ export const PipelineRunLogsWithActiveTask: React.FC<PipelineRunLogsWithActiveTa
obj,
params,
}) => {
const [data, setData] = React.useState<TaskRunKind[]>();
const [loaded, setLoaded] = React.useState(false);
const activeTask = _.get(params, 'match.params.name');
const [taskRuns, taskRunsLoaded] = useTaskRuns(obj?.metadata?.namespace, obj?.metadata?.name);
return (
taskRunsLoaded && <PipelineRunLogs obj={obj} activeTask={activeTask} taskRuns={taskRuns} />
);
const [resultTaskRuns, resultTaskRunsLoaded] = useTRTaskRuns(obj?.metadata?.namespace, {
selector: {
matchLabels: {
[TektonResourceLabel.pipelinerun]: obj?.metadata?.name,
},
},
});
React.useEffect(() => {
setData(taskRuns || resultTaskRuns);
setLoaded(taskRunsLoaded || resultTaskRunsLoaded);
}, [taskRuns, resultTaskRuns, taskRunsLoaded, resultTaskRunsLoaded]);
return loaded && <PipelineRunLogs obj={obj} activeTask={activeTask} taskRuns={data} />;
};

export default PipelineRunLogs;