Skip to content

Commit

Permalink
Added ApprovalTask Node to the Pipeline Visualization
Browse files Browse the repository at this point in the history
  • Loading branch information
Lucifergene committed Mar 8, 2024
1 parent d6e2e1e commit 1f4e00f
Show file tree
Hide file tree
Showing 15 changed files with 706 additions and 4 deletions.
Expand Up @@ -323,11 +323,13 @@
"Represents the location of the blob storage i.e gs://some-private-bucket": "Represents the location of the blob storage i.e gs://some-private-bucket",
"Directory": "Directory",
"Represents whether the blob storage is a directory or not": "Represents whether the blob storage is a directory or not",
"Approval Task": "Approval Task",
"Task does not exist": "Task does not exist",
"Add finally task": "Add finally task",
"Add a sequential task after this task": "Add a sequential task after this task",
"Add a sequential task before this task": "Add a sequential task before this task",
"Add a parallel task": "Add a parallel task",
"Custom Task": "Custom Task",
"Installing": "Installing",
"Add task": "Add task",
"No tasks": "No tasks",
Expand Down Expand Up @@ -436,6 +438,10 @@
"TaskRun log": "TaskRun log",
"Pod not found": "Pod not found",
"{{taskLabel}} details": "{{taskLabel}} details",
"CustomRun": "CustomRun",
"CustomRuns": "CustomRuns",
"ApprovalTask": "ApprovalTask",
"ApprovalTasks": "ApprovalTasks",
"TektonConfig": "TektonConfig",
"TektonConfigs": "TektonConfigs",
"TektonHub": "TektonHub",
Expand Down Expand Up @@ -465,5 +471,10 @@
"Cancelling": "Cancelling",
"Pending": "Pending",
"PipelineRun not started yet": "PipelineRun not started yet",
"Request sent": "Request sent",
"Approved": "Approved",
"Rejected": "Rejected",
"Timed out": "Timed out",
"Idle": "Idle",
"Other": "Other"
}
Expand Up @@ -4,10 +4,18 @@ import { CheckCircleIcon } from '@patternfly/react-icons/dist/esm/icons/check-ci
import { CircleIcon } from '@patternfly/react-icons/dist/esm/icons/circle-icon';
import { ExclamationCircleIcon } from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon';
import { HourglassHalfIcon } from '@patternfly/react-icons/dist/esm/icons/hourglass-half-icon';
import NewProcessIcon from '@patternfly/react-icons/dist/esm/icons/new-process-icon';
import { QuestionCircleIcon } from '@patternfly/react-icons/dist/esm/icons/question-circle-icon';
import { ResourcesAlmostEmptyIcon } from '@patternfly/react-icons/dist/esm/icons/resources-almost-empty-icon';
import { ResourcesAlmostFullIcon } from '@patternfly/react-icons/dist/esm/icons/resources-almost-full-icon';
import { ResourcesEmptyIcon } from '@patternfly/react-icons/dist/esm/icons/resources-empty-icon';
import { SyncAltIcon } from '@patternfly/react-icons/dist/esm/icons/sync-alt-icon';
import * as cx from 'classnames';
import { YellowExclamationTriangleIcon } from '@console/dynamic-plugin-sdk';
import { ComputedStatus } from '../../../../types';
import { YellowExclamationTriangleIcon } from '@console/dynamic-plugin-sdk/src/app/components/status/icons';
import FailedApprovalTaskIcon from '../../../../images/FailedApprovalTaskIcon';
import SuccessApprovalTaskIcon from '../../../../images/SuccessApprovalTaskIcon';
import TimeoutApprovalTaskIcon from '../../../../images/TimeoutApprovalTaskIcon';
import { ApprovalStatus, ComputedStatus } from '../../../../types';
import { getRunStatusColor } from '../../../../utils/pipeline-augment';

