Skip to content

Commit

Permalink
Adding a list of associated exercised on the pipeline overview page.
Browse files Browse the repository at this point in the history
  • Loading branch information
krulis-martin committed Jan 9, 2022
1 parent af1b405 commit cb4b4b7
Show file tree
Hide file tree
Showing 10 changed files with 281 additions and 72 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { Table } from 'react-bootstrap';
import { injectIntl, FormattedMessage } from 'react-intl';
import { defaultMemoize } from 'reselect';

import Button, { TheButtonGroup } from '../../widgets/TheButton';
import { DetailIcon, EditIcon, LimitsIcon, LoadingIcon, TestsIcon, WarningIcon } from '../../icons';
import { resourceStatus } from '../../../redux/helpers/resourceManager';
import { getLocalizedName } from '../../../helpers/localizedData';
import UsersNameContainer from '../../../containers/UsersNameContainer';
import withLinks from '../../../helpers/withLinks';

const COLLAPSE_LIMIT = 50;
const PREVIEW_SIZE = 10;

const nameComparator = locale => (a, b) =>
getLocalizedName(a, locale).localeCompare(getLocalizedName(b, locale), locale);

const preprocessExercises = defaultMemoize((exercises, locale, offset = 0, limit = 1000) =>
exercises
.toJS()
.sort(nameComparator(locale))
.slice(offset >= exercises.size ? offset : 0, offset + Math.max(limit, 10))
);

const PipelineExercisesList = ({
pipelineExercises = null,
intl: { locale },
links: {
EXERCISE_URI_FACTORY,
EXERCISE_EDIT_URI_FACTORY,
EXERCISE_EDIT_CONFIG_URI_FACTORY,
EXERCISE_EDIT_LIMITS_URI_FACTORY,
},
}) => {
const [fullView, setFullView] = useState(false);

if (!pipelineExercises || pipelineExercises === resourceStatus.PENDING) {
return (
<div className="text-center p-2">
<LoadingIcon gapRight />
<FormattedMessage id="generic.loading" defaultMessage="Loading..." />
</div>
);
}

if (pipelineExercises === resourceStatus.REJECTED) {
return (
<div className="text-center p-2">
<WarningIcon gapRight className="text-danger" />
<FormattedMessage id="app.resourceRenderer.loadingFailed" defaultMessage="Loading failed." />
</div>
);
}

const exercises = preprocessExercises(
pipelineExercises,
locale,
0,
fullView || pipelineExercises.size < COLLAPSE_LIMIT ? 1000000 : PREVIEW_SIZE
);
return (
<Table hover className="mb-1" size="xs">
<thead>
<tr>
<th className="pl-4">
<FormattedMessage id="generic.name" defaultMessage="Name" />
</th>
<th>
<FormattedMessage id="generic.author" defaultMessage="Author" />
</th>
<th className="shrink-col text-nowrap">
{pipelineExercises.size > 5 && (
<small className="text-muted">
<FormattedMessage
id="app.pipelineExercisessList.totalCount"
defaultMessage="Total exercises: {count}"
values={{ count: pipelineExercises.size }}
/>
</small>
)}
</th>
</tr>
</thead>

<tbody>
{exercises.map(exercise => (
<tr key={exercise.id}>
<td className="pl-4">{getLocalizedName(exercise, locale)}</td>
<td>
<UsersNameContainer
userId={exercise.authorId}
showEmail="icon"
noAvatar={exercises.length > COLLAPSE_LIMIT}
noAutoload
/>
</td>
<td className="shrink-col text-nowrap">
{exercise.canViewDetail && (
<TheButtonGroup>
<Link to={EXERCISE_URI_FACTORY(exercise.id)}>
<Button size="xs" variant="secondary">
<DetailIcon gapRight />
<FormattedMessage id="generic.detail" defaultMessage="Detail" />
</Button>
</Link>

<Link to={EXERCISE_EDIT_URI_FACTORY(exercise.id)}>
<Button size="xs" variant="warning">
<EditIcon gapRight />
<FormattedMessage id="app.exercises.listEdit" defaultMessage="Settings" />
</Button>
</Link>

<Link to={EXERCISE_EDIT_CONFIG_URI_FACTORY(exercise.id)}>
<Button size="xs" variant="warning">
<TestsIcon gapRight />
<FormattedMessage id="app.exercises.listEditConfig" defaultMessage="Tests" />
</Button>
</Link>

<Link to={EXERCISE_EDIT_LIMITS_URI_FACTORY(exercise.id)}>
<Button size="xs" variant="warning">
<LimitsIcon gapRight />
<FormattedMessage id="app.exercises.listEditLimits" defaultMessage="Limits" />
</Button>
</Link>
</TheButtonGroup>
)}
</td>
</tr>
))}

{exercises.length === 0 && (
<tr>
<td className="text-center" colSpan={3}>
<FormattedMessage
id="app.pipelineExercisessList.empty"
defaultMessage="There are no exercises using this pipelines at the moment."
/>
</td>
</tr>
)}
</tbody>

{pipelineExercises.size >= COLLAPSE_LIMIT && (
<tfoot>
<tr>
<td className="text-center" colSpan={3}>
{fullView ? (
<a
href=""
onClick={ev => {
setFullView(false);
ev.preventDefault();
}}>
<FormattedMessage
id="app.pipelineExercisessList.collapse"
defaultMessage="Collapse the list and show only short preview."
/>
</a>
) : (
<a
href=""
onClick={ev => {
setFullView(true);
ev.preventDefault();
}}>
<FormattedMessage
id="app.pipelineExercisessList.expand"
defaultMessage="Expand the list and show all {count} exercises."
values={{ count: pipelineExercises.size }}
/>
</a>
)}
</td>
</tr>
</tfoot>
)}
</Table>
);
};

