Skip to content

Commit 0af1fc8

Browse files
committed
feat: Add account filtering in aws configure sso
Signed-off-by: frauniki <[email protected]>
1 parent 9f13a1d commit 0af1fc8

File tree

4 files changed

+686
-8
lines changed

4 files changed

+686
-8
lines changed

awscli/customizations/configure/sso.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -439,10 +439,12 @@ def _handle_single_account(self, accounts):
439439
def _handle_multiple_accounts(self, accounts):
440440
available_accounts_msg = (
441441
'There are {} AWS accounts available to you.\n'
442+
'Use arrow keys to navigate, type to filter, and press Enter to select.\n'
442443
)
443444
uni_print(available_accounts_msg.format(len(accounts)))
444445
selected_account = self._selector(
445-
accounts, display_format=display_account
446+
accounts, display_format=display_account, enable_filter=True,
447+
no_results_message='No matching accounts found'
446448
)
447449
sso_account_id = selected_account['accountId']
448450
return sso_account_id
@@ -456,6 +458,8 @@ def _prompt_for_account(self, sso, sso_token):
456458
accounts = self._get_all_accounts(sso, sso_token)['accountList']
457459
if not accounts:
458460
raise RuntimeError('No AWS accounts are available to you.')
461+
# Sort accounts by accountName for consistent ordering
462+
accounts = sorted(accounts, key=lambda x: x.get('accountName', ''))
459463
if len(accounts) == 1:
460464
sso_account_id = self._handle_single_account(accounts)
461465
else:
@@ -489,6 +493,8 @@ def _prompt_for_role(self, sso, sso_token, sso_account_id):
489493
if not roles:
490494
error_msg = 'No roles are available for the account {}'
491495
raise RuntimeError(error_msg.format(sso_account_id))
496+
# Sort roles by roleName for consistent ordering
497+
roles = sorted(roles, key=lambda x: x.get('roleName', ''))
492498
if len(roles) == 1:
493499
sso_role_name = self._handle_single_role(roles)
494500
else:

awscli/customizations/wizard/ui/selectmenu.py

Lines changed: 176 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@
2323
from prompt_toolkit.utils import get_cwidth
2424

2525

26-
def select_menu(items, display_format=None, max_height=10):
26+
def select_menu(
27+
items, display_format=None, max_height=10, enable_filter=False,
28+
no_results_message=None
29+
):
2730
"""Presents a list of options and allows the user to select one.
2831
2932
This presents a static list of options and prompts the user to select one.
@@ -42,6 +45,12 @@ def select_menu(items, display_format=None, max_height=10):
4245
:type max_height: int
4346
:param max_height: The max number of items to show in the list at a time.
4447
48+
:type enable_filter: bool
49+
:param enable_filter: Enable keyboard filtering of items.
50+
51+
:type no_results_message: str
52+
:param no_results_message: Message to show when filtering returns no results.
53+
4554
:returns: The selected element from the items list.
4655
"""
4756
app_bindings = KeyBindings()
@@ -51,8 +60,20 @@ def exit_app(event):
5160
event.app.exit(exception=KeyboardInterrupt, style='class:aborting')
5261

