mirror of
https://github.com/ansible/awx.git
synced 2026-01-16 04:10:44 -03:30
Adds sync button to project details page
This commit is contained in:
parent
7e74f823f4
commit
d1fcb96ee2
@ -19,6 +19,7 @@ import CredentialChip from '../../../components/CredentialChip';
|
||||
import { ProjectsAPI } from '../../../api';
|
||||
import { toTitleCase } from '../../../util/strings';
|
||||
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
||||
import ProjectSyncButton from '../shared/ProjectSyncButton';
|
||||
|
||||
function ProjectDetail({ project, i18n }) {
|
||||
const {
|
||||
@ -148,27 +149,28 @@ function ProjectDetail({ project, i18n }) {
|
||||
/>
|
||||
</DetailList>
|
||||
<CardActionsRow>
|
||||
{summary_fields.user_capabilities &&
|
||||
summary_fields.user_capabilities.edit && (
|
||||
<Button
|
||||
aria-label={i18n._(t`edit`)}
|
||||
component={Link}
|
||||
to={`/projects/${id}/edit`}
|
||||
>
|
||||
{i18n._(t`Edit`)}
|
||||
</Button>
|
||||
)}
|
||||
{summary_fields.user_capabilities &&
|
||||
summary_fields.user_capabilities.delete && (
|
||||
<DeleteButton
|
||||
name={name}
|
||||
modalTitle={i18n._(t`Delete Project`)}
|
||||
onConfirm={deleteProject}
|
||||
isDisabled={isLoading}
|
||||
>
|
||||
{i18n._(t`Delete`)}
|
||||
</DeleteButton>
|
||||
)}
|
||||
{summary_fields.user_capabilities?.edit && (
|
||||
<Button
|
||||
aria-label={i18n._(t`edit`)}
|
||||
component={Link}
|
||||
to={`/projects/${id}/edit`}
|
||||
>
|
||||
{i18n._(t`Edit`)}
|
||||
</Button>
|
||||
)}
|
||||
{summary_fields.user_capabilities?.start && (
|
||||
<ProjectSyncButton projectId={project.id} />
|
||||
)}
|
||||
{summary_fields.user_capabilities?.delete && (
|
||||
<DeleteButton
|
||||
name={name}
|
||||
modalTitle={i18n._(t`Delete Project`)}
|
||||
onConfirm={deleteProject}
|
||||
isDisabled={isLoading}
|
||||
>
|
||||
{i18n._(t`Delete`)}
|
||||
</DeleteButton>
|
||||
)}
|
||||
</CardActionsRow>
|
||||
{/* Update delete modal to show dependencies https://github.com/ansible/awx/issues/5546 */}
|
||||
{error && (
|
||||
|
||||
@ -9,7 +9,12 @@ import { ProjectsAPI } from '../../../api';
|
||||
import ProjectDetail from './ProjectDetail';
|
||||
|
||||
jest.mock('../../../api');
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useRouteMatch: () => ({
|
||||
url: '/projects/1/details',
|
||||
}),
|
||||
}));
|
||||
describe('<ProjectDetail />', () => {
|
||||
const mockProject = {
|
||||
id: 1,
|
||||
@ -139,13 +144,19 @@ describe('<ProjectDetail />', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('should show edit button for users with edit permission', async () => {
|
||||
test('should show edit and sync button for users with edit permission', async () => {
|
||||
const wrapper = mountWithContexts(<ProjectDetail project={mockProject} />);
|
||||
const editButton = await waitForElement(
|
||||
wrapper,
|
||||
'ProjectDetail Button[aria-label="edit"]'
|
||||
);
|
||||
|
||||
const syncButton = await waitForElement(
|
||||
wrapper,
|
||||
'ProjectDetail Button[aria-label="Sync Project"]'
|
||||
);
|
||||
expect(editButton.text()).toEqual('Edit');
|
||||
expect(syncButton.text()).toEqual('Sync');
|
||||
expect(editButton.prop('to')).toBe(`/projects/${mockProject.id}/edit`);
|
||||
});
|
||||
|
||||
@ -166,6 +177,9 @@ describe('<ProjectDetail />', () => {
|
||||
expect(wrapper.find('ProjectDetail Button[aria-label="edit"]').length).toBe(
|
||||
0
|
||||
);
|
||||
expect(wrapper.find('ProjectDetail Button[aria-label="sync"]').length).toBe(
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
test('edit button should navigate to project edit', () => {
|
||||
@ -180,6 +194,17 @@ describe('<ProjectDetail />', () => {
|
||||
expect(history.location.pathname).toEqual('/projects/1/edit');
|
||||
});
|
||||
|
||||
test('sync button should call api to syn project', async () => {
|
||||
ProjectsAPI.readSync.mockResolvedValue({ data: { can_update: true } });
|
||||
const wrapper = mountWithContexts(<ProjectDetail project={mockProject} />);
|
||||
await act(() =>
|
||||
wrapper
|
||||
.find('ProjectDetail Button[aria-label="Sync Project"]')
|
||||
.prop('onClick')(1)
|
||||
);
|
||||
expect(ProjectsAPI.sync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('expected api calls are made for delete', async () => {
|
||||
const wrapper = mountWithContexts(<ProjectDetail project={mockProject} />);
|
||||
await waitForElement(wrapper, 'ProjectDetail Button[aria-label="Delete"]');
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { PencilAltIcon, SyncIcon } from '@patternfly/react-icons';
|
||||
import { PencilAltIcon } from '@patternfly/react-icons';
|
||||
import styled from 'styled-components';
|
||||
import { formatDateString, timeOfDay } from '../../../util/dates';
|
||||
import { ProjectsAPI } from '../../../api';
|
||||
@ -153,23 +153,10 @@ function ProjectListItem({
|
||||
aria-labelledby={labelId}
|
||||
id={labelId}
|
||||
>
|
||||
{project.summary_fields.user_capabilities.start ? (
|
||||
{project.summary_fields.user_capabilities.start && (
|
||||
<Tooltip content={i18n._(t`Sync Project`)} position="top">
|
||||
<ProjectSyncButton projectId={project.id}>
|
||||
{handleSync => (
|
||||
<Button
|
||||
isDisabled={isDisabled}
|
||||
aria-label={i18n._(t`Sync Project`)}
|
||||
variant="plain"
|
||||
onClick={handleSync}
|
||||
>
|
||||
<SyncIcon />
|
||||
</Button>
|
||||
)}
|
||||
</ProjectSyncButton>
|
||||
<ProjectSyncButton projectId={project.id} />
|
||||
</Tooltip>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
{project.summary_fields.user_capabilities.edit ? (
|
||||
<Tooltip content={i18n._(t`Edit Project`)} position="top">
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useRouteMatch } from 'react-router-dom';
|
||||
import { Button } from '@patternfly/react-core';
|
||||
import { SyncIcon } from '@patternfly/react-icons';
|
||||
|
||||
import { number } from 'prop-types';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
@ -8,28 +12,27 @@ import AlertModal from '../../../components/AlertModal';
|
||||
import ErrorDetail from '../../../components/ErrorDetail';
|
||||
import { ProjectsAPI } from '../../../api';
|
||||
|
||||
function ProjectSyncButton({ i18n, children, projectId }) {
|
||||
function ProjectSyncButton({ i18n, projectId }) {
|
||||
const match = useRouteMatch();
|
||||
|
||||
const { request: handleSync, error: syncError } = useRequest(
|
||||
useCallback(async () => {
|
||||
const { data } = await ProjectsAPI.readSync(projectId);
|
||||
if (data.can_update) {
|
||||
await ProjectsAPI.sync(projectId);
|
||||
} else {
|
||||
throw new Error(
|
||||
i18n._(
|
||||
t`You don't have the necessary permissions to sync this project.`
|
||||
)
|
||||
);
|
||||
}
|
||||
}, [i18n, projectId]),
|
||||
await ProjectsAPI.sync(projectId);
|
||||
}, [projectId]),
|
||||
null
|
||||
);
|
||||
|
||||
const { error, dismissError } = useDismissableError(syncError);
|
||||
|
||||
const isDetailsView = match.url.endsWith('/details');
|
||||
return (
|
||||
<>
|
||||
{children(handleSync)}
|
||||
<Button
|
||||
aria-label={i18n._(t`Sync Project`)}
|
||||
variant={isDetailsView ? 'secondary' : 'plain'}
|
||||
onClick={handleSync}
|
||||
>
|
||||
{match.url.endsWith('/details') ? i18n._(t`Sync`) : <SyncIcon />}
|
||||
</Button>
|
||||
{error && (
|
||||
<AlertModal
|
||||
isOpen={error}
|
||||
|
||||
@ -10,11 +10,6 @@ jest.mock('../../../api');
|
||||
|
||||
describe('ProjectSyncButton', () => {
|
||||
let wrapper;
|
||||
ProjectsAPI.readSync.mockResolvedValue({
|
||||
data: {
|
||||
can_update: true,
|
||||
},
|
||||
});
|
||||
|
||||
const children = handleSync => (
|
||||
<button type="submit" onClick={() => handleSync()} />
|
||||
@ -43,8 +38,7 @@ describe('ProjectSyncButton', () => {
|
||||
await act(async () => {
|
||||
button.prop('onClick')();
|
||||
});
|
||||
expect(ProjectsAPI.readSync).toHaveBeenCalledWith(1);
|
||||
await sleep(0);
|
||||
|
||||
expect(ProjectsAPI.sync).toHaveBeenCalledWith(1);
|
||||
});
|
||||
test('displays error modal after unsuccessful sync', async () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user