feat: right-click menus in Files/Users/Artifacts; several SSH keys and accounts per host
- Files: right-click a sidebar workspace (Open, Copy path) or home (Open, Open Trash), a path crumb (Open, Download as zip, Paste into, Copy path), a Trash row (Restore, Copy path, Delete forever), the Trash area (Refresh, Empty Trash) and an admin's per-user Trash folder (Open, Empty Trash). - Users: row menu (Edit + Open home in Files for an admin, Copy username/email). - Artifacts: card menu repeating its buttons, plus Copy link for a page. - Git identity: named SSH keys beside the default ones (id_<type>_<name>, max 10, DELETE ?id=); SSH logins per alias (`ssh://<alias>/` → `ssh://<user>@<host>/`, so several AWS CodeCommit accounts share a host), each optionally tied to one key (`agenthub.<from>.sshKey`) — a clone of that URL offers only that key and persists it. Terminal sshCommand follows the keys. Settings → Git: "More SSH keys" + account name/key in the AWS and other-host forms. Docs/API.md, OpenAPI updated.
This commit is contained in:
@@ -13,14 +13,15 @@
|
||||
* tmux server beside the installed version, restartable by an admin
|
||||
* (`MemberTmuxCard`). Every change asks for the admin's OWN password again (the
|
||||
* server refuses without it); an analyst only reads. A row shows the display
|
||||
* name and email under the username when set.
|
||||
* name and email under the username when set; right-clicking a row offers
|
||||
* Edit, Open home (in /files), and copying the username or email.
|
||||
* Laid out like the other list pages: icon header, a row of totals
|
||||
* (`StatCard`), the accounts in a `card` table, the audit log in a card.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useState, type KeyboardEvent, type ReactNode } from "react";
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { Navigate, useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
AlertCircle,
|
||||
@@ -52,6 +53,8 @@ import { ROLE_BADGE, UserEditDialog } from "../components/users/UserEditDialog";
|
||||
import { AccountPoolCard } from "../components/users/AccountPoolCard";
|
||||
import { MemberTmuxCard } from "../components/users/MemberTmuxCard";
|
||||
import { TableRowSkeleton } from "../components/Skeleton";
|
||||
import { ContextMenu, MenuItem, MenuSeparator, useContextMenu } from "../components/ContextMenu";
|
||||
import { copyText } from "../lib/clipboard";
|
||||
|
||||
type Role = AuthUser["role"];
|
||||
const ROLES: Role[] = ["admin", "analyst", "developer"];
|
||||
@@ -67,6 +70,8 @@ export function Users() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [adding, setAdding] = useState(false);
|
||||
const [editing, setEditing] = useState<ServerUserUsage | null>(null);
|
||||
const rowMenu = useContextMenu<ServerUserUsage>();
|
||||
const navigate = useNavigate();
|
||||
const [storage, setStorage] = useState<Record<string, UserStorage> | null>(null);
|
||||
const [measuring, setMeasuring] = useState(false);
|
||||
|
||||
@@ -231,6 +236,7 @@ export function Users() {
|
||||
return (
|
||||
<tr
|
||||
key={u.id}
|
||||
onContextMenu={(e) => rowMenu.open(e, u)}
|
||||
className={`hover:bg-surface-4/40 transition-colors ${u.disabled ? "opacity-60" : ""}`}
|
||||
>
|
||||
<td className="px-5 py-3">
|
||||
@@ -298,6 +304,47 @@ export function Users() {
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{rowMenu.menu &&
|
||||
(() => {
|
||||
const u = rowMenu.menu.item;
|
||||
const pick = (fn: () => void) => () => {
|
||||
rowMenu.close();
|
||||
fn();
|
||||
};
|
||||
return (
|
||||
<ContextMenu
|
||||
x={rowMenu.menu.x}
|
||||
y={rowMenu.menu.y}
|
||||
onClose={rowMenu.close}
|
||||
testId="users-row-menu"
|
||||
>
|
||||
{isAdmin && (
|
||||
<MenuItem onSelect={pick(() => setEditing(u))}>{t("edit.button")}</MenuItem>
|
||||
)}
|
||||
{isAdmin && !u.disabled && (
|
||||
<MenuItem
|
||||
onSelect={pick(() =>
|
||||
navigate(
|
||||
`/files?home=${encodeURIComponent(u.id === me.id ? "me" : u.username)}`
|
||||
)
|
||||
)}
|
||||
>
|
||||
{t("rowMenu.openHome")}
|
||||
</MenuItem>
|
||||
)}
|
||||
{isAdmin && <MenuSeparator />}
|
||||
<MenuItem onSelect={pick(() => void copyText(u.username))}>
|
||||
{t("rowMenu.copyUsername")}
|
||||
</MenuItem>
|
||||
{u.email && (
|
||||
<MenuItem onSelect={pick(() => void copyText(u.email!))}>
|
||||
{t("rowMenu.copyEmail")}
|
||||
</MenuItem>
|
||||
)}
|
||||
</ContextMenu>
|
||||
);
|
||||
})()}
|
||||
|
||||
<AccountPoolCard isAdmin={isAdmin} />
|
||||
<MemberTmuxCard isAdmin={isAdmin} />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user