예제 #1
0
    def build_widgets(self, title_widgets):
        if title_widgets is None:
            title_widgets = [Text("Machines", align='center')]

        self.filter_edit_box = FilterBox(self.handle_filter_change)

        header_widgets = title_widgets + [Divider()]

        if self.show_filter_box:
            header_widgets += [self.filter_edit_box, Divider()]
        labels = ["ID", "Cores", "Memory (GiB)", "Storage (GiB)"]
        if self.show_assignments:
            labels += ["Assignments", ""]
        else:
            labels += [""]

        header_label_col = Columns([Text(m) for m in labels], dividechars=2)
        header_widgets.append(header_label_col)
        self.header_padding = len(header_widgets)
        self.add_new_button = AttrMap(
            PlainButton("Add New Machine", on_press=self.do_add_machine),
            'button_secondary', 'button_secondary focus')
        self.add_new_cols = Columns(
            [Text(s)
             for s in [' ', ' ', ' ', ' ', ' ']] + [self.add_new_button],
            dividechars=2)
        self.machine_pile = Pile(header_widgets + self.machine_widgets +
                                 [self.add_new_cols])
        return self.machine_pile
예제 #2
0
    def build_widgets(self, title_widgets):
        if title_widgets is None:
            title_widgets = [Text("Machines", align='center')]

        self.filter_edit_box = FilterBox(self.handle_filter_change)

        header_widgets = title_widgets + [Divider()]

        if self.show_filter_box:
            header_widgets += [self.filter_edit_box, Divider()]
        labels = ["ID", "Cores", "Memory (GiB)", "Storage (GiB)"]
        if self.show_assignments:
            labels += ["Assignments", ""]
        else:
            labels += [""]

        header_label_col = Columns([Text(m) for m in labels],
                                   dividechars=2)
        header_widgets.append(header_label_col)
        self.header_padding = len(header_widgets)
        self.add_new_button = AttrMap(
            PlainButton("Add New Machine",
                        on_press=self.do_add_machine),
            'button_secondary',
            'button_secondary focus')
        self.add_new_cols = Columns([Text(s) for s in
                                     [' ', ' ', ' ', ' ', ' ']] +
                                    [self.add_new_button], dividechars=2)
        self.machine_pile = Pile(header_widgets + self.machine_widgets +
                                 [self.add_new_cols])
        return self.machine_pile
예제 #3
0
    def build_widgets(self, title_widgets):
        if title_widgets is None:
            if len(self.constraints) > 0:
                cstr = " matching constraints '{}'".format(
                    constraints_from_dict(self.constraints))
            else:
                cstr = ""

            title_widgets = [Text("Machines" + cstr, align='center')]

        self.filter_edit_box = FilterBox(self.handle_filter_change)

        header_widgets = title_widgets + [Divider()]

        if self.show_filter_box:
            header_widgets += [self.filter_edit_box, Divider()]
        labels = ["FQDN", "Cores", "Memory (GiB)", "Storage (GiB)", ""]
        header_label_col = Columns([Text(m) for m in labels])
        header_widgets.append(header_label_col)
        self.header_padding = len(header_widgets)
        self.machine_pile = Pile(header_widgets + self.machine_widgets)
        return self.machine_pile
예제 #4
0
    def build_widgets(self, title_widgets):
        if title_widgets is None:
            if len(self.constraints) > 0:
                cstr = " matching constraints '{}'".format(
                    constraints_from_dict(self.constraints))
            else:
                cstr = ""

            title_widgets = [Text("Machines" + cstr, align='center')]

        self.filter_edit_box = FilterBox(self.handle_filter_change)

        header_widgets = title_widgets + [Divider()]

        if self.show_filter_box:
            header_widgets += [self.filter_edit_box, Divider()]
        labels = ["FQDN", "Cores", "Memory (GiB)", "Storage (GiB)", ""]
        header_label_col = Columns([Text(m) for m in labels])
        header_widgets.append(header_label_col)
        self.header_padding = len(header_widgets)
        self.machine_pile = Pile(header_widgets + self.machine_widgets)
        return self.machine_pile