PipelineExercisesList.propTypes = {
pipelineExercises: PropTypes.any,
intl: PropTypes.object.isRequired,
links: PropTypes.object,
};

export default withLinks(injectIntl(PipelineExercisesList));
2 changes: 2 additions & 0 deletions src/components/Pipelines/PipelineExercisesList/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import PipelineExercisesList from './PipelineExercisesList';
export default PipelineExercisesList;
9 changes: 7 additions & 2 deletions src/containers/UsersNameContainer/UsersNameContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@ import { LoadingIcon, FailureIcon } from '../../components/icons';
import './UsersNameContainer.css';

class UsersNameContainer extends Component {
componentDidMount = () => this.props.loadUserIfNeeded(this.props.userId);
componentDidMount = () => {
if (!this.props.noAutoload) {
this.props.loadUserIfNeeded(this.props.userId);
}
};

componentDidUpdate(prevProps) {
if (this.props.userId !== prevProps.userId) {
if (this.props.userId !== prevProps.userId && !this.props.noAutoload) {
this.props.loadUserIfNeeded(this.props.userId);
}
}
Expand Down Expand Up @@ -73,6 +77,7 @@ UsersNameContainer.propTypes = {
showEmail: PropTypes.string,
showExternalIdentifiers: PropTypes.bool,
showRoleIcon: PropTypes.bool,
noAutoload: PropTypes.bool,
loadUserIfNeeded: PropTypes.func.isRequired,
};

Expand Down
5 changes: 5 additions & 0 deletions src/locales/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -1101,6 +1101,7 @@
"app.passwordStrength.somewhatOk": "Šlo by to i lépe.",
"app.passwordStrength.unknown": "...",
"app.passwordStrength.worst": "Nevyhovující",
"app.pipeline.associatedExercises": "Přidružené úlohy",
"app.pipeline.description": "Popis",
"app.pipeline.failedDetail": "Načítání detailů pipeline selhalo. Ujistěte se prosím, že jste připojeni k Internetu a zkuste to později.",
"app.pipeline.forkPipeline": "Duplikovat pipeline",
Expand Down Expand Up @@ -1135,6 +1136,10 @@
"app.pipelineEditContainer.variablesTitle": "Proměnné",
"app.pipelineEditContainer.versionChanged": "Původní stuktura pipeline byla změněna v průběhu provádění vašich změn. Pokud necháte znovu načíst pipeline, zůstane současná verze v historii (takže můžete použít tlačítko zpět a vrátit se k vámi rozpracované verzi).",
"app.pipelineEditContainer.versionChangedTitle": "Pipeline byla změněna",
"app.pipelineExercisessList.collapse": "Skrýt úplný seznam a zobrazit pouze náhled.",
"app.pipelineExercisessList.empty": "Žádné úlohy nepoužívají tuto pipeline v tomto okamžiku.",
"app.pipelineExercisessList.expand": "Zobrazit kompletní seznam {count} úloh.",
"app.pipelineExercisessList.totalCount": "Celkem úloh: {count}",
"app.pipelineFilesTable.description": "Testovací soubory jsou soubory, které mohou být odkazované v konfiguraci pipeline.",
"app.pipelineFilesTable.title": "Soubory pipeline",
"app.pipelineParams.hasEntryPoint": "Testované řešení by mělo specifikovat vstupní bod aplikace",
Expand Down
5 changes: 5 additions & 0 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1101,6 +1101,7 @@
"app.passwordStrength.somewhatOk": "You can do better.",
"app.passwordStrength.unknown": "...",
"app.passwordStrength.worst": "Unsatisfactory",
"app.pipeline.associatedExercises": "Associated exercises",
"app.pipeline.description": "Description",
"app.pipeline.failedDetail": "Loading the details of the pipeline failed. Please make sure you are connected to the Internet and try again later.",
"app.pipeline.forkPipeline": "Duplicate Pipeline",
Expand Down Expand Up @@ -1135,6 +1136,10 @@
"app.pipelineEditContainer.variablesTitle": "Variables",
"app.pipelineEditContainer.versionChanged": "The pipeline structure was updated whilst you were editing it. If you load the new pipeline, it will be pushed as a new state in editor (you can use undo button to revert it).",
"app.pipelineEditContainer.versionChangedTitle": "The pipeline was updated",
"app.pipelineExercisessList.collapse": "Collapse the list and show only short preview.",
"app.pipelineExercisessList.empty": "There are no exercises using this pipelines at the moment.",
"app.pipelineExercisessList.expand": "Expand the list and show all {count} exercises.",
"app.pipelineExercisessList.totalCount": "Total exercises: {count}",
"app.pipelineFilesTable.description": "Supplementary files are files which can be referenced as remote file in pipeline configuration.",
"app.pipelineFilesTable.title": "Pipeline files",
"app.pipelineParams.hasEntryPoint": "Tested solution is expected to specify entry point of the application",
Expand Down
35 changes: 30 additions & 5 deletions src/pages/Pipeline/Pipeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,17 @@ import Button, { TheButtonGroup } from '../../components/widgets/TheButton';
import Confirm from '../../components/forms/Confirm';
import Callout from '../../components/widgets/Callout';

import { fetchPipelineIfNeeded, forkPipeline } from '../../redux/modules/pipelines';
import { getPipeline, pipelineEnvironmentsSelector } from '../../redux/selectors/pipelines';
import { fetchPipelineIfNeeded, fetchPipelineExercises, forkPipeline } from '../../redux/modules/pipelines';
import { fetchByIds } from '../../redux/modules/users';
import { fetchRuntimeEnvironments } from '../../redux/modules/runtimeEnvironments';
import { getPipeline, getPipelineExercises, pipelineEnvironmentsSelector } from '../../redux/selectors/pipelines';

import { getVariablesUtilization } from '../../helpers/pipelines';
import PipelineDetail from '../../components/Pipelines/PipelineDetail';
import PipelineGraph from '../../components/Pipelines/PipelineGraph';
import PipelineExercisesList from '../../components/Pipelines/PipelineExercisesList';
import ResourceRenderer from '../../components/helpers/ResourceRenderer';
import { fetchRuntimeEnvironments } from '../../redux/modules/runtimeEnvironments';
import { hasPermissions } from '../../helpers/common';
import { hasPermissions, identity } from '../../helpers/common';
import withLinks from '../../helpers/withLinks';

class Pipeline extends Component {
Expand All @@ -32,7 +34,15 @@ class Pipeline extends Component {
};

static loadAsync = ({ pipelineId }, dispatch) =>
Promise.all([dispatch(fetchPipelineIfNeeded(pipelineId)), dispatch(fetchRuntimeEnvironments())]);
Promise.all([
dispatch(fetchPipelineIfNeeded(pipelineId))
.then(() => dispatch(fetchPipelineExercises(pipelineId)))
.then(({ value: exercises }) => {
const ids = new Set(exercises.map(exercise => exercise && exercise.authorId).filter(identity));
return dispatch(fetchByIds(Array.from(ids)));
}),
dispatch(fetchRuntimeEnvironments()),
]);

componentDidMount() {
this.props.loadAsync();
Expand Down Expand Up @@ -81,6 +91,7 @@ class Pipeline extends Component {
params: { pipelineId },
},
pipeline,
pipelineExercises,
runtimeEnvironments,
} = this.props;

Expand Down Expand Up @@ -149,6 +160,7 @@ class Pipeline extends Component {
</Col>
)}
</ResourceRenderer>

<Col lg={12}>
<Box
title={<FormattedMessage id="app.pipeline.visualization" defaultMessage="Visualization" />}
Expand All @@ -160,6 +172,17 @@ class Pipeline extends Component {
/>
</Box>
</Col>

<Col lg={12}>
<Box
title={
<FormattedMessage id="app.pipeline.associatedExercises" defaultMessage="Associated exercises" />
}
unlimitedHeight
noPadding>
<PipelineExercisesList pipelineExercises={pipelineExercises} />
</Box>
</Col>
</Row>
</>
)}
Expand All @@ -179,6 +202,7 @@ Pipeline.propTypes = {
replace: PropTypes.func.isRequired,
}),
pipeline: ImmutablePropTypes.map,
pipelineExercises: PropTypes.any,
runtimeEnvironments: PropTypes.array,
loadAsync: PropTypes.func.isRequired,
forkPipeline: PropTypes.func.isRequired,
Expand All @@ -197,6 +221,7 @@ export default withLinks(
) => {
return {
pipeline: getPipeline(pipelineId)(state),
pipelineExercises: getPipelineExercises(state, pipelineId),
runtimeEnvironments: pipelineEnvironmentsSelector(pipelineId)(state),
};
},
Expand Down
12 changes: 0 additions & 12 deletions src/redux/modules/groups.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,6 @@ const { actions, actionTypes, reduceActions } = factory({ resourceName });
export { actionTypes };

