Skip to content

Commit

Permalink
Merge pull request #3918 from karthikjeeyar/add-resource-menu
Browse files Browse the repository at this point in the history
feat(topology-resourcemenu): add resource menu actions
  • Loading branch information
openshift-merge-robot committed Jan 21, 2020
2 parents 97842d4 + 45f8300 commit fb0a5b2
Show file tree
Hide file tree
Showing 12 changed files with 311 additions and 13 deletions.
45 changes: 45 additions & 0 deletions frontend/packages/dev-console/src/actions/add-resources.tsx
@@ -0,0 +1,45 @@
import * as React from 'react';
import {
GitAltIcon,
OsImageIcon,
CatalogIcon,
CubeIcon,
DatabaseIcon,
} from '@patternfly/react-icons';
import { ImportOptions } from '../components/import/import-types';
import { KebabAction, createKebabAction } from '../utils/add-resources-menu-utils';

export const fromGit = createKebabAction('From Git', <GitAltIcon />, ImportOptions.GIT);

export const containerImage = createKebabAction(
'Container Image',
<OsImageIcon />,
ImportOptions.CONTAINER,
);

export const fromCatalog = createKebabAction(
'From Catalog',
<CatalogIcon />,
ImportOptions.CATALOG,
false,
);
export const fromDockerfile = createKebabAction(
'From Dockerfile',
<CubeIcon />,
ImportOptions.DOCKERFILE,
);

export const fromDatabaseCatalog = createKebabAction(
'Database',
<DatabaseIcon />,
ImportOptions.DATABASE,
false,
);

export const addResourceMenu: KebabAction[] = [
fromGit,
containerImage,
fromCatalog,
fromDockerfile,
fromDatabaseCatalog,
];
Expand Up @@ -271,3 +271,11 @@ export enum MemoryUnits {
Mi = 'Mi',
Gi = 'Gi',
}

export enum ImportOptions {
GIT = 'GIT',
CONTAINER = 'CONTAINER',
CATALOG = 'CATALOG',
DOCKERFILE = 'DOCKERFILE',
DATABASE = 'DATABASE',
}
@@ -0,0 +1,25 @@
import * as _ from 'lodash';
import { KebabOption } from '@console/internal/components/utils/kebab';
import { GraphElement, Node } from '@console/topology';
import { TYPE_WORKLOAD } from '../const';
import { addResourceMenu } from '../../../actions/add-resources';
import { TopologyDataObject } from '../topology-types';

const addResourcesMenu = (workload: TopologyDataObject) => {
let menuItems = [];
if (_.isEmpty(workload)) {
return menuItems;
}
const primaryResource = _.get(workload, ['resources', 'obj'], null);
if (primaryResource) {
menuItems = addResourceMenu.map((menuItem) => menuItem(primaryResource, false));
}
return menuItems;
};

export const graphActions = (elements: GraphElement[]): KebabOption[] => {
const primaryResource: Node = _.find(elements, {
type: TYPE_WORKLOAD,
}) as Node;
return [...addResourcesMenu(primaryResource.getData())];
};
Expand Up @@ -2,6 +2,7 @@ import * as _ from 'lodash';
import { KebabOption } from '@console/internal/components/utils/kebab';
import { modelFor, referenceFor } from '@console/internal/module/k8s';
import { asAccessReview } from '@console/internal/components/utils';
import { addResourceMenu } from '../../../actions/add-resources';
import { TopologyDataMap, TopologyApplicationObject } from '../topology-types';
import { getTopologyResourceObject } from '../topology-utils';
import { deleteApplicationModal } from '../../modals';
Expand Down Expand Up @@ -50,6 +51,10 @@ const deleteGroup = (application: TopologyApplicationObject) => {
};
};

