mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
chore(studio): improve HeaderBanner (#41525)
* design polish * better banner * defensive truncation * better incident banner * better prop names * improve warnings * fix variant * OrganizationResourceBanner * notice banner * improve ClockSkewBanner * add ARIA label * rabbit * lil dot * 📝 Add docstrings to `dnywh/chore/improve-header-banner` (#41526) * 📝 Add docstrings to `dnywh/chore/improve-header-banner` Docstrings generation was requested by @dnywh. * https://github.com/supabase/supabase/pull/41525#issuecomment-3680124020 The following files were modified: * `apps/studio/hooks/misc/useOrganizationRestrictions.ts` * new line --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Danny White <3104761+dnywh@users.noreply.github.com> * fix clock docs link * Small nits * Fix URL for grace period warning to point to usage instead of billing * rabbit --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
10 files changed
+276
-243
No files matched your search
@@ -1,8 +1,9 @@
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import Link from 'next/link'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
import { useOrganizationRestrictions } from 'hooks/misc/useOrganizationRestrictions'
|
||||
import { cn, CriticalIcon, WarningIcon } from 'ui'
|
||||
import { XIcon } from 'lucide-react'
|
||||
import { Button, cn, CriticalIcon, WarningIcon } from 'ui'
|
||||
|
||||
const bannerMotionProps = {
|
||||
initial: { height: 0, opacity: 0 },
|
||||
@@ -11,104 +12,110 @@ const bannerMotionProps = {
|
||||
transition: { duration: 0.2, delay: 0.5 },
|
||||
} as const
|
||||
|
||||
const linkStyles =
|
||||
'[&_a]:underline [&_a]:underline-offset-2 [&_a]:decoration-foreground-muted/80 [&_a]:hover:decoration-foreground [&_a]:hover:text-foreground [&_a]:transition-all'
|
||||
|
||||
export const OrganizationResourceBanner = () => {
|
||||
const { warnings } = useOrganizationRestrictions()
|
||||
|
||||
return (
|
||||
<AnimatePresence initial={false}>
|
||||
{warnings.map((warning, i) => (
|
||||
<HeaderBanner key={i} {...warning} />
|
||||
{warnings.map((warning) => (
|
||||
<HeaderBanner key={`${warning.variant}-${warning.title}`} {...warning} />
|
||||
))}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
||||
export const HeaderBanner = ({
|
||||
type,
|
||||
title,
|
||||
message,
|
||||
link,
|
||||
}: {
|
||||
type: 'danger' | 'warning' | 'note' | 'incident'
|
||||
interface HeaderBannerProps {
|
||||
variant: 'danger' | 'warning' | 'note'
|
||||
title: string
|
||||
message: string
|
||||
link?: string
|
||||
}) => {
|
||||
const bannerStyles =
|
||||
type === 'danger'
|
||||
? 'bg-destructive-300 dark:bg-destructive-200'
|
||||
: type === 'incident'
|
||||
? 'bg-brand-400'
|
||||
: 'bg-warning-300 dark:bg-warning-200'
|
||||
const Icon = type === 'danger' ? CriticalIcon : WarningIcon
|
||||
description: string | ReactNode
|
||||
onDismiss?: () => void
|
||||
}
|
||||
const variantStyles = {
|
||||
danger: {
|
||||
banner: 'bg-destructive-200 border-destructive-400',
|
||||
icon: 'text-destructive-200 bg-destructive-600',
|
||||
},
|
||||
warning: {
|
||||
banner: 'bg-warning-200 border-warning-400',
|
||||
icon: 'text-warning-200 bg-warning-600',
|
||||
},
|
||||
note: {
|
||||
banner: 'bg-surface-200/25 border-default',
|
||||
icon: 'text-background bg-foreground',
|
||||
},
|
||||
} as const
|
||||
|
||||
export const HeaderBanner = ({ variant, title, description, onDismiss }: HeaderBannerProps) => {
|
||||
const { banner: bannerStyles, icon: iconStyles } = variantStyles[variant]
|
||||
const Icon = variant === 'danger' ? CriticalIcon : WarningIcon
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
{...bannerMotionProps}
|
||||
className={cn(
|
||||
`relative ${bannerStyles} border-b border-muted py-1 flex items-center justify-center flex-shrink-0 px-0`,
|
||||
type === 'incident' && 'hover:bg-brand-300',
|
||||
'flex-shrink-0'
|
||||
)}
|
||||
className={cn('relative border-b overflow-hidden', bannerStyles)}
|
||||
layout="position"
|
||||
>
|
||||
<div className={cn('items-center flex flex-row gap-3')}>
|
||||
<div className="absolute inset-y-0 left-0 right-0 overflow-hidden z-0">
|
||||
<div
|
||||
className="absolute inset-0 opacity-[0.8%]"
|
||||
style={{
|
||||
background: `repeating-linear-gradient(
|
||||
45deg,
|
||||
currentColor,
|
||||
currentColor 10px,
|
||||
transparent 10px,
|
||||
transparent 20px
|
||||
)`,
|
||||
maskImage: 'linear-gradient(to top, black, transparent)',
|
||||
WebkitMaskImage: 'linear-gradient(to top, black, transparent)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Icon
|
||||
className={cn('z-[1] flex-shrink-0', type === 'incident' && 'bg-brand text-brand-200')}
|
||||
{/* Wrapped content for smooth reveal animation */}
|
||||
<div className="px-4 py-4 md:py-3 flex items-center md:justify-center">
|
||||
{/* Striped background */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-[1.6%] dark:opacity-[0.8%]"
|
||||
style={{
|
||||
background: `repeating-linear-gradient(
|
||||
45deg,
|
||||
currentColor,
|
||||
currentColor 10px,
|
||||
transparent 10px,
|
||||
transparent 20px
|
||||
)`,
|
||||
maskImage: 'linear-gradient(to bottom, transparent 0%, black 90%)',
|
||||
WebkitMaskImage: 'linear-gradient(to bottom, transparent 0%, black 90%)',
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col md:flex-row gap-0 md:gap-3">
|
||||
<span
|
||||
{/* Icon and text content */}
|
||||
<div
|
||||
className={cn(
|
||||
'relative items-start md:items-center flex flex-row gap-3 min-w-0',
|
||||
// Avoid collision with the dismiss button
|
||||
onDismiss && 'pr-7 md:px-7'
|
||||
)}
|
||||
>
|
||||
<Icon className={cn('flex-shrink-0 w-5 h-5 md:w-4 md:h-4', iconStyles)} />
|
||||
{/* Title and description */}
|
||||
<div
|
||||
className={cn(
|
||||
'text-xs sm:text-sm z-[1]',
|
||||
type === 'danger'
|
||||
? 'text-destructive'
|
||||
: type === 'incident'
|
||||
? 'text-foreground'
|
||||
: 'text-warning'
|
||||
'flex flex-col md:flex-row gap-0.5 md:gap-1.5 text-balance md:flex-nowrap min-w-0 flex-1'
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
'text-xs sm:text-sm z-[1] opacity-75',
|
||||
type === 'danger'
|
||||
? 'text-destructive'
|
||||
: type === 'incident'
|
||||
? 'text-foreground'
|
||||
: 'text-warning'
|
||||
)}
|
||||
>
|
||||
{message}
|
||||
</span>
|
||||
{/* Title */}
|
||||
<p className="text-sm text-foreground font-medium md:truncate">{title}</p>
|
||||
{/* Description */}
|
||||
<div className="flex flex-row items-center gap-1.5 min-w-0 md:flex-nowrap text-sm">
|
||||
<span className="hidden md:inline text-foreground-muted">·</span>
|
||||
{typeof description === 'string' ? (
|
||||
<p className="text-foreground-light md:truncate">{description}</p>
|
||||
) : (
|
||||
<div className={cn('text-foreground-light md:truncate', linkStyles)}>
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{link && (
|
||||
<Link
|
||||
href={link}
|
||||
className={cn(
|
||||
'lg:block hidden',
|
||||
'text-foreground-lighter text-sm z-[1] m-0',
|
||||
type === 'danger' ? 'text-destructive' : 'text-warning'
|
||||
)}
|
||||
|
||||
{onDismiss && (
|
||||
<Button
|
||||
type="text"
|
||||
size="tiny"
|
||||
className="opacity-75 z-[1] flex-shrink-0 p-0.5 h-auto absolute right-5 md:right-4 top-1/2 -translate-y-1/2"
|
||||
onClick={onDismiss}
|
||||
aria-label="Dismiss banner"
|
||||
>
|
||||
View Details
|
||||
</Link>
|
||||
<XIcon size={16} className="text-foreground" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
// Add at the top of the file, with other imports
|
||||
export const RESTRICTION_MESSAGES = {
|
||||
GRACE_PERIOD: {
|
||||
title: 'Organization plan has exceeded its quota',
|
||||
message: (date: string) => `You are given a grace period until ${date}`,
|
||||
},
|
||||
GRACE_PERIOD_OVER: {
|
||||
title: 'Grace period is over',
|
||||
message: 'Your Projects will not be able to serve requests when you used up your quota.',
|
||||
},
|
||||
RESTRICTED: {
|
||||
title: 'Services Restricted',
|
||||
message:
|
||||
'Your Projects are unable to serve any requests as your Organization plan has used up its quota.',
|
||||
},
|
||||
OVERDUE_INVOICES: {
|
||||
title: 'Outstanding Invoices',
|
||||
message: 'Please pay invoices to avoid service disruption.',
|
||||
},
|
||||
OVERDUE_INVOICES_FROM_OTHER_ORGS: {
|
||||
title: 'Outstanding Invoices in other Organization',
|
||||
message: 'Please pay invoices for other Organization to avoid service disruption.',
|
||||
},
|
||||
MISSING_BILLING_INFO: {
|
||||
title: 'Missing Billing Information',
|
||||
message:
|
||||
'Please add a billing address to avoid restrictions. If you are a registered business, please add a tax ID too.',
|
||||
},
|
||||
} as const
|
||||
@@ -0,0 +1,60 @@
|
||||
import { type ReactNode } from 'react'
|
||||
|
||||
import { InlineLink } from 'components/ui/InlineLink'
|
||||
|
||||
export const RESTRICTION_MESSAGES = {
|
||||
GRACE_PERIOD: {
|
||||
title: 'Organization plan has exceeded its quota',
|
||||
description: (date: string, slug: string): ReactNode => (
|
||||
<>
|
||||
You have been given a grace period until {date}.{' '}
|
||||
<InlineLink href={`/org/${slug}/usage`}>Review usage</InlineLink>
|
||||
</>
|
||||
),
|
||||
},
|
||||
GRACE_PERIOD_OVER: {
|
||||
title: 'Grace period is over',
|
||||
description: (slug: string): ReactNode => (
|
||||
<>
|
||||
Your projects will not be able to serve requests when you use up your quota.{' '}
|
||||
<InlineLink href={`/org/${slug}/billing`}>Review billing</InlineLink>
|
||||
</>
|
||||
),
|
||||
},
|
||||
RESTRICTED: {
|
||||
title: 'Services restricted',
|
||||
description: (slug: string): ReactNode => (
|
||||
<>
|
||||
Your projects are unable to serve requests as your organization has used up its quota.{' '}
|
||||
<InlineLink href={`/org/${slug}/billing`}>Resolve billing issues</InlineLink>
|
||||
</>
|
||||
),
|
||||
},
|
||||
OVERDUE_INVOICES: {
|
||||
title: 'Outstanding invoices',
|
||||
description: (slug: string): ReactNode => (
|
||||
<>
|
||||
Please <InlineLink href={`/org/${slug}/billing#invoices`}>pay your invoices</InlineLink> to
|
||||
avoid service disruption
|
||||
</>
|
||||
),
|
||||
},
|
||||
OVERDUE_INVOICES_FROM_OTHER_ORGS: {
|
||||
title: 'Outstanding invoices in other organization',
|
||||
description: (slug: string): ReactNode => (
|
||||
<>
|
||||
Please <InlineLink href={`/org/${slug}/billing#invoices`}>pay invoices</InlineLink> for
|
||||
other organizations to avoid service disruption
|
||||
</>
|
||||
),
|
||||
},
|
||||
MISSING_BILLING_INFO: {
|
||||
title: 'Missing billing information',
|
||||
description: (slug: string): ReactNode => (
|
||||
<>
|
||||
Please <InlineLink href={`/org/${slug}/billing#address`}>add a billing address</InlineLink>{' '}
|
||||
to avoid restrictions. If you are a registered business, please add a tax ID too
|
||||
</>
|
||||
),
|
||||
},
|
||||
}
|
||||
@@ -11,7 +11,7 @@ interface IncidentAdmonitionProps {
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
const STATUS_DESCRIPTION_SIGN_OFF = 'Please follow the status page for updates.'
|
||||
const STATUS_DESCRIPTION_SIGN_OFF = 'Follow the status page for updates.'
|
||||
|
||||
const capitalizeFirstLetter = (str: string) => str.charAt(0).toUpperCase() + str.slice(1)
|
||||
|
||||
|
||||
@@ -1,56 +1,32 @@
|
||||
import { fetchHandler } from 'data/fetchers'
|
||||
import { BASE_PATH, DOCS_URL } from 'lib/constants'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Button } from 'ui'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
// Show the banner if the clock skew is greater than 2 minutes
|
||||
const CLOCK_SKEW_THRESHOLD = 2 * 60 * 1000
|
||||
// check every 5 minutes
|
||||
const CLOCK_SKEW_CHECK_INTERVAL = 30 * 60 * 1000
|
||||
|
||||
const isClockSkewed = async () => {
|
||||
try {
|
||||
const response = await fetchHandler(`${BASE_PATH}/api/get-utc-time`)
|
||||
const data = await response.json()
|
||||
// The received time is in UTC timezone, add Z at the end to make JS understand that
|
||||
const serverTime = new Date(data.utcTime).getTime()
|
||||
const clientTime = new Date().getTime()
|
||||
const clockSkew = Math.abs(clientTime - serverTime)
|
||||
|
||||
return clockSkew > CLOCK_SKEW_THRESHOLD
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
import { HeaderBanner } from 'components/interfaces/Organization/HeaderBanner'
|
||||
import { InlineLink } from 'components/ui/InlineLink'
|
||||
import { useClockSkewQuery } from 'data/misc/clock-skew-query'
|
||||
import { DOCS_URL } from 'lib/constants'
|
||||
|
||||
export const ClockSkewBanner = () => {
|
||||
const [clockSkew, setClockSkew] = useState(false)
|
||||
const [isClockSkewed, setIsClockSkewed] = useState(false)
|
||||
|
||||
const checkClockSkew = useCallback(async () => {
|
||||
const value = await isClockSkewed()
|
||||
setClockSkew(value)
|
||||
}, [])
|
||||
const { data } = useClockSkewQuery()
|
||||
useEffect(() => setIsClockSkewed(!!data), [data])
|
||||
|
||||
useEffect(() => {
|
||||
// check for clock skew every CLOCK_SKEW_CHECK_INTERVAL
|
||||
checkClockSkew()
|
||||
const interval = setInterval(checkClockSkew, CLOCK_SKEW_CHECK_INTERVAL)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [checkClockSkew])
|
||||
|
||||
if (!clockSkew) return null
|
||||
if (!isClockSkewed) return null
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-x-4 bg-destructive-400 py-0.5 transition text-foreground box-border border-b border-default">
|
||||
<p className="text-sm">
|
||||
Your computer's clock appears to be inaccurate. This can cause issues with certain features.
|
||||
</p>
|
||||
<Button asChild type="text">
|
||||
<a href={`${DOCS_URL}`} target="_blank">
|
||||
More information
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
<HeaderBanner
|
||||
variant="warning"
|
||||
title="Your computer’s clock appears to be inaccurate"
|
||||
description={
|
||||
<>
|
||||
This can cause issues with certain features.{' '}
|
||||
<InlineLink
|
||||
href={`${DOCS_URL}/guides/troubleshooting/jwt-expired-error-in-supabase-dashboard-F06k3x`}
|
||||
>
|
||||
Learn more
|
||||
</InlineLink>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,15 +1,20 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
import { HeaderBanner } from 'components/interfaces/Organization/HeaderBanner'
|
||||
import { InlineLink } from 'components/ui/InlineLink'
|
||||
|
||||
/**
|
||||
* Used to display ongoing incidents
|
||||
*/
|
||||
export const IncidentBanner = () => {
|
||||
return (
|
||||
<Link href="https://status.supabase.com" target="_blank" rel="noopener noreferrer">
|
||||
<HeaderBanner
|
||||
type="incident"
|
||||
title="We are currently investigating a technical issue"
|
||||
message="Follow status.supabase.com for updates"
|
||||
/>
|
||||
</Link>
|
||||
<HeaderBanner
|
||||
variant="note"
|
||||
title="We are investigating a technical issue"
|
||||
description={
|
||||
<>
|
||||
Follow the <InlineLink href="https://status.supabase.com">status page</InlineLink> for
|
||||
updates
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,71 +1,35 @@
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { useAppBannerContext } from 'components/interfaces/App/AppBannerWrapperContext'
|
||||
import { Button, WarningIcon, cn } from 'ui'
|
||||
|
||||
// This file, like AppBannerWrapperContext.tsx, is meant to be dynamic - update this as and when we need to use the NoticeBanner
|
||||
// We can disable this banner after 23rd November 2025 as the maintenance window is complete
|
||||
import { HeaderBanner } from 'components/interfaces/Organization/HeaderBanner'
|
||||
import { InlineLink } from 'components/ui/InlineLink'
|
||||
|
||||
/**
|
||||
* Used to display urgent notices that apply for all users, such as maintenance windows.
|
||||
* This file, like AppBannerWrapperContext.tsx, is meant to be dynamic.
|
||||
* Update this as and when we need to use the NoticeBanner.
|
||||
*/
|
||||
export const NoticeBanner = () => {
|
||||
const router = useRouter()
|
||||
const { maintenanceWindowBannerAcknowledged, onUpdateAcknowledged } = useAppBannerContext()
|
||||
|
||||
const appBannerContext = useAppBannerContext()
|
||||
const { maintenanceWindowBannerAcknowledged, onUpdateAcknowledged } = appBannerContext
|
||||
|
||||
const acknowledged = maintenanceWindowBannerAcknowledged
|
||||
|
||||
if (router.pathname.includes('sign-in') || acknowledged) {
|
||||
if (router.pathname.includes('sign-in') || maintenanceWindowBannerAcknowledged) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'relative bg-warning-300 dark:bg-warning-200 border-b border-muted py-1 flex items-center justify-center flex-shrink-0 px-0'
|
||||
)}
|
||||
>
|
||||
<div className="absolute inset-y-0 left-0 right-0 overflow-hidden z-0">
|
||||
<div
|
||||
className="absolute inset-0 opacity-[0.8%]"
|
||||
style={{
|
||||
background: `repeating-linear-gradient(
|
||||
45deg,
|
||||
currentColor,
|
||||
currentColor 10px,
|
||||
transparent 10px,
|
||||
transparent 20px
|
||||
)`,
|
||||
maskImage: 'linear-gradient(to top, black, transparent)',
|
||||
WebkitMaskImage: 'linear-gradient(to top, black, transparent)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="items-center flex flex-row gap-3 z-[1]">
|
||||
<WarningIcon className="z-[1] flex-shrink-0" />
|
||||
<div className="flex-1 text-xs sm:text-sm z-[1] text-warning">
|
||||
Urgent Dashboard and Management API maintenance between 23:00 UTC on Nov 21, 2025 and
|
||||
23:00 UTC on Nov 23, 2025. For full details,{' '}
|
||||
<Link
|
||||
href="https://status.supabase.com/incidents/z0l2157y33xk"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="opacity-75 hover:opacity-100 underline"
|
||||
>
|
||||
check here
|
||||
</Link>
|
||||
.
|
||||
</div>
|
||||
<Button
|
||||
type="text"
|
||||
className="opacity-75 z-[1] flex-shrink-0"
|
||||
onClick={() => {
|
||||
onUpdateAcknowledged('maintenance-window-banner-2025-11-21')
|
||||
}}
|
||||
>
|
||||
Dismiss
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<HeaderBanner
|
||||
variant="warning"
|
||||
title="Urgent dashboard and management API maintenance"
|
||||
description={
|
||||
<>
|
||||
23:00 UTC Nov 21–23, 2025.{' '}
|
||||
<InlineLink href="https://status.supabase.com/incidents/z0l2157y33xk">
|
||||
Learn more
|
||||
</InlineLink>
|
||||
</>
|
||||
}
|
||||
onDismiss={() => onUpdateAcknowledged('maintenance-window-banner-2025-11-21')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
import { fetchHandler } from 'data/fetchers'
|
||||
import { BASE_PATH, IS_PLATFORM } from 'lib/constants'
|
||||
import type { ResponseError, UseCustomQueryOptions } from 'types'
|
||||
import { miscKeys } from './keys'
|
||||
|
||||
// Warn if the clock skew is greater than 2 minutes
|
||||
const CLOCK_SKEW_THRESHOLD = 2 * 60 * 1000
|
||||
// Check every 30 minutes
|
||||
const CLOCK_SKEW_CHECK_INTERVAL = 30 * 60 * 1000
|
||||
|
||||
export async function getClockSkew() {
|
||||
try {
|
||||
const data = await fetchHandler(`${BASE_PATH}/api/get-utc-time`).then((res) => res.json())
|
||||
const serverTime = new Date(data.utcTime).getTime()
|
||||
const clientTime = new Date().getTime()
|
||||
const clockSkew = Math.abs(clientTime - serverTime)
|
||||
return clockSkew > CLOCK_SKEW_THRESHOLD
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export type ClockSkewData = Awaited<ReturnType<typeof getClockSkew>>
|
||||
export type ClockSkewError = ResponseError
|
||||
|
||||
export const useClockSkewQuery = <TData = ClockSkewData>({
|
||||
enabled = true,
|
||||
refetchInterval = CLOCK_SKEW_CHECK_INTERVAL,
|
||||
...options
|
||||
}: UseCustomQueryOptions<ClockSkewData, ClockSkewError, TData> = {}) =>
|
||||
useQuery<ClockSkewData, ClockSkewError, TData>({
|
||||
queryKey: miscKeys.clockSkew(),
|
||||
queryFn: () => getClockSkew(),
|
||||
enabled: enabled && IS_PLATFORM,
|
||||
refetchInterval,
|
||||
...options,
|
||||
})
|
||||
@@ -3,4 +3,5 @@ export const miscKeys = {
|
||||
defaultRegion: (cloudProvider: string | undefined, useRestrictedPool: boolean) =>
|
||||
['defaultRegion', [cloudProvider, useRestrictedPool]] as const,
|
||||
ipAddress: () => ['ip-address'] as const,
|
||||
clockSkew: () => ['clock-skew'] as const,
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import dayjs from 'dayjs'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
import { RESTRICTION_MESSAGES } from 'components/interfaces/Organization/restriction.constants'
|
||||
import { useOverdueInvoicesQuery } from 'data/invoices/invoices-overdue-query'
|
||||
@@ -7,12 +8,21 @@ import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization
|
||||
import { useIsFeatureEnabled } from './useIsFeatureEnabled'
|
||||
|
||||
export type WarningBannerProps = {
|
||||
type: 'danger' | 'warning' | 'note'
|
||||
variant: 'danger' | 'warning' | 'note'
|
||||
title: string
|
||||
message: string
|
||||
link: string
|
||||
description: ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute billing-related restriction banners for the currently selected organization.
|
||||
*
|
||||
* The hook examines billing feature availability, overdue invoices, organization billing data,
|
||||
* and restriction status to produce an ordered list of warning/danger banner props.
|
||||
*
|
||||
* @returns An object containing:
|
||||
* - `warnings`: an array of `WarningBannerProps` to display for the selected organization (may be empty).
|
||||
* - `org`: the selected organization data (may be `undefined`).
|
||||
*/
|
||||
export function useOrganizationRestrictions() {
|
||||
const { data: org } = useSelectedOrganizationQuery()
|
||||
|
||||
@@ -35,57 +45,57 @@ export function useOrganizationRestrictions() {
|
||||
|
||||
if (org && org.organization_missing_address && !org.billing_partner) {
|
||||
warnings.push({
|
||||
type: 'danger',
|
||||
variant: 'danger',
|
||||
title: RESTRICTION_MESSAGES.MISSING_BILLING_INFO.title,
|
||||
message: RESTRICTION_MESSAGES.MISSING_BILLING_INFO.message,
|
||||
link: `/org/${org?.slug}/billing#address`,
|
||||
description: RESTRICTION_MESSAGES.MISSING_BILLING_INFO.description(org.slug),
|
||||
})
|
||||
}
|
||||
|
||||
if (thisOrgHasOverdueInvoices?.length) {
|
||||
warnings.push({
|
||||
type: 'danger',
|
||||
variant: 'danger',
|
||||
title: RESTRICTION_MESSAGES.OVERDUE_INVOICES.title,
|
||||
message: RESTRICTION_MESSAGES.OVERDUE_INVOICES.message,
|
||||
link: `/org/${org?.slug}/billing#invoices`,
|
||||
description: RESTRICTION_MESSAGES.OVERDUE_INVOICES.description(org?.slug ?? 'default'),
|
||||
})
|
||||
}
|
||||
|
||||
if (overdueInvoicesFromOtherOrgs?.length) {
|
||||
const otherOrgSlug = organizations?.find(
|
||||
(o) => o.id === overdueInvoicesFromOtherOrgs[0].organization_id
|
||||
)?.slug
|
||||
warnings.push({
|
||||
type: 'danger',
|
||||
variant: 'danger',
|
||||
title: RESTRICTION_MESSAGES.OVERDUE_INVOICES_FROM_OTHER_ORGS.title,
|
||||
message: RESTRICTION_MESSAGES.OVERDUE_INVOICES_FROM_OTHER_ORGS.message,
|
||||
link: `/org/${organizations ? organizations?.find((org) => org.id === overdueInvoicesFromOtherOrgs[0].organization_id)?.slug : org?.slug}/billing#invoices`,
|
||||
description: RESTRICTION_MESSAGES.OVERDUE_INVOICES_FROM_OTHER_ORGS.description(
|
||||
otherOrgSlug ?? org?.slug ?? 'default'
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
if (org?.restriction_status === 'grace_period') {
|
||||
warnings.push({
|
||||
type: 'warning',
|
||||
variant: 'warning',
|
||||
title: RESTRICTION_MESSAGES.GRACE_PERIOD.title,
|
||||
message: RESTRICTION_MESSAGES.GRACE_PERIOD.message(
|
||||
dayjs(org?.restriction_data?.['grace_period_end']).format('DD MMM, YYYY')
|
||||
description: RESTRICTION_MESSAGES.GRACE_PERIOD.description(
|
||||
dayjs(org?.restriction_data?.['grace_period_end']).format('DD MMM, YYYY'),
|
||||
org.slug
|
||||
),
|
||||
link: `/org/${org?.slug}/billing`,
|
||||
})
|
||||
}
|
||||
|
||||
if (org?.restriction_status === 'grace_period_over') {
|
||||
warnings.push({
|
||||
type: 'warning',
|
||||
variant: 'warning',
|
||||
title: RESTRICTION_MESSAGES.GRACE_PERIOD_OVER.title,
|
||||
message: RESTRICTION_MESSAGES.GRACE_PERIOD_OVER.message,
|
||||
link: `/org/${org?.slug}/billing`,
|
||||
description: RESTRICTION_MESSAGES.GRACE_PERIOD_OVER.description(org.slug),
|
||||
})
|
||||
}
|
||||
|
||||
if (org?.restriction_status === 'restricted') {
|
||||
warnings.push({
|
||||
type: 'danger',
|
||||
variant: 'danger',
|
||||
title: RESTRICTION_MESSAGES.RESTRICTED.title,
|
||||
message: RESTRICTION_MESSAGES.RESTRICTED.message,
|
||||
link: `/org/${org?.slug}/billing`,
|
||||
description: RESTRICTION_MESSAGES.RESTRICTED.description(org.slug),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user