Merge pull request #751 from dod-ccpo/view-app-envs

View Application Environments Table
This commit is contained in:
montana-mil 2019-04-11 17:10:43 -04:00 committed by GitHub
commit bf4eb23557
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 395 additions and 251 deletions

View File

@ -64,11 +64,16 @@ def edit_application(portfolio_id, application_id):
application = Applications.get(application_id) application = Applications.get(application_id)
form = ApplicationForm(name=application.name, description=application.description) form = ApplicationForm(name=application.name, description=application.description)
environments_obj = {}
for env in application.environments:
environments_obj[env.name] = [user.full_name for user in env.users]
return render_template( return render_template(
"portfolios/applications/edit.html", "portfolios/applications/edit.html",
portfolio=portfolio, portfolio=portfolio,
application=application, application=application,
form=form, form=form,
environments_obj=environments_obj,
) )

View File

@ -0,0 +1,34 @@
import { set } from 'vue/dist/vue'
export default {
name: 'environments-table',
props: {
environments: Object,
},
data: function() {
return {
environmentsState: this.environments,
}
},
created: function() {
Object.keys(this.environments).forEach(environment => {
set(this.environmentsState[environment], 'isVisible', false)
})
},
methods: {
toggle: function(e, environmentName) {
this.environmentsState = Object.assign(this.environmentsState, {
[environmentName]: Object.assign(
this.environmentsState[environmentName],
{
isVisible: !this.environmentsState[environmentName].isVisible,
}
),
})
},
},
}

View File

