Merge pull request #613 from dod-ccpo/reskin-portfolio-apps
Reskin portfolio applications page
This commit is contained in:
commit
c5ebdf89a7
@ -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
1
static/icons/minus.svg
Normal 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 |
@ -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';
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
110
styles/elements/_accordians.scss
Normal file
110
styles/elements/_accordians.scss
Normal 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;
|
||||||
|
}
|
@ -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 doesn’t have any applications yet.',
|
'This portfolio doesn’t have any applications yet.',
|
||||||
@ -19,36 +32,59 @@
|
|||||||
|
|
||||||
{% 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 %}
|
||||||
|
|
||||||
{% endblock %}
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
22
tests/models/test_application.py
Normal file
22
tests/models/test_application.py
Normal 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
|
@ -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}!'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user