Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions ui/apps/platform/cypress/helpers/clusters.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,26 @@ export function visitClusterByNameWithFixtureMetadataDatetime(
cy.get(selectors.clusterSidePanelHeading).contains(clusterName);
});
}

export function visitDashboardWithNoClusters() {
cy.intercept('POST', api.graphql('summary_counts'), {
body: {
data: {
clusterCount: 0,
nodeCount: 3,
violationCount: 20,
deploymentCount: 35,
imageCount: 31,
secretCount: 15,
},
},
}).as('summary_counts');
cy.intercept('GET', api.clusters.list, {
clusters: [],
}).as('clusters');

// visitMainDashboard(); // with a count of 0 clusters, app should redirect to the clusters pages
cy.visit('/main/dashboard'); // with a count of 0 clusters, app should redirect to the clusters pages

cy.wait(['@summary_counts', '@clusters']);
}
21 changes: 20 additions & 1 deletion ui/apps/platform/cypress/integration/clusters/clusters.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import cloneDeep from 'lodash/cloneDeep';

import { selectors } from '../../constants/ClustersPage';
import { clustersUrl, selectors } from '../../constants/ClustersPage';
import { clusters as clustersApi } from '../../constants/apiEndpoints';
import withAuth from '../../helpers/basicAuth';
import {
Expand All @@ -9,6 +9,7 @@ import {
visitClustersWithFixtureMetadataDatetime,
visitClusterByNameWithFixture,
visitClusterByNameWithFixtureMetadataDatetime,
visitDashboardWithNoClusters,
} from '../../helpers/clusters';
import { hasFeatureFlag } from '../../helpers/features';

Expand Down Expand Up @@ -48,6 +49,24 @@ describe('Clusters page', () => {
});
});
});