@ -25,6 +25,7 @@ import Modal from './mixins/modal'
import selector from './components/selector' import selector from './components/selector'
import BudgetChart from './components/charts/budget_chart' import BudgetChart from './components/charts/budget_chart'
import SpendTable from './components/tables/spend_table' import SpendTable from './components/tables/spend_table'
import EnvironmentsTable from './components/tables/application_environments'
import TaskOrderList from './components/tables/task_order_list.js' import TaskOrderList from './components/tables/task_order_list.js'
import MembersList from './components/members_list' import MembersList from './components/members_list'
import LocalDatetime from './components/local_datetime' import LocalDatetime from './components/local_datetime'
@ -56,6 +57,7 @@ const app = new Vue({
selector, selector,
BudgetChart, BudgetChart,
SpendTable, SpendTable,
EnvironmentsTable,
TaskOrderList, TaskOrderList,
MembersList, MembersList,
LocalDatetime, LocalDatetime,

View File

@ -12,7 +12,7 @@
@import 'elements/buttons'; @import 'elements/buttons';
@import 'elements/panels'; @import 'elements/panels';
@import 'elements/block_lists'; @import 'elements/block_lists';
@import 'elements/accordians'; @import 'elements/accordions';
@import 'elements/tables'; @import 'elements/tables';
@import 'elements/sidenav'; @import 'elements/sidenav';
@import 'elements/action_group'; @import 'elements/action_group';
@ -23,6 +23,7 @@
@import 'elements/graphs'; @import 'elements/graphs';
@import 'elements/menu'; @import 'elements/menu';
@import 'components/accordion_table';
@import 'components/topbar'; @import 'components/topbar';
@import 'components/top_message'; @import 'components/top_message';
@import 'components/global_layout'; @import 'components/global_layout';

View File

@ -0,0 +1,66 @@
.accordion-table {
table {
thead th {
text-transform: uppercase;
border-bottom: 1px solid $color-gray-lightest;
border-top: none;
}
th, td {
white-space: nowrap;
button {
margin: 0;
}
}
.accordion-table__items {
.accordion-table__item__toggler {
@include icon-link-color($color-blue, $color-gray-lightest);
float: right;
margin-left: -$gap;
color: $color-blue;
.icon {
@include icon-size(12);
margin-right: $gap;
}
.open-indicator {
position: absolute;
bottom: 0;
left: 5 * $gap;
width: 0;
height: 0;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-bottom: 10px solid $color-blue-light;
}
}
th, td, tr {
border-bottom: 1px dashed $color-gray-lightest;
}
th[scope=rowgroup] {
position: relative;
}
.accordion-table__item__expanded {
margin-left: 2 * $gap;
th, td {
.icon-link {
font-weight: $font-normal;
font-size: $base-font-size;
}
border-bottom: 1px dashed $color-white;
background-color: $color-gray-lightest;
}
}
}
}
}

View File

@ -171,26 +171,6 @@
padding-bottom: 0; padding-bottom: 0;
} }
.member-list-header {
margin: 2 * $gap 5 * $gap;
padding: inherit;
overflow: auto;
.left {
float: left;
padding-bottom: 0.8rem;
}
.icon-link {
float: right;
margin-top: 0.8rem;
}
.icon {
}
}
.subheading { .subheading {
font-size: 1.4rem; font-size: 1.4rem;
color: $color-gray; color: $color-gray;
@ -326,15 +306,6 @@
} }
} }
.members-table-footer {
float: right;
padding: 3 * $gap 0;
.action-group.save {
padding-right: 3 * $gap;
}
}
a.modal-link.icon-link { a.modal-link.icon-link {
float: right; float: right;

View File

@ -1,4 +1,4 @@
.accordian { .accordion {
@include block-list; @include block-list;
box-shadow: 0 4px 10px 0 rgba(193,193,193,0.5); box-shadow: 0 4px 10px 0 rgba(193,193,193,0.5);
@ -21,7 +21,7 @@
} }
} }
.accordian__header { .accordion__header {
@include block-list-header; @include block-list-header;
border-top: 3px solid $color-blue; border-top: 3px solid $color-blue;
border-bottom: none; border-bottom: none;
@ -32,7 +32,7 @@
} }
} }
.accordian__title { .accordion__title {
@include block-list__title; @include block-list__title;
color: $color-blue; color: $color-blue;
@include h3; @include h3;
@ -45,14 +45,14 @@
} }
} }
.accordian__description { .accordion__description {
@include block-list__description; @include block-list__description;
font-style: italic; font-style: italic;
font-size: $small-font-size; font-size: $small-font-size;
color: $color-gray; color: $color-gray;
} }
.accordian__actions { .accordion__actions {
margin-top: $gap; margin-top: $gap;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -76,14 +76,14 @@
} }
} }
.accordian__item { .accordion__item {
@include block-list-item; @include block-list-item;
opacity: 0.75; opacity: 0.75;
background-color: $color-blue-light; background-color: $color-blue-light;
border-bottom: 1px solid rgba($color-gray-light, 0.5); border-bottom: 1px solid rgba($color-gray-light, 0.5);
&.accordian__item--selectable { &.accordion__item--selectable {
> div { > div {
display: flex; display: flex;
flex-direction: row-reverse; flex-direction: row-reverse;
@ -117,7 +117,7 @@
} }
} }
.accordian__footer { .accordion__footer {
@include block-list__footer; @include block-list__footer;
border-top: 0; border-top: 0;
} }

View File

@ -107,6 +107,10 @@
} }
} }
.panel__footer {
padding: 3 * $gap;
}
hr { hr {
border: 0; border: 0;
border-bottom: 1px dashed $color-gray-light; border-bottom: 1px dashed $color-gray-light;

View File

@ -102,6 +102,25 @@
overflow-x: auto; overflow-x: auto;
@include panel-margin; @include panel-margin;
.responsive-table-wrapper__header {
@include panel-base;
@include panel-theme-default;
border-top: none;
border-bottom: 0;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
padding: $gap * 2;
.responsive-table-wrapper__title {
@include h4;
font-size: $lead-font-size;
flex: 2;
}
}
table { table {
margin-bottom: 0; margin-bottom: 0;
} }

View File

@ -24,7 +24,41 @@
} }
} }
header.accordian__header { .list-header {
padding: 1.6rem; margin: 2 * $gap 5 * $gap;
padding: inherit;
overflow: auto;
}
.icon-link {
.icon--info {
bottom: -1px;
left: 4px;
}
}
table {
thead {
td {
font-weight: bold;
font-size: 1.4rem;
border-top: 0;
}
}
tbody {
th {
font-weight: bold;
font-size: 1.6rem;
}
td {
font-size: 1.6rem;
border-bottom: 1px solid $color-gray-lightest;
border-top: 0;
padding: 3 * $gap 2 * $gap;
}
}
} }
} }

