diff --git a/apps/gitness/package.json b/apps/gitness/package.json
index 64358c24e..69751f99b 100644
--- a/apps/gitness/package.json
+++ b/apps/gitness/package.json
@@ -23,7 +23,7 @@
"@harnessio/code-service-client": "3.2.1-beta.1",
"@harnessio/forms": "workspace:*",
"@harnessio/ui": "workspace:*",
- "@harnessio/unified-pipeline": "workspace:*",
+ "@harnessio/pipeline-graph": "workspace:*",
"@harnessio/views": "workspace:*",
"@harnessio/yaml-editor": "workspace:*",
"@hookform/resolvers": "^3.6.0",
diff --git a/apps/gitness/src/App.tsx b/apps/gitness/src/App.tsx
index 78c59eb7e..952827e7c 100644
--- a/apps/gitness/src/App.tsx
+++ b/apps/gitness/src/App.tsx
@@ -23,9 +23,11 @@ import { ThemeProvider } from './framework/context/ThemeContext'
import { queryClient } from './framework/queryClient'
import i18n from './i18n/i18n'
import { useTranslationStore } from './i18n/stores/i18n-store'
+import PipelineLayout from './layouts/PipelineStudioLayout'
import CreateProject from './pages-v2/create-project/create-project-container'
import { LandingPage } from './pages-v2/landing-page-container'
import { Logout } from './pages-v2/logout'
+import PipelineEditPage from './pages-v2/pipeline/pipeline-edit/pipeline-edit'
import { SettingsProfileGeneralPage } from './pages-v2/profile-settings/profile-settings-general-container'
import { SettingsProfileKeysPage } from './pages-v2/profile-settings/profile-settings-keys-container'
import { ProfileSettingsThemePage } from './pages-v2/profile-settings/profile-settings-theme-page'
@@ -148,6 +150,26 @@ export default function App() {
path: ':spaceId/repos',
element:
},
+ {
+ path: ':spaceId/repos/:repoId/pipelines/:pipelineId',
+ element: ,
+ children: [
+ {
+ index: true,
+ element:
+ // children: [
+ // {
+ // path: 'edit',
+ // element:
+ // }
+ // ]
+ },
+ {
+ path: 'edit',
+ element:
+ }
+ ]
+ },
{
path: ':spaceId/repos/:repoId',
element: ,
@@ -275,13 +297,7 @@ export default function App() {
},
{
path: 'pipelines',
- children: [
- { index: true, element: },
- {
- path: ':pipelineId',
- element:
- }
- ]
+ children: [{ index: true, element: }]
}
]
},
diff --git a/apps/gitness/src/components-v2/file-content-viewer.tsx b/apps/gitness/src/components-v2/file-content-viewer.tsx
index 4ad3ba86d..7a73af9cf 100644
--- a/apps/gitness/src/components-v2/file-content-viewer.tsx
+++ b/apps/gitness/src/components-v2/file-content-viewer.tsx
@@ -11,8 +11,8 @@ import { useThemeStore } from '../framework/context/ThemeContext'
import { useDownloadRawFile } from '../framework/hooks/useDownloadRawFile'
import { useGetRepoRef } from '../framework/hooks/useGetRepoPath'
import useCodePathDetails from '../hooks/useCodePathDetails'
+import { themes } from '../pages-v2/pipeline/pipeline-edit/theme/monaco-theme'
import { useRepoBranchesStore } from '../pages-v2/repo/stores/repo-branches-store'
-import { themes } from '../pages/pipeline-edit/theme/monaco-theme'
import { PathParams } from '../RouteDefinitions'
import { decodeGitContent, FILE_SEPERATOR, filenameToLanguage, formatBytes, GitCommitAction } from '../utils/git-utils'
diff --git a/apps/gitness/src/components-v2/file-editor.tsx b/apps/gitness/src/components-v2/file-editor.tsx
index aebf28688..36a726b43 100644
--- a/apps/gitness/src/components-v2/file-editor.tsx
+++ b/apps/gitness/src/components-v2/file-editor.tsx
@@ -11,8 +11,8 @@ import { useThemeStore } from '../framework/context/ThemeContext'
import { useExitConfirm } from '../framework/hooks/useExitConfirm'
import useCodePathDetails from '../hooks/useCodePathDetails'
import { useTranslationStore } from '../i18n/stores/i18n-store'
+import { themes } from '../pages-v2/pipeline/pipeline-edit/theme/monaco-theme'
import { useRepoBranchesStore } from '../pages-v2/repo/stores/repo-branches-store'
-import { themes } from '../pages/pipeline-edit/theme/monaco-theme'
import { PathParams } from '../RouteDefinitions'
import { decodeGitContent, FILE_SEPERATOR, filenameToLanguage, GitCommitAction, PLAIN_TEXT } from '../utils/git-utils'
import { splitPathWithParents } from '../utils/path-utils'
diff --git a/apps/gitness/src/components/FileContentViewer.tsx b/apps/gitness/src/components/FileContentViewer.tsx
index dabaf22ad..83b2a6ab3 100644
--- a/apps/gitness/src/components/FileContentViewer.tsx
+++ b/apps/gitness/src/components/FileContentViewer.tsx
@@ -22,7 +22,7 @@ import { CodeEditor } from '@harnessio/yaml-editor'
import { useDownloadRawFile } from '../framework/hooks/useDownloadRawFile'
import { useGetRepoRef } from '../framework/hooks/useGetRepoPath'
-import { themes } from '../pages/pipeline-edit/theme/monaco-theme'
+import { themes } from '../pages-v2/pipeline/pipeline-edit/theme/monaco-theme'
import { timeAgoFromISOTime } from '../pages/pipeline-edit/utils/time-utils'
import { PathParams } from '../RouteDefinitions'
import { decodeGitContent, filenameToLanguage, formatBytes, getTrimmedSha, GitCommitAction } from '../utils/git-utils'
diff --git a/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/canvas/canvas-button.tsx b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/canvas/canvas-button.tsx
new file mode 100644
index 000000000..25f5e643c
--- /dev/null
+++ b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/canvas/canvas-button.tsx
@@ -0,0 +1,14 @@
+export function CanvasButton(props: React.PropsWithChildren<{ onClick: () => void }>) {
+ const { children, onClick } = props
+
+ return (
+
+ {children}
+
+ )
+}
diff --git a/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/canvas/canvas-controls.tsx b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/canvas/canvas-controls.tsx
new file mode 100644
index 000000000..4094fa5fc
--- /dev/null
+++ b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/canvas/canvas-controls.tsx
@@ -0,0 +1,20 @@
+import { useCanvasContext } from '@harnessio/pipeline-graph'
+
+import { CanvasButton } from './canvas-button'
+
+export function CanvasControls() {
+ const { fit } = useCanvasContext()
+
+ return (
+
+ {/* TODO: uncomment increase/decrease once its fixed in pipeline-graph */}
+ {/*
+
increase()}>+
+
decrease()}>-
+ */}
+
fit()}>
+
+
+
+ )
+}
diff --git a/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/content-node-types.ts b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/content-node-types.ts
new file mode 100644
index 000000000..8a003c43f
--- /dev/null
+++ b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/content-node-types.ts
@@ -0,0 +1,11 @@
+export enum ContentNodeTypes {
+ add = 'add',
+ start = 'start',
+ end = 'end',
+ step = 'step',
+ approval = 'approval',
+ parallel = 'parallel',
+ serial = 'serial',
+ stage = 'stage',
+ form = 'form'
+}
diff --git a/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/nodes/add-node.tsx b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/nodes/add-node.tsx
new file mode 100644
index 000000000..d2fa3fb85
--- /dev/null
+++ b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/nodes/add-node.tsx
@@ -0,0 +1,39 @@
+import { Icon } from '@harnessio/canary'
+import { LeafNodeInternalType } from '@harnessio/pipeline-graph'
+import { Button } from '@harnessio/ui/components'
+
+import { useNodeContext } from '../../../context/NodeContextMenuProvider'
+import { CommonNodeDataType } from '../types/nodes'
+
+export interface AddNodeDataType extends CommonNodeDataType {}
+
+export function AddNode(props: { node: LeafNodeInternalType }) {
+ const { node } = props
+ const { data } = node
+
+ const { handleAddIn } = useNodeContext()
+
+ return (
+
+
+
+ )
+}
diff --git a/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/nodes/common/CommonContextMenu.tsx b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/nodes/common/CommonContextMenu.tsx
new file mode 100644
index 000000000..f769a34ed
--- /dev/null
+++ b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/nodes/common/CommonContextMenu.tsx
@@ -0,0 +1,281 @@
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ Icon,
+ Text
+} from '@harnessio/ui/components'
+
+import { useNodeContext } from '../../../../context/NodeContextMenuProvider'
+import { YamlEntityType } from '../../types/nodes'
+
+export const CommonNodeContextMenu = () => {
+ const {
+ contextMenuData,
+ hideContextMenu,
+ deleteStep,
+ addStep,
+ addStage,
+ editStep,
+ addSerialGroup,
+ addParallelGroup,
+ revealInYaml
+ } = useNodeContext()
+
+ const showAddStepBeforeAfter =
+ (contextMenuData?.nodeData.yamlEntityType === YamlEntityType.Step ||
+ contextMenuData?.nodeData.yamlEntityType === YamlEntityType.StepParallelGroup ||
+ contextMenuData?.nodeData.yamlEntityType === YamlEntityType.StepSerialGroup) &&
+ !contextMenuData.isIn
+
+ const showAddStageBeforeAfter =
+ (contextMenuData?.nodeData.yamlEntityType === YamlEntityType.Stage ||
+ contextMenuData?.nodeData.yamlEntityType === YamlEntityType.ParallelGroup ||
+ contextMenuData?.nodeData.yamlEntityType === YamlEntityType.SerialGroup) &&
+ !contextMenuData.isIn
+
+ const showAddSerialParallelGroupBeforeAfter = showAddStageBeforeAfter
+
+ const enableEdit =
+ contextMenuData?.nodeData.yamlEntityType === YamlEntityType.Step ||
+ contextMenuData?.nodeData.yamlEntityType === YamlEntityType.StepParallelGroup ||
+ contextMenuData?.nodeData.yamlEntityType === YamlEntityType.StepSerialGroup
+
+ const enableDelete = true
+
+ const showAddStageIn =
+ (contextMenuData?.nodeData.yamlEntityType === YamlEntityType.ParallelGroup ||
+ contextMenuData?.nodeData.yamlEntityType === YamlEntityType.SerialGroup) &&
+ contextMenuData.isIn
+
+ const showAddSerialParallelGroupIn =
+ (contextMenuData?.nodeData.yamlEntityType === YamlEntityType.ParallelGroup ||
+ contextMenuData?.nodeData.yamlEntityType === YamlEntityType.SerialGroup) &&
+ contextMenuData.isIn
+
+ const showRevealInYaml = contextMenuData?.nodeData.yamlEntityType === YamlEntityType.Step
+
+ const getMenu = () => {
+ const menu: React.ReactNode[][] = []
+
+ if (!contextMenuData?.isIn) {
+ menu.push([
+ {
+ contextMenuData && editStep(contextMenuData.nodeData)
+ }}
+ >
+
+ Edit
+
+ ])
+ }
+
+ if (showAddStepBeforeAfter) {
+ menu.push([
+ {
+ contextMenuData && addStep(contextMenuData.nodeData, 'before')
+ }}
+ >
+
+ Add step/group before
+ ,
+ {
+ contextMenuData && addStep(contextMenuData.nodeData, 'after')
+ }}
+ >
+
+ Add step/group after
+
+ ])
+ }
+
+ if (showAddStageBeforeAfter) {
+ menu.push([
+ {
+ contextMenuData && addStage(contextMenuData.nodeData, 'before')
+ }}
+ >
+
+ Add stage before
+ ,
+ {
+ contextMenuData && addStage(contextMenuData.nodeData, 'after')
+ }}
+ >
+
+ Add stage after
+
+ ])
+ }
+
+ if (showAddSerialParallelGroupBeforeAfter) {
+ menu.push([
+ {
+ contextMenuData && addSerialGroup(contextMenuData.nodeData, 'before')
+ }}
+ >
+
+ Add serial group before
+ ,
+ {
+ contextMenuData && addSerialGroup(contextMenuData.nodeData, 'after')
+ }}
+ >
+
+ Add serial group after
+
+ ])
+
+ menu.push([
+ {
+ contextMenuData && addParallelGroup(contextMenuData.nodeData, 'before')
+ }}
+ >
+
+ Add parallel group before
+ ,
+ {
+ contextMenuData && addParallelGroup(contextMenuData.nodeData, 'after')
+ }}
+ >
+
+ Add parallel group after
+
+ ])
+ }
+
+ if (showAddStageIn) {
+ menu.push([
+ {
+ contextMenuData && addStage(contextMenuData.nodeData, 'in')
+ }}
+ >
+
+ Add stage
+
+ ])
+ }
+
+ if (showAddSerialParallelGroupIn) {
+ menu.push([
+ {
+ contextMenuData && addSerialGroup(contextMenuData.nodeData, 'in')
+ }}
+ >
+
+ Add serial group
+
+ ])
+
+ menu.push([
+ {
+ contextMenuData && addParallelGroup(contextMenuData.nodeData, 'in')
+ }}
+ >
+
+ Add parallel group
+
+ ])
+ }
+
+ if (showRevealInYaml) {
+ menu.push([
+ {
+ contextMenuData && revealInYaml(contextMenuData.nodeData.yamlPath)
+ }}
+ >
+
+ Reveal in Yaml
+
+ ])
+ }
+
+ if (!contextMenuData?.isIn) {
+ menu.push([
+ {
+ contextMenuData && deleteStep(contextMenuData.nodeData)
+ }}
+ >
+
+ Delete
+
+ ])
+ }
+
+ return menu
+ }
+
+ return (
+ {
+ if (open === false) {
+ hideContextMenu()
+ }
+ }}
+ >
+
+ {getMenu().map((menuItem, idx) => {
+ return idx !== 0 ? (
+ <>
+
+ {menuItem}
+ >
+ ) : (
+ menuItem
+ )
+ })}
+
+
+ )
+}
diff --git a/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/nodes/end-node.tsx b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/nodes/end-node.tsx
new file mode 100644
index 000000000..43ad739df
--- /dev/null
+++ b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/nodes/end-node.tsx
@@ -0,0 +1,19 @@
+import { LeafNodeInternalType } from '@harnessio/pipeline-graph'
+
+export interface EndNodeDataType {}
+
+export function EndNode(_props: { node: LeafNodeInternalType }) {
+ return (
+
+ {/* TODO: replace with icon */}
+
+
+ )
+}
diff --git a/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/nodes/parallel-group-node.tsx b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/nodes/parallel-group-node.tsx
new file mode 100644
index 000000000..92132dbe2
--- /dev/null
+++ b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/nodes/parallel-group-node.tsx
@@ -0,0 +1,69 @@
+import { ParallelNodeInternalType } from '@harnessio/pipeline-graph'
+import { Button, Icon } from '@harnessio/ui/components'
+
+import { useNodeContext } from '../../../context/NodeContextMenuProvider'
+import { CommonNodeDataType } from '../types/nodes'
+
+export interface ParallelGroupContentNodeDataType extends CommonNodeDataType {
+ icon?: React.ReactElement
+}
+
+export function ParallelGroupContentNode(props: {
+ node: ParallelNodeInternalType
+ children?: React.ReactElement
+ collapsed?: boolean
+}) {
+ const { node, children, collapsed } = props
+ const data = node.data as ParallelGroupContentNodeDataType
+
+ const { showContextMenu, handleAddIn } = useNodeContext()
+
+ return (
+ <>
+
+
+
+ {/* //flex h-9 items-center */}
+
+ {data.name}
+
+
+
+
+
+ {!collapsed && node.children.length === 0 && (
+
+ )}
+
+ {children}
+ >
+ )
+}
diff --git a/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/nodes/serial-group-node.tsx b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/nodes/serial-group-node.tsx
new file mode 100644
index 000000000..4c90d9124
--- /dev/null
+++ b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/nodes/serial-group-node.tsx
@@ -0,0 +1,70 @@
+import { SerialNodeInternalType } from '@harnessio/pipeline-graph'
+import { Button, Icon } from '@harnessio/ui/components'
+
+import { useNodeContext } from '../../../context/NodeContextMenuProvider'
+import { CommonNodeDataType } from '../types/nodes'
+
+export interface SerialGroupContentNodeDataType extends CommonNodeDataType {
+ icon?: React.ReactElement
+}
+
+export function SerialGroupContentNode(props: {
+ node: SerialNodeInternalType
+ children?: React.ReactElement
+ collapsed?: boolean
+}) {
+ const { node, children, collapsed } = props
+ const data = node.data as SerialGroupContentNodeDataType
+
+ const { showContextMenu, handleAddIn } = useNodeContext()
+
+ return (
+ <>
+
+
+
+ {/* //flex h-9 items-center */}
+
+ {data.name}
+
+
+
+
+
+ {!collapsed && node.children.length === 0 && (
+
+ )}
+
+ {children}
+ >
+ )
+}
diff --git a/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/nodes/stage-node.tsx b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/nodes/stage-node.tsx
new file mode 100644
index 000000000..d481c7a7e
--- /dev/null
+++ b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/nodes/stage-node.tsx
@@ -0,0 +1,31 @@
+import { Tooltip, TooltipContent, TooltipTrigger } from '@harnessio/canary'
+import { SerialNodeInternalType } from '@harnessio/pipeline-graph'
+
+import { CommonNodeDataType } from '../types/nodes'
+
+export interface StageNodeContentType extends CommonNodeDataType {
+ icon?: React.ReactElement
+}
+
+export function SerialGroupNodeContent(props: {
+ node: SerialNodeInternalType
+ children: React.ReactElement
+}) {
+ const { node, children } = props
+ const data = node.data as StageNodeContentType
+
+ return (
+ <>
+
+
+
+
+ {data.name}
+
+ {data.name}
+
+
+ {children}
+ >
+ )
+}
diff --git a/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/nodes/start-node.tsx b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/nodes/start-node.tsx
new file mode 100644
index 000000000..d7f45ed02
--- /dev/null
+++ b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/nodes/start-node.tsx
@@ -0,0 +1,19 @@
+import { Icon } from '@harnessio/canary'
+import { LeafNodeInternalType } from '@harnessio/pipeline-graph'
+
+export interface StartNodeDataType {}
+
+export function StartNode(_props: { node: LeafNodeInternalType }) {
+ return (
+
+
+
+ )
+}
diff --git a/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/nodes/step-node.tsx b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/nodes/step-node.tsx
new file mode 100644
index 000000000..1930d5a18
--- /dev/null
+++ b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/nodes/step-node.tsx
@@ -0,0 +1,51 @@
+import { useMemo } from 'react'
+
+import { LeafNodeInternalType } from '@harnessio/pipeline-graph'
+import { Button, Icon, Text } from '@harnessio/ui/components'
+
+import { useNodeContext } from '../../../context/NodeContextMenuProvider'
+import { CommonNodeDataType } from '../types/nodes'
+
+export interface StepNodeDataType extends CommonNodeDataType {
+ icon?: React.ReactElement
+ state?: 'success' | 'loading'
+ selected?: boolean
+}
+
+export function StepNode(props: { node: LeafNodeInternalType }) {
+ const { node } = props
+ const data = node.data
+
+ const { showContextMenu, selectionPath } = useNodeContext()
+
+ const selected = useMemo(() => selectionPath === data.yamlPath, [selectionPath])
+
+ return (
+
+
+
+
{data.icon}
+
+ {data.name}
+
+
+ )
+}
diff --git a/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/types/nodes.ts b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/types/nodes.ts
new file mode 100644
index 000000000..213fed669
--- /dev/null
+++ b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/types/nodes.ts
@@ -0,0 +1,15 @@
+export enum YamlEntityType {
+ Step = 'Step',
+ Stage = 'Stage',
+ ParallelGroup = 'ParallelGroup',
+ SerialGroup = 'SerialGroup',
+ StepSerialGroup = 'StepSerialGroup',
+ StepParallelGroup = 'StepParallelGroup'
+}
+
+export interface CommonNodeDataType {
+ yamlPath: string
+ yamlChildrenPath?: string
+ name: string
+ yamlEntityType: YamlEntityType
+}
diff --git a/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/utils/common-step-utils.ts b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/utils/common-step-utils.ts
new file mode 100644
index 000000000..ba8535555
--- /dev/null
+++ b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/utils/common-step-utils.ts
@@ -0,0 +1,9 @@
+export const getIsRunStep = (step: Record) => Object.hasOwn(step, 'run')
+
+export const getIsRunTestStep = (step: Record) => Object.hasOwn(step, 'run-test')
+
+export const getIsBackgroundStep = (step: Record) => Object.hasOwn(step, 'background')
+
+export const getIsActionStep = (step: Record) => Object.hasOwn(step, 'action')
+
+export const getIsTemplateStep = (step: Record) => Object.hasOwn(step, 'template')
diff --git a/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/utils/step-icon-utils.ts b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/utils/step-icon-utils.ts
new file mode 100644
index 000000000..15564eae1
--- /dev/null
+++ b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/utils/step-icon-utils.ts
@@ -0,0 +1,28 @@
+import { IconProps } from '@harnessio/canary'
+
+import {
+ getIsActionStep,
+ getIsBackgroundStep,
+ getIsRunStep,
+ getIsRunTestStep,
+ getIsTemplateStep
+} from './common-step-utils'
+
+export const getIconBasedOnStep = (step: any): IconProps['name'] => {
+ if (getIsRunStep(step)) return 'run'
+
+ if (getIsRunTestStep(step)) return 'run-test'
+
+ if (getIsBackgroundStep(step)) return 'cog-6'
+
+ if (getIsActionStep(step)) return 'github-actions'
+
+ if (getIsTemplateStep(step)) return 'harness-plugin'
+
+ /**
+ * Yet to add Bitrise plugins,
+ * Request backend to add a property to identify bitrise-plugin
+ */
+
+ return 'harness'
+}
diff --git a/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/utils/step-name-utils.ts b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/utils/step-name-utils.ts
new file mode 100644
index 000000000..6552efe36
--- /dev/null
+++ b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/utils/step-name-utils.ts
@@ -0,0 +1,43 @@
+import { get } from 'lodash-es'
+
+import {
+ getIsActionStep,
+ getIsBackgroundStep,
+ getIsRunStep,
+ getIsRunTestStep,
+ getIsTemplateStep
+} from './common-step-utils'
+
+const getNameOrScriptText = (stepData: string | Record<'script', string>, defaultString: string): string => {
+ const isStepHasString = typeof stepData === 'string'
+
+ return isStepHasString ? stepData : get(stepData, 'script', defaultString)
+}
+
+export const getNameBasedOnStep = (step: any, stepIndex: number): string => {
+ if (step.name) return step.name
+
+ let displayName = `Step ${stepIndex}`
+ // Run
+ if (getIsRunStep(step)) {
+ displayName = getNameOrScriptText(step.run, 'Run')
+ }
+ // Run test
+ else if (getIsRunTestStep(step)) {
+ displayName = getNameOrScriptText(step?.['run-test'], 'Run Test')
+ }
+ // Background
+ else if (getIsBackgroundStep(step)) {
+ displayName = getNameOrScriptText(step?.background, 'Background')
+ }
+ // Action
+ else if (getIsActionStep(step)) {
+ displayName = get(step?.action, 'uses', 'GitHub Action')
+ }
+ // Template
+ else if (getIsTemplateStep(step)) {
+ displayName = get(step?.template, 'uses', 'Harness Template')
+ }
+
+ return displayName.split('\n')[0]
+}
diff --git a/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/utils/yaml-to-pipeline-graph.tsx b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/utils/yaml-to-pipeline-graph.tsx
new file mode 100644
index 000000000..13bb9cb4d
--- /dev/null
+++ b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/graph-implementation/utils/yaml-to-pipeline-graph.tsx
@@ -0,0 +1,188 @@
+import { Icon } from '@harnessio/canary'
+import {
+ AnyContainerNodeType,
+ LeafContainerNodeType,
+ ParallelContainerNodeType,
+ SerialContainerNodeType
+} from '@harnessio/pipeline-graph'
+
+import { ContentNodeTypes } from '../content-node-types'
+import { ParallelGroupContentNodeDataType } from '../nodes/parallel-group-node'
+import { StageNodeContentType } from '../nodes/stage-node'
+import { StepNodeDataType } from '../nodes/step-node'
+import { YamlEntityType } from '../types/nodes'
+import { getIconBasedOnStep } from './step-icon-utils'
+import { getNameBasedOnStep } from './step-name-utils'
+
+export const yaml2Nodes = (
+ yamlObject: Record,
+ options: { selectedPath?: string } = {}
+): AnyContainerNodeType[] => {
+ const nodes: AnyContainerNodeType[] = []
+
+ const stages = yamlObject?.pipeline?.stages ?? []
+
+ if (stages) {
+ const stagesNodes = processStages(stages, 'pipeline.stages', options)
+ nodes.push(...stagesNodes)
+ }
+
+ return nodes
+}
+
+const getGroupKey = (stage: Record): 'group' | 'parallel' | undefined => {
+ if ('group' in stage) return 'group'
+ else if ('parallel' in stage) return 'parallel'
+ return undefined
+}
+
+const processStages = (
+ stages: any[],
+ currentPath: string,
+ options: { selectedPath?: string }
+): AnyContainerNodeType[] => {
+ return stages.map((stage, idx) => {
+ // parallel stage
+ const groupKey = getGroupKey(stage)
+ if (groupKey === 'group') {
+ const name = stage.name ?? `Serial ${idx + 1}`
+ const path = `${currentPath}.${idx}`
+ const childrenPath = `${path}.${groupKey}.stages`
+
+ return {
+ type: ContentNodeTypes.serial,
+ config: {
+ minWidth: 192,
+ hideDeleteButton: true,
+ hideBeforeAdd: true,
+ hideAfterAdd: true
+ },
+ data: {
+ yamlPath: path,
+ yamlChildrenPath: childrenPath,
+ yamlEntityType: YamlEntityType.SerialGroup,
+ name
+ } satisfies StageNodeContentType,
+ children: processStages(stage[groupKey].stages, childrenPath, options)
+ } satisfies SerialContainerNodeType
+ } else if (groupKey === 'parallel') {
+ const name = stage.name ?? `Parallel ${idx + 1}`
+ const path = `${currentPath}.${idx}`
+ const childrenPath = `${path}.${groupKey}.stages`
+
+ return {
+ type: ContentNodeTypes.parallel,
+ config: {
+ minWidth: 192,
+ hideDeleteButton: true,
+ hideBeforeAdd: true,
+ hideAfterAdd: true
+ },
+ data: {
+ yamlPath: path,
+ yamlChildrenPath: childrenPath,
+ yamlEntityType: YamlEntityType.ParallelGroup,
+ name
+ } satisfies ParallelGroupContentNodeDataType,
+ children: processStages(stage[groupKey].stages, childrenPath, options)
+ } satisfies ParallelContainerNodeType
+ }
+ // regular stage
+ else {
+ const name = stage.name ?? `Stage ${idx + 1}`
+ const path = `${currentPath}.${idx}`
+ const childrenPath = `${path}.steps`
+
+ return {
+ type: ContentNodeTypes.stage,
+ config: {
+ minWidth: 192,
+ hideDeleteButton: true,
+ hideBeforeAdd: true,
+ hideAfterAdd: true
+ },
+ data: {
+ yamlPath: path,
+ yamlChildrenPath: childrenPath,
+ yamlEntityType: YamlEntityType.Stage,
+ name
+ } satisfies StageNodeContentType,
+ children: processSteps(stage.steps, childrenPath, options)
+ } satisfies SerialContainerNodeType
+ }
+ })
+}
+
+const processSteps = (
+ steps: any[],
+ currentPath: string,
+ options: { selectedPath?: string }
+): AnyContainerNodeType[] => {
+ return steps.map((step, idx) => {
+ // parallel stage
+ const groupKey = getGroupKey(step)
+ if (groupKey === 'group') {
+ const name = step.name ?? `Serial steps ${idx + 1}`
+ const path = `${currentPath}.${idx}`
+ const childrenPath = `${path}.${groupKey}.steps`
+
+ return {
+ type: ContentNodeTypes.serial,
+ config: {
+ minWidth: 192,
+ hideDeleteButton: true,
+ hideCollapseButton: false
+ },
+ data: {
+ yamlPath: path,
+ yamlChildrenPath: childrenPath,
+ yamlEntityType: YamlEntityType.StepSerialGroup,
+ name
+ } satisfies StageNodeContentType,
+
+ children: processSteps(step[groupKey].steps, childrenPath, options)
+ } satisfies SerialContainerNodeType
+ } else if (groupKey === 'parallel') {
+ const name = step.name ?? `Parallel steps ${idx + 1}`
+ const path = `${currentPath}.${idx}`
+ const childrenPath = `${path}.${groupKey}.steps`
+
+ return {
+ type: ContentNodeTypes.parallel,
+ config: {
+ minWidth: 192,
+ hideDeleteButton: true
+ },
+ data: {
+ yamlPath: path,
+ yamlChildrenPath: childrenPath,
+ yamlEntityType: YamlEntityType.StepParallelGroup,
+ name
+ } satisfies ParallelGroupContentNodeDataType,
+ children: processSteps(step[groupKey].steps, childrenPath, options)
+ } satisfies ParallelContainerNodeType
+ }
+ // regular step
+ else {
+ const name = getNameBasedOnStep(step, idx + 1)
+ const path = `${currentPath}.${idx}`
+
+ return {
+ type: ContentNodeTypes.step,
+ config: {
+ maxWidth: 140,
+ width: 140,
+ hideDeleteButton: false,
+ selectable: true
+ },
+ data: {
+ yamlPath: path,
+ yamlEntityType: YamlEntityType.Step,
+ name,
+ icon: ,
+ selected: path === options?.selectedPath
+ } satisfies StepNodeDataType
+ } satisfies LeafContainerNodeType
+ }
+ })
+}
diff --git a/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/pipeline-studio-graph-view.tsx b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/pipeline-studio-graph-view.tsx
new file mode 100644
index 000000000..8b75b01dd
--- /dev/null
+++ b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/pipeline-studio-graph-view.tsx
@@ -0,0 +1,138 @@
+import { useEffect, useState } from 'react'
+
+import { parse } from 'yaml'
+
+import {
+ AnyContainerNodeType,
+ CanvasProvider,
+ ContainerNode,
+ NodeContent,
+ PipelineGraph
+} from '@harnessio/pipeline-graph'
+
+import { usePipelineDataContext } from '../context/PipelineStudioDataProvider'
+import { CanvasControls } from './graph-implementation/canvas/canvas-controls'
+import { ContentNodeTypes } from './graph-implementation/content-node-types'
+import { EndNode } from './graph-implementation/nodes/end-node'
+import { StartNode } from './graph-implementation/nodes/start-node'
+import { StepNode } from './graph-implementation/nodes/step-node'
+import { yaml2Nodes } from './graph-implementation/utils/yaml-to-pipeline-graph'
+
+import '@harnessio/pipeline-graph/dist/index.css'
+
+import { NodeContextProvider } from '../context/NodeContextMenuProvider'
+import { AddNode, AddNodeDataType } from './graph-implementation/nodes/add-node'
+import { CommonNodeContextMenu } from './graph-implementation/nodes/common/CommonContextMenu'
+import { ParallelGroupContentNode } from './graph-implementation/nodes/parallel-group-node'
+import { SerialGroupContentNode } from './graph-implementation/nodes/serial-group-node'
+import { YamlEntityType } from './graph-implementation/types/nodes'
+
+const nodes: NodeContent[] = [
+ {
+ type: ContentNodeTypes.add,
+ component: AddNode,
+ containerType: ContainerNode.leaf
+ },
+ {
+ type: ContentNodeTypes.start,
+ component: StartNode,
+ containerType: ContainerNode.leaf
+ },
+ {
+ type: ContentNodeTypes.end,
+ containerType: ContainerNode.leaf,
+ component: EndNode
+ },
+ {
+ type: ContentNodeTypes.step,
+ containerType: ContainerNode.leaf,
+ component: StepNode
+ } as NodeContent,
+ {
+ type: ContentNodeTypes.parallel,
+ containerType: ContainerNode.parallel,
+ component: ParallelGroupContentNode
+ } as NodeContent,
+ {
+ type: ContentNodeTypes.serial,
+ containerType: ContainerNode.serial,
+ component: SerialGroupContentNode
+ } as NodeContent,
+ {
+ type: ContentNodeTypes.stage,
+ containerType: ContainerNode.serial,
+ component: SerialGroupContentNode
+ } as NodeContent
+]
+
+const startNode = {
+ type: ContentNodeTypes.start,
+ config: {
+ width: 40,
+ height: 40,
+ hideDeleteButton: true,
+ hideBeforeAdd: true,
+ hideLeftPort: true
+ },
+ data: {}
+} satisfies AnyContainerNodeType
+
+const endNode = {
+ type: ContentNodeTypes.end,
+ config: {
+ width: 40,
+ height: 40,
+ hideDeleteButton: true,
+ hideAfterAdd: true,
+ hideRightPort: true
+ },
+ data: {}
+} satisfies AnyContainerNodeType
+
+export const PipelineStudioGraphView = (): React.ReactElement => {
+ const {
+ state: { yamlRevision, editStepIntention }
+ } = usePipelineDataContext()
+
+ const [data, setData] = useState([])
+
+ useEffect(() => {
+ return () => {
+ setData([])
+ }
+ }, [])
+
+ useEffect(() => {
+ const yamlJson = parse(yamlRevision.yaml)
+ const newData = yaml2Nodes(yamlJson, { selectedPath: editStepIntention?.path })
+
+ if (newData.length === 0) {
+ newData.push({
+ type: ContentNodeTypes.add,
+ data: {
+ yamlChildrenPath: 'pipeline.stages',
+ name: '',
+ yamlEntityType: YamlEntityType.SerialGroup,
+ yamlPath: ''
+ } satisfies AddNodeDataType
+ })
+ }
+
+ newData.unshift(startNode)
+ newData.push(endNode)
+ setData(newData)
+ }, [yamlRevision, editStepIntention])
+
+ return (
+ // TODO: fix style.width
+
+ )
+}
diff --git a/apps/gitness/src/pages/pipeline-edit/components/pipeline-studio-header-actions.tsx b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/pipeline-studio-header-actions.tsx
similarity index 81%
rename from apps/gitness/src/pages/pipeline-edit/components/pipeline-studio-header-actions.tsx
rename to apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/pipeline-studio-header-actions.tsx
index 69c834ac5..12155351c 100644
--- a/apps/gitness/src/pages/pipeline-edit/components/pipeline-studio-header-actions.tsx
+++ b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/pipeline-studio-header-actions.tsx
@@ -1,10 +1,10 @@
import { useState } from 'react'
import { useParams } from 'react-router-dom'
-import { Button, DropdownMenuItem, SplitButton } from '@harnessio/canary'
+import { Button } from '@harnessio/canary'
import { OpenapiCommitFilesRequest, useCommitFilesMutation } from '@harnessio/code-service-client'
-import RunPipelineDialog from '../../run-pipeline-dialog/run-pipeline-dialog'
+import RunPipelineDialog from '../../../../pages/run-pipeline-dialog/run-pipeline-dialog'
import { PipelineParams, usePipelineDataContext } from '../context/PipelineStudioDataProvider'
const PipelineStudioHeaderActions = (): JSX.Element => {
@@ -69,27 +69,20 @@ const PipelineStudioHeaderActions = (): JSX.Element => {
Run
) : (
- handleSave(true)}
- dropdown={<>>>}
- menu={
- <>
- handleSave(false)} disabled={disabled}>
- Save
-
- >
- }
- >
- Save and Run
-
+ <>
+
+
+ >
)
}
return (
<>
-
+
{/*