Merge pull request #613 from dod-ccpo/reskin-portfolio-apps

Reskin portfolio applications page
This commit is contained in:
patricksmithdds 2019-02-08 10:46:33 -05:00 committed by GitHub
commit c5ebdf89a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 283 additions and 45 deletions

View File

@ -17,6 +17,14 @@ class Application(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
portfolio = relationship("Portfolio") portfolio = relationship("Portfolio")
environments = relationship("Environment", back_populates="application") environments = relationship("Environment", back_populates="application")
@property
def users(self):
return set([user for env in self.environments for user in env.users])
@property
def num_users(self):
return len(self.users)
@property @property
def displayname(self): def displayname(self):
return self.name return self.name

1
static/icons/minus.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M416 208H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h384c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z"/></svg>

After

Width:  |  Height:  |  Size: 196 B

View File

@ -12,6 +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/tables'; @import 'elements/tables';
@import 'elements/sidenav'; @import 'elements/sidenav';
@import 'elements/action_group'; @import 'elements/action_group';

View File

@ -86,7 +86,60 @@
} }
.portfolio-content { .portfolio-content {
margin-top: 6 * $gap; margin: 6 * $gap $gap 0 $gap;
}
.portfolio-applications {
.portfolio-applications__header {
margin-bottom: 4 * $gap;
.portfolio-applications__header--title {
color: $color-gray-dark;
padding: $gap 0;
text-transform: uppercase;
opacity: 0.54;
font-size: $small-font-size;
font-weight: bold;
}
.portfolio-applications__header--actions {
color: $color-blue;
font-size: $small-font-size;
.icon {
@include icon-color($color-blue);
@include icon-size(14);
}
}
}
.application-list {
.toggle-link {
background-color: $color-blue-light;
.icon {
margin: $gap / 2;
}
}
.application-list-item {
border-radius: 5px;
box-shadow: 0 4px 8px 1px rgba(230,230,230,0.5), -4px 4px 8px 1px rgba(230,230,230,0.5);
.col {
max-width: 95%;
}
.application-list-item__environment__name {
}
.application-list-item__environment__csp_link {
font-size: $small-font-size;
font-weight: normal;
&:hover {
background-color: $color-aqua-light;
}
}
}
}
} }
.portfolio-funding { .portfolio-funding {

View File

@ -43,6 +43,7 @@ $font-bold: 700;
$color-blue: #0071bc; $color-blue: #0071bc;
$color-blue-darker: #205493; $color-blue-darker: #205493;
$color-blue-darkest: #112e51; $color-blue-darkest: #112e51;
$color-blue-light: #e5f1ff;
$color-aqua: #02bfe7; $color-aqua: #02bfe7;
$color-aqua-dark: #00a6d2; $color-aqua-dark: #00a6d2;
@ -83,7 +84,7 @@ $color-green-lighter: #94bfa2;
$color-green-lightest: #e7f4e4; $color-green-lightest: #e7f4e4;
$color-cool-blue: #205493; $color-cool-blue: #205493;
$color-cool-blue-light: #4773aa; $color-cool-blue-light: #4190e2;
$color-cool-blue-lighter: #8ba6ca; $color-cool-blue-lighter: #8ba6ca;
$color-cool-blue-lightest: #dce4ef; $color-cool-blue-lightest: #dce4ef;

View File

@ -0,0 +1,110 @@
.accordian {
@include block-list;
box-shadow: 0 4px 10px 0 rgba(193,193,193,0.5);
.icon-link {
margin: -$gap 0;
}
.icon-link,
.label {
&:first-child {
margin-left: -$gap;
}
&:last-child {
margin-right: -$gap;
}
}
}
.accordian__header {
@include block-list-header;
border-top: 3px solid $color-blue;
border-bottom: none;
box-shadow: 0 2px 4px 0 rgba(216,218,222,0.58);
}
.accordian__title {
@include block-list__title;
color: $color-blue;
@include h3;
}
.accordian__description {
@include block-list__description;
font-style: italic;
font-size: $small-font-size;
color: $color-gray;
}
.accordian__actions {
margin-top: $gap;
display: flex;
flex-direction: row;
.icon-link {
font-size: $small-font-size;
}
.counter {
background-color: $color-cool-blue-light;
color: $color-white;
border-radius: 2px;
padding: $gap / 2 $gap;
margin-left: $gap;
}
.separator {
border: 1px solid $color-gray-medium;
opacity: 0.75;
}
}
.accordian__item {
@include block-list-item;
opacity: 0.75;
background-color: $color-blue-light;
border-bottom: 1px solid rgba($color-gray-light, 0.5);
&.accordian__item--selectable {
> div {
display: flex;
flex-direction: row-reverse;
@include ie-only {
width: 100%;
}
> label {
@include block-list-selectable-label;
}
}
> label {
@include block-list-selectable-label;
}
input:checked {
+ label {
color: $color-primary;
}
}
@include ie-only {
dl {
width: 100%;
padding-left: $gap*4;
}
}
}
}
.accordian__footer {
@include block-list__footer;
border-top: 0;
}

View File

@ -3,11 +3,24 @@
{% extends "portfolios/base.html" %} {% extends "portfolios/base.html" %}
{% set can_create_applications = user_can(permissions.ADD_APPLICATION_IN_PORTFOLIO) %}
{% block portfolio_content %} {% block portfolio_content %}
{% if not portfolio.applications %} <div class='portfolio-applications'>
<div class='portfolio-applications__header row'>
<div class='portfolio-applications__header--title col col--grow'>Applications</div>
<div class='portfolio-applications__header--actions col'>
{% if can_create_applications %}
<a class='icon-link' href='{{ url_for('portfolios.new_application', portfolio_id=portfolio.id) }}'>
{{ 'portfolios.applications.add_application_text' | translate }}
{{ Icon("plus", classes="sidenav__link-icon") }}
</a>
{% endif %}
</div>
</div>
{% set can_create_applications = user_can(permissions.ADD_APPLICATION_IN_PORTFOLIO) %} {% if not portfolio.applications %}
{{ EmptyState( {{ EmptyState(
'This portfolio doesnt have any applications yet.', 'This portfolio doesnt have any applications yet.',
@ -17,38 +30,61 @@
sub_message=None if can_create_applications else 'Please contact your JEDI Cloud portfolio administrator to set up a new application.' sub_message=None if can_create_applications else 'Please contact your JEDI Cloud portfolio administrator to set up a new application.'
) }} ) }}
{% else %} {% else %}
{% for application in portfolio.applications %} <div class='application-list'>
<div v-cloak class='block-list application-list-item'> {% for application in portfolio.applications|sort(attribute='name') %}
<header class='block-list__header'> <div is='toggler' v-cloak class='accordian application-list-item'>
<h2 class='block-list__title'>{{ application.name }} ({{ application.environments|length }} environments)</h2> <template slot-scope='props'>
<header class='accordian__header row'>
<div class='col col-grow'>
<h3 class='accordian__title'>{{ application.name }}</h3>
<span class='accordian__description'>{{ application.description }}</span>
<div class='accordian__actions'>
{% if user_can(permissions.RENAME_APPLICATION_IN_PORTFOLIO) %} {% if user_can(permissions.RENAME_APPLICATION_IN_PORTFOLIO) %}
<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) }}'>
{{ Icon('edit') }} <span>{{ "portfolios.applications.app_settings_text" | translate }}</span>
<span>edit</span>
</a> </a>
<div class='separator'></div>
{% endif %} {% endif %}
<a class='icon-link' href='{{ url_for("portfolios.portfolio_members", portfolio_id=portfolio.id) }}'>
<span>{{ "portfolios.applications.team_text" | translate }}</span>
<span class='counter'>{{ application.num_users }}</span>
</a>
</div>
</div>
<div class='col'>
<span v-on:click="props.toggle" class='icon-link toggle-link'>
<template v-if="props.isVisible">
{{ Icon('minus') }}
</template>
<template v-else>
{{ Icon('plus') }}
</template>
</span>
</div>
</header> </header>
<ul> <ul v-if="props.isVisible">
{% for environment in application.environments %} {% for environment in application.environments %}
<li class='block-list__item application-list-item__environment'> <li class='accordian__item application-list-item__environment'>
<a href='{{ url_for("portfolios.access_environment", portfolio_id=portfolio.id, environment_id=environment.id)}}' target='_blank' rel='noopener noreferrer' class='application-list-item__environment__link'> <div class='application-list-item__environment__name'>
{{ Icon('link') }}
<span>{{ environment.name }}</span> <span>{{ environment.name }}</span>
</div>
<a href='{{ url_for("portfolios.access_environment", portfolio_id=portfolio.id, environment_id=environment.id)}}' target='_blank' rel='noopener noreferrer' class='application-list-item__environment__csp_link icon-link'>
<span>{{ "portfolios.applications.csp_console_text" | translate }}</span>
</a> </a>
<div class='application-list-item__environment__members'>
<div class='label'>{{ environment.num_users }}</div>
<span>members</span>
</div>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
</template>
</div> </div>
{% endfor %} {% endfor %}
</div>
{% endif %} {% endif %}
</div>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,22 @@
from atst.domain.environments import Environments
from tests.factories import ApplicationFactory, UserFactory
def test_application_num_users():
application = ApplicationFactory.create(
environments=[{"name": "dev"}, {"name": "staging"}, {"name": "prod"}]
)
assert application.num_users == 0
first_env = application.environments[0]
user1 = UserFactory()
Environments.add_member(first_env, user1, "developer")
assert application.num_users == 1
second_env = application.environments[-1]
Environments.add_member(second_env, user1, "developer")
assert application.num_users == 1
user2 = UserFactory()
Environments.add_member(second_env, user2, "developer")
assert application.num_users == 2

View File

@ -481,6 +481,12 @@ task_orders:
title: Task Order Builder title: Task Order Builder
submitted_by: Below is an overview of the projected portfolio submitted by {name} submitted_by: Below is an overview of the projected portfolio submitted by {name}
task_order_information: Task Order Information task_order_information: Task Order Information
portfolios:
applications:
add_application_text: Add A New Application
app_settings_text: App Settings
team_text: Team
csp_console_text: CSP Console
testing: testing:
example_string: Hello World example_string: Hello World
example_with_variables: 'Hello, {name}!' example_with_variables: 'Hello, {name}!'