Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)];
};
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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)];
};
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ 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';
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;
Expand Down Expand Up @@ -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<
Expand All @@ -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<
Expand Down Expand Up @@ -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;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
);
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
});

Expand All @@ -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(
Expand All @@ -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', () => {
Expand All @@ -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', () => {
Expand All @@ -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', () => {
Expand All @@ -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 = <GitAltIcon />;
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 = <GitAltIcon />;
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'));
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const getAddPageUrl = (
obj: K8sResourceKind,
type: string,
hasApplication: boolean,
contextSource?: string,
): string => {
let pageUrl = '';
const params = new URLSearchParams();
Expand Down Expand Up @@ -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,
Expand All @@ -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'),
};
};
Loading