mirror of
https://github.com/supabase/supabase.git
synced 2026-10-17 07:09:07 +03:00
moved some components around
This commit is contained in:
1 parent
a695060b8e
commit
994953a4d0
11 files changed
+163
-130
No files matched your search
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
@@ -47,13 +47,17 @@ import {
|
||||
} from './ui/DiskManagement.constants'
|
||||
import { DiskManagementPlanUpgradeRequired } from './ui/DiskManagementPlanUpgradeRequired'
|
||||
import { NoticeBar } from './ui/NoticeBar'
|
||||
import { DiskMangementRestartRequiredSection } from './DiskManagementRestartRequiredSection'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { PROJECT_STATUS } from 'lib/constants'
|
||||
import { setProjectStatus } from 'data/projects/projects-query'
|
||||
|
||||
export function DiskManagementForm() {
|
||||
const showDiskAndComputeForm = useFlag('diskAndComputeForm')
|
||||
|
||||
const { project } = useProjectContext()
|
||||
const org = useSelectedOrganization()
|
||||
const { ref: projectRef } = useParams()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false)
|
||||
const [remainingTime, setRemainingTime] = useState(0)
|
||||
@@ -229,7 +233,11 @@ export function DiskManagementForm() {
|
||||
const { mutateAsync: updateSubscriptionAddon, isLoading: isUpdatingCompute } =
|
||||
useProjectAddonUpdateMutation({
|
||||
onSuccess: () => {
|
||||
setShowRestartPendingState(true)
|
||||
/**
|
||||
* Manually set project status to RESIZING
|
||||
* Project status should be RESIZING on next project status request.
|
||||
*/
|
||||
setProjectStatus(queryClient, projectRef!, PROJECT_STATUS.RESIZING)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -311,28 +319,33 @@ export function DiskManagementForm() {
|
||||
// return <></>
|
||||
|
||||
const isDirty = !!Object.keys(form.formState.dirtyFields).length
|
||||
const isProjectResizing = project?.status === PROJECT_STATUS.RESIZING
|
||||
const isProjectRequestingDiskChanges = isRequestingChanges && !isProjectResizing
|
||||
|
||||
return (
|
||||
<>
|
||||
{isProjectResizing || isProjectRequestingDiskChanges ? (
|
||||
<ScaffoldContainer className="relative flex flex-col gap-10" bottomPadding>
|
||||
<DiskMangementRestartRequiredSection
|
||||
visible={isProjectResizing}
|
||||
title="Your project will now automatically restart."
|
||||
description="Your project will be unavailable for up to 2 mins."
|
||||
/>
|
||||
<NoticeBar
|
||||
visible={isProjectRequestingDiskChanges}
|
||||
title="Disk configuration changes have been requested"
|
||||
description="The requested changes will be applied to your disk shortly"
|
||||
/>
|
||||
</ScaffoldContainer>
|
||||
) : null}
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-8">
|
||||
<ScaffoldContainer className="relative flex flex-col gap-10" bottomPadding>
|
||||
{showRestartPending ? (
|
||||
<NoticeBar
|
||||
title="Project about to restart"
|
||||
description={`This project is about to restart to change compute to ${formatComputeName(watchedComputeSize)}.`}
|
||||
/>
|
||||
) : isRequestingChanges ? (
|
||||
<NoticeBar
|
||||
title="Disk configuration changes have been requested"
|
||||
description="The requested changes will be applied to your disk shortly"
|
||||
/>
|
||||
) : null}
|
||||
<Separator />
|
||||
<ComputeSizeField form={form} />
|
||||
<Separator />
|
||||
<DiskSizeField form={form} disableInput={disableInput} />
|
||||
<DiskCountdownRadial />
|
||||
<DiskSizeField form={form} disableInput={disableInput} />
|
||||
{isPlanUpgradeRequired && <DiskManagementPlanUpgradeRequired />}
|
||||
<Separator />
|
||||
<Collapsible_Shadcn_
|
||||
|
||||
+20
-23
@@ -1,42 +1,39 @@
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { RotateCcw, X } from 'lucide-react'
|
||||
import React, { useState } from 'react'
|
||||
import { DialogSection } from 'ui'
|
||||
import { RotateCcw } from 'lucide-react'
|
||||
import React from 'react'
|
||||
import { DialogSection, WarningIcon } from 'ui'
|
||||
|
||||
export const DiskMangementRestartRequiredSection: React.FC = () => {
|
||||
const [progress, setProgress] = useState(100)
|
||||
const [showCountdown, setShowCountdown] = useState(false)
|
||||
const [isJumping, setIsJumping] = useState(false)
|
||||
const [isVisible, setIsVisible] = useState(true)
|
||||
interface Props {
|
||||
visible: boolean
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export const DiskMangementRestartRequiredSection: React.FC<Props> = ({
|
||||
visible,
|
||||
title,
|
||||
description,
|
||||
}) => {
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isVisible && (
|
||||
{visible && (
|
||||
<motion.div
|
||||
initial={{ opacity: 1, height: 'auto' }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
className="w-full"
|
||||
>
|
||||
<DialogSection className="bg-surface-100 text-sm text-foreground-light flex items-center gap-4 relative w-full border rounded-md">
|
||||
<div className="w-12 h-12 opacity-75">
|
||||
<div className="w-10 h-10 m-1 bg-studio border rounded-md flex items-center justify-center">
|
||||
<DialogSection className="bg-surface-100 text-sm text-foreground-light flex items-center gap-4 relative w-full rounded-md border-spacing-0 border">
|
||||
<div className="w-12 h-12">
|
||||
<div className="relative w-10 h-10 m-1 bg-alternative text-foreground border rounded-full flex items-center justify-center">
|
||||
<RotateCcw strokeWidth={1} />
|
||||
<WarningIcon className="absolute -right-2 -top-2" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0 flex-grow">
|
||||
<p className="text-sm text-foreground">A Project restart will be required.</p>
|
||||
<p className="text-sm text-foreground-light">
|
||||
You can restart after confirming changes or when convenient.
|
||||
</p>
|
||||
<p className="text-sm text-foreground">{title}</p>
|
||||
<p className="text-sm text-foreground-light">{description}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="text-foreground-lighter hover:text-foreground-light transition-colors self-start p-1"
|
||||
onClick={() => setIsVisible(false)}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</DialogSection>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
+21
-20
@@ -1,12 +1,14 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { ChevronRight } from 'lucide-react'
|
||||
import { UseFormReturn } from 'react-hook-form'
|
||||
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
|
||||
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
|
||||
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
|
||||
import { formatCurrency } from 'lib/helpers'
|
||||
import { ChevronRight } from 'lucide-react'
|
||||
import { useMemo } from 'react'
|
||||
import { UseFormReturn } from 'react-hook-form'
|
||||
import {
|
||||
Alert_Shadcn_,
|
||||
AlertTitle_Shadcn_,
|
||||
@@ -28,8 +30,7 @@ import {
|
||||
TableRow,
|
||||
WarningIcon,
|
||||
} from 'ui'
|
||||
import { BillingChangeBadge } from './ui/BillingChangeBadge'
|
||||
import { DiskType } from './ui/DiskManagement.constants'
|
||||
import { DiskStorageSchemaType } from './DiskManagement.schema'
|
||||
import {
|
||||
calculateComputeSizePrice,
|
||||
calculateDiskSizePrice,
|
||||
@@ -37,12 +38,10 @@ import {
|
||||
calculateThroughputPrice,
|
||||
getAvailableComputeOptions,
|
||||
} from './DiskManagement.utils'
|
||||
import { DiskMangementCoolDownSection } from './ui/DiskManagementCoolDownSection'
|
||||
import { DiskStorageSchemaType } from './DiskManagement.schema'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import { useMemo } from 'react'
|
||||
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
|
||||
import { DiskMangementRestartRequiredSection } from './DiskManagementRestartRequiredSection'
|
||||
import { BillingChangeBadge } from './ui/BillingChangeBadge'
|
||||
import { DiskType } from './ui/DiskManagement.constants'
|
||||
import { DiskMangementCoolDownSection } from './ui/DiskManagementCoolDownSection'
|
||||
|
||||
const TableHeaderRow = () => (
|
||||
<TableRow>
|
||||
@@ -249,7 +248,15 @@ export const DiskManagementReviewAndSubmitDialog = ({
|
||||
<DialogDescription>Changes will be applied shortly once confirmed.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogSectionSeparator />
|
||||
|
||||
<div className="flex flex-col gap-2 p-5">
|
||||
<DiskMangementRestartRequiredSection
|
||||
visible={form.formState.defaultValues?.computeSize !== form.getValues('computeSize')}
|
||||
title="Your project will be unavailable for up to 2 mins."
|
||||
description="Project will restart automatically on confirmation."
|
||||
/>
|
||||
{hasDiskConfigChanges && <DiskMangementCoolDownSection />}
|
||||
</div>
|
||||
<DialogSectionSeparator />
|
||||
<Table>
|
||||
<TableHeader className="font-mono uppercase text-xs [&_th]:h-auto [&_th]:pb-2 [&_th]:pt-4">
|
||||
<TableHeaderRow />
|
||||
@@ -314,23 +321,17 @@ export const DiskManagementReviewAndSubmitDialog = ({
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<div className="flex flex-col gap-2 p-5">
|
||||
{form.formState.defaultValues?.computeSize !== form.getValues('computeSize') && (
|
||||
<DiskMangementRestartRequiredSection />
|
||||
)}
|
||||
{hasDiskConfigChanges && <DiskMangementCoolDownSection />}
|
||||
</div>
|
||||
|
||||
{/* <DialogSectionSeparator /> */}
|
||||
|
||||
<DialogFooter>
|
||||
<Button block size="small" type="default" onClick={() => setIsDialogOpen(false)}>
|
||||
<Button block size="large" type="default" onClick={() => setIsDialogOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
block
|
||||
size="small"
|
||||
type={'warning'}
|
||||
size="large"
|
||||
htmlType="submit"
|
||||
loading={loading}
|
||||
onClick={async () => {
|
||||
|
||||
@@ -81,7 +81,7 @@ export function ComputeSizeField({ form }: ComputeSizeFieldProps) {
|
||||
}
|
||||
>
|
||||
<RadioGroupCard
|
||||
className="grid grid-cols-3 flex-wrap gap-3"
|
||||
className="grid grid-cols-2 xl:grid-cols-3 flex-wrap gap-3"
|
||||
onValueChange={(value: components['schemas']['AddonVariantId']) => {
|
||||
setValue('computeSize', value, {
|
||||
shouldDirty: true,
|
||||
|
||||
@@ -6,37 +6,42 @@ import CountdownTimerSpan from '../../../ui/CountdownTimer/CountdownTimerSpan'
|
||||
import { useRemainingDurationForDiskAttributeUpdate } from 'data/config/disk-attributes-query'
|
||||
import { useParams } from 'common'
|
||||
import FormMessage from './FormMessage'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export function DiskCountdownRadial() {
|
||||
const { projectRef } = useParams()
|
||||
const { ref } = useParams()
|
||||
const [remainingTime, setRemainingTime] = useState(0)
|
||||
|
||||
const {
|
||||
remainingDuration: remainingTime,
|
||||
remainingDuration: initialRemainingTime,
|
||||
isWithinCooldownWindow,
|
||||
error,
|
||||
isSuccess,
|
||||
} = useRemainingDurationForDiskAttributeUpdate({
|
||||
projectRef,
|
||||
projectRef: ref,
|
||||
})
|
||||
|
||||
const progressPercentage = (remainingTime / COOLDOWN_DURATION) * 100
|
||||
|
||||
// useEffect(() => {
|
||||
// if (initialRemainingTime > 0) setRemainingTime(initialRemainingTime)
|
||||
// }, [initialRemainingTime])
|
||||
useEffect(() => {
|
||||
if (initialRemainingTime > 0) setRemainingTime(initialRemainingTime)
|
||||
}, [initialRemainingTime])
|
||||
|
||||
// useEffect(() => {
|
||||
// if (remainingTime <= 0) return
|
||||
useEffect(() => {
|
||||
if (remainingTime <= 0) return
|
||||
|
||||
// const timer = setInterval(() => {
|
||||
// setRemainingTime(Math.max(0, remainingTime - 1))
|
||||
// }, 1000)
|
||||
const timer = setInterval(() => {
|
||||
setRemainingTime(Math.max(0, remainingTime - 1))
|
||||
}, 1000)
|
||||
|
||||
// return () => clearInterval(timer)
|
||||
// }, [remainingTime])
|
||||
return () => clearInterval(timer)
|
||||
}, [remainingTime])
|
||||
|
||||
console.log('isWithinCooldownWindow IN COMPONENT', isWithinCooldownWindow)
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{remainingTime !== 0 && isSuccess && (
|
||||
{remainingTime > 0 && isSuccess && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
@@ -44,16 +49,18 @@ export function DiskCountdownRadial() {
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<Card className="px-2 rounded bg-surface-100">
|
||||
<CardContent className="py-3 flex gap-3 px-3 items-center">
|
||||
<CardContent className="py-3 flex gap-3 px-3 items-start">
|
||||
<CountdownTimerRadial progress={progressPercentage} />
|
||||
<div className="flex flex-col">
|
||||
<p className="text-foreground text-sm p-0">
|
||||
6-hour wait period between disk modifications
|
||||
</p>
|
||||
<p className="text-foreground-lighter text-sm p-0">
|
||||
You can't modify your disk configuration until the 6-hour cool down period ends.
|
||||
You can however change the compute size..
|
||||
</p>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div>
|
||||
<p className="text-foreground text-sm p-0">
|
||||
6-hour cooldown period is in progress
|
||||
</p>
|
||||
<p className="text-foreground-lighter text-sm p-0">
|
||||
You can't modify your disk configuration again until the 6-hour cool down period
|
||||
ends.
|
||||
</p>
|
||||
</div>
|
||||
<CountdownTimerSpan seconds={remainingTime} />
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
+5
-9
@@ -1,9 +1,7 @@
|
||||
import CountdownTimerRadial from 'components/ui/CountdownTimer/CountdownTimerRadial'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { X } from 'lucide-react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
import { DialogSection, TableCell, TableRow } from 'ui'
|
||||
import CountdownTimerRadial from '../../../ui/CountdownTimer/CountdownTimerRadial'
|
||||
import { DialogSection } from 'ui'
|
||||
|
||||
export const DiskMangementCoolDownSection: React.FC = () => {
|
||||
const [progress, setProgress] = useState(100)
|
||||
@@ -39,8 +37,6 @@ export const DiskMangementCoolDownSection: React.FC = () => {
|
||||
return () => clearTimeout(initialDelay)
|
||||
}, [])
|
||||
|
||||
const MotionTableRow = motion(TableRow)
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isVisible && (
|
||||
@@ -51,7 +47,7 @@ export const DiskMangementCoolDownSection: React.FC = () => {
|
||||
className="w-full"
|
||||
>
|
||||
<DialogSection className="bg-surface-100 text-sm text-foreground-light flex items-center gap-4 relative w-full border rounded-md">
|
||||
<div className="w-12 h-12 opacity-75">
|
||||
<div className="w-12 h-12">
|
||||
<AnimatePresence>
|
||||
{showCountdown && (
|
||||
<motion.div
|
||||
@@ -82,13 +78,13 @@ export const DiskMangementCoolDownSection: React.FC = () => {
|
||||
This is due to a cooldown period between disk changes.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
{/* <button
|
||||
type="button"
|
||||
className="text-foreground-lighter hover:text-foreground-light transition-colors self-start p-1"
|
||||
onClick={() => setIsVisible(false)}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</button> */}
|
||||
</DialogSection>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
@@ -54,7 +54,7 @@ export default function DiskSpaceBar({ form }: DiskSpaceBarProps) {
|
||||
|
||||
const databaseSizeBytes = data?.result[0].db_size ?? 0
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center h-6 gap-3">
|
||||
<span className="text-foreground-light text-sm font-mono flex items-center gap-2">
|
||||
{usedSize.toFixed(2)}
|
||||
@@ -75,7 +75,7 @@ export default function DiskSpaceBar({ form }: DiskSpaceBarProps) {
|
||||
<div className="relative">
|
||||
<div
|
||||
className={cn(
|
||||
'h-[34px] relative border rounded-sm w-full transition',
|
||||
'h-[35px] relative border rounded-sm w-full transition',
|
||||
show ? 'bg-selection border border-brand-button' : 'bg-surface-300'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,30 +1,40 @@
|
||||
import { Card, CardContent } from 'ui'
|
||||
import { formatComputeName } from '../DiskManagement.utils'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { LucideIcon } from 'lucide-react'
|
||||
import { Card, CardContent } from 'ui'
|
||||
|
||||
interface NoticeBarProps {
|
||||
title?: string
|
||||
description?: string
|
||||
icon?: LucideIcon
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
export function NoticeBar({
|
||||
visible,
|
||||
title = 'Project about to restart',
|
||||
description = `This project is about to restart to change compute to.`,
|
||||
icon: Icon,
|
||||
}: NoticeBarProps) {
|
||||
return (
|
||||
<Card
|
||||
// TO DO: wrap component into pattern
|
||||
className="px-2 bg-surface-100"
|
||||
>
|
||||
<CardContent className="py-3 flex gap-3 px-3 items-center">
|
||||
{Icon && <Icon className="text-foreground-light w-4 h-4" />}
|
||||
<div className="flex flex-col">
|
||||
<p className="text-foreground text-sm p-0">{title}</p>
|
||||
<p className="text-foreground-lighter text-sm">{description}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
{visible && (
|
||||
<Card className="px-2 bg-surface-100">
|
||||
<CardContent className="py-3 flex gap-3 px-3 items-center">
|
||||
{Icon && <Icon className="text-foreground-light w-4 h-4" />}
|
||||
<div className="flex flex-col">
|
||||
<p className="text-foreground text-sm p-0">{title}</p>
|
||||
<p className="text-foreground-lighter text-sm">{description}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
@@ -24,13 +24,15 @@ export const ResizingState = () => {
|
||||
<div className="space-y-6 py-6">
|
||||
<div className="flex px-8 space-x-8">
|
||||
<div className="mt-1">
|
||||
<Loader2 className="animate-spin" size={18} />
|
||||
<Loader2 className="animate-spin text-foreground-light" size={18} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p>Resizing project...</p>
|
||||
<div className="flex flex-col gap-1">
|
||||
<p>Resizing Project Compute size</p>
|
||||
<p className="text-sm text-foreground-light">
|
||||
Your project is being restarted to apply compute size changes. It can take a few
|
||||
minutes. Your project will be offline while it is being restarted.
|
||||
Your project is being restarted to apply compute size changes.
|
||||
</p>
|
||||
<p className="text-sm text-foreground-light">
|
||||
This can take a few minutes. Project will be offline while it is being restarted.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,25 +17,27 @@ const CountdownTimerSpan = ({ seconds }: CountdownTimerSpanProps) => {
|
||||
const formatValue = (value: number) => value.toString().padStart(2, '0')
|
||||
|
||||
return (
|
||||
<span className="text-foreground text-sm font-mono">
|
||||
<MotionNumber
|
||||
format={formatConfig}
|
||||
value={formatValue(hours)}
|
||||
transition={transitionConfig}
|
||||
/>
|
||||
hr
|
||||
<MotionNumber
|
||||
format={formatConfig}
|
||||
value={formatValue(minutes)}
|
||||
transition={transitionConfig}
|
||||
/>
|
||||
m
|
||||
<MotionNumber
|
||||
format={formatConfig}
|
||||
value={formatValue(remainingSeconds)}
|
||||
transition={transitionConfig}
|
||||
/>
|
||||
s
|
||||
<span className="inline-flex gap-2">
|
||||
<span className="text-foreground-lighter text-sm p-0">Time remaining: </span>
|
||||
<span className="text-foreground text-sm font-mono flex items-center gap-0">
|
||||
<MotionNumber
|
||||
format={formatConfig}
|
||||
value={formatValue(hours)}
|
||||
transition={transitionConfig}
|
||||
/>
|
||||
hr
|
||||
<MotionNumber
|
||||
format={formatConfig}
|
||||
value={formatValue(minutes)}
|
||||
transition={transitionConfig}
|
||||
/>
|
||||
m
|
||||
<MotionNumber
|
||||
format={formatConfig}
|
||||
value={formatValue(remainingSeconds)}
|
||||
transition={transitionConfig}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user