View File

@ -201,143 +201,60 @@
} }
} }
.spend-table__month-select {
margin: 0;
flex: 1;
}
.spend-table { table {
box-shadow: 0 6px 18px 0 rgba(144,164,183,0.3); .spend-table__portfolio {
th, td {
.spend-table__header { font-weight: bold;
@include panel-base; border-bottom: 1px solid $color-gray-lightest;
@include panel-theme-default;
border-top: none;
border-bottom: 0;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
padding: $gap * 2;
.spend-table__title {
@include h4;
font-size: $lead-font-size;
flex: 2;
}
.spend-table__month-select {
margin: 0;
flex: 1;
} }
} }
table { th, td {
thead th { &.previous-month {
text-transform: uppercase; color: $color-gray;
border-bottom: 1px solid $color-gray-lightest;
border-top: none;
} }
th, td { &.meter-cell {
white-space: nowrap; padding-left: 0;
position: relative;
min-width: 4rem;
button { @include media($medium-screen) {
margin: 0; min-width: 12rem;
} }
&.previous-month { meter {
color: $color-gray; width: 100%;
} height: 3rem;
background: $color-white;
&.meter-cell { display: none;
padding-left: 0;
position: relative;
min-width: 4rem;
@include media($medium-screen) { @include media($medium-screen) {
min-width: 12rem; display: block;
} }
meter { &::-webkit-meter-bar {
width: 100%;
height: 3rem;
background: $color-white; background: $color-white;
display: none;
@include media($medium-screen) {
display: block;
}
&::-webkit-meter-bar {
background: $color-white;
}
}
.spend-table__meter-value {
@include h5;
@include media($medium-screen) {
display: block;
color: $color-white;
background-color: rgba($color-blue, 0.65);
border-radius: $gap/2;
position: absolute;
top: 2.3rem;
left: $gap / 2;
padding: 0 ($gap / 2);
}
} }
} }
}
.spend-table__portfolio { .spend-table__meter-value {
th, td { @include h5;
font-weight: bold;
border-bottom: 1px solid $color-gray-lightest;
}
}
.spend-table__application { @include media($medium-screen) {
.spend-table__application__toggler { display: block;
@include icon-link-color($color-blue, $color-gray-lightest); color: $color-white;
margin-left: -$gap; background-color: rgba($color-blue, 0.65);
color: $color-blue; border-radius: $gap/2;
.icon {
@include icon-size(12);
margin-right: $gap;
}
.open-indicator {
position: absolute; position: absolute;
bottom: 0; top: 2.3rem;
left: 5 * $gap; left: $gap / 2;
width: 0; padding: 0 ($gap / 2);
height: 0;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-bottom: 10px solid $color-blue-light;
}
}
th, td {
border-bottom: none;
}
th[scope=rowgroup] {
position: relative;
}
.spend-table__application__env {
margin-left: 2 * $gap;
th, td {
.icon-link {
font-weight: $font-normal;
font-size: $base-font-size;
}
border-bottom: 1px dashed $color-white;
background-color: $color-blue-light;
} }
} }
} }

View File

@ -6,7 +6,7 @@
{% from "components/alert.html" import Alert %} {% from "components/alert.html" import Alert %}
<section class="member-list" id="portfolio-members"> <section class="member-list" id="portfolio-members">
<div class='responsive-table-wrapper panel'> <div class='responsive-table-wrapper panel accordion-table'>
{% if g.matchesPath("portfolio-members") %} {% if g.matchesPath("portfolio-members") %}
{% include "fragments/flash.html" %} {% include "fragments/flash.html" %}
{% endif %} {% endif %}
@ -14,53 +14,62 @@
<form method='POST' id="member-perms" action='{{ url_for("portfolios.edit_portfolio_members", portfolio_id=portfolio.id) }}' autocomplete="off" enctype="multipart/form-data"> <form method='POST' id="member-perms" action='{{ url_for("portfolios.edit_portfolio_members", portfolio_id=portfolio.id) }}' autocomplete="off" enctype="multipart/form-data">
{{ member_perms_form.csrf_token }} {{ member_perms_form.csrf_token }}
<div class='member-list-header'> <div class='application-list-item'>
<div class='left'> <header>
<div class='h3'>{{ "portfolios.admin.portfolio_members_title" | translate }}</div> <div class='responsive-table-wrapper__header'>
<div class='subheading'> <div class='responsive-table-wrapper__title'>
{{ "portfolios.admin.portfolio_members_subheading" | translate }} <div class='h3'>{{ "portfolios.admin.portfolio_members_title" | translate }}</div>
<div class='subheading'>
{{ "portfolios.admin.portfolio_members_subheading" | translate }}
</div>
</div>
<a class='icon-link'>
{{ Icon('info') }}
{{ "portfolios.admin.settings_info" | translate }}
</a>
</div> </div>
</header>
{% if not portfolio.members %}
<p>{{ "portfolios.admin.no_members" | translate }}</p>
{% else %}
<table>
<thead>
<tr>
<td>{{ "portfolios.members.permissions.name" | translate }}</td>
<td>{{ "portfolios.members.permissions.app_mgmt" | translate }}</td>
<td>{{ "portfolios.members.permissions.funding" | translate }}</td>
<td>{{ "portfolios.members.permissions.reporting" | translate }}</td>
<td>{{ "portfolios.members.permissions.portfolio_mgmt" | translate }}</td>
<td></td>
</tr>
</thead>
<tbody>
{% if user_can(permissions.EDIT_PORTFOLIO_USERS) %}
{% include "fragments/admin/members_edit.html" %}
{% elif user_can(permissions.VIEW_PORTFOLIO_USERS) %}
{% include "fragments/admin/members_view.html" %}
{% endif %}
</tbody>
</table>
</div> </div>
<a class='icon-link'> <div class="panel__footer">
{{ Icon('info') }}
{{ "portfolios.admin.settings_info" | translate }}
</a>
</div>
{% if not portfolio.members %}
<p>{{ "portfolios.admin.no_members" | translate }}</p>
{% else %}
<table>
<thead>
<tr>
<td>{{ "portfolios.members.permissions.name" | translate }}</td>
<td>{{ "portfolios.members.permissions.app_mgmt" | translate }}</td>
<td>{{ "portfolios.members.permissions.funding" | translate }}</td>
<td>{{ "portfolios.members.permissions.reporting" | translate }}</td>
<td>{{ "portfolios.members.permissions.portfolio_mgmt" | translate }}</td>
<td></td>
</tr>
</thead>
<tbody>
{% if user_can(permissions.EDIT_PORTFOLIO_USERS) %}
{% include "fragments/admin/members_edit.html" %}
{% elif user_can(permissions.VIEW_PORTFOLIO_USERS) %}
{% include "fragments/admin/members_view.html" %}
{% endif %}
</tbody>
</table>
<div class="members-table-footer">
<div class="action-group save"> <div class="action-group save">
{% if user_can(permissions.EDIT_PORTFOLIO_USERS) %} {% if user_can(permissions.EDIT_PORTFOLIO_USERS) %}
{{ SaveButton(text=('common.save' | translate), element="input", form="member-perms") }} {{ SaveButton(text=('common.save' | translate), element="input", form="member-perms") }}
{% endif %} {% endif %}
{% if user_can(permissions.CREATE_PORTFOLIO_USERS) %}
{% include "fragments/admin/add_new_portfolio_member.html" %}
{% endif %}
</div> </div>
</div> </div>
{% endif %}
{% endif %}
</form> </form>
</base-form> </base-form>
@ -78,26 +87,21 @@
) )
}} }}
<div class="action-group"> <div class="panel__footer">
<form method="POST" action="{{ url_for('portfolios.remove_member', portfolio_id=portfolio.id, user_id=member.user_id) }}"> <div class="action-group">
{{ member_perms_form.csrf_token }} <form method="POST" action="{{ url_for('portfolios.remove_member', portfolio_id=portfolio.id, user_id=member.user_id) }}">
<button class="usa-button usa-button-danger"> {{ member_perms_form.csrf_token }}
{{ "portfolios.members.archive_button" | translate }} <button class="usa-button usa-button-danger">
</button> {{ "portfolios.members.archive_button" | translate }}
</form> </button>
<a v-on:click="closeModal('{{ modal_id }}')" class="action-group__action icon-link icon-link--default">{{ "common.cancel" | translate }}</a> </form>
<a v-on:click="closeModal('{{ modal_id }}')" class="action-group__action icon-link icon-link--default">{{ "common.cancel" | translate }}</a>
</div>
</div> </div>
{% endcall %} {% endcall %}
{% endfor %} {% endfor %}
{% endif %} {% endif %}
<div class="members-table-footer">
<div class="action-group">
{% if user_can(permissions.CREATE_PORTFOLIO_USERS) %}
{% include "fragments/admin/add_new_portfolio_member.html" %}
{% endif %}
</div>
</div>
</div> </div>
</section> </section>