const addResourcesMenu = (application: TopologyApplicationObject) => {
const primaryResource = _.get(application.resources[0], ['resources', 'obj']);
return addResourceMenu.map((menuItem) => menuItem(primaryResource, true));
};
export const groupActions = (application: TopologyApplicationObject): KebabOption[] => {
return [deleteGroup(application)];
return [deleteGroup(application), ...addResourcesMenu(application)];
};
Expand Up @@ -19,7 +19,12 @@ import EventSource from './components/nodes/EventSource';
import EventSourceLink from './components/edges/EventSourceLink';
import WorkloadNode from './components/nodes/WorkloadNode';
import GraphComponent from './components/GraphComponent';
import { workloadContextMenu, groupContextMenu, nodeContextMenu } from './nodeContextMenu';
import {
workloadContextMenu,
groupContextMenu,
nodeContextMenu,
graphContextMenu,
} from './nodeContextMenu';
import {
graphWorkloadDropTargetSpec,
nodeDragSourceSpec,
Expand Down Expand Up @@ -176,7 +181,18 @@ class ComponentFactory {
switch (kind) {
case ModelKind.graph:
return withDndDrop(graphWorkloadDropTargetSpec)(
withPanZoom()(withSelection(false, true)(GraphComponent)),
withPanZoom()(
withSelection(
false,
true,
)(
withContextMenu(
graphContextMenu,
document.getElementById('modal-container'),
'odc-topology-context-menu',
)(GraphComponent),
),
),
);
default:
return undefined;
Expand Down
@@ -1,13 +1,13 @@
import * as React from 'react';
import { connect } from 'react-redux';
import { RootState } from '@console/internal/redux';
import { GraphComponent as BaseGraphComponent } from '@console/topology';
import { GraphComponent as BaseGraphComponent, WithContextMenuProps } from '@console/topology';
import { TopologyFilters, getTopologyFilters } from '../filters/filter-utils';

type GraphComponentProps = React.ComponentProps<typeof BaseGraphComponent> & {
dragEditInProgress?: boolean;
filters: TopologyFilters;
};
} & WithContextMenuProps;

const DRAG_ACTIVE_CLASS = 'odc-m-drag-active';
const FILTER_ACTIVE_CLASS = 'odc-m-filter-active';
Expand Down
@@ -1,5 +1,5 @@
import * as React from 'react';
import { ContextMenuItem, ContextSubMenuItem, GraphElement, Node } from '@console/topology';
import { ContextMenuItem, ContextSubMenuItem, GraphElement, Node, Graph } from '@console/topology';
import {
history,
KebabItem,
Expand All @@ -11,6 +11,7 @@ import {
import { workloadActions } from './actions/workloadActions';
import { groupActions } from './actions/groupActions';
import { nodeActions } from './actions/nodeActions';
import { graphActions } from './actions/graphActions';
import { TopologyApplicationObject } from './topology-types';

const onKebabOptionClick = (option: KebabOption) => {
Expand All @@ -36,10 +37,10 @@ const createMenuItems = (actions: KebabMenuOption[]) =>
),
);

const workloadContextMenu = (element: Node) =>
export const workloadContextMenu = (element: Node) =>
createMenuItems(kebabOptionsToMenu(workloadActions(element.getData())));

const groupContextMenu = (element: Node) => {
export const groupContextMenu = (element: Node) => {
const applicationData: TopologyApplicationObject = {
id: element.getId(),
name: element.getLabel(),
Expand All @@ -48,8 +49,8 @@ const groupContextMenu = (element: Node) => {

return createMenuItems(kebabOptionsToMenu(groupActions(applicationData)));
};

const nodeContextMenu = (element: Node) =>
export const nodeContextMenu = (element: Node) =>
createMenuItems(kebabOptionsToMenu(nodeActions(element.getData())));

export { workloadContextMenu, groupContextMenu, nodeContextMenu };
export const graphContextMenu = (element: Graph) =>
createMenuItems(kebabOptionsToMenu(graphActions(element.getController().getElements())));
@@ -0,0 +1,115 @@
import { URL } from 'url';
import * as React from 'react';
import { GitAltIcon } from '@patternfly/react-icons';
import { KebabOption, asAccessReview } from '@console/internal/components/utils';
import { DeploymentModel } from '@console/internal/models';
import {
getMenuPath,
getAddPageUrl,
createKebabAction,
KebabAction,
} from '../add-resources-menu-utils';
import {
transformTopologyData,
getTopologyResourceObject,
} from '../../components/topology/topology-utils';
import { ImportOptions } from '../../components/import/import-types';
import { MockResources } from '../../components/topology/__tests__/topology-test-data';
import { TopologyDataResources } from '../../components/topology/topology-types';

const getTopologyData = (mockData: TopologyDataResources, transformByProp: string[]) => {
const result = transformTopologyData(mockData, transformByProp);
const keys = Object.keys(result.topology);
const resource = getTopologyResourceObject(result.topology[keys[0]]);
return { resource };
};

describe('addResourceMenuUtils: ', () => {
it('should give proper menu item path based on the application', () => {
expect(getMenuPath(true)).toEqual('Add to Application');
expect(getMenuPath(false)).toEqual('Add to Project');
});

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');

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(Array.from(url.searchParams.entries())).toHaveLength(3);
});

it('should return the page url without application 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('application')).toBe(false);
});

it('should return the page url with proper queryparams for container image flow', () => {
const { resource } = getTopologyData(MockResources, ['deployments']);
const url = new URL(
getAddPageUrl(resource, ImportOptions.CONTAINER, true),
'https://mock.test.com',
);
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);
});

it('should return the page url with proper queryparams for catalog flow', () => {
const { resource } = getTopologyData(MockResources, ['deployments']);
const url = new URL(
getAddPageUrl(resource, ImportOptions.CATALOG, true),
'https://mock.test.com',
);
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);
});

it('should return the page url with proper queryparams for dockerfile flow', () => {
const { resource } = getTopologyData(MockResources, ['deployments']);
const url = new URL(
getAddPageUrl(resource, ImportOptions.DOCKERFILE, true),
'https://mock.test.com',
);
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);
});

it('should return the page url with proper queryparams for database flow', () => {
const { resource } = getTopologyData(MockResources, ['deployments']);
const url = new URL(
getAddPageUrl(resource, ImportOptions.DATABASE, true),
'https://mock.test.com',
);
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);
});

it('it should return a valid kebabAction on invoking createKebabAction', () => {
const { resource } = getTopologyData(MockResources, ['deployments']);
const icon = <GitAltIcon />;
const hasApplication = true;
const label = 'From Git';

const kebabAction: KebabAction = createKebabAction(label, icon, ImportOptions.GIT);
const kebabOption: KebabOption = kebabAction(resource, 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'));
});
});
@@ -0,0 +1,72 @@
import * as _ from 'lodash';
import { K8sResourceKind, modelFor, referenceFor } from '@console/internal/module/k8s';
import { KebabOption, asAccessReview } from '@console/internal/components/utils';
import { ImportOptions } from '../components/import/import-types';