5362
min_height = min(max_height, len(items))
63+
if enable_filter:
64+
# Add 1 to height for filter line
65+
min_height = min(max_height + 1, len(items) + 1)
66+
menu_control = FilterableSelectionMenuControl(
67+
items, display_format=display_format,
68+
no_results_message=no_results_message
69+
)
70+
else:
71+
menu_control = SelectionMenuControl(
72+
items, display_format=display_format
73+
)
74+
5475
menu_window = Window(
55-
SelectionMenuControl(items, display_format=display_format),
76+
menu_control,
5677
always_hide_cursor=False,
5778
height=Dimension(min=min_height, max=min_height),
5879
scroll_offsets=ScrollOffsets(),
@@ -122,6 +143,8 @@ def is_focusable(self):
122143

123144
def preferred_width(self, max_width):
124145
items = self._get_items()
146+
if not items:
147+
return self.MIN_WIDTH
125148
if self._display_format:
126149
items = (self._display_format(i) for i in items)
127150
max_item_width = max(get_cwidth(i) for i in items)
@@ -188,6 +211,157 @@ def app_result(event):
188211
return kb
189212

190213

214+
class FilterableSelectionMenuControl(SelectionMenuControl):
215+
"""Menu that supports keyboard filtering of items"""
216+
217+
def __init__(self, items, display_format=None, cursor='>', no_results_message=None):
218+
super().__init__(items, display_format=display_format, cursor=cursor)
219+
self._filter_text = ''
220+
self._filtered_items = items if items else []
221+
self._all_items = items if items else []
222+
self._filter_enabled = True
223+
self._no_results_message = no_results_message or 'No matching items found'
224+
225+
def _get_items(self):
226+
if callable(self._all_items):
227+
self._all_items = self._all_items()
228+
return self._filtered_items
229+
230+
def preferred_width(self, max_width):
231+
# Ensure minimum width for search display
232+
min_search_width = max(20, len("Search: " + self._filter_text) + 5)
233+
234+
# Get width from filtered items
235+
items = self._filtered_items
236+
if not items:
237+
# Width for no results message
238+
no_results_width = get_cwidth(self._no_results_message) + 4
239+
return max(no_results_width, min_search_width)
240+
241+
if self._display_format:
242+
items_display = [self._display_format(i) for i in items]
243+
else:
244+
items_display = [str(i) for i in items]
245+
246+
if items_display:
247+
max_item_width = max(get_cwidth(i) for i in items_display)
248+
max_item_width += self._format_overhead
249+
else:
250+
max_item_width = self.MIN_WIDTH
251+
252+
max_item_width = max(max_item_width, min_search_width)
253+
254+
if max_item_width < self.MIN_WIDTH:
255+
max_item_width = self.MIN_WIDTH
256+
return min(max_width, max_item_width)
257+
258+
def _update_filtered_items(self):
259+
"""Update the filtered items based on the current filter text"""
260+
if not self._filter_text:
261+
self._filtered_items = self._all_items
262+
else:
263+
filter_lower = self._filter_text.lower()
264+
self._filtered_items = [
265+
item
266+
for item in self._all_items
267+
if filter_lower
268+
in (
269+
self._display_format(item)
270+
if self._display_format
271+
else str(item)
272+
).lower()
273+
]
274+
275+
# Reset selection if it's out of bounds
276+
if self._selection >= len(self._filtered_items):
277+
self._selection = 0
278+
279+
def preferred_height(self, width, max_height, wrap_lines, get_line_prefix):
280+
# Add 1 extra line for the filter display
281+
return min(max_height, len(self._get_items()) + 1)
282+
283+
def create_content(self, width, height):
284+
def get_line(i):
285+
# First line shows the filter
286+
if i == 0:
287+
filter_display = (
288+
f"Search: {self._filter_text}_"
289+
if self._filter_enabled
290+
else f"Search: {self._filter_text}"
291+
)
292+
return [('class:filter', filter_display)]
293+
294+
# Show "No results" message if filtered items is empty
295+
if not self._filtered_items:
296+
if i == 1:
297+
return [
298+
('class:no-results', f' {self._no_results_message}')
299+
]
300+
return [('', '')]
301+
302+
# Adjust for the filter line
303+
item_index = i - 1
304+
if item_index >= len(self._filtered_items):
305+
return [('', '')]
306+
307+
item = self._filtered_items[item_index]
308+
is_selected = item_index == self._selection
309+
return self._menu_item_fragment(item, is_selected, width)
310+
311+
# Ensure at least 2 lines (search + no results or items)
312+
line_count = max(2, len(self._filtered_items) + 1)
313+
cursor_y = self._selection + 1 if self._filtered_items else 0
314+
315+
return UIContent(
316+
get_line=get_line,
317+
cursor_position=Point(x=0, y=cursor_y),
318+
line_count=line_count,
319+
)
320+
321+
def get_key_bindings(self):
322+
kb = KeyBindings()
323+
324+
@kb.add('up')
325+
def move_up(event):
326+
if len(self._filtered_items) > 0:
327+
self._move_cursor(-1)
328+
329+
@kb.add('down')
330+
def move_down(event):
331+
if len(self._filtered_items) > 0:
332+
self._move_cursor(1)
333+
334+
@kb.add('enter')
335+
def app_result(event):
336+
if len(self._filtered_items) > 0:
337+
result = self._filtered_items[self._selection]
338+
event.app.exit(result=result)
339+
340+
@kb.add('backspace')
341+
def delete_char(event):
342+
if self._filter_text:
343+
self._filter_text = self._filter_text[:-1]
344+
self._update_filtered_items()
345+
346+
@kb.add('c-u')
347+
def clear_filter(event):
348+
self._filter_text = ''
349+
self._update_filtered_items()
350+
351+
# Add support for typing any character
352+
from string import printable
353+
354+
for char in printable:
355+
if char not in ('\n', '\r', '\t'):
356+
357+
@kb.add(char)
358+
def add_char(event, c=char):
359+
self._filter_text += c
360+
self._update_filtered_items()
361+
362+
return kb
363+
364+
191365
class CollapsableSelectionMenuControl(SelectionMenuControl):
192366
"""Menu that collapses to text with selection when loses focus"""
193367

0 commit comments

Comments
 (0)