Skip to content

Commit

Permalink
fix(ComponentRelation): fix visualization on details page
Browse files Browse the repository at this point in the history
  • Loading branch information
sahil143 committed Mar 6, 2024
1 parent 881d7fb commit dd171f7
Show file tree
Hide file tree
Showing 16 changed files with 307 additions and 163 deletions.
10 changes: 9 additions & 1 deletion src/components/ComponentRelation/cr-modals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { PlusCircleIcon } from '@patternfly/react-icons/dist/js/icons/plus-circl
import { FieldArray, useFormikContext } from 'formik';
import { isEmpty } from 'lodash-es';
import { FormFooter } from '../../shared';
import ExternalLink from '../../shared/components/links/ExternalLink';
import { RawComponentProps } from '../modal/createModalLauncher';
import { ComponentRelation } from './ComponentRelationForm';
import { ComponentRelationFormikValue, ComponentRelationNudgeType } from './type';
Expand All @@ -49,7 +50,14 @@ export const DefineComponentRelationModal: React.FC<DefineComponentRelationModal
{...modalProps}
onClose={onCancel}
title="Component relationships"
description="Nudging references another component by digest."
description={
<>
Nudging references another component by digest.{' '}
<ExternalLink href="https://redhat-appstudio.github.io/docs.appstudio.io/Documentation/main/how-to-guides/configuring-builds/proc_defining_component_relationships/">
Learn more about nudging.
</ExternalLink>
</>
}
variant={ModalVariant.medium}
footer={
<FormFooter
Expand Down
71 changes: 71 additions & 0 deletions src/components/ComponentRelation/details-page/ComponentNudges.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
.component-nudge {
--spacing: 3.5rem;
--diff: 1rem;
--guide-color: var(--pf-global--Color--400);
--stroke-width: 2px;

margin-left: var(--pf-v5-global--spacer--lg);

&__status-icon {
height: 10px;
max-width: 25px;
}

&__tree {
& ul {
margin-left: calc(var(--spacing) - var(--diff));
padding-left: 0;
padding-top: 6px;
}

& li {
display: block;
position: relative;
padding-left: calc(1.5 * var(--spacing) - var(--diff) - 2px);
padding-top: var(--diff);
}

& ul li {
border-left: var(--stroke-width) solid var(--guide-color);
}

& ul li:last-child {
border-left: var(--stroke-width) solid transparent;
}

& ul li::before {
content: '';
display: block;
position: absolute;
top: -1px;
left: -2px;
width: calc(var(--spacing));
height: calc(var(--spacing) / 3.5 + var(--diff));
border: solid var(--guide-color);
border-width: 0 0 var(--stroke-width) var(--stroke-width);
}
}

&__nudges-arrow {
& ul li::after {
content: "\25B6";
color: var(--guide-color);
display: inline-block;
position: absolute;
left: calc(var(--spacing) - 6px);
top: 19px;
}
}

&__nudged-by-arrow {
& ul li:first-child::after {
content: "\25B6";
color: var(--guide-color);
display: inline-block;
position: absolute;
left: -7.5px;
top: -15px;
transform: rotate(-90deg);
}
}
}
105 changes: 105 additions & 0 deletions src/components/ComponentRelation/details-page/ComponentNudges.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import * as React from 'react';
import { Link } from 'react-router-dom';
import { Button, Text } from '@patternfly/react-core';
import { ExternalLinkAltIcon } from '@patternfly/react-icons/dist/esm/icons/external-link-alt-icon';
import { css } from '@patternfly/react-styles';
import { useAllComponents } from '../../../hooks/useComponents';
import { ComponentKind } from '../../../types';
import { useWorkspaceInfo } from '../../../utils/workspace-context-utils';
import { ComponentRelationStatusIcon } from './ComponentRelationStatusIcon';

import './ComponentNudges.scss';

export const enum NudgeRadios {
NUDGES = 'nudges',
NUDGED_BY = 'isNudgedBy',
}

interface ComponentNudgesSVGprops {
nudgeComponents: string[];
radioChecked: string;
component: ComponentKind;
}

const ComponentNudgesSVG: React.FC<ComponentNudgesSVGprops> = ({
nudgeComponents,
radioChecked,
component,
}) => {
const { workspace, namespace } = useWorkspaceInfo();
const [components, loaded, error] = useAllComponents(namespace);

const emptyState = (
<div data-test="nudges-empty-state" className="component-nudges__empty-state">
No dependencies found
</div>
);

const relatedComponents = React.useMemo(() => {
return loaded && !error
? components.filter((comp) => nudgeComponents?.includes(comp.metadata.name))
: [];

Check warning on line 41 in src/components/ComponentRelation/details-page/ComponentNudges.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/ComponentRelation/details-page/ComponentNudges.tsx#L41

Added line #L41 was not covered by tests
}, [components, error, loaded, nudgeComponents]);

const isNudges = React.useMemo(() => radioChecked === NudgeRadios.NUDGES, [radioChecked]);

if (!nudgeComponents || nudgeComponents?.length === 0) {
return emptyState;
}

return Array.isArray(nudgeComponents) && nudgeComponents.length > 0 ? (
<div
className={css(
'component-nudge',
'component-nudge__tree',
radioChecked === NudgeRadios.NUDGES && 'component-nudge__nudges-arrow',
radioChecked === NudgeRadios.NUDGED_BY && 'component-nudge__nudged-by-arrow',
)}
>
<Text style={{ marginTop: 'var(--pf-v5-global--spacer--lg)' }}>
<b>{component.metadata.name}</b>
</Text>
<ul>
{relatedComponents.map((comp, i) => (
<li key={i} data-test="nudges-connector">
<Link
to={`/application-pipeline/workspaces/${workspace}/applications/${comp.spec?.application}/components/${comp.metadata.name}`}
data-test={isNudges ? 'nudges-cmp-link' : 'nudged-by-cmp-link'}
>
{comp.metadata.name}
</Link>
<ComponentRelationStatusIcon
component={comp}
className="component-nudge__status-icon"
/>
{comp.spec?.application !== component.spec?.application ? (
<>
(
<Button
isInline
variant="link"
icon={<ExternalLinkAltIcon />}
iconPosition="end"
component={(props) => (
<Link
{...props}
to={`/application-pipeline/workspaces/${workspace}/applications/${comp?.spec?.application}/`}
target="_blank"
/>
)}
>
{comp.spec?.application}
</Button>
)
</>
) : null}

Check warning on line 95 in src/components/ComponentRelation/details-page/ComponentNudges.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/ComponentRelation/details-page/ComponentNudges.tsx#L95

Added line #L95 was not covered by tests
</li>
))}
</ul>
</div>
) : (
emptyState

Check warning on line 101 in src/components/ComponentRelation/details-page/ComponentNudges.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/ComponentRelation/details-page/ComponentNudges.tsx#L101

Added line #L101 was not covered by tests
);
};

export default ComponentNudgesSVG;
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.component-nudges {
&__radio {
font-weight: 400;
}
&__empty-state {
padding: var(--pf-v5-global--spacer--md);
color: var(--pf-global--Color--400);
}
& .pf-v5-c-expandable-section__toggle-text {
margin-left: var(--pf-v5-global--spacer--sm);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as React from 'react';
import { ExpandableSection, Radio, Tooltip } from '@patternfly/react-core';
import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons/dist/js/icons/outlined-question-circle-icon';
import { ComponentKind, NudgeStats } from '../../../../types';
import ComponentNudgesSVG, { NudgeRadios } from './ComponentNudgesSVG';
import { ComponentKind, NudgeStats } from '../../../types';
import ComponentNudgesSVG, { NudgeRadios } from './ComponentNudges';

import './ComponentNudgesDependencies.scss';

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.component-relation-status-icon {
margin: var(--pf-v5-global--spacer--xs);
vertical-align: middle;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as React from 'react';
import { Tooltip } from '@patternfly/react-core';
import { css } from '@patternfly/react-styles';
import relationIcon from '../../../imgs/RelationsIcon.svg';
import { ComponentKind } from '../../../types';

import './ComponentRelationStatusIcon.scss';

export const ComponentRelationStatusIcon: React.FC<{
component: ComponentKind;
className?: string;
style?: { [key: string]: any };
}> = ({ component, className, style }) => {
const isInRelationship = !!(
component.spec?.['build-nudges-ref']?.length ?? component.status?.['build-nudged-by']?.length
);
return isInRelationship ? (
<Tooltip content="This component is in a relationship">
<img
style={style}
className={css('component-relation-status-icon', className)}
src={relationIcon}
alt="Component is in a relationship icon"
/>
</Tooltip>
) : null;
};
Original file line number Diff line number Diff line change
@@ -1,28 +1,43 @@
import * as React from 'react';
import '@testing-library/jest-dom';
import { screen, configure, fireEvent, waitFor } from '@testing-library/react';
import { NudgeStats } from '../../../../../types';
import { routerRenderer } from '../../../../../utils/test-utils';
import { mockComponent } from '../../__data__/mockComponentDetails';
import { ComponentKind, NudgeStats } from '../../../../types';
import { routerRenderer } from '../../../../utils/test-utils';
import ComponentNudgesDependencies from '../ComponentNudgesDependencies';

jest.mock('../../../../../utils/workspace-context-utils', () => ({
jest.mock('../../../../utils/workspace-context-utils', () => ({
useWorkspaceInfo: jest.fn(() => ({ namespace: 'test-ns', workspace: 'test-ws' })),
}));
const mockComponent = {
metadata: { name: 'component' },
spec: { application: 'application' },
} as ComponentKind;

const mockAllComponents = [
{ metadata: { name: 'cmp1' }, spec: { application: 'app1' } },
{ metadata: { name: 'cmp2' }, spec: { application: 'app2' } },
{ metadata: { name: 'cmp3' }, spec: { application: 'app3' } },
{ metadata: { name: 'cmp4' }, spec: { application: 'app4' } },
{ metadata: { name: 'cmp5' }, spec: { application: 'app5' } },
] as ComponentKind[];

jest.mock('../../../../hooks/useComponents', () => ({
useAllComponents: jest.fn(() => [mockAllComponents, true, undefined]),
}));

configure({ testIdAttribute: 'data-test' });

describe('ComponentNudgesDependencies', () => {
it('should render empty state when no dependencies', async () => {
routerRenderer(<ComponentNudgesDependencies component={mockComponent[0]} />);
routerRenderer(<ComponentNudgesDependencies component={mockComponent} />);
screen.queryByTestId('nudges-empty-state');
});

it('should render 3 nudges dependencies', async () => {
const cmp = {
...mockComponent[0],
...mockComponent,
spec: { [NudgeStats.NUDGES]: ['cmp1', 'cmp2', 'cmp3'] },
};
} as ComponentKind;
routerRenderer(<ComponentNudgesDependencies component={cmp} />);
fireEvent.click(screen.getByTestId('nudges-radio'));
await waitFor(() => {
Expand All @@ -34,9 +49,9 @@ describe('ComponentNudgesDependencies', () => {

it('should render 3 connectors', async () => {
const cmp = {
...mockComponent[0],
...mockComponent,
spec: { [NudgeStats.NUDGES]: ['cmp1', 'cmp2', 'cmp3'] },
};
} as ComponentKind;
routerRenderer(<ComponentNudgesDependencies component={cmp} />);

fireEvent.click(screen.getByTestId('nudges-radio'));
Expand All @@ -47,28 +62,28 @@ describe('ComponentNudgesDependencies', () => {

it('should render correct links', async () => {
const cmp = {
...mockComponent[0],
...mockComponent,
metadata: { name: 'test' },
spec: { application: 'testApp', [NudgeStats.NUDGES]: ['cmp1', 'cmp2', 'cmp3'] },
};
} as ComponentKind;
routerRenderer(<ComponentNudgesDependencies component={cmp} />);

fireEvent.click(screen.getByTestId('nudges-radio'));

const links = screen.queryAllByTestId('nudges-cmp-link');
expect(links.length).toBe(3);
expect(links[0].getAttribute('href')).toBe(
'/application-pipeline/workspaces/test-ws/application/testApp/components/cmp1',
'/application-pipeline/workspaces/test-ws/applications/app1/components/cmp1',
);
});
});

describe('ComponentNudgesDependencies nudged by', () => {
it('should render 4 nudged-by dependencies', async () => {
const cmp = {
...mockComponent[0],
...mockComponent,
status: { [NudgeStats.NUDGED_BY]: ['cmp1', 'cmp2', 'cmp3', 'cmp4'] },
};
} as ComponentKind;
routerRenderer(<ComponentNudgesDependencies component={cmp} />);

fireEvent.click(screen.getByTestId('nudged-by-radio'));
Expand All @@ -83,33 +98,33 @@ describe('ComponentNudgesDependencies nudged by', () => {

it('should render 4 connectors', async () => {
const cmp = {
...mockComponent[0],
...mockComponent,
status: { [NudgeStats.NUDGED_BY]: ['cmp1', 'cmp2', 'cmp3', 'cmp4'] },
spec: { application: 'testApp' },
};
} as ComponentKind;
routerRenderer(<ComponentNudgesDependencies component={cmp} />);

fireEvent.click(screen.getByTestId('nudged-by-radio'));

const connectors = screen.queryAllByTestId('nudged-by-connector');
const connectors = screen.queryAllByTestId('nudges-connector');
expect(connectors.length).toBe(4);
});

it('should render correct links', async () => {
const cmp = {
...mockComponent[0],
...mockComponent,
metadata: { name: 'test' },
status: { [NudgeStats.NUDGED_BY]: ['cmp1', 'cmp2'] },
spec: { application: 'testApp' },
};
} as ComponentKind;
routerRenderer(<ComponentNudgesDependencies component={cmp} />);

fireEvent.click(screen.getByTestId('nudged-by-radio'));

const links = screen.queryAllByTestId('nudged-by-cmp-link');
expect(links.length).toBe(2);
expect(links[1].getAttribute('href')).toBe(
'/application-pipeline/workspaces/test-ws/application/testApp/components/cmp2',
'/application-pipeline/workspaces/test-ws/applications/app2/components/cmp2',
);
});
});
Loading

0 comments on commit dd171f7

Please sign in to comment.