interface StatusIconProps {
Expand Down Expand Up @@ -44,6 +52,27 @@ export const StatusIcon: React.FC<StatusIconProps> = ({ status, disableSpin, ...
}
};

export const ApprovalStatusIcon: React.FC<StatusIconProps> = ({ status, ...others }) => {
switch (status) {
case ApprovalStatus.Idle:
return <NewProcessIcon {...others} />;
case ApprovalStatus.RequestSent:
return <ResourcesEmptyIcon {...others} />;
case 'partially approved (1)':
return <ResourcesAlmostEmptyIcon {...others} />;
case 'partially approved (2)':
return <ResourcesAlmostFullIcon {...others} />;
case ApprovalStatus.Accepted:
return <SuccessApprovalTaskIcon {...others} />;
case ApprovalStatus.Rejected:
return <FailedApprovalTaskIcon {...others} />;
case ApprovalStatus.TimedOut:
return <TimeoutApprovalTaskIcon {...others} />;
default:
return <QuestionCircleIcon {...others} />;
}
};

export const ColoredStatusIcon: React.FC<StatusIconProps> = ({ status, ...others }) => {
return (
<div
Expand Down
@@ -0,0 +1,220 @@
import * as React from 'react';
import { Tooltip } from '@patternfly/react-core';
import { observer, Node, NodeModel, useHover, createSvgIdUrl } from '@patternfly/react-topology';
import * as cx from 'classnames';
import * as _ from 'lodash';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom-v5-compat';
import {
K8sResourceKind,
WatchK8sResults,
getGroupVersionKindForModel,
} from '@console/dynamic-plugin-sdk/src/lib-core';
import { useK8sWatchResources } from '@console/dynamic-plugin-sdk/src/utils/k8s/hooks';
import { resourcePathFromModel, truncateMiddle } from '@console/internal/components/utils';
import { ApprovalTaskModel, CustomRunModelV1Beta1 } from '@console/pipelines-plugin/src/models';
import { pipelineRunFilterReducer } from '@console/pipelines-plugin/src/utils/pipeline-filter-reducer';
import { SvgDropShadowFilter } from '@console/topology/src/components/svg';
import {
TaskKind,
ApprovalTaskKind,
ApprovalStatus,
ComputedStatus,
CustomRunKind,
CustomRunStatus,
} from '../../../types';
import { getApprovalStatusColor } from '../../../utils/pipeline-augment';
import { ApprovalStatusIcon } from '../detail-page-tabs/pipeline-details/StatusIcon';
import { TaskNodeModelData } from './types';

import './CustomTaskNode.scss';

type ApprovalTaskNodeProps = {
element: Node<NodeModel, TaskNodeModelData>;
disableTooltip?: boolean;
};

type WatchResource = {
[key: string]: K8sResourceKind[] | K8sResourceKind;
};

interface ApprovalTaskComponentProps {
pipelineRunName?: string;
name: string;
loaded?: boolean;
task?: {
data: TaskKind;
};
status: string;
namespace: string;
disableVisualizationTooltip?: boolean;
width: number;
height: number;
customTask?: K8sResourceKind;
}

const FILTER_ID = 'SvgTaskDropShadowFilterId';

const ApprovalTaskComponent: React.FC<ApprovalTaskComponentProps> = ({
pipelineRunName,
namespace,
task,
status,
name,
disableVisualizationTooltip,
width,
height,
customTask,
}) => {
const { t } = useTranslation();
const showStatusState: boolean = !!pipelineRunName;
const visualName = name || _.get(task, ['metadata', 'name'], '');
const nameRef = React.useRef();
const pillRef = React.useRef();

const path = `${resourcePathFromModel(ApprovalTaskModel, customTask?.metadata?.name, namespace)}`;

const enableLogLink = status !== ApprovalStatus.Idle && !!path;
const taskStatusColor = status
? getApprovalStatusColor(status).pftoken.value
: getApprovalStatusColor(ApprovalStatus.Idle).pftoken.value;

const [hover, hoverRef] = useHover();
const truncatedVisualName = React.useMemo(
() => truncateMiddle(visualName, { length: showStatusState ? 11 : 14, truncateEnd: true }),
[visualName, showStatusState],
);

const renderVisualName = (
<text
ref={nameRef}
x={showStatusState ? 30 : width / 2}
y={height / 2 + 1}
className={cx('odc-pipeline-vis-task-text', {
'is-text-center': !pipelineRunName,
'is-linked': enableLogLink,
})}
>
{truncatedVisualName}
</text>
);

let taskPill = (
<g ref={hoverRef}>
<SvgDropShadowFilter dy={1} id={FILTER_ID} />
<rect
filter={hover ? createSvgIdUrl(FILTER_ID) : ''}
width={width}
height={height}
rx={5}
className={cx('odc-pipeline-vis-task', {
'is-selected': !!pipelineRunName && hover,
'is-linked': !!pipelineRunName && enableLogLink,
})}
style={{
stroke: pipelineRunName
? status
? getApprovalStatusColor(status).pftoken.value
: getApprovalStatusColor(ApprovalStatus.Idle).pftoken.value
: '',
}}
/>
{visualName !== truncatedVisualName && disableVisualizationTooltip ? (
<Tooltip triggerRef={nameRef} content={visualName}>
{renderVisualName}
</Tooltip>
) : (
renderVisualName
)}

{showStatusState && (
<svg
width={30}
height={30}
viewBox="-10 -7 30 30"
style={{
color: taskStatusColor,
}}
>
<ApprovalStatusIcon status={status} />
</svg>
)}
</g>
);

if (!disableVisualizationTooltip) {
taskPill = (
<Tooltip
triggerRef={pillRef}
position="bottom"
enableFlip={false}
content={t('pipelines-plugin~Approval Task')}
>
<g ref={pillRef}>{taskPill}</g>
</Tooltip>
);
}
return (
<g className={cx('odc-pipeline-topology__task-node', { 'is-link': enableLogLink })}>
{enableLogLink ? <Link to={path}>{taskPill}</Link> : taskPill}
</g>
);
};

const ApprovalTaskNode: React.FC<ApprovalTaskNodeProps> = ({ element, disableTooltip }) => {
const { height, width } = element.getBounds();

const { pipeline, pipelineRun, task } = element.getData();

const customTaskName = `${pipelineRun?.metadata?.name}-${task?.name}`;
const pipelineRunStatus = pipelineRun && pipelineRunFilterReducer(pipelineRun);
let customTaskStatus: string = '';

const watchedResources = {
customRun: {
groupVersionKind: getGroupVersionKindForModel(CustomRunModelV1Beta1),
name: customTaskName,
namespace: pipeline?.metadata?.namespace,
prop: 'task',
},
approvalTask: {
groupVersionKind: getGroupVersionKindForModel(ApprovalTaskModel),
name: customTaskName,
namespace: pipeline?.metadata?.namespace,
prop: 'task',
},
};

const resourcesData: WatchK8sResults<WatchResource> = useK8sWatchResources<WatchResource>(
watchedResources,
);

let approvalStatus = (resourcesData?.approvalTask?.data as ApprovalTaskKind)?.status
?.approvalState;
if (pipelineRunStatus === ComputedStatus.Running && !approvalStatus) {
approvalStatus = ApprovalStatus.Idle;
}
if (
(resourcesData?.customRun?.data as CustomRunKind)?.spec?.status === CustomRunStatus.RunCancelled
) {
approvalStatus = ApprovalStatus.TimedOut;
}
customTaskStatus = approvalStatus;

const taskComponent: JSX.Element = (
<ApprovalTaskComponent
pipelineRunName={pipelineRun?.metadata?.name}
name={task.name || ''}
task={task.taskSpec && { data: { spec: task.taskSpec } }}
namespace={pipeline?.metadata?.namespace}
status={customTaskStatus}
disableVisualizationTooltip={disableTooltip}
width={width}
height={height}
customTask={resourcesData.approvalTask?.data as ApprovalTaskKind}
/>
);
return taskComponent;
};

export default React.memo(observer(ApprovalTaskNode));
@@ -0,0 +1,37 @@
.odc-pipeline-topology__task-node {
&.is-link {
> a:hover {
text-decoration: none;
}
}
&:focus {
outline: -webkit-focus-ring-color auto 5px;
}
}

.odc-pipeline-vis-task {
fill: var(--pf-v5-global--BackgroundColor--light-100);
stroke-width: 1;
cursor: default;

&.is-selected {
stroke-width: 2;
}
&.is-linked {
cursor: pointer;
}
}

.odc-pipeline-vis-task-text {
cursor: default;
dominant-baseline: middle;
fill: var(--pf-v5-global--Color--100);
font-size: var(--pf-global--FontSize--sm);

&.is-text-center {
text-anchor: middle;
}
&.is-linked {
cursor: pointer;
}
}

0 comments on commit 1f4e00f

Please sign in to comment.