Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show tooltips for each step on the canvas #629

Merged
merged 12 commits into from
Jan 25, 2024
18 changes: 11 additions & 7 deletions packages/ui/src/components/Visualization/Custom/CustomNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
withSelection,
WithSelectionProps,
} from '@patternfly/react-topology';
import { Tooltip } from '@patternfly/react-core';
import { FunctionComponent } from 'react';
import { AddStepMode } from '../../../models/visualization/base-visual-entity';
import { CanvasDefaults } from '../Canvas/canvas.defaults';
Expand All @@ -24,6 +25,7 @@ const noopFn = () => {};
const CustomNode: FunctionComponent<CustomNodeProps> = ({ element, ...rest }) => {
const vizNode = element.getData()?.vizNode;
const label = vizNode?.getNodeLabel();
const tooltipContent = vizNode?.getTooltipContent();
const statusDecoratorTooltip = vizNode?.getNodeValidationText();
const nodeStatus = !statusDecoratorTooltip ? NodeStatus.default : NodeStatus.warning;

Expand All @@ -45,13 +47,15 @@ const CustomNode: FunctionComponent<CustomNodeProps> = ({ element, ...rest }) =>
width={CanvasDefaults.DEFAULT_NODE_DIAMETER}
height={CanvasDefaults.DEFAULT_NODE_DIAMETER}
>
<div className="custom-node__image">
<img
src={vizNode?.data.icon}
width={CanvasDefaults.DEFAULT_NODE_DIAMETER * 0.7}
height={CanvasDefaults.DEFAULT_NODE_DIAMETER * 0.7}
/>
</div>
<Tooltip content={tooltipContent}>
<div className="custom-node__image">
<img
src={vizNode?.data.icon}
width={CanvasDefaults.DEFAULT_NODE_DIAMETER * 0.7}
height={CanvasDefaults.DEFAULT_NODE_DIAMETER * 0.7}
/>
</div>
</Tooltip>
</foreignObject>
</g>
</DefaultNode>
Expand Down
6 changes: 6 additions & 0 deletions packages/ui/src/models/visualization/base-visual-entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export interface BaseVisualCamelEntity extends BaseCamelEntity {
/** Given a path, get the component label */
getNodeLabel: (path?: string) => string;

/** Given a path, get the component tooltip content */
getTooltipContent: (path?: string) => string;

/** Given a path, get the component type and definition */
getComponentSchema: (path?: string) => VisualComponentSchema | undefined;

Expand Down Expand Up @@ -71,6 +74,9 @@ export interface IVisualizationNode<T extends IVisualizationNodeData = IVisualiz
/** This method returns the label to be used by the canvas nodes */
getNodeLabel(): string;

/** This method returns the tooltip content to be used by the canvas nodes */
getTooltipContent(): string;

addBaseEntityStep(definedComponent: DefinedComponent, mode: AddStepMode, targetProperty?: string): void;

getNodeInteraction(): NodeInteraction;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,17 @@ export abstract class AbstractCamelVisualEntity implements BaseVisualCamelEntity
return label;
}

getTooltipContent(path?: string): string {
if (!path) return '';
const componentModel = get(this.route, path);

const content = CamelComponentSchemaService.getTooltipContent(
CamelComponentSchemaService.getCamelComponentLookup(path, componentModel),
);

return content;
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we please create an issue for adding a test case for this? There are similar test cases that you could get inspiration from.

getComponentSchema(path?: string): VisualComponentSchema | undefined {
if (!path) return undefined;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ export class PipeVisualEntity implements BaseVisualCamelEntity {
return KameletSchemaService.getNodeLabel(stepModel, path);
}

getTooltipContent(path?: string): string {
if (!path) return '';

const stepModel = get(this.spec, path) as PipeStep;
return KameletSchemaService.getTooltipContent(stepModel, path);
}

shivamG640 marked this conversation as resolved.
Show resolved Hide resolved
getComponentSchema(path?: string): VisualComponentSchema | undefined {
if (!path) return undefined;
const stepModel = get(this.spec, path) as PipeStep;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,60 @@ describe('CamelComponentSchemaService', () => {
);
});

describe('getTooltipContent', () => {
it('should return the component schema description', () => {
const camelElementLookup = { processorName: 'from' as keyof ProcessorDefinition, componentName: 'timer' };
const actualContent = CamelComponentSchemaService.getTooltipContent(camelElementLookup);

expect(actualContent).toEqual('Generate messages in specified intervals using java.util.Timer.');
});

it('should return the component name', () => {
const camelElementLookup = { processorName: 'from' as keyof ProcessorDefinition, componentName: 'xyz' };
const actualContent = CamelComponentSchemaService.getTooltipContent(camelElementLookup);

expect(actualContent).toEqual('xyz');
});

it('should return the kamelet schema description', () => {
const camelElementLookup = {
processorName: 'to' as keyof ProcessorDefinition,
componentName: 'kamelet:avro-deserialize-action',
};
const actualContent = CamelComponentSchemaService.getTooltipContent(camelElementLookup);

expect(actualContent).toEqual('Deserialize payload to Avro');
});

it('should return the kamelet name', () => {
const camelElementLookup = {
processorName: 'to' as keyof ProcessorDefinition,
componentName: 'kamelet:xyz',
};
const actualContent = CamelComponentSchemaService.getTooltipContent(camelElementLookup);

expect(actualContent).toEqual('kamelet:xyz');
});

it('should return the processor schema description', () => {
const path = 'from.steps.0.aggregate';
const definition = { id: 'aggregate-2202' };
const camelElementLookup = CamelComponentSchemaService.getCamelComponentLookup(path, definition);
const actualContent = CamelComponentSchemaService.getTooltipContent(camelElementLookup);

expect(actualContent).toEqual('Aggregates many messages into a single message');
});

it('should return the processor name', () => {
const path = 'from.steps.0.xyz';
const definition = { id: 'xyz-2202' };
const camelElementLookup = CamelComponentSchemaService.getCamelComponentLookup(path, definition);
const actualContent = CamelComponentSchemaService.getTooltipContent(camelElementLookup);

expect(actualContent).toEqual('xyz');
});
});

describe('canHavePreviousStep', () => {
it.each([
['from', false],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { CatalogKind } from '../../../catalog-kind';
import { VisualComponentSchema } from '../../base-visual-entity';
import { CamelCatalogService } from '../camel-catalog.service';
import { CamelProcessorStepsProperties, ICamelElementLookupResult } from './camel-component-types';
import { IKameletDefinition } from '../../../kamelets-catalog';
import { ICamelComponentDefinition } from '../../../camel-components-catalog';

export class CamelComponentSchemaService {
static DISABLED_SIBLING_STEPS = ['from', 'when', 'otherwise', 'doCatch', 'doFinally'];
Expand Down Expand Up @@ -70,6 +72,32 @@ export class CamelComponentSchemaService {
}
}

static getTooltipContent(camelElementLookup: ICamelElementLookupResult): string {
if (camelElementLookup.componentName !== undefined) {
const catalogLookup = CamelCatalogService.getCatalogLookup(camelElementLookup.componentName);
if (catalogLookup.catalogKind === CatalogKind.Component) {
return (
(catalogLookup.definition as unknown as ICamelComponentDefinition)?.component.description ??
camelElementLookup.componentName
);
}

if (catalogLookup.catalogKind === CatalogKind.Kamelet) {
return (
(catalogLookup.definition as unknown as IKameletDefinition)?.spec.definition.description ??
camelElementLookup.componentName
);
}
}

const schema = this.getSchema(camelElementLookup);
if (schema.description !== undefined) {
return schema.description;
}

return camelElementLookup.processorName;
}

static canHavePreviousStep(processorName: keyof ProcessorDefinition): boolean {
return !this.DISABLED_SIBLING_STEPS.includes(processorName);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,38 @@ describe('KameletSchemaService', () => {
expect(result).toEqual('sink: Unknown');
});
});

describe('getTooltipContent', () => {
it('should return the Kamelet description as the tooltip content', () => {
const step = {
ref: {
kind: 'Kamelet',
apiVersion: 'camel.apache.org/v1',
name: 'beer-source',
},
};
const result = KameletSchemaService.getTooltipContent(step, 'source');

expect(result).toEqual('Produces periodic events about beers!');
});

it('should return the Kamelet name as the tooltip content', () => {
const step = {
ref: {
kind: 'Kamelet',
apiVersion: 'camel.apache.org/v1',
name: 'xyz-source',
},
};
const result = KameletSchemaService.getTooltipContent(step, 'source');

expect(result).toEqual('xyz-source');
});

it('should return the Kamelet path as the tooltip content', () => {
const result = KameletSchemaService.getTooltipContent(undefined, 'sink');

expect(result).toEqual('sink: Unknown');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,16 @@ export class KameletSchemaService {
return CamelCatalogService.getComponent(CatalogKind.Kamelet, stepName);
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
shivamG640 marked this conversation as resolved.
Show resolved Hide resolved
static getNodeLabel(step: PipeStep, path: string): string {
return step?.ref?.name ?? `${path}: Unknown`;
}

static getTooltipContent(step: PipeStep, path: string): string {
const schema = this.getKameletDefinition(step)?.propertiesSchema;
if (schema?.description !== undefined) {
return schema.description;
}

return step?.ref?.name ?? `${path}: Unknown`;
}
}
22 changes: 22 additions & 0 deletions packages/ui/src/models/visualization/visualization-node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,28 @@ describe('VisualizationNode', () => {
});
});

describe('getTooltipContent', () => {
it('should return the tootltip content from the underlying BaseVisualCamelEntity', () => {
const getTooltipContentSpy = jest.fn().mockReturnValue('test-description');
const visualEntity = {
getTooltipContent: getTooltipContentSpy,
} as unknown as BaseVisualCamelEntity;

node = createVisualizationNode('test', { path: 'test-path', entity: visualEntity });
const content = node.getTooltipContent();

expect(getTooltipContentSpy).toHaveBeenCalledWith(node.data.path);
expect(content).toEqual('test-description');
});

it('should return the id when the underlying BaseVisualCamelEntity is not defined', () => {
node = createVisualizationNode('test', {});
const content = node.getTooltipContent();

expect(content).toEqual(node.id);
});
});

it('should return the component schema from the root node', () => {
/** Arrange */
const getComponentSchemaSpy = jest.fn();
Expand Down
4 changes: 4 additions & 0 deletions packages/ui/src/models/visualization/visualization-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ class VisualizationNode<T extends IVisualizationNodeData = IVisualizationNodeDat
return this.getRootNode().getBaseEntity()?.getNodeLabel(this.data.path) ?? this.id;
}

getTooltipContent(): string {
return this.getRootNode().getBaseEntity()?.getTooltipContent(this.data.path) ?? this.id;
}

addBaseEntityStep(definition: DefinedComponent, mode: AddStepMode): void {
this.getRootNode().getBaseEntity()?.addStep({ definedComponent: definition, mode, data: this.data });
}
Expand Down
Loading