Skip to content

Commit b4016e2

Browse files
committed
[edit] consolidate key management screens into browse and create only
1 parent e20a88b commit b4016e2

6 files changed

Lines changed: 389 additions & 196 deletions

File tree

Lines changed: 386 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,386 @@
1+
using EtcdTerminal.Console.Engine;
2+
using EtcdTerminal.Console.Modules;
3+
using EtcdTerminal.Models;
4+
using Spectre.Console;
5+
6+
namespace EtcdTerminal.Console.Screens;
7+
8+
public sealed class KeyBrowseScreen(IEtcdClient _etcdClient)
9+
{
10+
private const int PageSize = 10;
11+
12+
private List<EtcdKeyValue> _allKeys = [];
13+
private List<EtcdKeyValue> _filteredKeys = [];
14+
private string _searchQuery = "";
15+
private int _currentPage;
16+
private int _selectedIndex;
17+
private bool _showActions;
18+
private EtcdKeyValue? _selectedKey;
19+
private EtcdConnectionConfig _config = default!;
20+
21+
public async Task ShowAsync(EtcdConnectionConfig config)
22+
{
23+
_config = config;
24+
_searchQuery = "";
25+
_currentPage = 0;
26+
_selectedIndex = 0;
27+
_showActions = false;
28+
_selectedKey = null;
29+
30+
await LoadKeysAsync();
31+
32+
await MainLoopAsync();
33+
}
34+
35+
private async Task LoadKeysAsync()
36+
{
37+
var authEnabled = await _etcdClient.IsAuthenticationEnabledAsync();
38+
39+
if (!authEnabled)
40+
{
41+
_allKeys = [.. await _etcdClient.GetKeysByPrefixAsync("")];
42+
}
43+
else
44+
{
45+
var username = _config.Username;
46+
47+
if (username is null)
48+
{
49+
_allKeys = [.. await _etcdClient.GetKeysByPrefixAsync("")];
50+
_filteredKeys = [.. _allKeys];
51+
52+
return;
53+
}
54+
55+
var user = await _etcdClient.GetUserAsync(username);
56+
57+
if (user is null || user.Roles.Count == 0)
58+
{
59+
_allKeys = [.. await _etcdClient.GetKeysByPrefixAsync("")];
60+
}
61+
else
62+
{
63+
var keys = new List<EtcdKeyValue>();
64+
65+
foreach (var roleName in user.Roles)
66+
{
67+
var role = await _etcdClient.GetRoleAsync(roleName);
68+
69+
if (role is null)
70+
continue;
71+
72+
foreach (var perm in role.Permissions)
73+
{
74+
if (perm.Type is not (PermissionType.Read or PermissionType.ReadWrite))
75+
continue;
76+
77+
var prefix = perm.KeyPrefix;
78+
79+
if (prefix == "\0")
80+
prefix = "";
81+
82+
var prefixKeys = await _etcdClient.GetKeysByPrefixAsync(prefix);
83+
keys.AddRange(prefixKeys);
84+
}
85+
}
86+
87+
_allKeys = [.. keys.DistinctBy(kv => kv.Key)];
88+
}
89+
}
90+
91+
_filteredKeys = [.. _allKeys];
92+
}
93+
94+
private async Task MainLoopAsync()
95+
{
96+
while (true)
97+
{
98+
Render();
99+
100+
var keyInfo = System.Console.ReadKey(true);
101+
102+
if (_showActions)
103+
{
104+
switch (keyInfo.Key)
105+
{
106+
case ConsoleKey.Escape:
107+
_showActions = false;
108+
_selectedKey = null;
109+
break;
110+
case ConsoleKey.E:
111+
await EditKeyAsync();
112+
_showActions = false;
113+
_selectedKey = null;
114+
break;
115+
case ConsoleKey.D:
116+
await DeleteKeyAsync();
117+
_showActions = false;
118+
_selectedKey = null;
119+
break;
120+
}
121+
122+
continue;
123+
}
124+
125+
switch (keyInfo.Key)
126+
{
127+
case ConsoleKey.UpArrow:
128+
if (_selectedIndex > 0)
129+
_selectedIndex--;
130+
break;
131+
case ConsoleKey.DownArrow:
132+
var pageItemCount = GetPageItemCount();
133+
134+
if (_selectedIndex < pageItemCount - 1)
135+
_selectedIndex++;
136+
break;
137+
case ConsoleKey.LeftArrow:
138+
if (_currentPage > 0)
139+
{
140+
_currentPage--;
141+
_selectedIndex = 0;
142+
}
143+
break;
144+
case ConsoleKey.RightArrow:
145+
if (_currentPage < GetTotalPages() - 1)
146+
{
147+
_currentPage++;
148+
_selectedIndex = 0;
149+
}
150+
break;
151+
case ConsoleKey.Enter:
152+
var selected = GetCurrentPageKey();
153+
154+
if (selected is not null)
155+
{
156+
_selectedKey = selected;
157+
_showActions = true;
158+
}
159+
break;
160+
case ConsoleKey.Backspace:
161+
if (_searchQuery.Length > 0)
162+
{
163+
_searchQuery = _searchQuery[..^1];
164+
ApplyFilter();
165+
}
166+
break;
167+
case ConsoleKey.Escape:
168+
return;
169+
default:
170+
var c = keyInfo.KeyChar;
171+
172+
if (c >= ' ' && c <= '~')
173+
{
174+
_searchQuery += c;
175+
ApplyFilter();
176+
}
177+
break;
178+
}
179+
}
180+
}
181+
182+
private void Render()
183+
{
184+
AnsiConsole.Clear();
185+
StatusBar.Render(_config);
186+
187+
System.Console.Write(" ");
188+
RenderSearchBar();
189+
190+
System.Console.WriteLine();
191+
RenderKeyList();
192+
193+
System.Console.WriteLine();
194+
RenderPagination();
195+
196+
RenderActionBar();
197+
}
198+
199+
private void RenderSearchBar()
200+
{
201+
if (_searchQuery.Length == 0)
202+
AnsiConsole.Markup("[grey]🔍 Type to search keys...[/]");
203+
else
204+
AnsiConsole.Markup($"[yellow]🔍[/] [white]{Markup.Escape(_searchQuery)}[/]");
205+
}
206+
207+
private void RenderKeyList()
208+
{
209+
var pageKeys = GetCurrentPageKeys();
210+
211+
if (_filteredKeys.Count == 0)
212+
{
213+
AnsiConsole.MarkupLine(" [grey]No keys found.[/]");
214+
215+
return;
216+
}
217+
218+
for (var i = 0; i < pageKeys.Count; i++)
219+
{
220+
var kv = pageKeys[i];
221+
var isSelected = i == _selectedIndex;
222+
223+
var prefix = isSelected ? " ▶ " : " ";
224+
var key = TruncateText(kv.Key, 60);
225+
var value = TruncateText(kv.Value, 60);
226+
var line = $"{prefix}{key,-62} {value}";
227+
228+
if (isSelected)
229+
AnsiConsole.MarkupLine($" [cyan]{Markup.Escape(line)}[/]");
230+
else
231+
AnsiConsole.MarkupLine($" [white]{Markup.Escape(line)}[/]");
232+
}
233+
}
234+
235+
private void RenderPagination()
236+
{
237+
var totalPages = GetTotalPages();
238+
var totalKeys = _filteredKeys.Count;
239+
var currentPageLabel = _currentPage + 1;
240+
241+
AnsiConsole.MarkupLine($" [grey]Page {currentPageLabel}/{totalPages} • {totalKeys} total keys[/]");
242+
}
243+
244+
private void RenderActionBar()
245+
{
246+
if (!_showActions || _selectedKey is null)
247+
{
248+
System.Console.WriteLine();
249+
250+
return;
251+
}
252+
253+
AnsiConsole.MarkupLine($" [grey]Selected:[/] [cyan]{Markup.Escape(_selectedKey.Key)}[/]");
254+
AnsiConsole.Markup(" ");
255+
AnsiConsole.Markup("[bold yellow][[E]][/] [white]Edit[/] ");
256+
AnsiConsole.Markup("[bold yellow][[D]][/] [white]Delete[/] ");
257+
AnsiConsole.Markup("[bold yellow][[Esc]][/] [white]Cancel[/]");
258+
System.Console.WriteLine();
259+
}
260+
261+
private async Task EditKeyAsync()
262+
{
263+
if (_selectedKey is null)
264+
return;
265+
266+
AnsiConsole.Clear();
267+
StatusBar.Render(_config);
268+
AnsiConsole.MarkupLine($"Editing key: [cyan]{Markup.Escape(_selectedKey.Key)}[/]");
269+
AnsiConsole.MarkupLine($"Current value: [green]{Markup.Escape(TruncateText(_selectedKey.Value, 200))}[/]");
270+
System.Console.WriteLine();
271+
272+
var newValue = Prompt.Ask("Enter new value:", _selectedKey.Value);
273+
274+
if (newValue is null)
275+
return;
276+
277+
var result = await _etcdClient.UpdateKeyAsync(_selectedKey.Key, newValue);
278+
279+
AnsiConsole.Clear();
280+
StatusBar.Render(_config);
281+
282+
if (result)
283+
{
284+
AnsiConsole.MarkupLine("[green]Key updated successfully![/]");
285+
await ReloadAsync();
286+
}
287+
else
288+
{
289+
AnsiConsole.MarkupLine("[red]Could not update key.[/]");
290+
}
291+
292+
System.Console.WriteLine();
293+
AnsiConsole.Markup("[grey]Press any key to continue...[/]");
294+
System.Console.ReadKey(true);
295+
}
296+
297+
private async Task DeleteKeyAsync()
298+
{
299+
if (_selectedKey is null)
300+
return;
301+
302+
AnsiConsole.Clear();
303+
StatusBar.Render(_config);
304+
AnsiConsole.MarkupLine($"Delete key: [red]{Markup.Escape(_selectedKey.Key)}[/]");
305+
System.Console.WriteLine();
306+
307+
var confirm = Prompt.Confirm("Are you sure?");
308+
309+
if (confirm is not true)
310+
return;
311+
312+
var result = await _etcdClient.DeleteKeyAsync(_selectedKey.Key);
313+
314+
AnsiConsole.Clear();
315+
StatusBar.Render(_config);
316+
317+
if (result)
318+
{
319+
AnsiConsole.MarkupLine("[green]Key deleted successfully![/]");
320+
await ReloadAsync();
321+
}
322+
else
323+
{
324+
AnsiConsole.MarkupLine("[red]Key could not be deleted.[/]");
325+
}
326+
327+
System.Console.WriteLine();
328+
AnsiConsole.Markup("[grey]Press any key to continue...[/]");
329+
System.Console.ReadKey(true);
330+
}
331+
332+
private async Task ReloadAsync()
333+
{
334+
await LoadKeysAsync();
335+
336+
_currentPage = Math.Min(_currentPage, GetTotalPages() - 1);
337+
338+
if (_currentPage < 0)
339+
_currentPage = 0;
340+
341+
_selectedIndex = 0;
342+
}
343+
344+
private void ApplyFilter()
345+
{
346+
if (string.IsNullOrEmpty(_searchQuery))
347+
{
348+
_filteredKeys = [.. _allKeys];
349+
}
350+
else
351+
{
352+
var query = _searchQuery;
353+
354+
_filteredKeys = [.. _allKeys
355+
.Where(kv =>
356+
kv.Key.Contains(query, StringComparison.OrdinalIgnoreCase) ||
357+
kv.Value.Contains(query, StringComparison.OrdinalIgnoreCase))];
358+
}
359+
360+
_currentPage = 0;
361+
_selectedIndex = 0;
362+
}
363+
364+
private List<EtcdKeyValue> GetCurrentPageKeys()
365+
{
366+
var start = _currentPage * PageSize;
367+
368+
return _filteredKeys.Skip(start).Take(PageSize).ToList();
369+
}
370+
371+
private EtcdKeyValue? GetCurrentPageKey()
372+
{
373+
var pageKeys = GetCurrentPageKeys();
374+
375+
return _selectedIndex < pageKeys.Count ? pageKeys[_selectedIndex] : null;
376+
}
377+
378+
private int GetPageItemCount() =>
379+
Math.Min(PageSize, _filteredKeys.Count - _currentPage * PageSize);
380+
381+
private int GetTotalPages() =>
382+
_filteredKeys.Count == 0 ? 1 : (int)Math.Ceiling((double)_filteredKeys.Count / PageSize);
383+
384+
private static string TruncateText(string text, int maxLength) =>
385+
text.Length <= maxLength ? text : text[..maxLength] + "...";
386+
}

0 commit comments

Comments
 (0)