예제 #5
0
class MachinesList(WidgetWrap):
    """A list of machines with configurable action buttons for each
    machine.

    select_cb - a function that takes a machine and assignmenttype to
    perform the button action

    unselect_cb - a function that takes a machine and clears assignments

    target_info - a string that describes what you're picking a machine for

    current_pin_cb - a function that takes a machine and returns
    None if the machine is not currently selected, or returns a
    context value to show the user about the current selection (like a
    juju machine ID that represents the current pin)

    constraints - a dict of constraints to filter the machines list.
    only machines matching all the constraints will be shown.

    show_hardware - bool, whether or not to show the hardware details
    for each of the machines

    title_widgets - A Text Widget to be used in place of the default
    title.

    show_only_ready - bool, only show machines with a ready state.

    show_filter_box - bool, show text box to filter listed machines

    """
    def __init__(self,
                 select_cb,
                 unselect_cb,
                 target_info,
                 current_pin_cb,
                 constraints=None,
                 show_hardware=False,
                 title_widgets=None,
                 show_only_ready=False,
                 show_filter_box=False):
        self.select_cb = select_cb
        self.unselect_cb = unselect_cb
        self.target_info = target_info
        self.current_pin_cb = current_pin_cb

        self.n_selected = 0
        self.machine_widgets = []
        if constraints is None:
            self.constraints = {}
        else:
            self.constraints = constraints
        self.show_hardware = show_hardware
        self.show_only_ready = show_only_ready
        self.show_filter_box = show_filter_box
        self.filter_string = ""
        self.loading = False
        w = self.build_widgets(title_widgets)
        self.update()
        super().__init__(w)

    def __repr__(self):
        return "machineslist"

    def selectable(self):
        # overridden to ensure that we can arrow through the buttons
        # shouldn't be necessary according to documented behavior of
        # Pile & Columns, but discovered via trial & error.
        return True

    def build_widgets(self, title_widgets):
        if title_widgets is None:
            if len(self.constraints) > 0:
                cstr = " matching constraints '{}'".format(
                    constraints_from_dict(self.constraints))
            else:
                cstr = ""

            title_widgets = [Text("Machines" + cstr, align='center')]

        self.filter_edit_box = FilterBox(self.handle_filter_change)

        header_widgets = title_widgets + [Divider()]

        if self.show_filter_box:
            header_widgets += [self.filter_edit_box, Divider()]
        labels = ["FQDN", "Cores", "Memory (GiB)", "Storage (GiB)", ""]
        header_label_col = Columns([Text(m) for m in labels])
        header_widgets.append(header_label_col)
        self.header_padding = len(header_widgets)
        self.machine_pile = Pile(header_widgets + self.machine_widgets)
        return self.machine_pile

    def handle_filter_change(self, edit_button, userdata):
        self.filter_string = userdata
        self.update()

    def find_machine_widget(self, m):
        return next((mw for mw in self.machine_widgets
                     if mw.machine.instance_id == m.instance_id), None)

    def update(self):
        if app.maas.client:
            machines = app.maas.client.get_machines()
        else:
            machines = None

        if machines is None:
            if not self.loading:
                self.loading = True
                p = (Text("\n\nLoading...",
                          align='center'), self.machine_pile.options())
                self.machine_pile.contents.append(p)
            return

        if self.loading:
            self.loading = False
            self.machine_pile.contents = self.machine_pile.contents[:-1]

        if self.show_only_ready:
            machines = [
                m for m in machines if m.status == MaasMachineStatus.READY
            ]
        for mw in self.machine_widgets:
            machine = next(
                (m
                 for m in machines if mw.machine.instance_id == m.instance_id),
                None)
            if machine is None:
                self.remove_machine(mw.machine)

        n_satisfying_machines = len(machines)

        def get_placement_filter_label(d):
            s = ""
            for atype, al in d.items():
                s += " ".join([
                    "{} {}".format(cc.service_name, cc.display_name)
                    for cc in al
                ])
            return s

        for m in machines:
            if not satisfies(m, self.constraints)[0]:
                self.remove_machine(m)
                n_satisfying_machines -= 1
                continue

            filter_label = m.filter_label()
            if self.filter_string != "" and \
               self.filter_string not in filter_label:
                self.remove_machine(m)
                continue

            mw = self.find_machine_widget(m)
            if mw is None:
                mw = self.add_machine_widget(m)

            mw.update()

        self.filter_edit_box.set_info(len(self.machine_widgets),
                                      n_satisfying_machines)

        self.sort_machine_widgets()

    def add_machine_widget(self, machine):
        mw = MachineWidget(machine, self.handle_select, self.handle_unselect,
                           self.target_info, self.current_pin_cb)

        self.machine_widgets.append(mw)
        options = self.machine_pile.options()
        self.machine_pile.contents.append((mw, options))

        return mw

    def remove_machine(self, machine):
        mw = self.find_machine_widget(machine)
        if mw is None:
            return

        self.machine_widgets.remove(mw)
        mw_idx = 0
        for w, opts in self.machine_pile.contents:
            if w == mw:
                break
            mw_idx += 1

        c = self.machine_pile.contents[:mw_idx] + \
            self.machine_pile.contents[mw_idx + 1:]
        self.machine_pile.contents = c

    def sort_machine_widgets(self):
        def keyfunc(mw):
            m = mw.machine
            hwinfo = " ".join(map(str,
                                  [m.arch, m.cpu_cores, m.mem, m.storage]))
            if str(mw.machine.status) == 'ready':
                skey = 'A'
            else:
                skey = str(mw.machine.status)
            return skey + mw.machine.hostname + hwinfo

        self.machine_widgets.sort(key=keyfunc)

        def wrappedkeyfunc(t):
            mw, options = t
            if not isinstance(mw, MachineWidget):
                return 'A'
            return keyfunc(mw)

        self.machine_pile.contents.sort(key=wrappedkeyfunc)

    def focus_prev_or_top(self):
        self.update()
        try:
            if self.machine_pile.focus_position <= self.header_padding:
                self.machine_pile.focus_position = self.header_padding
        except IndexError:
            log.debug("index error in machines_list focus_top")

    def handle_select(self, machine):
        self.select_cb(machine)
        self.n_selected += 1
        self.update()

    def handle_unselect(self, machine):
        self.unselect_cb(machine)
        self.n_selected -= 1
        self.update()
