diff --git a/apps/web/src/actions/workspaces/usage.ts b/apps/web/src/actions/workspaces/usage.ts new file mode 100644 index 000000000..1c3463e25 --- /dev/null +++ b/apps/web/src/actions/workspaces/usage.ts @@ -0,0 +1,11 @@ +'use server' + +import { computeWorkspaceUsage } from '@latitude-data/core/services/workspaces/usage' + +import { authProcedure } from '../procedures' + +export const fetchWorkspaceUsageAction = authProcedure + .createServerAction() + .handler(async ({ ctx }) => { + return await computeWorkspaceUsage(ctx.workspace).then((r) => r.unwrap()) + }) diff --git a/apps/web/src/components/layouts/AppLayout/Header/UsageIndicator/index.tsx b/apps/web/src/components/layouts/AppLayout/Header/UsageIndicator/index.tsx new file mode 100644 index 000000000..1e1103825 --- /dev/null +++ b/apps/web/src/components/layouts/AppLayout/Header/UsageIndicator/index.tsx @@ -0,0 +1,160 @@ +'use client' + +import { ReactNode, useMemo } from 'react' + +import { WorkspaceUsage } from '@latitude-data/core/browser' +import { + Badge, + Button, + CircularProgress, + CircularProgressProps, + Skeleton, + Text, +} from '@latitude-data/web-ui' +import useWorkspaceUsage from '$/stores/workspaceUsage' +import Link from 'next/link' +import Popover from 'node_modules/@latitude-data/web-ui/src/ds/atoms/Popover' + +function UsageIndicatorCircle({ + data, + isLoading, + ...props +}: Omit & { + data: WorkspaceUsage | undefined + isLoading: boolean +}) { + const ratio = useMemo(() => { + if (!data) return 0 + if (data.max === 0) return 0 + const actualRatio = (data.max - data.usage) / data.max + + if (actualRatio <= 0) return 0 + if (actualRatio >= 1) return 1 + + if (actualRatio < 0.01) return 0.01 // Too low of a ratio makes the circle so small it disappears + return actualRatio + }, [data]) + + if (isLoading) { + return ( + + ) + } + if (ratio <= 0) { + return + } + + return ( + 0.25 + ? 'primary' + : ratio === 0 + ? 'destructive' + : 'warningMutedForeground' + } + {...props} + /> + ) +} + +function LoadingText({ + isLoading, + children, +}: { + isLoading: boolean + children: ReactNode +}) { + if (isLoading) { + return + } + + return children +} + +function descriptionText({ usage, max }: WorkspaceUsage) { + const ratio = (max - usage) / max + + if (ratio <= 0) { + return "You've reached the maximum number of runs. Your app will continue working, the Latitude limit reached means you'll no longer be able to run tests, view new logs or evaluation results." + } + if (ratio < 0.25) { + return "You're running low on runs in your current plan. Your app will continue working, the Latitude limit reached means you'll no longer be able to run tests, view new logs or evaluation results." + } + + return `Your plan has included ${max} runs. You can upgrade your plan to get more runs.` +} + +export function UsageIndicator() { + const { data, isLoading } = useWorkspaceUsage() + + return ( + + + + + +
+
+ + +
+ {data?.usage} + + {' '} + / {data?.max} runs + +
+ + Team Plan + +
+
+
+
+ {data ? ( + {descriptionText(data)} + ) : ( +
+ + + +
+ )} +
+ + + +
+
+
+
+ ) +} diff --git a/apps/web/src/components/layouts/AppLayout/Header/index.tsx b/apps/web/src/components/layouts/AppLayout/Header/index.tsx index 61bcf3f40..eb2310276 100644 --- a/apps/web/src/components/layouts/AppLayout/Header/index.tsx +++ b/apps/web/src/components/layouts/AppLayout/Header/index.tsx @@ -10,6 +10,7 @@ import Link from 'next/link' import { Fragment } from 'react/jsx-runtime' import AvatarDropdown from './AvatarDropdown' +import { UsageIndicator } from './UsageIndicator' function BreadcrumbSeparator() { return ( @@ -110,7 +111,8 @@ export default function AppHeader({
-