Skip to content
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

Adds workflow approval list and details #8375

Merged
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
3 changes: 3 additions & 0 deletions awx/ui_next/src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import Tokens from './models/Tokens';
import UnifiedJobTemplates from './models/UnifiedJobTemplates';
import UnifiedJobs from './models/UnifiedJobs';
import Users from './models/Users';
import WorkflowApprovals from './models/WorkflowApprovals';
import WorkflowApprovalTemplates from './models/WorkflowApprovalTemplates';
import WorkflowJobTemplateNodes from './models/WorkflowJobTemplateNodes';
import WorkflowJobTemplates from './models/WorkflowJobTemplates';
Expand Down Expand Up @@ -71,6 +72,7 @@ const TokensAPI = new Tokens();
const UnifiedJobTemplatesAPI = new UnifiedJobTemplates();
const UnifiedJobsAPI = new UnifiedJobs();
const UsersAPI = new Users();
const WorkflowApprovalsAPI = new WorkflowApprovals();
const WorkflowApprovalTemplatesAPI = new WorkflowApprovalTemplates();
const WorkflowJobTemplateNodesAPI = new WorkflowJobTemplateNodes();
const WorkflowJobTemplatesAPI = new WorkflowJobTemplates();
Expand Down Expand Up @@ -111,6 +113,7 @@ export {
UnifiedJobTemplatesAPI,
UnifiedJobsAPI,
UsersAPI,
WorkflowApprovalsAPI,
WorkflowApprovalTemplatesAPI,
WorkflowJobTemplateNodesAPI,
WorkflowJobTemplatesAPI,
Expand Down
18 changes: 18 additions & 0 deletions awx/ui_next/src/api/models/WorkflowApprovals.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Base from '../Base';

class WorkflowApprovals extends Base {
constructor(http) {
super(http);
this.baseUrl = '/api/v2/workflow_approvals/';
}

approve(id) {
return this.http.post(`${this.baseUrl}${id}/approve/`);
}

deny(id) {
return this.http.post(`${this.baseUrl}${id}/deny/`);
}
}

export default WorkflowApprovals;
Original file line number Diff line number Diff line change
Expand Up @@ -61,17 +61,14 @@ const ItemToDelete = shape({
}).isRequired,
});

function cannotDelete(item) {
return !item.summary_fields.user_capabilities.delete;
}

