Disable action buttons according to the statuses of environments

In environments list view, action buttons are only clickable if
all checked environments have appropriate status to run this
action.
Page reload after environment status change is added to refresh
displayed buttons.
Selenium tests to check buttons availability are added.

Change-Id: Iae50fb87697c3fa745e412f1cab50dc0da9d981b
Closes-bug: #1593292
This commit is contained in:
Valerii Kovalchuk 2016-06-21 13:32:24 +03:00
parent d791127dbd
commit f719229431
8 changed files with 358 additions and 5 deletions

View File

@ -379,6 +379,11 @@ class ShowEnvironmentServices(tables.LinkAction):
class UpdateEnvironmentRow(tables.Row):
ajax = True
def __init__(self, table, datum=None):
super(UpdateEnvironmentRow, self).__init__(table, datum)
if hasattr(datum, 'status'):
self.attrs['status'] = datum.status
def get_data(self, request, environment_id):
try:
return api.environment_get(request, environment_id)

View File

@ -50,7 +50,10 @@ $(function() {
});
var reloadEnvironmentCalled = false;
var lastStatuses = [];
// Reload page after table update if no more environments left
// or status of some environment changed
$(function() {
"use strict";
$("table#environments").on("update", function () {
@ -60,6 +63,104 @@ $(function() {
reloadEnvironmentCalled = true;
location.reload(true);
}
} else {
var $statuses = [];
for (var $i = 0; $i < $environmentsRows.length; $i++) {
var $row = $($environmentsRows[$i]);
var $rowStatus = getRowStatus($row);
$statuses.push($rowStatus);
}
if (lastStatuses.length !== 0 && areArraysEqual($statuses, lastStatuses) === false) {
if (reloadEnvironmentCalled === false) {
reloadEnvironmentCalled = true;
location.reload(true);
}
} else {
lastStatuses = $statuses;
}
}
});
});
function getRowStatus($row) {
"use strict";
if ($row.hasClass('status_unknown')) {
return "in process";
} else {
return $row.attr("status");
}
}
function areArraysEqual($arr1, $arr2) {
"use strict";
if ($arr1.length !== $arr2.length) {
return false;
}
for (var $i = 0; $i < $arr1.length; $i++) {
if ($arr1[$i] !== $arr2[$i]) {
return false;
}
}
return true;
}
// Disable action buttons according to the statuses of checked environments
$(function() {
"use strict";
var $statuses = {
environments__deploy: ['pending', 'deploy failure'],
environments__delete: ['ready', 'pending', 'new', 'deploy failure'],
environments__abandon: ['ready', 'in process', 'deploy failure', 'delete failure']
};
// Change of individual checkboxes or table update
// TODO(vakovalchuk): improve checkbox detection on the deploying rows
// Deploying rows don't react to selectors less broad than table body, e.g.:
// $("table#environments tbody input[type='checkbox']").change(enableButtons);
$("table#environments tbody").click(enableButtons);
$("table#environments").on("update", enableButtons);
function enableButtons() {
var $buttons = $("table#environments div.table_actions").find('button[name="action"]');
var $environmentsRows = $("table#environments").find('tbody tr:visible').not('.empty');
for (var $i = 0; $i < $buttons.length; $i++) {
var $buttonValue = $buttons[$i].value;
for (var $j = 0; $j < $environmentsRows.length; $j++) {
var $row = $($environmentsRows[$j]);
var $checkbox = $row.find("input.table-row-multi-select").first();
if ($checkbox.prop('checked')) {
var $rowStatus = getRowStatus($row);
if ($statuses[$buttonValue].indexOf($rowStatus) === -1) {
$($buttons[$i]).prop("disabled", true);
break;
}
} else {
$($buttons[$i]).prop("disabled", false);
}
}
}
}
// Change of all checkboxes at once
$("table#environments thead input.table-row-multi-select:checkbox").change(function () {
var $buttons = $("table#environments div.table_actions").find('button[name="action"]');
var $environmentsRows = $("table#environments").find('tbody tr:visible').not('.empty');
if ($(this).prop('checked')) {
for (var $j = 0; $j < $buttons.length; $j++) {
var $buttonValue = $buttons[$j].value;
for (var $k = 0; $k < $environmentsRows.length; $k++) {
var $row = $($environmentsRows[$k]);
var $rowStatus = getRowStatus($row);
if ($statuses[$buttonValue].indexOf($rowStatus) === -1) {
$($buttons[$j]).prop("disabled", true);
break;
}
}
}
} else {
for (var $l = 0; $l < $buttons.length; $l++) {
$($buttons[$l]).prop("disabled", false);
}
}
});
});

