Merge pull request #6115 from jlmitch5/scheduleListInDetailViews

add schedule list to detail views

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot]
2020-03-02 16:31:03 +00:00
committed by GitHub
17 changed files with 200 additions and 91 deletions

View File

@@ -61,6 +61,10 @@ class JobTemplates extends InstanceGroupsMixin(NotificationsMixin(Base)) {
params, params,
}); });
} }
readScheduleList(id, params) {
return this.http.get(`${this.baseUrl}${id}/schedules/`, { params });
}
} }
export default JobTemplates; export default JobTemplates;

View File

@@ -21,6 +21,10 @@ class Projects extends LaunchUpdateMixin(NotificationsMixin(Base)) {
return this.http.get(`${this.baseUrl}${id}/playbooks/`); return this.http.get(`${this.baseUrl}${id}/playbooks/`);
} }
readScheduleList(id, params) {
return this.http.get(`${this.baseUrl}${id}/schedules/`, { params });
}
readSync(id) { readSync(id) {
return this.http.get(`${this.baseUrl}${id}/update/`); return this.http.get(`${this.baseUrl}${id}/update/`);
} }

View File

@@ -27,6 +27,10 @@ class WorkflowJobTemplates extends Base {
createNode(id, data) { createNode(id, data) {
return this.http.post(`${this.baseUrl}${id}/workflow_nodes/`, data); return this.http.post(`${this.baseUrl}${id}/workflow_nodes/`, data);
} }
readScheduleList(id, params) {
return this.http.get(`${this.baseUrl}${id}/schedules/`, { params });
}
} }
export default WorkflowJobTemplates; export default WorkflowJobTemplates;

View File

