diff --git a/source/javascripts/components/unified-editor/WorkflowCard/WorkflowCard.tsx b/source/javascripts/components/unified-editor/WorkflowCard/WorkflowCard.tsx index 568f69663..01f2c0d4f 100644 --- a/source/javascripts/components/unified-editor/WorkflowCard/WorkflowCard.tsx +++ b/source/javascripts/components/unified-editor/WorkflowCard/WorkflowCard.tsx @@ -14,18 +14,19 @@ import SortableWorkflowsContext from './components/SortableWorkflowsContext'; type ContentProps = { id: string; + basedOn?: string; isCollapsable?: boolean; containerProps?: CardProps; }; -const WorkflowCardContent = memo(({ id, isCollapsable, containerProps }: ContentProps) => { - const { onCreateWorkflow, onChainWorkflow, onEditWorkflow, onRemoveWorkflow } = useWorkflowActions(); - const workflow = useWorkflow(id); +const WorkflowCardContent = memo(({ id, basedOn, isCollapsable, containerProps }: ContentProps) => { + const workflowId = basedOn || id; + const containerRef = useRef(null); + const workflow = useWorkflow(workflowId); const { data: stacksAndMachines } = useStacksAndMachines(); - const { isOpen, onOpen, onToggle } = useDisclosure({ - defaultIsOpen: !isCollapsable, - }); + const { isOpen, onOpen, onToggle } = useDisclosure({ defaultIsOpen: !isCollapsable }); + const { onCreateWorkflow, onChainWorkflow, onEditWorkflow, onRemoveWorkflow } = useWorkflowActions(); const { isSelected } = useSelection(); const isHighlighted = isSelected(id); @@ -73,10 +74,10 @@ const WorkflowCardContent = memo(({ id, isCollapsable, containerProps }: Content - {workflow.userValues.title || id} + {basedOn ? id : workflow.userValues.title || id} - {stack.name || 'Unknown stack'} + {basedOn ? `Based on ${basedOn}` : stack.name || 'Unknown stack'} @@ -125,9 +126,13 @@ const WorkflowCardContent = memo(({ id, isCollapsable, containerProps }: Content - before_run`} placement="before_run" parentWorkflowId={id} /> - - after_run`} placement="after_run" parentWorkflowId={id} /> + before_run`} + placement="before_run" + parentWorkflowId={workflowId} + /> + + after_run`} placement="after_run" parentWorkflowId={workflowId} /> @@ -143,6 +148,7 @@ type Props = ContentProps & WorkflowActions & StepActions & Selection; const WorkflowCard = ({ id, + basedOn, isCollapsable, containerProps, selectedWorkflowId = '', @@ -154,7 +160,7 @@ const WorkflowCard = ({ selectedStepIndex={selectedStepIndex} {...actions} > - + ); diff --git a/source/javascripts/core/models/BitriseYml.schema.ts b/source/javascripts/core/models/BitriseYml.schema.ts index 0772fc42d..a6570c13a 100644 --- a/source/javascripts/core/models/BitriseYml.schema.ts +++ b/source/javascripts/core/models/BitriseYml.schema.ts @@ -266,26 +266,7 @@ const BitriseYmlSchema = { workflows: { patternProperties: { '.*': { - properties: { - depends_on: { - type: 'array', - items: { - type: 'string', - }, - }, - abort_on_fail: { - type: 'boolean', - }, - should_always_run: { - type: 'string', - enum: ['off', 'workflow'], - }, - run_if: { - $ref: '#/definitions/RunIfModel', - }, - }, - additionalProperties: false, - type: 'object', + $ref: '#/definitions/GraphPipelineWorkflowModel', }, }, type: 'object', @@ -306,6 +287,41 @@ const BitriseYmlSchema = { additionalProperties: false, type: 'object', }, + GraphPipelineWorkflowModel: { + properties: { + based_on: { + type: 'string', + }, + depends_on: { + items: { + type: 'string', + }, + type: 'array', + }, + abort_on_fail: { + type: 'boolean', + }, + should_always_run: { + type: 'string', + enum: ['off', 'workflow'], + }, + run_if: { + $ref: '#/definitions/GraphPipelineWorkflowRunIfModel', + }, + }, + additionalProperties: false, + type: 'object', + }, + GraphPipelineWorkflowRunIfModel: { + required: ['expression'], + properties: { + expression: { + type: 'string', + }, + }, + additionalProperties: false, + type: 'object', + }, StageModel: { properties: { title: { diff --git a/source/javascripts/core/models/BitriseYmlService.ts b/source/javascripts/core/models/BitriseYmlService.ts index ed0327d9f..4fd190cbd 100644 --- a/source/javascripts/core/models/BitriseYmlService.ts +++ b/source/javascripts/core/models/BitriseYmlService.ts @@ -479,7 +479,7 @@ function addWorkflowToPipeline( } if (parentWorkflowId) { - if (!copy.workflows?.[parentWorkflowId] || !copy.pipelines?.[pipelineId]?.workflows?.[parentWorkflowId]) { + if (!copy.pipelines?.[pipelineId]?.workflows?.[parentWorkflowId]) { return copy; } } diff --git a/source/javascripts/core/models/Workflow.ts b/source/javascripts/core/models/Workflow.ts index 632a9e4d6..fad7b299e 100644 --- a/source/javascripts/core/models/Workflow.ts +++ b/source/javascripts/core/models/Workflow.ts @@ -7,6 +7,6 @@ type WorkflowYmlObject = Workflows[string] & { }; type Workflow = { id: string; userValues: WorkflowYmlObject }; type ChainedWorkflowPlacement = 'before_run' | 'after_run'; -type PipelineWorkflow = { id: string; dependsOn: string[] }; +type PipelineWorkflow = { id: string; basedOn?: string; dependsOn: string[] }; export { Workflow, WorkflowYmlObject, Workflows, ChainedWorkflowPlacement, PipelineWorkflow }; diff --git a/source/javascripts/pages/PipelinesPage/PipelinesPage.stories.tsx b/source/javascripts/pages/PipelinesPage/PipelinesPage.stories.tsx index 1b8b59746..76af54ea4 100644 --- a/source/javascripts/pages/PipelinesPage/PipelinesPage.stories.tsx +++ b/source/javascripts/pages/PipelinesPage/PipelinesPage.stories.tsx @@ -1,5 +1,6 @@ import { Meta, StoryObj } from '@storybook/react'; import { Box } from '@bitrise/bitkit'; +import { cloneDeep } from 'es-toolkit'; import PipelinesPage from './PipelinesPage'; export default { @@ -116,3 +117,22 @@ export const ReactivatePlan: Story = { }; export const GraphPipelineWithEditing: Story = {}; + +const withWorkflowOverrideYml = () => { + const yml = cloneDeep(TEST_BITRISE_YML); + + if (yml.pipelines?.['graph-pipeline']?.workflows) { + yml.pipelines['graph-pipeline'].workflows.override = { + based_on: 'wf3', + depends_on: ['wf1'], + }; + } + + return yml; +}; + +export const WithWorkflowOverride: Story = { + args: { + yml: withWorkflowOverrideYml(), + }, +}; diff --git a/source/javascripts/pages/PipelinesPage/components/PipelineCanvas/GraphPipelineCanvas/GraphPipelineCanvas.types.ts b/source/javascripts/pages/PipelinesPage/components/PipelineCanvas/GraphPipelineCanvas/GraphPipelineCanvas.types.ts index 19ef70b1e..c4da31c32 100644 --- a/source/javascripts/pages/PipelinesPage/components/PipelineCanvas/GraphPipelineCanvas/GraphPipelineCanvas.types.ts +++ b/source/javascripts/pages/PipelinesPage/components/PipelineCanvas/GraphPipelineCanvas/GraphPipelineCanvas.types.ts @@ -1,7 +1,7 @@ import { Edge, Node } from '@xyflow/react'; import { PLACEHOLDER_NODE_TYPE, WORKFLOW_NODE_TYPE } from './GraphPipelineCanvas.const'; -export type WorkflowNodeType = Node<{ fixed?: boolean; highlighted?: boolean }>; +export type WorkflowNodeType = Node<{ basedOn?: string; fixed?: boolean; highlighted?: boolean }>; export type PlaceholderNodeType = Node<{ dependsOn: string[] }>; export type GraphPipelineEdgeType = Edge<{ highlighted?: boolean }>; export type GraphPipelineNodeType = PlaceholderNodeType | WorkflowNodeType; diff --git a/source/javascripts/pages/PipelinesPage/components/PipelineCanvas/GraphPipelineCanvas/components/WorkflowNode.tsx b/source/javascripts/pages/PipelinesPage/components/PipelineCanvas/GraphPipelineCanvas/components/WorkflowNode.tsx index 6c7c1a0d8..780c16452 100644 --- a/source/javascripts/pages/PipelinesPage/components/PipelineCanvas/GraphPipelineCanvas/components/WorkflowNode.tsx +++ b/source/javascripts/pages/PipelinesPage/components/PipelineCanvas/GraphPipelineCanvas/components/WorkflowNode.tsx @@ -32,18 +32,19 @@ const selectedStyle = { outlineOffset: '-2px', } satisfies CardProps; -const WorkflowNode = ({ id, zIndex, selected }: Props) => { +const WorkflowNode = ({ id, selected, zIndex, data }: Props) => { const ref = useRef(null); const hovered = useHover(ref); const workflows = useWorkflows(); + const { selectedPipeline } = usePipelineSelector(); + const openDialog = usePipelinesPageStore((s) => s.openDialog); const closeDialog = usePipelinesPageStore((s) => s.closeDialog); - const selectedWorkflowId = usePipelinesPageStore((s) => s.workflowId); - const selectedStepIndex = usePipelinesPageStore((s) => s.stepIndex); const setStepIndex = usePipelinesPageStore((s) => s.setStepIndex); - - const { selectedPipeline } = usePipelineSelector(); + const selectedStepIndex = usePipelinesPageStore((s) => s.stepIndex); + const selectedWorkflowId = usePipelinesPageStore((s) => s.workflowId); const isGraphPipelinesEnabled = useFeatureFlag('enable-dag-pipelines'); + const { updateNode, deleteElements, setEdges } = useReactFlow(); const { moveStep, cloneStep, deleteStep, upgradeStep, setChainedWorkflows, removeChainedWorkflow } = @@ -61,6 +62,8 @@ const WorkflowNode = ({ id, zIndex, selected }: Props) => { onResize: ({ height }) => updateNode(id, { height }), }); + const basedOn = 'basedOn' in data ? data.basedOn : undefined; + const { handleAddStep, handleMoveStep, @@ -131,6 +134,15 @@ const WorkflowNode = ({ id, zIndex, selected }: Props) => { } } + if (basedOn) { + return { + handleRemoveWorkflow: (deletedWorkflowId: string) => { + deleteElements({ nodes: [{ id: deletedWorkflowId }] }); + handleWorkflowActionDialogChange(deletedWorkflowId, 'remove'); + }, + }; + } + return { handleAddStep: (workflowId: string, stepIndex: number) => openDialog({ @@ -206,21 +218,22 @@ const WorkflowNode = ({ id, zIndex, selected }: Props) => { }, }; }, [ + basedOn, + workflows, + selectedPipeline, + selectedStepIndex, + selectedWorkflowId, isGraphPipelinesEnabled, moveStep, cloneStep, upgradeStep, openDialog, - selectedPipeline, deleteStep, - selectedWorkflowId, - selectedStepIndex, closeDialog, setStepIndex, deleteElements, - removeChainedWorkflow, - workflows, setChainedWorkflows, + removeChainedWorkflow, ]); const containerProps = useMemo( @@ -252,9 +265,10 @@ const WorkflowNode = ({ id, zIndex, selected }: Props) => { { return Object.entries(pipelineWorkflows).map(([id, pipelineWorkflow]) => { return { id, + basedOn: pipelineWorkflow.based_on, dependsOn: pipelineWorkflow.depends_on ?? [], } satisfies PipelineWorkflow; }); diff --git a/source/javascripts/pages/PipelinesPage/components/PipelineCanvas/GraphPipelineCanvas/utils/createWorkflowNode.ts b/source/javascripts/pages/PipelinesPage/components/PipelineCanvas/GraphPipelineCanvas/utils/createWorkflowNode.ts index 90ab0020e..9b02e6fe7 100644 --- a/source/javascripts/pages/PipelinesPage/components/PipelineCanvas/GraphPipelineCanvas/utils/createWorkflowNode.ts +++ b/source/javascripts/pages/PipelinesPage/components/PipelineCanvas/GraphPipelineCanvas/utils/createWorkflowNode.ts @@ -10,7 +10,7 @@ import { GraphPipelineNodeType } from '../GraphPipelineCanvas.types'; function createWorkflowNode(workflow: PipelineWorkflow, actionable: boolean) { return { id: workflow.id, - data: {}, + data: { basedOn: workflow.basedOn }, deletable: actionable, draggable: false, focusable: false,