Skip to content

Commit

Permalink
feat(ui): display alerts for invalid licenses (#3351)
Browse files Browse the repository at this point in the history
* feat(ui): display alerts for invalid licenses

* update

* [autofix.ci] apply automated fixes

* update: seats exceeded

* update: license banner

* [autofix.ci] apply automated fixes

* Update ee/tabby-ui/lib/hooks/use-license.ts

* Apply suggestions from code review

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Meng Zhang <[email protected]>
  • Loading branch information
3 people authored Nov 4, 2024
1 parent 48419e6 commit 44d65fb
Show file tree
Hide file tree
Showing 10 changed files with 327 additions and 47 deletions.
13 changes: 10 additions & 3 deletions ee/tabby-ui/app/(dashboard)/components/main-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,24 @@
import { ScrollArea } from '@/components/ui/scroll-area'
import { BANNER_HEIGHT, useShowDemoBanner } from '@/components/demo-banner'
import { Header } from '@/components/header'
import { useShowLicenseBanner } from '@/components/license-banner'

export default function MainContent({
children
}: {
children: React.ReactNode
}) {
const [isShowDemoBanner] = useShowDemoBanner()
const [isShowLicenseBanner] = useShowLicenseBanner()
const style =
isShowDemoBanner || isShowLicenseBanner
? {
height: `calc(100vh - ${
isShowDemoBanner ? BANNER_HEIGHT : '0rem'
} - ${isShowLicenseBanner ? BANNER_HEIGHT : '0rem'})`
}
: { height: '100vh' }

const style = isShowDemoBanner
? { height: `calc(100vh - ${BANNER_HEIGHT})` }
: { height: '100vh' }
return (
<>
{/* Wraps right hand side into ScrollArea, making scroll bar consistent across all browsers */}
Expand Down
13 changes: 10 additions & 3 deletions ee/tabby-ui/app/(dashboard)/components/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
} from '@/components/ui/icons'
import { ScrollArea } from '@/components/ui/scroll-area'
import { BANNER_HEIGHT, useShowDemoBanner } from '@/components/demo-banner'
import { useShowLicenseBanner } from '@/components/license-banner'
import LoadingWrapper from '@/components/loading-wrapper'

export interface SidebarProps {
Expand Down Expand Up @@ -124,10 +125,16 @@ const menus: Menu[] = [

export default function Sidebar({ children, className }: SidebarProps) {
const [{ data, fetching: fetchingMe }] = useMe()
const [isShowDemoBanner] = useShowDemoBanner()
const isAdmin = data?.me.isAdmin
const style = isShowDemoBanner
? { height: `calc(100vh - ${BANNER_HEIGHT})` }
const [isShowDemoBanner] = useShowDemoBanner()
const [isShowLicenseBanner] = useShowLicenseBanner()
const showBanner = isShowDemoBanner || isShowLicenseBanner
const style = showBanner
? {
height: `calc(100vh - ${isShowDemoBanner ? BANNER_HEIGHT : '0rem'} - ${
isShowLicenseBanner ? BANNER_HEIGHT : '0rem'
})`
}
: { height: '100vh' }

return (
Expand Down
13 changes: 9 additions & 4 deletions ee/tabby-ui/app/(dashboard)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Metadata } from 'next'

import { LicenseBanner } from '@/components/license-banner'

import MainContent from './components/main-content'
import Sidebar from './components/sidebar'

Expand All @@ -16,9 +18,12 @@ export default function RootLayout({
children: React.ReactNode
}) {
return (
<main className="flex flex-1">
<Sidebar />
<MainContent>{children}</MainContent>
</main>
<>
<LicenseBanner />
<main className="flex flex-1">
<Sidebar />
<MainContent>{children}</MainContent>
</main>
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { cva } from 'class-variance-authority'

import { cn } from '@/lib/utils'
import { Badge } from '@/components/ui/badge'
import { BANNER_HEIGHT, useShowDemoBanner } from '@/components/demo-banner'
import { useShowDemoBanner } from '@/components/demo-banner'
import { useShowLicenseBanner } from '@/components/license-banner'

import { PROVIDER_KIND_METAS } from '../constants'

Expand All @@ -33,9 +34,12 @@ interface SidebarButtonProps {

export default function NavBar({ className }: { className?: string }) {
const [isShowDemoBanner] = useShowDemoBanner()

const style = isShowDemoBanner
? { height: `calc(100vh - ${BANNER_HEIGHT} - 4rem)` }
const [isShowLicenseBanner] = useShowLicenseBanner()
const showBanner = isShowDemoBanner || isShowLicenseBanner
const bannerHeight =
isShowDemoBanner && isShowLicenseBanner ? '7rem' : '3.5rem'
const style = showBanner
? { height: `calc(100vh - ${bannerHeight} - 4rem)` }
: { height: 'calc(100vh - 4rem)' }

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import { capitalize } from 'lodash-es'
import moment from 'moment'

import { LicenseInfo, LicenseType } from '@/lib/gql/generates/graphql'
import { useLicense } from '@/lib/hooks/use-license'
import { useLicense, useLicenseValidity } from '@/lib/hooks/use-license'
import useRouterStuff from '@/lib/hooks/use-router-stuff'
import { Badge } from '@/components/ui/badge'
import { IconAlertTriangle } from '@/components/ui/icons'
import { Skeleton } from '@/components/ui/skeleton'
import LoadingWrapper from '@/components/loading-wrapper'
import { SubHeader } from '@/components/sub-header'
Expand All @@ -13,9 +16,17 @@ import { LicenseForm } from './license-form'
import { LicenseTable } from './license-table'

export default function Subscription() {
const { updateUrlComponents } = useRouterStuff()
const [{ data, fetching }, reexecuteQuery] = useLicense()
const license = data?.license
const onUploadLicenseSuccess = () => {
// FIXME for testing
updateUrlComponents({
searchParams: {
del: ['licenseExpired', 'seatsExceeded']
}
})

reexecuteQuery()
}
const canReset = !!license?.type && license.type !== LicenseType.Community
Expand Down Expand Up @@ -50,6 +61,7 @@ export default function Subscription() {
}

function License({ license }: { license: LicenseInfo }) {
const { isExpired, isSeatsExceeded } = useLicenseValidity()
const expiresAt = license.expiresAt
? moment(license.expiresAt).format('MM/DD/YYYY')
: '–'
Expand All @@ -60,11 +72,27 @@ function License({ license }: { license: LicenseInfo }) {
<div className="grid font-bold lg:grid-cols-3">
<div>
<div className="mb-1 text-muted-foreground">Expires at</div>
<div className="text-3xl">{expiresAt}</div>
<div className="flex items-center gap-2 text-3xl">
{expiresAt}
{isExpired && (
<Badge variant="destructive" className="flex items-center gap-1">
<IconAlertTriangle className="h-3 w-3" />
Expired
</Badge>
)}
</div>
</div>
<div>
<div className="mb-1 text-muted-foreground">Assigned / Total Seats</div>
<div className="text-3xl">{seatsText}</div>
<div className="flex items-center gap-2 text-3xl">
{seatsText}
{isSeatsExceeded && (
<Badge variant="destructive" className="flex items-center gap-1">
<IconAlertTriangle className="h-3 w-3" />
Seats exceeded
</Badge>
)}
</div>
</div>
<div>
<div className="mb-1 text-muted-foreground">Current plan</div>
Expand Down
2 changes: 1 addition & 1 deletion ee/tabby-ui/components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function Header() {

return (
<header className="sticky top-0 z-50 flex h-16 w-full shrink-0 items-center justify-between border-b px-4 backdrop-blur-xl lg:px-10">
<div className="flex items-center">
<div className="flex items-center gap-4">
{newVersionAvailable && (
<a
target="_blank"
Expand Down
115 changes: 115 additions & 0 deletions ee/tabby-ui/components/license-banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
'use client'

import React, { useMemo } from 'react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'

import { useLicenseValidity } from '@/lib/hooks/use-license'
import { cn } from '@/lib/utils'
import { IconClose, IconNotice } from '@/components/ui/icons'

import { buttonVariants } from './ui/button'

export const BANNER_HEIGHT = '3.5rem'

interface ShowLicenseBannerContextValue {
isShowLicenseBanner: boolean
setIsShowLicenseBanner: React.Dispatch<React.SetStateAction<boolean>>
}

const ShowLicenseBannerContext =
React.createContext<ShowLicenseBannerContextValue>(
{} as ShowLicenseBannerContextValue
)

export const ShowLicenseBannerProvider = ({
children
}: {
children: React.ReactNode
}) => {
const { isExpired, isSeatsExceeded, isLicenseOK } = useLicenseValidity()

const [isShowLicenseBanner, setIsShowLicenseBanner] = React.useState(false)

React.useEffect(() => {
const isInIframe = window.self !== window.top
if (isInIframe) return

if (isExpired || isSeatsExceeded) {
setIsShowLicenseBanner(true)
} else if (isLicenseOK) {
setIsShowLicenseBanner(false)
}
}, [isLicenseOK, isExpired, isSeatsExceeded])

return (
<ShowLicenseBannerContext.Provider
value={{ isShowLicenseBanner, setIsShowLicenseBanner }}
>
{children}
</ShowLicenseBannerContext.Provider>
)
}

export function useShowLicenseBanner(): [
boolean,
React.Dispatch<React.SetStateAction<boolean>>
] {
const { isShowLicenseBanner, setIsShowLicenseBanner } = React.useContext(
ShowLicenseBannerContext
)
return [isShowLicenseBanner, setIsShowLicenseBanner]
}

export function LicenseBanner() {
const [isShowLicenseBanner, setIsShowLicenseBanner] = useShowLicenseBanner()
const { isExpired, isSeatsExceeded } = useLicenseValidity()
const pathname = usePathname()
const style = isShowLicenseBanner ? { height: BANNER_HEIGHT } : { height: 0 }

const tips = useMemo(() => {
if (isExpired) {
return 'Your subscription is expired.'
}

if (isSeatsExceeded) {
return 'You have more active users than seats included in your subscription.'
}

return 'No valid license configured'
}, [isExpired, isSeatsExceeded])

return (
<div
className={cn(
'flex items-center justify-between border-b bg-secondary px-4 text-secondary-foreground transition-all md:px-5',
{
'opacity-100 pointer-events-auto': isShowLicenseBanner,
'opacity-0 pointer-events-none': !isShowLicenseBanner
}
)}
style={style}
>
<div className="flex items-center gap-1 font-semibold text-destructive">
<IconNotice />
{tips}
</div>

<div className="flex items-center gap-x-4 md:gap-x-8">
{pathname !== '/settings/subscription' && (
<Link
href="/settings/subscription"
className={cn(buttonVariants(), 'gap-1')}
>
See more
</Link>
)}

<IconClose
className="cursor-pointer transition-all hover:opacity-70"
onClick={() => setIsShowLicenseBanner(false)}
/>
</div>
</div>
)
}
Loading

0 comments on commit 44d65fb

Please sign in to comment.