ui - add team indicator to ui

This commit is contained in:
Ishaan Jaff 2025-06-21 14:55:09 -07:00
parent b7843b9113
commit 045026462f
2 changed files with 226 additions and 74 deletions

View file

@ -5740,9 +5740,12 @@ export const uiAuditLogsCall = async (
export const getRemainingUsers = async (
accessToken: string
): Promise<{
total_users: number;
total_users: number | null;
total_users_used: number;
total_users_remaining: number;
total_users_remaining: number | null;
total_teams: number | null;
total_teams_used: number;
total_teams_remaining: number | null;
} | null> => {
try {
const url = proxyBaseUrl

View file

@ -1,6 +1,6 @@
import { useState, useEffect } from "react"
import { Badge } from "@tremor/react"
import { AlertTriangle, Users, TrendingUp, Loader2, ChevronDown, ChevronUp, Minus } from "lucide-react"
import { AlertTriangle, Users, TrendingUp, Loader2, ChevronDown, ChevronUp, Minus, UserCheck } from "lucide-react"
import { getRemainingUsers } from "./networking"
// Simple utility function to combine class names
@ -17,6 +17,9 @@ interface UsageData {
total_users: number | null
total_users_used: number
total_users_remaining: number | null
total_teams: number | null
total_teams_used: number
total_teams_remaining: number | null
}
export default function UsageIndicator({accessToken, width = 220}: UsageIndicatorProps) {
@ -54,18 +57,53 @@ export default function UsageIndicator({accessToken, width = 220}: UsageIndicato
return {
isOverLimit: false,
isNearLimit: false,
usagePercentage: 0
usagePercentage: 0,
userMetrics: {
isOverLimit: false,
isNearLimit: false,
usagePercentage: 0
},
teamMetrics: {
isOverLimit: false,
isNearLimit: false,
usagePercentage: 0
}
}
}
const isOverLimit = data.total_users_remaining ? data.total_users_remaining <= 0 : false
const isNearLimit = data.total_users_remaining ? data.total_users_remaining <= 5 && data.total_users_remaining > 0 : false
const usagePercentage = data.total_users ? (data.total_users_used / data.total_users) * 100 : 0
// User metrics
const userIsOverLimit = data.total_users_remaining ? data.total_users_remaining <= 0 : false
const userIsNearLimit = data.total_users_remaining ? data.total_users_remaining <= 5 && data.total_users_remaining > 0 : false
const userUsagePercentage = data.total_users ? (data.total_users_used / data.total_users) * 100 : 0
return { isOverLimit, isNearLimit, usagePercentage }
// Team metrics
const teamIsOverLimit = data.total_teams_remaining ? data.total_teams_remaining <= 0 : false
const teamIsNearLimit = data.total_teams_remaining ? data.total_teams_remaining <= 5 && data.total_teams_remaining > 0 : false
const teamUsagePercentage = data.total_teams ? (data.total_teams_used / data.total_teams) * 100 : 0
// Combined status (worst case scenario)
const isOverLimit = userIsOverLimit || teamIsOverLimit
const isNearLimit = (userIsNearLimit || teamIsNearLimit) && !isOverLimit
const usagePercentage = Math.max(userUsagePercentage, teamUsagePercentage)
return {
isOverLimit,
isNearLimit,
usagePercentage,
userMetrics: {
isOverLimit: userIsOverLimit,
isNearLimit: userIsNearLimit,
usagePercentage: userUsagePercentage
},
teamMetrics: {
isOverLimit: teamIsOverLimit,
isNearLimit: teamIsNearLimit,
usagePercentage: teamUsagePercentage
}
}
}
const { isOverLimit, isNearLimit, usagePercentage } = getUsageMetrics(data)
const { isOverLimit, isNearLimit, usagePercentage, userMetrics, teamMetrics } = getUsageMetrics(data)
const getStatusColor = () => {
if (isOverLimit) return "red"
@ -105,7 +143,17 @@ export default function UsageIndicator({accessToken, width = 220}: UsageIndicato
>
<Users className="h-3 w-3 flex-shrink-0" />
{hasIssues && <span className="flex-shrink-0">{getStatusIcon()}</span>}
<span className="truncate">{data ? `${data.total_users_used}/${data.total_users}` : "Usage"}</span>
<div className="flex items-center gap-1 truncate">
{data && data.total_users !== null && (
<span className="flex-shrink-0">U:{data.total_users_used}/{data.total_users}</span>
)}
{data && data.total_teams !== null && (
<span className="flex-shrink-0">T:{data.total_teams_used}/{data.total_teams}</span>
)}
{!data || (data.total_users === null && data.total_teams === null) && (
<span className="truncate">Usage</span>
)}
</div>
</button>
</div>
)
@ -191,33 +239,78 @@ export default function UsageIndicator({accessToken, width = 220}: UsageIndicato
{/* Expanded details - simple and compact */}
{isExpanded && (
<div className="mt-2 pl-7 text-xs text-gray-600">
<div className="mb-1">
<span className="font-medium">{data.total_users_used}/{data.total_users}</span>
<span className="text-gray-500"> users</span>
</div>
{/* Simple progress bar */}
<div className="w-full bg-gray-200 rounded-full h-1 mb-1">
<div
className={cn(
"h-1 rounded-full transition-all duration-300",
isOverLimit && "bg-red-500",
isNearLimit && "bg-yellow-500",
!isOverLimit && !isNearLimit && "bg-green-500",
)}
style={{ width: `${Math.min(usagePercentage, 100)}%` }}
/>
</div>
<div className="mt-2 pl-7 text-xs text-gray-600 space-y-3">
{/* Users section */}
{data.total_users !== null && (
<div>
<div className="mb-1 flex items-center gap-1">
<Users className="h-3 w-3" />
<span className="font-medium">{data.total_users_used}/{data.total_users}</span>
<span className="text-gray-500">users</span>
</div>
{/* User progress bar */}
<div className="w-full bg-gray-200 rounded-full h-1 mb-1">
<div
className={cn(
"h-1 rounded-full transition-all duration-300",
userMetrics.isOverLimit && "bg-red-500",
userMetrics.isNearLimit && "bg-yellow-500",
!userMetrics.isOverLimit && !userMetrics.isNearLimit && "bg-green-500",
)}
style={{ width: `${Math.min(userMetrics.usagePercentage, 100)}%` }}
/>
</div>
{(isOverLimit || isNearLimit) && (
<div className={cn(
"flex items-center gap-1 text-xs",
isOverLimit && "text-red-600",
isNearLimit && "text-yellow-600"
)}>
{getStatusIcon()}
<span className="truncate">{getStatusText()}</span>
{(userMetrics.isOverLimit || userMetrics.isNearLimit) && (
<div className={cn(
"flex items-center gap-1 text-xs",
userMetrics.isOverLimit && "text-red-600",
userMetrics.isNearLimit && "text-yellow-600"
)}>
{userMetrics.isOverLimit ? <AlertTriangle className="h-3 w-3" /> : <TrendingUp className="h-3 w-3" />}
<span className="truncate">
Users {userMetrics.isOverLimit ? "Over Limit" : "Near Limit"}
</span>
</div>
)}
</div>
)}
{/* Teams section */}
{data.total_teams !== null && (
<div>
<div className="mb-1 flex items-center gap-1">
<UserCheck className="h-3 w-3" />
<span className="font-medium">{data.total_teams_used}/{data.total_teams}</span>
<span className="text-gray-500">teams</span>
</div>
{/* Team progress bar */}
<div className="w-full bg-gray-200 rounded-full h-1 mb-1">
<div
className={cn(
"h-1 rounded-full transition-all duration-300",
teamMetrics.isOverLimit && "bg-red-500",
teamMetrics.isNearLimit && "bg-yellow-500",
!teamMetrics.isOverLimit && !teamMetrics.isNearLimit && "bg-green-500",
)}
style={{ width: `${Math.min(teamMetrics.usagePercentage, 100)}%` }}
/>
</div>
{(teamMetrics.isOverLimit || teamMetrics.isNearLimit) && (
<div className={cn(
"flex items-center gap-1 text-xs",
teamMetrics.isOverLimit && "text-red-600",
teamMetrics.isNearLimit && "text-yellow-600"
)}>
{teamMetrics.isOverLimit ? <AlertTriangle className="h-3 w-3" /> : <TrendingUp className="h-3 w-3" />}
<span className="truncate">
Teams {teamMetrics.isOverLimit ? "Over Limit" : "Near Limit"}
</span>
</div>
)}
</div>
)}
</div>
@ -243,11 +336,17 @@ export default function UsageIndicator({accessToken, width = 220}: UsageIndicato
<div className="flex items-center gap-2">
<Users className="h-4 w-4 flex-shrink-0" />
{hasIssues && <span className="flex-shrink-0">{getStatusIcon()}</span>}
{data && (
<span className="text-sm font-medium truncate">
{data.total_users_used}/{data.total_users}
</span>
)}
<div className="flex items-center gap-2 text-sm font-medium truncate">
{data && data.total_users !== null && (
<span className="flex-shrink-0">U: {data.total_users_used}/{data.total_users}</span>
)}
{data && data.total_teams !== null && (
<span className="flex-shrink-0">T: {data.total_teams_used}/{data.total_teams}</span>
)}
{!data || (data.total_users === null && data.total_teams === null) && (
<span className="truncate">Usage</span>
)}
</div>
</div>
</button>
)
@ -314,45 +413,95 @@ export default function UsageIndicator({accessToken, width = 220}: UsageIndicato
</div>
{/* Compact stats optimized for 220px */}
<div className="space-y-2 text-sm">
<div className="flex justify-between items-center">
<span className="text-gray-600 text-xs">Used:</span>
<span className="font-medium text-right">{data.total_users_used}/{data.total_users}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600 text-xs">Remaining:</span>
<span className={cn(
"font-medium text-right",
isOverLimit && "text-red-600",
isNearLimit && "text-yellow-600"
)}>
{data.total_users_remaining}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600 text-xs">Usage:</span>
<span className="font-medium text-right">{Math.round(usagePercentage)}%</span>
</div>
<div className="space-y-3 text-sm">
{/* Users section */}
{data.total_users !== null && (
<div className="space-y-1">
<div className="flex items-center gap-1 text-xs text-gray-600 mb-1">
<Users className="h-3 w-3" />
<span className="font-medium">Users</span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600 text-xs">Used:</span>
<span className="font-medium text-right">{data.total_users_used}/{data.total_users}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600 text-xs">Remaining:</span>
<span className={cn(
"font-medium text-right",
userMetrics.isOverLimit && "text-red-600",
userMetrics.isNearLimit && "text-yellow-600"
)}>
{data.total_users_remaining}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600 text-xs">Usage:</span>
<span className="font-medium text-right">{Math.round(userMetrics.usagePercentage)}%</span>
</div>
{/* Progress bar */}
<div className="w-full bg-gray-200 rounded-full h-2 mt-3">
<div
className={cn(
"h-2 rounded-full transition-all duration-300",
isOverLimit && "bg-red-500",
isNearLimit && "bg-yellow-500",
!isOverLimit && !isNearLimit && "bg-green-500",
)}
style={{ width: `${Math.min(usagePercentage, 100)}%` }}
/>
</div>
{/* User progress bar */}
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className={cn(
"h-2 rounded-full transition-all duration-300",
userMetrics.isOverLimit && "bg-red-500",
userMetrics.isNearLimit && "bg-yellow-500",
!userMetrics.isOverLimit && !userMetrics.isNearLimit && "bg-green-500",
)}
style={{ width: `${Math.min(userMetrics.usagePercentage, 100)}%` }}
/>
</div>
</div>
)}
{/* Teams section */}
{data.total_teams !== null && (
<div className="space-y-1">
<div className="flex items-center gap-1 text-xs text-gray-600 mb-1">
<UserCheck className="h-3 w-3" />
<span className="font-medium">Teams</span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600 text-xs">Used:</span>
<span className="font-medium text-right">{data.total_teams_used}/{data.total_teams}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600 text-xs">Remaining:</span>
<span className={cn(
"font-medium text-right",
teamMetrics.isOverLimit && "text-red-600",
teamMetrics.isNearLimit && "text-yellow-600"
)}>
{data.total_teams_remaining}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600 text-xs">Usage:</span>
<span className="font-medium text-right">{Math.round(teamMetrics.usagePercentage)}%</span>
</div>
{/* Team progress bar */}
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className={cn(
"h-2 rounded-full transition-all duration-300",
teamMetrics.isOverLimit && "bg-red-500",
teamMetrics.isNearLimit && "bg-yellow-500",
!teamMetrics.isOverLimit && !teamMetrics.isNearLimit && "bg-green-500",
)}
style={{ width: `${Math.min(teamMetrics.usagePercentage, 100)}%` }}
/>
</div>
</div>
)}
</div>
</div>
)
}
// Don't render anything if no access token or if total_users is null
if (!accessToken || data?.total_users === null) {
// Don't render anything if no access token or if both total_users and total_teams are null
if (!accessToken || (data?.total_users === null && data?.total_teams === null)) {
return null
}