diff --git a/atst/domain/requests/requests.py b/atst/domain/requests/requests.py index 86e2908d..4f8ed46e 100644 --- a/atst/domain/requests/requests.py +++ b/atst/domain/requests/requests.py @@ -207,3 +207,7 @@ class Requests(object): comment = RequestInternalComment(request=request, text=comment_text, user=user) RequestsQuery.add_and_commit(comment) return request + + @classmethod + def possible_statuses(cls): + return [s[1].value for s in RequestStatus.__members__.items()] diff --git a/atst/routes/requests/index.py b/atst/routes/requests/index.py index 99e3ad5c..4c5f281d 100644 --- a/atst/routes/requests/index.py +++ b/atst/routes/requests/index.py @@ -15,10 +15,12 @@ class RequestsIndex(object): Permissions.REVIEW_AND_APPROVE_JEDI_WORKSPACE_REQUEST in self.user.atat_permissions ): - return self._ccpo_view(self.user) + context = self._ccpo_view(self.user) else: - return self._non_ccpo_view(self.user) + context = self._non_ccpo_view(self.user) + + return {**context, "possible_statuses": Requests.possible_statuses()} def _ccpo_view(self, user): requests = Requests.get_many() @@ -55,6 +57,14 @@ class RequestsIndex(object): "extended_view": False, } + def _workspace_link_for_request(self, request): + if request.is_approved: + return url_for( + "workspaces.workspace_projects", workspace_id=request.workspace_id + ) + else: + return None + def _map_request(self, request, viewing_role): time_created = pendulum.instance(request.time_created) is_new = time_created.add(days=1) > pendulum.now() @@ -67,6 +77,7 @@ class RequestsIndex(object): "workspace_id": request.workspace.id if request.workspace else None, "name": request.displayname, "is_new": is_new, + "is_approved": request.is_approved, "status": request.status_displayname, "app_count": app_count, "last_submission_timestamp": request.last_submission_timestamp, @@ -76,6 +87,7 @@ class RequestsIndex(object): "edit_link": url_for("requests.edit", request_id=request.id), "action_required": request.action_required_by == viewing_role, "dod_component": request.latest_revision.dod_component, + "workspace_link": self._workspace_link_for_request(request), } diff --git a/js/components/forms/requests_list.js b/js/components/forms/requests_list.js new file mode 100644 index 00000000..c3b07518 --- /dev/null +++ b/js/components/forms/requests_list.js @@ -0,0 +1,139 @@ +import LocalDatetime from '../../components/local_datetime' +import { formatDollars } from '../../lib/dollars' +import { parse } from 'date-fns' +import { compose, partial, indexBy, prop, sortBy, reverse, pipe } from 'ramda' + +export default { + name: 'requests-list', + + components: { + LocalDatetime, + }, + + props: { + requests: { + type: Array, + default: [], + }, + isExtended: { + type: Boolean, + default: false, + }, + statuses: { + type: Array, + default: [], + }, + }, + + data: function () { + const defaultSort = (sort, requests) => sortBy(prop(sort.columnName), requests) + const dateSort = (sort, requests) => { + const parseDate = compose(partial(parse), prop(sort.columnName)) + return sortBy(parseDate, requests) + } + + const columnList = [ + { + displayName: 'JEDI Cloud Request Name', + attr: 'name', + sortFunc: defaultSort, + }, + { + displayName: 'Date Request Submitted', + attr: 'last_submission_timestamp', + sortFunc: dateSort, + }, + { + displayName: 'Date Request Last Edited', + attr: 'last_edited_timestamp', + extendedOnly: true, + sortFunc: dateSort, + }, + { + displayName: 'Requester', + attr: 'full_name', + extendedOnly: true, + sortFunc: defaultSort, + }, + { + displayName: 'Projected Annual Usage ($)', + attr: 'annual_usage', + sortFunc: defaultSort, + }, + { + displayName: 'Request Status', + attr: 'status', + sortFunc: defaultSort, + }, + { + displayName: 'DOD Component', + attr: 'dod_component', + extendedOnly: true, + sortFunc: defaultSort, + }, + ] + + return { + searchValue: '', + statusValue: '', + sort: { + columnName: '', + isAscending: true + }, + columns: indexBy(prop('attr'), columnList), + } + }, + + computed: { + filteredRequests: function () { + return pipe( + partial(this.applySearch, [this.searchValue]), + partial(this.applyFilters, [this.statusValue]), + partial(this.applySort, [this.sort]), + )(this.requests) + } + }, + + methods: { + getColumns: function() { + return Object.values(this.columns) + .filter((column) => !column.extendedOnly || this.isExtended) + }, + applySearch: (query, requests) => { + return requests.filter( + (request) => query !== '' ? + request.name.toLowerCase().includes(query.toLowerCase()) : + true + ) + }, + applyFilters: (status, requests) => { + return requests.filter( + (request) => status !== '' ? + request.status === status : + true + ) + }, + applySort: function(sort, requests) { + if (sort.columnName === '') { + return requests + } else { + const { sortFunc } = this.columns[sort.columnName] + const sorted = sortFunc(sort, requests) + return sort.isAscending ? + sorted : + reverse(sorted) + } + }, + dollars: (value) => formatDollars(value, false), + updateSortValue: function(columnName) { + if (!this.isExtended) { return } + + // toggle ascending / descending if column is clicked twice + if (columnName === this.sort.columnName) { + this.sort.isAscending = !this.sort.isAscending + } + + this.sort.columnName = columnName; + }, + }, + } diff --git a/js/index.js b/js/index.js index ac1f7c79..e6c4ae68 100644 --- a/js/index.js +++ b/js/index.js @@ -22,6 +22,7 @@ import SpendTable from './components/tables/spend_table' import CcpoApproval from './components/forms/ccpo_approval' import MembersList from './components/forms/members_list' import LocalDatetime from './components/local_datetime' +import RequestsList from './components/forms/requests_list' Vue.use(VTooltip) @@ -46,6 +47,7 @@ const app = new Vue({ LocalDatetime, EditEnvironmentRole, EditProjectRoles, + RequestsList, }, mounted: function() { diff --git a/package.json b/package.json index 2a939edf..9dd73d40 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "date-fns": "^1.29.0", "npm": "^6.0.1", "parcel": "^1.9.7", + "ramda": "^0.25.0", "svg-innerhtml": "^1.1.0", "text-mask-addons": "^3.8.0", "uswds": "^1.6.3", diff --git a/templates/requests/index.html b/templates/requests/index.html index 3f5c121b..acba403f 100644 --- a/templates/requests/index.html +++ b/templates/requests/index.html @@ -3,6 +3,7 @@ {% from "components/alert.html" import Alert %} {% from "components/modal.html" import Modal %} {% from "components/empty_state.html" import EmptyState %} +{% from "components/icon.html" import Icon %} {% block content %} @@ -36,6 +37,9 @@ {% endcall %} + +
+ {% if num_action_required %} {% set title -%} Action required on {{ num_action_required }} requests. @@ -55,7 +59,6 @@ ) }} {% else %} - {% if extended_view %}
@@ -76,22 +79,21 @@
{% if extended_view %} - @@ -101,52 +103,49 @@ - - - {% if extended_view %} - - - {% endif %} - - - {% if extended_view %} - - {% endif %} + - - {% for r in requests %} + - - {% if extended_view %} - - + + {% if extended_view %} + + {% endif %} - + {% if extended_view %} - + {% endif %} - {% endfor %}
JEDI Cloud Request NameDate Request SubmittedDate Request Last EditedRequesterProjected Annual Usage ($)Request StatusDOD Component + !{ column.displayName } + + {{ Icon("caret_down") }} + + + {{ Icon("caret_up") }} + +
- {{ r.name }} - {% if r.action_required %}Action Required{% endif %} + !{ r.name } + Action Required {{ r.last_submission_timestamp | formattedDate }}{{ r.last_edited_timestamp | formattedDate }}{{ r.full_name }}!{ r.full_name }{{ r.annual_usage | dollars }}!{ dollars(r.annual_usage) } - {% if r.status == 'Approved' %} - - {{ r.status }} - - {% else %} - {{ r.status }} - {% endif %} + + !{ r.status } + + + !{ r.status } + {{ r.dod_component }}!{ r.dod_component }
- {% endif %} +
+ + {% endblock %} diff --git a/yarn.lock b/yarn.lock index 965179de..f7a9368e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6101,6 +6101,11 @@ qw@~1.0.1: resolved "https://registry.yarnpkg.com/qw/-/qw-1.0.1.tgz#efbfdc740f9ad054304426acb183412cc8b996d4" integrity sha1-77/cdA+a0FQwRCassYNBLMi5ltQ= +ramda@^0.25.0: + version "0.25.0" + resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.25.0.tgz#8fdf68231cffa90bc2f9460390a0cb74a29b29a9" + integrity sha512-GXpfrYVPwx3K7RQ6aYT8KPS8XViSXUVJT1ONhoKPE9VAleW42YE+U+8VEyGWt41EnEQW7gwecYJriTI0pKoecQ== + randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: version "2.0.6" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.6.tgz#d302c522948588848a8d300c932b44c24231da80"