Skip to content

Commit

Permalink
feat: Support graph pipelines in BitriseYmlService renameWorkflow and…
Browse files Browse the repository at this point in the history
… deleteWorkflow methods (#1265)

* feat: rename workflow in graph pipelines

* feat: delete workflow from graph pipelines
  • Loading branch information
zoltanszabo-bitrise authored Nov 8, 2024
1 parent 382979a commit 243d7d3
Show file tree
Hide file tree
Showing 9 changed files with 121 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ import WorkflowConfigHeader from './components/WorkflowConfigHeader';

type Props = UseDisclosureProps & {
workflowId: string;
onRename: (name: string) => void;
onCloseComplete?: VoidFunction;
};

const WorkflowConfigDrawerContent = ({ onCloseComplete, ...props }: Omit<Props, 'workflowId'>) => {
const WorkflowConfigDrawerContent = ({ onRename, onCloseComplete, ...props }: Omit<Props, 'workflowId'>) => {
const { isOpen, onClose } = useDisclosure(props);

return (
Expand Down Expand Up @@ -54,7 +55,7 @@ const WorkflowConfigDrawerContent = ({ onCloseComplete, ...props }: Omit<Props,
<ConfigurationTab />
</TabPanel>
<TabPanel>
<PropertiesTab variant="drawer" />
<PropertiesTab variant="drawer" onRename={onRename} />
</TabPanel>
</TabPanels>
</DrawerBody>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { TabPanel, TabPanels, Tabs, useTabs } from '@bitrise/bitkit';
import TriggersTabPanel from '@/pages/WorkflowsPage/components/WorkflowConfigPanel/components/TriggersTabPanel';
import useFeatureFlag from '@/hooks/useFeatureFlag';
import useSearchParams from '@/hooks/useSearchParams';
import useSelectedWorkflow from '@/hooks/useSelectedWorkflow';
import WorkflowConfigHeader from './components/WorkflowConfigHeader';
import ConfigurationTab from './tabs/ConfigurationTab';
import PropertiesTab from './tabs/PropertiesTab';
Expand All @@ -13,6 +14,7 @@ const TAB_IDS = [WorkflowConfigTab.CONFIGURATION, WorkflowConfigTab.PROPERTIES,

const WorkflowConfigPanelContent = () => {
const isTargetBasedTriggersEnabled = useFeatureFlag('enable-target-based-triggers');
const [, setSelectedWorkflow] = useSelectedWorkflow();

const [searchParams, setSearchParams] = useSearchParams();

Expand Down Expand Up @@ -48,7 +50,7 @@ const WorkflowConfigPanelContent = () => {
<ConfigurationTab />
</TabPanel>
<TabPanel p="24" overflowY="auto" h="100%">
<PropertiesTab variant="panel" />
<PropertiesTab variant="panel" onRename={setSelectedWorkflow} />
</TabPanel>
{isTargetBasedTriggersEnabled && (
<TabPanel overflowY="auto" h="100%">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,14 @@ import { useDebounceCallback } from 'usehooks-ts';
import useBitriseYmlStore from '@/hooks/useBitriseYmlStore';
import WorkflowService from '@/core/models/WorkflowService';
import { useWorkflows } from '@/hooks/useWorkflows';
import useSelectedWorkflow from '@/hooks/useSelectedWorkflow';
import { useWorkflowsPageStore } from '@/pages/WorkflowsPage/WorkflowsPage.store';
import useRenameWorkflow from '@/components/unified-editor/WorkflowConfig/hooks/useRenameWorkflow';
import { useWorkflowConfigContext } from '../WorkflowConfig.context';
import GitStatusNameInput from '../components/GitStatusNameInput';
import useFeatureFlag from '../../../../hooks/useFeatureFlag';
import useRenameWorkflow from '../hooks/useRenameWorkflow';

type Props = {
variant: 'panel' | 'drawer';
onRename: (name: string) => void;
};

type State = {
Expand All @@ -31,11 +30,9 @@ type State = {
validationResult: boolean | string;
};

const NameInput = ({ variant }: Props) => {
const NameInput = ({ onRename }: { onRename: (name: string) => void }) => {
const workflow = useWorkflowConfigContext();
const workflowNames = Object.keys(useWorkflows()).filter((id) => id !== workflow?.id);
const [, setSelectedWorkflow] = useSelectedWorkflow();
const { openWorkflowConfigDrawer } = useWorkflowsPageStore();

// TODO maybe useEditable hook from Chakra UI
const [editable, updateEditable] = useReducer<Reducer<State, Partial<State>>>(
Expand Down Expand Up @@ -66,15 +63,7 @@ const NameInput = ({ variant }: Props) => {
updateEditable({ value, isEditing: false, validationResult: true });
}, [editable.committedValue]);

const performRename = useRenameWorkflow((newWorkflowId: string) => {
if (variant === 'panel') {
setSelectedWorkflow(newWorkflowId);
}

if (variant === 'drawer') {
openWorkflowConfigDrawer(newWorkflowId);
}
});
const performRename = useRenameWorkflow(onRename);

const handleCommit = useCallback(() => {
if (editable.validationResult !== true) {
Expand Down Expand Up @@ -151,7 +140,7 @@ const NameInput = ({ variant }: Props) => {
);
};

const PropertiesTab = ({ variant }: Props) => {
const PropertiesTab = ({ variant, onRename }: Props) => {
const workflow = useWorkflowConfigContext();

const isGitStatusNameEnabled =
Expand Down Expand Up @@ -198,7 +187,7 @@ const PropertiesTab = ({ variant }: Props) => {

return (
<Box gap="24" display="flex" flexDir="column">
<NameInput variant={variant} />
<NameInput onRename={onRename} />
<Textarea label="Summary" value={summary} onChange={onSummaryChange} />
<Textarea label="Description" value={description} onChange={onDescriptionChange} />
{isGitStatusNameEnabled && (
Expand Down
40 changes: 37 additions & 3 deletions source/javascripts/core/models/BitriseYmlService.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,12 @@ describe('BitriseYmlService', () => {
const sourceYml: BitriseYml = {
format_version: '',
pipelines: {
graph: {
workflows: {
wf1: { depends_on: ['wf2'] },
wf2: {},
},
},
pl1: { stages: [{ st1: {} }] },
pl2: { stages: [{ st2: { workflows: [{ wf2: {} }] } }] },
},
Expand All @@ -213,6 +219,12 @@ describe('BitriseYmlService', () => {
const expectedYml: BitriseYml = {
format_version: '',
pipelines: {
graph: {
workflows: {
wf1: { depends_on: ['wf3'] },
wf3: {},
},
},
pl1: { stages: [{ st1: {} }] },
pl2: { stages: [{ st2: { workflows: [{ wf3: {} }] } }] },
},
Expand Down Expand Up @@ -537,7 +549,11 @@ describe('BitriseYmlService', () => {
wf1: {
steps: [
{ clone: {} },
{ script: { inputs: [{ other: 'value' }, { contents: 'echo "Hello, World!"' }] } },
{
script: {
inputs: [{ other: 'value' }, { contents: 'echo "Hello, World!"' }],
},
},
{ test: {} },
],
},
Expand All @@ -550,7 +566,11 @@ describe('BitriseYmlService', () => {
wf1: {
steps: [
{ clone: {} },
{ script: { inputs: [{ other: 'value' }, { contents: 'echo "Hello, Bitrise!"' }] } },
{
script: {
inputs: [{ other: 'value' }, { contents: 'echo "Hello, Bitrise!"' }],
},
},
{ test: {} },
],
},
Expand Down Expand Up @@ -955,6 +975,12 @@ describe('BitriseYmlService', () => {
const sourceYml: BitriseYml = {
format_version: '',
pipelines: {
graph: {
workflows: {
wf1: {},
wf2: { depends_on: ['wf1'] },
},
},
pl1: {
stages: [{ st1: {} }],
},
Expand Down Expand Up @@ -984,6 +1010,11 @@ describe('BitriseYmlService', () => {
const expectedYml: BitriseYml = {
format_version: '',
pipelines: {
graph: {
workflows: {
wf2: {},
},
},
pl2: {
stages: [{ st2: {} }],
},
Expand Down Expand Up @@ -2055,7 +2086,10 @@ describe('BitriseYmlService', () => {
});

it('should keep originally empty envs field', () => {
const sourceAndExpectedYml: BitriseYml = { format_version: '', workflows: { wf1: { envs: [] } } };
const sourceAndExpectedYml: BitriseYml = {
format_version: '',
workflows: { wf1: { envs: [] } },
};
const actualYml = BitriseYmlService.updateWorkflowEnvVars('wf1', [], sourceAndExpectedYml);

expect(actualYml).toMatchBitriseYml(sourceAndExpectedYml);
Expand Down
55 changes: 54 additions & 1 deletion source/javascripts/core/models/BitriseYmlService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { BitriseYml, Meta } from './BitriseYml';
import { StagesYml } from './Stage';
import { TriggerMapYml } from './TriggerMap';
import { ChainedWorkflowPlacement as Placement, Workflows, WorkflowYmlObject } from './Workflow';
import { PipelinesYml, PipelineYmlObject } from './Pipeline';
import { PipelinesYml, PipelineWorkflows, PipelineYmlObject } from './Pipeline';
import { BITRISE_STEP_LIBRARY_URL, StepInputVariable, StepYmlObject } from './Step';

function addStep(workflowId: string, cvs: string, to: number, yml: BitriseYml): BitriseYml {
Expand Down Expand Up @@ -703,6 +703,24 @@ function renameWorkflowInChains(workflowId: string, newWorkflowId: string, workf
});
}

function renameWorkflowInDependsOn(
workflowId: string,
newWorkflowId: string,
workflows: PipelineWorkflows,
): PipelineWorkflows {
return mapValues(workflows, (workflow) => {
const workflowCopy = deepCloneSimpleObject(workflow);

workflowCopy.depends_on = workflowCopy.depends_on?.map((id: string) => (id === workflowId ? newWorkflowId : id));

if (shouldRemoveField(workflowCopy.depends_on, workflow.depends_on)) {
delete workflowCopy.depends_on;
}

return workflowCopy;
});
}

function deleteWorkflowFromChains(workflowId: string, workflows: Workflows = {}): Workflows {
return mapValues(workflows, (workflow) => {
const workflowCopy = deepCloneSimpleObject(workflow);
Expand All @@ -722,6 +740,20 @@ function deleteWorkflowFromChains(workflowId: string, workflows: Workflows = {})
});
}

function deleteWorkflowFromDependsOn(workflowId: string, workflows: PipelineWorkflows = {}): PipelineWorkflows {
return mapValues(workflows, (workflow) => {
const workflowCopy = deepCloneSimpleObject(workflow);

workflowCopy.depends_on = workflowCopy.depends_on?.filter((id) => id !== workflowId);

if (shouldRemoveField(workflowCopy.depends_on, workflow.depends_on)) {
delete workflowCopy.depends_on;
}

return workflowCopy;
});
}

function renameWorkflowInStages(workflowId: string, newWorkflowId: string, stages: StagesYml): StagesYml {
return mapValues(stages, (stage) => {
const stageCopy = deepCloneSimpleObject(stage);
Expand Down Expand Up @@ -769,6 +801,18 @@ function renameWorkflowInPipelines(workflowId: string, newWorkflowId: string, pi
delete pipelineCopy.stages;
}

pipelineCopy.workflows = Object.fromEntries(
Object.entries(pipelineCopy.workflows ?? {}).map(([id, workflow]) => {
return [id === workflowId ? newWorkflowId : id, workflow];
}),
);

pipelineCopy.workflows = renameWorkflowInDependsOn(workflowId, newWorkflowId, pipelineCopy.workflows);

if (shouldRemoveField(pipelineCopy.workflows, pipeline.workflows)) {
delete pipelineCopy.workflows;
}

return pipelineCopy;
});
}
Expand All @@ -791,6 +835,15 @@ function deleteWorkflowFromPipelines(
delete pipelineCopy.stages;
}

// Remove workflow from `workflows` section of the pipeline
delete pipelineCopy.workflows?.[workflowId];

pipelineCopy.workflows = deleteWorkflowFromDependsOn(workflowId, pipelineCopy.workflows);

if (shouldRemoveField(pipelineCopy.workflows, pipeline.workflows)) {
delete pipelineCopy.workflows;
}

return pipelineCopy;
});
}
Expand Down
3 changes: 2 additions & 1 deletion source/javascripts/core/models/Pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ import { BitriseYml } from './BitriseYml';
type PipelinesYml = Required<BitriseYml>['pipelines'];
type PipelineYmlObject = PipelinesYml[string];
type Pipeline = { id: string; userValues: PipelineYmlObject };
type PipelineWorkflows = Required<PipelineYmlObject>['workflows'];

export { Pipeline, PipelinesYml, PipelineYmlObject };
export { Pipeline, PipelinesYml, PipelineYmlObject, PipelineWorkflows };
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import PipelineConfigDrawer from '../PipelineConfigDrawer/PipelineConfigDrawer';
import CreatePipelineDialog from '../CreatePipelineDialog/CreatePipelineDialog';
import WorkflowSelectorDrawer from '../WorkflowSelectorDrawer/WorkflowSelectorDrawer';
import transformWorkflowsToNodesAndEdges from '../PipelineCanvas/GraphPipelineCanvas/utils/transformWorkflowsToNodesAndEdges';
import usePipelineWorkflows from '../PipelineCanvas/GraphPipelineCanvas/hooks/usePipelineWorkflows';

const Drawers = ({ children }: PropsWithChildren) => {
const { addNodes } = useReactFlow();
const { addNodes, setNodes, setEdges } = useReactFlow();
const workflows = usePipelineWorkflows();
const { pipelineId, workflowId, isDialogMounted, isDialogOpen, closeDialog, unmountDialog } = usePipelinesPageStore();

const { createPipeline, addWorkflowToPipeline } = useBitriseYmlStore((s) => ({
Expand All @@ -26,6 +28,15 @@ const Drawers = ({ children }: PropsWithChildren) => {
addNodes(nodes);
};

const handleRenameWorkflow = () => {
const { nodes, edges } = transformWorkflowsToNodesAndEdges(pipelineId, workflows, {
x: -9999,
y: 0,
});
setNodes(nodes);
setEdges(edges);
};

return (
<>
{children}
Expand Down Expand Up @@ -61,6 +72,7 @@ const Drawers = ({ children }: PropsWithChildren) => {
{isDialogMounted(PipelineConfigDialogType.WORKFLOW_CONFIG) && (
<WorkflowConfigDrawer
workflowId={workflowId}
onRename={handleRenameWorkflow}
isOpen={isDialogOpen(PipelineConfigDialogType.WORKFLOW_CONFIG)}
onClose={closeDialog}
onCloseComplete={unmountDialog}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useCallback } from 'react';
import { ReactFlow, EdgeTypes, NodeTypes, ReactFlowProps, useEdgesState, useNodesState } from '@xyflow/react';
import { EdgeTypes, NodeTypes, ReactFlow, ReactFlowProps, useEdgesState, useNodesState } from '@xyflow/react';
import { PipelineConfigDialogType, usePipelinesPageStore } from '@/pages/PipelinesPage/PipelinesPage.store';
import usePipelineSelector from '@/pages/PipelinesPage/hooks/usePipelineSelector';
import WorkflowNode from './components/WorkflowNode/WorkflowNode';
Expand Down
2 changes: 2 additions & 0 deletions source/javascripts/pages/WorkflowsPage/WorkflowsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const WorkflowsPageContent = () => {
openStepConfigDrawer,
unmountStepConfigDrawer,
openCreateWorkflowDialog,
openWorkflowConfigDrawer,
unmountWorkflowConfigDrawer,
} = useWorkflowsPageStore();

Expand Down Expand Up @@ -158,6 +159,7 @@ const WorkflowsPageContent = () => {
{isWorkflowConfigDrawerMounted && (
<WorkflowConfigDrawer
workflowId={workflowId}
onRename={openWorkflowConfigDrawer}
isOpen={isWorkflowConfigDrawerOpen}
onClose={closeDialog}
onCloseComplete={unmountWorkflowConfigDrawer}
Expand Down

0 comments on commit 243d7d3

Please sign in to comment.