예제 #6
0
class JujuMachinesList(WidgetWrap):
    """A list of machines in a juju bundle, with configurable action
    buttons for each machine.

    application - an application

    machines - list of machine info dicts

    assign_cb - a function that takes a machine and assignmenttype to
    perform the button action

    unassign_cb - a function that takes a machine and clears assignments

    controller - a controller object that provides get_placements and get_pin.

    show_constraints - bool, whether or not to show the constraints
    for each of the machines

    title_widgets - A list of widgets to be used in place of the default
    title.

    show_assignments - bool, whether or not to show the assignments
    for each of the machines.

    show_filter_box - bool, show text box to filter listed machines

    """
    def __init__(self,
                 application,
                 machines,
                 assign_cb,
                 unassign_cb,
                 add_machine_cb,
                 remove_machine_cb,
                 controller,
                 show_constraints=True,
                 title_widgets=None,
                 show_assignments=True,
                 show_filter_box=False,
                 show_pins=False):
        self.application = application
        self.machines = machines
        self.assign_cb = assign_cb
        self.unassign_cb = unassign_cb
        self.add_machine_cb = add_machine_cb
        self.remove_machine_cb = remove_machine_cb
        self.controller = controller
        self.machine_widgets = []
        self.show_assignments = show_assignments
        self.all_assigned = False
        self.show_filter_box = show_filter_box
        self.show_pins = show_pins
        self.filter_string = ""
        w = self.build_widgets(title_widgets)
        self.update()
        super().__init__(w)

    def __repr__(self):
        return "machineslist"

    def selectable(self):
        # overridden to ensure that we can arrow through the buttons
        # shouldn't be necessary according to documented behavior of
        # Pile & Columns, but discovered via trial & error.
        return True

    def build_widgets(self, title_widgets):
        if title_widgets is None:
            title_widgets = [Text("Machines", align='center')]

        self.filter_edit_box = FilterBox(self.handle_filter_change)

        header_widgets = title_widgets + [Divider()]

        if self.show_filter_box:
            header_widgets += [self.filter_edit_box, Divider()]
        labels = ["ID", "Cores", "Memory (GiB)", "Storage (GiB)"]
        if self.show_assignments:
            labels += ["Assignments", ""]
        else:
            labels += [""]

        header_label_col = Columns([Text(m) for m in labels], dividechars=2)
        header_widgets.append(header_label_col)
        self.header_padding = len(header_widgets)
        self.add_new_button = AttrMap(
            PlainButton("Add New Machine", on_press=self.do_add_machine),
            'button_secondary', 'button_secondary focus')
        self.add_new_cols = Columns(
            [Text(s)
             for s in [' ', ' ', ' ', ' ', ' ']] + [self.add_new_button],
            dividechars=2)
        self.machine_pile = Pile(header_widgets + self.machine_widgets +
                                 [self.add_new_cols])
        return self.machine_pile

    def do_add_machine(self, sender):
        self.add_machine_cb()
        self.update()

    def remove_machine(self, sender):
        self.remove_machine_cb()
        self.update()

    def handle_filter_change(self, edit_button, userdata):
        self.filter_string = userdata
        self.update()

    def find_machine_widget(self, midx):
        return next(
            (mw for mw in self.machine_widgets if mw.juju_machine_id == midx),
            None)

    def update(self):

        for midx, md in sorted(self.machines.items()):
            allvalues = ["{}={}".format(k, v) for k, v in md.items()]
            filter_label = midx + " " + " ".join(allvalues)
            if self.filter_string != "" and \
               self.filter_string not in filter_label:
                self.remove_machine_widget(midx)
                continue

            mw = self.find_machine_widget(midx)
            if mw is None:
                mw = self.add_machine_widget(midx, md)
            mw.all_assigned = self.all_assigned
            mw.update()

        n = len(self.machine_widgets)
        self.filter_edit_box.set_info(n, n)

        self.sort_machine_widgets()

    def add_machine_widget(self, midx, md):
        mw = JujuMachineWidget(midx, md, self.application, self.assign_cb,
                               self.unassign_cb, self.controller,
                               self.show_assignments, self.show_pins)
        self.machine_widgets.append(mw)
        options = self.machine_pile.options()
        self.machine_pile.contents.insert(
            len(self.machine_pile.contents) - 1, (mw, options))

        return mw

    def remove_machine_widget(self, midx):
        mw = self.find_machine_widget(midx)
        if mw is None:
            return

        self.machine_widgets.remove(mw)
        mw_idx = 0
        for w, opts in self.machine_pile.contents:
            if w == mw:
                break
            mw_idx += 1

        c = self.machine_pile.contents[:mw_idx] + \
            self.machine_pile.contents[mw_idx + 1:]
        self.machine_pile.contents = c

    def sort_machine_widgets(self):

        self.machine_widgets.sort(key=attrgetter('juju_machine_id'))

        def wrappedkeyfunc(t):
            mw, options = t
            if isinstance(mw, JujuMachineWidget):
                return "B{}".format(mw.juju_machine_id)
            if mw in [self.add_new_cols]:
                return 'C'
            return 'A'

        self.machine_pile.contents.sort(key=wrappedkeyfunc)

    def focus_prev_or_top(self):
        self.update()
        try:
            if self.machine_pile.focus_position <= self.header_padding:
                self.machine_pile.focus_position = self.header_padding
        except IndexError:
            log.debug("index error in machines_list focus_top")