describe('when no secured clusters are added yet (only applies to Cloud Service)', () => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please add test to a new file like clustersCloudService.test.js

describe('Clusters Cloud Service', () => {
    it('should redirect from main dashboard when no secured clusters', () => {

it('should should redirect to the Clusters page', () => {
visitDashboardWithNoClusters();

cy.url().should('contain', `${clustersUrl}`);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please rewrite assertion according to majority pattern (118 versus 3 occurrences) and move into helper function because semantically related to heading assertion there, which is repeated here.

Also removes need to import url here. By the way, it can be exact match, true?

cy.location('pathname').should('eq', clustersUrl);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I intentionally took all assertions out of the helper function, because this is a one-off test, where the helper function is mainly just to abstract the API mocks and waiting.
(I almost moved everything into test, instead of keeping the mocking in a helper function, but in the end made the subjective call that the setup distracted from the assertions, so that's where I drew the line in this special case.


cy.get(selectors.clustersListHeading);

cy.get(
'p:contains("You have successfully deployed a Red Hat Advanced Cluster Security platform.")'
);

cy.get('h2:contains("Configure the clusters you want to secure.")');

cy.get('a:contains("View instructions")');
});
});
});

describe('Cluster Certificate Expiration', () => {
Expand Down
62 changes: 62 additions & 0 deletions ui/apps/platform/src/Containers/Clusters/AddClusterPrompt.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React from 'react';
import {
Alert,
AlertVariant,
Button,
ButtonVariant,
Flex,
FlexItem,
Text,
TextContent,
TextVariants,
} from '@patternfly/react-core';

import SecurityFullColorPink from 'images/Security_FullColor_Pink.svg';

function AddClusterPrompt() {
return (
<>
<Alert isInline variant={AlertVariant.success} title="You are ready to go!">
<p className="pf-u-font-weight-normal">
You have successfully deployed a Red Hat Advanced Cluster Security platform. Now
you can configure the clusters you want to secure.
</p>
</Alert>
<Flex
alignItems={{ default: 'alignItemsCenter' }}
justifyContent={{ default: 'justifyContentCenter' }}
className="pf-u-text-align-center"
direction={{ default: 'column' }}
Comment on lines +26 to +29
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this equivalent to Bullseye layout element?

https://www.patternfly.org/v4/layouts/bullseye/design-guidelines

Use a Bullseye layout to center content, both vertically and horizontally within a container.

Copy link
Copy Markdown
Contributor Author

@vjwilson vjwilson Nov 4, 2022

Choose a reason for hiding this comment

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

I tried a Bullseye at first, but required style overrides to get the spacing between the image, text, and button correct. Rather than hack in overrides, I chose to simplify. (The larger UX team recommended this look to Zhenpeng as a variation the Empty State pattern.)

>
<FlexItem className="pf-u-mb-0">
<img src={SecurityFullColorPink} alt="" style={{ height: '430px' }} />
</FlexItem>
<FlexItem className="pf-u-w-66">
<TextContent className="pf-u-mb-md">
<Text component={TextVariants.h2} className="pf-u-font-size-2xl">
Configure the clusters you want to secure.
</Text>
<Text component={TextVariants.p} className="pf-u-font-weight-normal">
Follow the instructions to add secured clusters for Central to monitor.
<br />
Upon successful installation, secured clusters are listed here.
</Text>
</TextContent>
</FlexItem>
<FlexItem>
<Button
variant={ButtonVariant.primary}
component="a"
target="_blank"
rel="noopener noreferrer nofollow"
href="https://docs.openshift.com/acs/3.72/installing/install-ocp-operator.html#adding-a-new-cluster-to-rhacs"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this URL something we can determine dynamically, or do we just need to make a note to check this before each release?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

We plan to redirect to a page in the 3.73 doc set once we publish the 3.73 docs. So it's not dynamic. We may want a different approach in future.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why plan if redirects work already? #3799

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@msugakov The redirect that Lynne is referring to, is a 301 redirect of that doc link to a new improved documentation page about installation, which would not exist until 3.73 is released. The redirect in the documentation means we will not have to change this hard-coded link in the product itself.

>
View instructions
</Button>
</FlexItem>
</Flex>
</>
);
}

export default AddClusterPrompt;
55 changes: 34 additions & 21 deletions ui/apps/platform/src/Containers/Clusters/ClustersTablePanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { filterAllowedSearch, convertToRestSearch } from 'utils/searchUtils';
import AutoUpgradeToggle from './Components/AutoUpgradeToggle';
import { clusterTablePollingInterval, getUpgradeableClusters } from './cluster.helpers';
import { getColumnsForClusters } from './clustersTableColumnDescriptors';
import AddClusterPrompt from './AddClusterPrompt';

function ClustersTablePanel({ selectedClusterId, setSelectedClusterId, searchOptions }) {
const { hasReadWriteAccess } = usePermissions();
Expand All @@ -46,6 +47,7 @@ function ClustersTablePanel({ selectedClusterId, setSelectedClusterId, searchOpt
const [pollingCount, setPollingCount] = useState(0);
const [tableRef, setTableRef] = useState(null);
const [showDialog, setShowDialog] = useState(false);
const [fetchingClusters, setFetchingClusters] = useState(false);

// Handle changes to applied search options.
const [isViewFiltered, setIsViewFiltered] = useState(false);
Expand Down Expand Up @@ -84,10 +86,16 @@ function ClustersTablePanel({ selectedClusterId, setSelectedClusterId, searchOpt
));

function refreshClusterList(restSearch) {
return fetchClustersWithRetentionInfo(restSearch).then((clustersResponse) => {
setCurrentClusters(clustersResponse.clusters);
setClusterIdToRetentionInfo(clustersResponse.clusterIdToRetentionInfo);
});
setFetchingClusters(true);
return fetchClustersWithRetentionInfo(restSearch)
.then((clustersResponse) => {
setCurrentClusters(clustersResponse.clusters);
setClusterIdToRetentionInfo(clustersResponse.clusterIdToRetentionInfo);
setFetchingClusters(false);
})
.catch(() => {
setFetchingClusters(false);
});
}

const filteredSearch = filterAllowedSearch(searchOptions, pageSearch || {});
Expand Down Expand Up @@ -263,23 +271,28 @@ function ClustersTablePanel({ selectedClusterId, setSelectedClusterId, searchOpt
{messages}
</div>
)}
<div data-testid="clusters-table" className="h-full w-full">
<CheckboxTable
ref={(table) => {
setTableRef(table);
}}
rows={currentClusters}
columns={clusterColumns}
onRowClick={setSelectedClusterId}
toggleRow={toggleCluster}
toggleSelectAll={toggleAllClusters}
selection={checkedClusterIds}
selectedRowId={selectedClusterId}
noDataText="No clusters to show."
minRows={20}
pageSize={pageSize}
/>
</div>
{(!fetchingClusters || pollingCount > 0) && currentClusters.length <= 0 && (
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

To follow up excellent improvement to add fetchingClusters and move to a pattern which supports error message if that becomes necessary, something like {content} here and if-else-if-else for assignment makes it clearer to render spinner by default (else in potential future, error message) else if no clusters AddClusterPrompt else CheckboxTable element.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I started to improve the non-empty flow, but stopped myself, so as not to set a bad example through "scope creep".

Copy link
Copy Markdown
Contributor

@pedrottimark pedrottimark Nov 4, 2022

Choose a reason for hiding this comment

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

The complicated conditions imply non-obvious behavior change to render neither when fetching.

How about the following, which keeps render neither until after first fetch, but simplifies the conditions:

  • It is hard to see how array length can become less than zero.
  • If length is non-zero (or if you prefer, greater than zero) it seems redundant to require not fetching.
{currentClusters.length === 0 && (!fetchingClusters || pollingCount > 0) && (
    <AddClusterPrompt />
)}
{currentClusters.length !== 0 && (
    <div data-testid="clusters-table" className="h-full w-full">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

After release 3.73 we can circle back to question by Dave whether no clusters unambiguously implies Cloud Service.

For the moment, no support for cluster auto-upgrade implies Cloud Service. Because it is not impossible that auto-upgrade might become supported in the future, we can follow up about how to get unambiguous indication. Besides this initial conditional rendering, it seems relevant information to render in Clusters and System Health.

https://github.com/stackrox/stackrox/blob/master/ui/apps/platform/src/services/ClustersService.ts#L100-L110

<AddClusterPrompt />
)}
{(!fetchingClusters || pollingCount > 0) && currentClusters.length > 0 && (
<div data-testid="clusters-table" className="h-full w-full">
<CheckboxTable
ref={(table) => {
setTableRef(table);
}}
rows={currentClusters}
columns={clusterColumns}
onRowClick={setSelectedClusterId}
toggleRow={toggleCluster}
toggleSelectAll={toggleAllClusters}
selection={checkedClusterIds}
selectedRowId={selectedClusterId}
noDataText="No clusters to show."
minRows={20}
pageSize={pageSize}
/>
</div>
)}
</PanelBody>
</PanelNew>
<Dialog
Expand Down
25 changes: 25 additions & 0 deletions ui/apps/platform/src/Containers/MainPage/MainPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { createStructuredSelector } from 'reselect';
import { Page } from '@patternfly/react-core';
import { gql, useQuery } from '@apollo/client';

import { selectors } from 'reducers';
import { actions as globalSearchActions } from 'reducers/globalSearch';
Expand All @@ -15,6 +16,7 @@ import AppWrapper from 'Containers/AppWrapper';
import Body from 'Containers/MainPage/Body';
import useFeatureFlags from 'hooks/useFeatureFlags';
import usePermissions from 'hooks/usePermissions';
import { clustersBasePath } from 'routePaths';

import CredentialExpiryBanner from './CredentialExpiryBanner';
import VersionOutOfDate from './VersionOutOfDate';
Expand All @@ -28,6 +30,16 @@ const mainPageSelector = createStructuredSelector({
serverState: selectors.getServerState,
});

type ClusterCountResponse = {
clusterCount: number;
};

const CLUSTER_COUNT = gql`
query summary_counts {
clusterCount
}
`;

function MainPage(): ReactElement {
const {
isGlobalSearchView,
Expand All @@ -51,6 +63,19 @@ function MainPage(): ReactElement {
const { isFeatureFlagEnabled, isLoadingFeatureFlags } = useFeatureFlags();
const { hasReadAccess, hasReadWriteAccess, isLoadingPermissions } = usePermissions();

// Check for clusters under management
// if none, and user can admin Clusters, redirect to clusters section
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The phrase and use can admin Clusters implies a potential timing problem, because the following if statement waits until response from /v1/mypermissions request.

// (only applicable in Cloud Services version)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

By "only applicable in Cloud Services version", does that mean that this situation is only ever possible in the cloud services version? Is there any way an on-prem install could return no clusters?

Related, is the ability to view clusters something that is controlled by SAC? Is it possible a user could have insufficient permissions to view clusters but still be able to do useful work, and this would cause them to get stuck on the clusters page?

Copy link
Copy Markdown
Contributor Author

@vjwilson vjwilson Nov 4, 2022

Choose a reason for hiding this comment

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

...Is there any way an on-prem install could return no clusters?

While I never underestimate the ability of a user to "bork" their environment, the affordance of encouraging them to connect clusters after they have accidentally unconnected them all, is still helpful even if they are not in the Cloud Services environment. The comment is mainly to explain to folks in the future why this needed to be added at all, since the self-managed installation always adds the cluster where central is installed.

Is it possible a user could have insufficient permissions to view clusters but still be able to do useful work, and this would cause them to get stuck on the clusters page?

The check only happens on page load, so the redirect would only happen on first visit, or if the user triggered a browser refresh. They can always navigate away to do other things. There is not much useful to do until a least one cluster is connected--maybe create custom policies, or integrations--but any user without permissions wouldn't be stuck.

const hasClusterWritePermission = hasReadWriteAccess('Cluster');

useQuery<ClusterCountResponse>(CLUSTER_COUNT, {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Another thought, I wonder if it would be worth adding some text either in the UI or on the docs page that tells the user they need to refresh the ACS UI once a cluster is added. Since Apollo caches the GQL query results, this call will continue to return 0 every time the user navigates to a new page, even after a cluster is added.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

^ @lmaynard88
Is Dave's suggestion something you can add to the docs?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Yes, this can be added to the 3.73 doc.

onCompleted: (data) => {
if (hasClusterWritePermission && data?.clusterCount < 1) {
history.push(clustersBasePath);
}
},
});

// Render Body and NavigationSideBar only when feature flags and permissions are available.
if (isLoadingFeatureFlags || isLoadingPermissions) {
return <LoadingSection message="Loading..." />;
Expand Down
Loading