Skip to content

Commit

Permalink
Bug 1797652: Fix crash on Edit of applications
Browse files Browse the repository at this point in the history
  • Loading branch information
jeff-phillips-18 committed Feb 7, 2020
1 parent 63830a9 commit e19f218
Show file tree
Hide file tree
Showing 12 changed files with 95 additions and 37 deletions.
46 changes: 46 additions & 0 deletions frontend/__tests__/components/utils/truncate-utils.spec.ts
@@ -0,0 +1,46 @@
import {
truncateMiddle,
shouldTruncate,
TruncateOptions,
} from '../../../public/components/utils/truncate-middle';

const testTruncateText = 'ThisTextShouldBeTruncatedByDefault';

describe('truncateMiddle: ', () => {
it('should truncate to 20 characters, in the middle, with ellipse by default', () => {
expect(shouldTruncate(testTruncateText)).toBe(true);
const truncateResult = truncateMiddle(testTruncateText);
const splits = truncateResult.split('\u2026');
expect(truncateResult.length).toBe(20);
expect(splits[0].length).toBeGreaterThan(1);
expect(splits[1].length).toBeGreaterThan(1);
});

it('should honor setting the length', () => {
const options: TruncateOptions = { length: 50 };
expect(shouldTruncate(testTruncateText, options)).toBe(false);
const truncateResult = truncateMiddle(testTruncateText, options);
const splits = truncateResult.split('\u2026');
expect(truncateResult.length).toBe(testTruncateText.length);
expect(splits.length).toBe(1);
});

it('should honor truncating at the end', () => {
const options: TruncateOptions = { truncateEnd: true };
expect(shouldTruncate(testTruncateText, options)).toBe(true);
const truncateResult = truncateMiddle(testTruncateText, options);
const splits = truncateResult.split('\u2026');
expect(truncateResult.length).toBe(20);
expect(splits[0].length).toBe(19);
expect(splits[1].length).toBe(0);
});

it('should honor the omission text', () => {
const options: TruncateOptions = { omission: 'zzz' };
expect(shouldTruncate(testTruncateText, options)).toBe(true);
const truncateResult = truncateMiddle(testTruncateText, options);
const splits = truncateResult.split('zzz');
expect(truncateResult.length).toBe(20);
expect(splits.length).toBe(2);
});
});
@@ -1,4 +1,5 @@
import { KebabOption } from '@console/internal/components/utils';
import { truncateMiddle } from '@console/internal/components/utils/truncate-middle';
import { K8sResourceKind, K8sKind } from '@console/internal/module/k8s';
import { editApplicationModal } from '../components/modals';

Expand All @@ -24,8 +25,8 @@ export const ModifyApplication = (kind: K8sKind, obj: K8sResourceKind): KebabOpt

