diff --git a/apps/design-system/package.json b/apps/design-system/package.json index bcc260507e..65d6e08971 100644 --- a/apps/design-system/package.json +++ b/apps/design-system/package.json @@ -13,14 +13,18 @@ "typecheck": "tsc -b" }, "dependencies": { + "@harnessio/pipeline-graph": "workspace:*", "@harnessio/ui": "workspace:*", "@harnessio/yaml-editor": "workspace:*", + "@types/lodash-es": "^4.17.12", "clsx": "^2.1.1", + "lodash-es": "^4.17.21", "monaco-editor": "0.50.0", "react": "^17.0.2", "react-dom": "^17.0.2", "react-live": "^4.1.8", - "react-router-dom": "^6.26.0" + "react-router-dom": "^6.26.0", + "vite-plugin-monaco-editor": "^1.1.0" }, "devDependencies": { "@types/react": "^17.0.3", diff --git a/apps/design-system/src/pages/view-preview/view-preview.tsx b/apps/design-system/src/pages/view-preview/view-preview.tsx index c48e54dd6e..6ceb6fe671 100644 --- a/apps/design-system/src/pages/view-preview/view-preview.tsx +++ b/apps/design-system/src/pages/view-preview/view-preview.tsx @@ -8,6 +8,9 @@ import { RepoSettingsViewWrapper } from '@/pages/view-preview/repo-settings-view import ExecutionListWrapper from '@subjects/views/execution-list/execution-list' import { ProjectLabelsList } from '@subjects/views/labels/project-labels-list' import { RepoLabelsList } from '@subjects/views/labels/repo-labels-list' +import PipelineStudioWrapper from '@subjects/views/pipeline-edit/pipeline-edit' +import PipelineGraphWrapper from '@subjects/views/pipeline-graph/pipeline-graph' +import PipelineGraphMinimalWrapper from '@subjects/views/pipeline-graph/pipeline-graph-minimal' import PipelineListWrapper from '@subjects/views/pipeline-list/pipeline-list' import PullRequestCompareWrapper from '@subjects/views/pull-request-compare/pull-request-compare' import PullRequestChangesWrapper from '@subjects/views/pull-request-conversation/pull-request-changes-wrapper' @@ -148,6 +151,17 @@ export const viewPreviews: Record = { ), + 'pipeline-studio': , + 'pipeline-graph': ( + + + + ), + 'pipeline-graph-minimal': ( + + + + ), 'execution-list': ( diff --git a/apps/design-system/src/subjects/views/pipeline-edit/mocks/pipelineYaml1.ts b/apps/design-system/src/subjects/views/pipeline-edit/mocks/pipelineYaml1.ts new file mode 100644 index 0000000000..84cc0535f0 --- /dev/null +++ b/apps/design-system/src/subjects/views/pipeline-edit/mocks/pipelineYaml1.ts @@ -0,0 +1,19 @@ +export const pipelineYaml1 = `pipeline: + stages: + - group: + stages: + - parallel: + stages: + - steps: + - run: go build + - run: go test + - steps: + - run: npm test + - group: + stages: + - steps: + - run: go build + - steps: + - run: npm run + - run: npm test +` diff --git a/apps/design-system/src/subjects/views/pipeline-edit/mocks/pipelineYaml2.ts b/apps/design-system/src/subjects/views/pipeline-edit/mocks/pipelineYaml2.ts new file mode 100644 index 0000000000..9b07b6cef0 --- /dev/null +++ b/apps/design-system/src/subjects/views/pipeline-edit/mocks/pipelineYaml2.ts @@ -0,0 +1,11 @@ +export const pipelineYaml2 = ` +pipeline: + stages: + - group: + stages: [] + - group: + stages: + - steps: [] + - parallel: + stages: [] +` diff --git a/apps/design-system/src/subjects/views/pipeline-edit/pipeline-edit.tsx b/apps/design-system/src/subjects/views/pipeline-edit/pipeline-edit.tsx new file mode 100644 index 0000000000..546a587b1d --- /dev/null +++ b/apps/design-system/src/subjects/views/pipeline-edit/pipeline-edit.tsx @@ -0,0 +1,101 @@ +import { useState } from 'react' + +import { Button, ButtonGroup } from '@harnessio/ui/components' +import { + CommonNodeDataType, + deleteItemInArray, + injectItemInArray, + PipelineEdit, + YamlEntityType +} from '@harnessio/ui/views' + +import { pipelineYaml1 } from './mocks/pipelineYaml1' + +const PipelineStudioWrapper = () => { + const [yamlRevision, setYamlRevision] = useState({ yaml: pipelineYaml1 }) + const [view, setView] = useState<'graph' | 'yaml'>('graph') + + const processAddIntention = ( + nodeData: CommonNodeDataType, + position: 'after' | 'before' | 'in', + yamlEntityTypeToAdd?: YamlEntityType | undefined + ) => { + let newYaml = yamlRevision.yaml + + switch (yamlEntityTypeToAdd) { + case YamlEntityType.SerialStageGroup: + // NOTE: if we are adding in the array we have to provide path to children array + newYaml = injectItemInArray(yamlRevision.yaml, { + path: position === 'in' && nodeData.yamlChildrenPath ? nodeData.yamlChildrenPath : nodeData.yamlPath, + position, + item: { group: { stages: [] } } + }) + break + + case YamlEntityType.ParallelStageGroup: + // NOTE: if we are adding in the array we have to provide path to children array + newYaml = injectItemInArray(yamlRevision.yaml, { + path: position === 'in' && nodeData.yamlChildrenPath ? nodeData.yamlChildrenPath : nodeData.yamlPath, + position, + item: { parallel: { stages: [] } } + }) + break + + case YamlEntityType.Stage: + // NOTE: if we are adding in the array we have to provide path to children array + newYaml = injectItemInArray(yamlRevision.yaml, { + path: position === 'in' && nodeData.yamlChildrenPath ? nodeData.yamlChildrenPath : nodeData.yamlPath, + position, + item: { steps: [] } + }) + break + + default: + if (nodeData.yamlEntityType === YamlEntityType.Stage) { + // TODO: set addIntent state and open drawer.... + } + break + } + + setYamlRevision({ yaml: newYaml }) + } + + return ( +
+ + + + + + { + console.log('onAddIntention') + processAddIntention(nodeData, position, yamlEntityTypeToAdd) + }} + onDeleteIntention={data => { + console.log('onDeleteIntention') + const updatedYaml = deleteItemInArray(yamlRevision.yaml, { path: data.yamlPath }) + setYamlRevision({ yaml: updatedYaml }) + }} + onEditIntention={() => { + console.log('onEditIntention') + }} + onSelectIntention={() => { + console.log('onSelectIntention') + }} + onRevealInYaml={() => { + console.log('onSelectIntention') + }} + /> +
+ ) +} + +export default PipelineStudioWrapper diff --git a/apps/design-system/src/subjects/views/pipeline-graph/pipeline-graph-minimal.tsx b/apps/design-system/src/subjects/views/pipeline-graph/pipeline-graph-minimal.tsx new file mode 100644 index 0000000000..6be0e86026 --- /dev/null +++ b/apps/design-system/src/subjects/views/pipeline-graph/pipeline-graph-minimal.tsx @@ -0,0 +1,229 @@ +import { + AnyContainerNodeType, + CanvasProvider, + ContainerNode, + LeafNodeInternalType, + NodeContent, + ParallelNodeContent, + ParallelNodeInternalType, + PipelineGraph, + SerialNodeContent, + SerialNodeInternalType +} from '@harnessio/pipeline-graph' +import { Icon, Text } from '@harnessio/ui/components' + +// ***************************************************** +// 1. Import CSS +// ***************************************************** + +import '@harnessio/pipeline-graph/dist/index.css' + +// ***************************************************** +// 2. Define content nodes types +// ***************************************************** + +export enum ContentNodeTypes { + step = 'step', + parallel = 'parallel', + serial = 'serial' +} + +// ***************************************************** +// 3. Define nodes +// ***************************************************** + +// * step node +export interface StepNodeDataType { + name?: string + icon?: React.ReactElement + selected?: boolean +} + +export function StepNodeComponent({ node }: { node: LeafNodeInternalType }) { + const { name, icon } = node.data + + return ( +
+
{icon}
+ + {name} + +
+ ) +} + +// * serial group node +export interface SerialGroupNodeDataType { + name?: string + selected?: boolean +} + +export function SerialGroupNodeComponent({ + node, + children +}: { + node: SerialNodeInternalType + children: React.ReactElement +}) { + const { name } = node.data + + return ( + <> +
+
+
+ {name} +
+
+ + {children} + + ) +} + +// * parallel group node +export interface ParallelGroupNodeDataType { + name?: string + selected?: boolean +} + +export function ParallelGroupNodeComponent({ + node, + children +}: { + node: ParallelNodeInternalType + children: React.ReactElement +}) { + const { name } = node.data + + return ( + <> +
+
+
+ {name} +
+
+ + {children} + + ) +} + +// ***************************************************** +// 4. Match Content and containers nodes +// ***************************************************** + +const nodes: NodeContent[] = [ + { + type: ContentNodeTypes.serial, + containerType: ContainerNode.serial, + component: SerialGroupNodeComponent + } as SerialNodeContent, + { + type: ContentNodeTypes.parallel, + containerType: ContainerNode.parallel, + component: ParallelGroupNodeComponent + } as ParallelNodeContent, + { + type: ContentNodeTypes.step, + containerType: ContainerNode.leaf, + component: StepNodeComponent + } as NodeContent +] + +// ***************************************************** +// 5. Graph data model +// ***************************************************** + +const data: AnyContainerNodeType[] = [ + { + type: ContentNodeTypes.step, + data: { + name: 'Step 1', + icon: + } satisfies StepNodeDataType, + config: { + width: 160, + height: 80 + } + }, + { + type: ContentNodeTypes.serial, + config: { + minWidth: 200, + minHeight: 40 + }, + data: { + name: 'Serial group' + } satisfies SerialGroupNodeDataType, + children: [ + { + type: ContentNodeTypes.step, + data: { + name: 'Step 2', + icon: + } satisfies StepNodeDataType, + config: { + width: 160, + height: 80 + } + }, + { + type: ContentNodeTypes.step, + data: { + name: 'Step 3', + icon: + } satisfies StepNodeDataType, + config: { + width: 160, + height: 80 + } + } + ] + }, + { + type: ContentNodeTypes.parallel, + config: { + minWidth: 200, + minHeight: 40 + }, + data: { + name: 'Parallel group' + } satisfies ParallelGroupNodeDataType, + children: [ + { + type: ContentNodeTypes.step, + data: { + name: 'Step 4', + icon: + } satisfies StepNodeDataType, + config: { + width: 160, + height: 80 + } + }, + { + type: ContentNodeTypes.step, + data: { + name: 'Step 4', + icon: + } satisfies StepNodeDataType, + config: { + width: 160, + height: 80 + } + } + ] + } +] + +const PipelineGraphMinimalWrapper = () => { + return ( + + + + ) +} + +export default PipelineGraphMinimalWrapper diff --git a/apps/design-system/src/subjects/views/pipeline-graph/pipeline-graph.tsx b/apps/design-system/src/subjects/views/pipeline-graph/pipeline-graph.tsx new file mode 100644 index 0000000000..ba5fe8305d --- /dev/null +++ b/apps/design-system/src/subjects/views/pipeline-graph/pipeline-graph.tsx @@ -0,0 +1,283 @@ +import { + AnyContainerNodeType, + CanvasProvider, + ContainerNode, + LeafNodeInternalType, + NodeContent, + ParallelNodeContent, + ParallelNodeInternalType, + PipelineGraph, + SerialNodeContent, + SerialNodeInternalType +} from '@harnessio/pipeline-graph' +import { Icon, PipelineNodes } from '@harnessio/ui/components' + +// ***************************************************** +// 1. Import CSS +// ***************************************************** + +import '@harnessio/pipeline-graph/dist/index.css' + +// ***************************************************** +// 2. Define content nodes types +// ***************************************************** + +export enum ContentNodeTypes { + add = 'add', + start = 'start', + end = 'end', + step = 'step', + approval = 'approval', + parallel = 'parallel', + serial = 'serial', + stage = 'stage' +} + +// ***************************************************** +// 3. Define nodes +// ***************************************************** + +// * start node +const StartNodeComponent = () => + +// * end node +const EndNodeComponent = () => + +// * step node +export interface StepNodeDataType { + name?: string + icon?: React.ReactElement + selected?: boolean +} + +export function StepNodeComponent({ node }: { node: LeafNodeInternalType }) { + const { name, icon } = node.data + + return undefined} /> +} + +// * approval step node +export interface ApprovalNodeDataType { + name?: string + selected?: boolean +} + +export function ApprovalStepNodeComponent({ node }: { node: LeafNodeInternalType }) { + const { name } = node.data + + return ( +
+
+
{name}
+
+ ) +} + +// * serial group node +export interface SerialGroupNodeDataType { + name?: string + selected?: boolean +} + +export function SerialGroupNodeComponent({ + node, + children +}: { + node: SerialNodeInternalType + children: React.ReactElement +}) { + const { name } = node.data + + return ( + undefined} onAddClick={() => undefined}> + {children} + + ) +} + +// * parallel group node +export interface ParallelGroupNodeDataType { + name?: string + selected?: boolean +} + +export function ParallelGroupNodeComponent({ + node, + children +}: { + node: ParallelNodeInternalType + children: React.ReactElement +}) { + const { name } = node.data + + return ( + undefined} onAddClick={() => undefined}> + {children} + + ) +} + +// ***************************************************** +// 4. Match Content and containers nodes +// ***************************************************** + +const nodes: NodeContent[] = [ + { + type: ContentNodeTypes.start, + containerType: ContainerNode.leaf, + component: StartNodeComponent + }, + { + type: ContentNodeTypes.end, + containerType: ContainerNode.leaf, + component: EndNodeComponent + }, + { + type: ContentNodeTypes.serial, + containerType: ContainerNode.serial, + component: SerialGroupNodeComponent + } as SerialNodeContent, + { + type: ContentNodeTypes.parallel, + containerType: ContainerNode.parallel, + component: ParallelGroupNodeComponent + } as ParallelNodeContent, + { + type: ContentNodeTypes.step, + containerType: ContainerNode.leaf, + component: StepNodeComponent + } as NodeContent, + { + type: ContentNodeTypes.approval, + containerType: ContainerNode.leaf, + component: ApprovalStepNodeComponent + } as NodeContent +] + +// ***************************************************** +// 5. Graph data model +// ***************************************************** + +const data: AnyContainerNodeType[] = [ + { + type: ContentNodeTypes.start, + data: {}, + config: { + width: 40, + height: 40, + hideLeftPort: true + } + }, + { + type: ContentNodeTypes.step, + data: { + name: 'Step 1', + icon: + } satisfies StepNodeDataType, + config: { + width: 160, + height: 80 + } + }, + { + type: ContentNodeTypes.approval, + data: { + name: 'Approval 1', + icon: + } satisfies StepNodeDataType, + config: { + width: 120, + height: 120 + } + }, + { + type: ContentNodeTypes.serial, + config: { + minWidth: 200, + minHeight: 40 + }, + data: { + name: 'Serial group' + } satisfies SerialGroupNodeDataType, + children: [ + { + type: ContentNodeTypes.step, + data: { + name: 'Step 2', + icon: + } satisfies StepNodeDataType, + config: { + width: 160, + height: 80 + } + }, + { + type: ContentNodeTypes.step, + data: { + name: 'Step 3', + icon: + } satisfies StepNodeDataType, + config: { + width: 160, + height: 80 + } + } + ] + }, + { + type: ContentNodeTypes.parallel, + config: { + minWidth: 200, + minHeight: 40 + }, + data: { + name: 'Parallel group' + } satisfies ParallelGroupNodeDataType, + children: [ + { + type: ContentNodeTypes.step, + data: { + name: 'Step 4', + icon: + } satisfies StepNodeDataType, + config: { + width: 160, + height: 80 + } + }, + { + type: ContentNodeTypes.step, + data: { + name: 'Step 4', + icon: + } satisfies StepNodeDataType, + config: { + width: 160, + height: 80 + } + } + ] + }, + { + type: ContentNodeTypes.end, + data: {}, + config: { + width: 40, + height: 40, + hideRightPort: true + } + } +] + +const PipelineGraphWrapper = () => { + return ( + + + + ) +} + +export default PipelineGraphWrapper diff --git a/apps/design-system/vite.config.ts b/apps/design-system/vite.config.ts index 3cdbc8b96b..521c9515d0 100644 --- a/apps/design-system/vite.config.ts +++ b/apps/design-system/vite.config.ts @@ -1,8 +1,14 @@ import react from '@vitejs/plugin-react-swc' import { defineConfig } from 'vite' +import monacoEditorPlugin from 'vite-plugin-monaco-editor' import tsConfigPaths from 'vite-tsconfig-paths' // https://vite.dev/config/ export default defineConfig({ - plugins: [react(), tsConfigPaths()] + plugins: [ + react(), + tsConfigPaths(), + // @ts-expect-error: @TODO: Fix this. Should be removed, added to enable typecheck + monacoEditorPlugin.default({ customWorkers: [{ entry: 'monaco-yaml/yaml.worker', label: 'yaml' }] }) + ] }) 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 index d2fa3fb852..78bf4c05f3 100644 --- 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 @@ -1,6 +1,5 @@ -import { Icon } from '@harnessio/canary' import { LeafNodeInternalType } from '@harnessio/pipeline-graph' -import { Button } from '@harnessio/ui/components' +import { PipelineNodes } from '@harnessio/ui/components' import { useNodeContext } from '../../../context/NodeContextMenuProvider' import { CommonNodeDataType } from '../types/nodes' @@ -14,26 +13,10 @@ export function AddNode(props: { node: LeafNodeInternalType }) const { handleAddIn } = useNodeContext() return ( -
{ + handleAddIn(data, e.currentTarget) }} - > - -
+ /> ) } 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 index 43ad739df0..c3f19e344b 100644 --- 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 @@ -1,19 +1,8 @@ import { LeafNodeInternalType } from '@harnessio/pipeline-graph' +import { PipelineNodes } from '@harnessio/ui/components' export interface EndNodeDataType {} export function EndNode(_props: { node: LeafNodeInternalType }) { - return ( -
- {/* TODO: replace with icon */} -
-
- ) + return } 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 index 92132dbe22..7cdda5047c 100644 --- 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 @@ -1,5 +1,5 @@ import { ParallelNodeInternalType } from '@harnessio/pipeline-graph' -import { Button, Icon } from '@harnessio/ui/components' +import { PipelineNodes } from '@harnessio/ui/components' import { useNodeContext } from '../../../context/NodeContextMenuProvider' import { CommonNodeDataType } from '../types/nodes' @@ -19,51 +19,19 @@ export function ParallelGroupContentNode(props: { const { showContextMenu, handleAddIn } = useNodeContext() return ( - <> -
- -
- {/* //flex h-9 items-center */} -
- {data.name} -
-
- - - - {!collapsed && node.children.length === 0 && ( - - )} - + { + handleAddIn(data, e.currentTarget) + }} + onEllipsisClick={e => { + e.stopPropagation() + showContextMenu(data, e.currentTarget) + }} + > {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 index 4c90d91249..3708a0ec1d 100644 --- 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 @@ -1,5 +1,5 @@ import { SerialNodeInternalType } from '@harnessio/pipeline-graph' -import { Button, Icon } from '@harnessio/ui/components' +import { PipelineNodes } from '@harnessio/ui/components' import { useNodeContext } from '../../../context/NodeContextMenuProvider' import { CommonNodeDataType } from '../types/nodes' @@ -19,52 +19,19 @@ export function SerialGroupContentNode(props: { const { showContextMenu, handleAddIn } = useNodeContext() return ( - <> -
- -
- {/* //flex h-9 items-center */} -
- {data.name} -
-
- - - - {!collapsed && node.children.length === 0 && ( - - )} - + { + handleAddIn(data, e.currentTarget) + }} + onEllipsisClick={e => { + e.stopPropagation() + showContextMenu(data, e.currentTarget) + }} + > {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 index d481c7a7e6..e53c1a364f 100644 --- 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 @@ -1,31 +1,37 @@ -import { Tooltip, TooltipContent, TooltipTrigger } from '@harnessio/canary' import { SerialNodeInternalType } from '@harnessio/pipeline-graph' +import { PipelineNodes } from '@harnessio/ui/components' +import { useNodeContext } from '../../../context/NodeContextMenuProvider' import { CommonNodeDataType } from '../types/nodes' -export interface StageNodeContentType extends CommonNodeDataType { +export interface StageContentNodeDataType extends CommonNodeDataType { icon?: React.ReactElement } -export function SerialGroupNodeContent(props: { - node: SerialNodeInternalType - children: React.ReactElement +export function StageContentNode(props: { + node: SerialNodeInternalType + children?: React.ReactElement + collapsed?: boolean }) { - const { node, children } = props - const data = node.data as StageNodeContentType + const { node, children, collapsed } = props + const data = node.data as StageContentNodeDataType + + const { showContextMenu, handleAddIn } = useNodeContext() return ( - <> -
-
- - -
{data.name}
-
- {data.name} -
-
+ { + handleAddIn(data, e.currentTarget) + }} + onEllipsisClick={e => { + e.stopPropagation() + showContextMenu(data, e.currentTarget) + }} + > {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 index d7f45ed028..70a425aa20 100644 --- 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 @@ -1,19 +1,8 @@ -import { Icon } from '@harnessio/canary' import { LeafNodeInternalType } from '@harnessio/pipeline-graph' +import { PipelineNodes } from '@harnessio/ui/components' export interface StartNodeDataType {} export function StartNode(_props: { node: LeafNodeInternalType }) { - return ( -
- -
- ) + 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 index 1930d5a188..0b4bc8f294 100644 --- 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 @@ -1,7 +1,7 @@ import { useMemo } from 'react' import { LeafNodeInternalType } from '@harnessio/pipeline-graph' -import { Button, Icon, Text } from '@harnessio/ui/components' +import { PipelineNodes } from '@harnessio/ui/components' import { useNodeContext } from '../../../context/NodeContextMenuProvider' import { CommonNodeDataType } from '../types/nodes' @@ -21,31 +21,14 @@ export function StepNode(props: { node: LeafNodeInternalType } const selected = useMemo(() => selectionPath === data.yamlPath, [selectionPath]) return ( -
{ + e.stopPropagation() + showContextMenu(data, e.currentTarget) }} - > - - -
{data.icon}
- - {data.name} - -
+ selected={selected} + > ) } 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 index 13bb9cb4dc..7683839849 100644 --- 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 @@ -8,7 +8,7 @@ import { import { ContentNodeTypes } from '../content-node-types' import { ParallelGroupContentNodeDataType } from '../nodes/parallel-group-node' -import { StageNodeContentType } from '../nodes/stage-node' +import { StageContentNodeDataType } from '../nodes/stage-node' import { StepNodeDataType } from '../nodes/step-node' import { YamlEntityType } from '../types/nodes' import { getIconBasedOnStep } from './step-icon-utils' @@ -62,7 +62,7 @@ const processStages = ( yamlChildrenPath: childrenPath, yamlEntityType: YamlEntityType.SerialGroup, name - } satisfies StageNodeContentType, + } satisfies StageContentNodeDataType, children: processStages(stage[groupKey].stages, childrenPath, options) } satisfies SerialContainerNodeType } else if (groupKey === 'parallel') { @@ -106,7 +106,7 @@ const processStages = ( yamlChildrenPath: childrenPath, yamlEntityType: YamlEntityType.Stage, name - } satisfies StageNodeContentType, + } satisfies StageContentNodeDataType, children: processSteps(stage.steps, childrenPath, options) } satisfies SerialContainerNodeType } @@ -138,7 +138,7 @@ const processSteps = ( yamlChildrenPath: childrenPath, yamlEntityType: YamlEntityType.StepSerialGroup, name - } satisfies StageNodeContentType, + } satisfies StageContentNodeDataType, children: processSteps(step[groupKey].steps, childrenPath, options) } satisfies SerialContainerNodeType 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 index 8b75b01dda..219a7bd575 100644 --- 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 @@ -25,6 +25,7 @@ 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 { StageContentNode } from './graph-implementation/nodes/stage-node' import { YamlEntityType } from './graph-implementation/types/nodes' const nodes: NodeContent[] = [ @@ -61,7 +62,7 @@ const nodes: NodeContent[] = [ { type: ContentNodeTypes.stage, containerType: ContainerNode.serial, - component: SerialGroupContentNode + component: StageContentNode } as NodeContent ] @@ -128,7 +129,7 @@ export const PipelineStudioGraphView = (): React.ReactElement => {
- + diff --git a/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/pipeline-studio.tsx b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/pipeline-studio.tsx index a5fb300969..c3161a2df4 100644 --- a/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/pipeline-studio.tsx +++ b/apps/gitness/src/pages-v2/pipeline/pipeline-edit/components/pipeline-studio.tsx @@ -122,7 +122,7 @@ export default function PipelineEdit() { ) return ( -
+
setView(view as PipelineStudioView)} /> diff --git a/packages/pipeline-graph/examples/src/example1.tsx b/packages/pipeline-graph/examples/src/example1.tsx index 8bbcca712c..8ec0ab1168 100644 --- a/packages/pipeline-graph/examples/src/example1.tsx +++ b/packages/pipeline-graph/examples/src/example1.tsx @@ -12,7 +12,7 @@ import { ParallelContainerNodeType, SerialContainerNodeType } from '../../src/types/nodes' -import { getPathPeaces } from '../../src/utils/path-utils' +// import { getPathPieces } from '../../src/utils/path-utils' import { ApprovalNode } from './nodes/approval-node' import { EndNode } from './nodes/end-node' import { ParallelGroupNodeContent } from './nodes/parallel-group-node' @@ -135,7 +135,7 @@ function Example1({ addStepType }: { addStepType: ContentNodeTypes }) { // set(newData, childrenPath, arr) // } else { // // add before or after - // const { arrayPath, index } = getPathPeaces(itemPath) + // const { arrayPath, index } = getPathPieces(itemPath) // const arr = arrayPath ? (get(newData, arrayPath) as unknown[]) : newData // arr.splice(position === 'before' ? index : index + 1, 0, getNode(addStepType)) @@ -145,7 +145,7 @@ function Example1({ addStepType }: { addStepType: ContentNodeTypes }) { // }} // onDelete={(node: AnyNodeInternal) => { // const newData = cloneDeep(data) - // const { arrayPath, index } = getPathPeaces(node.path.replace(/^pipeline.children./, '')) + // const { arrayPath, index } = getPathPieces(node.path.replace(/^pipeline.children./, '')) // const arr = arrayPath ? (get(newData, arrayPath) as unknown[]) : newData // arr.splice(index, 1) // setData(newData) diff --git a/packages/pipeline-graph/src/components/nodes/parallel-container.tsx b/packages/pipeline-graph/src/components/nodes/parallel-container.tsx index b8adf39f79..3a13ab22be 100644 --- a/packages/pipeline-graph/src/components/nodes/parallel-container.tsx +++ b/packages/pipeline-graph/src/components/nodes/parallel-container.tsx @@ -45,18 +45,22 @@ export default function ParallelNodeContainer(props: ContainerNodeProps - - + {!node.config?.hideLeftPort && ( + + )} + {!node.config?.hideRightPort && ( + + )}
diff --git a/packages/pipeline-graph/src/components/nodes/serial-container.tsx b/packages/pipeline-graph/src/components/nodes/serial-container.tsx index 3480a064b1..6846498e52 100644 --- a/packages/pipeline-graph/src/components/nodes/serial-container.tsx +++ b/packages/pipeline-graph/src/components/nodes/serial-container.tsx @@ -44,18 +44,22 @@ export default function SerialNodeContainer(props: ContainerNodeProps - - + {!node.config?.hideLeftPort && ( + + )} + {!node.config?.hideRightPort && ( + + )}
diff --git a/packages/pipeline-graph/src/pipeline-graph-internal.tsx b/packages/pipeline-graph/src/pipeline-graph-internal.tsx index 40244d9a1c..2a9963f959 100644 --- a/packages/pipeline-graph/src/pipeline-graph-internal.tsx +++ b/packages/pipeline-graph/src/pipeline-graph-internal.tsx @@ -12,13 +12,16 @@ import { addPaths } from './utils/path-utils' export interface PipelineGraphInternalProps { data: AnyContainerNodeType[] + config?: { + edgeClassName?: string + } } export function PipelineGraphInternal(props: PipelineGraphInternalProps) { const { initialized, nodes: nodesBank, rerenderConnections, setInitialized } = useGraphContext() const { setCanvasTransform, canvasTransformRef, config: canvasConfig, setTargetEl } = useCanvasContext() - const { data } = props + const { data, config = {} } = props const graphSizeRef = useRef<{ h: number; w: number } | undefined>() const svgGroupRef = useRef(null) @@ -65,7 +68,7 @@ export function PipelineGraphInternal(props: PipelineGraphInternalProps) { if (svgGroupRef.current) { let allPaths: string[] = [] connections.map(portPair => { - const path = getPortsConnectionPath(rootContainerEl, portPair) + const path = getPortsConnectionPath(rootContainerEl, portPair, config.edgeClassName) allPaths.push(path) }) svgGroupRef.current.innerHTML = allPaths.join('') diff --git a/packages/pipeline-graph/src/pipeline-graph.tsx b/packages/pipeline-graph/src/pipeline-graph.tsx index c23563bf06..1f5ee78f7b 100644 --- a/packages/pipeline-graph/src/pipeline-graph.tsx +++ b/packages/pipeline-graph/src/pipeline-graph.tsx @@ -10,12 +10,12 @@ export interface PipelineGraphProps extends PipelineGraphInternalProps { } export function PipelineGraph(props: PipelineGraphProps) { - const { data, nodes } = props + const { data, nodes, config } = props return ( - + ) diff --git a/packages/pipeline-graph/src/render/render-svg-lines.ts b/packages/pipeline-graph/src/render/render-svg-lines.ts index d190ab54d6..d63b52e8b7 100644 --- a/packages/pipeline-graph/src/render/render-svg-lines.ts +++ b/packages/pipeline-graph/src/render/render-svg-lines.ts @@ -17,7 +17,8 @@ export function getPortsConnectionPath( serial?: { position: 'left' | 'right' } - } + }, + edgeClassName?: string ) { const { source, target, parallel, serial } = connection @@ -31,15 +32,16 @@ export function getPortsConnectionPath( const pipelineGraphRootBB = pipelineGraphRoot?.getBoundingClientRect() ?? new DOMRect(0, 0) - const pathHtml = getPath( - `${source}-${target}`, - fromElBB.left - pipelineGraphRootBB.left, - fromElBB.top - pipelineGraphRootBB.top, - toElBB.left - pipelineGraphRootBB.left, - toElBB.top - pipelineGraphRootBB.top, + const pathHtml = getPath({ + id: `${source}-${target}`, + startX: fromElBB.left - pipelineGraphRootBB.left, + startY: fromElBB.top - pipelineGraphRootBB.top, + endX: toElBB.left - pipelineGraphRootBB.left, + endY: toElBB.top - pipelineGraphRootBB.top, parallel, - serial - ) + serial, + edgeClassName + }) return pathHtml } @@ -76,19 +78,29 @@ function getVArcConfig(direction: 'down' | 'up') { } } -function getPath( - id: string, - startX: number, - startY: number, - endX: number, - endY: number, +function getPath({ + id, + startX, + startY, + endX, + endY, + parallel, + serial, + edgeClassName +}: { + id: string + startX: number + startY: number + endX: number + endY: number parallel?: { position: 'left' | 'right' - }, + } serial?: { position: 'left' | 'right' } -) { + edgeClassName?: string +}) { const correction = 3 let path = '' @@ -136,6 +148,8 @@ function getPath( (endX + correction) } - // TODO: line style (color) - return `` + // NOTE: if edgeClassName is not provided use hardcoded color + const pathStyle = edgeClassName ? ` class="${edgeClassName}"` : ` stroke="#5D5B65"` + + return `` } diff --git a/packages/pipeline-graph/src/utils/layout-utils.ts b/packages/pipeline-graph/src/utils/layout-utils.ts index 06d352beea..84555aa8fa 100644 --- a/packages/pipeline-graph/src/utils/layout-utils.ts +++ b/packages/pipeline-graph/src/utils/layout-utils.ts @@ -3,13 +3,13 @@ import { SERIAL_GROUP_ADJUSTMENT } from '../components/nodes/serial-container' import { ContainerNode } from '../types/nodes' import { AnyNodeInternal, ParallelNodeInternalType, SerialNodeInternalType } from '../types/nodes-internal' -export function getThreeDepth(node: AnyNodeInternal): number { +export function getTreeDepth(node: AnyNodeInternal): number { if (node.containerType === ContainerNode.leaf) { return 1 } else { let maxRet = 1 node.children.forEach(item => { - maxRet = Math.max(getThreeDepth(item), maxRet) + maxRet = Math.max(getTreeDepth(item), maxRet) }) return maxRet + 1 diff --git a/packages/pipeline-graph/src/utils/path-utils.ts b/packages/pipeline-graph/src/utils/path-utils.ts index 7ba20c2da4..5e239a6f76 100644 --- a/packages/pipeline-graph/src/utils/path-utils.ts +++ b/packages/pipeline-graph/src/utils/path-utils.ts @@ -27,7 +27,7 @@ export function addPaths( } /** split path of item to 1. path to array and 2. element index */ -export function getPathPeaces(path: string) { +export function getPathPieces(path: string) { const peaces = path.split('.') if (peaces.length === 1) { diff --git a/packages/ui/global.d.ts b/packages/ui/global.d.ts new file mode 100644 index 0000000000..e609ee4487 --- /dev/null +++ b/packages/ui/global.d.ts @@ -0,0 +1,19 @@ +declare module 'monaco-editor/esm/vs/editor/common/services/languageFeatures.js' { + export const ILanguageFeaturesService: { documentSymbolProvider: unknown } +} + +declare module 'monaco-editor/esm/vs/editor/contrib/documentSymbols/browser/outlineModel.js' { + import type { editor, languages } from 'monaco-editor' + + export abstract class OutlineModel { + static create(registry: unknown, model: editor.ITextModel): Promise + + asListOfDocumentSymbols(): languages.DocumentSymbol[] + } +} + +declare module 'monaco-editor/esm/vs/editor/standalone/browser/standaloneServices.js' { + export const StandaloneServices: { + get: (id: unknown) => { documentSymbolProvider: unknown } + } +} diff --git a/packages/ui/package.json b/packages/ui/package.json index 0ffb54db3c..3dc447c6e7 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -54,6 +54,8 @@ "@dnd-kit/utilities": "^3.2.2", "@git-diff-view/react": "^0.0.21", "@git-diff-view/shiki": "^0.0.21", + "@harnessio/pipeline-graph": "workspace:*", + "@harnessio/yaml-editor": "workspace:*", "@hookform/resolvers": "^3.6.0", "@radix-ui/react-accordion": "^1.2.2", "@radix-ui/react-alert-dialog": "^1.1.4", @@ -99,6 +101,7 @@ "i18next-browser-languagedetector": "^8.0.0", "input-otp": "^1.2.4", "lodash-es": "^4.17.21", + "monaco-editor": "0.50.0", "overlayscrollbars": "^2.10.0", "react-day-picker": "^8.10.1", "react-hook-form": "^7.28.0", @@ -112,6 +115,7 @@ "sonner": "^1.5.0", "tailwind-merge": "^2.3.0", "tailwindcss-animate": "^1.0.7", + "yaml": "^2.7.0", "zod": "^3.23.8" }, "peerDependencies": { diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index 518e745bf9..a9caff2f8b 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -68,6 +68,7 @@ export * from './multi-select' export * from './button-with-options' export * from './tooltip' export * from './navigation-menu' +export * from './pipeline-nodes' export * from './sidebar/sidebar' export * as NodeGroup from './node-group' diff --git a/packages/ui/src/components/pipeline-nodes/add-node.tsx b/packages/ui/src/components/pipeline-nodes/add-node.tsx new file mode 100644 index 0000000000..dd02fafcd1 --- /dev/null +++ b/packages/ui/src/components/pipeline-nodes/add-node.tsx @@ -0,0 +1,24 @@ +import { Button, Icon } from '..' + +export interface AddNodeProp { + onClick?: (event: React.MouseEvent) => void +} + +export function AddNode(props: AddNodeProp) { + const { onClick } = props + + return ( +
+ +
+ ) +} diff --git a/packages/ui/src/components/pipeline-nodes/end-node.tsx b/packages/ui/src/components/pipeline-nodes/end-node.tsx new file mode 100644 index 0000000000..824c4e72e6 --- /dev/null +++ b/packages/ui/src/components/pipeline-nodes/end-node.tsx @@ -0,0 +1,8 @@ +export function EndNode() { + return ( +
+ {/* TODO: replace with icon */} +
+
+ ) +} diff --git a/packages/ui/src/components/pipeline-nodes/index.ts b/packages/ui/src/components/pipeline-nodes/index.ts new file mode 100644 index 0000000000..07a836e9ac --- /dev/null +++ b/packages/ui/src/components/pipeline-nodes/index.ts @@ -0,0 +1,17 @@ +import { AddNode } from './add-node' +import { EndNode } from './end-node' +import { ParallelGroupNode } from './parallel-group-node' +import { SerialGroupNode } from './serial-group-node' +import { StageNode } from './stage-node' +import { StartNode } from './start-node' +import { StepNode } from './step-node' + +export const PipelineNodes = { + AddNode, + StageNode, + StartNode, + EndNode, + StepNode, + SerialGroupNode, + ParallelGroupNode +} diff --git a/packages/ui/src/components/pipeline-nodes/parallel-group-node.tsx b/packages/ui/src/components/pipeline-nodes/parallel-group-node.tsx new file mode 100644 index 0000000000..376d38b727 --- /dev/null +++ b/packages/ui/src/components/pipeline-nodes/parallel-group-node.tsx @@ -0,0 +1,50 @@ +import { Button, Icon } from '..' + +export interface ParallelGroupNodeProps { + name?: string + children?: React.ReactElement + collapsed?: boolean + isEmpty?: boolean + onEllipsisClick: (e: React.MouseEvent) => void + onAddClick: (e: React.MouseEvent) => void +} + +export function ParallelGroupNode(props: ParallelGroupNodeProps) { + const { name, children, collapsed, isEmpty, onEllipsisClick, onAddClick } = props + + return ( + <> +
+ +
+
+ {name} +
+
+ + + + {!collapsed && isEmpty && ( + + )} + + {children} + + ) +} diff --git a/packages/ui/src/components/pipeline-nodes/serial-group-node.tsx b/packages/ui/src/components/pipeline-nodes/serial-group-node.tsx new file mode 100644 index 0000000000..ba3d107bc3 --- /dev/null +++ b/packages/ui/src/components/pipeline-nodes/serial-group-node.tsx @@ -0,0 +1,50 @@ +import { Button, Icon } from '..' + +export interface SerialGroupNodeProps { + name?: string + children?: React.ReactElement + collapsed?: boolean + isEmpty?: boolean + onEllipsisClick: (e: React.MouseEvent) => void + onAddClick: (e: React.MouseEvent) => void +} + +export function SerialGroupNode(props: SerialGroupNodeProps) { + const { name, children, collapsed, isEmpty, onEllipsisClick, onAddClick } = props + + return ( + <> +
+ +
+
+ {name} +
+
+ + + + {!collapsed && isEmpty && ( + + )} + + {children} + + ) +} diff --git a/packages/ui/src/components/pipeline-nodes/stage-node.tsx b/packages/ui/src/components/pipeline-nodes/stage-node.tsx new file mode 100644 index 0000000000..d5ec7de7e4 --- /dev/null +++ b/packages/ui/src/components/pipeline-nodes/stage-node.tsx @@ -0,0 +1,52 @@ +import { Button, Icon } from '..' + +export interface StageNodeProps { + name?: string + children?: React.ReactElement + collapsed?: boolean + isEmpty?: boolean + onEllipsisClick?: (e: React.MouseEvent) => void + onAddClick: (e: React.MouseEvent) => void +} + +export function StageNode(props: StageNodeProps) { + const { name, children, collapsed, isEmpty, onEllipsisClick, onAddClick } = props + + return ( + <> +
+ +
+
+ {name} +
+
+ + {onEllipsisClick && ( + + )} + + {!collapsed && isEmpty && ( + + )} + + {children} + + ) +} diff --git a/packages/ui/src/components/pipeline-nodes/start-node.tsx b/packages/ui/src/components/pipeline-nodes/start-node.tsx new file mode 100644 index 0000000000..192e1fb79d --- /dev/null +++ b/packages/ui/src/components/pipeline-nodes/start-node.tsx @@ -0,0 +1,9 @@ +import { Icon } from '..' + +export function StartNode() { + return ( +
+ +
+ ) +} diff --git a/packages/ui/src/components/pipeline-nodes/step-node.tsx b/packages/ui/src/components/pipeline-nodes/step-node.tsx new file mode 100644 index 0000000000..d9b943b8ef --- /dev/null +++ b/packages/ui/src/components/pipeline-nodes/step-node.tsx @@ -0,0 +1,40 @@ +import { cn } from '@utils/cn' + +import { Button, Icon, Text } from '..' + +export interface StepNodeProps { + name?: string + icon?: React.ReactNode + selected?: boolean + onEllipsisClick?: (e: React.MouseEvent) => void +} + +export function StepNode(props: StepNodeProps) { + const { name, icon, selected, onEllipsisClick } = props + + return ( +
+ {onEllipsisClick && ( + + )} + +
{icon}
+ + {name} + +
+ ) +} diff --git a/packages/ui/src/views/index.ts b/packages/ui/src/views/index.ts index 858b4e8032..02ce3995f2 100644 --- a/packages/ui/src/views/index.ts +++ b/packages/ui/src/views/index.ts @@ -39,6 +39,9 @@ export * from './profile-settings' // pipelines export * from './pipelines' +// pipeline-edit +export * from './pipeline-edit' + // user-management export * from './user-management' diff --git a/packages/ui/src/views/pipeline-edit/components/graph-implementation/canvas/canvas-button.tsx b/packages/ui/src/views/pipeline-edit/components/graph-implementation/canvas/canvas-button.tsx new file mode 100644 index 0000000000..25f5e643c6 --- /dev/null +++ b/packages/ui/src/views/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/packages/ui/src/views/pipeline-edit/components/graph-implementation/canvas/canvas-controls.tsx b/packages/ui/src/views/pipeline-edit/components/graph-implementation/canvas/canvas-controls.tsx new file mode 100644 index 0000000000..4094fa5fc6 --- /dev/null +++ b/packages/ui/src/views/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/packages/ui/src/views/pipeline-edit/components/graph-implementation/context-menu/stage-group-add-in-node-context-menu.tsx b/packages/ui/src/views/pipeline-edit/components/graph-implementation/context-menu/stage-group-add-in-node-context-menu.tsx new file mode 100644 index 0000000000..c4d2170f04 --- /dev/null +++ b/packages/ui/src/views/pipeline-edit/components/graph-implementation/context-menu/stage-group-add-in-node-context-menu.tsx @@ -0,0 +1,61 @@ +import { DropdownMenu } from '@components/dropdown-menu' +import { Icon } from '@components/icon' +import { Text } from '@components/text' + +import { usePipelineStudioNodeContext } from '../context/PipelineStudioNodeContext' +import { YamlEntityType } from '../types/yaml-entity-type' + +export const StageGroupAddInNodeContextMenu = () => { + const { contextMenuData, onAddIntention, hideContextMenu } = usePipelineStudioNodeContext() + + if (!contextMenuData) return null + + return ( + { + if (open === false) { + hideContextMenu() + } + }} + > + + { + onAddIntention(contextMenuData.nodeData, 'in', YamlEntityType.Stage) + }} + > + + Add Stage + + + { + onAddIntention(contextMenuData.nodeData, 'in', YamlEntityType.SerialStageGroup) + }} + > + + Add Serial group + + { + onAddIntention(contextMenuData.nodeData, 'in', YamlEntityType.ParallelStageGroup) + }} + > + + Add Parallel group + + + + ) +} diff --git a/packages/ui/src/views/pipeline-edit/components/graph-implementation/context-menu/stage-group-node-context-menu.tsx b/packages/ui/src/views/pipeline-edit/components/graph-implementation/context-menu/stage-group-node-context-menu.tsx new file mode 100644 index 0000000000..5f6081ab7e --- /dev/null +++ b/packages/ui/src/views/pipeline-edit/components/graph-implementation/context-menu/stage-group-node-context-menu.tsx @@ -0,0 +1,96 @@ +import { DropdownMenu } from '@components/dropdown-menu' +import { Icon } from '@components/icon' +import { Text } from '@components/text' + +import { usePipelineStudioNodeContext } from '../context/PipelineStudioNodeContext' +import { YamlEntityType } from '../types/yaml-entity-type' + +export const StageGroupNodeContextMenu = () => { + const { contextMenuData, onAddIntention, hideContextMenu, onEditIntention, onDeleteIntention } = + usePipelineStudioNodeContext() + + if (!contextMenuData) return null + + return ( + { + if (open === false) { + hideContextMenu() + } + }} + > + + { + onEditIntention(contextMenuData.nodeData) + }} + > + + Edit + + + { + onAddIntention(contextMenuData.nodeData, 'before', YamlEntityType.SerialStageGroup) + }} + > + + Add Serial group before + + { + onAddIntention(contextMenuData.nodeData, 'after', YamlEntityType.SerialStageGroup) + }} + > + + Add Serial group after + + + { + onAddIntention(contextMenuData.nodeData, 'before', YamlEntityType.ParallelStageGroup) + }} + > + + Add Parallel group before + + { + onAddIntention(contextMenuData.nodeData, 'after', YamlEntityType.ParallelStageGroup) + }} + > + + Add Parallel group after + + + {/* */} + {/* */} + { + onDeleteIntention(contextMenuData.nodeData) + }} + > + + Delete + + + + ) +} diff --git a/packages/ui/src/views/pipeline-edit/components/graph-implementation/context-menu/stage-node-context-menu.tsx b/packages/ui/src/views/pipeline-edit/components/graph-implementation/context-menu/stage-node-context-menu.tsx new file mode 100644 index 0000000000..952cc3efd3 --- /dev/null +++ b/packages/ui/src/views/pipeline-edit/components/graph-implementation/context-menu/stage-node-context-menu.tsx @@ -0,0 +1,75 @@ +import { DropdownMenu } from '@components/dropdown-menu' +import { Icon } from '@components/icon' +import { Text } from '@components/text' + +import { usePipelineStudioNodeContext } from '../context/PipelineStudioNodeContext' +import { YamlEntityType } from '../types/yaml-entity-type' + +export const StageNodeContextMenu = (): (() => React.ReactNode)[] | null | any => { + const { contextMenuData, onAddIntention, hideContextMenu, onEditIntention, onDeleteIntention } = + usePipelineStudioNodeContext() + + if (!contextMenuData) return null + + return ( + { + if (open === false) { + hideContextMenu() + } + }} + > + + { + onEditIntention(contextMenuData.nodeData) + }} + > + + Edit + + + { + onAddIntention(contextMenuData.nodeData, 'before', YamlEntityType.Stage) // TODO what to add + }} + > + + Add stage before + + { + onAddIntention(contextMenuData.nodeData, 'after', YamlEntityType.Stage) // TODO what to add + }} + > + + Add stage after + + + {/* */} + {/* */} + { + onDeleteIntention(contextMenuData.nodeData) + }} + > + + Delete + + + + ) +} diff --git a/packages/ui/src/views/pipeline-edit/components/graph-implementation/context-menu/step-node-context-menu.tsx b/packages/ui/src/views/pipeline-edit/components/graph-implementation/context-menu/step-node-context-menu.tsx new file mode 100644 index 0000000000..4db90732dc --- /dev/null +++ b/packages/ui/src/views/pipeline-edit/components/graph-implementation/context-menu/step-node-context-menu.tsx @@ -0,0 +1,74 @@ +import { DropdownMenu } from '@components/dropdown-menu' +import { Icon } from '@components/icon' +import { Text } from '@components/text' + +import { usePipelineStudioNodeContext } from '../context/PipelineStudioNodeContext' + +export const StepNodeContextMenu = (): (() => React.ReactNode)[] | null | any => { + const { contextMenuData, onAddIntention, hideContextMenu, onEditIntention, onDeleteIntention } = + usePipelineStudioNodeContext() + + if (!contextMenuData) return null + + return ( + { + if (open === false) { + hideContextMenu() + } + }} + > + + { + onEditIntention(contextMenuData.nodeData) + }} + > + + Edit + + + { + onAddIntention(contextMenuData.nodeData, 'before') // TODO what to add + }} + > + + Add step/group before + + { + onAddIntention(contextMenuData.nodeData, 'after') // TODO what to add + }} + > + + Add step/group after + + + {/* */} + {/* */} + { + onDeleteIntention(contextMenuData.nodeData) + }} + > + + Delete + + + + ) +} diff --git a/packages/ui/src/views/pipeline-edit/components/graph-implementation/context/PipelineStudioNodeContext.tsx b/packages/ui/src/views/pipeline-edit/components/graph-implementation/context/PipelineStudioNodeContext.tsx new file mode 100644 index 0000000000..2c311b304b --- /dev/null +++ b/packages/ui/src/views/pipeline-edit/components/graph-implementation/context/PipelineStudioNodeContext.tsx @@ -0,0 +1,123 @@ +import { createContext, useContext, useState } from 'react' + +import { CommonNodeDataType } from '../types/common-node-data-type' +import { YamlEntityType } from '../types/yaml-entity-type' + +export interface ContextMenuData { + contextMenu: () => JSX.Element + nodeData: CommonNodeDataType + position: { x: number; y: number } + isIn?: boolean +} + +export interface PipelineStudioNodeContextProps { + // context menu + showContextMenu: ( + contextMenu: () => React.ReactNode, + nodeData: CommonNodeDataType, + initiator: HTMLElement, + isIn?: boolean + ) => void + hideContextMenu: () => void + contextMenuData: ContextMenuData | undefined + + // selected entity path + selectionPath?: string + setSelectionPath: (path: string) => void + onSelectIntention: (nodeData: CommonNodeDataType) => void + // step manipulation + onAddIntention: ( + nodeData: CommonNodeDataType, + position: 'after' | 'before' | 'in', + yamlEntityTypeToAdd?: YamlEntityType + ) => void + onEditIntention: (nodeData: CommonNodeDataType) => void + onDeleteIntention: (nodeData: CommonNodeDataType) => void + onRevealInYaml: (path: string | undefined) => void +} + +export const PipelineStudioNodeContext = createContext({ + showContextMenu: ( + _contextMenu: () => React.ReactNode, + _nodeData: CommonNodeDataType, + _initiator: HTMLElement, + _isIn?: boolean + ) => undefined, + hideContextMenu: () => undefined, + contextMenuData: undefined, + // + selectionPath: undefined, + setSelectionPath: (_path: string | null) => undefined, + onSelectIntention: (_nodeData: CommonNodeDataType) => undefined, + // + onAddIntention: ( + _nodeData: CommonNodeDataType, + _position: 'after' | 'before' | 'in', + _yamlEntityTypeToAdd?: YamlEntityType + ) => undefined, + onEditIntention: (_nodeData: CommonNodeDataType) => undefined, + onDeleteIntention: (_nodeData: CommonNodeDataType) => undefined, + onRevealInYaml: (_path: string | undefined) => undefined +}) + +export function usePipelineStudioNodeContext(): PipelineStudioNodeContextProps { + return useContext(PipelineStudioNodeContext) +} + +export interface PipelineStudioNodeContextProviderProps { + children: React.ReactNode + onSelectIntention: (nodeData: CommonNodeDataType) => undefined + onAddIntention: ( + nodeData: CommonNodeDataType, + position: 'after' | 'before' | 'in', + yamlEntityTypeToAdd?: YamlEntityType + ) => void + onEditIntention: (nodeData: CommonNodeDataType) => undefined + onDeleteIntention: (nodeData: CommonNodeDataType) => undefined + onRevealInYaml: (_path: string | undefined) => undefined +} +export const PipelineStudioNodeContextProvider: React.FC = props => { + const { onSelectIntention, onAddIntention, onEditIntention, onDeleteIntention, onRevealInYaml, children } = props + + const [contextMenuData, setContextMenuData] = useState(undefined) + const [selectionPath, setSelectionPath] = useState(undefined) + + const showContextMenu = ( + contextMenu: (() => React.ReactNode)[] | any, + nodeData: CommonNodeDataType, + initiator: HTMLElement, + isIn?: boolean + ) => { + const rect = initiator.getBoundingClientRect() + + setContextMenuData({ + contextMenu, + nodeData, + position: { x: rect.left + rect.width, y: rect.top }, + isIn + }) + } + + const hideContextMenu = () => { + setContextMenuData(undefined) + } + + return ( + + {children} + + ) +} diff --git a/packages/ui/src/views/pipeline-edit/components/graph-implementation/nodes/add-content-node.tsx b/packages/ui/src/views/pipeline-edit/components/graph-implementation/nodes/add-content-node.tsx new file mode 100644 index 0000000000..714f373325 --- /dev/null +++ b/packages/ui/src/views/pipeline-edit/components/graph-implementation/nodes/add-content-node.tsx @@ -0,0 +1,25 @@ +import { PipelineNodes } from '@components/pipeline-nodes' + +import { LeafNodeInternalType } from '@harnessio/pipeline-graph' + +import { StageGroupAddInNodeContextMenu } from '../context-menu/stage-group-add-in-node-context-menu' +import { usePipelineStudioNodeContext } from '../context/PipelineStudioNodeContext' +import { CommonNodeDataType } from '../types/common-node-data-type' + +export interface AddNodeDataType extends CommonNodeDataType {} + +export function AddNode(props: { node: LeafNodeInternalType }) { + const { node } = props + const { data } = node + + const { showContextMenu } = usePipelineStudioNodeContext() + + return ( + { + e.stopPropagation() + showContextMenu(StageGroupAddInNodeContextMenu, data, e.currentTarget) + }} + /> + ) +} diff --git a/packages/ui/src/views/pipeline-edit/components/graph-implementation/nodes/end-content-node.tsx b/packages/ui/src/views/pipeline-edit/components/graph-implementation/nodes/end-content-node.tsx new file mode 100644 index 0000000000..43becb5627 --- /dev/null +++ b/packages/ui/src/views/pipeline-edit/components/graph-implementation/nodes/end-content-node.tsx @@ -0,0 +1,9 @@ +import { PipelineNodes } from '@components/pipeline-nodes' + +import { LeafNodeInternalType } from '@harnessio/pipeline-graph' + +export interface EndNodeDataType {} + +export function EndContentNode(_props: { node: LeafNodeInternalType }) { + return +} diff --git a/packages/ui/src/views/pipeline-edit/components/graph-implementation/nodes/parallel-group-content-node.tsx b/packages/ui/src/views/pipeline-edit/components/graph-implementation/nodes/parallel-group-content-node.tsx new file mode 100644 index 0000000000..4a456f8d73 --- /dev/null +++ b/packages/ui/src/views/pipeline-edit/components/graph-implementation/nodes/parallel-group-content-node.tsx @@ -0,0 +1,41 @@ +import { PipelineNodes } from '@components/pipeline-nodes' + +import { ParallelNodeInternalType } from '@harnessio/pipeline-graph' + +import { StageGroupAddInNodeContextMenu } from '../context-menu/stage-group-add-in-node-context-menu' +import { StageGroupNodeContextMenu } from '../context-menu/stage-group-node-context-menu' +import { usePipelineStudioNodeContext } from '../context/PipelineStudioNodeContext' +import { CommonNodeDataType } from '../types/common-node-data-type' + +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 } = usePipelineStudioNodeContext() + + return ( + { + e.stopPropagation() + showContextMenu(StageGroupAddInNodeContextMenu, data, e.currentTarget, true) + }} + onEllipsisClick={e => { + e.stopPropagation() + showContextMenu(StageGroupNodeContextMenu, data, e.currentTarget) + }} + > + {children} + + ) +} diff --git a/packages/ui/src/views/pipeline-edit/components/graph-implementation/nodes/serial-group-content-node.tsx b/packages/ui/src/views/pipeline-edit/components/graph-implementation/nodes/serial-group-content-node.tsx new file mode 100644 index 0000000000..7f5d091670 --- /dev/null +++ b/packages/ui/src/views/pipeline-edit/components/graph-implementation/nodes/serial-group-content-node.tsx @@ -0,0 +1,41 @@ +import { PipelineNodes } from '@components/pipeline-nodes' + +import { SerialNodeInternalType } from '@harnessio/pipeline-graph' + +import { StageGroupAddInNodeContextMenu } from '../context-menu/stage-group-add-in-node-context-menu' +import { StageGroupNodeContextMenu } from '../context-menu/stage-group-node-context-menu' +import { usePipelineStudioNodeContext } from '../context/PipelineStudioNodeContext' +import { CommonNodeDataType } from '../types/common-node-data-type' + +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 } = usePipelineStudioNodeContext() + + return ( + { + e.stopPropagation() + showContextMenu(StageGroupAddInNodeContextMenu, data, e.currentTarget, true) + }} + onEllipsisClick={e => { + e.stopPropagation() + showContextMenu(StageGroupNodeContextMenu, data, e.currentTarget) + }} + > + {children} + + ) +} diff --git a/packages/ui/src/views/pipeline-edit/components/graph-implementation/nodes/stage-content-node.tsx b/packages/ui/src/views/pipeline-edit/components/graph-implementation/nodes/stage-content-node.tsx new file mode 100644 index 0000000000..43b9c225a6 --- /dev/null +++ b/packages/ui/src/views/pipeline-edit/components/graph-implementation/nodes/stage-content-node.tsx @@ -0,0 +1,39 @@ +import { PipelineNodes } from '@components/pipeline-nodes' + +import { SerialNodeInternalType } from '@harnessio/pipeline-graph' + +import { StageNodeContextMenu } from '../context-menu/stage-node-context-menu' +import { usePipelineStudioNodeContext } from '../context/PipelineStudioNodeContext' +import { CommonNodeDataType } from '../types/common-node-data-type' + +export interface StageContentNodeDataType extends CommonNodeDataType { + icon?: React.ReactElement +} + +export function StageContentNode(props: { + node: SerialNodeInternalType + children?: React.ReactElement + collapsed?: boolean +}) { + const { node, children, collapsed } = props + const data = node.data as StageContentNodeDataType + + const { showContextMenu, onAddIntention } = usePipelineStudioNodeContext() + + return ( + { + onAddIntention(data, 'in') + }} + onEllipsisClick={e => { + e.stopPropagation() + showContextMenu(StageNodeContextMenu, data, e.currentTarget) + }} + > + {children} + + ) +} diff --git a/packages/ui/src/views/pipeline-edit/components/graph-implementation/nodes/start-content-node.tsx b/packages/ui/src/views/pipeline-edit/components/graph-implementation/nodes/start-content-node.tsx new file mode 100644 index 0000000000..67035d929c --- /dev/null +++ b/packages/ui/src/views/pipeline-edit/components/graph-implementation/nodes/start-content-node.tsx @@ -0,0 +1,9 @@ +import { PipelineNodes } from '@components/pipeline-nodes' + +import { LeafNodeInternalType } from '@harnessio/pipeline-graph' + +export interface StartNodeDataType {} + +export function StartContentNode(_props: { node: LeafNodeInternalType }) { + return +} diff --git a/packages/ui/src/views/pipeline-edit/components/graph-implementation/nodes/step-content-node.tsx b/packages/ui/src/views/pipeline-edit/components/graph-implementation/nodes/step-content-node.tsx new file mode 100644 index 0000000000..bf7da600ac --- /dev/null +++ b/packages/ui/src/views/pipeline-edit/components/graph-implementation/nodes/step-content-node.tsx @@ -0,0 +1,37 @@ +import { useMemo } from 'react' + +import { PipelineNodes } from '@components/pipeline-nodes' + +import { LeafNodeInternalType } from '@harnessio/pipeline-graph' + +import { StepNodeContextMenu } from '../context-menu/step-node-context-menu' +import { usePipelineStudioNodeContext } from '../context/PipelineStudioNodeContext' +import { CommonNodeDataType } from '../types/common-node-data-type' + +export interface StepNodeDataType extends CommonNodeDataType { + icon?: React.ReactElement + state?: 'success' | 'loading' + selected?: boolean +} + +export function StepContentNode(props: { node: LeafNodeInternalType }) { + const { node } = props + + const data = node.data + + const { selectionPath, showContextMenu } = usePipelineStudioNodeContext() + + const selected = useMemo(() => selectionPath === data.yamlPath, [selectionPath]) + + return ( + { + e.stopPropagation() + showContextMenu(StepNodeContextMenu, data, e.currentTarget) + }} + selected={selected} + > + ) +} diff --git a/packages/ui/src/views/pipeline-edit/components/graph-implementation/types/common-node-data-type.ts b/packages/ui/src/views/pipeline-edit/components/graph-implementation/types/common-node-data-type.ts new file mode 100644 index 0000000000..ea5334e054 --- /dev/null +++ b/packages/ui/src/views/pipeline-edit/components/graph-implementation/types/common-node-data-type.ts @@ -0,0 +1,8 @@ +import { YamlEntityType } from './yaml-entity-type' + +export interface CommonNodeDataType { + yamlPath: string + yamlChildrenPath?: string + name: string + yamlEntityType: YamlEntityType +} diff --git a/packages/ui/src/views/pipeline-edit/components/graph-implementation/types/content-node-type.ts b/packages/ui/src/views/pipeline-edit/components/graph-implementation/types/content-node-type.ts new file mode 100644 index 0000000000..557b090bc6 --- /dev/null +++ b/packages/ui/src/views/pipeline-edit/components/graph-implementation/types/content-node-type.ts @@ -0,0 +1,10 @@ +export enum ContentNodeType { + Add = 'Add', + Start = 'Start', + End = 'End', + Step = 'Step', + Approval = 'Approval', + ParallelGroup = 'ParallelGroup', + SerialGroup = 'SerialGroup', + Stage = 'Stage' +} diff --git a/packages/ui/src/views/pipeline-edit/components/graph-implementation/types/yaml-entity-type.ts b/packages/ui/src/views/pipeline-edit/components/graph-implementation/types/yaml-entity-type.ts new file mode 100644 index 0000000000..64c8adbc85 --- /dev/null +++ b/packages/ui/src/views/pipeline-edit/components/graph-implementation/types/yaml-entity-type.ts @@ -0,0 +1,8 @@ +export enum YamlEntityType { + Step = 'Step', + Stage = 'Stage', + ParallelStageGroup = 'ParallelStageGroup', + SerialStageGroup = 'SerialStageGroup', + SerialStepGroup = 'SerialStepGroup', + ParallelStepGroup = 'ParallelStepGroup' +} diff --git a/packages/ui/src/views/pipeline-edit/components/graph-implementation/utils/common-step-utils.ts b/packages/ui/src/views/pipeline-edit/components/graph-implementation/utils/common-step-utils.ts new file mode 100644 index 0000000000..ba85355559 --- /dev/null +++ b/packages/ui/src/views/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/packages/ui/src/views/pipeline-edit/components/graph-implementation/utils/step-icon-utils.ts b/packages/ui/src/views/pipeline-edit/components/graph-implementation/utils/step-icon-utils.ts new file mode 100644 index 0000000000..5aeae25a13 --- /dev/null +++ b/packages/ui/src/views/pipeline-edit/components/graph-implementation/utils/step-icon-utils.ts @@ -0,0 +1,28 @@ +import { IconProps } from '@components/icon' + +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/packages/ui/src/views/pipeline-edit/components/graph-implementation/utils/step-name-utils.ts b/packages/ui/src/views/pipeline-edit/components/graph-implementation/utils/step-name-utils.ts new file mode 100644 index 0000000000..6552efe36a --- /dev/null +++ b/packages/ui/src/views/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/packages/ui/src/views/pipeline-edit/components/graph-implementation/utils/yaml-to-pipeline-graph.tsx b/packages/ui/src/views/pipeline-edit/components/graph-implementation/utils/yaml-to-pipeline-graph.tsx new file mode 100644 index 0000000000..9aa5a1ed0a --- /dev/null +++ b/packages/ui/src/views/pipeline-edit/components/graph-implementation/utils/yaml-to-pipeline-graph.tsx @@ -0,0 +1,193 @@ +import { Icon } from '@components/icon' + +import { + AnyContainerNodeType, + LeafContainerNodeType, + ParallelContainerNodeType, + SerialContainerNodeType +} from '@harnessio/pipeline-graph' + +import { ParallelGroupContentNodeDataType } from '../nodes/parallel-group-content-node' +import { SerialGroupContentNodeDataType } from '../nodes/serial-group-content-node' +import { StageContentNodeDataType } from '../nodes/stage-content-node' +import { StepNodeDataType } from '../nodes/step-content-node' +import { ContentNodeType } from '../types/content-node-type' +import { YamlEntityType } from '../types/yaml-entity-type' +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: ContentNodeType.SerialGroup, + config: { + minWidth: 192, + minHeight: 40, + hideDeleteButton: true, + hideBeforeAdd: true, + hideAfterAdd: true + }, + data: { + yamlPath: path, + yamlChildrenPath: childrenPath, + yamlEntityType: YamlEntityType.SerialStageGroup, + name + } satisfies SerialGroupContentNodeDataType, + 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: ContentNodeType.ParallelGroup, + config: { + minWidth: 192, + minHeight: 40, + hideDeleteButton: true, + hideBeforeAdd: true, + hideAfterAdd: true + }, + data: { + yamlPath: path, + yamlChildrenPath: childrenPath, + yamlEntityType: YamlEntityType.ParallelStageGroup, + 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: ContentNodeType.Stage, + config: { + minWidth: 192, + minHeight: 40, + hideDeleteButton: true, + hideBeforeAdd: true, + hideAfterAdd: true + }, + data: { + yamlPath: path, + yamlChildrenPath: childrenPath, + yamlEntityType: YamlEntityType.Stage, + name + } satisfies StageContentNodeDataType, + 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: ContentNodeType.SerialGroup, + config: { + minWidth: 192, + hideDeleteButton: true, + hideCollapseButton: false + }, + data: { + yamlPath: path, + yamlChildrenPath: childrenPath, + yamlEntityType: YamlEntityType.SerialStepGroup, + name + }, // satisfies StageContentNodeDataType, + + 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: ContentNodeType.ParallelGroup, + config: { + minWidth: 192, + hideDeleteButton: true + }, + data: { + yamlPath: path, + yamlChildrenPath: childrenPath, + yamlEntityType: YamlEntityType.ParallelStepGroup, + 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: ContentNodeType.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/packages/ui/src/views/pipeline-edit/components/pipeline-studio-graph-view.tsx b/packages/ui/src/views/pipeline-edit/components/pipeline-studio-graph-view.tsx new file mode 100644 index 0000000000..4ce9ac9e17 --- /dev/null +++ b/packages/ui/src/views/pipeline-edit/components/pipeline-studio-graph-view.tsx @@ -0,0 +1,90 @@ +import { useEffect, useMemo, useState } from 'react' + +import { parse } from 'yaml' + +import { AnyContainerNodeType, CanvasProvider, PipelineGraph } from '@harnessio/pipeline-graph' + +import { ContentNodeFactory, YamlRevision } from '../pipeline-studio' +import { CanvasControls } from './graph-implementation/canvas/canvas-controls' +import { yaml2Nodes } from './graph-implementation/utils/yaml-to-pipeline-graph' + +import '@harnessio/pipeline-graph/dist/index.css' + +import { ContentNodeType } from './graph-implementation/types/content-node-type' + +const startNode = { + type: ContentNodeType.Start, + config: { + width: 40, + height: 40, + hideDeleteButton: true, + hideBeforeAdd: true, + hideLeftPort: true + }, + data: {} +} satisfies AnyContainerNodeType + +const endNode = { + type: ContentNodeType.End, + config: { + width: 40, + height: 40, + hideDeleteButton: true, + hideAfterAdd: true, + hideRightPort: true + }, + data: {} +} satisfies AnyContainerNodeType + +export interface PipelineStudioGraphViewProps { + contentNodeFactory: ContentNodeFactory + yamlRevision: YamlRevision + onYamlRevisionChange: (YamlRevision: YamlRevision) => void +} + +export const PipelineStudioGraphView = (props: PipelineStudioGraphViewProps): React.ReactElement => { + const { yamlRevision, contentNodeFactory } = props + + const [data, setData] = useState([]) + + useEffect(() => { + return () => { + setData([]) + } + }, []) + + useEffect(() => { + const yamlJson = parse(yamlRevision.yaml) + const newData = yaml2Nodes(yamlJson) + + if (newData.length === 0) { + // TODO: empty pipeline state + // 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]) + + const nodes = useMemo(() => { + return contentNodeFactory.getNodesDefinition() + }, [contentNodeFactory]) + + return ( +
+ + + + +
+ ) +} diff --git a/packages/ui/src/views/pipeline-edit/components/pipeline-studio-internal.tsx b/packages/ui/src/views/pipeline-edit/components/pipeline-studio-internal.tsx new file mode 100644 index 0000000000..604f275f40 --- /dev/null +++ b/packages/ui/src/views/pipeline-edit/components/pipeline-studio-internal.tsx @@ -0,0 +1,24 @@ +import { ContentNodeFactory, YamlRevision } from '../pipeline-studio' +import { PipelineStudioGraphView } from './pipeline-studio-graph-view' +import { PipelineStudioYamlView } from './pipeline-studio-yaml-view' + +export interface PipelineStudioInternalProps { + view: 'yaml' | 'graph' + contentNodeFactory: ContentNodeFactory + yamlRevision: YamlRevision + onYamlRevisionChange: (YamlRevision: YamlRevision) => void +} + +export default function PipelineStudioInternal(props: PipelineStudioInternalProps) { + const { view, yamlRevision, onYamlRevisionChange, contentNodeFactory } = props + + return view === 'graph' ? ( + + ) : ( + + ) +} diff --git a/packages/ui/src/views/pipeline-edit/components/pipeline-studio-node-context-menu.tsx b/packages/ui/src/views/pipeline-edit/components/pipeline-studio-node-context-menu.tsx new file mode 100644 index 0000000000..701343fc89 --- /dev/null +++ b/packages/ui/src/views/pipeline-edit/components/pipeline-studio-node-context-menu.tsx @@ -0,0 +1,7 @@ +import { usePipelineStudioNodeContext } from './graph-implementation/context/PipelineStudioNodeContext' + +export const PipelineStudioNodeContextMenu = () => { + const { contextMenuData } = usePipelineStudioNodeContext() + + return contextMenuData ? contextMenuData.contextMenu() : null +} diff --git a/packages/ui/src/views/pipeline-edit/components/pipeline-studio-yaml-view.tsx b/packages/ui/src/views/pipeline-edit/components/pipeline-studio-yaml-view.tsx new file mode 100644 index 0000000000..4a56e1e571 --- /dev/null +++ b/packages/ui/src/views/pipeline-edit/components/pipeline-studio-yaml-view.tsx @@ -0,0 +1,134 @@ +import { useEffect, useMemo, useRef, useState } from 'react' + +import { ILanguageFeaturesService } from 'monaco-editor/esm/vs/editor/common/services/languageFeatures.js' +import { OutlineModel } from 'monaco-editor/esm/vs/editor/contrib/documentSymbols/browser/outlineModel.js' +import { StandaloneServices } from 'monaco-editor/esm/vs/editor/standalone/browser/standaloneServices.js' + +import { MonacoGlobals, YamlEditor } from '@harnessio/yaml-editor' + +import { YamlRevision } from '../pipeline-studio' +import unifiedSchema from '../schema/unifiedSchema.json' +import { themes } from '../theme/monaco-theme' + +MonacoGlobals.set({ + ILanguageFeaturesService, + OutlineModel, + StandaloneServices +}) + +export interface PipelineStudioYamlViewProps { + yamlRevision: YamlRevision + onYamlRevisionChange: (YamlRevision: YamlRevision) => void +} + +const PipelineStudioYamlView = (props: PipelineStudioYamlViewProps): JSX.Element => { + const { yamlRevision, onYamlRevisionChange } = props + + const [reRenderYamlEditor, setRerenderYamlEditor] = useState(0) + const forceRerender = () => { + setRerenderYamlEditor(reRenderYamlEditor + 1) + } + + // stores current yaml we have in monaco + const currentYamlRef = useRef('') + + // hold yaml from context, this will be used for feeding YamlEditor as we use reRenderYamlEditor for rerendering YamlEditor + const newYamlRef = useRef(yamlRevision) + newYamlRef.current = yamlRevision + + const schemaConfig = useMemo( + () => ({ + schema: unifiedSchema, + uri: 'https://raw.githubusercontent.com/bradrydzewski/spec/master/dist/schema.json' + }), + [] + ) + + const themeConfig = useMemo( + () => ({ + defaultTheme: 'dark', + themes + }), + [] + ) + + // const addStep = useCallback( + // (path: string, position: InlineActionArgsType['position']) => { + // setStepDrawerOpen(StepDrawer.Collection) + // setAddStepIntention({ path, position }) + // }, + // [setStepDrawerOpen, setAddStepIntention] + // ) + + // const deleteStep = useCallback( + // (path: string) => { + // deleteInArray({ path }) + // }, + // [deleteInArray] + // ) + + // const editStep = useCallback( + // (path: string) => { + // setStepDrawerOpen(StepDrawer.Form) + // setEditStepIntention({ path }) + // }, + // [setEditStepIntention] + // ) + + // const inlineActionCallback: InlineAction['onClick'] = useCallback( + // props => { + // const { data, path } = props + // // TODO: move this to utils, refactor + // switch (data.entityType) { + // case 'step': + // switch (data.action) { + // case 'add': + // addStep(path, data.position) + // break + // case 'edit': + // editStep(path) + // break + // case 'delete': + // deleteStep(path) + // break + // } + // break + // default: + // break + // } + // }, + // [addStep, deleteStep, editStep] + // ) + // const inlineActions = useMemo(() => getInlineActionConfig(inlineActionCallback), [inlineActionCallback]) + + useEffect(() => { + if (yamlRevision.yaml !== currentYamlRef.current) { + forceRerender() + } + }, [yamlRevision]) + + return useMemo(() => { + // const selection = highlightInYamlPath + // ? { path: highlightInYamlPath, className: 'bg-background-4', revealInCenter: true } + // : undefined + + return ( +
+ { + currentYamlRef.current = value?.yaml + onYamlRevisionChange(value ?? { yaml: '', revisionId: 0 }) + }} + yamlRevision={newYamlRef.current} + themeConfig={themeConfig} + //theme={theme} // TODO + schemaConfig={schemaConfig} + // inlineActions={inlineActions} // TODO + // selection={selection} // TODO + /> +
+ ) + }, [reRenderYamlEditor, themeConfig, schemaConfig]) // inlineActions, highlightInYamlPath +} + +export { PipelineStudioYamlView } diff --git a/packages/ui/src/views/pipeline-edit/index.ts b/packages/ui/src/views/pipeline-edit/index.ts new file mode 100644 index 0000000000..27807b1ee0 --- /dev/null +++ b/packages/ui/src/views/pipeline-edit/index.ts @@ -0,0 +1,5 @@ +export * from './pipeline-edit' +export * from './utils/yaml-utils' +export * from './components/graph-implementation/types/yaml-entity-type' +export * from './components/graph-implementation/types/common-node-data-type' +export * from './components/graph-implementation/types/content-node-type' diff --git a/packages/ui/src/views/pipeline-edit/pipeline-edit.tsx b/packages/ui/src/views/pipeline-edit/pipeline-edit.tsx new file mode 100644 index 0000000000..112d10c3f6 --- /dev/null +++ b/packages/ui/src/views/pipeline-edit/pipeline-edit.tsx @@ -0,0 +1,101 @@ +import { ContainerNode } from '@harnessio/pipeline-graph' + +import { PipelineStudioNodeContextProvider } from './components/graph-implementation/context/PipelineStudioNodeContext' +import { EndContentNode } from './components/graph-implementation/nodes/end-content-node' +import { ParallelGroupContentNode } from './components/graph-implementation/nodes/parallel-group-content-node' +import { SerialGroupContentNode } from './components/graph-implementation/nodes/serial-group-content-node' +import { StageContentNode } from './components/graph-implementation/nodes/stage-content-node' +import { StartContentNode } from './components/graph-implementation/nodes/start-content-node' +import { StepContentNode } from './components/graph-implementation/nodes/step-content-node' +import { CommonNodeDataType } from './components/graph-implementation/types/common-node-data-type' +import { ContentNodeType } from './components/graph-implementation/types/content-node-type' +import { YamlEntityType } from './components/graph-implementation/types/yaml-entity-type' +import { PipelineStudioNodeContextMenu } from './components/pipeline-studio-node-context-menu' +import { ContentNodeFactory, PipelineStudio, YamlRevision } from './pipeline-studio' + +export interface PipelineEditProps { + /* pipeline view */ + view: 'yaml' | 'graph' + /* yaml state */ + yamlRevision: YamlRevision + /* yaml change callback */ + onYamlRevisionChange: (YamlRevision: YamlRevision) => void + onSelectIntention: (nodeData: CommonNodeDataType) => undefined + onAddIntention: ( + nodeData: CommonNodeDataType, + position: 'after' | 'before' | 'in', + yamlEntityTypeToAdd?: YamlEntityType + ) => void + onEditIntention: (nodeData: CommonNodeDataType) => undefined + onDeleteIntention: (nodeData: CommonNodeDataType) => undefined + onRevealInYaml: (_path: string | undefined) => undefined +} + +export const PipelineEdit = (props: PipelineEditProps): JSX.Element => { + const { + view, + yamlRevision, + onYamlRevisionChange, + onAddIntention, + onDeleteIntention, + onEditIntention, + onSelectIntention, + onRevealInYaml + } = props + + const contentNodeFactory = new ContentNodeFactory() + + contentNodeFactory.registerEntity(ContentNodeType.Start, { + type: ContentNodeType.Start, + component: StartContentNode, + containerType: ContainerNode.leaf + }) + + contentNodeFactory.registerEntity(ContentNodeType.End, { + type: ContentNodeType.End, + component: EndContentNode, + containerType: ContainerNode.leaf + }) + + contentNodeFactory.registerEntity(ContentNodeType.Step, { + type: ContentNodeType.Step, + component: StepContentNode, + containerType: ContainerNode.leaf + }) + + contentNodeFactory.registerEntity(ContentNodeType.Stage, { + type: ContentNodeType.Stage, + component: StageContentNode, + containerType: ContainerNode.serial + }) + + contentNodeFactory.registerEntity(ContentNodeType.ParallelGroup, { + type: ContentNodeType.ParallelGroup, + component: ParallelGroupContentNode, + containerType: ContainerNode.parallel + }) + + contentNodeFactory.registerEntity(ContentNodeType.SerialGroup, { + type: ContentNodeType.SerialGroup, + component: SerialGroupContentNode, + containerType: ContainerNode.serial + }) + + return ( + + + + + ) +} diff --git a/packages/ui/src/views/pipeline-edit/pipeline-studio.tsx b/packages/ui/src/views/pipeline-edit/pipeline-studio.tsx new file mode 100644 index 0000000000..b1a7613d2e --- /dev/null +++ b/packages/ui/src/views/pipeline-edit/pipeline-studio.tsx @@ -0,0 +1,42 @@ +import { NodeContent } from '@harnessio/pipeline-graph' + +import { ContentNodeType } from './components/graph-implementation/types/content-node-type' +import PipelineStudioInternal from './components/pipeline-studio-internal' + +export class ContentNodeFactory { + private entityBank: Map + + constructor() { + this.entityBank = new Map() + } + + registerEntity(entityType: ContentNodeType, definition: NodeContent) { + this.entityBank.set(entityType, definition) + } + + getEntityDefinition(entityType: ContentNodeType) { + return this.entityBank.get(entityType) + } + + getNodesDefinition() { + return Array.from(this.entityBank.values()) + } +} + +export interface YamlRevision { + yaml: string + revision?: number +} + +export interface PipelineStudioProps { + view: 'yaml' | 'graph' + contentNodeFactory: ContentNodeFactory + yamlRevision: YamlRevision + onYamlRevisionChange: (YamlRevision: YamlRevision) => void +} + +const PipelineStudio = (props: PipelineStudioProps): JSX.Element => { + return +} + +export { PipelineStudio } diff --git a/packages/ui/src/views/pipeline-edit/schema/unifiedSchema.json b/packages/ui/src/views/pipeline-edit/schema/unifiedSchema.json new file mode 100644 index 0000000000..4ad7df324d --- /dev/null +++ b/packages/ui/src/views/pipeline-edit/schema/unifiedSchema.json @@ -0,0 +1,2251 @@ +{ + "$ref": "#/definitions/Schema", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "Action": { + "anyOf": [ + { + "$ref": "#/definitions/ActionType" + }, + { + "$ref": "#/definitions/ActionLong" + } + ] + }, + "ActionLong": { + "properties": { + "abort": { + "type": "boolean" + }, + "fail": { + "type": "boolean" + }, + "ignore": { + "type": "boolean" + }, + "manual-intervention": { + "$ref": "#/definitions/ActionManual" + }, + "pipeline-rollback": { + "type": "boolean" + }, + "retry": { + "$ref": "#/definitions/ActionRetry" + }, + "retry-step-group": { + "type": "boolean" + }, + "stage-rollback": { + "type": "boolean" + }, + "success": { + "type": "boolean" + } + }, + "type": "object" + }, + "ActionManual": { + "properties": { + "timeout": { + "type": "string" + }, + "timeout-action": { + "$ref": "#/definitions/Action" + } + }, + "type": "object", + "x-go-file": "action_manual.go" + }, + "ActionRetry": { + "properties": { + "attempts": { + "type": "number" + }, + "failure-action": { + "$ref": "#/definitions/Action" + }, + "interval": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + } + }, + "type": "object", + "x-go-file": "action_retry.go" + }, + "ActionType": { + "enum": [ + "abort", + "fail", + "ignore", + "manual-intervention", + "pipeline-rollback", + "retry", + "retry-step-group", + "stage-rollback", + "success" + ], + "type": "string" + }, + "Cache": { + "description": "Cache defines pipeline caching behavior.", + "properties": { + "disabled": { + "description": "Disabled disables cache intelligence.", + "type": "boolean" + }, + "key": { + "description": "Key provides a caching key.", + "type": "string" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ], + "description": "Paths provides one or more paths to cache." + }, + "policy": { + "description": "Policy configures the pull and push behavior of the cache. By default, the stage pulls the cache when the stage starts and pushes changes to the cache when the stage ends.", + "enum": ["pull", "pull-push", "push"], + "type": "string" + } + }, + "type": "object", + "x-go-file": "cache.go" + }, + "Clone": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/definitions/CloneLong" + } + ] + }, + "CloneLong": { + "properties": { + "depth": { + "description": "Depth defines the clone depth.", + "type": "number" + }, + "disabled": { + "description": "Disabled disables the default clone step.", + "type": "boolean" + }, + "insecure": { + "description": "Insecure enables cloning without ssl verification.", + "type": "boolean" + }, + "lfs": { + "description": "Lfs enables cloning lfs files.", + "type": "boolean" + }, + "ref": { + "$ref": "#/definitions/CloneRef", + "description": "Reference defines the clone ref." + }, + "strategy": { + "description": "Strategy configures the clone strategy.", + "enum": ["source-branch", "merge"], + "type": "string" + }, + "submodules": { + "description": "Submodules enables cloning all submodules;", + "type": "boolean" + }, + "tags": { + "description": "Tags enables cloning all tags;", + "type": "boolean" + }, + "trace": { + "description": "Trace enables trace logging.", + "type": "boolean" + } + }, + "type": "object" + }, + "CloneRef": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/CloneRefLong" + } + ] + }, + "CloneRefLong": { + "properties": { + "name": { + "description": "Name provides the ref name. This can be the branch or tag name. Or this can be the full reference, e.g. refs/heads/main.", + "type": "string" + }, + "sha": { + "description": "Sha provides the commit sha.", + "type": "string" + }, + "type": { + "description": "Type provides the ref type. If undefined, the reference name is used to determine the reference type.", + "enum": ["branch", "pull-request", "tag"], + "type": "string" + } + }, + "type": "object" + }, + "Concurrency": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/ConcurrencyLong" + } + ], + "description": "Concurrency groups provide a way to limit concurrency execution of pipelines that share the same concurrency key." + }, + "ConcurrencyLong": { + "properties": { + "cancel-in-progress": { + "description": "Cancel any in-progress pipelines or stages that are in-progress when a new pipeline or stage is received.", + "type": "boolean" + }, + "group": { + "description": "Group provides the key used to group pipelines or stages together into a concurrency group.", + "type": "string" + } + }, + "type": "object" + }, + "Container": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/ContainerLong" + } + ] + }, + "ContainerLong": { + "description": "ContainerLong defines the container configuration syntax in long form.", + "properties": { + "args": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + }, + "connector": { + "description": "Connector provides the Connect used to authenticate to the registry.", + "type": "string" + }, + "cpu": { + "type": ["string", "number"] + }, + "credentials": { + "$ref": "#/definitions/Credentials", + "description": "Credentials provides the registry authentication credentials." + }, + "dns": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + }, + "entrypoint": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + }, + "env": { + "additionalProperties": { + "type": "string" + }, + "description": "Env provides the container environment variables.", + "type": "object" + }, + "extra-hosts": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + }, + "group": { + "type": ["string", "number"] + }, + "image": { + "description": "Image defines the container image.", + "type": "string" + }, + "memory": { + "type": ["string", "number"] + }, + "network": { + "type": "string" + }, + "network-mode": { + "type": "string" + }, + "ports": { + "items": { + "type": "string" + }, + "type": "array" + }, + "privileged": { + "type": "boolean" + }, + "pull": { + "enum": ["always", "never", "if-not-exists"], + "type": "string" + }, + "shm-size": { + "type": ["string", "number"] + }, + "user": { + "type": ["string", "number"] + }, + "volumes": { + "items": { + "$ref": "#/definitions/Mount" + }, + "type": "array" + }, + "workdir": { + "type": "string" + } + }, + "type": "object" + }, + "Credentials": { + "properties": { + "aws": { + "$ref": "#/definitions/CredentialsAWS", + "description": "AWS defines registry credentials for amazon web services." + }, + "password": { + "description": "Password provides the registry password.", + "type": "string" + }, + "username": { + "description": "Username provides the registry username.", + "type": "string" + } + }, + "type": "object", + "x-go-file": "credentials.go" + }, + "CredentialsAWS": { + "properties": { + "access-key": { + "description": "AccessKey provides the aws access key id.", + "type": "string" + }, + "secret-key": { + "description": "SecretKey provides the aws access key secret.", + "type": "string" + } + }, + "type": "object", + "x-go-file": "credentials_aws.go" + }, + "Environment": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/EnvironmentLong" + } + ] + }, + "EnvironmentItem": { + "properties": { + "deploy-to": { + "anyOf": [ + { + "const": "all", + "type": "string" + }, + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ], + "description": "DeployToAll deploys to all infrastructure definitions (clusters) associated with the environment." + }, + "name": { + "description": "Name provides the environment name.", + "type": "string" + } + }, + "type": "object" + }, + "EnvironmentLong": { + "properties": { + "items": { + "items": { + "$ref": "#/definitions/EnvironmentItem" + }, + "type": "array" + }, + "parallel": { + "type": "boolean" + } + }, + "type": "object" + }, + "EnvironmentSchema": { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "org": { + "deprecated": true, + "type": "string" + }, + "project": { + "deprecated": true, + "type": "string" + }, + "tags": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "type": { + "enum": ["production", "non-production"], + "type": "string" + } + }, + "type": "object", + "x-go-file": "schema_environment.go" + }, + "Event": { + "anyOf": [ + { + "$ref": "#/definitions/EventType" + }, + { + "$ref": "#/definitions/EventLong" + } + ] + }, + "EventFilter": { + "properties": { + "types": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + } + }, + "type": "object" + }, + "EventLong": { + "properties": { + "branch_protection_rule": { + "$ref": "#/definitions/EventFilter" + }, + "check_run": { + "$ref": "#/definitions/EventFilter" + }, + "check_suite": { + "$ref": "#/definitions/EventFilter" + }, + "create": {}, + "delete": {}, + "deployment": {}, + "deployment_status": {}, + "discussion": { + "$ref": "#/definitions/EventFilter" + }, + "discussion_comment": { + "$ref": "#/definitions/EventFilter" + }, + "fork": {}, + "issue_comment": { + "$ref": "#/definitions/EventFilter" + }, + "issues": { + "$ref": "#/definitions/EventFilter" + }, + "label": { + "$ref": "#/definitions/EventFilter" + }, + "member": { + "$ref": "#/definitions/EventFilter" + }, + "merge_group": { + "$ref": "#/definitions/EventFilter" + }, + "milestone": { + "$ref": "#/definitions/EventFilter" + }, + "page_build": {}, + "project": { + "$ref": "#/definitions/EventFilter" + }, + "project_card": { + "$ref": "#/definitions/EventFilter" + }, + "project_column": { + "$ref": "#/definitions/EventFilter" + }, + "public": {}, + "pull_request": { + "$ref": "#/definitions/PullRequestFilter" + }, + "pull_request_review": { + "$ref": "#/definitions/Event" + }, + "pull_request_review_comment": { + "$ref": "#/definitions/Event" + }, + "pull_request_target": { + "$ref": "#/definitions/PullRequestFilter" + }, + "push": { + "$ref": "#/definitions/PushFilter" + }, + "registry_package": { + "$ref": "#/definitions/EventFilter" + }, + "release": { + "$ref": "#/definitions/EventFilter" + }, + "repository_dispatch": { + "$ref": "#/definitions/EventFilter" + }, + "schedule": {}, + "status": {}, + "watch": { + "$ref": "#/definitions/EventFilter" + }, + "workflow_call": {}, + "workflow_dispatch": {}, + "workflow_run": {} + }, + "type": "object" + }, + "EventType": { + "enum": [ + "branch_protection_rule", + "check_run", + "check_suite", + "create", + "delete", + "deployment", + "deployment_status", + "discussion", + "discussion_comment", + "fork", + "issue_comment", + "issues", + "label", + "member", + "merge_group", + "milestone", + "page_build", + "project", + "project_card", + "project_column", + "public", + "pull_request", + "pull_request_review", + "pull_request_review_comment", + "pull_request_target", + "push", + "registry_package", + "repository_dispatch", + "release", + "schedule", + "status", + "watch", + "workflow_call", + "workflow_dispatch", + "workflow_run" + ], + "type": "string" + }, + "FailureStrategy": { + "properties": { + "action": { + "$ref": "#/definitions/Action" + }, + "errors": { + "anyOf": [ + { + "$ref": "#/definitions/FailureType" + }, + { + "items": { + "$ref": "#/definitions/FailureType" + }, + "type": "array" + } + ] + } + }, + "type": "object" + }, + "FailureType": { + "enum": [ + "all", + "approval-rejection", + "authentication", + "authorization", + "connectivity", + "delegate-provisioning", + "delegate-restart", + "input-timeout", + "policy-evaluation", + "timeout", + "unknown", + "verification", + "user-mark-fail" + ], + "type": "string" + }, + "For": { + "description": "For defines a for loop execution strategy.", + "properties": { + "iterations": { + "description": "Iterations defines maximum number of interations.", + "type": "number" + } + }, + "type": "object", + "x-go-file": "strategy_for.go" + }, + "InfraSchema": { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "org": { + "deprecated": true, + "type": "string" + }, + "project": { + "deprecated": true, + "type": "string" + }, + "tags": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + } + }, + "type": "object", + "x-go-file": "schema_infra.go" + }, + "Input": { + "properties": { + "default": {}, + "description": { + "description": "Description defines the input description.", + "type": "string" + }, + "enum": { + "description": "Enum defines a list of accepted input values.", + "items": {}, + "type": "array" + }, + "items": { + "description": "Items defines an array type.", + "items": {}, + "type": "array" + }, + "mask": { + "deprecated": true, + "description": "Mask indicates the input should be masked.", + "type": "boolean" + }, + "options": { + "description": "Options defines a list of accepted input values. This is an alias for enum.", + "items": {}, + "type": "array" + }, + "required": { + "description": "Required indicates the input is required.", + "type": "boolean" + }, + "type": { + "description": "Type defines the input type.", + "enum": ["string", "number", "boolean", "array", "duration", "choice", "environment", "secret"], + "type": "string" + } + }, + "required": ["type"], + "type": "object", + "x-go-file": "input.go" + }, + "MachineImage": { + "enum": ["ubuntu-latest", "macos-latest", "wndows-latest"], + "type": "string" + }, + "MachineSize": { + "enum": ["flex", "small", "medium", "large", "xlarge", "xxlarge"], + "type": "string" + }, + "Matrix": { + "description": "Matrix defines a matrix execution strategy.", + "properties": { + "exclude": { + "items": { + "type": "object" + }, + "type": "array" + }, + "include": { + "items": { + "type": "object" + }, + "type": "array" + } + }, + "type": "object", + "x-go-file": "strategy_matrix.go" + }, + "Mount": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/MountLong" + } + ] + }, + "MountLong": { + "properties": { + "source": { + "type": "string" + }, + "target": { + "type": "string" + } + }, + "required": ["source", "target"], + "type": "object" + }, + "On": { + "anyOf": [ + { + "$ref": "#/definitions/EventType" + }, + { + "items": { + "$ref": "#/definitions/Event" + }, + "type": "array" + }, + { + "$ref": "#/definitions/Event" + } + ] + }, + "Output": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/OutputLong" + } + ] + }, + "OutputLong": { + "properties": { + "alias": { + "type": "string" + }, + "mask": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "scope": { + "enum": ["pipeline", "stage"], + "type": "string" + } + }, + "type": "object" + }, + "Permissions": { + "anyOf": [ + { + "const": "write-all", + "type": "string" + }, + { + "const": "read-all", + "type": "string" + }, + { + "$ref": "#/definitions/PermissionsLong" + } + ], + "description": "Permissions defines the permission granted to the token injected into the pipeline environment." + }, + "PermissionsLong": { + "properties": { + "actions": { + "enum": ["read", "write", "none"], + "type": "string" + }, + "checks": { + "enum": ["read", "write", "none"], + "type": "string" + }, + "contents": { + "enum": ["read", "write", "none"], + "type": "string" + }, + "deployments": { + "enum": ["read", "write", "none"], + "type": "string" + }, + "discussions": { + "enum": ["read", "write", "none"], + "type": "string" + }, + "id-token": { + "enum": ["read", "write", "none"], + "type": "string" + }, + "issues": { + "enum": ["read", "write", "none"], + "type": "string" + }, + "packages": { + "enum": ["read", "write", "none"], + "type": "string" + }, + "pages": { + "enum": ["read", "write", "none"], + "type": "string" + }, + "pull-requests": { + "enum": ["read", "write", "none"], + "type": "string" + }, + "repository-projects": { + "enum": ["read", "write", "none"], + "type": "string" + }, + "security-events": { + "enum": ["read", "write", "none"], + "type": "string" + }, + "statuses": { + "enum": ["read", "write", "none"], + "type": "string" + } + }, + "type": "object" + }, + "Pipeline": { + "properties": { + "barriers": { + "description": "Barriers provides optional pipeline barriers.", + "items": { + "type": "string" + }, + "type": "array" + }, + "clone": { + "$ref": "#/definitions/Clone", + "description": "Clone overrides the default clone behavior." + }, + "concurrency": { + "$ref": "#/definitions/Concurrency", + "description": "Concurrency groups provide a way to limit concurrency execution of pipelines that share the same concurrency key." + }, + "default": {}, + "env": { + "additionalProperties": { + "type": "string" + }, + "description": "Env provides global environment variables that propagate to all pipeline steps.", + "type": "object" + }, + "environment": { + "$ref": "#/definitions/Environment", + "description": "Environment defines the target deployment environment (e.g. development, prod)." + }, + "id": { + "deprecated": true, + "description": "Id provides a unique pipeline identifer.", + "type": "string" + }, + "if": { + "description": "If provides conditional pipeline execution logic. If the condition resolves to false, the pipeline is skipped.", + "type": "string" + }, + "inputs": { + "additionalProperties": { + "$ref": "#/definitions/Input" + }, + "description": "Inputs provides pipeline input variables.", + "type": "object" + }, + "jobs": { + "additionalProperties": { + "$ref": "#/definitions/Stage" + }, + "description": "Jobs defines jobs (stages) in the pipeline.\n\nThis property is available solely for the purpose of backward compatibility with GitHub Actions.", + "type": "object" + }, + "name": { + "deprecated": true, + "description": "Name provides a pipeline name.", + "type": "string" + }, + "on": { + "$ref": "#/definitions/On", + "description": "On provides condition pipeline execution logic based on trigger event and action mapping. If the conditional logic resolves to folse, the pipeline is skipped." + }, + "permissions": { + "$ref": "#/definitions/Permissions" + }, + "repo": { + "$ref": "#/definitions/Repository", + "description": "Repo overrides the default repository." + }, + "service": { + "$ref": "#/definitions/Service", + "description": "Service defines the service being deployed." + }, + "stages": { + "description": "Stages provides a list of stages. Each pipeline is made up of one or more stages that executes sequentially.", + "items": { + "$ref": "#/definitions/Stage" + }, + "type": "array" + }, + "status": { + "$ref": "#/definitions/Status", + "description": "Status overrides the default status behavior." + } + }, + "type": "object", + "x-go-file": "pipeline.go" + }, + "Platform": { + "description": "Platform defines the target execution environment.", + "properties": { + "arch": { + "description": "OS defines the target operating system.", + "type": "string" + }, + "features": { + "description": "Features defines the target platform features. Not currently used.", + "items": { + "type": "string" + }, + "type": "array" + }, + "os": { + "description": "Arch defines the target cpu architecture.", + "type": "string" + }, + "variant": { + "description": "Variant defines the target cpu architecture variant. Not currently used.", + "type": "string" + }, + "version": { + "description": "Version defines the target operating system version. Not currently used.", + "type": "string" + } + }, + "type": "object", + "x-go-file": "platform.go" + }, + "PullRequestFilter": { + "properties": { + "branches": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + }, + "branches-ignore": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + }, + "paths": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + }, + "paths-ignore": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + }, + "review-approved": { + "type": "boolean" + }, + "review-dismissed": { + "type": "boolean" + }, + "tags": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + }, + "tags-ignore": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + }, + "types": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + } + }, + "type": "object" + }, + "PushFilter": { + "properties": { + "branches": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + }, + "branches-ignore": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + }, + "paths": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + }, + "paths-ignore": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + }, + "tags": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + }, + "tags-ignore": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + } + }, + "type": "object" + }, + "Report": { + "properties": { + "path": { + "type": "string" + }, + "type": { + "enum": ["junit", "xunit", "nunit"], + "type": "string" + } + }, + "type": "object", + "x-go-file": "report.go" + }, + "Repository": { + "description": "Repository defines a remote git repository.", + "properties": { + "connector": { + "description": "Connector provides the repository connector.", + "type": "string" + }, + "name": { + "description": "Name provides the repository name.", + "type": "string" + } + }, + "type": "object", + "x-go-file": "repository.go" + }, + "Runtime": { + "anyOf": [ + { + "$ref": "#/definitions/RuntimeShort" + }, + { + "$ref": "#/definitions/RuntimeLong" + } + ] + }, + "RuntimeCloud": { + "description": "RuntimeClone configures the cloud runtime environment.", + "properties": { + "image": { + "anyOf": [ + { + "$ref": "#/definitions/MachineImage" + }, + { + "type": "string" + } + ] + }, + "size": { + "$ref": "#/definitions/MachineSize" + } + }, + "type": "object" + }, + "RuntimeInstance": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/RuntimeInstanceLong" + } + ] + }, + "RuntimeInstanceLong": { + "description": "RuntimeInstanceLong configures the vm runtime environment.", + "properties": { + "image": { + "type": "string" + } + }, + "type": "object" + }, + "RuntimeKubernetes": { + "description": "RuntimeKubernetes configures the kubernetes runtime environment.", + "properties": { + "namespace": { + "type": "string" + } + }, + "type": "object", + "x-go-file": "runtime_kubernetes.go" + }, + "RuntimeLong": { + "description": "RuntimeLong configures the runtime environment.", + "properties": { + "cloud": { + "$ref": "#/definitions/RuntimeCloud" + }, + "kubernetes": { + "$ref": "#/definitions/RuntimeKubernetes" + }, + "shell": { + "type": "boolean" + }, + "vm": { + "$ref": "#/definitions/RuntimeInstance" + } + }, + "type": "object" + }, + "RuntimeShort": { + "enum": ["cloud", "vm", "kubernetes", "shell"], + "type": "string" + }, + "Schema": { + "properties": { + "action": { + "$ref": "#/definitions/Template", + "deprecated": "use \"template\" instead", + "description": "Action defines re-usable pipeline steps and stages." + }, + "concurrency": { + "$ref": "#/definitions/Concurrency", + "description": "Concurrency groups provide a way to limit concurrency execution of pipelines that share the same concurrency key." + }, + "defaults": { + "description": "Defaults provides default settings that apply to all jobs in the workflow.\n\nThis property is available solely for the purpose of backward compatibility with GitHub Actions.", + "type": "object" + }, + "env": { + "additionalProperties": { + "type": "string" + }, + "description": "Envs defines environment variables that are available to all steps in the workflow.\n\nThis property is available solely for the purpose of backward compatibility with GitHub Actions.", + "type": "object" + }, + "environment": { + "$ref": "#/definitions/EnvironmentSchema", + "description": "Environment defines a deployment environment." + }, + "infrastructure": { + "$ref": "#/definitions/InfraSchema", + "description": "Infrastructure defines the service infrastructure." + }, + "inputset": { + "description": "Inputset defines re-usable inputs." + }, + "jobs": { + "additionalProperties": { + "$ref": "#/definitions/Stage" + }, + "description": "Jobs defines the parallel workflow jobs.\n\nThis property is available solely for the purpose of backward compatibility with GitHub Actions.", + "type": "object" + }, + "name": { + "description": "Name defines the pipeline name.\n\nThis property is available solely for the purpose of backward compatibility with GitHub Actions.", + "type": "string" + }, + "on": { + "$ref": "#/definitions/On", + "description": "On defines the workflow triggers.\n\nThis property is available solely for the purpose of backward compatibility with GitHub Actions." + }, + "permissions": { + "$ref": "#/definitions/Permissions", + "description": "Permissions defines the permission granted to the token injected into the pipeline environment." + }, + "pipeline": { + "$ref": "#/definitions/Pipeline", + "description": "Pipeline defines the pipeline configuration." + }, + "service": { + "$ref": "#/definitions/ServiceSchema", + "description": "Service defines a service." + }, + "template": { + "$ref": "#/definitions/Template", + "description": "Template defines re-usable pipeline steps and stages." + }, + "version": { + "description": "Version defines the schema version.", + "type": ["string", "number"] + } + }, + "type": "object", + "x-go-file": "schema.go" + }, + "Service": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/ServiceLong" + } + ] + }, + "ServiceLong": { + "properties": { + "items": { + "items": { + "type": "string" + }, + "type": "array" + }, + "parallel": { + "type": "boolean" + } + }, + "required": ["items"], + "type": "object" + }, + "ServiceSchema": { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "org": { + "deprecated": true, + "type": "string" + }, + "project": { + "deprecated": true, + "type": "string" + }, + "tags": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + } + }, + "type": "object", + "x-go-file": "schema_service.go" + }, + "Stage": { + "properties": { + "approval": { + "$ref": "#/definitions/StageApproval", + "description": "Approval defines an approval stage." + }, + "cache": { + "$ref": "#/definitions/Cache", + "description": "Cache defines the cache configuration." + }, + "clone": { + "$ref": "#/definitions/Clone", + "description": "Clone overrides the default clone settings." + }, + "concurrency": { + "$ref": "#/definitions/Concurrency", + "description": "Concurrency groups provide a way to limit concurrency execution of pipelines that share the same concurrency key." + }, + "delegate": { + "description": "Delegage defines the delegate that should handle stage execution. This is optional.", + "type": "string" + }, + "env": { + "additionalProperties": { + "type": "string" + }, + "description": "Env defines the environment of the stage. These environment variables are shared by all steps in the stage.", + "type": "object" + }, + "environment": { + "$ref": "#/definitions/Environment", + "description": "Environment defines the deployment environment (production, staging)." + }, + "group": { + "$ref": "#/definitions/StageGroup", + "description": "Group defines a group of stages." + }, + "id": { + "description": "Id defines the pipeline id.", + "type": "string" + }, + "if": { + "description": "If defines conditional execution logic.", + "type": "string" + }, + "name": { + "description": "Name defines the pipeline name.", + "type": "string" + }, + "needs": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ], + "description": "Needs defines stages that must be completed before this stage can run." + }, + "on-failure": { + "$ref": "#/definitions/FailureStrategy" + }, + "outputs": { + "description": "Outputs configures the stage to export variables for use by other stages.", + "type": "object" + }, + "parallel": { + "$ref": "#/definitions/StageGroup", + "description": "Parallel defines a set of parallel stages." + }, + "permissions": { + "$ref": "#/definitions/Permissions", + "description": "Permissions defines the permission granted to the token injected into the stage environment." + }, + "platform": { + "$ref": "#/definitions/Platform", + "description": "Platform defines the target platform." + }, + "rollback": { + "$ref": "#/definitions/Step", + "description": "Rollback defines the rollback steps." + }, + "runs-on": { + "description": "RunsOn defines the type of machine to run the job.\n\nThis property is available solely for the purpose of backward compatibility with GitHub Actions.", + "type": "string" + }, + "runtime": { + "$ref": "#/definitions/Runtime", + "description": "Runtime defines the execution runtime." + }, + "service": { + "$ref": "#/definitions/Service", + "description": "Service defines the deployment target." + }, + "services": { + "additionalProperties": { + "$ref": "#/definitions/Container" + }, + "description": "Services defines background service containers.\n\nThis property is available solely for the purpose of backward compatibility with GitHub Actions.", + "type": "object" + }, + "status": { + "$ref": "#/definitions/Status", + "description": "Status overrides the default status settings." + }, + "steps": { + "description": "Steps defines a list of steps.", + "items": { + "$ref": "#/definitions/Step" + }, + "type": "array" + }, + "strategy": { + "$ref": "#/definitions/Strategy", + "description": "Strategy defines the matrix or looping strategy." + }, + "template": { + "$ref": "#/definitions/StageTemplate", + "description": "Template defines a stage template." + }, + "volumes": { + "items": { + "$ref": "#/definitions/Volume" + }, + "type": "array" + }, + "workspace": { + "$ref": "#/definitions/Workspace", + "description": "Workspace configures the local workspace directory." + } + }, + "type": "object", + "x-go-file": "stage.go" + }, + "StageApproval": { + "properties": { + "uses": { + "type": "string" + }, + "with": { + "type": "object" + } + }, + "type": "object", + "x-go-file": "stage_approval.go" + }, + "StageGroup": { + "properties": { + "parallel": { + "deprecated": true, + "description": "Parallel defines the maximum number of stages that can run in parallel. If unset or zero, the stages run sequentially.", + "type": "number" + }, + "stages": { + "description": "Stages defines a list of stages.", + "items": { + "$ref": "#/definitions/Stage" + }, + "type": "array" + } + }, + "type": "object", + "x-go-file": "stage_group.go" + }, + "StageTemplate": { + "properties": { + "uses": { + "type": "string" + }, + "with": { + "type": "object" + } + }, + "type": "object", + "x-go-file": "stage_template.go" + }, + "Status": { + "properties": { + "disabled": { + "description": "Disabled disables the status check.", + "type": "boolean" + }, + "level": { + "description": "Level stes the status level.", + "enum": ["pipeline", "stage", "step"], + "type": "string" + }, + "matrix": { + "description": "Matrix defines how matrix statuses are handled.", + "enum": ["itemize", "aggregate"], + "type": "string" + }, + "name": { + "description": "Name sets the default status name.", + "type": "string" + } + }, + "type": "object", + "x-go-file": "status.go" + }, + "Step": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/StepLong" + } + ] + }, + "StepAction": { + "properties": { + "env": { + "additionalProperties": { + "type": "string" + }, + "description": "Env defines the environment of the step.", + "type": "object" + }, + "output": { + "anyOf": [ + { + "$ref": "#/definitions/Output" + }, + { + "items": { + "$ref": "#/definitions/Output" + }, + "type": "array" + } + ], + "deprecated": true, + "description": "Output defines the output variables." + }, + "report": { + "anyOf": [ + { + "$ref": "#/definitions/Report" + }, + { + "items": { + "$ref": "#/definitions/Report" + }, + "type": "array" + } + ], + "description": "Report uploads reports at the the provided path(s)" + }, + "uses": { + "description": "Uses defines the action.", + "type": "string" + }, + "with": { + "description": "With defines the action configuration parameters.", + "type": "object" + } + }, + "type": "object", + "x-go-file": "step_action.go" + }, + "StepApproval": { + "properties": { + "env": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "uses": { + "type": "string" + }, + "with": { + "type": "object" + } + }, + "type": "object", + "x-go-file": "step_approval.go" + }, + "StepBarrier": { + "properties": { + "name": { + "type": "string" + } + }, + "required": ["name"], + "type": "object", + "x-go-file": "step_barrier.go" + }, + "StepGroup": { + "properties": { + "parallel": { + "deprecated": true, + "description": "Parallel defines the maximum number of steps that can run in parallel. If unset or zero, the steps run sequentially.", + "type": ["number", "boolean"] + }, + "steps": { + "description": "Steps defines a list of steps.", + "items": { + "$ref": "#/definitions/Step" + }, + "type": "array" + } + }, + "type": "object", + "x-go-file": "step_group.go" + }, + "StepLong": { + "properties": { + "action": { + "$ref": "#/definitions/StepAction", + "description": "Action defines an action step." + }, + "approval": { + "$ref": "#/definitions/StepApproval", + "description": "Approval defines an approval step." + }, + "background": { + "$ref": "#/definitions/StepRun", + "description": "Background defines a background step." + }, + "barrier": { + "$ref": "#/definitions/StepBarrier", + "description": "Barrier defines a step barrier." + }, + "env": { + "additionalProperties": { + "type": "string" + }, + "description": "Env defines the environment of the step.\n\nThis property is available solely for the purpose of backward compatibility with GitHub Actions.", + "type": "object" + }, + "group": { + "$ref": "#/definitions/StepGroup", + "description": "Group defines a step group." + }, + "id": { + "description": "Id defines the step id.", + "type": "string" + }, + "if": { + "description": "If defines conditional execution logic.", + "type": "string" + }, + "name": { + "description": "Name defines the step name.", + "type": "string" + }, + "needs": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ], + "description": "Needs defines steps that must be completed before this step can run." + }, + "on-failure": { + "$ref": "#/definitions/FailureStrategy", + "description": "FailureStrategy defines error handling." + }, + "parallel": { + "$ref": "#/definitions/StepGroup", + "description": "Parallel defines a parallel step group." + }, + "queue": { + "$ref": "#/definitions/StepQueue", + "description": "Queue defines a queue step." + }, + "run": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/StepRun" + } + ], + "description": "Run defines a run step." + }, + "run-test": { + "$ref": "#/definitions/StepTest", + "description": "Test defines a run test step" + }, + "status": { + "$ref": "#/definitions/Status", + "description": "Status overrides the default status settings." + }, + "strategy": { + "$ref": "#/definitions/Strategy", + "description": "Strategy defines the matrix or looping strategy." + }, + "template": { + "$ref": "#/definitions/StepTemplate", + "description": "Template defines a step template." + }, + "timeout": { + "description": "Timeout defines the step timeout duration.", + "type": ["string", "number"] + }, + "uses": { + "description": "Uses defines the github action.\n\nThis property is available solely for the purpose of backward compatibility with GitHub Actions.", + "type": "string" + }, + "with": { + "description": "With defines the github action configuration parameters.\n\nThis property is available solely for the purpose of backward compatibility with GitHub Actions.", + "type": "object" + } + }, + "type": "object" + }, + "StepQueue": { + "properties": { + "key": { + "type": "string" + }, + "scope": { + "enum": ["pipeline", "stage"], + "type": "string" + } + }, + "required": ["key"], + "type": "object", + "x-go-file": "step_queue.go" + }, + "StepRun": { + "properties": { + "container": { + "$ref": "#/definitions/Container", + "description": "Container runs the step inside a container. If you do not set a container, the step will run directly on the host unless the target runtime in kubernetes, in which case the container is required." + }, + "delegate": { + "anyOf": [ + { + "const": "inherit-from-infrastrcuture", + "type": "string" + }, + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ], + "description": "This property is available solely for the purpose of backward compatibility with Harness Currrent Gen." + }, + "env": { + "additionalProperties": { + "type": "string" + }, + "description": "Env defines the environment of the step.", + "type": "object" + }, + "output": { + "anyOf": [ + { + "$ref": "#/definitions/Output" + }, + { + "items": { + "$ref": "#/definitions/Output" + }, + "type": "array" + } + ], + "deprecated": true, + "description": "Output defines the step output variables." + }, + "report": { + "anyOf": [ + { + "$ref": "#/definitions/Report" + }, + { + "items": { + "$ref": "#/definitions/Report" + }, + "type": "array" + } + ], + "description": "Report uploads reports at the the provided path(s)" + }, + "script": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ], + "description": "Script runs command line scripts using the operating system's shell. Each script represents a new process and shell in the runner environment. Note that when you provide multi-line commands, each line runs in the same shell." + }, + "shell": { + "description": "Shell defines the shell of the step.", + "enum": ["sh", "bash", "powershell", "pwsh", "python"], + "type": "string" + } + }, + "type": "object", + "x-go-file": "step_run.go" + }, + "StepTemplate": { + "properties": { + "env": { + "additionalProperties": { + "type": "string" + }, + "description": "Env defines the environment of the step.", + "type": "object" + }, + "uses": { + "description": "Uses defines the template.", + "type": "string" + }, + "with": { + "description": "With defines the template configuration parameters.", + "type": "object" + } + }, + "type": "object", + "x-go-file": "step_template.go" + }, + "StepTest": { + "properties": { + "container": { + "$ref": "#/definitions/Container", + "description": "Container runs the step inside a container. If you do not set a container, the step will run directly on the host unless the target runtime in kubernetes, in which case the container is required." + }, + "env": { + "additionalProperties": { + "type": "string" + }, + "description": "Env defines the environment of the step.", + "type": "object" + }, + "intelligence": { + "$ref": "#/definitions/TestIntelligence", + "description": "Intelligence configures the test intelligence behavior." + }, + "match": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ], + "description": "Match provides unit test matching logic in glob format." + }, + "output": { + "anyOf": [ + { + "$ref": "#/definitions/Output" + }, + { + "items": { + "$ref": "#/definitions/Output" + }, + "type": "array" + } + ], + "deprecated": true, + "description": "Output defines the output variables." + }, + "report": { + "anyOf": [ + { + "$ref": "#/definitions/Report" + }, + { + "items": { + "$ref": "#/definitions/Report" + }, + "type": "array" + } + ], + "description": "Report uploads reports at the the provided path(s)" + }, + "script": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ], + "description": "Script runs command line scripts using the operating system's shell. Each script represents a new process and shell in the runner environment. Note that when you provide multi-line commands, each line runs in the same shell." + }, + "shell": { + "description": "Shell defines the shell of the step.", + "enum": ["sh", "bash", "powershell", "pwsh", "python"], + "type": "string" + }, + "splitting": { + "$ref": "#/definitions/TestSplitting", + "description": "Splitting configures the test splitting behavior." + } + }, + "type": "object", + "x-go-file": "step_tester.go" + }, + "Strategy": { + "properties": { + "fail-fast": { + "description": "FailFast defines the how to handle stage or step failure. If true, all in-progress or pending stages or steps are cancelled if any stage or step in the matrix fails.\n\nThis property is available solely for the purpose of backward compatibility with GitHub Actions.", + "type": "boolean" + }, + "for": { + "$ref": "#/definitions/For", + "description": "For defines a for loop execution strategy." + }, + "matrix": { + "$ref": "#/definitions/Matrix", + "description": "Matrix defines a matrix execution strategy." + }, + "max-parallel": { + "description": "MaxParallel defines the maximum number of parallel stages or steps.\n\nThis property is available solely for the purpose of backward compatibility with GitHub Actions.", + "type": "number" + }, + "while": { + "$ref": "#/definitions/While", + "description": "While defines a while loop execution strategy." + } + }, + "type": "object", + "x-go-file": "strategy.go" + }, + "Template": { + "description": "Template defines a Pipeline, Stage or Step template.", + "properties": { + "inputs": { + "additionalProperties": { + "$ref": "#/definitions/Input" + }, + "type": "object" + }, + "stage": { + "$ref": "#/definitions/Stage" + }, + "step": { + "$ref": "#/definitions/Step" + } + }, + "type": "object", + "x-go-file": "template.go" + }, + "TestIntelligence": { + "properties": { + "disabled": { + "type": "boolean" + } + }, + "type": "object", + "x-go-file": "test_intelligence.go" + }, + "TestSplitting": { + "properties": { + "concurrency": { + "type": "number" + }, + "disabled": { + "type": "boolean" + } + }, + "type": "object", + "x-go-file": "test_splitting.go" + }, + "Volume": { + "properties": { + "name": { + "type": "string" + }, + "uses": { + "enum": ["bind", "claim", "config", "temp"], + "type": "string" + }, + "with": { + "anyOf": [ + { + "$ref": "#/definitions/VolumeBind" + }, + { + "$ref": "#/definitions/VolumeClaim" + }, + { + "$ref": "#/definitions/VolumeConfigMap" + }, + { + "$ref": "#/definitions/VolumeTemp" + } + ] + } + }, + "required": ["name", "uses"], + "type": "object" + }, + "VolumeBind": { + "properties": { + "path": { + "type": "string" + } + }, + "type": "object", + "x-go-file": "volume_bind.go" + }, + "VolumeClaim": { + "properties": { + "name": { + "type": "string" + } + }, + "type": "object", + "x-go-file": "volume_claim.go" + }, + "VolumeConfigMap": { + "properties": { + "mode": { + "type": "string" + }, + "name": { + "type": "string" + }, + "optional": { + "type": "boolean" + } + }, + "type": "object", + "x-go-file": "volume_config_map.go" + }, + "VolumeTemp": { + "properties": { + "limit": { + "type": ["string", "number"] + }, + "medium": { + "const": "memory", + "type": "string" + } + }, + "type": "object", + "x-go-file": "volume_temp.go" + }, + "While": { + "description": "While defines a while loop execution strategy.", + "properties": { + "condition": { + "description": "Condition defines the while condition.", + "type": "string" + }, + "iterations": { + "description": "Iterations defines maximum number of interations.", + "type": "number" + } + }, + "type": "object", + "x-go-file": "strategy_while.go" + }, + "Workspace": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/definitions/WorkspaceLong" + } + ] + }, + "WorkspaceLong": { + "properties": { + "disabled": { + "type": "boolean" + }, + "path": { + "type": "string" + } + }, + "type": "object" + } + } +} diff --git a/packages/ui/src/views/pipeline-edit/theme/monaco-theme.ts b/packages/ui/src/views/pipeline-edit/theme/monaco-theme.ts new file mode 100644 index 0000000000..dd94de4cab --- /dev/null +++ b/packages/ui/src/views/pipeline-edit/theme/monaco-theme.ts @@ -0,0 +1,404 @@ +import { editor } from 'monaco-editor' + +import { ThemeDefinition } from '@harnessio/yaml-editor' + +const harnessLightTheme = { + base: 'vs' as editor.BuiltinTheme, + inherit: true, + rules: [ + { + background: 'f8f8f8', + token: '' + }, + { + foreground: '10a567', + token: 'comment' + }, + { + foreground: '386ac3', + token: 'keyword.operator.class' + }, + { + foreground: 'e88501', + token: 'constant.other' + }, + { + foreground: 'e88501', + token: 'source.php.embedded.line' + }, + { + foreground: 'e06c75', + token: 'variable' + }, + { + foreground: 'e06c75', + token: 'support.other.variable' + }, + { + foreground: 'e06c75', + token: 'string.other.link' + }, + { + foreground: 'e06c75', + token: 'string.regexp' + }, + { + foreground: '386ac3', + token: 'entity.name.tag' + }, + { + foreground: '6d8600', + token: 'entity.other.attribute-name' + }, + { + foreground: '386ac3', + token: 'meta.tag' + }, + { + foreground: '386ac3', + token: 'declaration.tag' + }, + { + foreground: 'c82829', + token: 'markup.deleted.git_gutter' + }, + { + foreground: '6d8600', + token: 'constant.numeric' + }, + { + foreground: 'e88501', + token: 'constant.language' + }, + { + foreground: 'e88501', + token: 'support.constant' + }, + { + foreground: 'e88501', + token: 'constant.character' + }, + { + foreground: 'e88501', + token: 'variable.parameter' + }, + { + foreground: 'e88501', + token: 'punctuation.section.embedded' + }, + { + foreground: '6d8600', + token: 'keyword.other.unit' + }, + { + foreground: '386ac3', + token: 'entity.name.class' + }, + { + foreground: '386ac3', + token: 'entity.name.type.class' + }, + { + foreground: '6d8600', + token: 'string' + }, + { + foreground: '6d8600', + token: 'constant.other.symbol' + }, + { + foreground: '6d8600', + token: 'markup.heading' + }, + { + foreground: '718c00', + token: 'markup.inserted.git_gutter' + }, + { + foreground: '386ac3', + token: 'keyword.operator' + }, + { + foreground: '8431c5', + token: 'keyword' + }, + { + foreground: 'ffffff', + background: '4271ae', + token: 'meta.diff.header.to-file' + }, + { + foreground: 'ffffff', + background: '4271ae', + token: 'meta.diff.header.from-file' + }, + { + foreground: '3e999f', + fontStyle: 'italic', + token: 'meta.diff.range' + } + ], + colors: { + 'editor.foreground': '#353535', + 'editor.background': '#f8f8f8', + 'editor.selectionBackground': '#abdffa', + 'editor.lineHighlightBackground': '#f8f8f8', + 'editorCursor.foreground': '#000000', + 'editorWhitespace.foreground': '#eaeaea' + } +} + +const harnessDarkTheme = { + base: 'vs-dark' as editor.BuiltinTheme, + inherit: true, + rules: [ + { + background: '000000', + token: '' + }, + { + foreground: '969896', + token: 'comment' + }, + { + foreground: 'eeeeee', + token: 'keyword.operator.class' + }, + { + foreground: 'eeeeee', + token: 'constant.other' + }, + { + foreground: 'eeeeee', + token: 'source.php.embedded.line' + }, + { + foreground: 'd54e53', + token: 'variable' + }, + { + foreground: 'd54e53', + token: 'support.other.variable' + }, + { + foreground: 'd54e53', + token: 'string.other.link' + }, + { + foreground: 'd54e53', + token: 'string.regexp' + }, + { + foreground: 'd54e53', + token: 'entity.name.tag' + }, + { + foreground: 'd54e53', + token: 'entity.other.attribute-name' + }, + { + foreground: 'd54e53', + token: 'meta.tag' + }, + { + foreground: 'd54e53', + token: 'declaration.tag' + }, + { + foreground: 'd54e53', + token: 'markup.deleted.git_gutter' + }, + { + foreground: 'e78c45', + token: 'constant.numeric' + }, + { + foreground: 'e78c45', + token: 'constant.language' + }, + { + foreground: 'e78c45', + token: 'support.constant' + }, + { + foreground: 'e78c45', + token: 'constant.character' + }, + { + foreground: 'e78c45', + token: 'variable.parameter' + }, + { + foreground: 'e78c45', + token: 'punctuation.section.embedded' + }, + { + foreground: 'e78c45', + token: 'keyword.other.unit' + }, + { + foreground: 'e7c547', + token: 'entity.name.class' + }, + { + foreground: 'e7c547', + token: 'entity.name.type.class' + }, + { + foreground: 'e7c547', + token: 'support.type' + }, + { + foreground: 'e7c547', + token: 'support.class' + }, + { + foreground: 'b9ca4a', + token: 'string' + }, + { + foreground: 'b9ca4a', + token: 'constant.other.symbol' + }, + { + foreground: 'b9ca4a', + token: 'entity.other.inherited-class' + }, + { + foreground: 'b9ca4a', + token: 'markup.heading' + }, + { + foreground: 'b9ca4a', + token: 'markup.inserted.git_gutter' + }, + { + foreground: '70c0b1', + token: 'keyword.operator' + }, + { + foreground: '70c0b1', + token: 'constant.other.color' + }, + { + foreground: '7aa6da', + token: 'entity.name.function' + }, + { + foreground: '7aa6da', + token: 'meta.function-call' + }, + { + foreground: '7aa6da', + token: 'support.function' + }, + { + foreground: '7aa6da', + token: 'keyword.other.special-method' + }, + { + foreground: '7aa6da', + token: 'meta.block-level' + }, + { + foreground: '7aa6da', + token: 'markup.changed.git_gutter' + }, + { + foreground: 'c397d8', + token: 'keyword' + }, + { + foreground: 'c397d8', + token: 'storage' + }, + { + foreground: 'c397d8', + token: 'storage.type' + }, + { + foreground: 'c397d8', + token: 'entity.name.tag.css' + }, + { + foreground: 'ced2cf', + background: 'df5f5f', + token: 'invalid' + }, + { + foreground: 'ced2cf', + background: '82a3bf', + token: 'meta.separator' + }, + { + foreground: 'ced2cf', + background: 'b798bf', + token: 'invalid.deprecated' + }, + { + foreground: 'ffffff', + token: 'markup.inserted.diff' + }, + { + foreground: 'ffffff', + token: 'markup.deleted.diff' + }, + { + foreground: 'ffffff', + token: 'meta.diff.header.to-file' + }, + { + foreground: 'ffffff', + token: 'meta.diff.header.from-file' + }, + { + foreground: '718c00', + token: 'markup.inserted.diff' + }, + { + foreground: '718c00', + token: 'meta.diff.header.to-file' + }, + { + foreground: 'c82829', + token: 'markup.deleted.diff' + }, + { + foreground: 'c82829', + token: 'meta.diff.header.from-file' + }, + { + foreground: 'ffffff', + background: '4271ae', + token: 'meta.diff.header.from-file' + }, + { + foreground: 'ffffff', + background: '4271ae', + token: 'meta.diff.header.to-file' + }, + { + foreground: '3e999f', + fontStyle: 'italic', + token: 'meta.diff.range' + } + ], + colors: { + 'editor.foreground': '#DEDEDE', + 'editor.background': '#0F0F11', + 'editor.selectionBackground': '#424242', + 'editor.lineHighlightBackground': '#2A2A2A', + 'editorCursor.foreground': '#9F9F9F', + 'editorWhitespace.foreground': '#343434' + } +} + +export const themes: ThemeDefinition[] = [ + { themeName: 'dark', themeData: harnessDarkTheme }, + { themeName: 'light', themeData: harnessLightTheme } +] + +export const themesForBlame: ThemeDefinition[] = [ + { themeName: 'dark', themeData: harnessDarkTheme }, + { themeName: 'light', themeData: harnessLightTheme } +] diff --git a/packages/ui/src/views/pipeline-edit/utils/__tests__/yaml-mocks/pipeline1.ts b/packages/ui/src/views/pipeline-edit/utils/__tests__/yaml-mocks/pipeline1.ts new file mode 100644 index 0000000000..7c604e10e1 --- /dev/null +++ b/packages/ui/src/views/pipeline-edit/utils/__tests__/yaml-mocks/pipeline1.ts @@ -0,0 +1,73 @@ +export const pipeline1 = `# Root comment +pipeline: + stages: + - steps: + # Comment 1 + - name: run-step-1 + # Comment 2 + run: + # Comment 3 + shell: powershell + # Comment 4 + script: go build + # Comment 4 + report: + # Comment Before all + # Comment before path: path-1 + - path: path-1 + # Comment before type (path-1) + type: junit + # Comment before path: path-2 + - path: path-2 + # Comment before type (path-2) + type: xunit + # Comment after all + container: + # Comment 10 + image: image 1 + # Comment 11 + connector: connector1 + # Comment 12 + credentials: + # comment for username + username: u + # comment for password + password: p +` + +export const expectedPipeline1 = `# Root comment +pipeline: + stages: + - steps: + # Comment 1 + - name: Updated step name + # Comment 2 + run: + # Comment 3 + shell: powershell + # Comment 4 + script: go build + # Comment 4 + report: + # Comment Before all + # Comment before path: path-1 + - path: path-1 + # Comment before type (path-1) + type: junit + # Comment before path: path-2 + - path: path-2 + # Comment before type (path-2) + type: xunit + # Comment after all + container: + # Comment 10 + image: image 1 + # Comment 11 + connector: connector1 + # Comment 12 + credentials: + # comment for username + username: u + # comment for password + password: p +` diff --git a/packages/ui/src/views/pipeline-edit/utils/__tests__/yaml-mocks/pipeline2.ts b/packages/ui/src/views/pipeline-edit/utils/__tests__/yaml-mocks/pipeline2.ts new file mode 100644 index 0000000000..e0b26e273d --- /dev/null +++ b/packages/ui/src/views/pipeline-edit/utils/__tests__/yaml-mocks/pipeline2.ts @@ -0,0 +1,44 @@ +export const pipeline2 = `# Root comment +pipeline: + stages: + - steps: + # Comment 1 + - name: run-step-1 + # Comment 2 + run: + # Comment 3 + shell: powershell + # Comment 4 + script: go build + # Comment 4 + report: + # Comment Before all + # Comment before path: path-1 + - path: path-1 + # Comment before type (path-1) + type: junit + # Comment before path: path-2 + - path: path-2 + # Comment before type (path-2) + type: xunit + # Comment after all + container: + # Comment 10 + image: image 1 + # Comment 11 + connector: connector1 + # Comment 12 + credentials: + # comment for username + username: u + # comment for password + password: p +` + +export const expectedPipeline2 = `# Root comment +pipeline: + stages: + - steps: + # Comment 1 + - Step0 +` diff --git a/packages/ui/src/views/pipeline-edit/utils/__tests__/yaml-mocks/pipeline3.ts b/packages/ui/src/views/pipeline-edit/utils/__tests__/yaml-mocks/pipeline3.ts new file mode 100644 index 0000000000..6bb42f01e0 --- /dev/null +++ b/packages/ui/src/views/pipeline-edit/utils/__tests__/yaml-mocks/pipeline3.ts @@ -0,0 +1,41 @@ +export const pipeline3 = `# Root comment +pipeline: + stages: + - steps: + # Comment 1 + - name: run-step-1 + # Comment 2 + run: + # Comment 3 + shell: powershell + # Comment 4 + script: go build + # Comment 4 + report: + # Comment Before all + # Comment before path: path-1 + - path: path-1 + # Comment before type (path-1) + type: junit + # Comment before path: path-2 + - path: path-2 + # Comment before type (path-2) + type: xunit + # Comment after all + container: + # Comment 10 + image: image 1 + # Comment 11 + connector: connector1 + # Comment 12 + credentials: + # comment for username + username: u + # comment for password + password: p +` + +export const expectedPipeline3 = `# Root comment +pipeline: + stages: Stages +` diff --git a/packages/ui/src/views/pipeline-edit/utils/__tests__/yaml-mocks/pipeline4.ts b/packages/ui/src/views/pipeline-edit/utils/__tests__/yaml-mocks/pipeline4.ts new file mode 100644 index 0000000000..1f13a4e342 --- /dev/null +++ b/packages/ui/src/views/pipeline-edit/utils/__tests__/yaml-mocks/pipeline4.ts @@ -0,0 +1,72 @@ +export const pipeline4 = `# Root comment +pipeline: + stages: + - steps: + # Comment 1 + - name: run-step-1 + # Comment 2 + run: + # Comment 3 + shell: powershell + # Comment 4 + script: go build + # Comment 4 + report: + # Comment Before all + # Comment before path: path-1 + - path: path-1 + # Comment before type (path-1) + type: junit + # Comment before path: path-2 + - path: path-2 + # Comment before type (path-2) + type: xunit + # Comment after all + container: + # Comment 10 + image: image 1 + # Comment 11 + connector: connector1 + # Comment 12 + credentials: + # comment for username + username: u + # comment for password + password: p +` +export const expectedPipeline4 = `# Root comment +pipeline: + stages: + - steps: + # Comment 1 + - name: run-step-1 + # Comment 2 + run: + # Comment 3 + shell: powershell + # Comment 4 + script: go build + # Comment 4 + report: + # Comment Before all + # Comment before path: path-1 + - path: updatedPath1 + # Comment before type (path-1) + type: updatedType1 + # Comment before path: path-2 + - path: updatedPath2 + # Comment before type (path-2) + type: updatedType2 + # Comment after all + container: + # Comment 10 + image: image 1 + # Comment 11 + connector: connector1 + # Comment 12 + credentials: + # comment for username + username: u + # comment for password + password: p +` diff --git a/packages/ui/src/views/pipeline-edit/utils/__tests__/yaml-mocks/pipeline5.ts b/packages/ui/src/views/pipeline-edit/utils/__tests__/yaml-mocks/pipeline5.ts new file mode 100644 index 0000000000..f4a210ed6c --- /dev/null +++ b/packages/ui/src/views/pipeline-edit/utils/__tests__/yaml-mocks/pipeline5.ts @@ -0,0 +1,68 @@ +export const pipeline5 = `# Root comment +pipeline: + stages: + - steps: + # Comment 1 + - name: run-step-1 + # Comment 2 + run: + # Comment 3 + shell: powershell + # Comment 4 + script: go build + # Comment 4 + report: + # Comment Before all + # Comment before path: path-1 + - path: path-1 + # Comment before type (path-1) + type: junit + # Comment before path: path-2 + - path: path-2 + # Comment before type (path-2) + type: xunit + # Comment after all + container: + # Comment 10 + image: image1 + # Comment 11 + connector: connector1 + # Comment 12 + credentials: + # comment for username + username: u + # comment for password + password: p +` + +export const expectedPipeline5 = `# Root comment +pipeline: + stages: + - steps: + # Comment 1 + - name: run-step-1 + # Comment 2 + run: + # Comment 3 + shell: powershell 2 + # Comment 4 + script: go build 2 + # Comment 4 + report: + # Comment Before all + # Comment before path: path-1 + - path: path-1 2 + # Comment before type (path-1) + type: junit 2 + # Comment before path: path-2 + container: + # Comment 10 + # Comment 11 + connector: connector1 2 + # Comment 12 + credentials: + # comment for username + username: u 2 + # comment for password + password: p 2 +` diff --git a/packages/ui/src/views/pipeline-edit/utils/__tests__/yaml-utils.test.ts b/packages/ui/src/views/pipeline-edit/utils/__tests__/yaml-utils.test.ts new file mode 100644 index 0000000000..a2c774e2c4 --- /dev/null +++ b/packages/ui/src/views/pipeline-edit/utils/__tests__/yaml-utils.test.ts @@ -0,0 +1,55 @@ +import { updateItemInArray } from '../yaml-utils' +import { expectedPipeline1, pipeline1 } from './yaml-mocks/pipeline1' +import { expectedPipeline2, pipeline2 } from './yaml-mocks/pipeline2' +import { expectedPipeline3, pipeline3 } from './yaml-mocks/pipeline3' +import { expectedPipeline4, pipeline4 } from './yaml-mocks/pipeline4' +import { expectedPipeline5, pipeline5 } from './yaml-mocks/pipeline5' + +describe('test updateItemInArray', () => { + test('update string', () => { + const result = updateItemInArray(pipeline1, { path: 'pipeline.stages.0.steps.0.name', item: 'Updated step name' }) + expect(result).toBe(expectedPipeline1) + }) + + test('update element in array with string', () => { + const result = updateItemInArray(pipeline2, { path: 'pipeline.stages.0.steps.0', item: 'Step0' }) + expect(result).toBe(expectedPipeline2) + }) + + test('update array with string', () => { + const result = updateItemInArray(pipeline3, { path: 'pipeline.stages', item: 'Stages' }) + expect(result).toBe(expectedPipeline3) + }) + + test('update array with array', () => { + const arr = [ + { path: 'updatedPath1', type: 'updatedType1' }, + { path: 'updatedPath2', type: 'updatedType2' } + ] + const result = updateItemInArray(pipeline4, { path: 'pipeline.stages.0.steps.0.run.report', item: arr }) + expect(result).toBe(expectedPipeline4) + }) + + test('update object with object', () => { + const obj = { + shell: 'powershell 2', + script: 'go build 2', + report: [ + { + path: 'path-1 2', + type: 'junit 2' + } + ], + container: { + connector: 'connector1 2', + credentials: { + username: 'u 2', + password: 'p 2' + } + } + } + + const result = updateItemInArray(pipeline5, { path: 'pipeline.stages.0.steps.0.run', item: obj }) + expect(result).toBe(expectedPipeline5) + }) +}) diff --git a/packages/ui/src/views/pipeline-edit/utils/common-utils.ts b/packages/ui/src/views/pipeline-edit/utils/common-utils.ts new file mode 100644 index 0000000000..ae2c890bdf --- /dev/null +++ b/packages/ui/src/views/pipeline-edit/utils/common-utils.ts @@ -0,0 +1,5 @@ +import { capitalize } from 'lodash-es' + +export const generateFriendlyName = (propertyName: string): string => { + return capitalize(propertyName.split('_').join(' ')) +} diff --git a/packages/ui/src/views/pipeline-edit/utils/inline-actions.ts b/packages/ui/src/views/pipeline-edit/utils/inline-actions.ts new file mode 100644 index 0000000000..3868d3a10d --- /dev/null +++ b/packages/ui/src/views/pipeline-edit/utils/inline-actions.ts @@ -0,0 +1,133 @@ +import { SelectorType, type InlineAction, type PathSelector } from '@harnessio/yaml-editor' + +export type InlineActionArgsType = { + entityType: 'stages' | 'stage' | 'step' | 'inputs' | 'input' | 'group' + action: 'edit' | 'delete' | 'add' | 'manage' + position?: 'in' | 'after' | 'before' +} + +export const getInlineActionConfig = ( + onClick: InlineAction['onClick'] +): { + selectors: PathSelector[] + actions: InlineAction[] +}[] => [ + { + selectors: [{ type: SelectorType.ContainsPath, basePath: '', paths: [/.steps$/] }], + actions: [ + { + title: 'Add step', + onClick, + data: { + action: 'add', + entityType: 'step', + position: 'in' + } + } + ] + }, + { + selectors: [{ type: SelectorType.ContainsPath, basePath: '', paths: [/.steps.\d+$/] }], + actions: [ + { + title: 'Edit', + onClick, + data: { + action: 'edit', + entityType: 'step' + } + } + // , + // { + // title: 'Delete', + // onClick, + // data: { + // action: 'delete', + // entityType: 'step' + // } + // } + ] + } + // { + // selectors: [{ type: SelectorType.ContainsPath, basePath: '', paths: [/^inputs$/] }], + // actions: [ + // { + // title: 'manage', + // onClick, + // data: { + // action: 'manage', + // entityType: 'inputs' + // } + // }, + // { + // title: 'add', + // onClick, + // data: { + // action: 'add', + // entityType: 'inputs' + // } + // } + // ] + // }, + // { + // selectors: [{ type: SelectorType.ContainsPath, basePath: '', paths: [/inputs.\d+$/] }], + // actions: [ + // { + // title: 'edit', + // onClick, + // data: { + // action: 'edit', + // entityType: 'input' + // } + // } + // ] + // }, + // { + // selectors: [{ type: SelectorType.ContainsPath, basePath: '', paths: [/^pipeline$/] }], + // actions: [ + // { + // title: 'Pipeline settings', + // onClick + // } + // ] + // }, + // { + // selectors: [{ type: SelectorType.ContainsPath, basePath: '', paths: [/^pipeline.stages$/] }], + // actions: [ + // { + // title: 'add', + // onClick, + // data: { + // action: 'add', + // entityType: 'stage' + // } + // } + // ] + // }, + // { + // selectors: [{ type: SelectorType.ContainsPath, basePath: 'pipeline', paths: [/^.stages.\d+$/] }], + // actions: [ + // { + // title: 'edit', + // onClick, + // data: { + // action: 'edit', + // entityType: 'stage' + // } + // } + // ] + // }, + // { + // selectors: [{ type: SelectorType.ContainsPath, basePath: 'pipeline', paths: [/.group$/] }], + // actions: [ + // { + // title: 'edit', + // onClick, + // data: { + // action: 'edit', + // entityType: 'group' + // } + // } + // ] + // } +] diff --git a/packages/ui/src/views/pipeline-edit/utils/yaml-doc-utils.ts b/packages/ui/src/views/pipeline-edit/utils/yaml-doc-utils.ts new file mode 100644 index 0000000000..fd84d51eb5 --- /dev/null +++ b/packages/ui/src/views/pipeline-edit/utils/yaml-doc-utils.ts @@ -0,0 +1,171 @@ +import { forOwn } from 'lodash-es' +import { Document, Pair, Scalar, YAMLMap, YAMLSeq } from 'yaml' + +type YamlVariableType = 'object' | 'array' | 'number' | 'string' | 'boolean' | 'undefined' | 'unknown' + +function isPrimitiveType(type: YamlVariableType) { + return ['number', 'string', 'boolean'].includes(type) +} + +function getYamlElementType(el: YAMLMap | YAMLSeq | string | number | boolean | unknown): YamlVariableType { + if (typeof el === 'undefined') { + return 'undefined' + } else if (el instanceof YAMLMap) { + return 'object' + } else if (el instanceof YAMLSeq) { + return 'array' + } else if (el instanceof Scalar) { + return getYamlElementType(el.value) + } else if (typeof el === 'number') { + return 'number' + } else if (typeof el === 'string') { + return 'string' + } else if (typeof el === 'boolean') { + return 'boolean' + } + return 'unknown' +} + +function getDataType(obj: Record | unknown[] | string | number | boolean | unknown): YamlVariableType { + if (typeof obj === 'undefined') { + return 'undefined' + } else if (obj instanceof Object && !(obj instanceof Array)) { + return 'object' + } else if (obj instanceof Array) { + return 'array' + } else if (typeof obj === 'number') { + return 'number' + } else if (typeof obj === 'string') { + return 'string' + } else if (typeof obj === 'boolean') { + return 'boolean' + } + return 'unknown' +} + +/** NOTE: updateYamlPrimitive mutates targetEl */ +function updateYamlPrimitive(targetEl: Scalar, val: unknown) { + targetEl.value = val +} + +/** NOTE: updateYamlArray mutates targetEl */ +function updateYamlArray(targetEl: YAMLSeq, arr: unknown[], doc: Document.Parsed) { + targetEl.items = arr.reduce((acc, arrItem, idx) => { + const item = targetEl.items[idx] + const yamlElementType = getYamlElementType(item) + const dataType = getDataType(arrItem) + + // add element + if (yamlElementType === 'undefined') { + const yamlItem = doc.createNode(arrItem) + acc.push(yamlItem) + return acc + } + + // update element if same type + if (yamlElementType === dataType) { + if (isPrimitiveType(yamlElementType)) { + updateYamlPrimitive(item as Scalar, arr[idx]) + } else if (yamlElementType === 'object') { + updateYamlObject(item as YAMLMap, arr[idx] as Record, doc) + } else if (yamlElementType === 'array') { + updateYamlArray(item as YAMLSeq, arr[idx] as unknown[], doc) + } else { + console.log('Unknown data type:') + console.log(item) + } + } + // override element if different type + else { + const yamlItem = doc.createNode(arrItem) + acc.push(yamlItem) + return acc + } + + acc.push(item) + return acc + }, []) + + // delete element + if (targetEl.items.length > arr.length) { + targetEl.items.splice(arr.length) + } +} + +/** NOTE: updateYamlObject mutates targetEl */ +function updateYamlObject(targetEl: YAMLMap, obj: Record, doc: Document.Parsed) { + // update or delete + targetEl.items = targetEl.items.reduce[]>((acc, item) => { + // TODO: this "if" may be not needed + if (item.key instanceof Scalar) { + const propertyName = item.key.value + + if (propertyName in obj) { + const yamlElementType = getYamlElementType(item.value) + const dataType = getDataType(obj[propertyName]) + + if (dataType === 'undefined') { + // NOTE: undefined value is ignored/deleted + return acc + } + + // update element if same type + if (yamlElementType === dataType) { + if (isPrimitiveType(yamlElementType)) { + updateYamlPrimitive(item.value as Scalar, obj[propertyName]) + } else if (yamlElementType === 'object') { + updateYamlObject(item.value as YAMLMap, obj[propertyName] as Record, doc) + } else if (yamlElementType === 'array') { + updateYamlArray(item.value as YAMLSeq, obj[propertyName] as unknown[], doc) + } else { + console.log('Unknown data type:') + console.log(item) + } + } + // override element if different type + else { + const yamlItem = doc.createNode(obj[propertyName]) + item.value = yamlItem + } + + acc.push(item) + } else { + // delete element + // NOTE: No command required. We are deleting element by not adding it to acc. + } + } + + return acc + }, []) + + // add elements + const existingProperties: string[] = targetEl.items.map(item => (item.key instanceof Scalar ? item.key.value : '')) + forOwn(obj, (objItem, key) => { + if (!existingProperties.includes(key) && typeof objItem !== 'undefined') { + const yamlItem = doc.createPair(key, objItem) + targetEl.items.push(yamlItem) + } + }) +} + +export function updateYamlDocAtPath(path: string[], item: unknown, doc: Document.Parsed) { + const targetEl = doc.getIn(path) + const yamlElementType = getYamlElementType(targetEl) + const dataType = getDataType(item) + + if (yamlElementType === dataType) { + if (dataType === 'undefined') { + doc.deleteIn(path) + } else { + if (isPrimitiveType(yamlElementType)) { + doc.setIn(path, item) + } else if (yamlElementType === 'object') { + updateYamlObject(targetEl as YAMLMap, item as Record, doc) + } else if (yamlElementType === 'array') { + updateYamlArray(targetEl as YAMLSeq, item as unknown[], doc) + } + } + } else { + doc.setIn(path, item) + } +} diff --git a/packages/ui/src/views/pipeline-edit/utils/yaml-utils.ts b/packages/ui/src/views/pipeline-edit/utils/yaml-utils.ts new file mode 100644 index 0000000000..bda8783655 --- /dev/null +++ b/packages/ui/src/views/pipeline-edit/utils/yaml-utils.ts @@ -0,0 +1,71 @@ +import { parseDocument, YAMLSeq } from 'yaml' + +import { updateYamlDocAtPath } from './yaml-doc-utils' + +// TODO: split this to addToArray and insertInArray +export function injectItemInArray( + yaml: string, + injectData: { path: string; position: 'after' | 'before' | 'in' | undefined; item: unknown } +): string { + const { path, position, item } = injectData + + // if position is "last", path points to an array + if (position === 'in') { + const doc = parseDocument(yaml) + + const pathArr = path.split('.') + // NOTE: exception for minimal pipeline: "pipeline: {}" + if (path === 'pipeline' || path === 'pipeline.stages') { + const pl = doc.getIn(['pipeline']) as { flow: boolean } + pl.flow = false + } + + if (!doc.hasIn(pathArr)) { + // NOTE: if array does not exist, add array with item + doc.addIn(pathArr, [item]) + } else { + doc.addIn(pathArr, item) + } + + const arr = doc.getIn(path.split('.')) as { flow: boolean } //as Collection + arr.flow = false + + return doc.toString() + } + // if position is "after" or "before", path points to an array element + else if (position === 'after' || position === 'before') { + const pathArr = path.split('.') + const index = parseInt(pathArr.pop() ?? '0') + + const doc = parseDocument(yaml) + + const yamlItem = doc.createNode(item) + const collection = doc.getIn(pathArr) as YAMLSeq + collection.items.splice(position == 'before' ? index : index + 1, 0, yamlItem) + + return doc.toString() + } + + return yaml +} + +export function updateItemInArray(yaml: string, injectData: { path: string; item: unknown }): string { + const { path, item } = injectData + const pathArr = path.split('.') + + const doc = parseDocument(yaml) + + updateYamlDocAtPath(pathArr, item, doc) + + return doc.toString() +} + +export function deleteItemInArray(yaml: string, data: { path: string }) { + const { path } = data + + const doc = parseDocument(yaml) + + doc.deleteIn(path.split('.')) + + return doc.toString() +} diff --git a/packages/ui/tailwind.ts b/packages/ui/tailwind.ts index 2869672bcd..3cf96f0d61 100644 --- a/packages/ui/tailwind.ts +++ b/packages/ui/tailwind.ts @@ -357,5 +357,15 @@ export default { }) } ], - safelist: ['prose', 'prose-invert', 'prose-headings', 'prose-p', 'prose-a', 'prose-img', 'prose-code'] + safelist: [ + 'prose', + 'prose-invert', + 'prose-headings', + 'prose-p', + 'prose-a', + 'prose-img', + 'prose-code', + // NOTE: stroke-border-2 temporary here as it is used by in gitness for pipeline-graph + 'stroke-borders-2' + ] } satisfies TailwindConfig diff --git a/packages/views/src/components/form-inputs/common/InputLabel.tsx b/packages/views/src/components/form-inputs/common/InputLabel.tsx index 36bcaf5e54..50e856e85e 100644 --- a/packages/views/src/components/form-inputs/common/InputLabel.tsx +++ b/packages/views/src/components/form-inputs/common/InputLabel.tsx @@ -1,4 +1,4 @@ -import { Icon, Tooltip, TooltipContent, TooltipTrigger } from '@harnessio/canary' +// import { Icon, Tooltip, TooltipContent, TooltipTrigger } from '@harnessio/canary' export interface InputLabelProps { label?: string @@ -7,21 +7,22 @@ export interface InputLabelProps { } function InputLabel(props: InputLabelProps): JSX.Element { - const { label, description, required } = props + const { label, required } = props const labelText = required && label ? `${label} *` : label return (
{labelText}
- {description && ( + {/* TODO: TooltipProvider not available */} + {/* {description && ( {description} - )} + )} */}
) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ebb84ff932..5ae7808c03 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,15 +71,24 @@ importers: apps/design-system: dependencies: + '@harnessio/pipeline-graph': + specifier: workspace:* + version: link:../../packages/pipeline-graph '@harnessio/ui': specifier: workspace:* version: link:../../packages/ui '@harnessio/yaml-editor': specifier: workspace:* version: link:../../packages/yaml-editor + '@types/lodash-es': + specifier: ^4.17.12 + version: 4.17.12 clsx: specifier: ^2.1.1 version: 2.1.1 + lodash-es: + specifier: ^4.17.21 + version: 4.17.21 monaco-editor: specifier: 0.50.0 version: 0.50.0 @@ -95,6 +104,9 @@ importers: react-router-dom: specifier: ^6.26.0 version: 6.28.2(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + vite-plugin-monaco-editor: + specifier: ^1.1.0 + version: 1.1.0(monaco-editor@0.50.0) devDependencies: '@types/react': specifier: ^17.0.3 @@ -255,10 +267,10 @@ importers: version: 3.7.2(vite@6.0.11(@types/node@22.10.10)(jiti@1.21.7)(sass@1.83.4)(terser@5.37.0)(yaml@2.7.0)) '@vitest/coverage-istanbul': specifier: ^2.1.8 - version: 2.1.8(vitest@2.1.8) + version: 2.1.8(vitest@2.1.8(@types/node@22.10.10)(@vitest/ui@2.1.8)(jsdom@25.0.1)(sass@1.83.4)(terser@5.37.0)) css-loader: specifier: ^7.1.2 - version: 7.1.2(webpack@5.97.1) + version: 7.1.2(webpack@5.97.1(@swc/core@1.10.9)(webpack-cli@5.1.4)) eslint: specifier: ^8.57.1 version: 8.57.1 @@ -273,7 +285,7 @@ importers: version: 15.14.0 html-webpack-plugin: specifier: ^5.6.0 - version: 5.6.3(webpack@5.97.1) + version: 5.6.3(webpack@5.97.1(@swc/core@1.10.9)(webpack-cli@5.1.4)) immer: specifier: ^10.1.1 version: 10.1.1 @@ -282,19 +294,19 @@ importers: version: 4.1.5 postcss-loader: specifier: ^8.1.1 - version: 8.1.1(postcss@8.5.1)(typescript@5.7.3)(webpack@5.97.1) + version: 8.1.1(postcss@8.5.1)(typescript@5.7.3)(webpack@5.97.1(@swc/core@1.10.9)(webpack-cli@5.1.4)) prettier: specifier: ^3.0.3 version: 3.4.2 raw-loader: specifier: ^4.0.2 - version: 4.0.2(webpack@5.97.1) + version: 4.0.2(webpack@5.97.1(@swc/core@1.10.9)(webpack-cli@5.1.4)) style-loader: specifier: ^4.0.0 - version: 4.0.0(webpack@5.97.1) + version: 4.0.0(webpack@5.97.1(@swc/core@1.10.9)(webpack-cli@5.1.4)) swc-loader: specifier: ^0.2.6 - version: 0.2.6(@swc/core@1.10.9)(webpack@5.97.1) + version: 0.2.6(@swc/core@1.10.9)(webpack@5.97.1(@swc/core@1.10.9)(webpack-cli@5.1.4)) tailwindcss: specifier: ^3.4.4 version: 3.4.17 @@ -813,6 +825,12 @@ importers: '@git-diff-view/shiki': specifier: ^0.0.21 version: 0.0.21 + '@harnessio/pipeline-graph': + specifier: workspace:* + version: link:../pipeline-graph + '@harnessio/yaml-editor': + specifier: workspace:* + version: link:../yaml-editor '@hookform/resolvers': specifier: ^3.6.0 version: 3.10.0(react-hook-form@7.54.2(react@17.0.2)) @@ -948,6 +966,9 @@ importers: lodash-es: specifier: ^4.17.21 version: 4.17.21 + monaco-editor: + specifier: 0.50.0 + version: 0.50.0 overlayscrollbars: specifier: ^2.10.0 version: 2.10.1 @@ -990,6 +1011,9 @@ importers: tailwindcss-animate: specifier: ^1.0.7 version: 1.0.7(tailwindcss@3.4.17) + yaml: + specifier: ^2.7.0 + version: 2.7.0 zod: specifier: ^3.23.8 version: 3.24.1 @@ -1014,7 +1038,7 @@ importers: version: 3.7.2(vite@6.0.11(@types/node@22.10.10)(jiti@1.21.7)(sass@1.83.4)(terser@5.37.0)(yaml@2.7.0)) '@vitest/coverage-istanbul': specifier: ^2.1.8 - version: 2.1.8(vitest@2.1.8) + version: 2.1.8(vitest@2.1.8(@types/node@22.10.10)(@vitest/ui@2.1.8)(jsdom@25.0.1)(sass@1.83.4)(terser@5.37.0)) '@vitest/ui': specifier: ^2.1.8 version: 2.1.8(vitest@2.1.8) @@ -1383,7 +1407,7 @@ importers: version: 3.7.2(vite@4.5.9(@types/node@16.18.125)(sass@1.83.4)(terser@5.37.0)) babel-loader: specifier: ^9.1.3 - version: 9.2.1(@babel/core@7.26.7)(webpack@5.97.1) + version: 9.2.1(@babel/core@7.26.7)(webpack@5.97.1(webpack-cli@5.1.4)) babel-plugin-syntax-dynamic-import: specifier: ^6.18.0 version: 6.18.0 @@ -1395,37 +1419,37 @@ importers: version: 6.26.2 css-loader: specifier: ^7.1.2 - version: 7.1.2(webpack@5.97.1) + version: 7.1.2(webpack@5.97.1(webpack-cli@5.1.4)) file-loader: specifier: ^6.2.0 - version: 6.2.0(webpack@5.97.1) + version: 6.2.0(webpack@5.97.1(webpack-cli@5.1.4)) html-webpack-plugin: specifier: ^5.6.0 - version: 5.6.3(webpack@5.97.1) + version: 5.6.3(webpack@5.97.1(webpack-cli@5.1.4)) lint-staged: specifier: ^15.2.9 version: 15.4.2 mini-css-extract-plugin: specifier: ^2.9.0 - version: 2.9.2(webpack@5.97.1) + version: 2.9.2(webpack@5.97.1(webpack-cli@5.1.4)) monaco-editor-webpack-plugin: specifier: ^7.1.0 - version: 7.1.0(monaco-editor@0.50.0)(webpack@5.97.1) + version: 7.1.0(monaco-editor@0.50.0)(webpack@5.97.1(webpack-cli@5.1.4)) sass-loader: specifier: ^14.2.1 - version: 14.2.1(sass@1.83.4)(webpack@5.97.1) + version: 14.2.1(sass@1.83.4)(webpack@5.97.1(webpack-cli@5.1.4)) style-loader: specifier: ^4.0.0 - version: 4.0.0(webpack@5.97.1) + version: 4.0.0(webpack@5.97.1(webpack-cli@5.1.4)) ts-loader: specifier: ^9.5.1 - version: 9.5.2(typescript@4.9.5)(webpack@5.97.1) + version: 9.5.2(typescript@4.9.5)(webpack@5.97.1(webpack-cli@5.1.4)) typescript: specifier: ^4.9.5 version: 4.9.5 url-loader: specifier: ^4.1.1 - version: 4.1.1(file-loader@6.2.0(webpack@5.97.1))(webpack@5.97.1) + version: 4.1.1(file-loader@6.2.0(webpack@5.97.1(webpack-cli@5.1.4)))(webpack@5.97.1(webpack-cli@5.1.4)) vite: specifier: ^4.4.9 version: 4.5.9(@types/node@16.18.125)(sass@1.83.4)(terser@5.37.0) @@ -1470,7 +1494,7 @@ importers: version: 17.0.2(react@17.0.2) ts-loader: specifier: ^9.5.1 - version: 9.5.2(typescript@4.9.5)(webpack@5.97.1) + version: 9.5.2(typescript@4.9.5)(webpack@5.97.1(webpack-cli@5.1.4)) typescript: specifier: ^4.9.5 version: 4.9.5 @@ -1510,7 +1534,7 @@ importers: version: 17.0.26(@types/react@17.0.83) babel-loader: specifier: ^9.1.3 - version: 9.2.1(@babel/core@7.26.7)(webpack@5.97.1) + version: 9.2.1(@babel/core@7.26.7)(webpack@5.97.1(webpack-cli@5.1.4)) babel-plugin-syntax-dynamic-import: specifier: ^6.18.0 version: 6.18.0 @@ -1522,31 +1546,31 @@ importers: version: 6.26.2 css-loader: specifier: ^7.1.2 - version: 7.1.2(webpack@5.97.1) + version: 7.1.2(webpack@5.97.1(webpack-cli@5.1.4)) file-loader: specifier: ^6.2.0 - version: 6.2.0(webpack@5.97.1) + version: 6.2.0(webpack@5.97.1(webpack-cli@5.1.4)) html-webpack-plugin: specifier: ^5.6.0 - version: 5.6.3(webpack@5.97.1) + version: 5.6.3(webpack@5.97.1(webpack-cli@5.1.4)) lint-staged: specifier: ^15.2.9 version: 15.4.2 mini-css-extract-plugin: specifier: ^2.9.0 - version: 2.9.2(webpack@5.97.1) + version: 2.9.2(webpack@5.97.1(webpack-cli@5.1.4)) monaco-editor-webpack-plugin: specifier: ^7.1.0 - version: 7.1.0(monaco-editor@0.50.0)(webpack@5.97.1) + version: 7.1.0(monaco-editor@0.50.0)(webpack@5.97.1(webpack-cli@5.1.4)) sass-loader: specifier: ^14.2.1 - version: 14.2.1(sass@1.83.4)(webpack@5.97.1) + version: 14.2.1(sass@1.83.4)(webpack@5.97.1(webpack-cli@5.1.4)) style-loader: specifier: ^4.0.0 - version: 4.0.0(webpack@5.97.1) + version: 4.0.0(webpack@5.97.1(webpack-cli@5.1.4)) url-loader: specifier: ^4.1.1 - version: 4.1.1(file-loader@6.2.0(webpack@5.97.1))(webpack@5.97.1) + version: 4.1.1(file-loader@6.2.0(webpack@5.97.1(webpack-cli@5.1.4)))(webpack@5.97.1(webpack-cli@5.1.4)) webpack: specifier: ^5.92.1 version: 5.97.1(webpack-cli@5.1.4) @@ -15144,7 +15168,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-istanbul@2.1.8(vitest@2.1.8)': + '@vitest/coverage-istanbul@2.1.8(vitest@2.1.8(@types/node@22.10.10)(@vitest/ui@2.1.8)(jsdom@25.0.1)(sass@1.83.4)(terser@5.37.0))': dependencies: '@istanbuljs/schema': 0.1.3 debug: 4.4.0 @@ -15414,17 +15438,17 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@xtuc/long': 4.2.2 - '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4)(webpack@5.97.1)': + '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4(webpack-dev-server@5.2.0)(webpack@5.97.1))(webpack@5.97.1(@swc/core@1.10.9)(webpack-cli@5.1.4))': dependencies: webpack: 5.97.1(@swc/core@1.10.9)(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack-dev-server@5.2.0)(webpack@5.97.1) - '@webpack-cli/info@2.0.2(webpack-cli@5.1.4)(webpack@5.97.1)': + '@webpack-cli/info@2.0.2(webpack-cli@5.1.4(webpack-dev-server@5.2.0)(webpack@5.97.1))(webpack@5.97.1(@swc/core@1.10.9)(webpack-cli@5.1.4))': dependencies: webpack: 5.97.1(@swc/core@1.10.9)(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack-dev-server@5.2.0)(webpack@5.97.1) - '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4)(webpack-dev-server@5.2.0)(webpack@5.97.1)': + '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4(webpack-dev-server@5.2.0)(webpack@5.97.1))(webpack-dev-server@5.2.0(webpack-cli@5.1.4)(webpack@5.97.1))(webpack@5.97.1(@swc/core@1.10.9)(webpack-cli@5.1.4))': dependencies: webpack: 5.97.1(@swc/core@1.10.9)(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack-dev-server@5.2.0)(webpack@5.97.1) @@ -15804,7 +15828,7 @@ snapshots: transitivePeerDependencies: - supports-color - babel-loader@9.2.1(@babel/core@7.26.7)(webpack@5.97.1): + babel-loader@9.2.1(@babel/core@7.26.7)(webpack@5.97.1(webpack-cli@5.1.4)): dependencies: '@babel/core': 7.26.7 find-cache-dir: 4.0.0 @@ -16520,7 +16544,7 @@ snapshots: dependencies: uncrypto: 0.1.3 - css-loader@7.1.2(webpack@5.97.1): + css-loader@7.1.2(webpack@5.97.1(@swc/core@1.10.9)(webpack-cli@5.1.4)): dependencies: icss-utils: 5.1.0(postcss@8.5.1) postcss: 8.5.1 @@ -16533,6 +16557,19 @@ snapshots: optionalDependencies: webpack: 5.97.1(@swc/core@1.10.9)(webpack-cli@5.1.4) + css-loader@7.1.2(webpack@5.97.1(webpack-cli@5.1.4)): + dependencies: + icss-utils: 5.1.0(postcss@8.5.1) + postcss: 8.5.1 + postcss-modules-extract-imports: 3.1.0(postcss@8.5.1) + postcss-modules-local-by-default: 4.2.0(postcss@8.5.1) + postcss-modules-scope: 3.2.1(postcss@8.5.1) + postcss-modules-values: 4.0.0(postcss@8.5.1) + postcss-value-parser: 4.2.0 + semver: 7.6.3 + optionalDependencies: + webpack: 5.97.1(webpack-cli@5.1.4) + css-modules-loader-core@1.1.0: dependencies: icss-replace-symbols: 1.1.0 @@ -17260,7 +17297,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: @@ -17282,7 +17319,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -17626,7 +17663,7 @@ snapshots: dependencies: flat-cache: 3.2.0 - file-loader@6.2.0(webpack@5.97.1): + file-loader@6.2.0(webpack@5.97.1(webpack-cli@5.1.4)): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 @@ -18232,7 +18269,7 @@ snapshots: html-void-elements@3.0.0: {} - html-webpack-plugin@5.6.3(webpack@5.97.1): + html-webpack-plugin@5.6.3(webpack@5.97.1(@swc/core@1.10.9)(webpack-cli@5.1.4)): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -18242,6 +18279,16 @@ snapshots: optionalDependencies: webpack: 5.97.1(@swc/core@1.10.9)(webpack-cli@5.1.4) + html-webpack-plugin@5.6.3(webpack@5.97.1(webpack-cli@5.1.4)): + dependencies: + '@types/html-minifier-terser': 6.1.0 + html-minifier-terser: 6.1.0 + lodash: 4.17.21 + pretty-error: 4.0.0 + tapable: 2.2.1 + optionalDependencies: + webpack: 5.97.1(webpack-cli@5.1.4) + html-whitespace-sensitive-tag-names@3.0.1: {} htmlparser2@6.1.0: @@ -19931,7 +19978,7 @@ snapshots: min-indent@1.0.1: {} - mini-css-extract-plugin@2.9.2(webpack@5.97.1): + mini-css-extract-plugin@2.9.2(webpack@5.97.1(webpack-cli@5.1.4)): dependencies: schema-utils: 4.3.0 tapable: 2.2.1 @@ -19982,7 +20029,7 @@ snapshots: moment@2.30.1: {} - monaco-editor-webpack-plugin@7.1.0(monaco-editor@0.50.0)(webpack@5.97.1): + monaco-editor-webpack-plugin@7.1.0(monaco-editor@0.50.0)(webpack@5.97.1(webpack-cli@5.1.4)): dependencies: loader-utils: 2.0.4 monaco-editor: 0.50.0 @@ -20461,7 +20508,7 @@ snapshots: optionalDependencies: postcss: 8.5.1 - postcss-loader@8.1.1(postcss@8.5.1)(typescript@5.7.3)(webpack@5.97.1): + postcss-loader@8.1.1(postcss@8.5.1)(typescript@5.7.3)(webpack@5.97.1(@swc/core@1.10.9)(webpack-cli@5.1.4)): dependencies: cosmiconfig: 9.0.0(typescript@5.7.3) jiti: 1.21.7 @@ -20680,7 +20727,7 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 - raw-loader@4.0.2(webpack@5.97.1): + raw-loader@4.0.2(webpack@5.97.1(@swc/core@1.10.9)(webpack-cli@5.1.4)): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 @@ -21323,7 +21370,7 @@ snapshots: dependencies: suf-log: 2.5.3 - sass-loader@14.2.1(sass@1.83.4)(webpack@5.97.1): + sass-loader@14.2.1(sass@1.83.4)(webpack@5.97.1(webpack-cli@5.1.4)): dependencies: neo-async: 2.6.2 optionalDependencies: @@ -21833,10 +21880,14 @@ snapshots: strip-json-comments@3.1.1: {} - style-loader@4.0.0(webpack@5.97.1): + style-loader@4.0.0(webpack@5.97.1(@swc/core@1.10.9)(webpack-cli@5.1.4)): dependencies: webpack: 5.97.1(@swc/core@1.10.9)(webpack-cli@5.1.4) + style-loader@4.0.0(webpack@5.97.1(webpack-cli@5.1.4)): + dependencies: + webpack: 5.97.1(webpack-cli@5.1.4) + style-to-object@1.0.8: dependencies: inline-style-parser: 0.2.4 @@ -21877,7 +21928,7 @@ snapshots: svg-parser@2.0.4: {} - swc-loader@0.2.6(@swc/core@1.10.9)(webpack@5.97.1): + swc-loader@0.2.6(@swc/core@1.10.9)(webpack@5.97.1(@swc/core@1.10.9)(webpack-cli@5.1.4)): dependencies: '@swc/core': 1.10.9 '@swc/counter': 0.1.3 @@ -21964,7 +22015,7 @@ snapshots: dependencies: streamx: 2.21.1 - terser-webpack-plugin@5.3.11(@swc/core@1.10.9)(webpack@5.97.1): + terser-webpack-plugin@5.3.11(@swc/core@1.10.9)(webpack@5.97.1(@swc/core@1.10.9)(webpack-cli@5.1.4)): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 @@ -21975,7 +22026,7 @@ snapshots: optionalDependencies: '@swc/core': 1.10.9 - terser-webpack-plugin@5.3.11(webpack@5.97.1): + terser-webpack-plugin@5.3.11(webpack@5.97.1(webpack-cli@5.1.4)): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 @@ -22118,7 +22169,7 @@ snapshots: '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.26.7) - ts-loader@9.5.2(typescript@4.9.5)(webpack@5.97.1): + ts-loader@9.5.2(typescript@4.9.5)(webpack@5.97.1(webpack-cli@5.1.4)): dependencies: chalk: 4.1.2 enhanced-resolve: 5.18.0 @@ -22391,14 +22442,14 @@ snapshots: dependencies: punycode: 2.3.1 - url-loader@4.1.1(file-loader@6.2.0(webpack@5.97.1))(webpack@5.97.1): + url-loader@4.1.1(file-loader@6.2.0(webpack@5.97.1(webpack-cli@5.1.4)))(webpack@5.97.1(webpack-cli@5.1.4)): dependencies: loader-utils: 2.0.4 mime-types: 2.1.35 schema-utils: 3.3.0 webpack: 5.97.1(webpack-cli@5.1.4) optionalDependencies: - file-loader: 6.2.0(webpack@5.97.1) + file-loader: 6.2.0(webpack@5.97.1(webpack-cli@5.1.4)) use-callback-ref@1.3.3(@types/react@17.0.83)(react@17.0.2): dependencies: @@ -22947,9 +22998,9 @@ snapshots: webpack-cli@5.1.4(webpack-dev-server@5.2.0)(webpack@5.97.1): dependencies: '@discoveryjs/json-ext': 0.5.7 - '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4)(webpack@5.97.1) - '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4)(webpack@5.97.1) - '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4)(webpack-dev-server@5.2.0)(webpack@5.97.1) + '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4(webpack-dev-server@5.2.0)(webpack@5.97.1))(webpack@5.97.1(@swc/core@1.10.9)(webpack-cli@5.1.4)) + '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4(webpack-dev-server@5.2.0)(webpack@5.97.1))(webpack@5.97.1(@swc/core@1.10.9)(webpack-cli@5.1.4)) + '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4(webpack-dev-server@5.2.0)(webpack@5.97.1))(webpack-dev-server@5.2.0(webpack-cli@5.1.4)(webpack@5.97.1))(webpack@5.97.1(@swc/core@1.10.9)(webpack-cli@5.1.4)) colorette: 2.0.20 commander: 10.0.1 cross-spawn: 7.0.6 @@ -22963,7 +23014,7 @@ snapshots: optionalDependencies: webpack-dev-server: 5.2.0(webpack-cli@5.1.4)(webpack@5.97.1) - webpack-dev-middleware@7.4.2(webpack@5.97.1): + webpack-dev-middleware@7.4.2(webpack@5.97.1(@swc/core@1.10.9)(webpack-cli@5.1.4)): dependencies: colorette: 2.0.20 memfs: 4.17.0 @@ -23001,7 +23052,7 @@ snapshots: serve-index: 1.9.1 sockjs: 0.3.24 spdy: 4.0.2 - webpack-dev-middleware: 7.4.2(webpack@5.97.1) + webpack-dev-middleware: 7.4.2(webpack@5.97.1(@swc/core@1.10.9)(webpack-cli@5.1.4)) ws: 8.18.0 optionalDependencies: webpack: 5.97.1(@swc/core@1.10.9)(webpack-cli@5.1.4) @@ -23042,7 +23093,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.11(@swc/core@1.10.9)(webpack@5.97.1) + terser-webpack-plugin: 5.3.11(@swc/core@1.10.9)(webpack@5.97.1(@swc/core@1.10.9)(webpack-cli@5.1.4)) watchpack: 2.4.2 webpack-sources: 3.2.3 optionalDependencies: @@ -23074,7 +23125,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.11(webpack@5.97.1) + terser-webpack-plugin: 5.3.11(webpack@5.97.1(webpack-cli@5.1.4)) watchpack: 2.4.2 webpack-sources: 3.2.3 optionalDependencies: