Merge pull request #620 from dod-ccpo/reskin-members-page
Reskin team management pages
This commit is contained in:
commit
37df5b4b9c
@ -23,26 +23,25 @@ from atst.models.permissions import Permissions
|
|||||||
from atst.utils.flash import formatted_flash as flash
|
from atst.utils.flash import formatted_flash as flash
|
||||||
|
|
||||||
|
|
||||||
|
def serialize_portfolio_role(portfolio_role):
|
||||||
|
return {
|
||||||
|
"name": portfolio_role.user_name,
|
||||||
|
"status": portfolio_role.display_status,
|
||||||
|
"id": portfolio_role.user_id,
|
||||||
|
"role": portfolio_role.role_displayname,
|
||||||
|
"num_env": portfolio_role.num_environment_roles,
|
||||||
|
"edit_link": url_for(
|
||||||
|
"portfolios.view_member",
|
||||||
|
portfolio_id=portfolio_role.portfolio_id,
|
||||||
|
member_id=portfolio_role.user_id,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@portfolios_bp.route("/portfolios/<portfolio_id>/members")
|
@portfolios_bp.route("/portfolios/<portfolio_id>/members")
|
||||||
def portfolio_members(portfolio_id):
|
def portfolio_members(portfolio_id):
|
||||||
portfolio = Portfolios.get_with_members(g.current_user, portfolio_id)
|
portfolio = Portfolios.get_with_members(g.current_user, portfolio_id)
|
||||||
new_member_name = http_request.args.get("newMemberName")
|
members_list = [serialize_portfolio_role(k) for k in portfolio.members]
|
||||||
new_member = next(
|
|
||||||
filter(lambda m: m.user_name == new_member_name, portfolio.members), None
|
|
||||||
)
|
|
||||||
members_list = [
|
|
||||||
{
|
|
||||||
"name": k.user_name,
|
|
||||||
"status": k.display_status,
|
|
||||||
"id": k.user_id,
|
|
||||||
"role": k.role_displayname,
|
|
||||||
"num_env": k.num_environment_roles,
|
|
||||||
"edit_link": url_for(
|
|
||||||
"portfolios.view_member", portfolio_id=portfolio.id, member_id=k.user_id
|
|
||||||
),
|
|
||||||
}
|
|
||||||
for k in portfolio.members
|
|
||||||
]
|
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"portfolios/members/index.html",
|
"portfolios/members/index.html",
|
||||||
@ -50,7 +49,21 @@ def portfolio_members(portfolio_id):
|
|||||||
role_choices=PORTFOLIO_ROLE_DEFINITIONS,
|
role_choices=PORTFOLIO_ROLE_DEFINITIONS,
|
||||||
status_choices=MEMBER_STATUS_CHOICES,
|
status_choices=MEMBER_STATUS_CHOICES,
|
||||||
members=members_list,
|
members=members_list,
|
||||||
new_member=new_member,
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@portfolios_bp.route("/portfolios/<portfolio_id>/applications/<application_id>/members")
|
||||||
|
def application_members(portfolio_id, application_id):
|
||||||
|
portfolio = Portfolios.get_with_members(g.current_user, portfolio_id)
|
||||||
|
application = Applications.get(g.current_user, portfolio, application_id)
|
||||||
|
# TODO: this should show only members that have env roles in this application
|
||||||
|
members_list = [serialize_portfolio_role(k) for k in portfolio.members]
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"portfolios/applications/members.html",
|
||||||
|
portfolio=portfolio,
|
||||||
|
application=application,
|
||||||
|
members=members_list,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -76,7 +89,7 @@ def create_member(portfolio_id):
|
|||||||
)
|
)
|
||||||
invite_service.invite()
|
invite_service.invite()
|
||||||
|
|
||||||
flash("new_portfolio_member", new_member=new_member, portfolio=portfolio)
|
flash("new_portfolio_member", new_member=member, portfolio=portfolio)
|
||||||
|
|
||||||
return redirect(
|
return redirect(
|
||||||
url_for("portfolios.portfolio_members", portfolio_id=portfolio.id)
|
url_for("portfolios.portfolio_members", portfolio_id=portfolio.id)
|
||||||
|
@ -61,8 +61,14 @@ export default {
|
|||||||
|
|
||||||
props: {
|
props: {
|
||||||
members: Array,
|
members: Array,
|
||||||
role_choices: Array,
|
role_choices: {
|
||||||
status_choices: Array,
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
status_choices: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data: function() {
|
data: function() {
|
||||||
@ -87,7 +93,7 @@ export default {
|
|||||||
displayName: 'Environments',
|
displayName: 'Environments',
|
||||||
attr: 'num_env',
|
attr: 'num_env',
|
||||||
sortFunc: numericSort,
|
sortFunc: numericSort,
|
||||||
class: 'table-cell--align-right',
|
class: 'table-cell--align-center',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Status',
|
displayName: 'Status',
|
||||||
|
@ -103,6 +103,36 @@
|
|||||||
.portfolio-content {
|
.portfolio-content {
|
||||||
margin: 6 * $gap $gap 0 $gap;
|
margin: 6 * $gap $gap 0 $gap;
|
||||||
|
|
||||||
|
.member-list {
|
||||||
|
.panel {
|
||||||
|
padding: $gap / 2 0;
|
||||||
|
box-shadow: 0 6px 18px 0 rgba(144,164,183,0.3);
|
||||||
|
border-top: none;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
box-shadow: 0 6px 18px 0 rgba(144,164,183,0.3);
|
||||||
|
thead {
|
||||||
|
th:first-child {
|
||||||
|
padding-left: 3 * $gap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background-color: $color-gray-lightest;
|
||||||
|
padding: $gap 2 * $gap;
|
||||||
|
border-top: none;
|
||||||
|
border-bottom: none;
|
||||||
|
color: $color-gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
border-bottom: 1px solid $color-gray-lightest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.application-content {
|
.application-content {
|
||||||
.subheading {
|
.subheading {
|
||||||
@include subheading;
|
@include subheading;
|
||||||
|
@ -6,6 +6,9 @@
|
|||||||
padding: $gap;
|
padding: $gap;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
border-top: none;
|
||||||
|
border-bottom: none;
|
||||||
|
|
||||||
@media (min-width:1000px) {
|
@media (min-width:1000px) {
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,10 @@
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.table-cell--align-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
&.table-cell--shrink {
|
&.table-cell--shrink {
|
||||||
width: 1%;
|
width: 1%;
|
||||||
}
|
}
|
||||||
|
@ -47,10 +47,12 @@
|
|||||||
</a>
|
</a>
|
||||||
<div class='separator'></div>
|
<div class='separator'></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a class='icon-link' href='{{ url_for("portfolios.portfolio_members", portfolio_id=portfolio.id) }}'>
|
{% if user_can(permissions.VIEW_PORTFOLIO_MEMBERS) %}
|
||||||
<span>{{ "portfolios.applications.team_text" | translate }}</span>
|
<a class='icon-link' href='{{ url_for("portfolios.application_members", portfolio_id=portfolio.id, application_id=application.id) }}'>
|
||||||
<span class='counter'>{{ application.num_users }}</span>
|
<span>{{ "portfolios.applications.team_text" | translate }}</span>
|
||||||
</a>
|
<span class='counter'>{{ application.num_users }}</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class='col'>
|
<div class='col'>
|
||||||
|
90
templates/portfolios/applications/members.html
Normal file
90
templates/portfolios/applications/members.html
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
{% extends "portfolios/applications/base.html" %}
|
||||||
|
|
||||||
|
{% from "components/empty_state.html" import EmptyState %}
|
||||||
|
{% from "components/icon.html" import Icon %}
|
||||||
|
|
||||||
|
{% set secondary_breadcrumb = 'portfolios.applications.team_management.title' | translate({ "application_name": application.name }) %}
|
||||||
|
|
||||||
|
{% block application_content %}
|
||||||
|
|
||||||
|
<div class='subheading'>{{ 'portfolios.applications.team_management.subheading' | translate }}</div>
|
||||||
|
|
||||||
|
{% if not portfolio.members %}
|
||||||
|
|
||||||
|
{% set user_can_invite = user_can(permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE) %}
|
||||||
|
|
||||||
|
{{ EmptyState(
|
||||||
|
'There are currently no members in this Portfolio.',
|
||||||
|
action_label='Invite a new Member' if user_can_invite else None,
|
||||||
|
action_href='/members/new' if user_can_invite else None,
|
||||||
|
sub_message=None if user_can_invite else 'Please contact your JEDI Cloud portfolio administrator to invite new members.',
|
||||||
|
icon='avatar'
|
||||||
|
) }}
|
||||||
|
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
{% include "fragments/flash.html" %}
|
||||||
|
|
||||||
|
<members-list
|
||||||
|
inline-template
|
||||||
|
id="search-template"
|
||||||
|
class='member-list'
|
||||||
|
v-bind:members='{{ members | tojson}}'>
|
||||||
|
<div class='responsive-table-wrapper panel'>
|
||||||
|
<table v-cloak v-if='searchedList && searchedList.length'>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th v-for="col in getColumns()" @click="updateSort(col.displayName)" :width="col.width" :class="col.class" scope="col">
|
||||||
|
!{ col.displayName }
|
||||||
|
<span class="sorting-direction" v-if="col.displayName === sortInfo.columnName && sortInfo.isAscending">
|
||||||
|
{{ Icon("caret_down") }}
|
||||||
|
</span>
|
||||||
|
<span class="sorting-direction" v-if="col.displayName === sortInfo.columnName && !sortInfo.isAscending">
|
||||||
|
{{ Icon("caret_up") }}
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr v-for='member in searchedList'>
|
||||||
|
<td>
|
||||||
|
<a :href="member.edit_link" class="icon-link icon-link--large" v-html="member.name"></a>
|
||||||
|
</td>
|
||||||
|
<td class="table-cell--align-center" v-if='member.num_env'>
|
||||||
|
<span v-html="member.num_env"></span>
|
||||||
|
</td>
|
||||||
|
<td class='table-cell--shrink' v-else>
|
||||||
|
<span class="label label--info">No Environment Access</span>
|
||||||
|
</td>
|
||||||
|
<td v-html="member.status"></td>
|
||||||
|
<td v-html="member.role"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan=4>
|
||||||
|
<a class="icon-link" href="{{ url_for('portfolios.new_member', portfolio_id=portfolio.id) }}">
|
||||||
|
Add A New Member
|
||||||
|
{{ Icon('plus') }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div v-else>
|
||||||
|
{{ EmptyState(
|
||||||
|
'No members found.',
|
||||||
|
action_label=None,
|
||||||
|
action_href=None,
|
||||||
|
sub_message='Please try a different search.',
|
||||||
|
icon=None
|
||||||
|
) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</members-list>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -3,6 +3,8 @@
|
|||||||
{% from "components/empty_state.html" import EmptyState %}
|
{% from "components/empty_state.html" import EmptyState %}
|
||||||
{% from "components/icon.html" import Icon %}
|
{% from "components/icon.html" import Icon %}
|
||||||
|
|
||||||
|
{% set secondary_breadcrumb = 'Portfolio Team Management' %}
|
||||||
|
|
||||||
{% block portfolio_content %}
|
{% block portfolio_content %}
|
||||||
|
|
||||||
{% if not portfolio.members %}
|
{% if not portfolio.members %}
|
||||||
@ -25,6 +27,7 @@
|
|||||||
<members-list
|
<members-list
|
||||||
inline-template
|
inline-template
|
||||||
id="search-template"
|
id="search-template"
|
||||||
|
class='member-list'
|
||||||
v-bind:members='{{ members | tojson}}'
|
v-bind:members='{{ members | tojson}}'
|
||||||
v-bind:role_choices='{{ role_choices | tojson}}'
|
v-bind:role_choices='{{ role_choices | tojson}}'
|
||||||
v-bind:status_choices='{{ status_choices | tojson}}'>
|
v-bind:status_choices='{{ status_choices | tojson}}'>
|
||||||
@ -61,7 +64,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class='responsive-table-wrapper'>
|
<div class='responsive-table-wrapper panel'>
|
||||||
<table v-cloak v-if='searchedList && searchedList.length'>
|
<table v-cloak v-if='searchedList && searchedList.length'>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@ -82,7 +85,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<a :href="member.edit_link" class="icon-link icon-link--large" v-html="member.name"></a>
|
<a :href="member.edit_link" class="icon-link icon-link--large" v-html="member.name"></a>
|
||||||
</td>
|
</td>
|
||||||
<td class="table-cell--align-right" v-if='member.num_env'>
|
<td class="table-cell--align-center" v-if='member.num_env'>
|
||||||
<span v-html="member.num_env"></span>
|
<span v-html="member.num_env"></span>
|
||||||
</td>
|
</td>
|
||||||
<td class='table-cell--shrink' v-else>
|
<td class='table-cell--shrink' v-else>
|
||||||
@ -91,6 +94,14 @@
|
|||||||
<td v-html="member.status"></td>
|
<td v-html="member.status"></td>
|
||||||
<td v-html="member.role"></td>
|
<td v-html="member.role"></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan=4>
|
||||||
|
<a class="icon-link" href="{{ url_for('portfolios.new_member', portfolio_id=portfolio.id) }}">
|
||||||
|
Add A New Member
|
||||||
|
{{ Icon('plus') }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
|
@ -92,6 +92,7 @@ def test_create_member(client, user_session):
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
assert user.full_name in response.data.decode()
|
||||||
assert user.has_portfolios
|
assert user.has_portfolios
|
||||||
assert user.invitations
|
assert user.invitations
|
||||||
assert len(queue.get_queue()) == queue_length + 1
|
assert len(queue.get_queue()) == queue_length + 1
|
||||||
|
@ -493,6 +493,9 @@ portfolios:
|
|||||||
environments_description: Each environment created within an application is logically separated from one another for easier management and security.
|
environments_description: Each environment created within an application is logically separated from one another for easier management and security.
|
||||||
update_button_text: Save Changes
|
update_button_text: Save Changes
|
||||||
create_button_text: Create Application
|
create_button_text: Create Application
|
||||||
|
team_management:
|
||||||
|
title: '{application_name} Team Management'
|
||||||
|
subheading: Team Management
|
||||||
testing:
|
testing:
|
||||||
example_string: Hello World
|
example_string: Hello World
|
||||||
example_with_variables: 'Hello, {name}!'
|
example_with_variables: 'Hello, {name}!'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user