const PART_OF = 'app.kubernetes.io/part-of';

export const getAddPageUrl = (
obj: K8sResourceKind,
type: string,
hasApplication: boolean,
): string => {
let pageUrl = '';
const params = new URLSearchParams();
const appGroup = _.get(obj, ['metadata', 'labels', PART_OF], '');
const {
metadata: { namespace: ns },
} = obj;
switch (type) {
case ImportOptions.GIT:
pageUrl = `/import/ns/${ns}`;
params.append('importType', 'git');
break;
case ImportOptions.CONTAINER:
pageUrl = `/deploy-image/ns/${ns}`;
break;
case ImportOptions.CATALOG:
pageUrl = `/catalog/ns/${ns}`;
break;
case ImportOptions.DOCKERFILE:
pageUrl = `/import/ns/${ns}`;
params.append('importType', 'docker');
break;
case ImportOptions.DATABASE:
pageUrl = `/catalog/ns/${ns}`;
params.append('category', 'databases');
break;
default:
throw new Error('Invalid Import option provided');
}
params.append('isKnativeDisabled', 'true');
if (hasApplication && appGroup) {
params.append('application', encodeURIComponent(appGroup));
}
return `${pageUrl}?${params.toString()}`;
};

export const getMenuPath = (hasApplication: boolean): string =>
hasApplication ? 'Add to Application' : 'Add to Project';

type KebabFactory = (
label: string,
icon: React.ReactNode,
importType: ImportOptions,
checkAccess?: boolean,
) => KebabAction;

export type KebabAction = (obj?: K8sResourceKind, hasApplication?: boolean) => KebabOption;

export const createKebabAction: KebabFactory = (label, icon, importType, checkAccess = true) => (
obj: K8sResourceKind,
hasApplication: boolean,
) => {
const resourceModel = modelFor(referenceFor(obj));
return {
label,
icon,
path: getMenuPath(hasApplication),
href: getAddPageUrl(obj, importType, hasApplication),
accessReview: checkAccess && asAccessReview(resourceModel, obj, 'create'),
};
};

0 comments on commit fb0a5b2

Please sign in to comment.