Skip to content

Commit

Permalink
Update empty state for TopologyView
Browse files Browse the repository at this point in the history
  • Loading branch information
jeff-phillips-18 committed Mar 16, 2021
1 parent 327b13c commit 4d3b230
Show file tree
Hide file tree
Showing 21 changed files with 189 additions and 127 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,4 @@ Scenario: Create the namespace
And user enters project name as "aut-project" in Create Project modal
And user clicks Create button present in Create Project modal
Then modal will get closed
And topology page displays with message "No resources found"
And topology page have cards from Add page
And topology page displays with the empty state
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const topologyPage = {
},
verifyContextMenu: () => cy.get(topologyPO.graph.contextMenu).should('be.visible'),
verifyNoWorkLoadsText: (text: string) =>
cy.get('h2.co-hint-block__title').should('contain.text', text),
cy.get('h3.pf-c-title.pf-m-lg').should('contain.text', text),
verifyWorkLoads: () => cy.get('g[data-surface="true"]').should('be.visible'),
search: (name: string) => {
topologyHelper.search(name);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { When, Then } from 'cypress-cucumber-preprocessor/steps';
import { projectNameSpace } from '../../pages/app';
import { cardTitle } from '../../pageObjects/add-flow-po';
import { modal } from '../../../../../integration-tests-cypress/views/modal';
import { topologyPage } from '../../pages/topology/topology-page';

When('user enters project name as {string} in Create Project modal', (projectName: string) => {
const d = new Date();
Expand All @@ -18,14 +18,8 @@ Then('modal will get closed', () => {
modal.shouldBeClosed();
});

Then('topology page displays with message {string}', (message: string) => {
projectNameSpace.verifyMessage(message);
// Bug: 1890678 is created related to Accesibiity violation - Until bug fix, below line is commented to execute the scripts in CI
// cy.testA11y('Topology Page with cards');
});

Then('topology page have cards from Add page', () => {
cy.get(cardTitle).should('be.visible');
Then('topology page displays with the empty state', () => {
topologyPage.verifyNoWorkLoadsText('No resources found');
});

When('user selects the Create Project option from Projects dropdown on top navigation bar', () => {
Expand Down
4 changes: 2 additions & 2 deletions frontend/packages/topology/locales/en/topology.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@
"Save": "Save",
"Connect <1>{{sourceLabel}}</1> to": "Connect <1>{{sourceLabel}}</1> to",
"Unable to move connector of type {{type}}.": "Unable to move connector of type {{type}}.",
"No resources found": "No resources found",
"To add content to your Project, create an Application, component or service using one of these options.": "To add content to your Project, create an Application, component or service using one of these options.",
"Topology": "Topology",
"No resources found": "No resources found",
"<0>Start building your application</0> or visit the <3>Add page</3> for more details.": "<0>Start building your application</0> or visit the <3>Add page</3> for more details.",
"Select a Project to view the topology or <2>create a Project</2>.": "Select a Project to view the topology or <2>create a Project</2>.",
"List view": "List view",
"Graph view": "Graph view",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ describe('DataModelProvider', () => {
spyUseURLPoll.mockReturnValue([{}, null, false]);
wrapper = mount(
<DataModelProvider namespace="test-project">
<TopologyDataRenderer viewType={TopologyViewType.graph} title="Topology" />
<TopologyDataRenderer viewType={TopologyViewType.graph} />
</DataModelProvider>,
{
wrappingComponent: ({ children }) => <Provider store={store}>{children}</Provider>,
Expand Down
29 changes: 7 additions & 22 deletions frontend/packages/topology/src/__tests__/TopologyPage.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,48 +58,33 @@ describe('Topology page tests', () => {
});

it('should render topology page', () => {
const wrapper = shallow(<TopologyPage match={match} title="Topology" hideProjects={false} />);
const wrapper = shallow(<TopologyPage match={match} hideProjects={false} />);
expect(wrapper.find(NamespacedPage).exists()).toBe(true);
});

it('should default to graph view', () => {
(useUserSettingsCompatibility as jest.Mock).mockReturnValue(['', () => {}, true]);
const wrapper = shallow(<TopologyPage match={match} title="Topology" hideProjects={false} />);
const wrapper = shallow(<TopologyPage match={match} hideProjects={false} />);
expect(wrapper.find('[data-test-id="topology-list-page"]').exists()).toBe(false);
});

it('should allow setting default to list view', () => {
const wrapper = shallow(
<TopologyPage
match={match}
title="Topology"
hideProjects={false}
defaultViewType={TopologyViewType.list}
/>,
<TopologyPage match={match} hideProjects={false} defaultViewType={TopologyViewType.list} />,
);
expect(wrapper.find('[data-test-id="topology-list-page"]').exists()).toBe(true);
});

it('should use useUserSettingsCompatibility setting', () => {
(useUserSettingsCompatibility as jest.Mock).mockReturnValue(['graph', () => {}, true]);
let wrapper = shallow(
<TopologyPage
match={match}
title="Topology"
hideProjects={false}
activeViewStorageKey="fake-key"
/>,
<TopologyPage match={match} hideProjects={false} activeViewStorageKey="fake-key" />,
);
expect(wrapper.find('[data-test-id="topology-list-page"]').exists()).toBe(false);

(useUserSettingsCompatibility as jest.Mock).mockReturnValue(['list', () => {}, true]);
wrapper = shallow(
<TopologyPage
match={match}
title="Topology"
hideProjects={false}
activeViewStorageKey="fake-key"
/>,
<TopologyPage match={match} hideProjects={false} activeViewStorageKey="fake-key" />,
);
expect(wrapper.find('[data-test-id="topology-list-page"]').exists()).toBe(true);
});
Expand All @@ -112,11 +97,11 @@ describe('Topology page tests', () => {
path: '/topology/graph',
url: '',
};
let wrapper = shallow(<TopologyPage match={viewMatch} title="Topology" hideProjects={false} />);
let wrapper = shallow(<TopologyPage match={viewMatch} hideProjects={false} />);
expect(wrapper.find('[data-test-id="topology-list-page"]').exists()).toBe(false);

viewMatch.path = '/topology/list';
wrapper = shallow(<TopologyPage match={viewMatch} title="Topology" hideProjects={false} />);
wrapper = shallow(<TopologyPage match={viewMatch} hideProjects={false} />);
expect(wrapper.find('[data-test-id="topology-list-page"]').exists()).toBe(true);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ describe('TopologyPageToolbar tests', () => {
expect(wrapper.find(Button).exists()).toBe(false);
});

it('should not contain view switcher when no model', () => {
it('should disable view switcher when no model', () => {
const mockViewChange = jest.fn();
spyOn(React, 'useContext').and.returnValue({
isEmptyModel: true,
Expand All @@ -107,6 +107,9 @@ describe('TopologyPageToolbar tests', () => {
const wrapper = shallow(
<TopologyPageToolbar viewType={TopologyViewType.graph} onViewChange={mockViewChange} />,
);
expect(wrapper.find(Button).exists()).toBe(false);
const switcher = wrapper.find(Button);
expect(switcher.exists()).toBe(true);
expect(switcher.at(0).props().isDisabled).toBe(true);
expect(switcher.at(1).props().isDisabled).toBe(true);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,13 @@ const setTopologyLayout = (namespace: string, nodes: NodeModel[], layout: string
interface TopologyGraphViewProps {
visualizationReady: boolean;
visualization: Visualization;
controlsDisabled?: boolean;
selectedId?: string;
dragHint?: string;
}

const TopologyGraphView: React.FC<TopologyGraphViewProps> = React.memo(
({ visualizationReady, visualization, selectedId, dragHint }) => {
({ visualizationReady, visualization, controlsDisabled, selectedId, dragHint }) => {
if (!visualizationReady) {
return null;
}
Expand All @@ -79,7 +80,7 @@ const TopologyGraphView: React.FC<TopologyGraphViewProps> = React.memo(
<div className="odc-topology__hint-background">{dragHint}</div>
</div>
)}
<TopologyControlBar visualization={visualization} />
<TopologyControlBar visualization={visualization} isDisabled={controlsDisabled} />
</VisualizationProvider>
</div>
);
Expand Down Expand Up @@ -301,6 +302,7 @@ const Topology: React.FC<TopologyProps &
<TopologyGraphView
visualizationReady={visualizationReady}
visualization={visualization}
controlsDisabled={!model?.nodes.length}
dragHint={dragHint}
selectedId={selectedId}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ import {
} from '@patternfly/react-topology';
import { COLA_FORCE_LAYOUT, COLA_LAYOUT } from './layouts/layoutFactory';

interface TopologyControlBarProps {
interface ControlBarProps {
visualization: Visualization;
isDisabled: boolean;
}

const TopologyControlBar: React.FC<TopologyControlBarProps> = observer(({ visualization }) => {
const TopologyControlBar: React.FC<ControlBarProps> = observer(({ visualization, isDisabled }) => {
const { t } = useTranslation();
const layout = visualization.getGraph()?.getLayout() ?? COLA_LAYOUT;
return (
Expand All @@ -31,22 +32,26 @@ const TopologyControlBar: React.FC<TopologyControlBarProps> = observer(({ visual
}),
zoomInTip: t('topology~Zoom in'),
zoomInAriaLabel: t('topology~Zoom in'),
zoomInDisabled: isDisabled,
zoomOutCallback: action(() => {
visualization.getGraph().scaleBy(0.75);
}),
zoomOutTip: t('topology~Zoom out'),
zoomOutAriaLabel: t('topology~Zoom out'),
zoomOutDisabled: isDisabled,
fitToScreenCallback: action(() => {
visualization.getGraph().fit(80);
}),
fitToScreenTip: t('topology~Fit to screen'),
fitToScreenAriaLabel: t('topology~Fit to screen'),
fitToScreenDisabled: isDisabled,
resetViewCallback: action(() => {
visualization.getGraph().reset();
visualization.getGraph().layout();
}),
resetViewTip: t('topology~Reset view'),
resetViewAriaLabel: t('topology~Reset view'),
resetViewDisabled: isDisabled,
legend: false,
}),
]}
Expand All @@ -59,6 +64,7 @@ const TopologyControlBar: React.FC<TopologyControlBarProps> = observer(({ visual
'pf-m-active': layout === COLA_LAYOUT,
})}
variant="tertiary"
isDisabled={isDisabled}
onClick={() => {
visualization.getGraph().setLayout(COLA_LAYOUT);
visualization.getGraph().layout();
Expand All @@ -79,6 +85,7 @@ const TopologyControlBar: React.FC<TopologyControlBarProps> = observer(({ visual
'pf-m-active': layout === COLA_FORCE_LAYOUT,
})}
variant="tertiary"
isDisabled={isDisabled}
onClick={() => {
visualization.getGraph().setLayout(COLA_FORCE_LAYOUT);
visualization.getGraph().layout();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,64 +1,42 @@
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import { observer } from '@patternfly/react-topology';
import { HintBlock, StatusBox } from '@console/internal/components/utils';
import EmptyState from '@console/dev-console/src/components/EmptyState';
import { StatusBox } from '@console/internal/components/utils';
import { ModelContext, ExtensibleModel } from '../../data-transforms/ModelContext';
import TopologyView from './TopologyView';
import { TopologyViewType } from '../../topology-types';
import { FilterProvider } from '../../filters/FilterProvider';

interface TopologyDataRendererProps {
viewType: TopologyViewType;
title: string;
}

const TopologyDataRenderer: React.FC<TopologyDataRendererProps> = observer(
({ viewType, title }) => {
const { t } = useTranslation();
const { namespace, model, loaded, loadError } = React.useContext<ExtensibleModel>(ModelContext);
const EmptyMsg = React.useCallback(
() => (
<EmptyState
title={title}
hintBlock={
<HintBlock title={t('topology~No resources found')}>
<p>
{t(
'topology~To add content to your Project, create an Application, component or service using one of these options.',
)}
</p>
</HintBlock>
}
/>
),
[t, title],
);
const TopologyDataRenderer: React.FC<TopologyDataRendererProps> = observer(({ viewType }) => {
const { t } = useTranslation();
const { namespace, model, loaded, loadError } = React.useContext<ExtensibleModel>(ModelContext);

return (
<StatusBox
skeleton={
viewType === TopologyViewType.list && (
<div className="co-m-pane__body skeleton-overview">
<div className="skeleton-overview--head" />
<div className="skeleton-overview--tile" />
<div className="skeleton-overview--tile" />
<div className="skeleton-overview--tile" />
</div>
)
}
data={model ? model.nodes : null}
label={t('topology~Topology')}
loaded={loaded}
loadError={loadError}
EmptyMsg={EmptyMsg}
>
<FilterProvider>
<TopologyView viewType={viewType} model={model} namespace={namespace} />
</FilterProvider>
</StatusBox>
);
},
);
return (
<StatusBox
skeleton={
viewType === TopologyViewType.list && (
<div className="co-m-pane__body skeleton-overview">
<div className="skeleton-overview--head" />
<div className="skeleton-overview--tile" />
<div className="skeleton-overview--tile" />
<div className="skeleton-overview--tile" />
</div>
)
}
data={model}
label={t('topology~Topology')}
loaded={loaded}
loadError={loadError}
>
<FilterProvider>
<TopologyView viewType={viewType} model={model} namespace={namespace} />
</FilterProvider>
</StatusBox>
);
});

export default TopologyDataRenderer;
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import * as React from 'react';
import { Link } from 'react-router-dom';
import { Trans, useTranslation } from 'react-i18next';
import {
Button,
EmptyState,
EmptyStateIcon,
EmptyStateSecondaryActions,
EmptyStateVariant,
Title,
} from '@patternfly/react-core';
import { TopologyIcon } from '@patternfly/react-icons';

type TopologyEmptyStateProps = {
setIsQuickSearchOpen: (isOpen: boolean) => void;
};

const TopologyEmptyState: React.FC<TopologyEmptyStateProps> = ({ setIsQuickSearchOpen }) => {
const { t } = useTranslation();

return (
<EmptyState className="odc-topology__empty-state" variant={EmptyStateVariant.full}>
<EmptyStateIcon variant="container" component={TopologyIcon} />
<Title headingLevel="h3" size="lg">
{t('topology~No resources found')}
</Title>
<EmptyStateSecondaryActions>
<Trans t={t} ns="topology">
<Button
isInline
variant="link"
onClick={() => {
setIsQuickSearchOpen(true);
}}
>
Start building your application
</Button>{' '}
or visit the <Link to="/add">Add page</Link> for more details.
</Trans>
</EmptyStateSecondaryActions>
</EmptyState>
);
};

export default React.memo(TopologyEmptyState);
Loading

0 comments on commit 4d3b230

Please sign in to comment.