예제 #7
0
class MachinesList(WidgetWrap):

    """A list of machines with configurable action buttons for each
    machine.

    select_cb - a function that takes a machine and assignmenttype to
    perform the button action

    unselect_cb - a function that takes a machine and clears assignments

    target_info - a string that describes what you're picking a machine for

    current_pin_cb - a function that takes a machine and returns
    None if the machine is not currently selected, or returns a
    context value to show the user about the current selection (like a
    juju machine ID that represents the current pin)

    constraints - a dict of constraints to filter the machines list.
    only machines matching all the constraints will be shown.

    show_hardware - bool, whether or not to show the hardware details
    for each of the machines

    title_widgets - A Text Widget to be used in place of the default
    title.

    show_only_ready - bool, only show machines with a ready state.

    show_filter_box - bool, show text box to filter listed machines

    """

    def __init__(self, select_cb, unselect_cb, target_info,
                 current_pin_cb,
                 constraints=None, show_hardware=False,
                 title_widgets=None, show_only_ready=False,
                 show_filter_box=False):
        self.select_cb = select_cb
        self.unselect_cb = unselect_cb
        self.target_info = target_info
        self.current_pin_cb = current_pin_cb

        self.n_selected = 0
        self.machine_widgets = []
        if constraints is None:
            self.constraints = {}
        else:
            self.constraints = constraints
        self.show_hardware = show_hardware
        self.show_only_ready = show_only_ready
        self.show_filter_box = show_filter_box
        self.filter_string = ""
        self.loading = False
        w = self.build_widgets(title_widgets)
        self.update()
        super().__init__(w)

    def __repr__(self):
        return "machineslist"

    def selectable(self):
        # overridden to ensure that we can arrow through the buttons
        # shouldn't be necessary according to documented behavior of
        # Pile & Columns, but discovered via trial & error.
        return True

    def build_widgets(self, title_widgets):
        if title_widgets is None:
            if len(self.constraints) > 0:
                cstr = " matching constraints '{}'".format(
                    constraints_from_dict(self.constraints))
            else:
                cstr = ""

            title_widgets = [Text("Machines" + cstr, align='center')]

        self.filter_edit_box = FilterBox(self.handle_filter_change)

        header_widgets = title_widgets + [Divider()]

        if self.show_filter_box:
            header_widgets += [self.filter_edit_box, Divider()]
        labels = ["FQDN", "Cores", "Memory (GiB)", "Storage (GiB)", ""]
        header_label_col = Columns([Text(m) for m in labels])
        header_widgets.append(header_label_col)
        self.header_padding = len(header_widgets)
        self.machine_pile = Pile(header_widgets + self.machine_widgets)
        return self.machine_pile

    def handle_filter_change(self, edit_button, userdata):
        self.filter_string = userdata
        self.update()

    def find_machine_widget(self, m):
        return next((mw for mw in self.machine_widgets if
                     mw.machine.instance_id == m.instance_id), None)

    def update(self):
        if app.maas.client:
            machines = app.maas.client.get_machines()
        else:
            machines = None

        if machines is None:
            if not self.loading:
                self.loading = True
                p = (Text("\n\nLoading...", align='center'),
                     self.machine_pile.options())
                self.machine_pile.contents.append(p)
            return

        if self.loading:
            self.loading = False
            self.machine_pile.contents = self.machine_pile.contents[:-1]

        if self.show_only_ready:
            machines = [m for m in machines
                        if m.status == MaasMachineStatus.READY]
        for mw in self.machine_widgets:
            machine = next((m for m in machines if
                            mw.machine.instance_id == m.instance_id), None)
            if machine is None:
                self.remove_machine(mw.machine)

        n_satisfying_machines = len(machines)

        def get_placement_filter_label(d):
            s = ""
            for atype, al in d.items():
                s += " ".join(["{} {}".format(cc.service_name,
                                              cc.display_name)
                               for cc in al])
            return s

        for m in machines:
            if not satisfies(m, self.constraints)[0]:
                self.remove_machine(m)
                n_satisfying_machines -= 1
                continue

            filter_label = m.filter_label()
            if self.filter_string != "" and \
               self.filter_string not in filter_label:
                self.remove_machine(m)
                continue

            mw = self.find_machine_widget(m)
            if mw is None:
                mw = self.add_machine_widget(m)

            mw.update()

        self.filter_edit_box.set_info(len(self.machine_widgets),
                                      n_satisfying_machines)

        self.sort_machine_widgets()

    def add_machine_widget(self, machine):
        mw = MachineWidget(machine,
                           self.handle_select,
                           self.handle_unselect,
                           self.target_info,
                           self.current_pin_cb)

        self.machine_widgets.append(mw)
        options = self.machine_pile.options()
        self.machine_pile.contents.append((mw, options))

        return mw

    def remove_machine(self, machine):
        mw = self.find_machine_widget(machine)
        if mw is None:
            return

        self.machine_widgets.remove(mw)
        mw_idx = 0
        for w, opts in self.machine_pile.contents:
            if w == mw:
                break
            mw_idx += 1

        c = self.machine_pile.contents[:mw_idx] + \
            self.machine_pile.contents[mw_idx + 1:]
        self.machine_pile.contents = c

    def sort_machine_widgets(self):
        def keyfunc(mw):
            m = mw.machine
            hwinfo = " ".join(map(str, [m.arch, m.cpu_cores, m.mem,
                                        m.storage]))
            if str(mw.machine.status) == 'ready':
                skey = 'A'
            else:
                skey = str(mw.machine.status)
            return skey + mw.machine.hostname + hwinfo
        self.machine_widgets.sort(key=keyfunc)

        def wrappedkeyfunc(t):
            mw, options = t
            if not isinstance(mw, MachineWidget):
                return 'A'
            return keyfunc(mw)

        self.machine_pile.contents.sort(key=wrappedkeyfunc)

    def focus_prev_or_top(self):
        self.update()
        try:
            if self.machine_pile.focus_position <= self.header_padding:
                self.machine_pile.focus_position = self.header_padding
        except IndexError:
            log.debug("index error in machines_list focus_top")

    def handle_select(self, machine):
        self.select_cb(machine)
        self.n_selected += 1
        self.update()

    def handle_unselect(self, machine):
        self.unselect_cb(machine)
        self.n_selected -= 1
        self.update()
