def test_custom_button_order_ansible_playbook_service(setup_provider, vmware_vm, custom_vm_button,
        service_request, service, appliance):
    view = navigate_to(vmware_vm, "Details")
    view.toolbar.custom_button(custom_vm_button.group.text).item_select(custom_vm_button.text)
    submit_button = WButton(appliance.browser.widgetastic, "Submit")
    submit_button.click()
    wait_for(service_request.exists, num_sec=600)
    service_request.wait_for_request()
    view = navigate_to(service, "Details")
    assert view.provisioning.details.get_text_of("Hosts") == "localhost"
    assert view.provisioning.results.get_text_of("Status") == "successful"
def test_embedded_ansible_custom_button(full_template_vm, custom_vm_button, service_request,
                                        service, appliance):
    view = navigate_to(full_template_vm, "Details")
    view.toolbar.custom_button(custom_vm_button.group.text).item_select(custom_vm_button.text)
    submit_button = WButton(appliance.browser.widgetastic, "Submit")
    submit_button.click()
    wait_for(service_request.exists, num_sec=600)
    service_request.wait_for_request()
    view = navigate_to(service, "Details")
    if custom_vm_button.inventory == "Localhost":
        assert view.provisioning.details.get_text_of("Hosts") == "localhost"
    else:
        assert view.provisioning.details.get_text_of("Hosts") == full_template_vm.ip_address
    assert view.provisioning.results.get_text_of("Status") == "successful"
Exemple #3
0
class ResourcePoolDetailsToolbar(View):
    """The toolbar on the details page"""
    configuration = Dropdown('Configuration')
    policy = Dropdown('Policy')
    download = Button(title='Download summary in PDF format')
Exemple #4
0
class CustomSearch(Search):
    search_field = TextInput(locator=".//input[@role='combobox']")
    clear_button = Text(".//span[contains(@class,'fa-times')]")
    search_button = Button('Search')
Exemple #5
0
 class toolbar(View):  # noqa
     configuration = Dropdown("Configuration")
     policy = Dropdown(text='Policy')
     download = Button(title="Print or export summary")
class SetOwnershipView(SetOwnershipForm):
    title = Text('#explorer_title_text')

    save_button = Button('Save')

    is_displayed = displayed_not_implemented
class CompanyTagsEditView(CompanyTagsAddView):
    """Edit Company Tags view"""
    save_button = Button('Save')
    reset_button = Button('Reset')
class TenantForm(ConfigurationView):
    """ Tenant Form """
    name = Input(name='name')
    description = Input(name='description')

    cancel_button = Button('Cancel')
Exemple #9
0
class NetworkRouterDetailsToolBar(View):
    """ Represents provider toolbar and its controls """
    configuration = Dropdown(text='Configuration')
    policy = Dropdown(text='Policy')
    edit = Dropdown(text='Edit')
    download = Button(title='Print or export summary')
Exemple #10
0
class NetworkProviderDetailsToolBar(NetworkProviderToolBar):
    """ Represents provider details toolbar """
    monitoring = Dropdown(text='Monitoring')
    download = Button(title='Print or export summary')
Exemple #11
0
class CloudNetworkDetailsToolBar(View):
    """ Represents provider details toolbar """
    policy = Dropdown(text='Policy')
    download = Button(title='Print or export summary')
Exemple #12
0
class BalancerDetailsToolBar(BalancerToolBar):
    """ Represents details toolbar of balancer summary """
    download = Button(title='Print or export summary')
class ObjectStoreContainerDetailsToolbar(View):
    """The toolbar on the Object Store Containers detail page"""
    policy = Dropdown('Policy')
    download = Button(title='Download summary in PDF format')
Exemple #14
0
class ClusterDetailsToolbar(View):
    """The toolbar on the detail page"""
    configuration = Dropdown('Configuration')
    policy = Dropdown('Policy')
    monitoring = Dropdown('Monitoring')
    download = Button('Download summary in PDF format')
class ZoneCollectLogToolbar(View):
    configuration = Dropdown('Configuration')
    edit = Button(title="Edit the Log Depot settings for the selected Zone")
    collect = Dropdown('Collect Logs')
class CollectLogsCredsEntities(View):
    username = Input(name='log_userid')
    password = Input(name='log_password')
    confirm_password = Input(name='log_verify')
    validate_button = Button('Validate')
