Skip to content

Commit

Permalink
Merge pull request #9496 from marshmalien/settings-subscription-wizard
Browse files Browse the repository at this point in the history
Subscription wizard

SUMMARY

Adds subscriptions routes
Extends Config context to return an array with two items: config state and config setter

Update components that use Config with the new return pattern


Move mock config into setupTests.js
Return only the routes the user is "authorized" (valid license key) to view

Subscription Details view: /settings/subscription/details

This is our standard details view
Clicking Edit will send the user to subscription wizard view /settings/subscription/edit
Route is not accessible when license type is OPEN

Subscription Add wizard view: /settings/subscription_management
Step 1 - Subscription:

If a user does not have a Red Hat Ansible Automation Platform subscription, they can request a trial subscription via the link
Toggle between uploading a subscription manifest .zip file or retrieving subscriptions using Red Hat credentials (username and password)
Get Subscriptions button fetches subscriptions and displays them in a modal



Step 2 - Tracking and analytics:

Shows two checkboxes to enable User analytics and Automation analytics
If the user has previously selected the RH subscription manifest flow, checking the Automation Analytics box will display required RH username and password fields
If the user has previously selected the RH username/password flow, they will not see this additional username/password field if Automation Analytics is checked


Step 3 - EULA: https://tower-mockups.testing.ansible.com/patternfly/settings/settings-license-step-03/

Submission should show a success message and navigate user to dashboard if this is the initial launch and to the subscription detail view if they are editing the subscription
Failed submission should show a wizard form error

ISSUE TYPE

Feature

COMPONENT NAME

UI

ADDITIONAL INFORMATION

Reviewed-by: Michael Abashian <None>
Reviewed-by: Kersom <None>
Reviewed-by: Tiago Góes <tiago.goes2009@gmail.com>
  • Loading branch information
