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")
|
||||
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
|
||||
def displayname(self):
|
||||
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/panels';
|
||||
@import 'elements/block_lists';
|
||||
@import 'elements/accordians';
|
||||
@import 'elements/tables';
|
||||
@import 'elements/sidenav';
|
||||
@import 'elements/action_group';
|
||||
|
@ -86,7 +86,60 @@
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
@ -43,6 +43,7 @@ $font-bold: 700;
|
||||
$color-blue: #0071bc;
|
||||
$color-blue-darker: #205493;
|
||||
$color-blue-darkest: #112e51;
|
||||
$color-blue-light: #e5f1ff;
|
||||
|
||||
$color-aqua: #02bfe7;
|
||||
$color-aqua-dark: #00a6d2;
|
||||
@ -83,7 +84,7 @@ $color-green-lighter: #94bfa2;
|
||||
$color-green-lightest: #e7f4e4;
|
||||
|
||||
$color-cool-blue: #205493;
|
||||
$color-cool-blue-light: #4773aa;
|
||||
$color-cool-blue-light: #4190e2;
|
||||
$color-cool-blue-lighter: #8ba6ca;
|
||||
$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,52 +3,88 @@
|
||||
|
||||
{% extends "portfolios/base.html" %}
|
||||
|
||||
{% set can_create_applications = user_can(permissions.ADD_APPLICATION_IN_PORTFOLIO) %}
|
||||
|
||||
{% block portfolio_content %}
|
||||
|
||||
{% if not portfolio.applications %}
|
||||
|
||||
{% set can_create_applications = user_can(permissions.ADD_APPLICATION_IN_PORTFOLIO) %}
|
||||
|
||||
{{ EmptyState(
|
||||
'This portfolio doesn’t have any applications yet.',
|
||||
action_label='Add a New Application' if can_create_applications else None,
|
||||
action_href=url_for('portfolios.new_application', portfolio_id=portfolio.id) if can_create_applications else None,
|
||||
icon='cloud',
|
||||
sub_message=None if can_create_applications else 'Please contact your JEDI Cloud portfolio administrator to set up a new application.'
|
||||
) }}
|
||||
|
||||
{% else %}
|
||||
|
||||
{% for application in portfolio.applications %}
|
||||
<div v-cloak class='block-list application-list-item'>
|
||||
<header class='block-list__header'>
|
||||
<h2 class='block-list__title'>{{ application.name }} ({{ application.environments|length }} environments)</h2>
|
||||
{% 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) }}'>
|
||||
{{ Icon('edit') }}
|
||||
<span>edit</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</header>
|
||||
<ul>
|
||||
{% for environment in application.environments %}
|
||||
<li class='block-list__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'>
|
||||
{{ Icon('link') }}
|
||||
<span>{{ environment.name }}</span>
|
||||
</a>
|
||||
|
||||
<div class='application-list-item__environment__members'>
|
||||
<div class='label'>{{ environment.num_users }}</div>
|
||||
<span>members</span>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<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>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
{% if not portfolio.applications %}
|
||||
|
||||
{{ EmptyState(
|
||||
'This portfolio doesn’t have any applications yet.',
|
||||
action_label='Add a New Application' if can_create_applications else None,
|
||||
action_href=url_for('portfolios.new_application', portfolio_id=portfolio.id) if can_create_applications else None,
|
||||
icon='cloud',
|
||||
sub_message=None if can_create_applications else 'Please contact your JEDI Cloud portfolio administrator to set up a new application.'
|
||||
) }}
|
||||
|
||||
{% else %}
|
||||
|
||||
<div class='application-list'>
|
||||
{% for application in portfolio.applications|sort(attribute='name') %}
|
||||
<div is='toggler' v-cloak class='accordian application-list-item'>
|
||||
<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) %}
|
||||
<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>
|
||||
</a>
|
||||
<div class='separator'></div>
|
||||
{% 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>
|
||||
<ul v-if="props.isVisible">
|
||||
{% for environment in application.environments %}
|
||||
<li class='accordian__item application-list-item__environment'>
|
||||
<div class='application-list-item__environment__name'>
|
||||
<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>
|
||||
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</template>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
</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
|
||||
submitted_by: Below is an overview of the projected portfolio submitted by {name}
|
||||
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:
|
||||
example_string: Hello World
|
||||
example_with_variables: 'Hello, {name}!'
|
||||
|
Loading…
x
Reference in New Issue
Block a user