class MachineWidget(WidgetWrap): """A widget displaying a machine and one action button. machine - the machine to display select_cb - a function that takes a machine to select unselect_cb - a function that takes a machine to un-select target_info - a string describing what we're pinning to current_pin_cb - a function that takes a machine and returns a string if it's already pinned or None if it is not """ def __init__(self, machine, select_cb, unselect_cb, target_info, current_pin_cb): self.machine = machine self.select_cb = select_cb self.unselect_cb = unselect_cb self.target_info = target_info self.current_pin_cb = current_pin_cb w = self.build_widgets() super().__init__(w) self.update() def selectable(self): return True def build_widgets(self): m = self.machine l = [ '{:20s}'.format(m.hostname), '{:3d}'.format(m.cpu_cores), '{:6s}'.format(m.mem), '{:10s}'.format(m.storage) ] self.select_button_label = "Pin {} to {}".format( m.hostname, self.target_info) self.unselect_button_label = "Un-pin {} from ".format( m.hostname) + "{}" self.select_button = PlainButton(self.select_button_label, self.handle_button) cols = [Text(m) for m in l] cols += [AttrMap(self.select_button, 'text', 'button_secondary focus')] self.columns = Columns(cols) return self.columns def update_machine(self): """Refresh with potentially updated machine info from controller. Assumes that machine exists - machines going away is handled in machineslist.update(). """ machines = app.maas.client.get_machines() if machines is None: return self.machine = next( (m for m in machines if m.instance_id == self.machine.instance_id), None) def __repr__(self): return "widget for " + str(self.machine) def update(self): self.update_machine() current_pin = self.current_pin_cb(self.machine) if current_pin: l = self.unselect_button_label.format(current_pin) self.select_button.set_label(l) else: self.select_button.set_label(self.select_button_label) def handle_button(self, sender): if self.current_pin_cb(self.machine): self.unselect_cb(self.machine) else: self.select_cb(self.machine) self.update()
class ApplicationConfigureView(WidgetWrap): def __init__(self, application, metadata_controller, controller): self.controller = controller self.application = application self.options_copy = self.application.options.copy() self.metadata_controller = metadata_controller self.widgets = self.build_widgets() self.description_w = Text("") self.showing_all = False self.buttons_selected = False self.frame = Frame(body=self.build_widgets(), footer=self.build_footer()) super().__init__(self.frame) self.metadata_controller.get_readme( self.application.csid.as_seriesname(), partial(self._handle_readme_load)) def _handle_readme_load(self, readme_f): EventLoop.loop.event_loop._loop.call_soon_threadsafe( partial(self._update_readme_on_main_thread, readme_f.result())) def _update_readme_on_main_thread(self, readme): rt = self._trim_readme(readme) self.description_w.set_text(rt) def _trim_readme(self, readme): rls = readme.splitlines() rls = [l for l in rls if not l.startswith("#")] nrls = [] for i in range(len(rls)): if i + 1 == len(rls): break if len(rls[i]) > 0: if rls[i][0] in ['-', '#', '=']: continue if len(rls[i + 1]) > 0: if rls[i + 1][0] in ['-', '=']: continue nrls.append(rls[i]) if len(nrls) == 0: return if nrls[0] == '': nrls = nrls[1:] # split after two paragraphs: if '' in nrls: firstparidx = nrls.index('') else: firstparidx = 1 try: splitidx = nrls.index('', firstparidx + 1) except: splitidx = firstparidx nrls = nrls[:splitidx] return "\n".join(nrls) def selectable(self): return True def keypress(self, size, key): # handle keypress first, then get new focus widget rv = super().keypress(size, key) if key in ['tab', 'shift tab']: self._swap_focus() return rv def _swap_focus(self): if not self.buttons_selected: self.buttons_selected = True self.frame.focus_position = 'footer' self.buttons.focus_position = 3 else: self.buttons_selected = False self.frame.focus_position = 'body' def build_widgets(self): ws = [Text("Configure {}".format( self.application.service_name))] num_unit_ow = OptionWidget("Units", "int", "How many units to deploy.", self.application.orig_num_units, current_value=self.application.num_units, value_changed_callback=self.handle_scale) ws.append(num_unit_ow) ws += self.get_whitelisted_option_widgets() self.toggle_show_all_button_index = len(ws) + 1 self.toggle_show_all_button = PlainButton( "Show Advanced Configuration", self.do_toggle_show_all_config) ws += [HR(), Columns([('weight', 1, Text(" ")), (36, Color.button_secondary( self.toggle_show_all_button))])] self.pile = Pile(ws) return Padding.center_90(Filler(self.pile, valign="top")) def build_footer(self): cancel = menu_btn(on_press=self.do_cancel, label="\n BACK\n") confirm = menu_btn(on_press=self.do_commit, label="\n APPLY CHANGES\n") self.buttons = Columns([ ('fixed', 2, Text("")), ('fixed', 13, Color.menu_button( cancel, focus_map='button_primary focus')), Text(""), ('fixed', 20, Color.menu_button( confirm, focus_map='button_primary focus')), ('fixed', 2, Text("")) ]) footer = Pile([ HR(top=0), Padding.center_90(self.description_w), Padding.line_break(""), Color.frame_footer(Pile([ Padding.line_break(""), self.buttons])) ]) return footer def get_whitelisted_option_widgets(self): service_id = self.application.csid.as_str_without_rev() options = self.metadata_controller.get_options(service_id) svc_opts_whitelist = utils.get_options_whitelist( self.application.service_name) hidden = [n for n in options.keys() if n not in svc_opts_whitelist] log.info("Hiding options not in the whitelist: {}".format(hidden)) return self._get_option_widgets(svc_opts_whitelist, options) def get_non_whitelisted_option_widgets(self): service_id = self.application.csid.as_str_without_rev() options = self.metadata_controller.get_options(service_id) svc_opts_whitelist = utils.get_options_whitelist( self.application.service_name) hidden = [n for n in options.keys() if n not in svc_opts_whitelist] return self._get_option_widgets(hidden, options) def _get_option_widgets(self, opnames, options): ws = [] for opname in opnames: opdict = options[opname] cv = self.application.options.get(opname, None) ow = OptionWidget(opname, opdict['Type'], opdict['Description'], opdict['Default'], current_value=cv, value_changed_callback=self.handle_edit) ws.append(ow) return ws def do_toggle_show_all_config(self, sender): if not self.showing_all: new_ows = self.get_non_whitelisted_option_widgets() header = Text("Advanced Configuration Options") opts = self.pile.options() self.pile.contents.append((header, opts)) for ow in new_ows: self.pile.contents.append((ow, opts)) self.toggle_show_all_button.set_label( "Hide Advanced Configuration") self.showing_all = True else: i = self.toggle_show_all_button_index self.pile.contents = self.pile.contents[:i + 1] self.toggle_show_all_button.set_label( "Show Advanced Configuration") self.showing_all = False def handle_edit(self, opname, value): self.options_copy[opname] = value def handle_scale(self, opname, scale): self.application.num_units = scale def do_cancel(self, sender): self.controller.handle_sub_view_done() def do_commit(self, sender): self.application.options = self.options_copy self.controller.handle_sub_view_done()
class ApplicationConfigureView(WidgetWrap): def __init__(self, application, metadata_controller, controller): self.controller = controller self.application = application self.options_copy = self.application.options.copy() self.num_units_copy = self.application.num_units self.metadata_controller = metadata_controller self.widgets = self.build_widgets() self.description_w = Text("") self.showing_all = False self.buttons_selected = False self.frame = Frame(body=self.build_widgets(), footer=self.build_footer()) super().__init__(self.frame) self.metadata_controller.get_readme( self.application.csid.as_seriesname(), partial(self._handle_readme_load)) def _handle_readme_load(self, readme_f): EventLoop.loop.event_loop._loop.call_soon_threadsafe( partial(self._update_readme_on_main_thread, readme_f.result())) def _update_readme_on_main_thread(self, readme): rt = self._trim_readme(readme) self.description_w.set_text(rt) def _trim_readme(self, readme): rls = readme.splitlines() rls = [l for l in rls if not l.startswith("#")] nrls = [] for i in range(len(rls)): if i + 1 == len(rls): break if len(rls[i]) > 0: if rls[i][0] in ['-', '#', '=']: continue if len(rls[i + 1]) > 0: if rls[i + 1][0] in ['-', '=']: continue nrls.append(rls[i]) if len(nrls) == 0: return if nrls[0] == '': nrls = nrls[1:] # split after two paragraphs: if '' in nrls: firstparidx = nrls.index('') else: firstparidx = 1 try: splitidx = nrls.index('', firstparidx + 1) except: splitidx = firstparidx nrls = nrls[:splitidx] return "\n".join(nrls) def selectable(self): return True def keypress(self, size, key): # handle keypress first, then get new focus widget rv = super().keypress(size, key) if key in ['tab', 'shift tab']: self._swap_focus() return rv def _swap_focus(self): if not self.buttons_selected: self.buttons_selected = True self.frame.focus_position = 'footer' self.buttons.focus_position = 3 else: self.buttons_selected = False self.frame.focus_position = 'body' def build_widgets(self): ws = [Text("Configure {}".format(self.application.service_name))] num_unit_ow = OptionWidget("Units", "int", "How many units to deploy.", self.application.orig_num_units, current_value=self.num_units_copy, value_changed_callback=self.handle_scale) ws.append(num_unit_ow) ws += self.get_whitelisted_option_widgets() self.toggle_show_all_button_index = len(ws) + 1 self.toggle_show_all_button = PlainButton( "Show Advanced Configuration", self.do_toggle_show_all_config) ws += [ HR(), Columns([('weight', 1, Text(" ")), (36, Color.button_secondary(self.toggle_show_all_button)) ]) ] self.pile = Pile(ws) return Padding.center_90(Filler(self.pile, valign="top")) def build_footer(self): cancel = menu_btn(on_press=self.do_cancel, label="\n BACK\n") confirm = menu_btn(on_press=self.do_commit, label="\n APPLY CHANGES\n") self.buttons = Columns([ ('fixed', 2, Text("")), ('fixed', 13, Color.menu_button(cancel, focus_map='button_primary focus')), Text(""), ('fixed', 20, Color.menu_button(confirm, focus_map='button_primary focus')), ('fixed', 2, Text("")) ]) footer = Pile([ HR(top=0), Padding.center_90(self.description_w), Padding.line_break(""), Color.frame_footer(Pile([Padding.line_break(""), self.buttons])) ]) return footer def get_whitelisted_option_widgets(self): service_id = self.application.csid.as_str_without_rev() options = self.metadata_controller.get_options(service_id) svc_opts_whitelist = utils.get_options_whitelist( self.application.service_name) hidden = [n for n in options.keys() if n not in svc_opts_whitelist] log.info("Hiding options not in the whitelist: {}".format(hidden)) return self._get_option_widgets(svc_opts_whitelist, options) def get_non_whitelisted_option_widgets(self): service_id = self.application.csid.as_str_without_rev() options = self.metadata_controller.get_options(service_id) svc_opts_whitelist = utils.get_options_whitelist( self.application.service_name) hidden = [n for n in options.keys() if n not in svc_opts_whitelist] return self._get_option_widgets(hidden, options) def _get_option_widgets(self, opnames, options): ws = [] for opname in opnames: try: opdict = options[opname] except KeyError: app.log.debug( "Unknown charm option ({}), skipping".format(opname)) continue cv = self.application.options.get(opname, None) ow = OptionWidget(opname, opdict['Type'], opdict['Description'], opdict['Default'], current_value=cv, value_changed_callback=self.handle_edit) ws.append(ow) return ws def do_toggle_show_all_config(self, sender): if not self.showing_all: new_ows = self.get_non_whitelisted_option_widgets() header = Text("Advanced Configuration Options") opts = self.pile.options() self.pile.contents.append((header, opts)) for ow in new_ows: self.pile.contents.append((ow, opts)) self.toggle_show_all_button.set_label( "Hide Advanced Configuration") self.showing_all = True else: i = self.toggle_show_all_button_index self.pile.contents = self.pile.contents[:i + 1] self.toggle_show_all_button.set_label( "Show Advanced Configuration") self.showing_all = False def handle_edit(self, opname, value): self.options_copy[opname] = value def handle_scale(self, opname, scale): self.num_units_copy = scale def do_cancel(self, sender): self.controller.handle_sub_view_done() def do_commit(self, sender): self.application.options = self.options_copy self.application.num_units = self.num_units_copy self.controller.handle_sub_view_done()
class MachineWidget(WidgetWrap): """A widget displaying a machine and one action button. machine - the machine to display select_cb - a function that takes a machine to select unselect_cb - a function that takes a machine to un-select target_info - a string describing what we're pinning to current_pin_cb - a function that takes a machine and returns a string if it's already pinned or None if it is not """ def __init__(self, machine, select_cb, unselect_cb, target_info, current_pin_cb): self.machine = machine self.select_cb = select_cb self.unselect_cb = unselect_cb self.target_info = target_info self.current_pin_cb = current_pin_cb w = self.build_widgets() super().__init__(w) self.update() def selectable(self): return True def build_widgets(self): m = self.machine l = ['{:20s}'.format(m.hostname), '{:3d}'.format(m.cpu_cores), '{:6s}'.format(m.mem), '{:10s}'.format(m.storage)] self.select_button_label = "Pin {} to {}".format(m.hostname, self.target_info) self.unselect_button_label = "Un-pin {} from ".format( m.hostname) + "{}" self.select_button = PlainButton(self.select_button_label, self.handle_button) cols = [Text(m) for m in l] cols += [AttrMap(self.select_button, 'text', 'button_secondary focus')] self.columns = Columns(cols) return self.columns def update_machine(self): """Refresh with potentially updated machine info from controller. Assumes that machine exists - machines going away is handled in machineslist.update(). """ machines = app.maas.client.get_machines() if machines is None: return self.machine = next((m for m in machines if m.instance_id == self.machine.instance_id), None) def __repr__(self): return "widget for " + str(self.machine) def update(self): self.update_machine() current_pin = self.current_pin_cb(self.machine) if current_pin: l = self.unselect_button_label.format(current_pin) self.select_button.set_label(l) else: self.select_button.set_label(self.select_button_label) def handle_button(self, sender): if self.current_pin_cb(self.machine): self.unselect_cb(self.machine) else: self.select_cb(self.machine) self.update()
class ApplicationListView(WidgetWrap): def __init__(self, applications, metadata_controller, controller): self.controller = controller self.applications = applications assert(len(applications) > 0) self.metadata_controller = metadata_controller self.n_remaining = len(self.applications) self.widgets = self.build_widgets() super().__init__(self.widgets) self.pile.focus_position = 2 self.selected_app_w = None self.handle_focus_changed() self.update_skip_rest_button() def selectable(self): return True def keypress(self, size, key): # handle keypress first, then get new focus widget rv = super().keypress(size, key) self.handle_focus_changed() return rv def handle_focus_changed(self): "Check if focused widget changed, then update readme." fw = self.pile.focus if not isinstance(fw, ApplicationWidget): return if fw != self.selected_app_w: self.selected_app_w = fw if fw is None: self.description_w.set_text("No selected application") else: self.metadata_controller.get_readme( fw.application.csid.as_seriesname(), partial(self._handle_readme_load, fw)) def _handle_readme_load(self, fw, readme_f): if self.selected_app_w == fw: EventLoop.loop.event_loop._loop.call_soon_threadsafe( partial(self._update_readme_on_main_thread, readme_f.result())) def _update_readme_on_main_thread(self, readme): rt = self._trim_readme(readme) self.description_w.set_text(rt) def _trim_readme(self, readme): rls = readme.splitlines() rls = [l for l in rls if not l.startswith("#")] nrls = [] for i in range(len(rls)): if i+1 == len(rls): break if len(rls[i]) > 0: if rls[i][0] in ['-', '#', '=']: continue if len(rls[i+1]) > 0: if rls[i+1][0] in ['-', '=']: continue nrls.append(rls[i]) if len(nrls) == 0: return if nrls[0] == '': nrls = nrls[1:] # split after two paragraphs: if '' in nrls: firstparidx = nrls.index('') else: firstparidx = 1 try: splitidx = nrls.index('', firstparidx + 1) except: splitidx = firstparidx nrls = nrls[:splitidx] return "\n".join(nrls) def build_widgets(self): ws = [Text("{} Applications in {}:".format(len(self.applications), app.config['spell']))] max_app_name_len = max([len(a.service_name) for a in self.applications]) for a in self.applications: ws.append(Text("")) ws.append(ApplicationWidget(a, max_app_name_len, self.controller, self.do_deploy)) self.description_w = Text("App description") ws += [HR(), self.description_w] self.skip_rest_button = PlainButton( "Deploy all", self.controller.do_deploy_remaining ) cws = [('weight', 1, Text(" ")), (20, Color.button_secondary( self.skip_rest_button, focus_map='button_secondary focus'))] self.button_columns = Columns(cws, dividechars=1) ws += [HR(), self.button_columns] self.pile = Pile(ws) return Padding.center_90(Filler(self.pile, valign="top")) def do_deploy(self, application, sender): self.n_remaining -= 1 self.update_skip_rest_button() self.selected_app_w.remove_buttons() self.controller.do_deploy(application, msg_cb=self.selected_app_w.set_progress) if self.n_remaining > 0: # find next available app widget to highlight. Start after # the current one and wrap around to top next_w = None reordered = self.pile.contents[self.pile.focus_position:] + \ self.pile.contents[:self.pile.focus_position] for w, opts in reordered: if isinstance(w, ApplicationWidget) and w.selectable(): next_w = w break ni = [w for w, _ in self.pile.contents].index(next_w) self.pile.focus_position = ni else: self.controller.finish() def update_skip_rest_button(self): t = "Deploy all {} Remaining Applications".format( self.n_remaining) self.skip_rest_button.set_label(t)