diff --git a/frontend/packages/dev-console/src/components/topology/actions/graphActions.ts b/frontend/packages/dev-console/src/components/topology/actions/graphActions.ts index fbae2f16251..49f70294ac9 100644 --- a/frontend/packages/dev-console/src/components/topology/actions/graphActions.ts +++ b/frontend/packages/dev-console/src/components/topology/actions/graphActions.ts @@ -5,21 +5,24 @@ import { TYPE_WORKLOAD } from '../const'; import { addResourceMenu } from '../../../actions/add-resources'; import { TopologyDataObject } from '../topology-types'; -const addResourcesMenu = (workload: TopologyDataObject) => { +const addResourcesMenu = (workload: TopologyDataObject, connectorSource?: Node) => { let menuItems = []; if (_.isEmpty(workload)) { return menuItems; } const primaryResource = _.get(workload, ['resources', 'obj'], null); + const connectorSourceObj = connectorSource?.getData()?.resources?.obj || {}; if (primaryResource) { - menuItems = addResourceMenu.map((menuItem) => menuItem(primaryResource, false)); + menuItems = addResourceMenu.map((menuItem) => + menuItem(primaryResource, false, connectorSourceObj), + ); } return menuItems; }; -export const graphActions = (elements: GraphElement[]): KebabOption[] => { +export const graphActions = (elements: GraphElement[], connectorSource?: Node): KebabOption[] => { const primaryResource: Node = _.find(elements, { type: TYPE_WORKLOAD, }) as Node; - return [...addResourcesMenu(primaryResource.getData())]; + return [...addResourcesMenu(primaryResource.getData(), connectorSource)]; }; diff --git a/frontend/packages/dev-console/src/components/topology/actions/groupActions.ts b/frontend/packages/dev-console/src/components/topology/actions/groupActions.ts index fbc32f65f81..72d9d4cdfcd 100644 --- a/frontend/packages/dev-console/src/components/topology/actions/groupActions.ts +++ b/frontend/packages/dev-console/src/components/topology/actions/groupActions.ts @@ -1,6 +1,7 @@ import * as _ from 'lodash'; import { KebabOption } from '@console/internal/components/utils/kebab'; import { modelFor, referenceFor } from '@console/internal/module/k8s'; +import { Node } from '@console/topology'; import { asAccessReview } from '@console/internal/components/utils'; import { addResourceMenu } from '../../../actions/add-resources'; import { TopologyDataMap, TopologyApplicationObject } from '../topology-types'; @@ -51,10 +52,17 @@ const deleteGroup = (application: TopologyApplicationObject) => { }; }; -const addResourcesMenu = (application: TopologyApplicationObject) => { +const addResourcesMenu = (application: TopologyApplicationObject, connectorSource?: Node) => { const primaryResource = _.get(application.resources[0], ['resources', 'obj']); - return addResourceMenu.map((menuItem) => menuItem(primaryResource, true)); + const connectorSourceObj = connectorSource?.getData()?.resources?.obj || {}; + return addResourceMenu.map((menuItem) => menuItem(primaryResource, true, connectorSourceObj)); }; -export const groupActions = (application: TopologyApplicationObject): KebabOption[] => { - return [deleteGroup(application), ...addResourcesMenu(application)]; + +export const groupActions = ( + application: TopologyApplicationObject, + connectorSource?: Node, +): KebabOption[] => { + return !connectorSource + ? [deleteGroup(application), ...addResourcesMenu(application)] + : [...addResourcesMenu(application, connectorSource)]; }; diff --git a/frontend/packages/dev-console/src/components/topology/componentFactory.ts b/frontend/packages/dev-console/src/components/topology/componentFactory.ts index 084b3e4d85f..1ec3d207c55 100644 --- a/frontend/packages/dev-console/src/components/topology/componentFactory.ts +++ b/frontend/packages/dev-console/src/components/topology/componentFactory.ts @@ -169,7 +169,10 @@ class ComponentFactory { case TYPE_REVISION_TRAFFIC: return TrafficLink; case TYPE_WORKLOAD: - return withCreateConnector(createConnectorCallback(this.hasServiceBinding))( + return withCreateConnector( + createConnectorCallback(this.hasServiceBinding), + 'odc-topology-context-menu', + )( withDndDrop< any, any, diff --git a/frontend/packages/dev-console/src/components/topology/componentUtils.ts b/frontend/packages/dev-console/src/components/topology/componentUtils.ts index bbebf9451c8..f7386f934b8 100644 --- a/frontend/packages/dev-console/src/components/topology/componentUtils.ts +++ b/frontend/packages/dev-console/src/components/topology/componentUtils.ts @@ -12,6 +12,7 @@ import { DropTargetMonitor, CREATE_CONNECTOR_DROP_TYPE, CREATE_CONNECTOR_OPERATION, + isGraph, } from '@console/topology'; import { K8sResourceKind } from '@console/internal/module/k8s'; import { createConnection } from './components/createConnection'; @@ -19,6 +20,7 @@ import { removeConnection } from './components/removeConnection'; import { moveNodeToGroup } from './components/moveNodeToGroup'; import { TYPE_CONNECTS_TO, TYPE_WORKLOAD, TYPE_KNATIVE_SERVICE, TYPE_EVENT_SOURCE } from './const'; import './components/GraphComponent.scss'; +import { graphContextMenu, groupContextMenu } from './nodeContextMenu'; type GraphProps = { element: Graph; @@ -152,13 +154,23 @@ const graphWorkloadDropTargetSpec: DropTargetSpec< { dragEditInProgress: boolean }, GraphProps > = { - accept: [TYPE_WORKLOAD, TYPE_KNATIVE_SERVICE, TYPE_EVENT_SOURCE, TYPE_CONNECTS_TO], + accept: [ + TYPE_WORKLOAD, + TYPE_KNATIVE_SERVICE, + TYPE_EVENT_SOURCE, + TYPE_CONNECTS_TO, + CREATE_CONNECTOR_DROP_TYPE, + ], canDrop: (item, monitor, props) => { - return monitor.getOperation() === REGROUP_OPERATION && item.getParent() !== props.element; + return ( + (monitor.getOperation() === REGROUP_OPERATION && item.getParent() !== props.element) || + monitor.getItemType() === CREATE_CONNECTOR_DROP_TYPE + ); }, collect: (monitor) => ({ dragEditInProgress: monitor.isDragging() && editOperations.includes(monitor.getOperation()), }), + dropHint: 'create', }; const groupWorkloadDropTargetSpec: DropTargetSpec< @@ -167,13 +179,16 @@ const groupWorkloadDropTargetSpec: DropTargetSpec< { droppable: boolean; dropTarget: boolean; canDrop: boolean }, any > = { - accept: [TYPE_WORKLOAD, TYPE_EVENT_SOURCE, TYPE_KNATIVE_SERVICE], - canDrop: (item, monitor) => monitor.getOperation() === REGROUP_OPERATION, + accept: [TYPE_WORKLOAD, TYPE_EVENT_SOURCE, TYPE_KNATIVE_SERVICE, CREATE_CONNECTOR_DROP_TYPE], + canDrop: (item, monitor) => + monitor.getOperation() === REGROUP_OPERATION || + monitor.getItemType() === CREATE_CONNECTOR_DROP_TYPE, collect: (monitor) => ({ droppable: monitor.isDragging() && monitor.getOperation() === REGROUP_OPERATION, dropTarget: monitor.isOver(), canDrop: monitor.canDrop(), }), + dropHint: 'create', }; const graphEventSourceDropTargetSpec: DropTargetSpec< @@ -226,8 +241,14 @@ const edgeDragSourceSpec = ( const createConnectorCallback = (serviceBinding: boolean) => ( source: Node, - target: Node, -): any[] | null => { + target: Node | Graph, +): React.ReactElement[] | null => { + if (isGraph(target)) { + return graphContextMenu(target, source); + } + if (target.isGroup()) { + return groupContextMenu(target, source); + } createConnection(source, target, null, serviceBinding); return null; }; diff --git a/frontend/packages/dev-console/src/components/topology/nodeContextMenu.tsx b/frontend/packages/dev-console/src/components/topology/nodeContextMenu.tsx index 434abbba63a..34aee76908d 100644 --- a/frontend/packages/dev-console/src/components/topology/nodeContextMenu.tsx +++ b/frontend/packages/dev-console/src/components/topology/nodeContextMenu.tsx @@ -40,17 +40,19 @@ const createMenuItems = (actions: KebabMenuOption[]) => export const workloadContextMenu = (element: Node) => createMenuItems(kebabOptionsToMenu(workloadActions(element.getData()))); -export const groupContextMenu = (element: Node) => { +export const groupContextMenu = (element: Node, connectorSource?: Node) => { const applicationData: TopologyApplicationObject = { id: element.getId(), name: element.getLabel(), resources: element.getChildren().map((node: GraphElement) => node.getData()), }; - return createMenuItems(kebabOptionsToMenu(groupActions(applicationData))); + return createMenuItems(kebabOptionsToMenu(groupActions(applicationData, connectorSource))); }; export const nodeContextMenu = (element: Node) => createMenuItems(kebabOptionsToMenu(nodeActions(element.getData()))); -export const graphContextMenu = (element: Graph) => - createMenuItems(kebabOptionsToMenu(graphActions(element.getController().getElements()))); +export const graphContextMenu = (element: Graph, connectorSource?: Node) => + createMenuItems( + kebabOptionsToMenu(graphActions(element.getController().getElements(), connectorSource)), + ); diff --git a/frontend/packages/dev-console/src/utils/__tests__/add-resources-menu-utils.spec.tsx b/frontend/packages/dev-console/src/utils/__tests__/add-resources-menu-utils.spec.tsx index 48cc32272fa..1a5e027c7b0 100644 --- a/frontend/packages/dev-console/src/utils/__tests__/add-resources-menu-utils.spec.tsx +++ b/frontend/packages/dev-console/src/utils/__tests__/add-resources-menu-utils.spec.tsx @@ -16,6 +16,7 @@ import { import { ImportOptions } from '../../components/import/import-types'; import { MockResources } from '../../components/topology/__tests__/topology-test-data'; import { TopologyDataResources } from '../../components/topology/topology-types'; +import { referenceFor } from '@console/internal/module/k8s'; const getTopologyData = (mockData: TopologyDataResources, transformByProp: string[]) => { const result = transformTopologyData(mockData, transformByProp); @@ -31,13 +32,22 @@ describe('addResourceMenuUtils: ', () => { }); it('should return the page url with proper queryparams for git import flow', () => { - const { resource } = getTopologyData(MockResources, ['deployments']); - const url = new URL(getAddPageUrl(resource, ImportOptions.GIT, true), 'https://mock.test.com'); + const primaryResource = getTopologyData(MockResources, ['deployments']).resource; + const connectorSourceObj = getTopologyData(MockResources, ['deploymentConfigs']).resource; + const contextSource: string = `${referenceFor(connectorSourceObj)}/${ + connectorSourceObj?.metadata?.name + }`; + const url = new URL( + getAddPageUrl(primaryResource, ImportOptions.GIT, true, contextSource), + 'https://mock.test.com', + ); expect(url.pathname).toBe('/import/ns/testproject1'); expect(url.searchParams.get('importType')).toBe('git'); expect(url.searchParams.get('application')).toBe('application-1'); - expect(url.searchParams.get('isKnativeDisabled')).toBe('true'); + expect(url.searchParams.get('contextSource')).toBe( + 'apps.openshift.io~v1~DeploymentConfig%2Fnodejs', + ); expect(Array.from(url.searchParams.entries())).toHaveLength(3); }); @@ -47,6 +57,12 @@ describe('addResourceMenuUtils: ', () => { expect(url.searchParams.has('application')).toBe(false); }); + it('should return the page url without contextSource params in the url', () => { + const { resource } = getTopologyData(MockResources, ['deployments']); + const url = new URL(getAddPageUrl(resource, ImportOptions.GIT, false), 'https://mock.test.com'); + expect(url.searchParams.has('contextSource')).toBe(false); + }); + it('should return the page url with proper queryparams for container image flow', () => { const { resource } = getTopologyData(MockResources, ['deployments']); const url = new URL( @@ -55,8 +71,7 @@ describe('addResourceMenuUtils: ', () => { ); expect(url.pathname).toBe('/deploy-image/ns/testproject1'); expect(url.searchParams.get('application')).toBe('application-1'); - expect(url.searchParams.get('isKnativeDisabled')).toBe('true'); - expect(Array.from(url.searchParams.entries())).toHaveLength(2); + expect(Array.from(url.searchParams.entries())).toHaveLength(1); }); it('should return the page url with proper queryparams for catalog flow', () => { @@ -67,8 +82,7 @@ describe('addResourceMenuUtils: ', () => { ); expect(url.pathname).toBe('/catalog/ns/testproject1'); expect(url.searchParams.get('application')).toBe('application-1'); - expect(url.searchParams.get('isKnativeDisabled')).toBe('true'); - expect(Array.from(url.searchParams.entries())).toHaveLength(2); + expect(Array.from(url.searchParams.entries())).toHaveLength(1); }); it('should return the page url with proper queryparams for dockerfile flow', () => { @@ -80,8 +94,7 @@ describe('addResourceMenuUtils: ', () => { expect(url.pathname).toBe('/import/ns/testproject1'); expect(url.searchParams.get('importType')).toBe('docker'); expect(url.searchParams.get('application')).toBe('application-1'); - expect(url.searchParams.get('isKnativeDisabled')).toBe('true'); - expect(Array.from(url.searchParams.entries())).toHaveLength(3); + expect(Array.from(url.searchParams.entries())).toHaveLength(2); }); it('should return the page url with proper queryparams for database flow', () => { @@ -93,23 +106,44 @@ describe('addResourceMenuUtils: ', () => { expect(url.pathname).toBe('/catalog/ns/testproject1'); expect(url.searchParams.get('category')).toBe('databases'); expect(url.searchParams.get('application')).toBe('application-1'); - expect(url.searchParams.get('isKnativeDisabled')).toBe('true'); - expect(Array.from(url.searchParams.entries())).toHaveLength(3); + expect(Array.from(url.searchParams.entries())).toHaveLength(2); }); - it('it should return a valid kebabAction on invoking createKebabAction', () => { - const { resource } = getTopologyData(MockResources, ['deployments']); + it('it should return a valid kebabAction on invoking createKebabAction with connectorSourceObj', () => { + const primaryObj = getTopologyData(MockResources, ['deployments']).resource; + const connectorSourceObj = getTopologyData(MockResources, ['deploymentConfigs']).resource; + const icon = ; + const hasApplication = true; + const label = 'From Git'; + + const kebabAction: KebabAction = createKebabAction(label, icon, ImportOptions.GIT); + const kebabOption: KebabOption = kebabAction(primaryObj, hasApplication, connectorSourceObj); + const contextSource: string = `${referenceFor(connectorSourceObj)}/${ + connectorSourceObj?.metadata?.name + }`; + + expect(kebabOption.label).toEqual(label); + expect(kebabOption.icon).toEqual(icon); + expect(kebabOption.path).toEqual(null); + expect(kebabOption.href).toEqual( + getAddPageUrl(primaryObj, ImportOptions.GIT, hasApplication, contextSource), + ); + expect(kebabOption.accessReview).toEqual(asAccessReview(DeploymentModel, primaryObj, 'create')); + }); + + it('it should return a valid kebabAction on invoking createKebabAction without connectorSourceObj', () => { + const primaryObj = getTopologyData(MockResources, ['deployments']).resource; const icon = ; const hasApplication = true; const label = 'From Git'; const kebabAction: KebabAction = createKebabAction(label, icon, ImportOptions.GIT); - const kebabOption: KebabOption = kebabAction(resource, hasApplication); + const kebabOption: KebabOption = kebabAction(primaryObj, hasApplication); expect(kebabOption.label).toEqual(label); expect(kebabOption.icon).toEqual(icon); expect(kebabOption.path).toEqual('Add to Application'); - expect(kebabOption.href).toEqual(getAddPageUrl(resource, ImportOptions.GIT, hasApplication)); - expect(kebabOption.accessReview).toEqual(asAccessReview(DeploymentModel, resource, 'create')); + expect(kebabOption.href).toEqual(getAddPageUrl(primaryObj, ImportOptions.GIT, hasApplication)); + expect(kebabOption.accessReview).toEqual(asAccessReview(DeploymentModel, primaryObj, 'create')); }); }); diff --git a/frontend/packages/dev-console/src/utils/add-resources-menu-utils.ts b/frontend/packages/dev-console/src/utils/add-resources-menu-utils.ts index 81db6d14e43..1ee3166272d 100644 --- a/frontend/packages/dev-console/src/utils/add-resources-menu-utils.ts +++ b/frontend/packages/dev-console/src/utils/add-resources-menu-utils.ts @@ -9,6 +9,7 @@ export const getAddPageUrl = ( obj: K8sResourceKind, type: string, hasApplication: boolean, + contextSource?: string, ): string => { let pageUrl = ''; const params = new URLSearchParams(); @@ -38,15 +39,17 @@ export const getAddPageUrl = ( default: throw new Error('Invalid Import option provided'); } - params.append('isKnativeDisabled', 'true'); if (hasApplication && appGroup) { params.append('application', encodeURIComponent(appGroup)); } + if (contextSource) { + params.append('contextSource', encodeURIComponent(contextSource)); + } return `${pageUrl}?${params.toString()}`; }; -export const getMenuPath = (hasApplication: boolean): string => - hasApplication ? 'Add to Application' : 'Add to Project'; +export const getMenuPath = (hasApplication: boolean, connectorSourceContext?: string): string => + connectorSourceContext?.length ? null : hasApplication ? 'Add to Application' : 'Add to Project'; type KebabFactory = ( label: string, @@ -55,18 +58,26 @@ type KebabFactory = ( checkAccess?: boolean, ) => KebabAction; -export type KebabAction = (obj?: K8sResourceKind, hasApplication?: boolean) => KebabOption; +export type KebabAction = ( + obj?: K8sResourceKind, + hasApplication?: boolean, + connectorSourceObj?: K8sResourceKind, +) => KebabOption; export const createKebabAction: KebabFactory = (label, icon, importType, checkAccess = true) => ( obj: K8sResourceKind, hasApplication: boolean, + connectorSourceObj: K8sResourceKind, ) => { const resourceModel = modelFor(referenceFor(obj)); + const connectorSourceContext: string = connectorSourceObj?.metadata + ? `${referenceFor(connectorSourceObj)}/${connectorSourceObj?.metadata?.name}` + : null; return { label, icon, - path: getMenuPath(hasApplication), - href: getAddPageUrl(obj, importType, hasApplication), + path: getMenuPath(hasApplication, connectorSourceContext), + href: getAddPageUrl(obj, importType, hasApplication, connectorSourceContext), accessReview: checkAccess && asAccessReview(resourceModel, obj, 'create'), }; }; diff --git a/frontend/packages/topology/src/behavior/withCreateConnector.tsx b/frontend/packages/topology/src/behavior/withCreateConnector.tsx index 9100796e99d..c5150fb79fa 100644 --- a/frontend/packages/topology/src/behavior/withCreateConnector.tsx +++ b/frontend/packages/topology/src/behavior/withCreateConnector.tsx @@ -28,8 +28,9 @@ type CreateConnectorWidgetProps = { target: Node | Graph, event: DragEvent, choice?: ConnectorChoice, - ) => ConnectorChoice[] | void | undefined | null; + ) => ConnectorChoice[] | void | undefined | null | React.ReactElement[]; renderConnector: ConnectorRenderer; + contextMenuClass?: string; } & CreateConnectorOptions; type CollectProps = { @@ -42,7 +43,7 @@ type PromptData = { element: Node; target: Node | Graph; event: DragEvent; - choices: ConnectorChoice[]; + choices: (ConnectorChoice | React.ReactElement)[]; }; export const CREATE_CONNECTOR_DROP_TYPE = '#createConnector#'; @@ -58,6 +59,7 @@ const CreateConnectorWidget: React.FC = observer((pr renderConnector, handleAngle = DEFAULT_HANDLE_ANGLE, handleLength = DEFAULT_HANDLE_LENGTH, + contextMenuClass, } = props; const [prompt, setPrompt] = React.useState(null); const [active, setActive] = React.useState(false); @@ -162,22 +164,25 @@ const CreateConnectorWidget: React.FC = observer((pr {prompt && ( { setActive(false); onKeepAlive(false); }} > - {prompt.choices.map((c) => ( - { - onCreate(prompt.element, prompt.target, prompt.event, c); - }} - > - {c.label} - - ))} + {React.isValidElement(prompt.choices?.[0]) + ? prompt.choices + : prompt.choices.map((c: ConnectorChoice) => ( + { + onCreate(prompt.element, prompt.target, prompt.event, c); + }} + > + {c.label} + + ))} )} @@ -207,6 +212,7 @@ const defaultRenderConnector: ConnectorRenderer = ( export const withCreateConnector =

( onCreate: React.ComponentProps['onCreate'], + contextMenuClass?: string, renderConnector: ConnectorRenderer = defaultRenderConnector, options?: CreateConnectorOptions, ) => (WrappedComponent: React.ComponentType

) => { @@ -232,6 +238,7 @@ export const withCreateConnector =

)} diff --git a/frontend/packages/topology/src/components/DefaultCreateConnector.tsx b/frontend/packages/topology/src/components/DefaultCreateConnector.tsx index 76c19f22a7e..108144650f0 100644 --- a/frontend/packages/topology/src/components/DefaultCreateConnector.tsx +++ b/frontend/packages/topology/src/components/DefaultCreateConnector.tsx @@ -23,7 +23,7 @@ const DefaultCreateConnector: React.FC = ({ x2={endPoint.x} y2={endPoint.y} /> - {hints && hints.length === 1 && hints[0] === 'create' ? ( + {hints && hints[hints.length - 1] === 'create' ? (