mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 04:36:31 +08:00
update
This commit is contained in:
parent
23ffbd2532
commit
fe51c9fbdf
@ -1,10 +1,8 @@
|
||||
import { AccessTab } from '@/features/deployments/detail/access-tab'
|
||||
|
||||
type PageProps = {
|
||||
export default async function InstanceDetailAccessPage({ params }: {
|
||||
params: Promise<{ instanceId: string }>
|
||||
}
|
||||
|
||||
export default async function InstanceDetailAccessPage({ params }: PageProps) {
|
||||
}) {
|
||||
const { instanceId } = await params
|
||||
return <AccessTab instanceId={instanceId} />
|
||||
}
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import { DeployTab } from '@/features/deployments/detail/deploy-tab'
|
||||
|
||||
type PageProps = {
|
||||
export default async function InstanceDetailDeployPage({ params }: {
|
||||
params: Promise<{ instanceId: string }>
|
||||
}
|
||||
|
||||
export default async function InstanceDetailDeployPage({ params }: PageProps) {
|
||||
}) {
|
||||
const { instanceId } = await params
|
||||
return <DeployTab instanceId={instanceId} />
|
||||
}
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { InstanceDetail } from '@/features/deployments/detail'
|
||||
|
||||
type LayoutProps = {
|
||||
export default async function InstanceDetailLayout({ children, params }: {
|
||||
children: ReactNode
|
||||
params: Promise<{ instanceId: string }>
|
||||
}
|
||||
|
||||
export default async function InstanceDetailLayout({ children, params }: LayoutProps) {
|
||||
}) {
|
||||
const { instanceId } = await params
|
||||
|
||||
return (
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import { OverviewTab } from '@/features/deployments/detail/overview-tab'
|
||||
|
||||
type PageProps = {
|
||||
export default async function InstanceDetailOverviewPage({ params }: {
|
||||
params: Promise<{ instanceId: string }>
|
||||
}
|
||||
|
||||
export default async function InstanceDetailOverviewPage({ params }: PageProps) {
|
||||
}) {
|
||||
const { instanceId } = await params
|
||||
return <OverviewTab instanceId={instanceId} />
|
||||
}
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import { redirect } from '@/next/navigation'
|
||||
|
||||
type PageProps = {
|
||||
export default async function InstanceDetailPage({ params }: {
|
||||
params: Promise<{ instanceId: string }>
|
||||
}
|
||||
|
||||
export default async function InstanceDetailPage({ params }: PageProps) {
|
||||
}) {
|
||||
const { instanceId } = await params
|
||||
redirect(`/deployments/${instanceId}/overview`)
|
||||
}
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import { SettingsTab } from '@/features/deployments/detail/settings-tab'
|
||||
|
||||
type PageProps = {
|
||||
export default async function InstanceDetailSettingsPage({ params }: {
|
||||
params: Promise<{ instanceId: string }>
|
||||
}
|
||||
|
||||
export default async function InstanceDetailSettingsPage({ params }: PageProps) {
|
||||
}) {
|
||||
const { instanceId } = await params
|
||||
return <SettingsTab instanceId={instanceId} />
|
||||
}
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import { VersionsTab } from '@/features/deployments/detail/versions-tab'
|
||||
|
||||
type PageProps = {
|
||||
export default async function InstanceDetailVersionsPage({ params }: {
|
||||
params: Promise<{ instanceId: string }>
|
||||
}
|
||||
|
||||
export default async function InstanceDetailVersionsPage({ params }: PageProps) {
|
||||
}) {
|
||||
const { instanceId } = await params
|
||||
return <VersionsTab instanceId={instanceId} />
|
||||
}
|
||||
|
||||
@ -203,11 +203,9 @@ export function AppPicker({ apps, isLoading, value, onChange }: AppPickerProps)
|
||||
)
|
||||
}
|
||||
|
||||
type CreateInstanceFormProps = {
|
||||
function CreateInstanceForm({ onClose }: {
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
function CreateInstanceForm({ onClose }: CreateInstanceFormProps) {
|
||||
}) {
|
||||
const { t } = useTranslation('deployments')
|
||||
const router = useRouter()
|
||||
const createInstance = useMutation(consoleQuery.enterprise.appDeploy.createAppInstance.mutationOptions())
|
||||
|
||||
@ -7,13 +7,11 @@ import { useTranslation } from 'react-i18next'
|
||||
import { environmentHealth, environmentMode, environmentName } from '../../utils'
|
||||
import { HealthBadge, ModeBadge } from '../status-badge'
|
||||
|
||||
type FieldProps = {
|
||||
export function Field({ label, hint, children }: {
|
||||
label: string
|
||||
hint?: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function Field({ label, hint, children }: FieldProps) {
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
@ -78,9 +76,7 @@ export function DeploymentSelect({ value, onChange, options, placeholder }: Sele
|
||||
)
|
||||
}
|
||||
|
||||
type EnvironmentRowProps = { env: EnvironmentOption }
|
||||
|
||||
export function EnvironmentRow({ env }: EnvironmentRowProps) {
|
||||
export function EnvironmentRow({ env }: { env: EnvironmentOption }) {
|
||||
return (
|
||||
<div className="flex items-center justify-between rounded-lg border border-components-panel-border bg-components-panel-bg-blur px-3 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@ -25,12 +25,10 @@ import {
|
||||
toAppInfoFromOverview,
|
||||
} from '../utils'
|
||||
|
||||
type InfoRowProps = {
|
||||
function InfoRow({ label, value }: {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
function InfoRow({ label, value }: InfoRowProps) {
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<span className="system-xs-medium-uppercase text-text-tertiary">{label}</span>
|
||||
|
||||
@ -3,11 +3,6 @@ import type { DeployStatus, EnvironmentHealth, EnvironmentMode } from '../types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type StatusBadgeProps = {
|
||||
status: DeployStatus
|
||||
className?: string
|
||||
}
|
||||
|
||||
const statusStyles: Record<DeployStatus, string> = {
|
||||
ready: 'border-util-colors-green-green-200 bg-util-colors-green-green-50 text-util-colors-green-green-700',
|
||||
deploying: 'border-util-colors-warning-warning-200 bg-util-colors-warning-warning-50 text-util-colors-warning-warning-700',
|
||||
@ -22,7 +17,10 @@ const statusKey = {
|
||||
|
||||
const baseBadge = 'inline-flex items-center gap-1 rounded-md border px-2 py-0.5 system-xs-medium whitespace-nowrap'
|
||||
|
||||
export function StatusBadge({ status, className }: StatusBadgeProps) {
|
||||
export function StatusBadge({ status, className }: {
|
||||
status: DeployStatus
|
||||
className?: string
|
||||
}) {
|
||||
const { t } = useTranslation('deployments')
|
||||
return (
|
||||
<span className={cn(baseBadge, statusStyles[status], className)}>
|
||||
@ -34,12 +32,10 @@ export function StatusBadge({ status, className }: StatusBadgeProps) {
|
||||
)
|
||||
}
|
||||
|
||||
type ModeBadgeProps = {
|
||||
export function ModeBadge({ mode, className }: {
|
||||
mode: EnvironmentMode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function ModeBadge({ mode, className }: ModeBadgeProps) {
|
||||
}) {
|
||||
const { t } = useTranslation('deployments')
|
||||
const style = mode === 'shared'
|
||||
? 'border-util-colors-green-green-200 bg-util-colors-green-green-50 text-util-colors-green-green-700'
|
||||
@ -51,12 +47,10 @@ export function ModeBadge({ mode, className }: ModeBadgeProps) {
|
||||
)
|
||||
}
|
||||
|
||||
type HealthBadgeProps = {
|
||||
export function HealthBadge({ health, className }: {
|
||||
health: EnvironmentHealth
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function HealthBadge({ health, className }: HealthBadgeProps) {
|
||||
}) {
|
||||
const { t } = useTranslation('deployments')
|
||||
const style = health === 'ready'
|
||||
? 'border-util-colors-green-green-200 bg-util-colors-green-green-50 text-util-colors-green-green-700'
|
||||
|
||||
@ -26,11 +26,9 @@ function uniqueEnvironments(environments: (ConsoleEnvironmentSummary | undefined
|
||||
})
|
||||
}
|
||||
|
||||
type AccessTabProps = {
|
||||
export function AccessTab({ instanceId: appId }: {
|
||||
instanceId: string
|
||||
}
|
||||
|
||||
export function AccessTab({ instanceId: appId }: AccessTabProps) {
|
||||
}) {
|
||||
const appInput = { params: { appInstanceId: appId } }
|
||||
const { data: accessConfig } = useQuery(consoleQuery.enterprise.appDeploy.getAppInstanceAccess.queryOptions({
|
||||
input: appInput,
|
||||
|
||||
@ -13,13 +13,11 @@ import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { environmentName } from '../../utils'
|
||||
|
||||
type ApiKeyRowProps = {
|
||||
export function ApiKeyRow({ apiKey, onCopy, onRevoke }: {
|
||||
apiKey: DeveloperAPIKeySummary
|
||||
onCopy: (apiKeyId: string) => Promise<string>
|
||||
onRevoke: () => void
|
||||
}
|
||||
|
||||
export function ApiKeyRow({ apiKey, onCopy, onRevoke }: ApiKeyRowProps) {
|
||||
}) {
|
||||
const { t } = useTranslation('deployments')
|
||||
const [copied, setCopied] = useState(false)
|
||||
const displayValue = apiKey.maskedKey || apiKey.maskedPrefix || apiKey.id || '—'
|
||||
@ -74,12 +72,10 @@ export function ApiKeyRow({ apiKey, onCopy, onRevoke }: ApiKeyRowProps) {
|
||||
)
|
||||
}
|
||||
|
||||
type ApiKeyGenerateMenuProps = {
|
||||
export function ApiKeyGenerateMenu({ environments, onGenerate }: {
|
||||
environments: ConsoleEnvironmentSummary[]
|
||||
onGenerate: (environmentId: string) => void
|
||||
}
|
||||
|
||||
export function ApiKeyGenerateMenu({ environments, onGenerate }: ApiKeyGenerateMenuProps) {
|
||||
}) {
|
||||
const { t } = useTranslation('deployments')
|
||||
const [open, setOpen] = useState(false)
|
||||
const selectableEnvironments = environments.filter(env => env.id)
|
||||
|
||||
@ -36,13 +36,11 @@ const permissionIcon: Record<AccessPermissionKind, string> = {
|
||||
|
||||
const permissionOrder: AccessPermissionKind[] = ['organization', 'specific', 'anyone']
|
||||
|
||||
type PermissionPickerProps = {
|
||||
function PermissionPicker({ value, disabled, onChange }: {
|
||||
value: AccessPermissionKind
|
||||
disabled?: boolean
|
||||
onChange: (kind: AccessPermissionKind) => void
|
||||
}
|
||||
|
||||
function PermissionPicker({ value, disabled, onChange }: PermissionPickerProps) {
|
||||
}) {
|
||||
const { t } = useTranslation('deployments')
|
||||
const icon = permissionIcon[value]
|
||||
const label = t(`access.permission.${value}`)
|
||||
@ -135,13 +133,11 @@ function selectedSubjectsFromPolicy(policy?: AccessPolicyDetail) {
|
||||
].map(normalizeSubject).filter((subject): subject is SelectableAccessSubject => Boolean(subject))
|
||||
}
|
||||
|
||||
type SubjectPillProps = {
|
||||
function SubjectPill({ subject, disabled, onRemove }: {
|
||||
subject: SelectableAccessSubject
|
||||
disabled?: boolean
|
||||
onRemove: () => void
|
||||
}
|
||||
|
||||
function SubjectPill({ subject, disabled, onRemove }: SubjectPillProps) {
|
||||
}) {
|
||||
const { t } = useTranslation('deployments')
|
||||
const isGroup = subject.subjectType === 'group'
|
||||
|
||||
|
||||
@ -32,11 +32,9 @@ import { DeploymentStatusSummary } from './deploy-tab/deployment-status-summary'
|
||||
|
||||
const GRID_TEMPLATE = 'lg:grid-cols-[minmax(180px,1fr)_minmax(140px,0.75fr)_minmax(180px,0.85fr)_240px]'
|
||||
|
||||
type DeployTabProps = {
|
||||
export function DeployTab({ instanceId: appInstanceId }: {
|
||||
instanceId: string
|
||||
}
|
||||
|
||||
export function DeployTab({ instanceId: appInstanceId }: DeployTabProps) {
|
||||
}) {
|
||||
const { t } = useTranslation('deployments')
|
||||
const { data: environmentDeployments } = useQuery(consoleQuery.enterprise.appDeploy.listRuntimeInstances.queryOptions({
|
||||
input: {
|
||||
|
||||
@ -18,12 +18,10 @@ import {
|
||||
runtimeBindingSummary,
|
||||
} from '../../utils'
|
||||
|
||||
type InfoBlockProps = {
|
||||
function InfoBlock({ title, children }: {
|
||||
title: string
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
function InfoBlock({ title, children }: InfoBlockProps) {
|
||||
}) {
|
||||
return (
|
||||
<div className="min-w-0 rounded-lg bg-background-default px-3 py-2.5">
|
||||
<div className="mb-2 system-xs-medium-uppercase text-text-tertiary">{title}</div>
|
||||
@ -51,11 +49,9 @@ function InfoRow({ label, value, mono, suffix }: InfoRowProps) {
|
||||
)
|
||||
}
|
||||
|
||||
type RuntimeBindingItemProps = {
|
||||
function RuntimeBindingItem({ binding }: {
|
||||
binding: RuntimeBindingDisplay
|
||||
}
|
||||
|
||||
function RuntimeBindingItem({ binding }: RuntimeBindingItemProps) {
|
||||
}) {
|
||||
const summary = runtimeBindingSummary(binding)
|
||||
|
||||
return (
|
||||
@ -67,11 +63,9 @@ function RuntimeBindingItem({ binding }: RuntimeBindingItemProps) {
|
||||
)
|
||||
}
|
||||
|
||||
type DeploymentPanelProps = {
|
||||
export function DeploymentPanel({ row }: {
|
||||
row: EnvironmentDeploymentRow
|
||||
}
|
||||
|
||||
export function DeploymentPanel({ row }: DeploymentPanelProps) {
|
||||
}) {
|
||||
const { t } = useTranslation('deployments')
|
||||
const observed = activeRelease(row)
|
||||
const env = row.environment
|
||||
|
||||
@ -9,11 +9,9 @@ import {
|
||||
releaseLabel,
|
||||
} from '../../utils'
|
||||
|
||||
type DeploymentStatusSummaryProps = {
|
||||
export function DeploymentStatusSummary({ row }: {
|
||||
row: EnvironmentDeploymentRow
|
||||
}
|
||||
|
||||
export function DeploymentStatusSummary({ row }: DeploymentStatusSummaryProps) {
|
||||
}) {
|
||||
const { t } = useTranslation('deployments')
|
||||
if (isUndeployedDeploymentRow(row)) {
|
||||
return (
|
||||
|
||||
@ -16,12 +16,10 @@ import { toAppInfoFromOverview } from '../utils'
|
||||
import { DeploymentSidebar } from './deployment-sidebar'
|
||||
import { isInstanceDetailTabKey } from './tabs'
|
||||
|
||||
type InstanceDetailProps = {
|
||||
export function InstanceDetail({ instanceId, children }: {
|
||||
instanceId: string
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export function InstanceDetail({ instanceId, children }: InstanceDetailProps) {
|
||||
}) {
|
||||
const { t } = useTranslation('deployments')
|
||||
const { t: tCommon } = useTranslation()
|
||||
const router = useRouter()
|
||||
|
||||
@ -17,19 +17,13 @@ import {
|
||||
webappUrl,
|
||||
} from '../utils'
|
||||
|
||||
type OverviewTabProps = {
|
||||
instanceId: string
|
||||
}
|
||||
|
||||
type SwitchableTab = 'deploy' | 'versions' | 'access' | 'settings'
|
||||
|
||||
type SectionProps = {
|
||||
function Section({ title, action, children }: {
|
||||
title: string
|
||||
action?: ReactNode
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
function Section({ title, action, children }: SectionProps) {
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-3 rounded-xl border border-components-panel-border bg-components-panel-bg p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
@ -41,13 +35,11 @@ function Section({ title, action, children }: SectionProps) {
|
||||
)
|
||||
}
|
||||
|
||||
type InfoRowProps = {
|
||||
function InfoRow({ label, value, mono }: {
|
||||
label: string
|
||||
value: ReactNode
|
||||
mono?: boolean
|
||||
}
|
||||
|
||||
function InfoRow({ label, value, mono }: InfoRowProps) {
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-start gap-3 py-1.5">
|
||||
<span className="w-32 shrink-0 system-xs-regular text-text-tertiary">{label}</span>
|
||||
@ -98,7 +90,9 @@ function overviewDeploymentStatus(status?: string) {
|
||||
return 'ready'
|
||||
}
|
||||
|
||||
export function OverviewTab({ instanceId }: OverviewTabProps) {
|
||||
export function OverviewTab({ instanceId }: {
|
||||
instanceId: string
|
||||
}) {
|
||||
const { t } = useTranslation('deployments')
|
||||
const { t: tCommon } = useTranslation()
|
||||
const router = useRouter()
|
||||
|
||||
@ -22,10 +22,6 @@ import {
|
||||
toAppInfoFromOverview,
|
||||
} from '../utils'
|
||||
|
||||
type SettingsTabProps = {
|
||||
instanceId: string
|
||||
}
|
||||
|
||||
type SettingsFormProps = {
|
||||
app: AppInfo
|
||||
settings?: GetAppInstanceSettingsReply
|
||||
@ -174,7 +170,9 @@ function SettingsForm({ app, settings, hasDeployments, onSave, onDelete }: Setti
|
||||
)
|
||||
}
|
||||
|
||||
export function SettingsTab({ instanceId }: SettingsTabProps) {
|
||||
export function SettingsTab({ instanceId }: {
|
||||
instanceId: string
|
||||
}) {
|
||||
const router = useRouter()
|
||||
const updateInstance = useMutation(consoleQuery.enterprise.appDeploy.updateAppInstance.mutationOptions())
|
||||
const deleteInstance = useMutation(consoleQuery.enterprise.appDeploy.deleteAppInstance.mutationOptions())
|
||||
|
||||
@ -23,11 +23,9 @@ import { getReleaseDeployments } from './versions-tab/release-deployments'
|
||||
|
||||
const GRID_TEMPLATE = 'grid-cols-[minmax(0,0.9fr)_minmax(0,1fr)_minmax(0,0.8fr)_minmax(0,1.5fr)_96px]'
|
||||
|
||||
type VersionsTabProps = {
|
||||
export function VersionsTab({ instanceId: appId }: {
|
||||
instanceId: string
|
||||
}
|
||||
|
||||
export function VersionsTab({ instanceId: appId }: VersionsTabProps) {
|
||||
}) {
|
||||
const { t } = useTranslation('deployments')
|
||||
const input = { params: { appInstanceId: appId } }
|
||||
const { data: overview } = useQuery(consoleQuery.enterprise.appDeploy.getAppInstanceOverview.queryOptions({
|
||||
|
||||
@ -21,12 +21,10 @@ import {
|
||||
environmentOptionsFromOptionsReply,
|
||||
} from '../../utils'
|
||||
|
||||
type DeployReleaseMenuProps = {
|
||||
export function DeployReleaseMenu({ appInstanceId, releaseId }: {
|
||||
appInstanceId: string
|
||||
releaseId: string
|
||||
}
|
||||
|
||||
export function DeployReleaseMenu({ appInstanceId, releaseId }: DeployReleaseMenuProps) {
|
||||
}) {
|
||||
const { t } = useTranslation('deployments')
|
||||
const openDeployDrawer = useDeploymentsStore(state => state.openDeployDrawer)
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
@ -11,11 +11,9 @@ const RELEASE_DEPLOYMENT_STYLES: Record<ReleaseDeploymentState, string> = {
|
||||
failed: 'border-util-colors-warning-warning-200 bg-util-colors-warning-warning-50 text-util-colors-warning-warning-700',
|
||||
}
|
||||
|
||||
type DeployedToBadgeProps = {
|
||||
export function DeployedToBadge({ item }: {
|
||||
item: ReleaseDeployment
|
||||
}
|
||||
|
||||
export function DeployedToBadge({ item }: DeployedToBadgeProps) {
|
||||
}) {
|
||||
const { t } = useTranslation('deployments')
|
||||
const statusLabel = t(`versions.deployedStatus.${item.state}`)
|
||||
|
||||
|
||||
@ -18,13 +18,11 @@ export type EnvironmentFilterOption = {
|
||||
disabledReason?: string
|
||||
}
|
||||
|
||||
type EnvironmentFilterProps = {
|
||||
export function EnvironmentFilter({ value, options, onChange }: {
|
||||
value: string
|
||||
options: EnvironmentFilterOption[]
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
export function EnvironmentFilter({ value, options, onChange }: EnvironmentFilterProps) {
|
||||
}) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const selectedOption = options.find(option => option.value === value) ?? options[0]
|
||||
|
||||
|
||||
@ -21,12 +21,10 @@ import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { useDeploymentsStore } from '../store'
|
||||
|
||||
type InstanceCardProps = {
|
||||
export function InstanceCard({ app, summary }: {
|
||||
app: AppInfo
|
||||
summary?: AppDeploymentSummary
|
||||
}
|
||||
|
||||
export function InstanceCard({ app, summary }: InstanceCardProps) {
|
||||
}) {
|
||||
const { t } = useTranslation('deployments')
|
||||
const router = useRouter()
|
||||
const { formatTimeFromNow } = useFormatTimeFromNow()
|
||||
|
||||
@ -3,10 +3,6 @@
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type NewInstanceCardProps = {
|
||||
onOpen: () => void
|
||||
}
|
||||
|
||||
type NewInstanceActionProps = {
|
||||
icon: string
|
||||
label: string
|
||||
@ -41,7 +37,9 @@ function NewInstanceAction({ icon, label, disabled, onClick }: NewInstanceAction
|
||||
)
|
||||
}
|
||||
|
||||
export function NewInstanceCard({ onOpen }: NewInstanceCardProps) {
|
||||
export function NewInstanceCard({ onOpen }: {
|
||||
onOpen: () => void
|
||||
}) {
|
||||
const { t } = useTranslation('deployments')
|
||||
return (
|
||||
<div className="relative col-span-1 inline-flex h-[160px] flex-col justify-between rounded-xl border-[0.5px] border-components-card-border bg-components-card-bg">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user