View File

@ -0,0 +1,50 @@
{% from "components/icon.html" import Icon %}
<div class="application-list-item">
<header>
<div class="responsive-table-wrapper__header">
<div class='responsive-table-wrapper__title'>
<div class='h3'>{{ 'portfolios.applications.environments_heading' | translate }}</div>
</div>
<a class='icon-link'>
{{ Icon('info') }}
{{ "portfolios.admin.settings_info" | translate }}
</a>
</div>
</header>
<environments-table
v-cloak
v-bind:environments='{{ environments_obj }}'
inline-template>
<table>
<thead>
<th scope='col'>{{ "portfolios.applications.environments.name" | translate }}</th>
<th scope='col' class='table-cell--align-right'>{{ "portfolios.applications.environments.members" | translate }}</th>
</thead>
<tbody v-for='(members_list, name) in environments' class='accordion-table__items'>
<tr>
<th scope='rowgroup' v-on:click="toggle($event, name)" v-html='name'></th>
<template v-if="environmentsState[name].isVisible">
<td v-on:click="toggle($event, name)" class='icon-link icon-link--large accordion-table__item__toggler'>Hide Members (<span v-html='members_list.length'></span>){{ Icon('caret_up') }}</td>
</template>
<template v-else>
<td v-on:click="toggle($event, name)" class='icon-link icon-link--large accordion-table__item__toggler'>Show Members (<span v-html='members_list.length'></span>){{ Icon('caret_down') }}</td>
</template>
</tr>
<tr scope='rowgroup' v-for='member in members_list' v-show='environmentsState[name].isVisible' class='accordion-table__item__expanded'>
<td>
<div>
<span v-html='member'></span>
</div>
</td>
<td class='table-cell--expand'></td>
</tr>
</tbody>
</table>
</environments-table>
</div>