export const EditApplication = (model: K8sKind, obj: K8sResourceKind): KebabOption => {
return {
label: 'Edit',
href: `/edit?name=${obj.metadata.name}&kind=${obj.kind}`,
label: `Edit ${truncateMiddle(obj.metadata.name)}`,
href: `/edit?name=${obj.metadata.name}&kind=${obj.kind || model.kind}`,
accessReview: {
group: model.apiGroup,
resource: model.plural,
Expand Down
14 changes: 4 additions & 10 deletions frontend/packages/dev-console/src/components/svg/SvgBoxedText.tsx
Expand Up @@ -6,6 +6,7 @@ import {
useCombineRefs,
createSvgIdUrl,
} from '@console/topology';
import { truncateMiddle } from '@console/internal/components/utils';
import SvgResourceIcon from '../topology/components/nodes/ResourceIcon';
import SvgCircledIcon from './SvgCircledIcon';
import SvgDropShadowFilter from './SvgDropShadowFilter';
Expand All @@ -30,13 +31,6 @@ export interface SvgBoxedTextProps {

const FILTER_ID = 'SvgBoxedTextDropShadowFilterId';

const truncateEnd = (text: string = '', length: number): string => {
if (text.length <= length) {
return text;
}
return `${text.substr(0, length - 1)}…`;
};

/**
* Renders a `<text>` component with a `<rect>` box behind.
*/
Expand All @@ -53,7 +47,7 @@ const SvgBoxedText: React.FC<SvgBoxedTextProps> = ({
typeIconPadding = 4,
onMouseEnter,
onMouseLeave,
truncate,
truncate = 20,
dragRef,
...other
}) => {
Expand Down Expand Up @@ -107,10 +101,10 @@ const SvgBoxedText: React.FC<SvgBoxedTextProps> = ({
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
{typeof truncate === 'number'
{truncate > 0
? labelHover
? children
: truncateEnd(children, truncate)
: truncateMiddle(children, { length: truncate })
: children}
</text>
</g>
Expand Down
Expand Up @@ -79,7 +79,6 @@ const HelmReleaseGroup: React.FC<HelmReleaseGroupProps> = ({ element, onSelect,
paddingX={8}
paddingY={4}
kind="HelmRelease"
truncate={16}
dragRef={dragLabelRef}
typeIconClass="icon-helm"
>
Expand Down
Expand Up @@ -161,7 +161,6 @@ const ApplicationGroup: React.FC<ApplicationGroupProps> = ({
y={labelLocation.current[1] + hullPadding(labelLocation.current) + 30}
paddingX={8}
paddingY={5}
truncate={16}
dragRef={dragLabelRef}
>
{element.getLabel()}
Expand Down
Expand Up @@ -135,7 +135,6 @@ const BaseNode: React.FC<BaseNodeProps> = ({
paddingX={8}
paddingY={4}
kind={kind}
truncate={16}
>
{element.getLabel()}
</SvgBoxedText>
Expand Down
Expand Up @@ -82,7 +82,6 @@ const EventSource: React.FC<EventSourceProps> = ({
paddingX={8}
paddingY={4}
kind={data.kind}
truncate={16}
>
{element.getLabel()}
</SvgBoxedText>
Expand Down
@@ -1,32 +1,32 @@
import * as React from 'react';
import { Tooltip, TooltipPosition } from '@patternfly/react-core';
import {
truncateMiddle,
shouldTruncate,
TruncateOptions,
} from '@console/internal/components/utils';
import { useSize, useHover } from '@console/topology';
import SvgResourceIcon from './ResourceIcon';
import SvgCircledIcon from '../../../svg/SvgCircledIcon';

import './GroupNode.scss';

const MAX_TITLE_LENGTH = 35;
const TOP_MARGIN = 20;
const LEFT_MARGIN = 20;
const TEXT_MARGIN = 10;
const CONTENT_MARGIN = 40;

const truncateOptions: TruncateOptions = {
length: 35,
};

type GroupNodeProps = {
title: string;
kind?: string;
children?: React.ReactNode;
typeIconClass?: string;
};

const shouldTruncateText = (text: string) => text.length > MAX_TITLE_LENGTH + 5;
const truncateText = (text: string = ''): string => {
if (!shouldTruncateText(text)) {
return text;
}
return `${text.substr(0, MAX_TITLE_LENGTH - 1)}…`;
};

const GroupNode: React.FC<GroupNodeProps> = ({ children, kind, title, typeIconClass }) => {
const [textHover, textHoverRef] = useHover();
const [iconSize, iconRef] = useSize([kind]);
Expand All @@ -50,7 +50,7 @@ const GroupNode: React.FC<GroupNodeProps> = ({ children, kind, title, typeIconCl
content={title}
position={TooltipPosition.top}
trigger="manual"
isVisible={textHover && shouldTruncateText(title)}
isVisible={textHover && shouldTruncate(title, truncateOptions)}
>
<text
ref={textHoverRef}
Expand All @@ -60,7 +60,7 @@ const GroupNode: React.FC<GroupNodeProps> = ({ children, kind, title, typeIconCl
textAnchor="start"
dy="-0.25em"
>
{truncateText(title)}
{truncateMiddle(title, truncateOptions)}
</text>
</Tooltip>
)}
Expand Down
Expand Up @@ -153,7 +153,6 @@ const KnativeServiceGroup: React.FC<KnativeServiceGroupProps> = ({
paddingX={8}
paddingY={4}
kind={data.kind}
truncate={16}
dragRef={dragLabelRef}
typeIconClass="icon-knative"
>
Expand Down
Expand Up @@ -90,7 +90,6 @@ const OperatorBackedServiceGroup: React.FC<OperatorBackedServiceGroupProps> = ({
paddingX={8}
paddingY={4}
kind="Operator"
truncate={16}
dragRef={dragLabelRef}
typeIconClass={element.getData().data.builderImage}
>
Expand Down
2 changes: 0 additions & 2 deletions frontend/packages/dev-console/src/utils/kebab-actions.ts
Expand Up @@ -5,7 +5,6 @@ import {
DaemonSetModel,
DeploymentConfigModel,
DeploymentModel,
ServiceModel,
StatefulSetModel,
} from '@console/internal/models';
import { ModifyApplication, EditApplication } from '../actions/modify-application';
Expand All @@ -15,7 +14,6 @@ const modifyApplicationRefs = [
referenceFor(DeploymentModel),
referenceFor(DaemonSetModel),
referenceFor(StatefulSetModel),
referenceFor(ServiceModel),
];

export const getKebabActionsForKind = (resourceKind: K8sKind): KebabAction[] => {
Expand Down
37 changes: 31 additions & 6 deletions frontend/public/components/utils/truncate-middle.ts
@@ -1,24 +1,49 @@
const DEFAULT_OPTIONS = {
export type TruncateOptions = {
length?: number; // Length to truncate text to
truncateEnd?: boolean; // Flag to alternatively truncate at the end
omission?: string; // Truncation text used to denote the truncation (ellipsis)
minTruncateChars?: number; // Minimum number of characters to truncate
};

const DEFAULT_OPTIONS: TruncateOptions = {
length: 20,
truncateEnd: false,
omission: '\u2026', // ellipsis character
minTruncateChars: 3,
};

// Truncates a string down to `maxLength` characters by replacing the middle
// the provided omission option (ellipsis character by default);
export const truncateMiddle = (text, options = {}): string => {
const { length, omission } = Object.assign({}, DEFAULT_OPTIONS, options);
// Truncates a string down to `maxLength` characters when the length
// is greater than the `maxLength` + `minTruncateChars` values.
// By default the middle is truncated, set the options.truncateEnd to true to truncate at the end.
// Truncated text is replaced with the provided omission option (ellipsis character by default);
export const truncateMiddle = (text: string, options: TruncateOptions = {}): string => {
const { length, truncateEnd, omission, minTruncateChars } = { ...DEFAULT_OPTIONS, ...options };
if (!text) {
return text;
}

if (text.length <= length) {
// Do not truncate less than the minimum truncate characters
if (text.length <= length + minTruncateChars) {
return text;
}

if (length <= omission.length) {
return omission;
}

if (truncateEnd) {
return `${text.substr(0, length - 1)}${omission}`;
}

const startLength = Math.ceil((length - omission.length) / 2);
const endLength = length - startLength - omission.length;
const startFragment = text.substr(0, startLength);
const endFragment = text.substr(text.length - endLength);
return `${startFragment}${omission}${endFragment}`;
};

export const shouldTruncate = (text, options: TruncateOptions = {}): boolean => {
const { length, minTruncateChars } = { ...DEFAULT_OPTIONS, ...options };

return text.length > length + minTruncateChars;
};

0 comments on commit e19f218

Please sign in to comment.