# -*- coding: utf-8 -*-
import attr
import re
from cached_property import cached_property
from navmazing import NavigateToAttribute
from cfme.modeling.base import BaseCollection, BaseEntity
from cfme.utils.timeutil import parsetime
from cfme.utils.appliance.implementations.ui import navigator, CFMENavigateStep, navigate_to
from cfme.utils.wait import wait_for
from widgetastic.utils import ParametrizedLocator, ParametrizedString, Parameter
from widgetastic.widget import ParametrizedView, Text, View, Widget, ConditionalSwitchableView
from widgetastic.xpath import quote
from widgetastic_patternfly import Button, Dropdown, Tab
from widgetastic_manageiq import Table
from .base.login import BaseLoggedInPage
# TODO: Move this into widgetastic_patternfly
[docs]class Kebab(Widget):
"""The so-called "kebab" widget of Patternfly.
<http://www.patternfly.org/pattern-library/widgets/#kebabs>
Args:
button_id: id of the button tag inside the kebab. If not specified, first kebab available
will be used
"""
ROOT = ParametrizedLocator('{@locator}')
UL = './ul[contains(@class, "dropdown-menu")]'
BUTTON = './button'
ITEM = './ul/li/a[normalize-space(.)={}]'
ITEMS = './ul/li/a'
def __init__(self, parent, button_id=None, logger=None):
super(Kebab, self).__init__(parent, logger=logger)
if button_id is not None:
self.locator = (
'.//div[contains(@class, "dropdown-kebab-pf") and ./button[@id={}]]'.format(
quote(button_id)))
else:
self.locator = './/div[contains(@class, "dropdown-kebab-pf") and ./button][1]'
@property
def is_opened(self):
"""Returns opened state of the kebab."""
return self.browser.is_displayed(self.UL)
@property
def items(self):
"""Lists all items in the kebab.
Returns:
:py:class:`list` of :py:class:`str`
"""
return [self.browser.text(item) for item in self.browser.elements(self.ITEMS)]
[docs] def open(self):
"""Open the kebab"""
if not self.is_opened:
self.browser.click(self.BUTTON)
[docs] def close(self):
"""Close the kebab"""
if self.is_opened:
self.browser.click(self.BUTTON)
[docs] def select(self, item, close=True):
"""Select a specific item from the kebab.
Args:
item: Item to be selected.
close: Whether to close the kebab after selection. If the item is a link, you may want
to set this to ``False``
"""
try:
self.open()
self.browser.click(self.ITEM.format(quote(item)))
finally:
if close:
self.close()
[docs]class DashboardView(BaseLoggedInPage):
"""View that represents the Intelligence/Dashboard."""
reset_button = Button(title="Reset Dashboard Widgets to the defaults")
add_widget = Dropdown('Add a widget')
@View.nested
class zoomed(View): # noqa
"""Represents the zoomed modal panel"""
title = Text('.//div[@id="lightbox-panel"]//h2[contains(@class, "card-pf-title")]')
close = Text('.//div[@id="lightbox-panel"]//a[normalize-space(@title)="Close"]')
[docs] def ensure_zoom_closed(self):
if self.zoomed.title.is_displayed:
self.zoomed.close.click()
@ParametrizedView.nested
class dashboards(Tab, ParametrizedView): # noqa
PARAMETERS = ('title', )
ALL_LOCATOR = './/ul[contains(@class, "nav-tabs-pf")]/li/a'
COLUMN_LOCATOR = '//div[@id="col{}"]//h2'
tab_name = Parameter('title')
@classmethod
def all(cls, browser):
return [(browser.text(e), ) for e in browser.elements(cls.ALL_LOCATOR)]
def column_widget_names(self, column_index):
"""Returns names of widgets in column specified.
Args:
column_index: Position of the column. Numbered from 1!
Returns:
:py:class:`list` of :py:class:`str`
"""
return [
self.browser.text(e)
for e
in self.browser.elements(self.COLUMN_LOCATOR.format(column_index))]
@ParametrizedView.nested
class widgets(ParametrizedView): # noqa
PARAMETERS = ('title', )
ALL_LOCATOR = '//div[starts-with(@id, "w_")]//h2[contains(@class, "card-pf-title")]'
BLANK_SLATE = './/div[contains(@class, "blank-slate-pf")]//h1'
CHART = './div/div/div[starts-with(@id, "miq_widgetchart_")]'
RSS = './div/div[contains(@class, "rss_widget")]'
RSS_TABLE = './div[./div[contains(@class, "rss_widget")]]/div/table'
TABLE = './div/table|./div/div/table'
MC = (
'./div/div[contains(@class, "mc")]/*[1]|./div/div[starts-with(@id, "dd_w") '
'and contains(@id, "_box")]/*[1]')
ROOT = ParametrizedLocator(
'.//div[starts-with(@id, "w_") and .//h2[contains(@class, "card-pf-title")'
' and normalize-space(.)={title|quote}]]')
title = Text('.//h2[contains(@class, "card-pf-title")]')
menu = Kebab(button_id=ParametrizedString('btn_{@widget_id}'))
contents = ConditionalSwitchableView(reference='content_type')
# Unsupported reading yet
contents.register(None, default=True, widget=Widget())
contents.register('chart', widget=Widget())
# Reading supported
contents.register('table', widget=Table(TABLE))
contents.register('rss', widget=Table(RSS_TABLE))
footer = Text('.//div[contains(@class, "card-pf-footer")]')
@property
def column(self):
"""Returns the column position of this widget. Numbered from 1!"""
parent = self.browser.element('..')
try:
parent_id = self.browser.get_attribute('id', parent).strip()
return int(re.sub(r'^col(\d+)$', '\\1', parent_id))
except (ValueError, TypeError, AttributeError):
raise ValueError('Could not get the column index of widget')
@property
def minimized(self):
return not self.browser.is_displayed(self.MC)
@cached_property
def widget_id(self):
id_attr = self.browser.get_attribute('id', self)
return int(id_attr.rsplit('_', 1)[-1])
@cached_property
def content_type(self):
if self.browser.elements(self.BLANK_SLATE):
# No data yet
return None
elif self.browser.elements(self.RSS):
return 'rss'
elif self.browser.is_displayed(self.CHART):
return 'chart'
elif self.browser.is_displayed(self.TABLE):
return 'table'
else:
return None
@property
def blank(self):
return bool(self.browser.elements(self.BLANK_SLATE))
@classmethod
def all(cls, browser):
return [(browser.text(e), ) for e in browser.elements(cls.ALL_LOCATOR)]
@property
def is_displayed(self):
return (
self.logged_in_as_current_user and
self.navigation.currently_selected == ['Cloud Intel', 'Dashboard'])
[docs]class ParticularDashboardView(DashboardView):
@property
def is_displayed(self):
return (
super(ParticularDashboardView, self).is_displayed and
self.dashboards(title=self.obj.name).is_active)
@attr.s
@attr.s
@attr.s
[docs]class Dashboard(BaseEntity):
name = attr.ib()
_collections = {'widgets': DashboardWidgetCollection}
@property
def dashboard_view(self):
"""Returns a view pointed at a particular dashboard."""
return navigate_to(self, 'Details').dashboards(title=self.name)
[docs] def drag_and_drop(self, dragged_widget_or_name, dropped_widget_or_name):
"""Drags and drops widgets onto each other."""
if isinstance(dragged_widget_or_name, DashboardWidget):
dragged_widget_or_name = dragged_widget_or_name.name
if isinstance(dropped_widget_or_name, DashboardWidget):
dropped_widget_object = dropped_widget_or_name
dropped_widget_or_name = dropped_widget_or_name.name
else:
dropped_widget_object = self.collections.widgets.instantiate(dropped_widget_or_name)
view = self.dashboard_view
first_widget = view.widgets(title=dragged_widget_or_name).title
if dropped_widget_object.last_in_column:
# Different behaviour
dropped_widget = view.widgets(title=dropped_widget_or_name)
middle = view.browser.middle_of(dropped_widget)
position = view.browser.location_of(dropped_widget)
size = view.browser.size_of(dropped_widget)
drop_x = middle.x
drop_y = position.x + size.height + 10
view.browser.drag_and_drop_to(first_widget, to_x=drop_x, to_y=drop_y)
else:
second_widget = view.widgets(title=dropped_widget_or_name).footer
view.browser.drag_and_drop(first_widget, second_widget)
view.browser.plugin.ensure_page_safe()
@attr.s
[docs]class DashboardCollection(BaseCollection):
"""Represents the Dashboard page and can jump around various dashboards present."""
ENTITY = Dashboard
@property
def default(self):
"""Returns an instance of the ``Default Dashboard``"""
return self.instantiate('Default Dashboard')
[docs] def all(self):
view = navigate_to(self.appliance.server, 'Dashboard')
result = []
# TODO: Idiomatize the following line
for (dashboard_name, ) in view.dashboards.view_class.all(view.browser):
result.append(self.instantiate(dashboard_name))
return result
[docs] def refresh(self):
"""Refreshes the dashboard view by forcibly clicking the navigation again."""
view = navigate_to(self.appliance.server, 'Dashboard')
view.navigation.select('Cloud Intel', 'Dashboard')
@property
def zoomed_name(self):
"""Grabs the name of the currently zoomed widget."""
view = navigate_to(self.appliance.server, 'Dashboard')
if not view.zoomed.is_displayed:
return None
return view.zoomed.title.text
[docs] def close_zoom(self):
"""Closes any zoomed widget."""
navigate_to(self.appliance.server, 'Dashboard').ensure_zoom_closed()
@navigator.register(Dashboard, 'Details')
[docs]class DashboardDetails(CFMENavigateStep):
VIEW = ParticularDashboardView
prerequisite = NavigateToAttribute('appliance.server', 'Dashboard')
[docs] def step(self):
self.prerequisite_view.dashboards(title=self.obj.name).select()