fix(rbac): Viewer-Rolle in Detail-Pages + ActionButtons — Edit ebenfalls sperren
- ActionButtons: Edit-Button wird für Viewer wie Delete gesperrt (Tooltip zeigt Reason) - Domains/Detail: isViewer-Flag an alle Sub-Panels weitergegeben; Save-, TLS-Cert-, Routing-Rules- und Headers-Buttons für Viewer disabled - Backends/Detail: Save-Button + ServerPanel Add-Button für Viewer disabled - i18n: domains.backendUp/backendDown Keys (waren noch hardkodiert) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -60,7 +60,7 @@ import (
|
|||||||
usersvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/users"
|
usersvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/users"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "1.1.88"
|
var version = "1.1.89"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
addr := os.Getenv("EDGEGUARD_API_ADDR")
|
addr := os.Getenv("EDGEGUARD_API_ADDR")
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/setup"
|
"git.netcell-it.de/projekte/edgeguard-native/internal/services/setup"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "1.1.88"
|
var version = "1.1.89"
|
||||||
|
|
||||||
const usage = `edgeguard-ctl — EdgeGuard CLI
|
const usage = `edgeguard-ctl — EdgeGuard CLI
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import (
|
|||||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts"
|
"git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "1.1.88"
|
var version = "1.1.89"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// renewTickInterval — how often we re-evaluate expiring certs.
|
// renewTickInterval — how often we re-evaluate expiring certs.
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import { useAuthStore } from '../stores/auth'
|
|||||||
//
|
//
|
||||||
// Either prop may be omitted to suppress that button — useful for
|
// Either prop may be omitted to suppress that button — useful for
|
||||||
// rows that aren't editable (e.g. builtin services).
|
// rows that aren't editable (e.g. builtin services).
|
||||||
// Viewer-role accounts automatically get delete disabled — mutations
|
// Viewer-role accounts get both buttons disabled — mutations are
|
||||||
// are blocked at the API level too, but hiding the button avoids
|
// blocked at the API level too, but disabling here avoids confusing
|
||||||
// confusing "access denied" errors for read-only users.
|
// "access denied" errors for read-only users.
|
||||||
interface ActionButtonsProps {
|
interface ActionButtonsProps {
|
||||||
onEdit?: () => void
|
onEdit?: () => void
|
||||||
onDelete?: () => void
|
onDelete?: () => void
|
||||||
@@ -35,20 +35,23 @@ export default function ActionButtons({
|
|||||||
const role = useAuthStore((s) => s.user?.role)
|
const role = useAuthStore((s) => s.user?.role)
|
||||||
const isViewer = role === 'viewer'
|
const isViewer = role === 'viewer'
|
||||||
|
|
||||||
|
const viewerReason = isViewer ? t('auth.viewerBadge') : undefined
|
||||||
|
|
||||||
|
const editDis = editDisabled || isViewer
|
||||||
|
const editDisReason = isViewer ? viewerReason : editDisabledReason
|
||||||
|
|
||||||
const delDisabled = deleteDisabled || isViewer
|
const delDisabled = deleteDisabled || isViewer
|
||||||
const delDisabledReason = isViewer
|
const delDisabledReason = isViewer ? viewerReason : deleteDisabledReason
|
||||||
? t('auth.viewerBadge')
|
|
||||||
: deleteDisabledReason
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space size={4}>
|
<Space size={4}>
|
||||||
{onEdit && (
|
{onEdit && (
|
||||||
<Tooltip title={editDisabled ? editDisabledReason : (editTooltip ?? t('common.edit'))}>
|
<Tooltip title={editDis ? editDisReason : (editTooltip ?? t('common.edit'))}>
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<EditOutlined />}
|
icon={<EditOutlined />}
|
||||||
disabled={editDisabled}
|
disabled={editDis}
|
||||||
onClick={onEdit}
|
onClick={onEdit}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -322,7 +322,9 @@
|
|||||||
"rateLimitTag": "Rate-Limit",
|
"rateLimitTag": "Rate-Limit",
|
||||||
"routingRulesTitle": "Routing-Regeln — {{name}}",
|
"routingRulesTitle": "Routing-Regeln — {{name}}",
|
||||||
"routingRulesHint": "Pfad-Präfix → Backend-Zuordnungen für diese Domain. Niedrigste Prioritätszahl gewinnt; nicht gematchte Anfragen gehen an das Primary-Backend.",
|
"routingRulesHint": "Pfad-Präfix → Backend-Zuordnungen für diese Domain. Niedrigste Prioritätszahl gewinnt; nicht gematchte Anfragen gehen an das Primary-Backend.",
|
||||||
"routingRulesEmpty": "Keine Routing-Regeln — alle Anfragen gehen an das Primary-Backend."
|
"routingRulesEmpty": "Keine Routing-Regeln — alle Anfragen gehen an das Primary-Backend.",
|
||||||
|
"backendUp": "Backend UP",
|
||||||
|
"backendDown": "Backend DOWN"
|
||||||
},
|
},
|
||||||
"backends": {
|
"backends": {
|
||||||
"title": "Backends",
|
"title": "Backends",
|
||||||
|
|||||||
@@ -322,7 +322,9 @@
|
|||||||
"rateLimitTag": "Rate limited",
|
"rateLimitTag": "Rate limited",
|
||||||
"routingRulesTitle": "Routing rules — {{name}}",
|
"routingRulesTitle": "Routing rules — {{name}}",
|
||||||
"routingRulesHint": "Path-prefix → backend mappings for this domain. Lowest priority number wins; unmatched requests go to the primary backend.",
|
"routingRulesHint": "Path-prefix → backend mappings for this domain. Lowest priority number wins; unmatched requests go to the primary backend.",
|
||||||
"routingRulesEmpty": "No routing rules — all requests go to the primary backend."
|
"routingRulesEmpty": "No routing rules — all requests go to the primary backend.",
|
||||||
|
"backendUp": "backend UP",
|
||||||
|
"backendDown": "backend DOWN"
|
||||||
},
|
},
|
||||||
"backends": {
|
"backends": {
|
||||||
"title": "Backends",
|
"title": "Backends",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import apiClient, { isEnvelope } from '../../api/client'
|
import apiClient, { isEnvelope } from '../../api/client'
|
||||||
import PageHeader from '../../components/PageHeader'
|
import PageHeader from '../../components/PageHeader'
|
||||||
import ActionButtons from '../../components/ActionButtons'
|
import ActionButtons from '../../components/ActionButtons'
|
||||||
|
import { useAuthStore } from '../../stores/auth'
|
||||||
|
|
||||||
const { Text } = Typography
|
const { Text } = Typography
|
||||||
|
|
||||||
@@ -90,6 +91,7 @@ export default function BackendDetailPage() {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
const backendID = Number(id)
|
const backendID = Number(id)
|
||||||
|
const isViewer = useAuthStore((s) => s.user?.role) === 'viewer'
|
||||||
|
|
||||||
const { data: backend, isLoading } = useQuery({
|
const { data: backend, isLoading } = useQuery({
|
||||||
queryKey: ['backend', backendID],
|
queryKey: ['backend', backendID],
|
||||||
@@ -213,9 +215,11 @@ export default function BackendDetailPage() {
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Button type="primary" htmlType="submit" loading={update.isPending}>
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||||
{t('common.save')}
|
<Button type="primary" htmlType="submit" loading={update.isPending} disabled={isViewer}>
|
||||||
</Button>
|
{t('common.save')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -223,7 +227,7 @@ export default function BackendDetailPage() {
|
|||||||
|
|
||||||
<Col xs={24} lg={14}>
|
<Col xs={24} lg={14}>
|
||||||
<Card size="small" title={t('backends.serversIn', { name: backend.name })}>
|
<Card size="small" title={t('backends.serversIn', { name: backend.name })}>
|
||||||
<ServerPanel backendID={backendID} haproxyStats={haproxyStats ?? []} />
|
<ServerPanel backendID={backendID} haproxyStats={haproxyStats ?? []} isViewer={isViewer} />
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
@@ -231,7 +235,7 @@ export default function BackendDetailPage() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ServerPanel({ backendID, haproxyStats }: { backendID: number; haproxyStats: HAProxyStat[] }) {
|
function ServerPanel({ backendID, haproxyStats, isViewer }: { backendID: number; haproxyStats: HAProxyStat[]; isViewer: boolean }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
@@ -347,12 +351,14 @@ function ServerPanel({ backendID, haproxyStats }: { backendID: number; haproxySt
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mb-8" style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
<div className="mb-8" style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
<Button type="primary" icon={<PlusOutlined />} size="small" onClick={() => {
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||||
setOpen(true); form.resetFields()
|
<Button type="primary" icon={<PlusOutlined />} size="small" disabled={isViewer} onClick={() => {
|
||||||
form.setFieldsValue({ weight: 100, backup: false, active: true, port: 8080 })
|
setOpen(true); form.resetFields()
|
||||||
}}>
|
form.setFieldsValue({ weight: 100, backup: false, active: true, port: 8080 })
|
||||||
{t('backends.server.add')}
|
}}>
|
||||||
</Button>
|
{t('backends.server.add')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<Table
|
<Table
|
||||||
size="small" rowKey="id" loading={isLoading}
|
size="small" rowKey="id" loading={isLoading}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import apiClient, { isEnvelope } from '../../api/client'
|
import apiClient, { isEnvelope } from '../../api/client'
|
||||||
import PageHeader from '../../components/PageHeader'
|
import PageHeader from '../../components/PageHeader'
|
||||||
import StatusDot from '../../components/StatusDot'
|
import StatusDot from '../../components/StatusDot'
|
||||||
|
import { useAuthStore } from '../../stores/auth'
|
||||||
|
|
||||||
const { Text } = Typography
|
const { Text } = Typography
|
||||||
|
|
||||||
@@ -109,6 +110,7 @@ export default function DomainDetailPage() {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
const domainID = Number(id)
|
const domainID = Number(id)
|
||||||
|
const isViewer = useAuthStore((s) => s.user?.role) === 'viewer'
|
||||||
|
|
||||||
const { data: domain, isLoading } = useQuery({
|
const { data: domain, isLoading } = useQuery({
|
||||||
queryKey: ['domain', domainID],
|
queryKey: ['domain', domainID],
|
||||||
@@ -193,8 +195,8 @@ export default function DomainDetailPage() {
|
|||||||
<Space size={6}>
|
<Space size={6}>
|
||||||
<StatusDot active={domain.active} />
|
<StatusDot active={domain.active} />
|
||||||
{certBadge()}
|
{certBadge()}
|
||||||
{backendHealth === 'UP' && <Tag color="green" style={{ margin: 0 }}>backend UP</Tag>}
|
{backendHealth === 'UP' && <Tag color="green" style={{ margin: 0 }}>{t('domains.backendUp')}</Tag>}
|
||||||
{backendHealth === 'DOWN' && <Tag color="red" style={{ margin: 0 }}>backend DOWN</Tag>}
|
{backendHealth === 'DOWN' && <Tag color="red" style={{ margin: 0 }}>{t('domains.backendDown')}</Tag>}
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
extra={
|
extra={
|
||||||
@@ -205,23 +207,30 @@ export default function DomainDetailPage() {
|
|||||||
description={t('ssl.renewConfirmDesc', { domain: domain.name })}
|
description={t('ssl.renewConfirmDesc', { domain: domain.name })}
|
||||||
onConfirm={() => issueCert.mutate(domain.name)}
|
onConfirm={() => issueCert.mutate(domain.name)}
|
||||||
okText={t('common.yes')} cancelText={t('common.no')}
|
okText={t('common.yes')} cancelText={t('common.no')}
|
||||||
|
disabled={isViewer}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||||
icon={<SafetyCertificateOutlined />}
|
<Button
|
||||||
loading={issueCert.isPending}
|
icon={<SafetyCertificateOutlined />}
|
||||||
>
|
loading={issueCert.isPending}
|
||||||
{t('ssl.renewBtn')}
|
disabled={isViewer}
|
||||||
</Button>
|
>
|
||||||
|
{t('ssl.renewBtn')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||||
icon={<SafetyCertificateOutlined />}
|
<Button
|
||||||
type="primary"
|
icon={<SafetyCertificateOutlined />}
|
||||||
loading={issueCert.isPending}
|
type="primary"
|
||||||
onClick={() => issueCert.mutate(domain.name)}
|
loading={issueCert.isPending}
|
||||||
>
|
disabled={isViewer}
|
||||||
{t('ssl.issueButton')}
|
onClick={() => issueCert.mutate(domain.name)}
|
||||||
</Button>
|
>
|
||||||
|
{t('ssl.issueButton')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/domains')}>
|
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/domains')}>
|
||||||
{t('domains.backToList')}
|
{t('domains.backToList')}
|
||||||
@@ -232,7 +241,7 @@ export default function DomainDetailPage() {
|
|||||||
|
|
||||||
<Row gutter={[24, 0]}>
|
<Row gutter={[24, 0]}>
|
||||||
<Col xs={24}>
|
<Col xs={24}>
|
||||||
<RoutingRulesPanel domainID={domainID} domainName={domain.name} />
|
<RoutingRulesPanel domainID={domainID} domainName={domain.name} isViewer={isViewer} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
@@ -347,16 +356,18 @@ export default function DomainDetailPage() {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Button type="primary" htmlType="submit" loading={update.isPending}>
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||||
{t('common.save')}
|
<Button type="primary" htmlType="submit" loading={update.isPending} disabled={isViewer}>
|
||||||
</Button>
|
{t('common.save')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col xs={24} lg={12}>
|
<Col xs={24} lg={12}>
|
||||||
<HeadersPanel domainID={domainID} domainName={domain.name} />
|
<HeadersPanel domainID={domainID} domainName={domain.name} isViewer={isViewer} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
@@ -365,7 +376,7 @@ export default function DomainDetailPage() {
|
|||||||
|
|
||||||
// ── Routing Rules Panel ─────────────────────────────────────────────────
|
// ── Routing Rules Panel ─────────────────────────────────────────────────
|
||||||
|
|
||||||
function RoutingRulesPanel({ domainID, domainName }: { domainID: number; domainName: string }) {
|
function RoutingRulesPanel({ domainID, domainName, isViewer }: { domainID: number; domainName: string; isViewer: boolean }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
@@ -428,6 +439,7 @@ function RoutingRulesPanel({ domainID, domainName }: { domainID: number; domainN
|
|||||||
<Switch
|
<Switch
|
||||||
size="small"
|
size="small"
|
||||||
checked={v}
|
checked={v}
|
||||||
|
disabled={isViewer}
|
||||||
loading={quickToggle.isPending && quickToggle.variables?.id === r.id}
|
loading={quickToggle.isPending && quickToggle.variables?.id === r.id}
|
||||||
onChange={(checked) => quickToggle.mutate({ id: r.id, row: r, checked })}
|
onChange={(checked) => quickToggle.mutate({ id: r.id, row: r, checked })}
|
||||||
/>
|
/>
|
||||||
@@ -437,19 +449,28 @@ function RoutingRulesPanel({ domainID, domainName }: { domainID: number; domainN
|
|||||||
title: t('common.actions'), key: 'a', width: 90,
|
title: t('common.actions'), key: 'a', width: 90,
|
||||||
render: (_, r) => (
|
render: (_, r) => (
|
||||||
<Space size={4}>
|
<Space size={4}>
|
||||||
<Button type="text" size="small" icon={<EditOutlined />}
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : t('common.edit')}>
|
||||||
onClick={() => {
|
<Button type="text" size="small" icon={<EditOutlined />}
|
||||||
setEditing(r)
|
disabled={isViewer}
|
||||||
rForm.setFieldsValue({ path_prefix: r.path_prefix, backend_id: r.backend_id, priority: r.priority, active: r.active })
|
onClick={() => {
|
||||||
}}
|
setEditing(r)
|
||||||
/>
|
rForm.setFieldsValue({ path_prefix: r.path_prefix, backend_id: r.backend_id, priority: r.priority, active: r.active })
|
||||||
<Popconfirm
|
}}
|
||||||
title={t('routing.deleteConfirm')}
|
/>
|
||||||
okText={t('common.yes')} cancelText={t('common.no')}
|
</Tooltip>
|
||||||
onConfirm={() => del.mutate(r.id)}
|
{isViewer ? (
|
||||||
>
|
<Tooltip title={t('auth.viewerBadge')}>
|
||||||
<Button type="text" size="small" danger icon={<DeleteOutlined />} />
|
<Button type="text" size="small" danger icon={<DeleteOutlined />} disabled />
|
||||||
</Popconfirm>
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<Popconfirm
|
||||||
|
title={t('routing.deleteConfirm')}
|
||||||
|
okText={t('common.yes')} cancelText={t('common.no')}
|
||||||
|
onConfirm={() => del.mutate(r.id)}
|
||||||
|
>
|
||||||
|
<Button type="text" size="small" danger icon={<DeleteOutlined />} />
|
||||||
|
</Popconfirm>
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -462,12 +483,14 @@ function RoutingRulesPanel({ domainID, domainName }: { domainID: number; domainN
|
|||||||
title={t('domains.routingRulesTitle', { name: domainName })}
|
title={t('domains.routingRulesTitle', { name: domainName })}
|
||||||
className="mb-16"
|
className="mb-16"
|
||||||
extra={
|
extra={
|
||||||
<Button type="primary" size="small" icon={<PlusOutlined />} onClick={() => {
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||||
setOpen(true); rForm.resetFields()
|
<Button type="primary" size="small" icon={<PlusOutlined />} disabled={isViewer} onClick={() => {
|
||||||
rForm.setFieldsValue({ path_prefix: '/', priority: 100, active: true })
|
setOpen(true); rForm.resetFields()
|
||||||
}}>
|
rForm.setFieldsValue({ path_prefix: '/', priority: 100, active: true })
|
||||||
{t('routing.addRule')}
|
}}>
|
||||||
</Button>
|
{t('routing.addRule')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 12 }}>
|
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 12 }}>
|
||||||
@@ -517,7 +540,7 @@ function RoutingRulesPanel({ domainID, domainName }: { domainID: number; domainN
|
|||||||
|
|
||||||
// ── Response Headers Panel ──────────────────────────────────────────────
|
// ── Response Headers Panel ──────────────────────────────────────────────
|
||||||
|
|
||||||
function HeadersPanel({ domainID, domainName }: { domainID: number; domainName: string }) {
|
function HeadersPanel({ domainID, domainName, isViewer }: { domainID: number; domainName: string; isViewer: boolean }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
@@ -566,19 +589,28 @@ function HeadersPanel({ domainID, domainName }: { domainID: number; domainName:
|
|||||||
title: t('common.actions'), key: 'a', width: 80,
|
title: t('common.actions'), key: 'a', width: 80,
|
||||||
render: (_, r) => (
|
render: (_, r) => (
|
||||||
<Space size={4}>
|
<Space size={4}>
|
||||||
<Button type="text" size="small" icon={<EditOutlined />}
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : t('common.edit')}>
|
||||||
onClick={() => {
|
<Button type="text" size="small" icon={<EditOutlined />}
|
||||||
setEditing(r)
|
disabled={isViewer}
|
||||||
hForm.setFieldsValue({ name: r.name, value: r.value, position: r.position })
|
onClick={() => {
|
||||||
}}
|
setEditing(r)
|
||||||
/>
|
hForm.setFieldsValue({ name: r.name, value: r.value, position: r.position })
|
||||||
<Popconfirm
|
}}
|
||||||
title={t('domains.headerDeleteConfirm', { name: r.name })}
|
/>
|
||||||
okText={t('common.yes')} cancelText={t('common.no')}
|
</Tooltip>
|
||||||
onConfirm={() => del.mutate(r.id)}
|
{isViewer ? (
|
||||||
>
|
<Tooltip title={t('auth.viewerBadge')}>
|
||||||
<Button type="text" size="small" danger icon={<DeleteOutlined />} />
|
<Button type="text" size="small" danger icon={<DeleteOutlined />} disabled />
|
||||||
</Popconfirm>
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<Popconfirm
|
||||||
|
title={t('domains.headerDeleteConfirm', { name: r.name })}
|
||||||
|
okText={t('common.yes')} cancelText={t('common.no')}
|
||||||
|
onConfirm={() => del.mutate(r.id)}
|
||||||
|
>
|
||||||
|
<Button type="text" size="small" danger icon={<DeleteOutlined />} />
|
||||||
|
</Popconfirm>
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -590,12 +622,14 @@ function HeadersPanel({ domainID, domainName }: { domainID: number; domainName:
|
|||||||
size="small"
|
size="small"
|
||||||
title={t('domains.headersTitle', { name: domainName })}
|
title={t('domains.headersTitle', { name: domainName })}
|
||||||
extra={
|
extra={
|
||||||
<Button type="primary" size="small" icon={<PlusOutlined />} onClick={() => {
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||||
setOpen(true); hForm.resetFields()
|
<Button type="primary" size="small" icon={<PlusOutlined />} disabled={isViewer} onClick={() => {
|
||||||
hForm.setFieldsValue({ name: '', value: '', position: headers?.length ?? 0 })
|
setOpen(true); hForm.resetFields()
|
||||||
}}>
|
hForm.setFieldsValue({ name: '', value: '', position: headers?.length ?? 0 })
|
||||||
{t('domains.addHeader')}
|
}}>
|
||||||
</Button>
|
{t('domains.addHeader')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 12 }}>
|
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 12 }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user