View File

@ -1,6 +1,7 @@
{% extends "portfolios/applications/base.html" %} {% extends "portfolios/applications/base.html" %}
{% from "components/text_input.html" import TextInput %} {% from "components/text_input.html" import TextInput %}
{% from "components/icon.html" import Icon %}
{% set secondary_breadcrumb = 'portfolios.applications.existing_application_title' | translate({ "application_name": application.name }) %} {% set secondary_breadcrumb = 'portfolios.applications.existing_application_title' | translate({ "application_name": application.name }) %}
@ -12,33 +13,28 @@
<div class="panel"> <div class="panel">
<div class="panel__content"> <div class="panel__content">
{% include "fragments/edit_application_form.html" %} {% include "fragments/applications/edit_application_form.html" %}
<div class="application-list-item">
<header>
<h2 class="block-list__title">{{ 'portfolios.applications.environments_heading' | translate }}</h2>
<p>
{{ 'portfolios.applications.environments_description' | translate }}
</p>
</header>
<ul>
{% for environment in application.environments %}
<li class="application-edit__env-list-item">
<div class="usa-input input--disabled">
<label>Environment Name</label>
<input type="text" disabled value="{{ environment.name }}" readonly />
</div>
</li>
{% endfor %}
</ul>
</div>
</div> </div>
</div> <div class="panel__footer">
<div class="action-group"> <div class="action-group">
<button class="usa-button usa-button-primary" tabindex="0" type="submit">{{ 'portfolios.applications.update_button_text' | translate }}</button> <button class="usa-button usa-button-primary" tabindex="0" type="submit">{{ 'portfolios.applications.update_button_text' | translate }}</button>
</div>
</div>
</div> </div>
</form> </form>
<div class="accordion-table responsive-table-wrapper panel">
{% include "fragments/applications/environments.html" %}
<div class="panel__footer">
<div class="action-group">
<button class="usa-button usa-button-primary" tabindex="0" type="submit">{{ 'portfolios.applications.update_button_text' | translate }}</button>
<a class='icon-link'>
{{ "portfolios.applications.add_environment" | translate }}
{{ Icon('plus-circle-solid') }}
</a>
</div>
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -34,13 +34,13 @@
<div class='application-list'> <div class='application-list'>
{% for application in portfolio.applications|sort(attribute='name') %} {% for application in portfolio.applications|sort(attribute='name') %}
<div is='toggler' v-cloak class='accordian application-list-item'> <div is='toggler' v-cloak class='accordion application-list-item'>
<template slot-scope='props'> <template slot-scope='props'>
<header class='accordian__header row'> <header class='accordion__header row'>
<div class='col col-grow'> <div class='col col-grow'>
<h3 class='icon-link accordian__title' v-on:click="props.toggle">{{ application.name }}</h3> <h3 class='icon-link accordion__title' v-on:click="props.toggle">{{ application.name }}</h3>
<span class='accordian__description'>{{ application.description }}</span> <span class='accordion__description'>{{ application.description }}</span>
<div class='accordian__actions'> <div class='accordion__actions'>
{% if user_can(permissions.EDIT_APPLICATION) %} {% if user_can(permissions.EDIT_APPLICATION) %}
<a class='icon-link' href='{{ url_for("portfolios.edit_application", portfolio_id=portfolio.id, application_id=application.id) }}'> <a class='icon-link' href='{{ url_for("portfolios.edit_application", portfolio_id=portfolio.id, application_id=application.id) }}'>
<span>{{ "portfolios.applications.app_settings_text" | translate }}</span> <span>{{ "portfolios.applications.app_settings_text" | translate }}</span>
@ -68,7 +68,7 @@
</header> </header>
<ul v-if="props.isVisible"> <ul v-if="props.isVisible">
{% for environment in application.environments %} {% for environment in application.environments %}
<li class='accordian__item application-list-item__environment'> <li class='accordion__item application-list-item__environment'>
<div class='application-list-item__environment__name'> <div class='application-list-item__environment__name'>
<span>{{ environment.name }}</span> <span>{{ environment.name }}</span>
</div> </div>