export const additionalActionTypes = {
LOAD_USERS_GROUPS: 'recodex/groups/LOAD_USERS_GROUPS',
LOAD_USERS_GROUPS_PENDING: 'recodex/groups/LOAD_USERS_GROUPS_PENDING',
LOAD_USERS_GROUPS_FULFILLED: 'recodex/groups/LOAD_USERS_GROUPS_FULFILLED',
LOAD_USERS_GROUPS_REJECTED: 'recodex/groups/LOAD_USERS_GROUPS_REJECTED',
JOIN_GROUP: 'recodex/groups/JOIN_GROUP',
JOIN_GROUP_PENDING: 'recodex/groups/JOIN_GROUP_PENDING',
JOIN_GROUP_FULFILLED: 'recodex/groups/JOIN_GROUP_FULFILLED',
Expand Down Expand Up @@ -291,14 +287,6 @@ const reducer = handleActions(
state
),

[additionalActionTypes.LOAD_USERS_GROUPS_FULFILLED]: (state, { payload, ...rest }) => {
const groups = [...payload.supervisor, ...payload.student];
return reduceActions[actionTypes.FETCH_MANY_FULFILLED](state, {
...rest,
payload: groups,
});
},

[assignmentsActionTypes.UPDATE_FULFILLED]: (state, { payload: { id: assignmentId, groupId } }) =>
state.updateIn(['resources', groupId, 'data', 'privateData', 'assignments'], assignments =>
assignments.push(assignmentId).toSet().toList()
Expand Down
Loading

0 comments on commit cb4b4b7

Please sign in to comment.