View File

@ -0,0 +1,22 @@
Namespaces:
=: io.murano.apps
std: io.murano
Name: DeployingApp
Extends: std:Application
Properties:
name:
Contract: $.string().notNull()
Methods:
testAction:
Usage: Action
Body:
- sleep(3)
- $this.find(std:Environment).reporter.report($this, 'Completed')
deploy:
Body:
- sleep(30)
- $this.find(std:Environment).reporter.report($this, 'Follow the white rabbit')

View File

@ -0,0 +1,114 @@
#Note: it is a fake application, it isn't intended to be deployed
Version: 2
Application:
?:
type: io.murano.apps.DeployingApp
name: $.appConfiguration.name
Forms:
- appConfiguration:
fields:
- name: title
type: string
required: false
hidden: true
description: >-
Fields with different types are presented in this step
- name: domain
type: string
required: false
label: String - Domain Name
description: >-
Requirements: only A-Z, a-z, 0-9, (.) and (-) and should not end with a dash.
Note: Only first 15 characters or characters
before first period is used as NetBIOS name, min/max length are defined
minLength: 2
maxLength: 255
validators:
- expr:
regexpValidator: '^([0-9A-Za-z]|[0-9A-Za-z][0-9A-Za-z-]*[0-9A-Za-z])\.[0-9A-Za-z][0-9A-Za-z-]*[0-9A-Za-z]$'
message: >-
Only letters, numbers and dashes in the middle are
allowed. Period characters are allowed only when they
are used to delimit the components of domain style
names. Single-level domain is not
appropriate. Subdomains are not allowed.
- expr:
regexpValidator: '(^[^.]+$|^[^.]{1,15}\..*$)'
message: >-
NetBIOS name cannot be shorter than 1 symbol and
longer than 15 symbols.
- expr:
regexpValidator: '(^[^.]+$|^[^.]*\.[^.]{2,63}.*$)'
message: >-
DNS host name cannot be shorter than 2 symbols and
longer than 63 symbols.
helpText: >-
Just letters, numbers and dashes are allowed.
A dot can be used to create subdomains
- name: name
type: string
label: Application Name
description: >-
Requirements: Just A-Z, a-z, 0-9, dash and
underline are allowed, min/max value are defined.
minLength: 2
maxLength: 12
regexpValidator: '^[-\w]+$'
errorMessages:
invalid: Just letters, numbers, underscores and hyphens are allowed.
helpText: Just letters, numbers, underscores and hyphens are allowed.
- name: integer
type: integer
required: false
label: Integer - Instance Count
description: >-
Integer field, min/max value are provided
minValue: 1
maxValue: 100
helpText: Enter an integer value between 1 and 100
- name: adminPassword
type: password
required: false
label: Password - Administrator password
descriptionTitle: Passwords
description: >-
Requirements: at least one letter in each
register, a number and a special character. Password length should be
a minimum of 7 characters.
- name: recoveryPassword
type: password
required: false
label: Recovery password
- bindedApps:
fields:
- name: DeployingApp
type: io.murano.apps.DeployingApp
required: false
- instanceConfiguration:
fields:
- name: flavor
type: flavor
label: Instance flavor
required: false
- name: osImage
type: image
imageType: linux
label: Instance image
- name: availabilityZone
type: azone
label: Availability zone
required: false

View File