View File

@ -37,7 +37,7 @@
</div> </div>
{% endcall %} {% endcall %}
{% include "fragments/edit_application_form.html" %} {% include "fragments/applications/edit_application_form.html" %}
<div> {# this extra div prevents this bug: https://www.pivotaltracker.com/story/show/160768940 #} <div> {# this extra div prevents this bug: https://www.pivotaltracker.com/story/show/160768940 #}
<div v-cloak v-for="title in errors" :key="title"> <div v-cloak v-for="title in errors" :key="title">

View File

@ -346,9 +346,9 @@
</div> </div>
</budget-chart> </budget-chart>
<div class='spend-table responsive-table-wrapper'> <div class='accordion-table responsive-table-wrapper'>
<div class='spend-table__header'> <div class='responsive-table-wrapper__header'>
<h2 class='spend-table__title'>Total spent per month</h2> <h2 class='responsive-table-wrapper__title'>Total spent per month</h2>
<select name='month' id='month' onchange='location = this.value' class='spend-table__month-select'> <select name='month' id='month' onchange='location = this.value' class='spend-table__month-select'>
{% for m in cumulative_budget["months"] %} {% for m in cumulative_budget["months"] %}
@ -402,10 +402,10 @@
</tr> </tr>
</tbody> </tbody>
<tbody v-for='(application, name) in applicationsState' class='spend-table__application'> <tbody v-for='(application, name) in applicationsState' class='accordion-table__items'>
<tr> <tr>
<th scope='rowgroup'> <th scope='rowgroup'>
<button v-on:click='toggle($event, name)' class='icon-link icon-link--large spend-table__application__toggler'> <button v-on:click='toggle($event, name)' class='icon-link icon-link--large accordion-table__item__toggler'>
<template v-if='application.isVisible'>{{ Icon('caret_down') }}<div class='open-indicator'></div></template> <template v-if='application.isVisible'>{{ Icon('caret_down') }}<div class='open-indicator'></div></template>
<template v-else>{{ Icon('caret_right') }}</template> <template v-else>{{ Icon('caret_right') }}</template>
<span v-html='name'></span> <span v-html='name'></span>
@ -433,9 +433,9 @@
</td> </td>
</tr> </tr>
<tr v-for='(environment, envName) in environments[name]' v-show='application.isVisible' class='spend-table__application__env'> <tr v-for='(environment, envName) in environments[name]' v-show='application.isVisible' class='accordion-table__item__expanded'>
<th scope='rowgroup'> <th scope='rowgroup'>
<div class='icon-link spend-table__application__env'> <div class='icon-link accordion-table__item__expanded'>
<span v-html='envName'></span> <span v-html='envName'></span>
</div> </div>
</th> </th>

View File

@ -13,6 +13,8 @@ from atst.domain.applications import Applications
from atst.domain.portfolios import Portfolios from atst.domain.portfolios import Portfolios
from atst.models.portfolio_role import Status as PortfolioRoleStatus from atst.models.portfolio_role import Status as PortfolioRoleStatus
from tests.utils import captured_templates
def test_user_with_permission_has_budget_report_link(client, user_session): def test_user_with_permission_has_budget_report_link(client, user_session):
portfolio = PortfolioFactory.create() portfolio = PortfolioFactory.create()
@ -105,6 +107,41 @@ def test_view_edit_application(client, user_session):
assert response.status_code == 200 assert response.status_code == 200
def test_edit_application_environments_obj(app, client, user_session):
portfolio = PortfolioFactory.create()
application = Applications.create(
portfolio,
"Snazzy Application",
"A new application for me and my friends",
{"env1", "env2"},
)
user1 = UserFactory.create()
user2 = UserFactory.create()
env1 = application.environments[0]
env2 = application.environments[1]
EnvironmentRoleFactory.create(environment=env1, user=user1)
EnvironmentRoleFactory.create(environment=env1, user=user2)
EnvironmentRoleFactory.create(environment=env2, user=user1)
user_session(portfolio.owner)
with captured_templates(app) as templates:
response = app.test_client().get(
url_for(
"portfolios.edit_application",
portfolio_id=portfolio.id,
application_id=application.id,
)
)
assert response.status_code == 200
_, context = templates[0]
assert context["environments_obj"] == {
env1.name: [user1.full_name, user2.full_name],
env2.name: [user1.full_name],
}
def test_user_with_permission_can_update_application(client, user_session): def test_user_with_permission_can_update_application(client, user_session):
owner = UserFactory.create() owner = UserFactory.create()
portfolio = PortfolioFactory.create( portfolio = PortfolioFactory.create(

View File

@ -580,13 +580,17 @@ portfolios:
existing_application_title: '{application_name} Application Settings' existing_application_title: '{application_name} Application Settings'
new_application_title: New Application new_application_title: New Application
settings_heading: Application Settings settings_heading: Application Settings
environments_heading: Environments environments_heading: Application Environments
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 update_button_text: Save
create_button_text: Create create_button_text: Create
team_management: team_management:
title: '{application_name} Team Management' title: '{application_name} Team Management'
subheading: Team Management subheading: Team Management
environments:
name: Name
members: Members
add_environment: Add New Environment
admin: admin:
portfolio_members_title: Portfolio members portfolio_members_title: Portfolio members
portfolio_members_subheading: These members have different levels of access to the portfolio. portfolio_members_subheading: These members have different levels of access to the portfolio.