mirror of
https://github.com/ansible/awx.git
synced 2026-05-11 11:27:36 -02:30
Adds ability to get latest revision from project list when a project is done syncing. Automatically refresh project data on project detail when it's done syncing.
This commit is contained in:
@@ -1,8 +1,6 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { Button } from '@patternfly/react-core';
|
import { Button } from '@patternfly/react-core';
|
||||||
import { CopyIcon } from '@patternfly/react-icons';
|
import { CopyIcon } from '@patternfly/react-icons';
|
||||||
import useRequest, { useDismissableError } from '../../util/useRequest';
|
import useRequest, { useDismissableError } from '../../util/useRequest';
|
||||||
@@ -16,7 +14,6 @@ function CopyButton({
|
|||||||
onCopyStart,
|
onCopyStart,
|
||||||
onCopyFinish,
|
onCopyFinish,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
|
|
||||||
ouiaId,
|
ouiaId,
|
||||||
}) {
|
}) {
|
||||||
const { isLoading, error: copyError, request: copyItemToAPI } = useRequest(
|
const { isLoading, error: copyError, request: copyItemToAPI } = useRequest(
|
||||||
@@ -44,16 +41,18 @@ function CopyButton({
|
|||||||
>
|
>
|
||||||
<CopyIcon />
|
<CopyIcon />
|
||||||
</Button>
|
</Button>
|
||||||
<AlertModal
|
{error && (
|
||||||
aria-label={t`Copy Error`}
|
<AlertModal
|
||||||
isOpen={error}
|
aria-label={t`Copy Error`}
|
||||||
variant="error"
|
isOpen={error}
|
||||||
title={t`Error!`}
|
variant="error"
|
||||||
onClose={dismissError}
|
title={t`Error!`}
|
||||||
>
|
onClose={dismissError}
|
||||||
{errorMessage}
|
>
|
||||||
<ErrorDetail error={error} />
|
{errorMessage}
|
||||||
</AlertModal>
|
<ErrorDetail error={error} />
|
||||||
|
</AlertModal>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import React, { Fragment, useCallback } from 'react';
|
import React, { Fragment, useCallback } from 'react';
|
||||||
import { Link, useHistory } from 'react-router-dom';
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Button, List, ListItem, Tooltip } from '@patternfly/react-core';
|
import styled from 'styled-components';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ClipboardCopy,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
Tooltip,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
import { Project } from '../../../types';
|
import { Project } from '../../../types';
|
||||||
import { Config } from '../../../contexts/Config';
|
import { Config } from '../../../contexts/Config';
|
||||||
|
|
||||||
import AlertModal from '../../../components/AlertModal';
|
import AlertModal from '../../../components/AlertModal';
|
||||||
import { CardBody, CardActionsRow } from '../../../components/Card';
|
import { CardBody, CardActionsRow } from '../../../components/Card';
|
||||||
import DeleteButton from '../../../components/DeleteButton';
|
import DeleteButton from '../../../components/DeleteButton';
|
||||||
@@ -27,6 +32,10 @@ import StatusLabel from '../../../components/StatusLabel';
|
|||||||
import { formatDateString } from '../../../util/dates';
|
import { formatDateString } from '../../../util/dates';
|
||||||
import useWsProject from './useWsProject';
|
import useWsProject from './useWsProject';
|
||||||
|
|
||||||
|
const Label = styled.span`
|
||||||
|
color: var(--pf-global--disabled-color--100);
|
||||||
|
`;
|
||||||
|
|
||||||
function ProjectDetail({ project }) {
|
function ProjectDetail({ project }) {
|
||||||
const {
|
const {
|
||||||
allow_override,
|
allow_override,
|
||||||
@@ -42,6 +51,7 @@ function ProjectDetail({ project }) {
|
|||||||
scm_delete_on_update,
|
scm_delete_on_update,
|
||||||
scm_track_submodules,
|
scm_track_submodules,
|
||||||
scm_refspec,
|
scm_refspec,
|
||||||
|
scm_revision,
|
||||||
scm_type,
|
scm_type,
|
||||||
scm_update_on_launch,
|
scm_update_on_launch,
|
||||||
scm_update_cache_timeout,
|
scm_update_cache_timeout,
|
||||||
@@ -146,6 +156,36 @@ function ProjectDetail({ project }) {
|
|||||||
label={t`Source Control Type`}
|
label={t`Source Control Type`}
|
||||||
value={scm_type === '' ? t`Manual` : toTitleCase(project.scm_type)}
|
value={scm_type === '' ? t`Manual` : toTitleCase(project.scm_type)}
|
||||||
/>
|
/>
|
||||||
|
<Detail
|
||||||
|
label={t`Source Control Revision`}
|
||||||
|
value={
|
||||||
|
scm_revision ? (
|
||||||
|
<ClipboardCopy
|
||||||
|
variant="inline-compact"
|
||||||
|
clickTip={t`Successfully copied to clipboard!`}
|
||||||
|
hoverTip={t`Copy full revision to clipboard.`}
|
||||||
|
onCopy={event => {
|
||||||
|
const clipboard = event.currentTarget.parentElement;
|
||||||
|
const el = document.createElement('textarea');
|
||||||
|
el.value = scm_revision.toString();
|
||||||
|
clipboard.appendChild(el);
|
||||||
|
el.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
clipboard.removeChild(el);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{scm_revision.substring(0, 7)}
|
||||||
|
</ClipboardCopy>
|
||||||
|
) : (
|
||||||
|
<Label
|
||||||
|
aria-label={t`The project must be synced before a revision is available.`}
|
||||||
|
>
|
||||||
|
{t`Sync for revision`}
|
||||||
|
</Label>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
alwaysVisible
|
||||||
|
/>
|
||||||
<Detail label={t`Source Control URL`} value={scm_url} />
|
<Detail label={t`Source Control URL`} value={scm_url} />
|
||||||
<Detail label={t`Source Control Branch`} value={scm_branch} />
|
<Detail label={t`Source Control Branch`} value={scm_branch} />
|
||||||
<Detail label={t`Source Control Refspec`} value={scm_refspec} />
|
<Detail label={t`Source Control Refspec`} value={scm_refspec} />
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import useWebsocket from '../../../util/useWebsocket';
|
import useWebsocket from '../../../util/useWebsocket';
|
||||||
|
import { ProjectsAPI } from '../../../api';
|
||||||
|
|
||||||
export default function useWsProjects(initialProject) {
|
export default function useWsProjects(initialProject) {
|
||||||
const [project, setProject] = useState(initialProject);
|
const [project, setProject] = useState(initialProject);
|
||||||
@@ -8,6 +9,11 @@ export default function useWsProjects(initialProject) {
|
|||||||
control: ['limit_reached_1'],
|
control: ['limit_reached_1'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const refreshProject = async () => {
|
||||||
|
const { data } = await ProjectsAPI.readDetail(project.id);
|
||||||
|
setProject(data);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setProject(initialProject);
|
setProject(initialProject);
|
||||||
}, [initialProject]);
|
}, [initialProject]);
|
||||||
@@ -22,6 +28,11 @@ export default function useWsProjects(initialProject) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (lastMessage.finished) {
|
||||||
|
refreshProject();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const updatedProject = {
|
const updatedProject = {
|
||||||
...project,
|
...project,
|
||||||
summary_fields: {
|
summary_fields: {
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ import React from 'react';
|
|||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
import WS from 'jest-websocket-mock';
|
import WS from 'jest-websocket-mock';
|
||||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
|
import { ProjectsAPI } from '../../../api';
|
||||||
import useWsProject from './useWsProject';
|
import useWsProject from './useWsProject';
|
||||||
|
|
||||||
|
jest.mock('../../../api/models/Projects');
|
||||||
|
|
||||||
function TestInner() {
|
function TestInner() {
|
||||||
return <div />;
|
return <div />;
|
||||||
}
|
}
|
||||||
@@ -15,13 +18,30 @@ function Test({ project }) {
|
|||||||
describe('useWsProject', () => {
|
describe('useWsProject', () => {
|
||||||
let debug;
|
let debug;
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
debug = global.console.debug; // eslint-disable-line prefer-destructuring
|
debug = global.console.debug; // eslint-disable-line prefer-destructuring
|
||||||
global.console.debug = () => {};
|
global.console.debug = () => {};
|
||||||
|
ProjectsAPI.readDetail.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
id: 1,
|
||||||
|
summary_fields: {
|
||||||
|
last_job: {
|
||||||
|
id: 19,
|
||||||
|
name: 'Test Project',
|
||||||
|
description: '',
|
||||||
|
finished: '2021-06-01T18:43:53.332201Z',
|
||||||
|
status: 'successful',
|
||||||
|
failed: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
global.console.debug = debug;
|
global.console.debug = debug;
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return project detail', async () => {
|
test('should return project detail', async () => {
|
||||||
@@ -127,14 +147,20 @@ describe('useWsProject', () => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
|
|
||||||
|
expect(ProjectsAPI.readDetail).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
wrapper.find('TestInner').prop('project').summary_fields.current_job
|
wrapper.find('TestInner').prop('project').summary_fields.last_job
|
||||||
).toEqual({
|
).toEqual({
|
||||||
id: 2,
|
id: 19,
|
||||||
|
name: 'Test Project',
|
||||||
|
description: '',
|
||||||
|
finished: '2021-06-01T18:43:53.332201Z',
|
||||||
status: 'successful',
|
status: 'successful',
|
||||||
finished: '2020-07-02T16:28:31.839071Z',
|
failed: false,
|
||||||
});
|
});
|
||||||
WS.clean();
|
WS.clean();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import React, { useEffect, useCallback } from 'react';
|
|||||||
import { useLocation, useRouteMatch } from 'react-router-dom';
|
import { useLocation, useRouteMatch } from 'react-router-dom';
|
||||||
import { t, Plural } from '@lingui/macro';
|
import { t, Plural } from '@lingui/macro';
|
||||||
import { Card, PageSection } from '@patternfly/react-core';
|
import { Card, PageSection } from '@patternfly/react-core';
|
||||||
|
|
||||||
import { ProjectsAPI } from '../../../api';
|
import { ProjectsAPI } from '../../../api';
|
||||||
import useRequest, { useDeleteItems } from '../../../util/useRequest';
|
import useRequest, {
|
||||||
|
useDeleteItems,
|
||||||
|
useDismissableError,
|
||||||
|
} from '../../../util/useRequest';
|
||||||
import AlertModal from '../../../components/AlertModal';
|
import AlertModal from '../../../components/AlertModal';
|
||||||
import DataListToolbar from '../../../components/DataListToolbar';
|
import DataListToolbar from '../../../components/DataListToolbar';
|
||||||
import ErrorDetail from '../../../components/ErrorDetail';
|
import ErrorDetail from '../../../components/ErrorDetail';
|
||||||
@@ -33,6 +35,21 @@ function ProjectList() {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const match = useRouteMatch();
|
const match = useRouteMatch();
|
||||||
|
|
||||||
|
const {
|
||||||
|
request: fetchUpdatedProject,
|
||||||
|
error: fetchUpdatedProjectError,
|
||||||
|
result: updatedProject,
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(async projectId => {
|
||||||
|
if (!projectId) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const { data } = await ProjectsAPI.readDetail(projectId);
|
||||||
|
return data;
|
||||||
|
}, []),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
result: {
|
result: {
|
||||||
results,
|
results,
|
||||||
@@ -43,7 +60,8 @@ function ProjectList() {
|
|||||||
},
|
},
|
||||||
error: contentError,
|
error: contentError,
|
||||||
isLoading,
|
isLoading,
|
||||||
request: fetchProjects,
|
request: fetchUpdatedProjects,
|
||||||
|
setValue: setProjects,
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const params = parseQueryString(QS_CONFIG, location.search);
|
const params = parseQueryString(QS_CONFIG, location.search);
|
||||||
@@ -73,8 +91,8 @@ function ProjectList() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchProjects();
|
fetchUpdatedProjects();
|
||||||
}, [fetchProjects]);
|
}, [fetchUpdatedProjects]);
|
||||||
|
|
||||||
const projects = useWsProjects(results);
|
const projects = useWsProjects(results);
|
||||||
|
|
||||||
@@ -99,7 +117,7 @@ function ProjectList() {
|
|||||||
{
|
{
|
||||||
qsConfig: QS_CONFIG,
|
qsConfig: QS_CONFIG,
|
||||||
allItemsSelected: isAllSelected,
|
allItemsSelected: isAllSelected,
|
||||||
fetchItems: fetchProjects,
|
fetchItems: fetchUpdatedProjects,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -115,6 +133,27 @@ function ProjectList() {
|
|||||||
selected[0]
|
selected[0]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (updatedProject) {
|
||||||
|
const updatedProjects = projects.map(project =>
|
||||||
|
project.id === updatedProject.id ? updatedProject : project
|
||||||
|
);
|
||||||
|
setProjects({
|
||||||
|
results: updatedProjects,
|
||||||
|
itemCount,
|
||||||
|
actions,
|
||||||
|
relatedSearchableKeys,
|
||||||
|
searchableKeys,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||||
|
}, [updatedProject]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
error: projectError,
|
||||||
|
dismissError: dismissProjectError,
|
||||||
|
} = useDismissableError(fetchUpdatedProjectError);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageSection>
|
<PageSection>
|
||||||
@@ -207,13 +246,14 @@ function ProjectList() {
|
|||||||
)}
|
)}
|
||||||
renderRow={(project, index) => (
|
renderRow={(project, index) => (
|
||||||
<ProjectListItem
|
<ProjectListItem
|
||||||
fetchProjects={fetchProjects}
|
fetchUpdatedProjects={fetchUpdatedProjects}
|
||||||
key={project.id}
|
key={project.id}
|
||||||
project={project}
|
project={project}
|
||||||
detailUrl={`${match.url}/${project.id}`}
|
detailUrl={`${match.url}/${project.id}`}
|
||||||
isSelected={selected.some(row => row.id === project.id)}
|
isSelected={selected.some(row => row.id === project.id)}
|
||||||
onSelect={() => handleSelect(project)}
|
onSelect={() => handleSelect(project)}
|
||||||
rowIndex={index}
|
rowIndex={index}
|
||||||
|
onRefreshRow={projectId => fetchUpdatedProject(projectId)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
emptyStateControls={
|
emptyStateControls={
|
||||||
@@ -224,16 +264,30 @@ function ProjectList() {
|
|||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
<AlertModal
|
{deletionError && (
|
||||||
isOpen={deletionError}
|
<AlertModal
|
||||||
variant="error"
|
isOpen={deletionError}
|
||||||
aria-label={t`Deletion Error`}
|
variant="error"
|
||||||
title={t`Error!`}
|
aria-label={t`Deletion Error`}
|
||||||
onClose={clearDeletionError}
|
title={t`Error!`}
|
||||||
>
|
onClose={clearDeletionError}
|
||||||
{t`Failed to delete one or more projects.`}
|
>
|
||||||
<ErrorDetail error={deletionError} />
|
{t`Failed to delete one or more projects.`}
|
||||||
</AlertModal>
|
<ErrorDetail error={deletionError} />
|
||||||
|
</AlertModal>
|
||||||
|
)}
|
||||||
|
{projectError && (
|
||||||
|
<AlertModal
|
||||||
|
isOpen={projectError}
|
||||||
|
variant="error"
|
||||||
|
aria-label={t`Error fetching updated project`}
|
||||||
|
title={t`Error!`}
|
||||||
|
onClose={dismissProjectError}
|
||||||
|
>
|
||||||
|
{t`Failed to fetch the updated project data.`}
|
||||||
|
<ErrorDetail error={projectError} />
|
||||||
|
</AlertModal>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
import 'styled-components/macro';
|
import 'styled-components/macro';
|
||||||
import React, { Fragment, useState, useCallback } from 'react';
|
import React, { Fragment, useState, useCallback } from 'react';
|
||||||
import { string, bool, func } from 'prop-types';
|
import { string, bool, func } from 'prop-types';
|
||||||
|
import { Button, ClipboardCopy, Tooltip } from '@patternfly/react-core';
|
||||||
import { Button, Tooltip } from '@patternfly/react-core';
|
|
||||||
import { Tr, Td, ExpandableRowContent } from '@patternfly/react-table';
|
import { Tr, Td, ExpandableRowContent } from '@patternfly/react-table';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
PencilAltIcon,
|
PencilAltIcon,
|
||||||
ExclamationTriangleIcon as PFExclamationTriangleIcon,
|
ExclamationTriangleIcon as PFExclamationTriangleIcon,
|
||||||
|
UndoIcon,
|
||||||
} from '@patternfly/react-icons';
|
} from '@patternfly/react-icons';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { ActionsTd, ActionItem } from '../../../components/PaginatedTable';
|
import { ActionsTd, ActionItem } from '../../../components/PaginatedTable';
|
||||||
import { formatDateString, timeOfDay } from '../../../util/dates';
|
import { formatDateString, timeOfDay } from '../../../util/dates';
|
||||||
import { ProjectsAPI } from '../../../api';
|
import { ProjectsAPI } from '../../../api';
|
||||||
import ClipboardCopyButton from '../../../components/ClipboardCopyButton';
|
|
||||||
import {
|
import {
|
||||||
DetailList,
|
DetailList,
|
||||||
Detail,
|
Detail,
|
||||||
@@ -23,6 +22,7 @@ import {
|
|||||||
import ExecutionEnvironmentDetail from '../../../components/ExecutionEnvironmentDetail';
|
import ExecutionEnvironmentDetail from '../../../components/ExecutionEnvironmentDetail';
|
||||||
import StatusLabel from '../../../components/StatusLabel';
|
import StatusLabel from '../../../components/StatusLabel';
|
||||||
import { toTitleCase } from '../../../util/strings';
|
import { toTitleCase } from '../../../util/strings';
|
||||||
|
import { isJobRunning } from '../../../util/jobs';
|
||||||
import CopyButton from '../../../components/CopyButton';
|
import CopyButton from '../../../components/CopyButton';
|
||||||
import ProjectSyncButton from '../shared/ProjectSyncButton';
|
import ProjectSyncButton from '../shared/ProjectSyncButton';
|
||||||
import { Project } from '../../../types';
|
import { Project } from '../../../types';
|
||||||
@@ -44,6 +44,7 @@ function ProjectListItem({
|
|||||||
detailUrl,
|
detailUrl,
|
||||||
fetchProjects,
|
fetchProjects,
|
||||||
rowIndex,
|
rowIndex,
|
||||||
|
onRefreshRow,
|
||||||
}) {
|
}) {
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
const [isDisabled, setIsDisabled] = useState(false);
|
const [isDisabled, setIsDisabled] = useState(false);
|
||||||
@@ -88,6 +89,68 @@ function ProjectListItem({
|
|||||||
setIsDisabled(false);
|
setIsDisabled(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const renderRevision = () => {
|
||||||
|
if (!project.summary_fields?.current_job || project.scm_revision) {
|
||||||
|
return project.scm_revision ? (
|
||||||
|
<ClipboardCopy
|
||||||
|
dataCy={`project-copy-revision-${project.id}`}
|
||||||
|
variant="inline-compact"
|
||||||
|
clickTip={t`Successfully copied to clipboard!`}
|
||||||
|
hoverTip={t`Copy full revision to clipboard.`}
|
||||||
|
onCopy={event => {
|
||||||
|
const clipboard = event.currentTarget.parentElement;
|
||||||
|
const el = document.createElement('textarea');
|
||||||
|
el.value = project.scm_revision.toString();
|
||||||
|
clipboard.appendChild(el);
|
||||||
|
el.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
clipboard.removeChild(el);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{project.scm_revision.substring(0, 7)}
|
||||||
|
</ClipboardCopy>
|
||||||
|
) : (
|
||||||
|
<Label
|
||||||
|
aria-label={t`The project must be synced before a revision is available.`}
|
||||||
|
>
|
||||||
|
{t`Sync for revision`}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isJobRunning(project.summary_fields.current_job.status) &&
|
||||||
|
!project.scm_revision
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
aria-label={t`The project is currently syncing and the revision will be available after the sync is complete.`}
|
||||||
|
>
|
||||||
|
{t`Syncing`}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Label
|
||||||
|
aria-label={t`The project revision is currently out of date. Please refresh to fetch the most recent revision.`}
|
||||||
|
>
|
||||||
|
{t`Refresh for revision`}
|
||||||
|
</Label>
|
||||||
|
<Tooltip content={t`Refresh project revision`}>
|
||||||
|
<Button
|
||||||
|
ouiaId={`project-refresh-revision-${project.id}`}
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => onRefreshRow(project.id)}
|
||||||
|
>
|
||||||
|
<UndoIcon />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const labelId = `check-action-${project.id}`;
|
const labelId = `check-action-${project.id}`;
|
||||||
|
|
||||||
const missingExecutionEnvironment =
|
const missingExecutionEnvironment =
|
||||||
@@ -153,21 +216,7 @@ function ProjectListItem({
|
|||||||
<Td dataLabel={t`Type`}>
|
<Td dataLabel={t`Type`}>
|
||||||
{project.scm_type === '' ? t`Manual` : toTitleCase(project.scm_type)}
|
{project.scm_type === '' ? t`Manual` : toTitleCase(project.scm_type)}
|
||||||
</Td>
|
</Td>
|
||||||
<Td dataLabel={t`Revision`}>
|
<Td dataLabel={t`Revision`}>{renderRevision()}</Td>
|
||||||
{project.scm_revision.substring(0, 7)}
|
|
||||||
{!project.scm_revision && (
|
|
||||||
<Label aria-label={t`copy to clipboard disabled`}>
|
|
||||||
{t`Sync for revision`}
|
|
||||||
</Label>
|
|
||||||
)}
|
|
||||||
<ClipboardCopyButton
|
|
||||||
isDisabled={!project.scm_revision}
|
|
||||||
stringToCopy={project.scm_revision}
|
|
||||||
copyTip={t`Copy full revision to clipboard.`}
|
|
||||||
copiedSuccessTip={t`Successfully copied to clipboard!`}
|
|
||||||
ouiaId="copy-revision-button"
|
|
||||||
/>
|
|
||||||
</Td>
|
|
||||||
<ActionsTd dataLabel={t`Actions`}>
|
<ActionsTd dataLabel={t`Actions`}>
|
||||||
{['running', 'pending', 'waiting'].includes(job?.status) ? (
|
{['running', 'pending', 'waiting'].includes(job?.status) ? (
|
||||||
<ActionItem
|
<ActionItem
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ describe('<ProjectsListItem />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should render proper alert modal on copy error', async () => {
|
test('should render proper alert modal on copy error', async () => {
|
||||||
ProjectsAPI.copy.mockRejectedValue(new Error());
|
ProjectsAPI.copy.mockRejectedValue(new Error('This is an error'));
|
||||||
|
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<table>
|
<table>
|
||||||
@@ -285,7 +285,7 @@ describe('<ProjectsListItem />', () => {
|
|||||||
);
|
);
|
||||||
expect(wrapper.find('CopyButton').length).toBe(0);
|
expect(wrapper.find('CopyButton').length).toBe(0);
|
||||||
});
|
});
|
||||||
test('should render disabled copy to clipboard button', () => {
|
test('should render proper revision text when project has not been synced', () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -314,10 +314,82 @@ describe('<ProjectsListItem />', () => {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
);
|
);
|
||||||
expect(
|
expect(wrapper.find('ClipboardCopy').length).toBe(0);
|
||||||
wrapper.find('span[aria-label="copy to clipboard disabled"]').text()
|
expect(wrapper.find('td[data-label="Revision"]').text()).toBe(
|
||||||
).toBe('Sync for revision');
|
'Sync for revision'
|
||||||
expect(wrapper.find('ClipboardCopyButton').prop('isDisabled')).toBe(true);
|
);
|
||||||
|
});
|
||||||
|
test('should render the clipboard copy with the right text when scm revision available', () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<ProjectsListItem
|
||||||
|
isSelected={false}
|
||||||
|
detailUrl="/project/1"
|
||||||
|
onSelect={() => {}}
|
||||||
|
project={{
|
||||||
|
id: 1,
|
||||||
|
name: 'Project 1',
|
||||||
|
url: '/api/v2/projects/1',
|
||||||
|
type: 'project',
|
||||||
|
scm_type: 'git',
|
||||||
|
scm_revision: 'osofej904r09a9sf0udfsajogsdfbh4e23489adf',
|
||||||
|
summary_fields: {
|
||||||
|
last_job: {
|
||||||
|
id: 9000,
|
||||||
|
status: 'successful',
|
||||||
|
},
|
||||||
|
user_capabilities: {
|
||||||
|
edit: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
expect(wrapper.find('ClipboardCopy').length).toBe(1);
|
||||||
|
expect(wrapper.find('ClipboardCopy').text()).toBe('osofej9');
|
||||||
|
});
|
||||||
|
test('should indicate that the revision needs to be refreshed when project sync is done', () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<ProjectsListItem
|
||||||
|
isSelected={false}
|
||||||
|
detailUrl="/project/1"
|
||||||
|
onSelect={() => {}}
|
||||||
|
project={{
|
||||||
|
id: 1,
|
||||||
|
name: 'Project 1',
|
||||||
|
url: '/api/v2/projects/1',
|
||||||
|
type: 'project',
|
||||||
|
scm_type: 'git',
|
||||||
|
scm_revision: null,
|
||||||
|
summary_fields: {
|
||||||
|
current_job: {
|
||||||
|
id: 9001,
|
||||||
|
status: 'successful',
|
||||||
|
finished: '2021-06-01T18:43:53.332201Z',
|
||||||
|
},
|
||||||
|
last_job: {
|
||||||
|
id: 9000,
|
||||||
|
status: 'successful',
|
||||||
|
},
|
||||||
|
user_capabilities: {
|
||||||
|
edit: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
expect(wrapper.find('ClipboardCopy').length).toBe(0);
|
||||||
|
expect(wrapper.find('td[data-label="Revision"]').text()).toBe(
|
||||||
|
'Refresh for revision'
|
||||||
|
);
|
||||||
|
expect(wrapper.find('UndoIcon').length).toBe(1);
|
||||||
});
|
});
|
||||||
test('should render expected details in expanded section', async () => {
|
test('should render expected details in expanded section', async () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ export default function useWsProjects(initialProjects) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (lastMessage.finished) {
|
||||||
|
updatedProject.scm_revision = null;
|
||||||
|
}
|
||||||
|
|
||||||
setProjects([
|
setProjects([
|
||||||
...projects.slice(0, index),
|
...projects.slice(0, index),
|
||||||
updatedProject,
|
updatedProject,
|
||||||
|
|||||||
Reference in New Issue
Block a user