@@ -3,7 +3,6 @@ import { useLocation } from 'react-router-dom';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { SchedulesAPI } from '@api'; import { SchedulesAPI } from '@api';
import { Card, PageSection } from '@patternfly/react-core';
import AlertModal from '@components/AlertModal'; import AlertModal from '@components/AlertModal';
import ErrorDetail from '@components/ErrorDetail'; import ErrorDetail from '@components/ErrorDetail';
import DataListToolbar from '@components/DataListToolbar'; import DataListToolbar from '@components/DataListToolbar';
@@ -12,7 +11,7 @@ import PaginatedDataList, {
} from '@components/PaginatedDataList'; } from '@components/PaginatedDataList';
import useRequest, { useDeleteItems } from '@util/useRequest'; import useRequest, { useDeleteItems } from '@util/useRequest';
import { getQSConfig, parseQueryString } from '@util/qs'; import { getQSConfig, parseQueryString } from '@util/qs';
import { ScheduleListItem } from '.'; import ScheduleListItem from './ScheduleListItem';
const QS_CONFIG = getQSConfig('schedule', { const QS_CONFIG = getQSConfig('schedule', {
page: 1, page: 1,
@@ -20,7 +19,7 @@ const QS_CONFIG = getQSConfig('schedule', {
order_by: 'unified_job_template__polymorphic_ctype__model', order_by: 'unified_job_template__polymorphic_ctype__model',
}); });
function ScheduleList({ i18n }) { function ScheduleList({ i18n, loadSchedules }) {
const [selected, setSelected] = useState([]); const [selected, setSelected] = useState([]);
const location = useLocation(); const location = useLocation();
@@ -33,14 +32,12 @@ function ScheduleList({ i18n }) {
} = useRequest( } = useRequest(
useCallback(async () => { useCallback(async () => {
const params = parseQueryString(QS_CONFIG, location.search); const params = parseQueryString(QS_CONFIG, location.search);
const response = loadSchedules(params);
const { const {
data: { count, results }, data: { count, results },
} = await SchedulesAPI.read(params); } = await response;
return { return { itemCount: count, schedules: results };
itemCount: count, }, [location, loadSchedules]),
schedules: results,
};
}, [location]),
{ {
schedules: [], schedules: [],
itemCount: 0, itemCount: 0,
@@ -88,63 +85,61 @@ function ScheduleList({ i18n }) {
}; };
return ( return (
<PageSection> <>
<Card> <PaginatedDataList
<PaginatedDataList contentError={contentError}
contentError={contentError} hasContentLoading={isLoading || isDeleteLoading}
hasContentLoading={isLoading || isDeleteLoading} items={schedules}
items={schedules} itemCount={itemCount}
itemCount={itemCount} qsConfig={QS_CONFIG}
qsConfig={QS_CONFIG} onRowClick={handleSelect}
onRowClick={handleSelect} renderItem={item => (
renderItem={item => ( <ScheduleListItem
<ScheduleListItem isSelected={selected.some(row => row.id === item.id)}
isSelected={selected.some(row => row.id === item.id)} key={item.id}
key={item.id} onSelect={() => handleSelect(item)}
onSelect={() => handleSelect(item)} schedule={item}
schedule={item} />
/> )}
)} toolbarSearchColumns={[
toolbarSearchColumns={[ {
{ name: i18n._(t`Name`),
name: i18n._(t`Name`), key: 'name',
key: 'name', isDefault: true,
isDefault: true, },
}, ]}
]} toolbarSortColumns={[
toolbarSortColumns={[ {
{ name: i18n._(t`Name`),
name: i18n._(t`Name`), key: 'name',
key: 'name', },
}, {
{ name: i18n._(t`Next Run`),
name: i18n._(t`Next Run`), key: 'next_run',
key: 'next_run', },
}, {
{ name: i18n._(t`Type`),
name: i18n._(t`Type`), key: 'unified_job_template__polymorphic_ctype__model',
key: 'unified_job_template__polymorphic_ctype__model', },
}, ]}
]} renderToolbar={props => (
renderToolbar={props => ( <DataListToolbar
<DataListToolbar {...props}
{...props} showSelectAll
showSelectAll isAllSelected={isAllSelected}
isAllSelected={isAllSelected} onSelectAll={handleSelectAll}
onSelectAll={handleSelectAll} qsConfig={QS_CONFIG}
qsConfig={QS_CONFIG} additionalControls={[
additionalControls={[ <ToolbarDeleteButton
<ToolbarDeleteButton key="delete"
key="delete" onDelete={handleDelete}
onDelete={handleDelete} itemsToDelete={selected}
itemsToDelete={selected} pluralizedItemName={i18n._(t`Schedules`)}
pluralizedItemName={i18n._(t`Schedules`)} />,
/>, ]}
]} />
/> )}
)} />
/>
</Card>
{deletionError && ( {deletionError && (
<AlertModal <AlertModal
isOpen={deletionError} isOpen={deletionError}
@@ -156,7 +151,7 @@ function ScheduleList({ i18n }) {
<ErrorDetail error={deletionError} /> <ErrorDetail error={deletionError} />
</AlertModal> </AlertModal>
)} )}
</PageSection> </>
); );
} }

View File

@@ -3,7 +3,7 @@ import { act } from 'react-dom/test-utils';
import { mountWithContexts } from '@testUtils/enzymeHelpers'; import { mountWithContexts } from '@testUtils/enzymeHelpers';
import { SchedulesAPI } from '@api'; import { SchedulesAPI } from '@api';
import ScheduleList from './ScheduleList'; import ScheduleList from './ScheduleList';
import mockSchedules from '../data.schedules.json'; import mockSchedules from './data.schedules.json';
jest.mock('@api/models/Schedules'); jest.mock('@api/models/Schedules');
@@ -22,8 +22,11 @@ describe('ScheduleList', () => {
describe('read call successful', () => { describe('read call successful', () => {
beforeAll(async () => { beforeAll(async () => {
SchedulesAPI.read.mockResolvedValue({ data: mockSchedules }); SchedulesAPI.read.mockResolvedValue({ data: mockSchedules });
const loadSchedules = params => SchedulesAPI.read(params);
await act(async () => { await act(async () => {
wrapper = mountWithContexts(<ScheduleList />); wrapper = mountWithContexts(
<ScheduleList loadSchedules={loadSchedules} />
);
}); });
wrapper.update(); wrapper.update();
}); });

View File

@@ -18,7 +18,7 @@ import { DetailList, Detail } from '@components/DetailList';
import styled from 'styled-components'; import styled from 'styled-components';
import { Schedule } from '@types'; import { Schedule } from '@types';
import { formatDateString } from '@util/dates'; import { formatDateString } from '@util/dates';
import ScheduleToggle from '../shared/ScheduleToggle'; import ScheduleToggle from './ScheduleToggle';
const DataListAction = styled(_DataListAction)` const DataListAction = styled(_DataListAction)`
align-items: center; align-items: center;

View File

@@ -0,0 +1 @@
export { default } from './ScheduleList';

View File

@@ -9,10 +9,10 @@ import RoutedTabs from '@components/RoutedTabs';
import ContentError from '@components/ContentError'; import ContentError from '@components/ContentError';
import NotificationList from '@components/NotificationList'; import NotificationList from '@components/NotificationList';
import { ResourceAccessList } from '@components/ResourceAccessList'; import { ResourceAccessList } from '@components/ResourceAccessList';
import ScheduleList from '@components/ScheduleList';
import ProjectDetail from './ProjectDetail'; import ProjectDetail from './ProjectDetail';
import ProjectEdit from './ProjectEdit'; import ProjectEdit from './ProjectEdit';
import ProjectJobTemplatesList from './ProjectJobTemplatesList'; import ProjectJobTemplatesList from './ProjectJobTemplatesList';
import ProjectSchedules from './ProjectSchedules';
import { OrganizationsAPI, ProjectsAPI } from '@api'; import { OrganizationsAPI, ProjectsAPI } from '@api';
class Project extends Component { class Project extends Component {
@@ -30,6 +30,7 @@ class Project extends Component {
}; };
this.loadProject = this.loadProject.bind(this); this.loadProject = this.loadProject.bind(this);
this.loadProjectAndRoles = this.loadProjectAndRoles.bind(this); this.loadProjectAndRoles = this.loadProjectAndRoles.bind(this);
this.loadSchedules = this.loadSchedules.bind(this);
} }
async componentDidMount() { async componentDidMount() {
@@ -103,6 +104,11 @@ class Project extends Component {
} }
} }
loadSchedules(params) {
const { project } = this.state;
return ProjectsAPI.readScheduleList(project.id, params);
}
render() { render() {
const { location, match, me, i18n } = this.props; const { location, match, me, i18n } = this.props;
@@ -134,16 +140,17 @@ class Project extends Component {
}); });
} }
tabsArray.push( tabsArray.push({
{ name: i18n._(t`Job Templates`),
name: i18n._(t`Job Templates`), link: `${match.url}/job_templates`,
link: `${match.url}/job_templates`, });
},
{ if (project?.scm_type) {
tabsArray.push({
name: i18n._(t`Schedules`), name: i18n._(t`Schedules`),
link: `${match.url}/schedules`, link: `${match.url}/schedules`,
} });
); }
tabsArray.forEach((tab, n) => { tabsArray.forEach((tab, n) => {
tab.id = n; tab.id = n;
@@ -230,10 +237,14 @@ class Project extends Component {
<ProjectJobTemplatesList id={Number(match.params.id)} /> <ProjectJobTemplatesList id={Number(match.params.id)} />
)} )}
/> />
<Route {project?.scm_type && project.scm_type !== '' && (
path="/projects/:id/schedules" <Route
render={() => <ProjectSchedules id={Number(match.params.id)} />} path="/projects/:id/schedules"
/> render={() => (
<ScheduleList loadSchedules={this.loadSchedules} />
)}
/>
)}
<Route <Route
key="not-found" key="not-found"
path="*" path="*"

View File

@@ -68,6 +68,49 @@ describe('<Project />', () => {
done(); done();
}); });
test('schedules tab shown for scm based projects.', async done => {
ProjectsAPI.readDetail.mockResolvedValue({ data: mockDetails });
OrganizationsAPI.read.mockResolvedValue({
count: 0,
next: null,
previous: null,
data: { results: [] },
});
const wrapper = mountWithContexts(
<Project setBreadcrumb={() => {}} me={mockMe} />
);
const tabs = await waitForElement(
wrapper,
'.pf-c-tabs__item',
el => el.length === 4
);
expect(tabs.at(3).text()).toEqual('Schedules');
done();
});
test('schedules tab hidden for manual projects.', async done => {
const manualDetails = Object.assign(mockDetails, { scm_type: '' });
ProjectsAPI.readDetail.mockResolvedValue({ data: manualDetails });
OrganizationsAPI.read.mockResolvedValue({
count: 0,
next: null,
previous: null,
data: { results: [] },
});
const wrapper = mountWithContexts(
<Project setBreadcrumb={() => {}} me={mockMe} />
);
const tabs = await waitForElement(
wrapper,
'.pf-c-tabs__item',
el => el.length === 3
);
tabs.forEach(tab => expect(tab.text()).not.toEqual('Schedules'));
done();
});
test('should show content error when user attempts to navigate to erroneous route', async () => { test('should show content error when user attempts to navigate to erroneous route', async () => {
const history = createMemoryHistory({ const history = createMemoryHistory({
initialEntries: ['/projects/1/foobar'], initialEntries: ['/projects/1/foobar'],

View File

@@ -1,2 +0,0 @@
export { default as ScheduleList } from './ScheduleList';
export { default as ScheduleListItem } from './ScheduleListItem';

View File

@@ -4,9 +4,15 @@ import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import Breadcrumbs from '@components/Breadcrumbs'; import Breadcrumbs from '@components/Breadcrumbs';
import { ScheduleList } from './ScheduleList'; import ScheduleList from '@components/ScheduleList';
import { SchedulesAPI } from '@api';
import { PageSection, Card } from '@patternfly/react-core';
function Schedules({ i18n }) { function Schedules({ i18n }) {
const loadSchedules = params => {
return SchedulesAPI.read(params);
};
return ( return (
<> <>
<Breadcrumbs <Breadcrumbs
@@ -16,7 +22,11 @@ function Schedules({ i18n }) {
/> />
<Switch> <Switch>
<Route path="/schedules"> <Route path="/schedules">
<ScheduleList /> <PageSection>
<Card>
<ScheduleList loadSchedules={loadSchedules} />
</Card>
</PageSection>
</Route> </Route>
</Switch> </Switch>
</> </>

View File

@@ -10,6 +10,7 @@ import ContentError from '@components/ContentError';
import JobList from '@components/JobList'; import JobList from '@components/JobList';
import NotificationList from '@components/NotificationList'; import NotificationList from '@components/NotificationList';
import RoutedTabs from '@components/RoutedTabs'; import RoutedTabs from '@components/RoutedTabs';
import ScheduleList from '@components/ScheduleList';
import { ResourceAccessList } from '@components/ResourceAccessList'; import { ResourceAccessList } from '@components/ResourceAccessList';
import JobTemplateDetail from './JobTemplateDetail'; import JobTemplateDetail from './JobTemplateDetail';
import JobTemplateEdit from './JobTemplateEdit'; import JobTemplateEdit from './JobTemplateEdit';
@@ -27,6 +28,7 @@ class Template extends Component {
}; };
this.loadTemplate = this.loadTemplate.bind(this); this.loadTemplate = this.loadTemplate.bind(this);
this.loadTemplateAndRoles = this.loadTemplateAndRoles.bind(this); this.loadTemplateAndRoles = this.loadTemplateAndRoles.bind(this);
this.loadSchedules = this.loadSchedules.bind(this);
} }
async componentDidMount() { async componentDidMount() {
@@ -81,6 +83,11 @@ class Template extends Component {
} }
} }
loadSchedules(params) {
const { template } = this.state;
return JobTemplatesAPI.readScheduleList(template.id, params);
}
render() { render() {
const { i18n, location, match, me } = this.props; const { i18n, location, match, me } = this.props;
const { const {
@@ -105,10 +112,6 @@ class Template extends Component {
} }
tabsArray.push( tabsArray.push(
{
name: i18n._(t`Schedules`),
link: '/home',
},
{ {
name: i18n._(t`Completed Jobs`), name: i18n._(t`Completed Jobs`),
link: `${match.url}/completed_jobs`, link: `${match.url}/completed_jobs`,
@@ -119,6 +122,13 @@ class Template extends Component {
} }
); );
if (template) {
tabsArray.push({
name: i18n._(t`Schedules`),
link: `${match.url}/schedules`,
});
}
tabsArray.forEach((tab, n) => { tabsArray.forEach((tab, n) => {
tab.id = n; tab.id = n;
}); });
@@ -210,6 +220,12 @@ class Template extends Component {
<JobList defaultParams={{ job__job_template: template.id }} /> <JobList defaultParams={{ job__job_template: template.id }} />
</Route> </Route>
)} )}
{template && (
<Route
path="/templates/:templateType/:id/schedules"
render={() => <ScheduleList loadSchedules={this.loadSchedules} />}
/>
)}
<Route <Route
key="not-found" key="not-found"
path="*" path="*"

View File

@@ -10,6 +10,7 @@ import ContentError from '@components/ContentError';
import FullPage from '@components/FullPage'; import FullPage from '@components/FullPage';
import JobList from '@components/JobList'; import JobList from '@components/JobList';
import RoutedTabs from '@components/RoutedTabs'; import RoutedTabs from '@components/RoutedTabs';
import ScheduleList from '@components/ScheduleList';
import { WorkflowJobTemplatesAPI, CredentialsAPI } from '@api'; import { WorkflowJobTemplatesAPI, CredentialsAPI } from '@api';
import WorkflowJobTemplateDetail from './WorkflowJobTemplateDetail'; import WorkflowJobTemplateDetail from './WorkflowJobTemplateDetail';
import { Visualizer } from './WorkflowJobTemplateVisualizer'; import { Visualizer } from './WorkflowJobTemplateVisualizer';
@@ -24,6 +25,7 @@ class WorkflowJobTemplate extends Component {
template: null, template: null,
}; };
this.loadTemplate = this.loadTemplate.bind(this); this.loadTemplate = this.loadTemplate.bind(this);
this.loadSchedules = this.loadSchedules.bind(this);
} }
async componentDidMount() { async componentDidMount() {
@@ -70,6 +72,11 @@ class WorkflowJobTemplate extends Component {
} }
} }
loadSchedules(params) {
const { template } = this.state;
return WorkflowJobTemplatesAPI.readScheduleList(template.id, params);
}
render() { render() {
const { i18n, location, match } = this.props; const { i18n, location, match } = this.props;
const { const {
@@ -85,6 +92,13 @@ class WorkflowJobTemplate extends Component {
{ name: i18n._(t`Completed Jobs`), link: `${match.url}/completed_jobs` }, { name: i18n._(t`Completed Jobs`), link: `${match.url}/completed_jobs` },
]; ];
if (template) {
tabsArray.push({
name: i18n._(t`Schedules`),
link: `${match.url}/schedules`,
});
}
tabsArray.forEach((tab, n) => { tabsArray.forEach((tab, n) => {
tab.id = n; tab.id = n;
}); });
@@ -162,6 +176,12 @@ class WorkflowJobTemplate extends Component {
/> />
</Route> </Route>
)} )}
{template && (
<Route
path="/templates/:templateType/:id/schedules"
render={() => <ScheduleList loadSchedules={this.loadSchedules} />}
/>
)}
<Route <Route
key="not-found" key="not-found"
path="*" path="*"