@ -348,6 +348,12 @@ class PackageBase(UITestCase):
cls.murano_client,
"HotExample",
{"tags": ["hot"]}, hot=True)
cls.deployingapp_id = utils.upload_app_package(
cls.murano_client,
"DeployingApp",
{"categories": ["Web"], "tags": ["tag"]},
hot=False,
package_dir=consts.DeployingPackageDir)
@classmethod
def tearDownClass(cls):
@ -355,6 +361,7 @@ class PackageBase(UITestCase):
cls.murano_client.packages.delete(cls.mockapp_id)
cls.murano_client.packages.delete(cls.postgre_id)
cls.murano_client.packages.delete(cls.hot_app_id)
cls.murano_client.packages.delete(cls.deployingapp_id)
class ImageTestCase(PackageBase):

View File

@ -4,6 +4,8 @@ PackageDir = os.path.join(os.path.dirname(os.path.realpath(__file__)),
'MockApp')
HotPackageDir = os.path.join(os.path.dirname(os.path.realpath(__file__)),
'HotApp')
DeployingPackageDir = os.path.join(os.path.dirname(os.path.realpath(__file__)),
'DeployingApp')
CategorySelector = "//a[contains(text(), '{0}')][contains(@class, 'dropdown-toggle')]" # noqa
EnvAppsCategorySelector = "//*[contains(@id, 'envAppsCategoryBtn')]"
@ -27,6 +29,7 @@ DatabaseCategory = "select[name='add_category-categories'] > option[value='Datab
CategoryPackageCount = "//tr[contains(@data-display, '{0}')]/td[contains(text(), '{1}')]" # noqa
Action = '//a[contains(@class, "murano_action") and contains(text(), "testAction")]' # noqa
HotFlavorField = '//div[contains(@class, "has-error")]//input'
EnvCheckbox = "//tr[contains(@data-display, '{0}')]/td[contains(@class, 'multi_select_column')]//div//label" # noqa
# Buttons
ButtonSubmit = ".//*[@name='wizard_goto_step'][2]"
@ -39,8 +42,11 @@ CreateEnvironment = ".add_env .btn"
DeployEnvironment = "services__action_deploy_env"
DeleteEnvironment = "//button[contains(@id, 'action_delete')]"
DeployEnvironments = ".btn#environments__action_deploy"
DeployEnvironmentsDisabled = ".btn#environments__action_deploy[disabled]"
DeleteEnvironments = ".btn#environments__action_delete"
DeleteEnvironmentsDisabled = ".btn#environments__action_delete[disabled]"
AbandonEnvironments = ".btn#environments__action_abandon"
AbandonEnvironmentsDisabled = ".btn#environments__action_abandon[disabled]"
ConfirmCreateEnvironment = 'confirm_create_env'
AddComponent = "services__action_AddApplication"
AddCategory = "categories__action_add_category"

View File

