From a333c5aa978dfee0050273d6e30683181252aa48 Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Mon, 8 Jun 2020 16:53:08 -0500 Subject: [PATCH 01/20] Allocations usage by user Added endpoint Saga and ui with reacstrap --- .../Allocations/AllocationsModals.js | 57 ++++++++++++++++++- .../Allocations/AllocationsTables.js | 11 +++- client/src/redux/sagas/allocations.sagas.js | 52 +++++++++++++++-- server/portal/apps/users/urls.py | 5 +- server/portal/apps/users/utils.py | 12 ++++ server/portal/apps/users/views.py | 12 +++- 6 files changed, 135 insertions(+), 14 deletions(-) diff --git a/client/src/components/Allocations/AllocationsModals.js b/client/src/components/Allocations/AllocationsModals.js index 56ee0496b8..ce31e16981 100644 --- a/client/src/components/Allocations/AllocationsModals.js +++ b/client/src/components/Allocations/AllocationsModals.js @@ -12,7 +12,7 @@ import { import { useSelector } from 'react-redux'; import { useTable } from 'react-table'; import { LoadingSpinner } from '_common'; -import { capitalize, has } from 'lodash'; +import { capitalize, has, isEmpty } from 'lodash'; const MODAL_PROPTYPES = { isOpen: bool.isRequired, @@ -190,9 +190,64 @@ export const ContactCard = ({ listing }) => {
Username: {username}
Email: {email}
+ {!isEmpty(listing.usageData) && ( + + )} ); }; ContactCard.propTypes = { listing: USER_LISTING_PROPTYPES }; ContactCard.defaultProps = { listing: {} }; + +export const UsageTable = ({ rawData }) => { + const data = React.useMemo(() => rawData, [rawData]); + const columns = React.useMemo( + () => [ + { + Header: 'System', + accessor: 'resource' + }, + { Header: 'Usage', accessor: 'usage' }, + { Header: '% of Allocation' } + ], + [rawData] + ); + const { getTableBodyProps, rows, prepareRow, headerGroups } = useTable({ + columns, + data + }); + return ( + + + {headerGroups.map(headerGroup => ( + + {headerGroup.headers.map(column => ( + + ))} + + ))} + + + {rows.map(row => { + prepareRow(row); + return ( + + {row.cells.map(cell => ( + + ))} + + ); + })} + +
{column.render('Header')}
{cell.render('Cell')}
+ ); +}; +UsageTable.propTypes = { + rawData: arrayOf( + shape({ + resource: string, + usage: number + }) + ).isRequired +}; diff --git a/client/src/components/Allocations/AllocationsTables.js b/client/src/components/Allocations/AllocationsTables.js index 037ea6fe11..b35f1d76ee 100644 --- a/client/src/components/Allocations/AllocationsTables.js +++ b/client/src/components/Allocations/AllocationsTables.js @@ -7,7 +7,6 @@ import { string } from 'prop-types'; import { Team, Systems, Awarded, Remaining, Expires } from './AllocationsCells'; import systemAccessor from './AllocationsUtils'; -/** Custom hook to get columns and data for table */ export const useAllocations = page => { const allocations = useSelector(state => { if (page === 'expired') return state.allocations.inactive; @@ -26,9 +25,15 @@ export const useAllocations = page => { }, { Header: 'Team', - accessor: ({ projectName, projectId }) => ({ + // TODO: Refactor to Util + accessor: ({ projectName, projectId, systems }) => ({ name: projectName.toLowerCase(), - projectId + projectId, + allocationIds: systems.map(sys => { + // Each system has an allocation object + const { id } = sys.allocation; + return { system: sys, id }; + }) }), Cell: Team }, diff --git a/client/src/redux/sagas/allocations.sagas.js b/client/src/redux/sagas/allocations.sagas.js index e944e447a2..4b5e3c49a3 100644 --- a/client/src/redux/sagas/allocations.sagas.js +++ b/client/src/redux/sagas/allocations.sagas.js @@ -1,4 +1,4 @@ -import { put, takeEvery, takeLatest, call } from 'redux-saga/effects'; +import { put, takeEvery, takeLatest, call, all } from 'redux-saga/effects'; import { flatten } from 'lodash'; import { fetchUtil } from 'utils/fetchUtil'; import 'cross-fetch'; @@ -37,7 +37,7 @@ export function* getAllocations(action) { } } -const getTeamPayload = (id, obj, error = false) => { +const getTeamPayload = (id, obj, error = false, usageData = {}) => { const loading = { [id]: false }; if (error) { return { @@ -45,19 +45,47 @@ const getTeamPayload = (id, obj, error = false) => { loading }; } + const data = { - [id]: obj.usernames.sort((a, b) => a.firstName.localeCompare(b.firstName)) + [id]: obj + .sort((a, b) => a.firstName.localeCompare(b.firstName)) + .map(user => { + const { username } = user; + const individualUsage = usageData.filter( + val => val.username === username + ); + if (!individualUsage) { + return user; + } + return { + ...user, + usageData: individualUsage.map(val => ({ + usage: val.usage, + resource: val.resource + })) + }; + }) }; - return { data, loading }; }; function* getUsernames(action) { try { - const json = yield call(fetchUtil, { + const res = yield call(fetchUtil, { url: `/api/users/team/${action.payload.name}` }); - const payload = getTeamPayload(action.payload.projectId, json); + const json = res.response; + + const { allocationIds } = action.payload; + const usageCalls = allocationIds.map(params => usageUtil(params)); + const usage = yield all(usageCalls); + + const payload = getTeamPayload( + action.payload.projectId, + json, + false, + flatten(usage) + ); yield put({ type: 'ADD_USERNAMES_TO_TEAM', payload }); } catch (error) { const payload = getTeamPayload(action.payload.projectId, error, true); @@ -68,6 +96,18 @@ function* getUsernames(action) { } } +const usageUtil = async params => { + const res = await fetchUtil({ + url: `/api/users/team/usage/${params.id}` + }); + const data = res.response + .map(user => { + return { ...user, resource: params.system.host }; + }) + .filter(Boolean); + return data; +}; + export function* watchAllocationData() { yield takeEvery('GET_ALLOCATIONS', getAllocations); } diff --git a/server/portal/apps/users/urls.py b/server/portal/apps/users/urls.py index ce50fc5081..8e7771564e 100644 --- a/server/portal/apps/users/urls.py +++ b/server/portal/apps/users/urls.py @@ -1,6 +1,6 @@ from django.conf.urls import url from django.urls import path -from portal.apps.users.views import SearchView, AuthenticatedView, UsageView, AllocationsView, TeamView, UserDataView +from portal.apps.users.views import SearchView, AuthenticatedView, UsageView, AllocationsView, TeamView, UserDataView, AllocationUsageView app_name = 'users' urlpatterns = [ @@ -9,5 +9,6 @@ url(r'^usage/$', UsageView.as_view(), name='user_usage'), url(r'^allocations/$', AllocationsView.as_view(), name='user_allocations'), path('team/', TeamView.as_view(), name='user_team'), - path('team/user/', UserDataView.as_view(), name='user_data') + path('team/user/', UserDataView.as_view(), name='user_data'), + path('team/usage/', AllocationUsageView.as_view(), name='allocation_usage') ] diff --git a/server/portal/apps/users/utils.py b/server/portal/apps/users/utils.py index 5141812ebf..5c35124496 100644 --- a/server/portal/apps/users/utils.py +++ b/server/portal/apps/users/utils.py @@ -196,3 +196,15 @@ def get_user_data(username): ) user_data = tas_client.get_user(username=username) return user_data + + +# TODO: Look up best practices for docstrings +def get_allocation_usage_by_user(allocation_id): + auth = requests.auth.HTTPBasicAuth(settings.TAS_CLIENT_KEY, settings.TAS_CLIENT_SECRET) + r = requests.get('{0}/v1/allocations/{1}/usage'.format(settings.TAS_URL, allocation_id), auth=auth) + logger.info(r) + resp = r.json() + if resp['status'] == 'success': + return resp['result'] + else: + raise Exception('Failed to get project users', resp['message']) diff --git a/server/portal/apps/users/views.py b/server/portal/apps/users/views.py index 90613b590d..0d50420d18 100644 --- a/server/portal/apps/users/views.py +++ b/server/portal/apps/users/views.py @@ -11,7 +11,7 @@ from elasticsearch_dsl import Q from portal.libs.elasticsearch.docs.base import IndexedFile from pytas.http import TASClient -from portal.apps.users.utils import get_allocations, get_usernames, get_user_data +from portal.apps.users.utils import get_allocations, get_usernames, get_user_data, get_allocation_usage_by_user logger = logging.getLogger(__name__) @@ -129,7 +129,7 @@ def get(self, request, project_name): : rtype: dict """ usernames = get_usernames(project_name) - return JsonResponse({'usernames': usernames}, safe=False) + return JsonResponse({'response': usernames}, safe=False) @method_decorator(login_required, name='dispatch') @@ -138,3 +138,11 @@ class UserDataView(BaseApiView): def get(self, request, username): user_data = get_user_data(username) return JsonResponse({username: user_data}) + + +@method_decorator(login_required, name='dispatch') +class AllocationUsageView(BaseApiView): + + def get(self, request, allocation_id): + usage = get_allocation_usage_by_user(allocation_id) + return JsonResponse({'response': usage, "status": 200}) From 3db86afa5243ed122cea7309c819259db2a02fc3 Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Thu, 11 Jun 2020 10:00:51 -0500 Subject: [PATCH 02/20] Refactored sagas --- client/src/redux/sagas/allocations.sagas.js | 88 +++++++++++---------- 1 file changed, 47 insertions(+), 41 deletions(-) diff --git a/client/src/redux/sagas/allocations.sagas.js b/client/src/redux/sagas/allocations.sagas.js index 4b5e3c49a3..a478253e9a 100644 --- a/client/src/redux/sagas/allocations.sagas.js +++ b/client/src/redux/sagas/allocations.sagas.js @@ -3,7 +3,8 @@ import { flatten } from 'lodash'; import { fetchUtil } from 'utils/fetchUtil'; import 'cross-fetch'; -const getTeams = allocations => { +const getTeams = data => { + const allocations = { active: data.active, inactive: data.inactive }; const teams = flatten(Object.values(allocations)).reduce( (obj, item) => ({ ...obj, [item.projectId]: {} }), {} @@ -17,26 +18,6 @@ const getTeams = allocations => { return { teams, loadingTeams }; }; -export function* getAllocations(action) { - yield put({ type: 'START_ADD_ALLOCATIONS' }); - try { - const { response } = yield call(fetchUtil, { - url: '/api/users/allocations/' - }); - const { active, inactive } = response; - yield put({ type: 'ADD_ALLOCATIONS', payload: response }); - yield put({ - type: 'POPULATE_TEAMS', - payload: getTeams({ - active, - inactive - }) - }); - } catch (error) { - yield put({ type: 'ADD_ALLOCATIONS_ERROR', payload: error }); - } -} - const getTeamPayload = (id, obj, error = false, usageData = {}) => { const loading = { [id]: false }; if (error) { @@ -54,9 +35,9 @@ const getTeamPayload = (id, obj, error = false, usageData = {}) => { const individualUsage = usageData.filter( val => val.username === username ); - if (!individualUsage) { - return user; - } + + if (!individualUsage) return user; + return { ...user, usageData: individualUsage.map(val => ({ @@ -69,15 +50,52 @@ const getTeamPayload = (id, obj, error = false, usageData = {}) => { return { data, loading }; }; -function* getUsernames(action) { +const getAllocationsUtil = async () => { + const res = await fetchUtil({ + url: '/api/users/allocations/' + }); + const json = res.response; + return json; +}; + +const getTeamsUtil = async team => { + const res = await fetchUtil({ url: `/api/users/team/${team}` }); + const json = res.response; + return json; +}; + +const getUsageUtil = async params => { + const res = await fetchUtil({ + url: `/api/users/team/usage/${params.id}` + }); + const data = res.response + .map(user => { + return { ...user, resource: params.system.host }; + }) + .filter(Boolean); + return data; +}; + +export function* getAllocations(action) { + yield put({ type: 'START_ADD_ALLOCATIONS' }); try { - const res = yield call(fetchUtil, { - url: `/api/users/team/${action.payload.name}` + const json = yield call(getAllocationsUtil); + yield put({ type: 'ADD_ALLOCATIONS', payload: json }); + yield put({ + type: 'POPULATE_TEAMS', + payload: getTeams(json) }); - const json = res.response; + } catch (error) { + yield put({ type: 'ADD_ALLOCATIONS_ERROR', payload: error }); + } +} + +function* getUsernames(action) { + try { + const json = yield call(getTeamsUtil, action.payload.name); const { allocationIds } = action.payload; - const usageCalls = allocationIds.map(params => usageUtil(params)); + const usageCalls = allocationIds.map(params => getUsageUtil(params)); const usage = yield all(usageCalls); const payload = getTeamPayload( @@ -96,18 +114,6 @@ function* getUsernames(action) { } } -const usageUtil = async params => { - const res = await fetchUtil({ - url: `/api/users/team/usage/${params.id}` - }); - const data = res.response - .map(user => { - return { ...user, resource: params.system.host }; - }) - .filter(Boolean); - return data; -}; - export function* watchAllocationData() { yield takeEvery('GET_ALLOCATIONS', getAllocations); } From fd76a3cc51334f95a3f7966bf1014226854fc55f Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Mon, 15 Jun 2020 15:56:23 -0500 Subject: [PATCH 03/20] UsageDisplayed on modal Added markup and styles for table Added utils to parse data --- .../components/Allocations/Allocations.scss | 16 +- .../Allocations/AllocationsModals.js | 73 +++++---- client/src/redux/sagas/allocations.sagas.js | 141 +++++++++++------- 3 files changed, 147 insertions(+), 83 deletions(-) diff --git a/client/src/components/Allocations/Allocations.scss b/client/src/components/Allocations/Allocations.scss index efb5629646..98e400d678 100644 --- a/client/src/components/Allocations/Allocations.scss +++ b/client/src/components/Allocations/Allocations.scss @@ -138,6 +138,20 @@ font-size: 11px; } } +.usage-table { + tbody{ + tr { + td{ + padding: 0.25rem; + vertical-align: middle; + } + } + tr:nth-child(odd) { + background-color: rgba(112, 112, 112, 0.1); + } + } + +} #pending { @extend .allocations-table; } @@ -187,9 +201,9 @@ .modal-right { display: flex; padding: 1em; - max-width: 500px; overflow: auto; .contact-card { + flex-grow: 1; display: flex; flex-direction: column; font: 16px Roboto, sans-serif; diff --git a/client/src/components/Allocations/AllocationsModals.js b/client/src/components/Allocations/AllocationsModals.js index ce31e16981..1e4f19a4f1 100644 --- a/client/src/components/Allocations/AllocationsModals.js +++ b/client/src/components/Allocations/AllocationsModals.js @@ -183,13 +183,14 @@ export const ContactCard = ({ listing }) => { const { firstName, lastName, email, username } = listing; return (
-
+
{capitalize(firstName)} {capitalize(lastName)}
-
Username: {username}
-
Email: {email}
+
+ Username: {username} | Email: {email} +
{!isEmpty(listing.usageData) && ( )} @@ -209,38 +210,58 @@ export const UsageTable = ({ rawData }) => { accessor: 'resource' }, { Header: 'Usage', accessor: 'usage' }, - { Header: '% of Allocation' } + { + Header: '% of Allocation', + accessor: entry => { + if (entry.percentUsed >= 1) { + return `${entry.percentUsed}%`; + } + return `< 1%`; + } + } ], [rawData] ); - const { getTableBodyProps, rows, prepareRow, headerGroups } = useTable({ + const { + getTableBodyProps, + rows, + prepareRow, + headerGroups, + getTableProps + } = useTable({ columns, data }); return ( - - - {headerGroups.map(headerGroup => ( - - {headerGroup.headers.map(column => ( - - ))} - - ))} - - - {rows.map(row => { - prepareRow(row); - return ( - - {row.cells.map(cell => ( - +
+
{column.render('Header')}
{cell.render('Cell')}
+ + {headerGroups.map(headerGroup => ( + + {headerGroup.headers.map(column => ( + ))} - ); - })} - -
{column.render('Header')}
+ ))} + + + {rows.map(row => { + prepareRow(row); + return ( + + {row.cells.map(cell => ( + {cell.render('Cell')} + ))} + + ); + })} + + +
); }; UsageTable.propTypes = { diff --git a/client/src/redux/sagas/allocations.sagas.js b/client/src/redux/sagas/allocations.sagas.js index a478253e9a..b5c3fe2fe3 100644 --- a/client/src/redux/sagas/allocations.sagas.js +++ b/client/src/redux/sagas/allocations.sagas.js @@ -1,9 +1,66 @@ -import { put, takeEvery, takeLatest, call, all } from 'redux-saga/effects'; -import { flatten } from 'lodash'; +import { + put, + takeEvery, + takeLatest, + call, + all, + select +} from 'redux-saga/effects'; +import { chain, flatten } from 'lodash'; import { fetchUtil } from 'utils/fetchUtil'; import 'cross-fetch'; -const getTeams = data => { +const getAllocationsUtil = async () => { + const res = await fetchUtil({ + url: '/api/users/allocations/' + }); + const json = res.response; + return json; +}; + +export function* getAllocations(action) { + yield put({ type: 'START_ADD_ALLOCATIONS' }); + try { + const json = yield call(getAllocationsUtil); + yield put({ type: 'ADD_ALLOCATIONS', payload: json }); + yield put({ + type: 'POPULATE_TEAMS', + payload: populateTeamsUtil(json) + }); + } catch (error) { + yield put({ type: 'ADD_ALLOCATIONS_ERROR', payload: error }); + } +} + +function* getUsernames(action) { + try { + const json = yield call(getTeamsUtil, action.payload.name); + const usage = yield all( + action.payload.allocationIds.map(params => getUsageUtil(params)) + ); + const allocations = yield select(state => [ + ...state.allocations.active, + ...state.allocations.inactive + ]); + yield put({ + type: 'ADD_USERNAMES_TO_TEAM', + payload: teamPayloadUtil( + action.payload.projectId, + json, + false, + flatten(usage), + allocations + ) + }); + } catch (error) { + yield put({ + type: 'POPULATE_TEAMS_ERROR', + payload: teamPayloadUtil(action.payload.projectId, error, true) + }); + } +} + +const populateTeamsUtil = data => { const allocations = { active: data.active, inactive: data.inactive }; const teams = flatten(Object.values(allocations)).reduce( (obj, item) => ({ ...obj, [item.projectId]: {} }), @@ -18,7 +75,13 @@ const getTeams = data => { return { teams, loadingTeams }; }; -const getTeamPayload = (id, obj, error = false, usageData = {}) => { +const teamPayloadUtil = ( + id, + obj, + error = false, + usageData = {}, + allocations = [] +) => { const loading = { [id]: false }; if (error) { return { @@ -37,27 +100,31 @@ const getTeamPayload = (id, obj, error = false, usageData = {}) => { ); if (!individualUsage) return user; - return { ...user, - usageData: individualUsage.map(val => ({ - usage: val.usage, - resource: val.resource - })) + usageData: individualUsage.map(val => { + const totalAllocated = chain(allocations) + .map('systems') + .flatten() + .filter({ host: val.resource }) + .map('allocation') + .filter({ id: val.allocationId }) + .head() + .value().computeAllocated; + + return { + usage: val.usage, + resource: val.resource, + allocationId: val.allocationId, + percentUsed: val.usage / totalAllocated + }; + }) }; }) }; return { data, loading }; }; -const getAllocationsUtil = async () => { - const res = await fetchUtil({ - url: '/api/users/allocations/' - }); - const json = res.response; - return json; -}; - const getTeamsUtil = async team => { const res = await fetchUtil({ url: `/api/users/team/${team}` }); const json = res.response; @@ -70,50 +137,12 @@ const getUsageUtil = async params => { }); const data = res.response .map(user => { - return { ...user, resource: params.system.host }; + return { ...user, resource: params.system.host, allocationId: params.id }; }) .filter(Boolean); return data; }; -export function* getAllocations(action) { - yield put({ type: 'START_ADD_ALLOCATIONS' }); - try { - const json = yield call(getAllocationsUtil); - yield put({ type: 'ADD_ALLOCATIONS', payload: json }); - yield put({ - type: 'POPULATE_TEAMS', - payload: getTeams(json) - }); - } catch (error) { - yield put({ type: 'ADD_ALLOCATIONS_ERROR', payload: error }); - } -} - -function* getUsernames(action) { - try { - const json = yield call(getTeamsUtil, action.payload.name); - - const { allocationIds } = action.payload; - const usageCalls = allocationIds.map(params => getUsageUtil(params)); - const usage = yield all(usageCalls); - - const payload = getTeamPayload( - action.payload.projectId, - json, - false, - flatten(usage) - ); - yield put({ type: 'ADD_USERNAMES_TO_TEAM', payload }); - } catch (error) { - const payload = getTeamPayload(action.payload.projectId, error, true); - yield put({ - type: 'POPULATE_TEAMS_ERROR', - payload - }); - } -} - export function* watchAllocationData() { yield takeEvery('GET_ALLOCATIONS', getAllocations); } From 358b2a89309d85b2ff56ed276414534ea749a40f Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Tue, 16 Jun 2020 11:29:10 -0500 Subject: [PATCH 04/20] Allocation modals tests Refactored allocation request UI flow for team view modal tests --- .../tests/AllocationsModals.test.js | 108 ++++++++++++++---- 1 file changed, 88 insertions(+), 20 deletions(-) diff --git a/client/src/components/Allocations/tests/AllocationsModals.test.js b/client/src/components/Allocations/tests/AllocationsModals.test.js index 91cbbe7cfd..33e112baca 100644 --- a/client/src/components/Allocations/tests/AllocationsModals.test.js +++ b/client/src/components/Allocations/tests/AllocationsModals.test.js @@ -1,30 +1,98 @@ import React from 'react'; -import { render } from '@testing-library/react'; -import { BrowserRouter as Wrapper } from 'react-router-dom'; -import { NewAllocReq } from '../AllocationsModals'; +import { render, fireEvent } from '@testing-library/react'; +import { BrowserRouter } from 'react-router-dom'; +import configureStore from 'redux-mock-store'; +import { Provider } from 'react-redux'; +import { NewAllocReq, TeamView } from '../AllocationsModals'; -describe('Allocations Request Form', () => { - const isOpen = true; - it('should tell the user what form they are viewing', () => { - const { getByText } = render( - - !isOpen} /> - +const mockStore = configureStore(); + +describe('New Allocations Request Modal', () => { + test('Allocations Request UI', () => { + const { getByText, getByTestId } = render( + + null} /> + ); expect(getByText('Manage Allocations')).toBeDefined(); - }); - it('have a body with content for the user', () => { - const { getByTestId } = render( - - !isOpen} /> - - ); expect(getByTestId('request-body')).toBeDefined(); }); }); describe('View Team Modal', () => { - it.todo( - 'assert that the components of view team modal are showing relevant information for the user' - ); + test('View Team Modal UI', () => { + const testProps = { + isOpen: true, + toggle: () => null, + pid: 1234 + }; + + const testStore = mockStore({ + allocations: { + teams: { + '1234': [ + { + id: '123456', + username: 'testuser1', + role: 'Standard', + firstName: 'Test', + lastName: 'User1', + email: 'user1@gmail.com', + usageData: [] + }, + { + id: '012345', + username: 'testuser2', + role: 'Standard', + firstName: 'Test', + lastName: 'User2', + email: 'user2@gmail.com', + usageData: [ + { + usage: 0.5, + resource: 'stampede2.tacc.utexas.edu', + allocationId: 1, + percentUsed: 0.005 + }, + { + usage: 10, + resource: 'frontera.tacc.utexas.edu', + allocationId: 2, + percentUsed: 10 + } + ] + } + ] + }, + loadingUsernames: { + '1234': { + loading: false + } + }, + errors: {} + } + }); + + // Render Modal + const { getByText, queryByText, getByRole } = render( + + + + ); + + // Check for the list of users + expect(getByText(/View Team/)).toBeDefined(); + expect(getByText(/Test User1/)).toBeDefined(); + expect(getByText(/Test User2/)).toBeDefined(); + + // View Information for the user without usage + fireEvent.click(getByText(/Test User1/)); + expect(getByText(/Username:/)).toBeDefined(); + expect(getByText(/Email:/)).toBeDefined(); + expect(queryByText(/Usage/)).toBeNull(); + + // View information for the user with usage + fireEvent.click(getByText(/Test User2/)); + expect(getByText(/Usage/)).toBeDefined(); + }); }); From 64467ec39fce5c5d2194301791a2336b296d1e20 Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Tue, 16 Jun 2020 12:44:42 -0500 Subject: [PATCH 05/20] Test Coverage and UI tweaks Added coverage for loading/errors Formatting of resource --- .../Allocations/AllocationsModals.js | 10 +- .../tests/AllocationsModals.test.js | 144 ++++++++++++------ 2 files changed, 106 insertions(+), 48 deletions(-) diff --git a/client/src/components/Allocations/AllocationsModals.js b/client/src/components/Allocations/AllocationsModals.js index 1e4f19a4f1..4eb70ca06e 100644 --- a/client/src/components/Allocations/AllocationsModals.js +++ b/client/src/components/Allocations/AllocationsModals.js @@ -207,7 +207,15 @@ export const UsageTable = ({ rawData }) => { () => [ { Header: 'System', - accessor: 'resource' + accessor: entry => { + const system = entry.resource.split('.')[0]; + const sysNum = system.match(/\d+$/); + const sysName = capitalize(system.replace(/[0-9]/g, '')); + if (sysNum) { + return `${sysName} ${sysNum[0]}`; + } + return sysName; + } }, { Header: 'Usage', accessor: 'usage' }, { diff --git a/client/src/components/Allocations/tests/AllocationsModals.test.js b/client/src/components/Allocations/tests/AllocationsModals.test.js index 33e112baca..5922397529 100644 --- a/client/src/components/Allocations/tests/AllocationsModals.test.js +++ b/client/src/components/Allocations/tests/AllocationsModals.test.js @@ -1,76 +1,100 @@ -import React from 'react'; -import { render, fireEvent } from '@testing-library/react'; -import { BrowserRouter } from 'react-router-dom'; -import configureStore from 'redux-mock-store'; -import { Provider } from 'react-redux'; -import { NewAllocReq, TeamView } from '../AllocationsModals'; +import React from "react"; +import { render, fireEvent } from "@testing-library/react"; +import { BrowserRouter } from "react-router-dom"; +import configureStore from "redux-mock-store"; +import { Provider } from "react-redux"; +import { NewAllocReq, TeamView } from "../AllocationsModals"; const mockStore = configureStore(); -describe('New Allocations Request Modal', () => { - test('Allocations Request UI', () => { +describe("New Allocations Request Modal", () => { + test("Allocations Request UI", () => { const { getByText, getByTestId } = render( null} /> ); - expect(getByText('Manage Allocations')).toBeDefined(); - expect(getByTestId('request-body')).toBeDefined(); + expect(getByText("Manage Allocations")).toBeDefined(); + expect(getByTestId("request-body")).toBeDefined(); }); }); -describe('View Team Modal', () => { - test('View Team Modal UI', () => { - const testProps = { - isOpen: true, - toggle: () => null, - pid: 1234 - }; +describe("View Team Modal", () => { + const testProps = { + isOpen: true, + toggle: () => null, + pid: 1234, + }; + + test("View Team Modal Loading", () => { + const testStore = mockStore({ + allocations: { + teams: { + 1234: [], + }, + loadingUsernames: { + 1234: { + loading: true, + }, + }, + errors: {}, + }, + }); + const { getByText } = render( + + + + ); + + expect(getByText(/Loading user list./)).toBeDefined(); + }); + + test("View Team Modal Listing", () => { const testStore = mockStore({ allocations: { teams: { - '1234': [ + "1234": [ { - id: '123456', - username: 'testuser1', - role: 'Standard', - firstName: 'Test', - lastName: 'User1', - email: 'user1@gmail.com', - usageData: [] + id: "123456", + username: "testuser1", + role: "Standard", + firstName: "Test", + lastName: "User1", + email: "user1@gmail.com", + usageData: [], }, { - id: '012345', - username: 'testuser2', - role: 'Standard', - firstName: 'Test', - lastName: 'User2', - email: 'user2@gmail.com', + id: "012345", + username: "testuser2", + role: "Standard", + firstName: "Test", + lastName: "User2", + email: "user2@gmail.com", usageData: [ { usage: 0.5, - resource: 'stampede2.tacc.utexas.edu', + resource: "stampede2.tacc.utexas.edu", allocationId: 1, - percentUsed: 0.005 + percentUsed: 0.005, }, { usage: 10, - resource: 'frontera.tacc.utexas.edu', + resource: "frontera.tacc.utexas.edu", allocationId: 2, - percentUsed: 10 - } - ] - } - ] + percentUsed: 10, + }, + ], + }, + ], }, loadingUsernames: { - '1234': { - loading: false - } + "1234": { + loading: false, + }, }, - errors: {} - } + errors: {}, + }, }); // Render Modal @@ -79,20 +103,46 @@ describe('View Team Modal', () => { ); - + // Check for the list of users expect(getByText(/View Team/)).toBeDefined(); expect(getByText(/Test User1/)).toBeDefined(); expect(getByText(/Test User2/)).toBeDefined(); - + // View Information for the user without usage fireEvent.click(getByText(/Test User1/)); expect(getByText(/Username:/)).toBeDefined(); expect(getByText(/Email:/)).toBeDefined(); expect(queryByText(/Usage/)).toBeNull(); - + // View information for the user with usage fireEvent.click(getByText(/Test User2/)); expect(getByText(/Usage/)).toBeDefined(); }); + + test("View Team Modal Errors", () => { + const testStore = mockStore({ + allocations: { + teams: { + 1234: [], + }, + loadingUsernames: { + 1234: { + loading: true, + }, + }, + errors: { + teams: { 1234: new Error('Unable to fetch') } + }, + }, + }); + + const { getByText } = render( + + + + ); + + expect(getByText(/Unable to retrieve team data./)).toBeDefined(); + }); }); From b976b812edaa927dba193f8abff90bd3e2496513 Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Thu, 2 Jul 2020 13:31:06 -0500 Subject: [PATCH 06/20] Refactored UsageTable Renamed and reworked file structure Implemented css module --- .../components/Allocations/Allocations.scss | 14 --- .../Allocations/AllocationsModals.js | 83 +----------------- .../AllocationsUsageTable.js | 85 +++++++++++++++++++ .../AllocationsUsageTable.module.scss | 31 +++++++ .../AllocationsUsageTable/index.js | 1 + 5 files changed, 119 insertions(+), 95 deletions(-) create mode 100644 client/src/components/Allocations/AllocationsUsageTable/AllocationsUsageTable.js create mode 100644 client/src/components/Allocations/AllocationsUsageTable/AllocationsUsageTable.module.scss create mode 100644 client/src/components/Allocations/AllocationsUsageTable/index.js diff --git a/client/src/components/Allocations/Allocations.scss b/client/src/components/Allocations/Allocations.scss index 98e400d678..15a9379253 100644 --- a/client/src/components/Allocations/Allocations.scss +++ b/client/src/components/Allocations/Allocations.scss @@ -138,20 +138,6 @@ font-size: 11px; } } -.usage-table { - tbody{ - tr { - td{ - padding: 0.25rem; - vertical-align: middle; - } - } - tr:nth-child(odd) { - background-color: rgba(112, 112, 112, 0.1); - } - } - -} #pending { @extend .allocations-table; } diff --git a/client/src/components/Allocations/AllocationsModals.js b/client/src/components/Allocations/AllocationsModals.js index 4eb70ca06e..9397b5e01a 100644 --- a/client/src/components/Allocations/AllocationsModals.js +++ b/client/src/components/Allocations/AllocationsModals.js @@ -13,6 +13,7 @@ import { useSelector } from 'react-redux'; import { useTable } from 'react-table'; import { LoadingSpinner } from '_common'; import { capitalize, has, isEmpty } from 'lodash'; +import AllocationsUsageTable from './AllocationsUsageTable'; const MODAL_PROPTYPES = { isOpen: bool.isRequired, @@ -192,7 +193,7 @@ export const ContactCard = ({ listing }) => { Username: {username} | Email: {email}
{!isEmpty(listing.usageData) && ( - + )} ); @@ -200,83 +201,3 @@ export const ContactCard = ({ listing }) => { ContactCard.propTypes = { listing: USER_LISTING_PROPTYPES }; ContactCard.defaultProps = { listing: {} }; - -export const UsageTable = ({ rawData }) => { - const data = React.useMemo(() => rawData, [rawData]); - const columns = React.useMemo( - () => [ - { - Header: 'System', - accessor: entry => { - const system = entry.resource.split('.')[0]; - const sysNum = system.match(/\d+$/); - const sysName = capitalize(system.replace(/[0-9]/g, '')); - if (sysNum) { - return `${sysName} ${sysNum[0]}`; - } - return sysName; - } - }, - { Header: 'Usage', accessor: 'usage' }, - { - Header: '% of Allocation', - accessor: entry => { - if (entry.percentUsed >= 1) { - return `${entry.percentUsed}%`; - } - return `< 1%`; - } - } - ], - [rawData] - ); - const { - getTableBodyProps, - rows, - prepareRow, - headerGroups, - getTableProps - } = useTable({ - columns, - data - }); - return ( -
- - - {headerGroups.map(headerGroup => ( - - {headerGroup.headers.map(column => ( - - ))} - - ))} - - - {rows.map(row => { - prepareRow(row); - return ( - - {row.cells.map(cell => ( - - ))} - - ); - })} - -
{column.render('Header')}
{cell.render('Cell')}
-
- ); -}; -UsageTable.propTypes = { - rawData: arrayOf( - shape({ - resource: string, - usage: number - }) - ).isRequired -}; diff --git a/client/src/components/Allocations/AllocationsUsageTable/AllocationsUsageTable.js b/client/src/components/Allocations/AllocationsUsageTable/AllocationsUsageTable.js new file mode 100644 index 0000000000..2ff9b28bc8 --- /dev/null +++ b/client/src/components/Allocations/AllocationsUsageTable/AllocationsUsageTable.js @@ -0,0 +1,85 @@ +import React from 'react'; +import { useTable } from 'react-table'; +import { capitalize } from 'lodash'; +import { arrayOf, shape, string, number } from 'prop-types'; +import './AllocationsUsageTable.module.scss'; + +const AllocationsUsageTable = ({ rawData }) => { + const data = React.useMemo(() => rawData, [rawData]); + const columns = React.useMemo( + () => [ + { + Header: 'System', + accessor: entry => { + const system = entry.resource.split('.')[0]; + const sysNum = system.match(/\d+$/); + const sysName = capitalize(system.replace(/[0-9]/g, '')); + if (sysNum) { + return `${sysName} ${sysNum[0]}`; + } + return sysName; + } + }, + { Header: 'Usage', accessor: 'usage' }, + { + Header: '% of Allocation', + accessor: entry => { + if (entry.percentUsed >= 1) { + return `${entry.percentUsed}%`; + } + return `< 1%`; + } + } + ], + [rawData] + ); + const { + getTableBodyProps, + rows, + prepareRow, + headerGroups, + getTableProps + } = useTable({ + columns, + data + }); + return ( +
+ + + {headerGroups.map(headerGroup => ( + + {headerGroup.headers.map(column => ( + + ))} + + ))} + + + {rows.map(row => { + prepareRow(row); + return ( + + {row.cells.map(cell => ( + + ))} + + ); + })} + +
{column.render('Header')}
+ {cell.render('Cell')} +
+
+ ); +}; +AllocationsUsageTable.propTypes = { + rawData: arrayOf( + shape({ + resource: string, + usage: number + }) + ).isRequired +}; + +export default AllocationsUsageTable; diff --git a/client/src/components/Allocations/AllocationsUsageTable/AllocationsUsageTable.module.scss b/client/src/components/Allocations/AllocationsUsageTable/AllocationsUsageTable.module.scss new file mode 100644 index 0000000000..059a063529 --- /dev/null +++ b/client/src/components/Allocations/AllocationsUsageTable/AllocationsUsageTable.module.scss @@ -0,0 +1,31 @@ +.container { + display: flex; + flex-direction: row; + width: 100%; +} +.root { + width: 100%; + border: 0; + font-size: 14px; +} +.header { + tr { + border-bottom: 1px solid #707070; + height: 2.25rem; + } +} +.body { + tr:nth-child(odd) { + background-color: rgba(112, 112, 112, 0.1); + } +} +.row { + height: 2.25rem; +} +.cell { + vertical-align: middle; +} +.content { + padding: 0.25rem; +} + diff --git a/client/src/components/Allocations/AllocationsUsageTable/index.js b/client/src/components/Allocations/AllocationsUsageTable/index.js new file mode 100644 index 0000000000..3a07db8642 --- /dev/null +++ b/client/src/components/Allocations/AllocationsUsageTable/index.js @@ -0,0 +1 @@ +export { default } from './AllocationsUsageTable'; From a56ff4f1d98f69d9c3a4dd2b9b59e3dc2bce1ff6 Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Thu, 2 Jul 2020 15:02:52 -0500 Subject: [PATCH 07/20] Modals in separate directory --- .../components/Allocations/Allocations.scss | 3 + .../Allocations/AllocationsCells.js | 4 +- .../Allocations/AllocationsLayout.js | 4 +- .../Allocations/AllocationsModals.js | 203 ------------------ .../AllocationsRequestModal.js | 36 ++++ .../AllocationsRequestModal/index.js | 1 + .../AllocationsContactCard.js | 37 ++++ .../AllocationsContactCard/index.js | 1 + .../AllocationsTeamTable.js | 75 +++++++ .../AllocationsTeamTable/index.js | 1 + .../AllocationsTeamViewModal.js | 72 +++++++ .../AllocationsUsageTable.js | 0 .../AllocationsUsageTable.module.scss | 0 .../AllocationsUsageTable/index.js | 0 .../AllocationsTeamViewModal/index.js | 1 + .../Allocations/AllocationsModals/index.js | 2 + 16 files changed, 233 insertions(+), 207 deletions(-) delete mode 100644 client/src/components/Allocations/AllocationsModals.js create mode 100644 client/src/components/Allocations/AllocationsModals/AllocationsRequestModal/AllocationsRequestModal.js create mode 100644 client/src/components/Allocations/AllocationsModals/AllocationsRequestModal/index.js create mode 100644 client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.js create mode 100644 client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/index.js create mode 100644 client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsTeamTable/AllocationsTeamTable.js create mode 100644 client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsTeamTable/index.js create mode 100644 client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsTeamViewModal.js rename client/src/components/Allocations/{ => AllocationsModals/AllocationsTeamViewModal}/AllocationsUsageTable/AllocationsUsageTable.js (100%) rename client/src/components/Allocations/{ => AllocationsModals/AllocationsTeamViewModal}/AllocationsUsageTable/AllocationsUsageTable.module.scss (100%) rename client/src/components/Allocations/{ => AllocationsModals/AllocationsTeamViewModal}/AllocationsUsageTable/index.js (100%) create mode 100644 client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/index.js create mode 100644 client/src/components/Allocations/AllocationsModals/index.js diff --git a/client/src/components/Allocations/Allocations.scss b/client/src/components/Allocations/Allocations.scss index 15a9379253..09c6b33640 100644 --- a/client/src/components/Allocations/Allocations.scss +++ b/client/src/components/Allocations/Allocations.scss @@ -200,6 +200,9 @@ text-overflow: ellipsis; overflow: auto; } + .contact-card-title { + font-weight: bold; + } } } .user-name { diff --git a/client/src/components/Allocations/AllocationsCells.js b/client/src/components/Allocations/AllocationsCells.js index ed22acdddd..35c08cef00 100644 --- a/client/src/components/Allocations/AllocationsCells.js +++ b/client/src/components/Allocations/AllocationsCells.js @@ -3,7 +3,7 @@ import { object, shape, array, number, string } from 'prop-types'; import { Button, Badge } from 'reactstrap'; import { useDispatch } from 'react-redux'; import { v4 as uuidv4 } from 'uuid'; -import { TeamView } from './AllocationsModals'; +import { AllocationsTeamViewModal } from './AllocationsModals'; const CELL_PROPTYPES = { cell: shape({ @@ -31,7 +31,7 @@ export const Team = ({ cell: { value } }) => { > View Team - setOpenModal(!openModal)} diff --git a/client/src/components/Allocations/AllocationsLayout.js b/client/src/components/Allocations/AllocationsLayout.js index 50b8a82f0c..f20a455d1c 100644 --- a/client/src/components/Allocations/AllocationsLayout.js +++ b/client/src/components/Allocations/AllocationsLayout.js @@ -7,7 +7,7 @@ import { faClipboard, faDesktop } from '@fortawesome/free-solid-svg-icons'; import { string } from 'prop-types'; import { LoadingSpinner } from '_common'; import { AllocationsTable } from './AllocationsTables'; -import { NewAllocReq } from './AllocationsModals'; +import { AllocationsRequestModal } from './AllocationsModals'; import * as ROUTES from '../../constants/routes'; export const Header = ({ page }) => { @@ -23,7 +23,7 @@ export const Header = ({ page }) => { Manage Allocations {openModal && ( - setOpenModal(!openModal)} /> diff --git a/client/src/components/Allocations/AllocationsModals.js b/client/src/components/Allocations/AllocationsModals.js deleted file mode 100644 index 9397b5e01a..0000000000 --- a/client/src/components/Allocations/AllocationsModals.js +++ /dev/null @@ -1,203 +0,0 @@ -import React, { useState } from 'react'; -import { number, string, func, bool, shape, arrayOf, object } from 'prop-types'; -import { - Modal, - ModalHeader, - ModalBody, - Table, - Container, - Col, - Row -} from 'reactstrap'; -import { useSelector } from 'react-redux'; -import { useTable } from 'react-table'; -import { LoadingSpinner } from '_common'; -import { capitalize, has, isEmpty } from 'lodash'; -import AllocationsUsageTable from './AllocationsUsageTable'; - -const MODAL_PROPTYPES = { - isOpen: bool.isRequired, - toggle: func.isRequired -}; -const USER_LISTING_PROPTYPES = shape({ - firstName: string.isRequired, - lastName: string.isRequired, - email: string.isRequired, - username: string.isRequired -}); - -export const NewAllocReq = ({ isOpen, toggle }) => ( - toggle()}> - - Manage Allocations - - -

- You can manage your allocation, your team members, or request more time - on a machine by using your TACC user account credentials to access the - Resource Allocation System at{' '} - - https://tacc-submit.xras.xsede.org/ - - . -

-
-
-); -NewAllocReq.propTypes = MODAL_PROPTYPES; - -const UserCell = ({ cell: { value } }) => { - const { firstName, lastName } = value; - return ( - - {`${capitalize(firstName)} ${capitalize(lastName)}`} - - ); -}; -UserCell.propTypes = { - cell: shape({ - value: shape({ - firstName: string.isRequired, - lastName: string.isRequired - }).isRequired - }).isRequired -}; - -const TeamTable = ({ rawData, clickHandler, visible }) => { - const data = React.useMemo(() => rawData, [rawData]); - const columns = React.useMemo( - () => [ - { - Header: 'name', - accessor: row => row, - UserCell - } - ], - [rawData] - ); - const { getTableProps, getTableBodyProps, rows, prepareRow } = useTable({ - columns, - data - }); - return ( - - - {rows.map(row => { - prepareRow(row); - return ( - { - clickHandler(row.values.name); - } - })} - > - {row.cells.map(cell => ( - - ))} - - ); - })} - -
{cell.render('UserCell')}
- ); -}; -TeamTable.propTypes = { - rawData: arrayOf(object), - clickHandler: func.isRequired, - visible: USER_LISTING_PROPTYPES -}; -TeamTable.defaultProps = { visible: {}, rawData: [] }; - -export const TeamView = ({ isOpen, toggle, pid }) => { - const { teams, loadingUsernames, errors } = useSelector( - state => state.allocations - ); - const error = has(errors.teams, pid); - const [card, setCard] = useState(null); - const isLoading = loadingUsernames[pid] && loadingUsernames[pid].loading; - return ( - - - View Team - - - - {error ? ( - - - Unable to retrieve team data. - - - ) : ( - - - {isLoading ? ( - - ) : ( - - )} - - - {isLoading ? ( - Loading user list. This may take a moment. - ) : ( - - )} - - - )} - - - - ); -}; -TeamView.propTypes = { ...MODAL_PROPTYPES, pid: number.isRequired }; - -export const ContactCard = ({ listing }) => { - if (!listing) - return ( - Click on a user’s name to view their contact information. - ); - const { firstName, lastName, email, username } = listing; - return ( -
-
- - {capitalize(firstName)} {capitalize(lastName)}
-
-
-
- Username: {username} | Email: {email} -
- {!isEmpty(listing.usageData) && ( - - )} -
- ); -}; - -ContactCard.propTypes = { listing: USER_LISTING_PROPTYPES }; -ContactCard.defaultProps = { listing: {} }; diff --git a/client/src/components/Allocations/AllocationsModals/AllocationsRequestModal/AllocationsRequestModal.js b/client/src/components/Allocations/AllocationsModals/AllocationsRequestModal/AllocationsRequestModal.js new file mode 100644 index 0000000000..42e19727e1 --- /dev/null +++ b/client/src/components/Allocations/AllocationsModals/AllocationsRequestModal/AllocationsRequestModal.js @@ -0,0 +1,36 @@ +import React from 'react'; +import { func, bool } from 'prop-types'; +import { Modal, ModalHeader, ModalBody } from 'reactstrap'; + +export const AllocationsRequestModal = ({ isOpen, toggle }) => ( + toggle()}> + + Manage Allocations + + +

+ You can manage your allocation, your team members, or request more time + on a machine by using your TACC user account credentials to access the + Resource Allocation System at{' '} + + https://tacc-submit.xras.xsede.org/ + + . +

+
+
+); +AllocationsRequestModal.propTypes = { + isOpen: bool.isRequired, + toggle: func.isRequired +}; + +export default AllocationsRequestModal; diff --git a/client/src/components/Allocations/AllocationsModals/AllocationsRequestModal/index.js b/client/src/components/Allocations/AllocationsModals/AllocationsRequestModal/index.js new file mode 100644 index 0000000000..c909c2f156 --- /dev/null +++ b/client/src/components/Allocations/AllocationsModals/AllocationsRequestModal/index.js @@ -0,0 +1 @@ +export { default } from './AllocationsRequestModal'; diff --git a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.js b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.js new file mode 100644 index 0000000000..3067307a30 --- /dev/null +++ b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.js @@ -0,0 +1,37 @@ +import React from 'react'; +import { string, shape } from 'prop-types'; +import { capitalize, isEmpty } from 'lodash'; +import AllocationsUsageTable from '../AllocationsUsageTable'; + +const AllocationsContactCard = ({ listing }) => { + if (!listing) + return ( + Click on a user’s name to view their contact information. + ); + const { firstName, lastName, email, username } = listing; + return ( +
+
+ {capitalize(firstName)} {capitalize(lastName)}
+
+
+ Username: {username} | Email: {email} +
+ {!isEmpty(listing.usageData) && ( + + )} +
+ ); +}; + +AllocationsContactCard.propTypes = { + listing: shape({ + firstName: string.isRequired, + lastName: string.isRequired, + email: string.isRequired, + username: string.isRequired + }) +}; +AllocationsContactCard.defaultProps = { listing: {} }; + +export default AllocationsContactCard; diff --git a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/index.js b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/index.js new file mode 100644 index 0000000000..8b27cc2b7c --- /dev/null +++ b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/index.js @@ -0,0 +1 @@ +export { default } from './AllocationsContactCard'; diff --git a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsTeamTable/AllocationsTeamTable.js b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsTeamTable/AllocationsTeamTable.js new file mode 100644 index 0000000000..b2a5203ce4 --- /dev/null +++ b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsTeamTable/AllocationsTeamTable.js @@ -0,0 +1,75 @@ +import React from 'react'; +import { string, func, shape, arrayOf, object } from 'prop-types'; +import { Table } from 'reactstrap'; +import { useTable } from 'react-table'; +import { capitalize } from 'lodash'; + +const UserCell = ({ cell: { value } }) => { + const { firstName, lastName } = value; + return ( + + {`${capitalize(firstName)} ${capitalize(lastName)}`} + + ); +}; +UserCell.propTypes = { + cell: shape({ + value: shape({ + firstName: string.isRequired, + lastName: string.isRequired + }).isRequired + }).isRequired +}; + +const AllocationsTeamTable = ({ rawData, clickHandler, visible }) => { + const data = React.useMemo(() => rawData, [rawData]); + const columns = React.useMemo( + () => [ + { + Header: 'name', + accessor: row => row, + UserCell + } + ], + [rawData] + ); + const { getTableProps, getTableBodyProps, rows, prepareRow } = useTable({ + columns, + data + }); + return ( + + + {rows.map(row => { + prepareRow(row); + return ( + { + clickHandler(row.values.name); + } + })} + > + {row.cells.map(cell => ( + + ))} + + ); + })} + +
{cell.render('UserCell')}
+ ); +}; +AllocationsTeamTable.propTypes = { + rawData: arrayOf(object), + clickHandler: func.isRequired, + visible: shape({ + firstName: string.isRequired, + lastName: string.isRequired, + email: string.isRequired, + username: string.isRequired + }) +}; +AllocationsTeamTable.defaultProps = { visible: {}, rawData: [] }; +export default AllocationsTeamTable; diff --git a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsTeamTable/index.js b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsTeamTable/index.js new file mode 100644 index 0000000000..278b022460 --- /dev/null +++ b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsTeamTable/index.js @@ -0,0 +1 @@ +export { default } from './AllocationsTeamTable'; diff --git a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsTeamViewModal.js b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsTeamViewModal.js new file mode 100644 index 0000000000..48702a4382 --- /dev/null +++ b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsTeamViewModal.js @@ -0,0 +1,72 @@ +import React, { useState } from 'react'; +import { number, bool, func } from 'prop-types'; +import { Modal, ModalHeader, ModalBody, Container, Col, Row } from 'reactstrap'; +import { useSelector } from 'react-redux'; +import { LoadingSpinner } from '_common'; +import { has } from 'lodash'; +import AllocationsTeamTable from './AllocationsTeamTable'; +import AllocationsContactCard from './AllocationsContactCard'; + +const AllocationsTeamViewModal = ({ isOpen, toggle, pid }) => { + const { teams, loadingUsernames, errors } = useSelector( + state => state.allocations + ); + const error = has(errors.teams, pid); + const [card, setCard] = useState(null); + const isLoading = loadingUsernames[pid] && loadingUsernames[pid].loading; + return ( + + + View Team + + + + {error ? ( + + + Unable to retrieve team data. + + + ) : ( + + + {isLoading ? ( + + ) : ( + + )} + + + {isLoading ? ( + Loading user list. This may take a moment. + ) : ( + + )} + + + )} + + + + ); +}; +AllocationsTeamViewModal.propTypes = { + isOpen: bool.isRequired, + toggle: func.isRequired, + pid: number.isRequired +}; + +export default AllocationsTeamViewModal; diff --git a/client/src/components/Allocations/AllocationsUsageTable/AllocationsUsageTable.js b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsUsageTable/AllocationsUsageTable.js similarity index 100% rename from client/src/components/Allocations/AllocationsUsageTable/AllocationsUsageTable.js rename to client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsUsageTable/AllocationsUsageTable.js diff --git a/client/src/components/Allocations/AllocationsUsageTable/AllocationsUsageTable.module.scss b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsUsageTable/AllocationsUsageTable.module.scss similarity index 100% rename from client/src/components/Allocations/AllocationsUsageTable/AllocationsUsageTable.module.scss rename to client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsUsageTable/AllocationsUsageTable.module.scss diff --git a/client/src/components/Allocations/AllocationsUsageTable/index.js b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsUsageTable/index.js similarity index 100% rename from client/src/components/Allocations/AllocationsUsageTable/index.js rename to client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsUsageTable/index.js diff --git a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/index.js b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/index.js new file mode 100644 index 0000000000..308b3f0136 --- /dev/null +++ b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/index.js @@ -0,0 +1 @@ +export { default } from './AllocationsTeamViewModal'; diff --git a/client/src/components/Allocations/AllocationsModals/index.js b/client/src/components/Allocations/AllocationsModals/index.js new file mode 100644 index 0000000000..3ac6e1b8c2 --- /dev/null +++ b/client/src/components/Allocations/AllocationsModals/index.js @@ -0,0 +1,2 @@ +export { default as AllocationsRequestModal } from './AllocationsRequestModal'; +export { default as AllocationsTeamViewModal } from './AllocationsTeamViewModal'; From bc815e30a7c3e5bbf176a6db41e1c9fd237cf2a0 Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Thu, 2 Jul 2020 16:08:30 -0500 Subject: [PATCH 08/20] CSS modules and test tweaks --- .../components/Allocations/Allocations.scss | 60 ------------------- .../AllocationsModals.test.js | 19 +++--- .../AllocationsRequestModal.js | 2 +- .../AllocationsContactCard.js | 7 ++- .../AllocationsContactCard.module.scss | 17 ++++++ .../AllocationsTeamTable.js | 46 +++++++------- .../AllocationsTeamTable.module.scss | 20 +++++++ .../AllocationsTeamViewModal.js | 12 ++-- .../AllocationsTeamViewModal.module.scss | 18 ++++++ 9 files changed, 98 insertions(+), 103 deletions(-) rename client/src/components/Allocations/{tests => AllocationsModals}/AllocationsModals.test.js (84%) create mode 100644 client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.module.scss create mode 100644 client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsTeamTable/AllocationsTeamTable.module.scss create mode 100644 client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsTeamViewModal.module.scss diff --git a/client/src/components/Allocations/Allocations.scss b/client/src/components/Allocations/Allocations.scss index 09c6b33640..1ff375ff7b 100644 --- a/client/src/components/Allocations/Allocations.scss +++ b/client/src/components/Allocations/Allocations.scss @@ -153,66 +153,6 @@ .allocations-request-body { color: #484848; } -.team-view-modal-wrapper { - .table-hover { - tbody { - /* FP-457: Will fix this *//* stylelint-disable no-descending-specificity */ - tr { - height: 35px; - cursor: pointer; - color: #484848; - td { - font-weight: 500; - font-size: 14px; - vertical-align: middle; - } - } - /* stylelint-enable no-descending-specificity */ - tr:hover { - background-color: #dfdaf5; - } - } - } - .modal-body { - overflow-y: initial !important; - } - .modal-left { - height: 50vh; - max-width: 230px; - overflow-y: scroll; - padding-left: 0; - padding-right: 0; - border-right: 1px solid rgba(112, 112, 112, 0.25); - } - .modal-right { - display: flex; - padding: 1em; - overflow: auto; - .contact-card { - flex-grow: 1; - display: flex; - flex-direction: column; - font: 16px Roboto, sans-serif; - height: 100%; - overflow: auto; - div { - padding: 0.25rem; - text-overflow: ellipsis; - overflow: auto; - } - .contact-card-title { - font-weight: bold; - } - } - } - .user-name { - margin-left: 1.5rem; - } - .active-user { - background-color: #dfdaf5; - } -} - /* HACK: The loading icon should not need height set, but it fixes Safari here */ /* FP-426: Do not use `height: 100%` or isolate usage pending solution to bigger problem */ #allocations-wrapper .loading-icon { diff --git a/client/src/components/Allocations/tests/AllocationsModals.test.js b/client/src/components/Allocations/AllocationsModals/AllocationsModals.test.js similarity index 84% rename from client/src/components/Allocations/tests/AllocationsModals.test.js rename to client/src/components/Allocations/AllocationsModals/AllocationsModals.test.js index 5922397529..47b1d7ce34 100644 --- a/client/src/components/Allocations/tests/AllocationsModals.test.js +++ b/client/src/components/Allocations/AllocationsModals/AllocationsModals.test.js @@ -3,19 +3,22 @@ import { render, fireEvent } from "@testing-library/react"; import { BrowserRouter } from "react-router-dom"; import configureStore from "redux-mock-store"; import { Provider } from "react-redux"; -import { NewAllocReq, TeamView } from "../AllocationsModals"; +import { AllocationsRequestModal, AllocationsTeamViewModal } from './index' const mockStore = configureStore(); describe("New Allocations Request Modal", () => { test("Allocations Request UI", () => { - const { getByText, getByTestId } = render( + const { getByText } = render( - null} /> + null} /> ); - expect(getByText("Manage Allocations")).toBeDefined(); - expect(getByTestId("request-body")).toBeDefined(); + const xrasLink = 'https://tacc-submit.xras.xsede.org/' + expect(getByText(/Manage Allocations/)).toBeDefined(); + expect(getByText(/You can manage your allocation/)).toBeDefined(); + expect(getByText(xrasLink)).toBeDefined(); + expect(getByText(xrasLink).href).toBe(xrasLink); }); }); @@ -43,7 +46,7 @@ describe("View Team Modal", () => { }); const { getByText } = render( - + ); @@ -100,7 +103,7 @@ describe("View Team Modal", () => { // Render Modal const { getByText, queryByText, getByRole } = render( - + ); @@ -139,7 +142,7 @@ describe("View Team Modal", () => { const { getByText } = render( - + ); diff --git a/client/src/components/Allocations/AllocationsModals/AllocationsRequestModal/AllocationsRequestModal.js b/client/src/components/Allocations/AllocationsModals/AllocationsRequestModal/AllocationsRequestModal.js index 42e19727e1..bbdec48e77 100644 --- a/client/src/components/Allocations/AllocationsModals/AllocationsRequestModal/AllocationsRequestModal.js +++ b/client/src/components/Allocations/AllocationsModals/AllocationsRequestModal/AllocationsRequestModal.js @@ -11,7 +11,7 @@ export const AllocationsRequestModal = ({ isOpen, toggle }) => ( > Manage Allocations - +

You can manage your allocation, your team members, or request more time on a machine by using your TACC user account credentials to access the diff --git a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.js b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.js index 3067307a30..2b24e6db45 100644 --- a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.js +++ b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.js @@ -2,6 +2,7 @@ import React from 'react'; import { string, shape } from 'prop-types'; import { capitalize, isEmpty } from 'lodash'; import AllocationsUsageTable from '../AllocationsUsageTable'; +import './AllocationsContactCard.module.scss'; const AllocationsContactCard = ({ listing }) => { if (!listing) @@ -10,11 +11,11 @@ const AllocationsContactCard = ({ listing }) => { ); const { firstName, lastName, email, username } = listing; return ( -

-
+
+
{capitalize(firstName)} {capitalize(lastName)}
-
+
Username: {username} | Email: {email}
{!isEmpty(listing.usageData) && ( diff --git a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.module.scss b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.module.scss new file mode 100644 index 0000000000..44581ef1e8 --- /dev/null +++ b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.module.scss @@ -0,0 +1,17 @@ +.root { + flex-grow: 1; + display: flex; + flex-direction: column; + font: 16px Roboto, sans-serif; + height: 100%; + overflow: auto; +} +.title { + padding: 0.25rem; + font-weight: bold; +} +.details { + padding: 0.25rem; + text-overflow: ellipsis; + overflow: auto; +} diff --git a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsTeamTable/AllocationsTeamTable.js b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsTeamTable/AllocationsTeamTable.js index b2a5203ce4..682d9da5f8 100644 --- a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsTeamTable/AllocationsTeamTable.js +++ b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsTeamTable/AllocationsTeamTable.js @@ -3,36 +3,36 @@ import { string, func, shape, arrayOf, object } from 'prop-types'; import { Table } from 'reactstrap'; import { useTable } from 'react-table'; import { capitalize } from 'lodash'; - -const UserCell = ({ cell: { value } }) => { - const { firstName, lastName } = value; - return ( - - {`${capitalize(firstName)} ${capitalize(lastName)}`} - - ); -}; -UserCell.propTypes = { - cell: shape({ - value: shape({ - firstName: string.isRequired, - lastName: string.isRequired - }).isRequired - }).isRequired -}; +import './AllocationsTeamTable.module.scss'; const AllocationsTeamTable = ({ rawData, clickHandler, visible }) => { const data = React.useMemo(() => rawData, [rawData]); const columns = React.useMemo( () => [ { - Header: 'name', - accessor: row => row, - UserCell + Header: 'listing', + accessor: el => el, + Cell: el => { + const { firstName, lastName } = el.value; + return ( + + {capitalize(firstName)} {capitalize(lastName)} + + ); + } } ], [rawData] ); + const getStyleName = listing => { + if ( + visible && + listing.firstName === visible.firstName && + listing.lastName === visible.lastName + ) + return 'active-user'; + return 'row'; + }; const { getTableProps, getTableBodyProps, rows, prepareRow } = useTable({ columns, data @@ -45,14 +45,14 @@ const AllocationsTeamTable = ({ rawData, clickHandler, visible }) => { return ( { - clickHandler(row.values.name); + clickHandler(row.values.listing); } })} + styleName={getStyleName(row.values.listing)} > {row.cells.map(cell => ( - {cell.render('UserCell')} + {cell.render('Cell')} ))} ); diff --git a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsTeamTable/AllocationsTeamTable.module.scss b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsTeamTable/AllocationsTeamTable.module.scss new file mode 100644 index 0000000000..45b13ec7f0 --- /dev/null +++ b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsTeamTable/AllocationsTeamTable.module.scss @@ -0,0 +1,20 @@ +.row { + height: 35px; + cursor: pointer; + color: #484848; + td { + font-weight: 500; + font-size: 14px; + vertical-align: middle; + } +} +.row:hover { + background-color: #dfdaf5; +} +.content { + margin-left: 1.5rem; +} +.active-user { + @extend .row; + background-color: #dfdaf5; +} diff --git a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsTeamViewModal.js b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsTeamViewModal.js index 48702a4382..c39c0bcd48 100644 --- a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsTeamViewModal.js +++ b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsTeamViewModal.js @@ -6,6 +6,7 @@ import { LoadingSpinner } from '_common'; import { has } from 'lodash'; import AllocationsTeamTable from './AllocationsTeamTable'; import AllocationsContactCard from './AllocationsContactCard'; +import './AllocationsTeamViewModal.module.scss'; const AllocationsTeamViewModal = ({ isOpen, toggle, pid }) => { const { teams, loadingUsernames, errors } = useSelector( @@ -15,12 +16,7 @@ const AllocationsTeamViewModal = ({ isOpen, toggle, pid }) => { const [card, setCard] = useState(null); const isLoading = loadingUsernames[pid] && loadingUsernames[pid].loading; return ( - + { ) : ( - + {isLoading ? ( ) : ( @@ -49,7 +45,7 @@ const AllocationsTeamViewModal = ({ isOpen, toggle, pid }) => { /> )} - + {isLoading ? ( Loading user list. This may take a moment. ) : ( diff --git a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsTeamViewModal.module.scss b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsTeamViewModal.module.scss new file mode 100644 index 0000000000..6d9107afea --- /dev/null +++ b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsTeamViewModal.module.scss @@ -0,0 +1,18 @@ +.root { + .modal-body { + overflow-y: initial !important; + } +} +.listing-wrapper { + height: 50vh; + max-width: 230px; + overflow-y: scroll; + padding-left: 0; + padding-right: 0; + border-right: 1px solid rgba(112, 112, 112, 0.25); +} +.information-wrapper { + display: flex; + padding: 1em; + overflow: auto; +} From dfd0aa5fad6d8f779d87c4c0cad2639281c3a6f2 Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Fri, 17 Jul 2020 15:20:14 -0500 Subject: [PATCH 09/20] Various Fixes Added units to entry Changed heading for usage Added 0 usage Changed exceptions in backend --- .../AllocationsModals.test.js | 9 +- .../AllocationsContactCard.js | 12 +- .../AllocationsUsageTable.js | 12 +- client/src/redux/sagas/allocations.sagas.js | 131 ++++++++++-------- server/portal/apps/users/utils.py | 14 +- server/portal/apps/users/views.py | 6 +- 6 files changed, 102 insertions(+), 82 deletions(-) diff --git a/client/src/components/Allocations/AllocationsModals/AllocationsModals.test.js b/client/src/components/Allocations/AllocationsModals/AllocationsModals.test.js index 47b1d7ce34..840db1a435 100644 --- a/client/src/components/Allocations/AllocationsModals/AllocationsModals.test.js +++ b/client/src/components/Allocations/AllocationsModals/AllocationsModals.test.js @@ -76,13 +76,13 @@ describe("View Team Modal", () => { email: "user2@gmail.com", usageData: [ { - usage: 0.5, + usage: '0.5 SU', resource: "stampede2.tacc.utexas.edu", allocationId: 1, percentUsed: 0.005, }, { - usage: 10, + usage: '10 SU', resource: "frontera.tacc.utexas.edu", allocationId: 2, percentUsed: 10, @@ -116,11 +116,12 @@ describe("View Team Modal", () => { fireEvent.click(getByText(/Test User1/)); expect(getByText(/Username:/)).toBeDefined(); expect(getByText(/Email:/)).toBeDefined(); - expect(queryByText(/Usage/)).toBeNull(); + expect(queryByText(/Usage/)).toBeDefined(); // View information for the user with usage fireEvent.click(getByText(/Test User2/)); - expect(getByText(/Usage/)).toBeDefined(); + expect(getByText(/Frontera/)).toBeDefined(); + expect(getByText(/Stampede 2/)).toBeDefined(); }); test("View Team Modal Errors", () => { diff --git a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.js b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.js index 2b24e6db45..77725e8918 100644 --- a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.js +++ b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.js @@ -1,6 +1,6 @@ import React from 'react'; -import { string, shape } from 'prop-types'; -import { capitalize, isEmpty } from 'lodash'; +import { string, shape, arrayOf, object } from 'prop-types'; +import { capitalize } from 'lodash'; import AllocationsUsageTable from '../AllocationsUsageTable'; import './AllocationsContactCard.module.scss'; @@ -10,6 +10,7 @@ const AllocationsContactCard = ({ listing }) => { Click on a user’s name to view their contact information. ); const { firstName, lastName, email, username } = listing; + return (
@@ -18,9 +19,7 @@ const AllocationsContactCard = ({ listing }) => {
Username: {username} | Email: {email}
- {!isEmpty(listing.usageData) && ( - - )} +
); }; @@ -30,7 +29,8 @@ AllocationsContactCard.propTypes = { firstName: string.isRequired, lastName: string.isRequired, email: string.isRequired, - username: string.isRequired + username: string.isRequired, + usageData: arrayOf(object).isRequired }) }; AllocationsContactCard.defaultProps = { listing: {} }; diff --git a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsUsageTable/AllocationsUsageTable.js b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsUsageTable/AllocationsUsageTable.js index 2ff9b28bc8..494e7c1f91 100644 --- a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsUsageTable/AllocationsUsageTable.js +++ b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsUsageTable/AllocationsUsageTable.js @@ -1,7 +1,7 @@ import React from 'react'; import { useTable } from 'react-table'; import { capitalize } from 'lodash'; -import { arrayOf, shape, string, number } from 'prop-types'; +import { arrayOf, shape, string } from 'prop-types'; import './AllocationsUsageTable.module.scss'; const AllocationsUsageTable = ({ rawData }) => { @@ -20,18 +20,19 @@ const AllocationsUsageTable = ({ rawData }) => { return sysName; } }, - { Header: 'Usage', accessor: 'usage' }, + { Header: 'Individual Usage', accessor: 'usage' }, { Header: '% of Allocation', accessor: entry => { if (entry.percentUsed >= 1) { return `${entry.percentUsed}%`; } + if (entry.percentUsed === 0) return '0%'; return `< 1%`; } } ], - [rawData] + [] ); const { getTableBodyProps, @@ -77,9 +78,10 @@ AllocationsUsageTable.propTypes = { rawData: arrayOf( shape({ resource: string, - usage: number + usage: string }) - ).isRequired + ) }; +AllocationsUsageTable.defaultProps = { rawData: [] }; export default AllocationsUsageTable; diff --git a/client/src/redux/sagas/allocations.sagas.js b/client/src/redux/sagas/allocations.sagas.js index b5c3fe2fe3..e791766803 100644 --- a/client/src/redux/sagas/allocations.sagas.js +++ b/client/src/redux/sagas/allocations.sagas.js @@ -6,18 +6,10 @@ import { all, select } from 'redux-saga/effects'; -import { chain, flatten } from 'lodash'; +import { chain, flatten, isEmpty, find } from 'lodash'; import { fetchUtil } from 'utils/fetchUtil'; import 'cross-fetch'; -const getAllocationsUtil = async () => { - const res = await fetchUtil({ - url: '/api/users/allocations/' - }); - const json = res.response; - return json; -}; - export function* getAllocations(action) { yield put({ type: 'START_ADD_ALLOCATIONS' }); try { @@ -32,6 +24,33 @@ export function* getAllocations(action) { } } +const getAllocationsUtil = async () => { + const res = await fetchUtil({ + url: '/api/users/allocations/' + }); + const json = res.response; + return json; +}; +const getTeamsUtil = async team => { + const res = await fetchUtil({ url: `/api/users/team/${team}` }); + const json = res.response; + return json; +}; +const populateTeamsUtil = data => { + const allocations = { active: data.active, inactive: data.inactive }; + const teams = flatten(Object.values(allocations)).reduce( + (obj, item) => ({ ...obj, [item.projectId]: {} }), + {} + ); + + const loadingTeams = Object.keys(teams).reduce( + (obj, teamID) => ({ ...obj, [teamID]: { loading: true } }), + {} + ); + + return { teams, loadingTeams }; +}; + function* getUsernames(action) { try { const json = yield call(getTeamsUtil, action.payload.name); @@ -59,20 +78,18 @@ function* getUsernames(action) { }); } } - -const populateTeamsUtil = data => { - const allocations = { active: data.active, inactive: data.inactive }; - const teams = flatten(Object.values(allocations)).reduce( - (obj, item) => ({ ...obj, [item.projectId]: {} }), - {} - ); - - const loadingTeams = Object.keys(teams).reduce( - (obj, teamID) => ({ ...obj, [teamID]: { loading: true } }), - {} - ); - - return { teams, loadingTeams }; +const getUsageUtil = async params => { + const res = await fetchUtil({ + url: `/api/users/team/usage/${params.id}` + }); + const data = res.response + .map(user => ({ + ...user, + resource: params.system.host, + allocationId: params.id + })) + .filter(Boolean); + return data; }; const teamPayloadUtil = ( @@ -98,51 +115,49 @@ const teamPayloadUtil = ( const individualUsage = usageData.filter( val => val.username === username ); - - if (!individualUsage) return user; - return { + const currentProject = allocations.find( + allocation => allocation.projectId === id + ); + const userData = { ...user, - usageData: individualUsage.map(val => { - const totalAllocated = chain(allocations) - .map('systems') - .flatten() - .filter({ host: val.resource }) - .map('allocation') - .filter({ id: val.allocationId }) - .head() - .value().computeAllocated; - + usageData: currentProject.systems.map(system => { return { - usage: val.usage, - resource: val.resource, - allocationId: val.allocationId, - percentUsed: val.usage / totalAllocated + usage: `0 ${system.type === 'HPC' ? 'SU' : 'GB'}`, + resource: system.host, + allocationId: system.allocation.id, + percentUsed: 0 }; }) }; + if (isEmpty(individualUsage)) return userData; + return { + ...userData, + usageData: userData.usageData.map(entry => { + const current = find(individualUsage, { resource: entry.resource }); + if (current) { + const { computeAllocated, type } = chain(allocations) + .map('systems') + .flatten() + .filter({ host: current.resource }) + .map('allocation') + .filter({ id: current.allocationId }) + .head() + .value(); + return { + usage: `${current.usage} ${type === 'HPC' ? 'SU' : 'GB'}`, + resource: current.resource, + allocationId: current.allocationId, + percentUsed: current.usage / computeAllocated + }; + } + return entry; + }) + }; }) }; return { data, loading }; }; -const getTeamsUtil = async team => { - const res = await fetchUtil({ url: `/api/users/team/${team}` }); - const json = res.response; - return json; -}; - -const getUsageUtil = async params => { - const res = await fetchUtil({ - url: `/api/users/team/usage/${params.id}` - }); - const data = res.response - .map(user => { - return { ...user, resource: params.system.host, allocationId: params.id }; - }) - .filter(Boolean); - return data; -}; - export function* watchAllocationData() { yield takeEvery('GET_ALLOCATIONS', getAllocations); } diff --git a/server/portal/apps/users/utils.py b/server/portal/apps/users/utils.py index 5c35124496..345ff9a8ca 100644 --- a/server/portal/apps/users/utils.py +++ b/server/portal/apps/users/utils.py @@ -4,6 +4,8 @@ import logging import requests +from portal.exceptions.api import ApiException + logger = logging.getLogger(__name__) @@ -175,10 +177,11 @@ def get_usernames(project_name): auth = requests.auth.HTTPBasicAuth(settings.TAS_CLIENT_KEY, settings.TAS_CLIENT_SECRET) r = requests.get('{0}/v1/projects/name/{1}/users'.format(settings.TAS_URL, project_name), auth=auth) resp = r.json() + logger.debug(resp) if resp['status'] == 'success': return resp['result'] else: - raise Exception('Failed to get project users', resp['message']) + raise ApiException('Failed to get project users', resp['message']) def get_user_data(username): @@ -192,19 +195,18 @@ def get_user_data(username): credentials={ 'username': settings.TAS_CLIENT_KEY, 'password': settings.TAS_CLIENT_SECRET - } + } ) user_data = tas_client.get_user(username=username) return user_data -# TODO: Look up best practices for docstrings -def get_allocation_usage_by_user(allocation_id): +def get_per_user_allocation_usage(allocation_id): auth = requests.auth.HTTPBasicAuth(settings.TAS_CLIENT_KEY, settings.TAS_CLIENT_SECRET) r = requests.get('{0}/v1/allocations/{1}/usage'.format(settings.TAS_URL, allocation_id), auth=auth) - logger.info(r) resp = r.json() + logger.debug(resp) if resp['status'] == 'success': return resp['result'] else: - raise Exception('Failed to get project users', resp['message']) + raise ApiException('Failed to get project users', resp['message']) diff --git a/server/portal/apps/users/views.py b/server/portal/apps/users/views.py index 0d50420d18..18c0cb1162 100644 --- a/server/portal/apps/users/views.py +++ b/server/portal/apps/users/views.py @@ -11,7 +11,7 @@ from elasticsearch_dsl import Q from portal.libs.elasticsearch.docs.base import IndexedFile from pytas.http import TASClient -from portal.apps.users.utils import get_allocations, get_usernames, get_user_data, get_allocation_usage_by_user +from portal.apps.users.utils import get_allocations, get_usernames, get_user_data, get_per_user_allocation_usage logger = logging.getLogger(__name__) @@ -144,5 +144,5 @@ def get(self, request, username): class AllocationUsageView(BaseApiView): def get(self, request, allocation_id): - usage = get_allocation_usage_by_user(allocation_id) - return JsonResponse({'response': usage, "status": 200}) + usage = get_per_user_allocation_usage(allocation_id) + return JsonResponse({'response': usage}) From 465392725a02c01415e7e404320dac7c6e8098b2 Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Fri, 17 Jul 2020 16:16:21 -0500 Subject: [PATCH 10/20] Fixed units --- .../AllocationsContactCard/AllocationsContactCard.js | 2 +- client/src/redux/sagas/allocations.sagas.js | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.js b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.js index 77725e8918..a23b0777bd 100644 --- a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.js +++ b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.js @@ -17,7 +17,7 @@ const AllocationsContactCard = ({ listing }) => { {capitalize(firstName)} {capitalize(lastName)}
- Username: {username} | Email: {email} + Username: {username} | Email: {email}
diff --git a/client/src/redux/sagas/allocations.sagas.js b/client/src/redux/sagas/allocations.sagas.js index e791766803..7b2c2fc449 100644 --- a/client/src/redux/sagas/allocations.sagas.js +++ b/client/src/redux/sagas/allocations.sagas.js @@ -122,6 +122,7 @@ const teamPayloadUtil = ( ...user, usageData: currentProject.systems.map(system => { return { + type: system.type, usage: `0 ${system.type === 'HPC' ? 'SU' : 'GB'}`, resource: system.host, allocationId: system.allocation.id, @@ -135,19 +136,19 @@ const teamPayloadUtil = ( usageData: userData.usageData.map(entry => { const current = find(individualUsage, { resource: entry.resource }); if (current) { - const { computeAllocated, type } = chain(allocations) + const totalAllocated = chain(allocations) .map('systems') .flatten() .filter({ host: current.resource }) .map('allocation') .filter({ id: current.allocationId }) .head() - .value(); + .value().computeAllocated; return { - usage: `${current.usage} ${type === 'HPC' ? 'SU' : 'GB'}`, + usage: `${current.usage} ${entry.type === 'HPC' ? 'SU' : 'GB'}`, resource: current.resource, allocationId: current.allocationId, - percentUsed: current.usage / computeAllocated + percentUsed: current.usage / totalAllocated }; } return entry; From 8693ca428d75a799f644ad9ceb06a2ed6c689b6c Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Mon, 20 Jul 2020 15:04:15 -0500 Subject: [PATCH 11/20] Added JSDoc comments to utils --- client/src/redux/sagas/allocations.sagas.js | 43 +++++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/client/src/redux/sagas/allocations.sagas.js b/client/src/redux/sagas/allocations.sagas.js index 7b2c2fc449..15ca98935f 100644 --- a/client/src/redux/sagas/allocations.sagas.js +++ b/client/src/redux/sagas/allocations.sagas.js @@ -23,7 +23,11 @@ export function* getAllocations(action) { yield put({ type: 'ADD_ALLOCATIONS_ERROR', payload: error }); } } - +/** + * Fetch allocations data + * @async + * @returns {{portal_alloc: String, active: Array, inactive: Array, hosts: Object}} + */ const getAllocationsUtil = async () => { const res = await fetchUtil({ url: '/api/users/allocations/' @@ -31,11 +35,24 @@ const getAllocationsUtil = async () => { const json = res.response; return json; }; -const getTeamsUtil = async team => { - const res = await fetchUtil({ url: `/api/users/team/${team}` }); + +/** + * Fetch user data for a project + * @param {String} projectId - project id + */ +const getTeamsUtil = async projectId => { + const res = await fetchUtil({ url: `/api/users/team/${projectId}` }); const json = res.response; return json; }; + +/** + * Generate an empty dictionary to look up users from project ID and map loading state + * to each project + * @param {{portal_alloc: String, active: Array, inactive: Array, hosts: Object}} data - + * Allocations data + * @returns {{teams: Object, loadingTeams: {}}} + */ const populateTeamsUtil = data => { const allocations = { active: data.active, inactive: data.inactive }; const teams = flatten(Object.values(allocations)).reduce( @@ -78,6 +95,14 @@ function* getUsernames(action) { }); } } + +/** + * Fetch Usage For an Allocation and Return an Array of Users with their data, + * resource used, and allocation id. + * @async + * @param {{id: Number, system: Object}} params + * @returns {{user: Object, resource: String, allocationId: Number}[]} data + */ const getUsageUtil = async params => { const res = await fetchUtil({ url: `/api/users/team/usage/${params.id}` @@ -92,6 +117,18 @@ const getUsageUtil = async params => { return data; }; +/** + * Generate a payload for the User Data saga. + * When there is not an error, this function maps team data to Projects. + * Each user has an entry for the resources in the allocation and if they have + * usage data, it is added to their entry + * @param {Number} id - Project Id + * @param {Object} obj - User Data + * @param {Boolean} error - Error present + * @param {Object} usageData - Usage Data + * @param {Array} allocations - All allocations + * @returns {{data: Object, loading: Boolean}} + */ const teamPayloadUtil = ( id, obj, From 484cf1494a2c794e6023f886bcd2c223bb954647 Mon Sep 17 00:00:00 2001 From: "W. Bomar" <62723358+tacc-wbomar@users.noreply.github.com> Date: Thu, 30 Jul 2020 13:42:16 -0500 Subject: [PATCH 12/20] Task/FP-204: UI tweak (#114) Quick solution for more space --- .../AllocationsContactCard/AllocationsContactCard.js | 4 +++- .../AllocationsContactCard/AllocationsContactCard.module.scss | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.js b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.js index a23b0777bd..f801101f3f 100644 --- a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.js +++ b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.js @@ -17,7 +17,9 @@ const AllocationsContactCard = ({ listing }) => { {capitalize(firstName)} {capitalize(lastName)}
- Username: {username} | Email: {email} + Username: {username} +   |   + Email: {email}
diff --git a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.module.scss b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.module.scss index 44581ef1e8..eb55cb6094 100644 --- a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.module.scss +++ b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.module.scss @@ -14,4 +14,8 @@ padding: 0.25rem; text-overflow: ellipsis; overflow: auto; + + & > strong { + font-weight: 500; + } } From 9c2f7d929f54e5c4195c36896f6c5ca827f0352d Mon Sep 17 00:00:00 2001 From: Wesley Bomar Date: Thu, 30 Jul 2020 13:48:15 -0500 Subject: [PATCH 13/20] FP-204: UI: Fix unit test (late commit) --- .../Allocations/AllocationsModals/AllocationsModals.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/components/Allocations/AllocationsModals/AllocationsModals.test.js b/client/src/components/Allocations/AllocationsModals/AllocationsModals.test.js index 840db1a435..335a635945 100644 --- a/client/src/components/Allocations/AllocationsModals/AllocationsModals.test.js +++ b/client/src/components/Allocations/AllocationsModals/AllocationsModals.test.js @@ -114,8 +114,8 @@ describe("View Team Modal", () => { // View Information for the user without usage fireEvent.click(getByText(/Test User1/)); - expect(getByText(/Username:/)).toBeDefined(); - expect(getByText(/Email:/)).toBeDefined(); + expect(getByText(/Username/)).toBeDefined(); + expect(getByText(/Email/)).toBeDefined(); expect(queryByText(/Usage/)).toBeDefined(); // View information for the user with usage From f6948cfb21af09c5cdee7ce7aec944b6a7dbc1de Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Tue, 4 Aug 2020 12:03:45 -0500 Subject: [PATCH 14/20] Changed wording for initial state of modal Clear the view in between openings Active user check --- .../AllocationsContactCard/AllocationsContactCard.js | 4 +--- .../AllocationsTeamTable/AllocationsTeamTable.js | 7 +------ .../AllocationsTeamViewModal.js | 11 ++++++++++- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.js b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.js index f801101f3f..8d404de5c0 100644 --- a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.js +++ b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.js @@ -6,9 +6,7 @@ import './AllocationsContactCard.module.scss'; const AllocationsContactCard = ({ listing }) => { if (!listing) - return ( - Click on a user’s name to view their contact information. - ); + return Click on a user’s name to view their allocation usage.; const { firstName, lastName, email, username } = listing; return ( diff --git a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsTeamTable/AllocationsTeamTable.js b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsTeamTable/AllocationsTeamTable.js index 682d9da5f8..b8287b7aaf 100644 --- a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsTeamTable/AllocationsTeamTable.js +++ b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsTeamTable/AllocationsTeamTable.js @@ -25,12 +25,7 @@ const AllocationsTeamTable = ({ rawData, clickHandler, visible }) => { [rawData] ); const getStyleName = listing => { - if ( - visible && - listing.firstName === visible.firstName && - listing.lastName === visible.lastName - ) - return 'active-user'; + if (visible && listing.username === visible.username) return 'active-user'; return 'row'; }; const { getTableProps, getTableBodyProps, rows, prepareRow } = useTable({ diff --git a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsTeamViewModal.js b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsTeamViewModal.js index c39c0bcd48..39fd0e6f36 100644 --- a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsTeamViewModal.js +++ b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsTeamViewModal.js @@ -15,8 +15,17 @@ const AllocationsTeamViewModal = ({ isOpen, toggle, pid }) => { const error = has(errors.teams, pid); const [card, setCard] = useState(null); const isLoading = loadingUsernames[pid] && loadingUsernames[pid].loading; + const resetCard = () => { + setCard(null); + }; return ( - + Date: Wed, 5 Aug 2020 09:19:57 -0500 Subject: [PATCH 15/20] Task/fp 204 ui tweak with fp 557 (#133) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Allocations usage by user Added endpoint Saga and ui with reacstrap * Refactored sagas * UsageDisplayed on modal Added markup and styles for table Added utils to parse data * Allocation modals tests Refactored allocation request UI flow for team view modal tests * Test Coverage and UI tweaks Added coverage for loading/errors Formatting of resource * Refactored UsageTable Renamed and reworked file structure Implemented css module * Modals in separate directory * CSS modules and test tweaks * Various Fixes Added units to entry Changed heading for usage Added 0 usage Changed exceptions in backend * Fixed units * Added JSDoc comments to utils * FP-204: UI: Swap contact card bold, and less bold * Task/fp 515 fp 354 support messages and new icons (#91) * Task/fp 515 install cortal icons (#90) * Task/fp 228/fp 515 isntall cortal icons (#89) * FP-515: Add Icon component from FP-354 * FP-515: Add Icon component usage from FP-354 * FP-515: Install Cortal 1.2 - Disable linting of `icon.fonts.css`. - Support WOFF in Webpack config. - Update several files (and tests) to use new icon names: - AppIcon - AppBrowser - DataFilesListingCell - DataFilesSidebar - DataFilesToolbar - Sidebar - TicketModal - Replace old Core Portal font with Cortal Icons font. - Migrate font styles to their own stylesheet. - Add icon aliases: - icon-collapse (icon-contract) * FP-515: Disable an "extra icon" * FP-515: Fix neglected instances of `icon-upload` * FP-228: Hide accessible Icon text * FP-228: Allow extra spaces in CSS before ruleset * Task/fp 354 message component (#88) * WIP: FP-354: Add component, icon sub-comp, tests To Do: - Run tests - Fix lint errors - Create tests for icon sub-component - Graduate icon sub-component to component * FP-354: Fix test * FP-354: Fix ES Lint error. Remove test CSS. * FP-354: Test sub-component * FP-354: Graduate MessageIcon to Icon * FP-354: Support `AppIcon` use of `Icon` * FP-354: Revert test tweaks to jest.config.js roots * FP-354: Support shortcut path to load new commons * FP-354: Accessible icon text. Missing icon class. * FP-354: Message text and icon text as `children` * FP-354: Styles for Message's * FP-354: Use `Message` in `DataFilesTable.js` * Quick: CSS lint allow align single-line rulesets * FP-354: Add missing `success` type * FP-354: Success type and fixes * FP-354: Fixes. Require icon. Color info type. * FP-534: Comments. Color var naming tweak. * FP-354: Fix AppIcon test fail (missing icon class) * FP-354: Expand tests. Simplify class match syntax. * Noop: Remove style remark from function comment * FP-354: CHoose different icon for `info` type * FP-515/FP-317: Update icons * Task/fp 537 add section for ui patterns (#93) * FP-537: New Section "UI Patterns" * FP-537: Update Sidebar tests * FP-537: More accurate `isDebug` comment * FP-537: Use new icon for 'UI Patterns' section * FP-357: Document `UIPatterns` component * FP-515/FP-354: Cleanup Icons & Messages * FP-354: More tests with less code * FP-354/FP-537: Add Message UI Patterns (#95) Follow up to FP-354 that was pending FP-537 * Quick: Remove outdated comments * FP-354/FP-537: Context for Message UI Patterns * FP-354/FP-537: Fix Unit Test i.e. Remove styleName * FP-354: Render/Style Links in UPatternsMessages * FP-537: Unset Bootstrap styles for `code` tag * FP-354: Replace `Action` w/ `link` tagged template * FP-515 (with FP-354): Change Icon Size to 18px … And replace `.app-icon` and `.category-icon` with `.icon`. * FP-557: New `
` Component * FP-204/FP-557: Use DL UI Comp in Allocations Modal * FP-557: Finish Unit Tests * Quick: Allow `composes` to Pass CSS LInter * FP-204: UI: Quick: Remove extra code (from merge) * FP-204/FP-557: Remove margin on horz DL Co-authored-by: Owais Jamil --- .../AllocationsContactCard.js | 17 +++++++++++------ .../AllocationsContactCard.module.scss | 6 ------ .../DescriptionList/DescriptionList.module.scss | 7 +++++-- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.js b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.js index 8d404de5c0..e708ac3855 100644 --- a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.js +++ b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.js @@ -1,6 +1,7 @@ import React from 'react'; import { string, shape, arrayOf, object } from 'prop-types'; import { capitalize } from 'lodash'; +import { DescriptionList } from '_common'; import AllocationsUsageTable from '../AllocationsUsageTable'; import './AllocationsContactCard.module.scss'; @@ -12,13 +13,17 @@ const AllocationsContactCard = ({ listing }) => { return (
- {capitalize(firstName)} {capitalize(lastName)}
-
-
- Username: {username} -   |   - Email: {email} + {capitalize(firstName)} {capitalize(lastName)}
+
); diff --git a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.module.scss b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.module.scss index eb55cb6094..dee58212c6 100644 --- a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.module.scss +++ b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.module.scss @@ -12,10 +12,4 @@ } .details { padding: 0.25rem; - text-overflow: ellipsis; - overflow: auto; - - & > strong { - font-weight: 500; - } } diff --git a/client/src/components/_common/DescriptionList/DescriptionList.module.scss b/client/src/components/_common/DescriptionList/DescriptionList.module.scss index d730a2d8b1..8c34dc635a 100644 --- a/client/src/components/_common/DescriptionList/DescriptionList.module.scss +++ b/client/src/components/_common/DescriptionList/DescriptionList.module.scss @@ -1,7 +1,10 @@ @import '../../../styles/tools/mixins.scss'; -.container { - /* … */ +.container.is-horz { + margin-bottom: 0; /* overwrite Bootstrap's `_reboot.scss` */ + & dd { + margin-bottom: 0; /* overwrite Bootstrap's `_reboot.scss` */ + } } /* Children */ From bc793e9c0412f9cf6c706d183db7db4bc8bdc72e Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Thu, 6 Aug 2020 12:13:44 -0500 Subject: [PATCH 16/20] Changed allocation computation --- client/src/redux/sagas/allocations.sagas.js | 49 +++++++++++++-------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/client/src/redux/sagas/allocations.sagas.js b/client/src/redux/sagas/allocations.sagas.js index 15ca98935f..096ddffc50 100644 --- a/client/src/redux/sagas/allocations.sagas.js +++ b/client/src/redux/sagas/allocations.sagas.js @@ -6,7 +6,7 @@ import { all, select } from 'redux-saga/effects'; -import { chain, flatten, isEmpty, find } from 'lodash'; +import { chain, flatten, isEmpty } from 'lodash'; import { fetchUtil } from 'utils/fetchUtil'; import 'cross-fetch'; @@ -78,15 +78,16 @@ function* getUsernames(action) { ...state.allocations.active, ...state.allocations.inactive ]); + const payload = teamPayloadUtil( + action.payload.projectId, + json, + false, + flatten(usage), + allocations + ); yield put({ type: 'ADD_USERNAMES_TO_TEAM', - payload: teamPayloadUtil( - action.payload.projectId, - json, - false, - flatten(usage), - allocations - ) + payload }); } catch (error) { yield put({ @@ -144,6 +145,7 @@ const teamPayloadUtil = ( }; } + // Add usage entries for a project const data = { [id]: obj .sort((a, b) => a.firstName.localeCompare(b.firstName)) @@ -158,11 +160,11 @@ const teamPayloadUtil = ( const userData = { ...user, usageData: currentProject.systems.map(system => { + // Create empty entry for each resource return { type: system.type, usage: `0 ${system.type === 'HPC' ? 'SU' : 'GB'}`, resource: system.host, - allocationId: system.allocation.id, percentUsed: 0 }; }) @@ -171,21 +173,30 @@ const teamPayloadUtil = ( return { ...userData, usageData: userData.usageData.map(entry => { - const current = find(individualUsage, { resource: entry.resource }); - if (current) { + const current = individualUsage.filter( + d => d.resource === entry.resource + ); + if (!isEmpty(current)) { + // Add usage data to empty entries const totalAllocated = chain(allocations) .map('systems') .flatten() - .filter({ host: current.resource }) + .filter({ host: entry.resource }) .map('allocation') - .filter({ id: current.allocationId }) - .head() - .value().computeAllocated; + .filter({ projectId: id }) + .reduce( + (sum, { computeAllocated }) => sum + computeAllocated, + 0 + ) + .value(); + const totalUsed = current.reduce( + (sum, { usage }) => sum + usage, + 0 + ); return { - usage: `${current.usage} ${entry.type === 'HPC' ? 'SU' : 'GB'}`, - resource: current.resource, - allocationId: current.allocationId, - percentUsed: current.usage / totalAllocated + usage: `${totalUsed} ${entry.type === 'HPC' ? 'SU' : 'GB'}`, + resource: entry.resource, + percentUsed: totalUsed / totalAllocated }; } return entry; From b859bd1dfb3adb2827280528fc882192a6e77e6a Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Thu, 6 Aug 2020 12:26:33 -0500 Subject: [PATCH 17/20] Fix storage systems showing 0gb --- .../Allocations/AllocationsUtils.js | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/client/src/components/Allocations/AllocationsUtils.js b/client/src/components/Allocations/AllocationsUtils.js index 08222d2325..6b4778fcfe 100644 --- a/client/src/components/Allocations/AllocationsUtils.js +++ b/client/src/components/Allocations/AllocationsUtils.js @@ -1,24 +1,28 @@ export default function systemAccessor(arr, header) { switch (header) { case 'Awarded': - return arr.map(({ allocation: { computeAllocated, id }, type }) => ({ - awarded: Math.round(computeAllocated), + return arr.map(({ allocation, type }) => ({ + awarded: + type === 'HPC' + ? Math.round(allocation.computeAllocated) + : Math.round(allocation.storageAllocated), type, - id + id: allocation.id })); case 'Remaining': - return arr.map( - ({ allocation: { id, computeAllocated, computeUsed }, type }) => { - const remaining = Math.round(computeAllocated - computeUsed); - const ratio = remaining / computeAllocated || 0; - return { - id, - remaining, - ratio, - type - }; - } - ); + return arr.map(({ allocation, type }) => { + const remaining = + type === 'HPC' + ? Math.round(allocation.computeAllocated - allocation.computeUsed) + : Math.round(allocation.storageAllocated); + const ratio = remaining / allocation.computeAllocated || 0; + return { + id: allocation.id, + remaining, + ratio, + type + }; + }); case 'Expires': return arr.map(({ allocation: { end, id } }) => ({ id, From 38009d1333aacd79ae158af1a4289124341045a0 Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Thu, 6 Aug 2020 13:16:11 -0500 Subject: [PATCH 18/20] Badge fix --- client/src/components/Allocations/AllocationsUtils.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/components/Allocations/AllocationsUtils.js b/client/src/components/Allocations/AllocationsUtils.js index 6b4778fcfe..a1ab2feba6 100644 --- a/client/src/components/Allocations/AllocationsUtils.js +++ b/client/src/components/Allocations/AllocationsUtils.js @@ -15,7 +15,8 @@ export default function systemAccessor(arr, header) { type === 'HPC' ? Math.round(allocation.computeAllocated - allocation.computeUsed) : Math.round(allocation.storageAllocated); - const ratio = remaining / allocation.computeAllocated || 0; + const ratio = + type === 'HPC' ? remaining / allocation.computeAllocated || 0 : 1; return { id: allocation.id, remaining, From 1ae9a21dd6f5b3c5adebe4a1c3e33be204a93c16 Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Thu, 6 Aug 2020 13:37:55 -0500 Subject: [PATCH 19/20] Contact information padding removed --- .../AllocationsContactCard/AllocationsContactCard.module.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.module.scss b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.module.scss index dee58212c6..a264f5957f 100644 --- a/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.module.scss +++ b/client/src/components/Allocations/AllocationsModals/AllocationsTeamViewModal/AllocationsContactCard/AllocationsContactCard.module.scss @@ -7,9 +7,9 @@ overflow: auto; } .title { - padding: 0.25rem; + padding: 0.25rem 0; font-weight: bold; } .details { - padding: 0.25rem; + padding: 0.25rem 0; } From c6241558cbb77fd2ab876be2c61e8aac5cee3328 Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Fri, 7 Aug 2020 11:17:15 -0500 Subject: [PATCH 20/20] Cell width and font size --- .../components/Allocations/Allocations.scss | 5 ++++- .../Allocations/AllocationsTables.js | 20 ++++++++++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/client/src/components/Allocations/Allocations.scss b/client/src/components/Allocations/Allocations.scss index 1ff375ff7b..aa4bd34845 100644 --- a/client/src/components/Allocations/Allocations.scss +++ b/client/src/components/Allocations/Allocations.scss @@ -135,7 +135,7 @@ } } .alloc-badge { - font-size: 11px; + font-size: 14px; } } #pending { @@ -158,3 +158,6 @@ #allocations-wrapper .loading-icon { height: auto; } +.system-cell { + width: 120px; +} diff --git a/client/src/components/Allocations/AllocationsTables.js b/client/src/components/Allocations/AllocationsTables.js index b35f1d76ee..1b2eb07015 100644 --- a/client/src/components/Allocations/AllocationsTables.js +++ b/client/src/components/Allocations/AllocationsTables.js @@ -41,25 +41,29 @@ export const useAllocations = page => { Header: 'Systems', accessor: ({ systems }) => systemAccessor(systems, 'Systems'), id: 'name', - Cell: Systems + Cell: Systems, + className: 'system-cell' }, { Header: 'Awarded', accessor: ({ systems }) => systemAccessor(systems, 'Awarded'), id: 'awarded', - Cell: Awarded + Cell: Awarded, + className: 'system-cell' }, { Header: 'Remaining', accessor: ({ systems }) => systemAccessor(systems, 'Remaining'), id: 'remaining', - Cell: Remaining + Cell: Remaining, + className: 'system-cell' }, { Header: 'Expires', accessor: ({ systems }) => systemAccessor(systems, 'Expires'), id: 'expires', - Cell: Expires + Cell: Expires, + className: 'system-cell' } ], [allocations] @@ -132,7 +136,13 @@ export const AllocationsTable = ({ page }) => { return ( {row.cells.map(cell => ( - {cell.render('Cell')} + + {cell.render('Cell')} + ))} );