Exemple #17
0
class FlavorDetailsToolBar(View):
    policy = Dropdown('Policy')
    download = Button(title='Print or export summary')
    configuration = Dropdown('Configuration')
Exemple #18
0
class NetworkRouterAddInterfaceView(BaseLoggedInPage):
    """ Represents Add Interface to Network Router page """
    subnet_name = Select(id='cloud_subnet_id')
    add = Button('Add')

    is_displayed = displayed_not_implemented
Exemple #19
0
class FlavorDetailsToolBar(View):
    policy = Dropdown('Policy')
    download = Button(title='Download summary in PDF format')
Exemple #20
0
class SecurityGroupDetailsToolBar(View):
    """ Represents provider details toolbar """
    policy = Dropdown(text='Policy')
    download = Button(title='Print or export summary')
    view_selector = View.nested(ItemsToolBarViewSelector)
class MapTagsEditView(MapTagsAddView):
    """Edit Map Tags view"""
    save_button = Button('Save')
    reset_button = Button('Reset')
Exemple #22
0
class SubnetDetailsToolBar(View):
    """ Represents provider details toolbar """
    configuration = Dropdown(text='Configuration')
    policy = Dropdown(text='Policy')
    download = Button(title='Print or export summary')
Exemple #23
0
 class toolbar(View):  # noqa
     reload = Button(title='Refresh this page')
     policy = Dropdown(text='Policy')
     download = Dropdown(text='Download')
     view_selector = View.nested(ItemsToolBarViewSelector)
Exemple #24
0
class OneProviderComponentsToolbar(View):
    policy = Dropdown(text='Policy')
    download = Dropdown(text='Download')
    back = Button(name='show_summary')
    view_selector = View.nested(ItemsToolBarViewSelector)
Exemple #25
0
    class schema(WaitTab):  # noqa
        schema_title = Text('//div[@class="form_div"]/h3')

        @ParametrizedView.nested
        class fields(ParametrizedView):  # noqa
            PARAMETERS = ('name', )
            # Points to the <tr>
            ROOT = ParametrizedLocator(
                './/input[starts-with(@id, "fields_name_") and @value={name|quote}]/../..'
            )
            ALL_FIELDS = './/input[starts-with(@name, "fields_name_")]'

            @cached_property
            def row_id(self):
                attr = self.browser.get_attribute(
                    'id',
                    './td/input[starts-with(@id, "fields_name_")',
                    parent=self)
                return int(attr.rsplit('_', 1)[-1])

            name = Input(name=ParametrizedString('fields_name_{@row_id}'))
            type = BootstrapSelect(
                ParametrizedString('fields_aetype_{@row_id}'))
            data_type = BootstrapSelect(
                ParametrizedString('fields_datatype_{@row_id}'))
            default_value = Input(
                name=ParametrizedString('fields_default_value_{@row_id}'))
            display_name = Input(
                name=ParametrizedString('fields_display_name_{@row_id}'))
            description = Input(
                name=ParametrizedString('fields_description_{@row_id}'))
            substitute = Checkbox(
                name=ParametrizedString('fields_substitute_{@row_id}'))
            collect = Input(
                name=ParametrizedString('fields_collect_{@row_id}'))
            message = Input(
                name=ParametrizedString('fields_message_{@row_id}'))
            on_entry = Input(
                name=ParametrizedString('fields_on_entry_{@row_id}'))
            on_exit = Input(
                name=ParametrizedString('fields_on_exit_{@row_id}'))
            on_error = Input(
                name=ParametrizedString('fields_on_error_{@row_id}'))
            max_retries = Input(
                name=ParametrizedString('fields_max_retries_{@row_id}'))
            max_time = Input(
                name=ParametrizedString('fields_max_time_{@row_id}'))

            def delete(self):
                xpath = './/a[@title="Click to delete this field from schema"]'
                self.browser.click(xpath, parent=self)
                try:
                    del self.row_id
                except AttributeError:
                    pass

            @classmethod
            def all(cls, browser):
                result = []
                for e in browser.elements(cls.ALL_FIELDS):
                    result.append((browser.get_attribute('value', e), ))
                return result

        add_field = Text(
            '//div[@id="class_fields_div"]//i[contains(@class, "fa-plus")]')
        name = Input(name='field_name')
        type = BootstrapSelect('field_aetype')
        data_type = BootstrapSelect('field_datatype')
        default_value = Input(name='field_default_value')
        display_name = Input(name='field_display_name')
        description = Input(name='field_description')
        substitute = Checkbox(name='field_substitute')
        collect = Input(name='field_collect')
        message = Input(name='field_message')
        on_entry = Input(name='field_on_entry')
        on_exit = Input(name='field_on_exit')
        on_error = Input(name='field_on_error')
        max_retries = Input(name='field_max_retries')
        max_time = Input(name='field_max_time')
        finish_add_field = Text('//a[@title="Add this entry"]')

        save_button = Button('Save')
        reset_button = Button('Reset')
        cancel_button = Button('Cancel')