예제 #8
0
class JujuMachinesList(WidgetWrap):

    """A list of machines in a juju bundle, with configurable action
    buttons for each machine.

    application - an application

    machines - list of machine info dicts

    assign_cb - a function that takes a machine and assignmenttype to
    perform the button action

    unassign_cb - a function that takes a machine and clears assignments

    controller - a controller object that provides get_placements and get_pin.

    show_constraints - bool, whether or not to show the constraints
    for each of the machines

    title_widgets - A list of widgets to be used in place of the default
    title.

    show_assignments - bool, whether or not to show the assignments
    for each of the machines.

    show_filter_box - bool, show text box to filter listed machines

    """

    def __init__(self, application, machines, assign_cb, unassign_cb,
                 add_machine_cb, remove_machine_cb,
                 controller,
                 show_constraints=True,
                 title_widgets=None, show_assignments=True,
                 show_filter_box=False,
                 show_pins=False):
        self.application = application
        self.machines = machines
        self.assign_cb = assign_cb
        self.unassign_cb = unassign_cb
        self.add_machine_cb = add_machine_cb
        self.remove_machine_cb = remove_machine_cb
        self.controller = controller
        self.machine_widgets = []
        self.show_assignments = show_assignments
        self.all_assigned = False
        self.show_filter_box = show_filter_box
        self.show_pins = show_pins
        self.filter_string = ""
        w = self.build_widgets(title_widgets)
        self.update()
        super().__init__(w)

    def __repr__(self):
        return "machineslist"

    def selectable(self):
        # overridden to ensure that we can arrow through the buttons
        # shouldn't be necessary according to documented behavior of
        # Pile & Columns, but discovered via trial & error.
        return True

    def build_widgets(self, title_widgets):
        if title_widgets is None:
            title_widgets = [Text("Machines", align='center')]

        self.filter_edit_box = FilterBox(self.handle_filter_change)

        header_widgets = title_widgets + [Divider()]

        if self.show_filter_box:
            header_widgets += [self.filter_edit_box, Divider()]
        labels = ["ID", "Cores", "Memory (GiB)", "Storage (GiB)"]
        if self.show_assignments:
            labels += ["Assignments", ""]
        else:
            labels += [""]

        header_label_col = Columns([Text(m) for m in labels],
                                   dividechars=2)
        header_widgets.append(header_label_col)
        self.header_padding = len(header_widgets)
        self.add_new_button = AttrMap(
            PlainButton("Add New Machine",
                        on_press=self.do_add_machine),
            'button_secondary',
            'button_secondary focus')
        self.add_new_cols = Columns([Text(s) for s in
                                     [' ', ' ', ' ', ' ', ' ']] +
                                    [self.add_new_button], dividechars=2)
        self.machine_pile = Pile(header_widgets + self.machine_widgets +
                                 [self.add_new_cols])
        return self.machine_pile

    def do_add_machine(self, sender):
        self.add_machine_cb()
        self.update()

    def remove_machine(self, sender):
        self.remove_machine_cb()
        self.update()

    def handle_filter_change(self, edit_button, userdata):
        self.filter_string = userdata
        self.update()

    def find_machine_widget(self, midx):
        return next((mw for mw in self.machine_widgets if
                     mw.juju_machine_id == midx), None)

    def update(self):

        for midx, md in sorted(self.machines.items()):
            allvalues = ["{}={}".format(k, v) for k, v in md.items()]
            filter_label = midx + " " + " ".join(allvalues)
            if self.filter_string != "" and \
               self.filter_string not in filter_label:
                self.remove_machine_widget(midx)
                continue

            mw = self.find_machine_widget(midx)
            if mw is None:
                mw = self.add_machine_widget(midx, md)
            mw.all_assigned = self.all_assigned
            mw.update()

        n = len(self.machine_widgets)
        self.filter_edit_box.set_info(n, n)

        self.sort_machine_widgets()

    def add_machine_widget(self, midx, md):
        mw = JujuMachineWidget(midx, md,
                               self.application,
                               self.assign_cb,
                               self.unassign_cb,
                               self.controller,
                               self.show_assignments,
                               self.show_pins)
        self.machine_widgets.append(mw)
        options = self.machine_pile.options()
        self.machine_pile.contents.insert(
            len(self.machine_pile.contents) - 1,
            (mw, options))

        return mw

    def remove_machine_widget(self, midx):
        mw = self.find_machine_widget(midx)
        if mw is None:
            return

        self.machine_widgets.remove(mw)
        mw_idx = 0
        for w, opts in self.machine_pile.contents:
            if w == mw:
                break
            mw_idx += 1

        c = self.machine_pile.contents[:mw_idx] + \
            self.machine_pile.contents[mw_idx + 1:]
        self.machine_pile.contents = c

    def sort_machine_widgets(self):

        self.machine_widgets.sort(key=attrgetter('juju_machine_id'))

        def wrappedkeyfunc(t):
            mw, options = t
            if isinstance(mw, JujuMachineWidget):
                return "B{}".format(mw.juju_machine_id)
            if mw in [self.add_new_cols]:
                return 'C'
            return 'A'

        self.machine_pile.contents.sort(key=wrappedkeyfunc)

    def focus_prev_or_top(self):
        self.update()
        try:
            if self.machine_pile.focus_position <= self.header_padding:
                self.machine_pile.focus_position = self.header_padding
        except IndexError:
            log.debug("index error in machines_list focus_top")