Merge pull request #620 from dod-ccpo/reskin-members-page

Reskin team management pages
This commit is contained in:
patricksmithdds 2019-02-11 14:10:42 -05:00 committed by GitHub
commit 37df5b4b9c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 191 additions and 28 deletions

View File

@ -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)

View File

@ -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',

View File

@ -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;

View File

@ -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;
} }

View File

@ -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%;
} }

View File

@ -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'>

View 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 %}

View File

@ -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>

View File

@ -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

View File

@ -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}!'