softwarefactory-project-zuul[bot] committed Apr 7, 2021
2 parents 091027f + 440bdee commit f9981c0
Show file tree
Hide file tree
Showing 45 changed files with 2,495 additions and 279 deletions.
3 changes: 2 additions & 1 deletion awx/ui_next/.eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@
"theme",
"gridColumns",
"rows",
"href"
"href",
"modifier"
],
"ignore": ["Ansible", "Tower", "JSON", "YAML", "lg"],
"ignoreComponent": [
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
67 changes: 50 additions & 17 deletions awx/ui_next/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import {
Redirect,
} from 'react-router-dom';
import { I18n, I18nProvider } from '@lingui/react';
import { Card, PageSection } from '@patternfly/react-core';

import { ConfigProvider, useAuthorizedPath } from './contexts/Config';
import AppContainer from './components/AppContainer';
import Background from './components/Background';
import NotFound from './screens/NotFound';
Expand All @@ -20,6 +22,49 @@ import { isAuthenticated } from './util/auth';
import { getLanguageWithoutRegionCode } from './util/language';

import getRouteConfig from './routeConfig';
import SubscriptionEdit from './screens/Setting/Subscription/SubscriptionEdit';

const AuthorizedRoutes = ({ routeConfig }) => {
const isAuthorized = useAuthorizedPath();
const match = useRouteMatch();

if (!isAuthorized) {
return (
<Switch>
<ProtectedRoute
key="/subscription_management"
path="/subscription_management"
>
<PageSection>
<Card>
<SubscriptionEdit />
</Card>
</PageSection>
</ProtectedRoute>
<Route path="*">
<Redirect to="/subscription_management" />
</Route>
</Switch>
);
}

return (
<Switch>
{routeConfig
.flatMap(({ routes }) => routes)
.map(({ path, screen: Screen }) => (
<ProtectedRoute key={path} path={path}>
<Screen match={match} />
</ProtectedRoute>
))
.concat(
<ProtectedRoute key="not-found" path="*">
<NotFound />
</ProtectedRoute>
)}
</Switch>
);
};

const ProtectedRoute = ({ children, ...rest }) =>
isAuthenticated(document.cookie) ? (
Expand All @@ -36,7 +81,6 @@ function App() {
// preferred language, default to one that has strings.
language = 'en';
}
const match = useRouteMatch();
const { hash, search, pathname } = useLocation();

return (
Expand All @@ -55,22 +99,11 @@ function App() {
<Redirect to="/home" />
</Route>
<ProtectedRoute>
<AppContainer navRouteConfig={getRouteConfig(i18n)}>
<Switch>
{getRouteConfig(i18n)
.flatMap(({ routes }) => routes)
.map(({ path, screen: Screen }) => (
<ProtectedRoute key={path} path={path}>
<Screen match={match} />
</ProtectedRoute>
))
.concat(
<ProtectedRoute key="not-found" path="*">
<NotFound />
</ProtectedRoute>
)}
</Switch>
</AppContainer>
<ConfigProvider>
<AppContainer navRouteConfig={getRouteConfig(i18n)}>
<AuthorizedRoutes routeConfig={getRouteConfig(i18n)} />
</AppContainer>
</ConfigProvider>
</ProtectedRoute>
</Switch>
</Background>
Expand Down
11 changes: 11 additions & 0 deletions awx/ui_next/src/api/models/Config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ class Config extends Base {
this.baseUrl = '/api/v2/config/';
this.read = this.read.bind(this);
}

readSubscriptions(username, password) {
return this.http.post(`${this.baseUrl}subscriptions/`, {
subscriptions_username: username,
subscriptions_password: password,
});
}

attach(data) {
return this.http.post(`${this.baseUrl}attach/`, data);
}
}

export default Config;
4 changes: 4 additions & 0 deletions awx/ui_next/src/api/models/Settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ class Settings extends Base {
return this.http.patch(`${this.baseUrl}all/`, data);
}

updateCategory(category, data) {
return this.http.patch(`${this.baseUrl}${category}/`, data);
}

readCategory(category) {
return this.http.get(`${this.baseUrl}${category}/`);
}
Expand Down
78 changes: 33 additions & 45 deletions awx/ui_next/src/components/AppContainer/AppContainer.jsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
import React, { useEffect, useState, useCallback, useRef } from 'react';
import { useHistory, useLocation, withRouter } from 'react-router-dom';
import { useHistory, withRouter } from 'react-router-dom';
import {
Button,
Nav,
NavList,
Page,
PageHeader as PFPageHeader,
PageHeaderTools,
PageHeaderToolsGroup,
PageHeaderToolsItem,
PageSidebar,
} from '@patternfly/react-core';
import { t } from '@lingui/macro';
import { withI18n } from '@lingui/react';
import styled from 'styled-components';

import { ConfigAPI, MeAPI, RootAPI } from '../../api';
import { ConfigProvider } from '../../contexts/Config';
import { MeAPI, RootAPI } from '../../api';
import { useConfig, useAuthorizedPath } from '../../contexts/Config';
import { SESSION_TIMEOUT_KEY } from '../../constants';
import { isAuthenticated } from '../../util/auth';
import About from '../About';
import AlertModal from '../AlertModal';
import ErrorDetail from '../ErrorDetail';
import BrandLogo from './BrandLogo';
import NavExpandableGroup from './NavExpandableGroup';
import PageHeaderToolbar from './PageHeaderToolbar';
Expand Down Expand Up @@ -85,11 +87,11 @@ function useStorage(key) {

function AppContainer({ i18n, navRouteConfig = [], children }) {
const history = useHistory();
const { pathname } = useLocation();
const [config, setConfig] = useState({});
const [configError, setConfigError] = useState(null);
const config = useConfig();

const isReady = !!config.license_info;
const isSidebarVisible = useAuthorizedPath();
const [isAboutModalOpen, setIsAboutModalOpen] = useState(false);
const [isReady, setIsReady] = useState(false);

const sessionTimeoutId = useRef();
const sessionIntervalId = useRef();
Expand All @@ -99,7 +101,6 @@ function AppContainer({ i18n, navRouteConfig = [], children }) {

const handleAboutModalOpen = () => setIsAboutModalOpen(true);
const handleAboutModalClose = () => setIsAboutModalOpen(false);
const handleConfigErrorClose = () => setConfigError(null);
const handleSessionTimeout = () => setTimeoutWarning(true);

const handleLogout = useCallback(async () => {
Expand Down Expand Up @@ -137,31 +138,6 @@ function AppContainer({ i18n, navRouteConfig = [], children }) {
}
}, [handleLogout, timeRemaining]);

useEffect(() => {
const loadConfig = async () => {
if (config?.version) return;
try {
const [
{ data },
{
data: {
results: [me],
},
},
] = await Promise.all([ConfigAPI.read(), MeAPI.read()]);
setConfig({ ...data, me });
setIsReady(true);
} catch (err) {
if (err.response.status === 401) {
handleLogout();
return;
}
setConfigError(err);
}
};
loadConfig();
}, [config, pathname, handleLogout]);

const header = (
<PageHeader
showNavToggle
Expand All @@ -178,6 +154,23 @@ function AppContainer({ i18n, navRouteConfig = [], children }) {
/>
);

const simpleHeader = config.isLoading ? null : (
<PageHeader
logo={<BrandLogo />}
headerTools={
<PageHeaderTools>
<PageHeaderToolsGroup>
<PageHeaderToolsItem>
<Button onClick={handleLogout} variant="tertiary" ouiaId="logout">
{i18n._(t`Logout`)}
</Button>
</PageHeaderToolsItem>
</PageHeaderToolsGroup>
</PageHeaderTools>
}
/>
);

const sidebar = (
<PageSidebar
theme="dark"
Expand All @@ -200,23 +193,18 @@ function AppContainer({ i18n, navRouteConfig = [], children }) {

return (
<>
<Page isManagedSidebar header={header} sidebar={sidebar}>
{isReady && <ConfigProvider value={config}>{children}</ConfigProvider>}
<Page
isManagedSidebar={isSidebarVisible}
header={isSidebarVisible ? header : simpleHeader}
sidebar={isSidebarVisible && sidebar}
>
{isReady ? children : null}
</Page>
<About
version={config?.version}
isOpen={isAboutModalOpen}
onClose={handleAboutModalClose}
/>
<AlertModal
isOpen={configError}
variant="error"
title={i18n._(t`Error!`)}
onClose={handleConfigErrorClose}
>
{i18n._(t`Failed to retrieve configuration.`)}
<ErrorDetail error={configError} />
</AlertModal>
<AlertModal
ouiaId="session-expiration-modal"
title={i18n._(t`Your session is about to expire`)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
waitForElement,
} from '../../../testUtils/enzymeHelpers';
import { ConfigAPI, MeAPI, RootAPI } from '../../api';
import { useAuthorizedPath } from '../../contexts/Config';
import AppContainer from './AppContainer';

jest.mock('../../api');
Expand All @@ -19,10 +20,12 @@ describe('<AppContainer />', () => {
},
});
MeAPI.read.mockResolvedValue({ data: { results: [{}] } });
useAuthorizedPath.mockImplementation(() => true);
});

afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});

test('expected content is rendered', async () => {
Expand Down Expand Up @@ -77,7 +80,9 @@ describe('<AppContainer />', () => {

let wrapper;
await act(async () => {
wrapper = mountWithContexts(<AppContainer />);
wrapper = mountWithContexts(<AppContainer />, {
context: { config: { version } },
});
});

// open about dropdown menu
Expand Down
Loading

0 comments on commit f9981c0

Please sign in to comment.