Exemple #26
0
class FloatingIpDetailsToolBar(View):
    """ Represents toolbar of summary of port """
    policy = Dropdown(text='Policy')
    download = Button(title='Print or export summary')
class ExpressionEditor(View, Pretty):
    """This class enables to embed the expression in a Form.

    Args:
        show_func: Function to call to show the expression if there are more of them.
    """
    @View.nested
    class field_form_view(View):  # noqa
        type = BootstrapSelect("chosen_typ")
        field = BootstrapSelect("chosen_field")
        key = BootstrapSelect("chosen_key")
        value = Input(name="chosen_value")
        user_input = Input(name="user_input")

    @View.nested
    class field_date_form(View):  # noqa
        dropdown_select = BootstrapSelect("chosen_from_1")
        input_select_date = Calendar(name="miq_date_1_0")
        input_select_time = BootstrapSelect("miq_time_1_0")

    @View.nested
    class count_form_view(View):  # noqa
        type = BootstrapSelect("chosen_typ")
        count = BootstrapSelect("chosen_count")
        key = BootstrapSelect("chosen_key")
        value = Input(name="chosen_value")
        user_input = Input(name="user_input")

    @View.nested
    class tag_form_view(View):  # noqa
        type = BootstrapSelect("chosen_typ")
        tag = BootstrapSelect("chosen_tag")
        value = BootstrapSelect("chosen_value")
        user_input = Input(name="user_input")

    @View.nested
    class find_form_view(View):  # noqa
        type = BootstrapSelect("chosen_typ")
        field = BootstrapSelect("chosen_field")
        skey = BootstrapSelect("chosen_skey")
        value = Input(name="chosen_value")
        check = BootstrapSelect("chosen_check")
        cfield = BootstrapSelect("chosen_cfield")
        ckey = BootstrapSelect("chosen_ckey")
        cvalue = Input(name="chosen_cvalue")

    @View.nested
    class registry_form_view(View):  # noqa
        type = BootstrapSelect("chosen_typ")
        key = Input(name="chosen_regkey")
        value = Input(name="chosen_regval")
        operation = BootstrapSelect("chosen_key")
        contents = Input(name="chosen_value")

    @View.nested
    class date_specific_form_view(View):  # noqa
        date = Calendar(name="miq_date_1_0")
        time = BootstrapSelect("miq_time_1_0")

    @View.nested
    class date_relative_form_view(View):  # noqa
        from_ = BootstrapSelect("chosen_from_1")
        through = BootstrapSelect("chosen_through_1")

    ROOT = "//div[@id='exp_editor_div']"
    MAKE_BUTTON = "//span[not(contains(@style,'none'))]//img[@alt='{}']"
    ATOM_ROOT = "./div[@id='exp_atom_editor_div']"
    EXPRESSIONS_ROOT = "./fieldset/div"
    COMMIT = VersionPick({
        Version.lowest():
        "//img[@alt='Commit expression element changes']",
        "5.7.1":
        Button(title="Commit expression element changes"),
    })
    DISCARD = VersionPick({
        Version.lowest():
        "//img[@alt='Discard expression element changes']",
        "5.7.1":
        Button(title="Discard expression element changes"),
    })
    REMOVE = VersionPick({
        Version.lowest(): ("//span[not(contains(@style, 'none'))]/"
                           "/img[@alt='Remove this expression element']"),
        "5.8":
        Button(title="Remove this expression element"),
    })
    NOT = VersionPick({
        Version.lowest():
        ("//span[not(contains(@style, 'none'))]"
         "//img[@alt='Wrap this expression element with a NOT']"),
        "5.8":
        Button(title="Wrap this expression element with a NOT"),
    })
    OR = VersionPick({
        Version.lowest(): ("//span[not(contains(@style, 'none'))]/"
                           "/img[@alt='OR with a new expression element']"),
        "5.8":
        Button(title="OR with a new expression element"),
    })
    AND = VersionPick({
        Version.lowest(): ("//span[not(contains(@style, 'none'))]/"
                           "/img[@alt='AND with a new expression element']"),
        "5.8":
        Button(title="AND with a new expression element"),
    })
    REDO = VersionPick({
        Version.lowest(): "//img[@alt='Redo']",
        "5.8": Button(title="Redo the last change"),
    })
    UNDO = VersionPick({
        Version.lowest(): "//img[@alt='Undo']",
        "5.8": Button(title="Undo the last change"),
    })
    SELECT_SPECIFIC = "//img[@alt='Click to change to a specific Date/Time format']"
    SELECT_RELATIVE = "//img[@alt='Click to change to a relative Date/Time format']"

    pretty_attrs = ['show_loc']

    def __init__(self, parent, show_loc=None, logger=None):
        View.__init__(self, parent, logger=logger)
        self.show_loc = show_loc

    def __locator__(self):
        return self.ROOT

    def click_undo(self):
        self.browser.click(self.UNDO)

    def click_redo(self):
        self.browser.click(self.REDO)

    def click_and(self):
        self.browser.click(self.AND)

    def click_or(self):
        self.browser.click(self.OR)

    def click_not(self):
        self.browser.click(self.NOT)

    def click_remove(self):
        self.browser.click(self.REMOVE)

    def click_commit(self):
        self.browser.click(self.COMMIT)

    def click_discard(self):
        self.browser.click(self.DISCARD)

    def click_switch_to_relative(self):
        self.browser.click(self.SELECT_RELATIVE)

    def click_switch_to_specific(self):
        self.browser.click(self.SELECT_SPECIFIC)

    @property
    def _atom_root(self):
        return self.browser.element(self.ATOM_ROOT)

    @property
    def _expressions_root(self):
        return self.browser.element(self.EXPRESSIONS_ROOT)

    def select_first_expression(self):
        """There is always at least one (???), so no checking of bounds."""
        self.browser.elements("//a[contains(@id,'exp_')]",
                              parent=self._expressions_root)[0].click()

    def select_expression_by_text(self, text):
        self.browser.click(
            "//a[contains(@id,'exp_')][contains(normalize-space(text()),'{}')]"
            .format(text))

    def no_expression_present(self):
        els = self.browser.elements("//a[contains(@id,'exp_')]",
                                    parent=self._expressions_root)
        if len(els) > 1:
            return False
        return els[0].text.strip() == "???"

    def any_expression_present(self):
        return not self.no_expression_present()

    def is_editing(self):
        try:
            self.browser.element(
                "//a[contains(@id,'exp_')][contains(normalize-space(text()),'???')]",
                parent=self._expressions_root)
            return True
        except NoSuchElementException:
            return False

    def delete_whole_expression(self):
        while self.any_expression_present():
            self.select_first_expression()
            self.click_remove()

    def read(self):
        """Returns whole expression as represented visually."""
        return self._expressions_root.text.encode("utf-8").strip()

    def enable_editor(self):
        try:
            el = self.browser.element(self.show_loc)
            wait_for(lambda: el.is_displayed, num_sec=2, delay=0.2)
            el.click()
        except (TimedOutError, NoSuchElementException):
            pass

    def fill(self, expression):
        if self.show_loc is not None:
            self.enable_editor()
        prog = create_program(expression, self)
        before = self._expressions_root.text.encode("utf-8").strip()
        prog()
        after = self._expressions_root.text.encode("utf-8").strip()
        return before != after

    def fill_count(self, count=None, key=None, value=None):
        """ Fills the 'Count of' type of form.

        If the value is unspecified and we are in the advanced search form (user input),
        the user_input checkbox will be checked if the value is None.

        Args:
            count: Name of the field to compare (Host.VMs, ...).
            key: Operation to do (=, <, >=, ...).
            value: Value to check against.
        """
        view = self.count_form_view
        view.fill(dict(type="Count of", count=count, key=key, value=value))
        # In case of advanced search box
        if view.user_input.is_displayed:
            user_input = value is None
            view.user_input.fill(user_input)
        self.click_commit()

    def fill_tag(self, tag=None, value=None):
        """ Fills the 'Tag' type of form.

        Args:
            tag: Name of the field to compare.
            value: Value to check against.
        """
        view = self.tag_form_view
        view.fill(dict(type="Tag", tag=tag, value=value))
        # In case of advanced search box
        if view.user_input.is_displayed:
            user_input = value is None
            view.user_input.fill(user_input)
        self.click_commit()

    def fill_registry(self,
                      key=None,
                      value=None,
                      operation=None,
                      contents=None):
        """ Fills the 'Registry' type of form."""
        view = self.registry_form_view
        view.fill(
            dict(
                type="Registry",
                key=key,
                value=value,
                operation=operation,
                contents=contents,
            ))
        self.click_commit()

    def fill_find(self,
                  field=None,
                  skey=None,
                  value=None,
                  check=None,
                  cfield=None,
                  ckey=None,
                  cvalue=None):
        view = self.find_form_view
        view.fill(
            dict(type="Find",
                 field=field,
                 skey=skey,
                 value=value,
                 check=check,
                 cfield=cfield,
                 ckey=ckey,
                 cvalue=cvalue))
        self.click_commit()

    def fill_field(self, field=None, key=None, value=None):
        """ Fills the 'Field' type of form.

        Args:
            tag: Name of the field to compare (Host.VMs, ...).
            key: Operation to do (=, <, >=, IS NULL, ...).
            value: Value to check against.
        """
        field_norm = field.strip().lower()
        if ("date updated" in field_norm or "date created" in field_norm
                or "boot time" in field_norm or "timestamp" in field_norm):
            no_date = False
        else:
            no_date = True
        view = self.field_form_view
        view.fill(
            dict(
                type="Field",
                field=field,
                key=key,
                value=value if no_date else None,
            ))
        # In case of advanced search box
        if view.user_input.is_displayed:
            user_input = value is None
            view.user_input.fill(user_input)
        if not no_date:
            # Flip the right part of form
            view = self.field_date_form
            if (isinstance(value, basestring)
                    and not re.match(r"^[0-9]{2}/[0-9]{2}/[0-9]{4}$", value)):
                if not view.dropdown_select.is_displayed:
                    self.click_switch_to_relative()
                view.fill({"dropdown_select": value})
                self.click_commit()
            else:
                # Specific selection
                if not view.input_select_date.is_displayed:
                    self.click_switch_to_specific()
                if (isinstance(value, tuple)
                        or isinstance(value, list)) and len(value) == 2:
                    date, time = value
                elif isinstance(value,
                                basestring):  # is in correct format mm/dd/yyyy
                    # Date only (for now)
                    date = value[:]
                    time = None
                else:
                    raise TypeError(
                        "fill_field expects a 2-tuple (date, time) or string with date"
                    )
                # TODO datetime.datetime support
                view.input_select_date.fill(date)
                # Try waiting a little bit for time field
                # If we don't wait, committing the expression will glitch
                try:
                    wait_for(lambda: view.input_select_time.is_displayed,
                             num_sec=6)
                    # It appeared, so if the time is to be set, we will set it
                    # (passing None glitches)
                    if time:
                        view.input_select_time.fill(time)
                except TimedOutError:
                    # Did not appear, ignore that
                    pass
                finally:
                    # And finally, commit the expression :)
                    self.click_commit()
        else:
            self.click_commit()
class ReportsMultiBoxSelect(MultiBoxSelect):
    move_into_button = Button(title=Parameter("@move_into"))
    move_from_button = Button(title=Parameter("@move_from"))
Exemple #29
0
class DashboardView(BaseLoggedInPage):
    """View that represents the Intelligence/Dashboard."""
    reset_button = Button(title="Reset Dashboard Widgets to the defaults")

    def reset_widgets(self, cancel=False):
        """Clicks the reset button to reset widgets and handles the alert."""
        self.browser.click(self.reset_button, ignore_ajax=True)
        self.browser.handle_alert(cancel=cancel, wait=10.0)
        self.browser.plugin.ensure_page_safe()

    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"]')

    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'])
class KeyPairDetailsToolbar(View):
    policy = Dropdown('Policy')
    configuration = Dropdown('Configuration')
    download = Button(title='Download summary in PDF format')
class KeyPairAddForm(View):
    name = TextInput(id='name')
    public_key = TextInput(id='public_key')
    provider = BootstrapSelect(id='ems_id')
    add = Button('Add')
    cancel = Button('Cancel')
class ServerCollectLogsToolbar(View):
    edit = Button(title="Edit the Log Depot settings for the selected Server")
    collect = Dropdown('Collect Logs')