track requesters for builds releases deployments and migrations
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -55,9 +55,9 @@ export function AdminManagementPage() {
|
||||
const [sshTarget, setSshTarget] = useState({
|
||||
admin: null as AdminAccount | null,
|
||||
label: '',
|
||||
giteaUsername: '',
|
||||
publicKey: ''
|
||||
});
|
||||
const [sshDeleteTarget, setSshDeleteTarget] = useState<{ admin: AdminAccount; keyId: string } | null>(null);
|
||||
const [adminForm, setAdminForm] = useState(emptyAdminForm);
|
||||
const [saveState, setSaveState] = useState<'idle' | 'saving' | 'error'>('idle');
|
||||
const [passwordState, setPasswordState] = useState<'idle' | 'saving' | 'error'>('idle');
|
||||
@@ -119,7 +119,7 @@ export function AdminManagementPage() {
|
||||
setMenuAdmin(null);
|
||||
const account = admin.email.split('@')[0] || 'admin';
|
||||
const random = Math.random().toString(36).slice(2, 8);
|
||||
setSshTarget({ admin, label: `${account}-${random}-key`, giteaUsername: '', publicKey: '' });
|
||||
setSshTarget({ admin, label: `${account}-${random}-key`, publicKey: '' });
|
||||
setSshState('idle');
|
||||
setSshError('');
|
||||
};
|
||||
@@ -207,12 +207,11 @@ export function AdminManagementPage() {
|
||||
setSshError('');
|
||||
createAdminSshKey(sshTarget.admin.id, {
|
||||
label: sshTarget.label.trim() || undefined,
|
||||
giteaUsername: sshTarget.giteaUsername.trim() || undefined,
|
||||
publicKey: sshTarget.publicKey.trim()
|
||||
})
|
||||
.then(() => {
|
||||
setSshState('idle');
|
||||
setSshTarget({ admin: null, label: '', giteaUsername: '', publicKey: '' });
|
||||
setSshTarget({ admin: null, label: '', publicKey: '' });
|
||||
toast({ type: 'success', title: 'SSH 키 등록 완료', message: '관리자 SSH 키가 등록되었습니다.' });
|
||||
refresh();
|
||||
})
|
||||
@@ -223,9 +222,12 @@ export function AdminManagementPage() {
|
||||
});
|
||||
};
|
||||
|
||||
const removeSshKey = (admin: AdminAccount, keyId: string) => {
|
||||
deleteAdminSshKey(admin.id, keyId)
|
||||
const removeSshKey = () => {
|
||||
if (!sshDeleteTarget) return;
|
||||
|
||||
deleteAdminSshKey(sshDeleteTarget.admin.id, sshDeleteTarget.keyId)
|
||||
.then(() => {
|
||||
setSshDeleteTarget(null);
|
||||
toast({ type: 'success', title: 'SSH 키 삭제 완료', message: '관리자 SSH 키가 삭제되었습니다.' });
|
||||
refresh();
|
||||
})
|
||||
@@ -240,9 +242,12 @@ export function AdminManagementPage() {
|
||||
header: '관리자',
|
||||
className: 'w-[260px] px-5 py-3 font-medium',
|
||||
cell: (row) => (
|
||||
<div>
|
||||
<p className="font-semibold">{row.name}</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">{row.email}</p>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<AdminAvatar admin={row} />
|
||||
<div className="min-w-0">
|
||||
<p className="truncate font-semibold">{row.name}</p>
|
||||
<p className="mt-1 truncate text-xs text-muted-foreground">{row.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
@@ -278,7 +283,7 @@ export function AdminManagementPage() {
|
||||
className="inline-flex rounded-full outline-none focus:ring-2 focus:ring-ring"
|
||||
onClick={() => openSshModal(row)}
|
||||
>
|
||||
<StatusBadge status={`${keyCount}개`} tone={keyCount > 0 ? 'green' : 'neutral'} />
|
||||
<StatusBadge status={keyCount > 0 ? '등록' : '미등록'} tone={keyCount > 0 ? 'green' : 'neutral'} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -355,13 +360,26 @@ export function AdminManagementPage() {
|
||||
if (!row) return null;
|
||||
return (
|
||||
<div onClick={(event) => event.stopPropagation()} className="fixed z-[70] min-w-36 overflow-hidden rounded-md border border-border bg-card py-1 text-left shadow-xl" style={{ left: menuAdmin.x, top: menuAdmin.y, transform: 'translateX(-100%)' }}>
|
||||
<button
|
||||
type="button"
|
||||
className="block w-full px-3 py-2 text-left text-xs text-foreground hover:bg-muted"
|
||||
onClick={() => openSshModal(row)}
|
||||
>
|
||||
SSH 키 등록
|
||||
</button>
|
||||
{row.sshKeys?.length ? (
|
||||
<button
|
||||
type="button"
|
||||
className="block w-full px-3 py-2 text-left text-xs text-rose-300 hover:bg-muted"
|
||||
onClick={() => {
|
||||
setMenuAdmin(null);
|
||||
setSshDeleteTarget({ admin: row, keyId: row.sshKeys![0].id });
|
||||
}}
|
||||
>
|
||||
SSH 키 삭제
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="block w-full px-3 py-2 text-left text-xs text-foreground hover:bg-muted"
|
||||
onClick={() => openSshModal(row)}
|
||||
>
|
||||
SSH 키 등록
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="block w-full px-3 py-2 text-left text-xs text-foreground hover:bg-muted"
|
||||
@@ -451,11 +469,11 @@ export function AdminManagementPage() {
|
||||
<Modal
|
||||
open={Boolean(sshTarget.admin)}
|
||||
title="SSH 키 등록"
|
||||
onClose={() => setSshTarget({ admin: null, label: '', giteaUsername: '', publicKey: '' })}
|
||||
onClose={() => setSshTarget({ admin: null, label: '', publicKey: '' })}
|
||||
className="max-w-2xl"
|
||||
footer={
|
||||
<>
|
||||
<Button variant="outline" onClick={() => setSshTarget({ admin: null, label: '', giteaUsername: '', publicKey: '' })}>취소</Button>
|
||||
<Button variant="outline" onClick={() => setSshTarget({ admin: null, label: '', publicKey: '' })}>취소</Button>
|
||||
<Button onClick={submitSshKey} disabled={sshState === 'saving'}>{sshState === 'saving' ? '저장 중' : '등록'}</Button>
|
||||
</>
|
||||
}
|
||||
@@ -465,10 +483,9 @@ export function AdminManagementPage() {
|
||||
<span className="text-xs font-medium text-muted-foreground">키 이름</span>
|
||||
<input className="mt-2 h-9 w-full rounded-md border border-input bg-background px-3 text-sm text-foreground outline-none focus:ring-2 focus:ring-ring" value={sshTarget.label} onChange={(event) => setSshTarget((prev) => ({ ...prev, label: event.target.value }))} />
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="text-xs font-medium text-muted-foreground">저장소 사용자명</span>
|
||||
<input className="mt-2 h-9 w-full rounded-md border border-input bg-background px-3 text-sm text-foreground outline-none focus:ring-2 focus:ring-ring" value={sshTarget.giteaUsername} onChange={(event) => setSshTarget((prev) => ({ ...prev, giteaUsername: event.target.value }))} />
|
||||
</label>
|
||||
<p className="rounded-md border border-border bg-muted/20 px-3 py-2 text-xs text-muted-foreground">
|
||||
저장소 사용자명은 관리자 이메일 기준으로 자동 적용됩니다.
|
||||
</p>
|
||||
<label className="block">
|
||||
<span className="text-xs font-medium text-muted-foreground">공개키</span>
|
||||
<textarea className="mt-2 min-h-32 w-full rounded-md border border-input bg-background px-3 py-2 font-mono text-sm text-foreground outline-none focus:ring-2 focus:ring-ring" value={sshTarget.publicKey} onChange={(event) => setSshTarget((prev) => ({ ...prev, publicKey: event.target.value }))} />
|
||||
@@ -482,7 +499,7 @@ export function AdminManagementPage() {
|
||||
<p className="truncate text-sm font-semibold">{key.label ?? 'SSH Key'}</p>
|
||||
<p className="truncate font-mono text-xs text-muted-foreground">{key.fingerprint}</p>
|
||||
</div>
|
||||
<Button variant="outline" className="size-8 px-0" title="삭제" aria-label="삭제" onClick={() => sshTarget.admin && removeSshKey(sshTarget.admin, key.id)}>
|
||||
<Button variant="outline" className="size-8 px-0" title="삭제" aria-label="삭제" onClick={() => sshTarget.admin && setSshDeleteTarget({ admin: sshTarget.admin, keyId: key.id })}>
|
||||
<Trash2 className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -492,6 +509,20 @@ export function AdminManagementPage() {
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
open={Boolean(sshDeleteTarget)}
|
||||
title="SSH 키 삭제"
|
||||
onClose={() => setSshDeleteTarget(null)}
|
||||
footer={
|
||||
<>
|
||||
<Button variant="outline" onClick={() => setSshDeleteTarget(null)}>취소</Button>
|
||||
<Button className="border border-rose-500/40 bg-rose-500/15 text-rose-200 hover:bg-rose-500/25" onClick={removeSshKey}>삭제</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<p className="text-sm text-muted-foreground">등록된 관리자 SSH 키를 삭제합니다. Gitea에 등록된 키도 함께 삭제됩니다.</p>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
open={Boolean(permissionTarget.admin)}
|
||||
title="앱 권한"
|
||||
@@ -547,3 +578,25 @@ export function AdminManagementPage() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function AdminAvatar({ admin }: { admin: AdminAccount }) {
|
||||
const [failed, setFailed] = useState(false);
|
||||
const initial = (admin.name || admin.email || '?').trim().charAt(0).toUpperCase() || '?';
|
||||
|
||||
if (admin.avatarUrl && !failed) {
|
||||
return (
|
||||
<img
|
||||
src={admin.avatarUrl}
|
||||
alt=""
|
||||
className="size-9 shrink-0 rounded-full border border-border bg-muted object-cover"
|
||||
onError={() => setFailed(true)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="inline-flex size-9 shrink-0 items-center justify-center rounded-full border border-border bg-muted text-sm font-semibold text-muted-foreground">
|
||||
{initial}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user