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