@ -2151,8 +2151,8 @@ class TestSuiteMultipleEnvironments(base.ApplicationTestCase):
4. Deploy created environments at once
5. Abandon environments before they are deployed
"""
self.add_app_to_env(self.mockapp_id)
self.add_app_to_env(self.mockapp_id)
self.add_app_to_env(self.deployingapp_id)
self.add_app_to_env(self.deployingapp_id)
self.go_to_submenu('Environments')
self.driver.find_element_by_css_selector(
"label[for=ui-id-1]").click()
@ -2167,3 +2167,100 @@ class TestSuiteMultipleEnvironments(base.ApplicationTestCase):
self.wait_for_alert_message()
self.check_element_not_on_page(by.By.LINK_TEXT, 'quick-env-1')
self.check_element_not_on_page(by.By.LINK_TEXT, 'quick-env-2')
def test_check_necessary_buttons_are_available(self):
"""Test check if the necessary buttons are available
Scenario:
1. Create 4 environments with different statuses.
2. Check that all action buttons are on page.
3. Check that "Deploy Environments" button is only clickable
if env with status "Ready to deploy" is checked
4. Check that "Delete Environments" button is only clickable
if envs with statuses "Ready", "Ready to deploy", "Ready to
configure" are checked
5. Check that "Abandon Environments" button is only clickable
if env with status "Ready", "Deploying" are checked
"""
self.go_to_submenu('Environments')
self.create_environment('quick-env-1')
self.go_to_submenu('Environments')
self.check_element_on_page(by.By.XPATH,
c.EnvStatus.format('quick-env-1',
'Ready to configure'))
self.check_element_on_page(by.By.CSS_SELECTOR, c.DeleteEnvironments)
self.add_app_to_env(self.mockapp_id)
self.go_to_submenu('Environments')
self.check_element_on_page(by.By.XPATH,
c.EnvStatus.format('quick-env-2',
'Ready to deploy'))
self.check_element_on_page(by.By.CSS_SELECTOR, c.DeployEnvironments)
self.add_app_to_env(self.mockapp_id)
self.driver.find_element_by_id('services__action_deploy_env').click()
self.check_element_on_page(by.By.XPATH,
c.Status.format('Ready'),
sec=90)
self.go_to_submenu('Environments')
self.check_element_on_page(by.By.XPATH,
c.EnvStatus.format('quick-env-3', 'Ready'))
self.check_element_on_page(by.By.CSS_SELECTOR, c.AbandonEnvironments)
self.add_app_to_env(self.deployingapp_id)
self.driver.find_element_by_id('services__action_deploy_env').click()
self.go_to_submenu('Environments')
self.check_element_on_page(by.By.XPATH,
c.EnvStatus.format('quick-env-4',
'Deploying'))
env_1_checkbox = self.driver.find_element_by_xpath(
c.EnvCheckbox.format('quick-env-1'))
env_2_checkbox = self.driver.find_element_by_xpath(
c.EnvCheckbox.format('quick-env-2'))
env_3_checkbox = self.driver.find_element_by_xpath(
c.EnvCheckbox.format('quick-env-3'))
# select 'Ready to deploy' env
env_2_checkbox.click()
# check that Deploy is possible
self.check_element_on_page(by.By.CSS_SELECTOR, c.DeployEnvironments)
self.check_element_not_on_page(by.By.CSS_SELECTOR,
c.DeployEnvironmentsDisabled)
# add 'Ready to configure' env to selection
env_1_checkbox.click()
# check that Deploy is no more possible
self.check_element_on_page(by.By.CSS_SELECTOR,
c.DeployEnvironmentsDisabled)
# add 'Ready' env to selection
env_3_checkbox.click()
# check that Delete is possible
self.check_element_on_page(by.By.CSS_SELECTOR, c.DeleteEnvironments)
self.check_element_not_on_page(by.By.CSS_SELECTOR,
c.DeleteEnvironmentsDisabled)
# add 'Deploying' env to selection
env_4_checkbox = self.driver.find_element_by_xpath(
c.EnvCheckbox.format('quick-env-4'))
env_4_checkbox.click()
# check that Delete is no more possible
self.check_element_on_page(by.By.CSS_SELECTOR,
c.DeleteEnvironmentsDisabled)
# check that Abandon is not possible
self.check_element_on_page(by.By.CSS_SELECTOR,
c.AbandonEnvironmentsDisabled)
# unselect all envs but 'Deploying' and 'Ready'
env_1_checkbox.click()
env_2_checkbox.click()
# check that Abandon is now possible
self.check_element_on_page(by.By.CSS_SELECTOR, c.AbandonEnvironments)
self.check_element_not_on_page(by.By.CSS_SELECTOR,
c.AbandonEnvironmentsDisabled)
self.check_element_on_page(by.By.XPATH,
c.EnvStatus.format('quick-env-4', 'Ready'),
sec=90)

View File

@ -31,11 +31,12 @@ class ImageException(Exception):
return self._error_string
def upload_app_package(client, app_name, data, hot=False):
def upload_app_package(client, app_name, data, hot=False,
package_dir=consts.PackageDir):
try:
if not hot:
manifest = os.path.join(consts.PackageDir, 'manifest.yaml')
archive = compose_package(app_name, manifest, consts.PackageDir)
manifest = os.path.join(package_dir, 'manifest.yaml')
archive = compose_package(app_name, manifest, package_dir)
else:
manifest = os.path.join(consts.HotPackageDir, 'manifest.yaml')
archive = compose_package(app_name, manifest,