╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ────────────╮ │src/ui/Repo/Roles.zig│ ╰─────────────────────╯ ╭───────────────────────────────────────────────────────────────────╮ │356 return self.scroll.getFocus(); │ │357 } │ │358 │ │311 - // up leaves the view from the add row │ │359 + // up leaves the view from the add row or the confirmation│ │360 pub fn atTop(self: *View) bool { │ │313 - return self.addActive(); │ │361 + return self.data.confirm != null or self.addActive(); │ │362 } │ │363 }; │ │364 │ ╰───────────────────────────────────────────────────────────────────╯ ╭ changed ──────────╮ │src/ui/UserKeys.zig│ ╰───────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │15 │ │16 // the logged-in user's ssh keys, one authorized_keys line each │ │17 keys: []const []const u8 = &.{}, │ │18 +// the key whose removal the route asks to confirm, which stands in for the list│ │19 +confirm: ?[]const u8 = null, │ │20 │ │21 const Self = @This(); │ │22 │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │21 -pub fn init(arena: *std.heap.ArenaAllocator, session: *ui.Session, user_id: *const [evt.event_id_size]u8) !Self { │ │23 +pub fn init(arena: *std.heap.ArenaAllocator, session: *ui.Session, user_id: *const [evt.event_id_size]u8, tab: ui.RoutablePage.UserTab) !Self {│ │24 const moment = session.haxy_moment orelse return .{}; │ │25 const user = (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, arena, user_id)) orelse return .{}; │ │26 + var result: Self = .{}; │ │27 var keys: std.ArrayList([]const u8) = .empty; │ │28 var lines = std.mem.splitScalar(u8, user.event.ssh_keys, '\n'); │ │29 while (lines.next()) |line| { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │27 - if (evt.User.fingerprintOfKey(line) == null) continue; │ │30 + const fingerprint = evt.User.fingerprintOfKey(line) orelse continue; │ │31 try keys.append(arena.allocator(), line); │ │32 + if (tab.remove_key and std.mem.eql(u8, &fingerprint, tab.key.slice())) result.confirm = line; │ │33 } │ │30 - return .{ .keys = keys.items }; │ │34 + result.keys = keys.items; │ │35 + return result; │ │36 +} │ │37 + │ │38 +// the confirmation of removing `key`, under the user tab `route` │ │39 +fn removeRoute(route: ui.RoutablePage, key: []const u8) ui.RoutablePage { │ │40 + const fingerprint = evt.User.fingerprintOfKey(key) orelse unreachable; │ │41 + const fingerprint_array = ui.RoutablePage.Array(evt.User.fingerprint_len).from(&fingerprint) orelse unreachable;│ │42 + return route.withUserTab(.{ .view = .keys, .key = fingerprint_array, .remove_key = true }); │ │43 } │ │44 │ │45 // a key's comment, or its type when it has none │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │63 │ │64 // `route` is the ssh keys route, which the forms post under │ │65 pub fn init(allocator: std.mem.Allocator, data: *const Self, session: *ui.Session, route: ui.RoutablePage) !View { │ │66 + const aa = session.page_arena.allocator(); │ │67 + if (data.confirm) |key| { │ │68 + const remove_url = try removeRoute(route, key).toUrl(session.page_arena); │ │69 + var center = try ui.widget.initConfirmationForm(allocator, try aa.print("form:{s}", .{remove_url}), &.{ │ │70 + .{ .label = "remove ssh key", .bottom_label = try aa.print(" {s} ", .{keyLabel(key)}) }, │ │71 + }); │ │72 + errdefer center.deinit(allocator); │ │73 + return .{ │ │74 + .scroll = try wgt.Scroll(ui.Widget).init(allocator, .{ .center = center }, .{ .direction = .vert, .web_native = !session.is_terminal, .fill = true }),│ │75 + .data = data, │ │76 + .session = session, │ │77 + }; │ │78 + } │ │79 + │ │80 var box = try wgt.Box(ui.Widget).init(allocator, .{ .border = null, .direction = .vert }); │ │81 errdefer box.deinit(allocator); │ │56 - const aa = session.page_arena.allocator(); │ │82 const url = try route.toUrl(session.page_arena); │ │83 │ │84 { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 94 try box.children.put(allocator, add.getFocus().id, .{ .widget = .{ .box = add }, .rect = null, .min_size = .{ .width = null, .height = 3 } });│ │ 95 } │ │ 96 │ │ 72 - // one row per key, whose form posts the key to remove │ │ 97 + // one row per key, whose ✕ asks to confirm its removal │ │ 98 for (data.keys) |key| { │ │ 99 var row = try wgt.Box(ui.Widget).init(allocator, .{ .border = null, .direction = .horiz }); │ │100 errdefer row.deinit(allocator); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 76 - row.getFocus().kind = .{ .custom = try aa.print("form:{s}/removekey", .{url}) };│ │101 { │ │102 var text_input = try wgt.TextInput.init(allocator, .{ │ │103 .top_label = .{ .text = try aa.print(" {s} ", .{keyLabel(key)}) }, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────╮ │ 80 - .name = "key", │ │104 .visible_width = input_width,│ │105 .round_corners = true, │ │106 .read_only = true, │ ╰──────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │117 var remove = try wgt.TextBox.init(allocator, "✕", .{ .border = .single, .round_corners = true, .wrap_kind = .none }); │ │118 errdefer remove.deinit(allocator); │ │119 remove.getFocus().mode = .all; │ │ 97 - remove.getFocus().kind = .{ .custom = "submit" }; │ │120 + remove.getFocus().kind = .{ .custom = try aa.print("a:{s}", .{try removeRoute(route, key).toUrl(session.page_arena)}) }; │ │121 try row.children.put(allocator, remove.getFocus().id, .{ .widget = .{ .text_box = remove }, .rect = null, .min_size = .{ .width = 3, .height = null } });│ │122 } │ │123 row.getFocus().child_id = row.children.keys()[0]; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │156 │ │157 pub fn build(self: *View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { │ │158 self.clearGrid(); │ │159 + if (self.data.confirm == null) try self.labelInputs(root_focus); │ │160 + // the window's height as a minimum centers rows that fit │ │161 + try self.scroll.build(allocator, .{ │ │162 + .min_size = .{ .width = constraint.min_size.width, .height = constraint.max_size.height orelse constraint.min_size.height },│ │163 + .max_size = constraint.max_size, │ │164 + }, root_focus); │ │165 + } │ │166 + │ │167 + // label the add input for this frame and register every input for the │ │168 + // web form handling │ │169 + fn labelInputs(self: *View, root_focus: *Focus) !void { │ │170 const add = self.addInput(); │ │171 const failure = if (self.session.formFeedback(.user_keys)) |saved| saved.failure else null; │ │172 add.options.top_label.text = if (failure) |value| switch (value) { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