-
Notifications
You must be signed in to change notification settings - Fork 174
ROX-13123, ROX-13124: Redirect to clusters page when no secured clusters detected #3541
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 { | ||
|
|
@@ -9,6 +9,7 @@ import { | |
| visitClustersWithFixtureMetadataDatetime, | ||
| visitClusterByNameWithFixture, | ||
| visitClusterByNameWithFixtureMetadataDatetime, | ||
| visitDashboardWithNoClusters, | ||
| } from '../../helpers/clusters'; | ||
| import { hasFeatureFlag } from '../../helpers/features'; | ||
|
|
||
|
|
@@ -48,6 +49,24 @@ describe('Clusters page', () => { | |
| }); | ||
| }); | ||
| }); | ||
|
|
||
| describe('when no secured clusters are added yet (only applies to Cloud Service)', () => { | ||
| it('should should redirect to the Clusters page', () => { | ||
| visitDashboardWithNoClusters(); | ||
|
|
||
| cy.url().should('contain', `${clustersUrl}`); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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);
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
|
|
||
| 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', () => { | ||
|
|
||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this equivalent to https://www.patternfly.org/v4/layouts/bullseye/design-guidelines
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why plan if redirects work already? #3799
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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(); | ||
|
|
@@ -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); | ||
|
|
@@ -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 || {}); | ||
|
|
@@ -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 && ( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To follow up excellent improvement to add
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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".
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
{currentClusters.length === 0 && (!fetchingClusters || pollingCount > 0) && (
<AddClusterPrompt />
)}
{currentClusters.length !== 0 && (
<div data-testid="clusters-table" className="h-full w-full">
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| <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 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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'; | ||
|
|
@@ -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'; | ||
|
|
@@ -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, | ||
|
|
@@ -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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| // (only applicable in Cloud Services version) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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.
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, { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ^ @lmaynard88 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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..." />; | ||
|
|
||
There was a problem hiding this comment.
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