Start Magent beta overhaul
This commit is contained in:
@@ -0,0 +1,696 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { authFetch, clearToken, getApiBase, getToken } from '../../lib/auth'
|
||||
import AdminShell from '../../ui/AdminShell'
|
||||
|
||||
type UserStats = {
|
||||
total: number
|
||||
ready: number
|
||||
pending: number
|
||||
approved: number
|
||||
working: number
|
||||
partial: number
|
||||
declined: number
|
||||
in_progress: number
|
||||
last_request_at?: string | null
|
||||
}
|
||||
|
||||
type AdminUser = {
|
||||
id?: number
|
||||
username: string
|
||||
email?: string | null
|
||||
role: string
|
||||
auth_provider?: string | null
|
||||
last_login_at?: string | null
|
||||
is_blocked?: boolean
|
||||
auto_search_enabled?: boolean
|
||||
invite_management_enabled?: boolean
|
||||
jellyseerr_user_id?: number | null
|
||||
profile_id?: number | null
|
||||
expires_at?: string | null
|
||||
is_expired?: boolean
|
||||
invited_by_code?: string | null
|
||||
invited_at?: string | null
|
||||
}
|
||||
|
||||
type UserLineage = {
|
||||
invite_code?: string | null
|
||||
invited_by?: string | null
|
||||
invite?: {
|
||||
id?: number
|
||||
code?: string
|
||||
label?: string | null
|
||||
created_by?: string | null
|
||||
created_at?: string | null
|
||||
enabled?: boolean
|
||||
is_usable?: boolean
|
||||
} | null
|
||||
} | null
|
||||
|
||||
type UserProfileOption = {
|
||||
id: number
|
||||
name: string
|
||||
is_active?: boolean
|
||||
}
|
||||
|
||||
const formatDateTime = (value?: string | null) => {
|
||||
if (!value) return 'Never'
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.valueOf())) return value
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
const toLocalDateTimeInput = (value?: string | null) => {
|
||||
if (!value) return ''
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.valueOf())) return ''
|
||||
const offsetMs = date.getTimezoneOffset() * 60_000
|
||||
const local = new Date(date.getTime() - offsetMs)
|
||||
return local.toISOString().slice(0, 16)
|
||||
}
|
||||
|
||||
const fromLocalDateTimeInput = (value: string) => {
|
||||
if (!value.trim()) return null
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.valueOf())) return null
|
||||
return date.toISOString()
|
||||
}
|
||||
|
||||
const normalizeStats = (stats: any): UserStats => ({
|
||||
total: Number(stats?.total ?? 0),
|
||||
ready: Number(stats?.ready ?? 0),
|
||||
pending: Number(stats?.pending ?? 0),
|
||||
approved: Number(stats?.approved ?? 0),
|
||||
working: Number(stats?.working ?? 0),
|
||||
partial: Number(stats?.partial ?? 0),
|
||||
declined: Number(stats?.declined ?? 0),
|
||||
in_progress: Number(stats?.in_progress ?? 0),
|
||||
last_request_at: stats?.last_request_at ?? null,
|
||||
})
|
||||
|
||||
export default function UserDetailPage() {
|
||||
const params = useParams()
|
||||
const router = useRouter()
|
||||
const idParam = Array.isArray(params?.id) ? params.id[0] : params?.id
|
||||
const [user, setUser] = useState<AdminUser | null>(null)
|
||||
const [stats, setStats] = useState<UserStats | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [profiles, setProfiles] = useState<UserProfileOption[]>([])
|
||||
const [profileSelection, setProfileSelection] = useState('')
|
||||
const [expiryInput, setExpiryInput] = useState('')
|
||||
const [savingProfile, setSavingProfile] = useState(false)
|
||||
const [savingExpiry, setSavingExpiry] = useState(false)
|
||||
const [systemActionBusy, setSystemActionBusy] = useState(false)
|
||||
const [actionStatus, setActionStatus] = useState<string | null>(null)
|
||||
const [lineage, setLineage] = useState<UserLineage>(null)
|
||||
|
||||
const loadProfiles = async () => {
|
||||
try {
|
||||
const baseUrl = getApiBase()
|
||||
const response = await authFetch(`${baseUrl}/admin/profiles`)
|
||||
if (!response.ok) {
|
||||
return
|
||||
}
|
||||
const data = await response.json()
|
||||
if (!Array.isArray(data?.profiles)) {
|
||||
setProfiles([])
|
||||
return
|
||||
}
|
||||
setProfiles(
|
||||
data.profiles.map((profile: any) => ({
|
||||
id: Number(profile.id ?? 0),
|
||||
name: String(profile.name ?? 'Unnamed profile'),
|
||||
is_active: Boolean(profile.is_active ?? true),
|
||||
}))
|
||||
)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
const loadUser = async () => {
|
||||
if (!idParam) return
|
||||
try {
|
||||
const baseUrl = getApiBase()
|
||||
const response = await authFetch(
|
||||
`${baseUrl}/admin/users/id/${encodeURIComponent(idParam)}`
|
||||
)
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
clearToken()
|
||||
router.push('/login')
|
||||
return
|
||||
}
|
||||
if (response.status === 403) {
|
||||
router.push('/')
|
||||
return
|
||||
}
|
||||
if (response.status === 404) {
|
||||
setError('User not found.')
|
||||
return
|
||||
}
|
||||
throw new Error('Could not load user.')
|
||||
}
|
||||
const data = await response.json()
|
||||
const nextUser = data?.user ?? null
|
||||
setUser(nextUser)
|
||||
setStats(normalizeStats(data?.stats))
|
||||
setLineage((data?.lineage ?? null) as UserLineage)
|
||||
setProfileSelection(
|
||||
nextUser?.profile_id == null || Number.isNaN(Number(nextUser?.profile_id))
|
||||
? ''
|
||||
: String(nextUser.profile_id)
|
||||
)
|
||||
setExpiryInput(toLocalDateTimeInput(nextUser?.expires_at))
|
||||
setError(null)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
setError('Could not load user.')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleUserBlock = async (blocked: boolean) => {
|
||||
if (!user) return
|
||||
try {
|
||||
setActionStatus(null)
|
||||
const baseUrl = getApiBase()
|
||||
const response = await authFetch(
|
||||
`${baseUrl}/admin/users/${encodeURIComponent(user.username)}/${blocked ? 'block' : 'unblock'}`,
|
||||
{ method: 'POST' }
|
||||
)
|
||||
if (!response.ok) {
|
||||
throw new Error('Update failed')
|
||||
}
|
||||
await loadUser()
|
||||
setActionStatus(blocked ? 'User blocked.' : 'User unblocked.')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
setError('Could not update user access.')
|
||||
}
|
||||
}
|
||||
|
||||
const updateUserRole = async (role: string) => {
|
||||
if (!user) return
|
||||
try {
|
||||
setActionStatus(null)
|
||||
const baseUrl = getApiBase()
|
||||
const response = await authFetch(
|
||||
`${baseUrl}/admin/users/${encodeURIComponent(user.username)}/role`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ role }),
|
||||
}
|
||||
)
|
||||
if (!response.ok) {
|
||||
throw new Error('Update failed')
|
||||
}
|
||||
await loadUser()
|
||||
setActionStatus(`Role updated to ${role}.`)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
setError('Could not update user role.')
|
||||
}
|
||||
}
|
||||
|
||||
const updateAutoSearchEnabled = async (enabled: boolean) => {
|
||||
if (!user) return
|
||||
try {
|
||||
setActionStatus(null)
|
||||
const baseUrl = getApiBase()
|
||||
const response = await authFetch(
|
||||
`${baseUrl}/admin/users/${encodeURIComponent(user.username)}/auto-search`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ enabled }),
|
||||
}
|
||||
)
|
||||
if (!response.ok) {
|
||||
throw new Error('Update failed')
|
||||
}
|
||||
await loadUser()
|
||||
setActionStatus(`Auto search/download ${enabled ? 'enabled' : 'disabled'}.`)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
setError('Could not update auto search access.')
|
||||
}
|
||||
}
|
||||
|
||||
const updateInviteManagementEnabled = async (enabled: boolean) => {
|
||||
if (!user) return
|
||||
try {
|
||||
setActionStatus(null)
|
||||
const baseUrl = getApiBase()
|
||||
const response = await authFetch(
|
||||
`${baseUrl}/admin/users/${encodeURIComponent(user.username)}/invite-access`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ enabled }),
|
||||
}
|
||||
)
|
||||
if (!response.ok) {
|
||||
throw new Error('Update failed')
|
||||
}
|
||||
await loadUser()
|
||||
setActionStatus(`Invite management ${enabled ? 'enabled' : 'disabled'} for this user.`)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
setError('Could not update invite access.')
|
||||
}
|
||||
}
|
||||
|
||||
const applyProfileToUser = async (profileOverride?: string | null) => {
|
||||
if (!user) return
|
||||
const profileValue = profileOverride ?? profileSelection
|
||||
setSavingProfile(true)
|
||||
setError(null)
|
||||
setActionStatus(null)
|
||||
try {
|
||||
const baseUrl = getApiBase()
|
||||
const response = await authFetch(
|
||||
`${baseUrl}/admin/users/${encodeURIComponent(user.username)}/profile`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ profile_id: profileValue || null }),
|
||||
}
|
||||
)
|
||||
if (!response.ok) {
|
||||
const text = await response.text()
|
||||
throw new Error(text || 'Profile update failed')
|
||||
}
|
||||
await loadUser()
|
||||
setActionStatus(profileValue ? 'Profile applied to user.' : 'Profile assignment cleared.')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
setError('Could not update user profile.')
|
||||
} finally {
|
||||
setSavingProfile(false)
|
||||
}
|
||||
}
|
||||
|
||||
const saveUserExpiry = async () => {
|
||||
if (!user) return
|
||||
const expiresAt = fromLocalDateTimeInput(expiryInput)
|
||||
if (expiryInput.trim() && !expiresAt) {
|
||||
setError('Invalid expiry date/time.')
|
||||
return
|
||||
}
|
||||
setSavingExpiry(true)
|
||||
setError(null)
|
||||
setActionStatus(null)
|
||||
try {
|
||||
const baseUrl = getApiBase()
|
||||
const response = await authFetch(
|
||||
`${baseUrl}/admin/users/${encodeURIComponent(user.username)}/expiry`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ expires_at: expiresAt }),
|
||||
}
|
||||
)
|
||||
if (!response.ok) {
|
||||
const text = await response.text()
|
||||
throw new Error(text || 'Expiry update failed')
|
||||
}
|
||||
await loadUser()
|
||||
setActionStatus(expiresAt ? 'User expiry updated.' : 'User expiry cleared.')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
setError('Could not update user expiry.')
|
||||
} finally {
|
||||
setSavingExpiry(false)
|
||||
}
|
||||
}
|
||||
|
||||
const clearUserExpiry = async () => {
|
||||
if (!user) return
|
||||
setSavingExpiry(true)
|
||||
setError(null)
|
||||
setActionStatus(null)
|
||||
try {
|
||||
const baseUrl = getApiBase()
|
||||
const response = await authFetch(
|
||||
`${baseUrl}/admin/users/${encodeURIComponent(user.username)}/expiry`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ clear: true }),
|
||||
}
|
||||
)
|
||||
if (!response.ok) {
|
||||
const text = await response.text()
|
||||
throw new Error(text || 'Expiry clear failed')
|
||||
}
|
||||
setExpiryInput('')
|
||||
await loadUser()
|
||||
setActionStatus('User expiry cleared.')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
setError('Could not clear user expiry.')
|
||||
} finally {
|
||||
setSavingExpiry(false)
|
||||
}
|
||||
}
|
||||
|
||||
const runSystemAction = async (action: 'ban' | 'unban' | 'remove') => {
|
||||
if (!user) return
|
||||
if (action === 'remove') {
|
||||
const confirmed = window.confirm(
|
||||
`Remove ${user.username} from Magent and external systems? This is destructive.`
|
||||
)
|
||||
if (!confirmed) return
|
||||
}
|
||||
if (action === 'ban') {
|
||||
const confirmed = window.confirm(
|
||||
`Ban ${user.username} across systems and disable invites they created?`
|
||||
)
|
||||
if (!confirmed) return
|
||||
}
|
||||
setSystemActionBusy(true)
|
||||
setError(null)
|
||||
setActionStatus(null)
|
||||
try {
|
||||
const baseUrl = getApiBase()
|
||||
const response = await authFetch(
|
||||
`${baseUrl}/admin/users/${encodeURIComponent(user.username)}/system-action`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action }),
|
||||
}
|
||||
)
|
||||
const text = await response.text()
|
||||
let data: any = null
|
||||
try {
|
||||
data = text ? JSON.parse(text) : null
|
||||
} catch {
|
||||
data = null
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(data?.detail || text || 'Cross-system action failed')
|
||||
}
|
||||
const state = data?.status === 'partial' ? 'partial' : 'complete'
|
||||
if (action === 'remove') {
|
||||
setActionStatus(`User removed (${state}).`)
|
||||
router.push('/users')
|
||||
return
|
||||
}
|
||||
await loadUser()
|
||||
setActionStatus(`${action === 'ban' ? 'Ban' : 'Unban'} completed (${state}).`)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
setError(err instanceof Error ? err.message : 'Could not run cross-system action.')
|
||||
} finally {
|
||||
setSystemActionBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!getToken()) {
|
||||
router.push('/login')
|
||||
return
|
||||
}
|
||||
void loadUser()
|
||||
void loadProfiles()
|
||||
}, [router, idParam])
|
||||
|
||||
if (loading) {
|
||||
return <main className="card">Loading user...</main>
|
||||
}
|
||||
|
||||
return (
|
||||
<AdminShell
|
||||
title={user?.username || 'User'}
|
||||
subtitle="User overview and request stats."
|
||||
actions={
|
||||
<button type="button" onClick={() => router.push('/users')}>
|
||||
Back to users
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<section className="admin-section">
|
||||
{error && <div className="error-banner">{error}</div>}
|
||||
{actionStatus && <div className="status-banner">{actionStatus}</div>}
|
||||
{!user ? (
|
||||
<div className="status-banner">No user data found.</div>
|
||||
) : (
|
||||
<div className="user-detail-page-grid">
|
||||
<div className="user-detail-main-column">
|
||||
<div className="admin-panel user-detail-panel">
|
||||
<div className="user-detail-panel-header">
|
||||
<div className="user-detail-title-row">
|
||||
<strong className="user-detail-name">{user.username}</strong>
|
||||
<span className={`user-grid-pill ${user.is_blocked ? 'is-blocked' : ''}`}>
|
||||
{user.is_blocked ? 'Blocked' : 'Active'}
|
||||
</span>
|
||||
<span className={`user-grid-pill ${user.is_expired ? 'is-blocked' : ''}`}>
|
||||
{user.is_expired ? 'Expired' : user.expires_at ? 'Expiry set' : 'No expiry'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="lede">
|
||||
User identity, access state, and request history for this account.
|
||||
</p>
|
||||
</div>
|
||||
<div className="user-detail-meta-grid">
|
||||
<div className="user-detail-meta-item">
|
||||
<span className="label">Email</span>
|
||||
<strong>{user.email || 'Not set'}</strong>
|
||||
</div>
|
||||
<div className="user-detail-meta-item">
|
||||
<span className="label">Seerr ID</span>
|
||||
<strong>{user.jellyseerr_user_id ?? user.id ?? 'Unknown'}</strong>
|
||||
</div>
|
||||
<div className="user-detail-meta-item">
|
||||
<span className="label">Role</span>
|
||||
<strong>{user.role}</strong>
|
||||
</div>
|
||||
<div className="user-detail-meta-item">
|
||||
<span className="label">Login type</span>
|
||||
<strong>{user.auth_provider || 'local'}</strong>
|
||||
</div>
|
||||
<div className="user-detail-meta-item">
|
||||
<span className="label">Assigned profile</span>
|
||||
<strong>{user.profile_id ?? 'None'}</strong>
|
||||
</div>
|
||||
<div className="user-detail-meta-item">
|
||||
<span className="label">Invited by</span>
|
||||
<strong>{lineage?.invited_by || 'Direct / unknown'}</strong>
|
||||
</div>
|
||||
<div className="user-detail-meta-item">
|
||||
<span className="label">Invite code used</span>
|
||||
<strong>{lineage?.invite_code || user.invited_by_code || 'None'}</strong>
|
||||
</div>
|
||||
<div className="user-detail-meta-item">
|
||||
<span className="label">Last login</span>
|
||||
<strong>{formatDateTime(user.last_login_at)}</strong>
|
||||
</div>
|
||||
<div className="user-detail-meta-item">
|
||||
<span className="label">Account expiry</span>
|
||||
<strong>{user.expires_at ? formatDateTime(user.expires_at) : 'Never'}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="admin-panel user-detail-panel">
|
||||
<div className="user-detail-panel-header">
|
||||
<h2>Request statistics</h2>
|
||||
<p className="lede">Snapshot of request states and recent activity for this user.</p>
|
||||
</div>
|
||||
<div className="user-detail-grid">
|
||||
<div className="user-detail-stat">
|
||||
<span className="label">Total</span>
|
||||
<span className="value">{stats?.total ?? 0}</span>
|
||||
</div>
|
||||
<div className="user-detail-stat">
|
||||
<span className="label">Ready</span>
|
||||
<span className="value">{stats?.ready ?? 0}</span>
|
||||
</div>
|
||||
<div className="user-detail-stat">
|
||||
<span className="label">Pending</span>
|
||||
<span className="value">{stats?.pending ?? 0}</span>
|
||||
</div>
|
||||
<div className="user-detail-stat">
|
||||
<span className="label">Approved</span>
|
||||
<span className="value">{stats?.approved ?? 0}</span>
|
||||
</div>
|
||||
<div className="user-detail-stat">
|
||||
<span className="label">Working</span>
|
||||
<span className="value">{stats?.working ?? 0}</span>
|
||||
</div>
|
||||
<div className="user-detail-stat">
|
||||
<span className="label">Partial</span>
|
||||
<span className="value">{stats?.partial ?? 0}</span>
|
||||
</div>
|
||||
<div className="user-detail-stat">
|
||||
<span className="label">Declined</span>
|
||||
<span className="value">{stats?.declined ?? 0}</span>
|
||||
</div>
|
||||
<div className="user-detail-stat">
|
||||
<span className="label">In progress</span>
|
||||
<span className="value">{stats?.in_progress ?? 0}</span>
|
||||
</div>
|
||||
<div className="user-detail-stat user-detail-stat--wide">
|
||||
<span className="label">Last request</span>
|
||||
<span className="value">{formatDateTime(stats?.last_request_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="user-detail-side-column">
|
||||
<div className="admin-panel user-detail-panel">
|
||||
<div className="user-detail-panel-header">
|
||||
<h2>Access controls</h2>
|
||||
<p className="lede">Role, login access, and auto-download behavior.</p>
|
||||
</div>
|
||||
<div className="user-detail-control-stack">
|
||||
<label className="toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={user.role === 'admin'}
|
||||
onChange={(event) => updateUserRole(event.target.checked ? 'admin' : 'user')}
|
||||
/>
|
||||
<span>Make admin</span>
|
||||
</label>
|
||||
<label className="toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(user.auto_search_enabled ?? true)}
|
||||
disabled={user.role === 'admin'}
|
||||
onChange={(event) => updateAutoSearchEnabled(event.target.checked)}
|
||||
/>
|
||||
<span>Allow auto search/download</span>
|
||||
</label>
|
||||
<label className="toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(user.invite_management_enabled ?? false)}
|
||||
disabled={user.role === 'admin'}
|
||||
onChange={(event) => updateInviteManagementEnabled(event.target.checked)}
|
||||
/>
|
||||
<span>Allow self-service invites</span>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-button"
|
||||
onClick={() => toggleUserBlock(!user.is_blocked)}
|
||||
disabled={systemActionBusy}
|
||||
>
|
||||
{user.is_blocked ? 'Allow access' : 'Block access'}
|
||||
</button>
|
||||
<div className="admin-inline-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-button"
|
||||
onClick={() => void runSystemAction(user.is_blocked ? 'unban' : 'ban')}
|
||||
disabled={systemActionBusy}
|
||||
>
|
||||
{systemActionBusy
|
||||
? 'Working...'
|
||||
: user.is_blocked
|
||||
? 'Unban everywhere'
|
||||
: 'Ban everywhere'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-button"
|
||||
onClick={() => void runSystemAction('remove')}
|
||||
disabled={systemActionBusy}
|
||||
>
|
||||
Remove everywhere
|
||||
</button>
|
||||
</div>
|
||||
{user.role === 'admin' && (
|
||||
<div className="user-detail-helper">
|
||||
Admins always have auto search/download and invite-management access.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="admin-panel user-detail-panel">
|
||||
<div className="user-detail-panel-header">
|
||||
<h2>Profile defaults</h2>
|
||||
<p className="lede">Assign or clear an invite profile for this user.</p>
|
||||
</div>
|
||||
<div className="user-detail-actions user-detail-actions--stacked">
|
||||
<label className="admin-select">
|
||||
<span>Assigned profile</span>
|
||||
<select
|
||||
value={profileSelection}
|
||||
onChange={(event) => setProfileSelection(event.target.value)}
|
||||
disabled={savingProfile}
|
||||
>
|
||||
<option value="">None</option>
|
||||
{profiles.map((profile) => (
|
||||
<option key={profile.id} value={profile.id}>
|
||||
{profile.name}
|
||||
{profile.is_active === false ? ' (disabled)' : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<div className="admin-inline-actions">
|
||||
<button type="button" onClick={() => void applyProfileToUser()} disabled={savingProfile}>
|
||||
{savingProfile ? 'Applying...' : 'Apply profile defaults'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-button"
|
||||
onClick={() => {
|
||||
setProfileSelection('')
|
||||
void applyProfileToUser('')
|
||||
}}
|
||||
disabled={savingProfile}
|
||||
>
|
||||
Clear profile
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="admin-panel user-detail-panel">
|
||||
<div className="user-detail-panel-header">
|
||||
<h2>Account expiry</h2>
|
||||
<p className="lede">Set a specific expiry date/time for this user account.</p>
|
||||
</div>
|
||||
<div className="user-detail-actions user-detail-actions--stacked">
|
||||
<label>
|
||||
<span className="user-bulk-label">Account expiry</span>
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={expiryInput}
|
||||
onChange={(event) => setExpiryInput(event.target.value)}
|
||||
disabled={savingExpiry}
|
||||
/>
|
||||
</label>
|
||||
<div className="admin-inline-actions">
|
||||
<button type="button" onClick={saveUserExpiry} disabled={savingExpiry}>
|
||||
{savingExpiry ? 'Saving...' : 'Save expiry'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-button"
|
||||
onClick={clearUserExpiry}
|
||||
disabled={savingExpiry}
|
||||
>
|
||||
Clear expiry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</AdminShell>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user