function ToolbarDeleteButton({
itemsToDelete,
pluralizedItemName,
errorMessage,
onDelete,
warningMessage,
i18n,
cannotDelete,
}) {
const { isKebabified, onKebabModalChange } = useContext(KebabifiedContext);
const [isModalOpen, setIsModalOpen] = useState(false);
Expand Down Expand Up @@ -100,7 +97,7 @@ function ToolbarDeleteButton({
return (
<div>
{errorMessage.length > 0
? errorMessage
? `${errorMessage}: ${itemsUnableToDelete}`
: i18n._(
t`You do not have permission to delete ${pluralizedItemName}: ${itemsUnableToDelete}`
)}
Expand Down Expand Up @@ -193,12 +190,14 @@ ToolbarDeleteButton.propTypes = {
pluralizedItemName: string,
errorMessage: string,
warningMessage: node,
cannotDelete: func,
};

ToolbarDeleteButton.defaultProps = {
pluralizedItemName: 'Items',
errorMessage: '',
warningMessage: null,
cannotDelete: item => !item.summary_fields.user_capabilities.delete,
};

export default withI18n()(ToolbarDeleteButton);
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

exports[`<ToolbarDeleteButton /> should render button 1`] = `
<ToolbarDeleteButton
cannotDelete={[Function]}
errorMessage=""
i18n={"/i18n/"}
itemsToDelete={Array []}
Expand Down
6 changes: 6 additions & 0 deletions awx/ui_next/src/routeConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import Settings from './screens/Setting';
import Teams from './screens/Team';
import Templates from './screens/Template';
import Users from './screens/User';
import WorkflowApprovals from './screens/WorkflowApproval';

// Ideally, this should just be a regular object that we export, but we
// need the i18n. When lingui3 arrives, we will be able to import i18n
Expand All @@ -43,6 +44,11 @@ function getRouteConfig(i18n) {
path: '/schedules',
screen: Schedules,
},
{
title: i18n._(t`Workflow Approvals`),
path: '/workflow_approvals',
screen: WorkflowApprovals,
},
],
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ function NotificationTemplatesList({ i18n }) {
key="delete"
onDelete={handleDelete}
itemsToDelete={selected}
pluralizedItemName="Organizations"
pluralizedItemName={i18n._(t`Notification Templates`)}
/>,
]}
/>
Expand All @@ -164,7 +164,7 @@ function NotificationTemplatesList({ i18n }) {
title={i18n._(t`Error!`)}
onClose={clearDeletionError}
>
{i18n._(t`Failed to delete one or more organizations.`)}
{i18n._(t`Failed to delete one or more notification template.`)}
<ErrorDetail error={deletionError} />
</AlertModal>
</>
Expand Down
116 changes: 116 additions & 0 deletions awx/ui_next/src/screens/WorkflowApproval/WorkflowApproval.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import React, { useEffect, useCallback } from 'react';
import { t } from '@lingui/macro';
import { withI18n } from '@lingui/react';
import { Card, PageSection } from '@patternfly/react-core';
import { CaretLeftIcon } from '@patternfly/react-icons';
import {
Link,
Switch,
Route,
Redirect,
useParams,
useRouteMatch,
useLocation,
} from 'react-router-dom';
import useRequest from '../../util/useRequest';
import RoutedTabs from '../../components/RoutedTabs';
import ContentError from '../../components/ContentError';
import { WorkflowApprovalsAPI } from '../../api';
import WorkflowApprovalDetail from './WorkflowApprovalDetail';

function WorkflowApproval({ setBreadcrumb, i18n }) {
const { id: workflowApprovalId } = useParams();
const match = useRouteMatch();
const location = useLocation();
const {
result: { workflowApproval },
isLoading,
error,
request: fetchWorkflowApproval,
} = useRequest(
useCallback(async () => {
const detail = await WorkflowApprovalsAPI.readDetail(workflowApprovalId);
setBreadcrumb(detail.data);
return {
workflowApproval: detail.data,
};
}, [workflowApprovalId, setBreadcrumb]),
{ workflowApproval: null }
);

useEffect(() => {
fetchWorkflowApproval();
}, [fetchWorkflowApproval, location.pathname]);

if (!isLoading && error) {
return (
<PageSection>
<Card>
<ContentError error={error}>
{error.response.status === 404 && (
<span>
{i18n._(t`Workflow Approval not found.`)}{' '}
<Link to="/workflow_approvals">
{i18n._(t`View all Workflow Approvals.`)}
</Link>
</span>
)}
</ContentError>
</Card>
</PageSection>
);
}

const tabs = [
{
name: (
<>
<CaretLeftIcon />
{i18n._(t`Back to Workflow Approvals`)}
</>
),
link: `/workflow_approvals`,
id: 99,
},
{
name: i18n._(t`Details`),
link: `${match.url}/details`,
id: 0,
},
];
return (
<PageSection>
<Card>
<RoutedTabs tabsArray={tabs} />
<Switch>
<Redirect
from="/workflow_approvals/:id"
to="/workflow_approvals/:id/details"
exact
/>
{workflowApproval && (
<Route path="/workflow_approvals/:id/details">
<WorkflowApprovalDetail
workflowApproval={workflowApproval}
isLoading={isLoading}
/>
</Route>
)}
<Route key="not-found" path="*">
{!isLoading && (
<ContentError isNotFound>
{match.params.id && (
<Link to={`/workflow_approvals/${match.params.id}/details`}>
{i18n._(t`View Workflow Approval Details`)}
</Link>
)}
</ContentError>
)}
</Route>
</Switch>
</Card>
</PageSection>
);
}

export default withI18n()(WorkflowApproval);
58 changes: 58 additions & 0 deletions awx/ui_next/src/screens/WorkflowApproval/WorkflowApproval.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { createMemoryHistory } from 'history';
import { WorkflowApprovalsAPI } from '../../api';
import {
mountWithContexts,
waitForElement,
} from '../../../testUtils/enzymeHelpers';
import mockWorkflowApprovals from './data.workflowApprovals.json';
import WorkflowApproval from './WorkflowApproval';

jest.mock('../../api');

const mockMe = {
is_super_user: true,
is_system_auditor: false,
};

describe('<WorkflowApproval />', () => {
test('initially renders succesfully', async () => {
WorkflowApprovalsAPI.readDetail.mockResolvedValue({
data: mockWorkflowApprovals.results[0],
});
await act(async () => {
mountWithContexts(
<WorkflowApproval setBreadcrumb={() => {}} me={mockMe} />
);
});
});

test('should show content error when user attempts to navigate to erroneous route', async () => {
const history = createMemoryHistory({
initialEntries: ['/workflow_approvals/1/foobar'],
});
let wrapper;
await act(async () => {
wrapper = mountWithContexts(
<WorkflowApproval setBreadcrumb={() => {}} me={mockMe} />,
{
context: {
router: {
history,
route: {
location: history.location,
match: {
params: { id: 1 },
url: '/workflow_approvals/1/foobar',
path: '/workflow_approvals/1/foobar',
},
},
},
},
}
);
});
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
});
});
Loading