mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Show individual usage stats to members (#70)
* Show individual usage stats to members * Tighter security * PR feedback
This commit is contained in:
parent
9b6ad19db0
commit
25f003c459
9 changed files with 286 additions and 83 deletions
|
|
@ -1,6 +1,7 @@
|
|||
'use server';
|
||||
|
||||
import { z } from 'zod';
|
||||
import { auth } from '@clerk/nextjs/server';
|
||||
|
||||
import {
|
||||
TelemetryEventName,
|
||||
|
|
@ -14,6 +15,56 @@ import { type User, getUsersById } from '@/db/server';
|
|||
|
||||
type Table = 'events' | 'messages';
|
||||
|
||||
/**
|
||||
* Validates authentication and authorization for analytics functions
|
||||
*/
|
||||
async function validateAnalyticsAccess({
|
||||
requestedOrgId,
|
||||
requestedUserId,
|
||||
requireAdmin = false,
|
||||
}: {
|
||||
requestedOrgId?: string | null;
|
||||
requestedUserId?: string | null;
|
||||
requireAdmin?: boolean;
|
||||
}): Promise<{
|
||||
authOrgId: string;
|
||||
authUserId: string;
|
||||
orgRole: string;
|
||||
effectiveUserId: string | null;
|
||||
}> {
|
||||
const { orgId: authOrgId, orgRole, userId: authUserId } = await auth();
|
||||
|
||||
// Ensure user is authenticated and belongs to the organization
|
||||
if (!authOrgId || !authUserId || authOrgId !== requestedOrgId) {
|
||||
throw new Error('Unauthorized: Invalid organization access');
|
||||
}
|
||||
|
||||
// Check if admin access is required
|
||||
if (requireAdmin && orgRole !== 'org:admin') {
|
||||
throw new Error('Unauthorized: Administrator access required');
|
||||
}
|
||||
|
||||
// If user is not an admin and trying to access data other than their own
|
||||
if (
|
||||
orgRole !== 'org:admin' &&
|
||||
requestedUserId &&
|
||||
requestedUserId !== authUserId
|
||||
) {
|
||||
throw new Error('Unauthorized: Members can only access their own data');
|
||||
}
|
||||
|
||||
// For non-admin users, force userId filter to their own ID
|
||||
const effectiveUserId =
|
||||
orgRole !== 'org:admin' ? authUserId : requestedUserId || null;
|
||||
|
||||
return {
|
||||
authOrgId,
|
||||
authUserId,
|
||||
orgRole: orgRole || 'unknown',
|
||||
effectiveUserId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* captureEvent
|
||||
*/
|
||||
|
|
@ -78,14 +129,30 @@ type UsageRecord = Partial<Record<TelemetryEventName, Usage>>;
|
|||
export const getUsage = async ({
|
||||
orgId,
|
||||
timePeriod = 90,
|
||||
userId,
|
||||
}: {
|
||||
orgId?: string | null;
|
||||
timePeriod?: AnyTimePeriod;
|
||||
userId?: string | null;
|
||||
}): Promise<UsageRecord> => {
|
||||
const { effectiveUserId } = await validateAnalyticsAccess({
|
||||
requestedOrgId: orgId,
|
||||
requestedUserId: userId,
|
||||
});
|
||||
|
||||
if (!orgId) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const userFilter = effectiveUserId ? 'AND userId = {userId: String}' : '';
|
||||
const queryParams: Record<string, string | number> = {
|
||||
orgId: orgId!,
|
||||
timePeriod,
|
||||
};
|
||||
if (effectiveUserId) {
|
||||
queryParams.userId = effectiveUserId;
|
||||
}
|
||||
|
||||
const results = await analytics.query({
|
||||
query: `
|
||||
SELECT
|
||||
|
|
@ -98,10 +165,11 @@ export const getUsage = async ({
|
|||
WHERE
|
||||
orgId = {orgId: String}
|
||||
AND timestamp >= toUnixTimestamp(now() - INTERVAL {timePeriod: Int32} DAY)
|
||||
${userFilter}
|
||||
GROUP BY 1
|
||||
`,
|
||||
format: 'JSONEachRow',
|
||||
query_params: { orgId, timePeriod },
|
||||
query_params: queryParams,
|
||||
});
|
||||
|
||||
return z
|
||||
|
|
@ -132,14 +200,36 @@ export type DeveloperUsage = z.infer<typeof developerUsageSchema> & {
|
|||
export const getDeveloperUsage = async ({
|
||||
orgId,
|
||||
timePeriod = 90,
|
||||
userId,
|
||||
}: {
|
||||
orgId?: string | null;
|
||||
timePeriod?: AnyTimePeriod;
|
||||
userId?: string | null;
|
||||
}): Promise<DeveloperUsage[]> => {
|
||||
await validateAnalyticsAccess({
|
||||
requestedOrgId: orgId,
|
||||
requestedUserId: userId,
|
||||
requireAdmin: true,
|
||||
});
|
||||
|
||||
if (!orgId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const userFilter = userId ? 'AND userId = {userId: String}' : '';
|
||||
const queryParams: Record<string, string | number | string[]> = {
|
||||
orgId: orgId!,
|
||||
timePeriod,
|
||||
types: [
|
||||
TelemetryEventName.TASK_CREATED,
|
||||
TelemetryEventName.TASK_COMPLETED,
|
||||
TelemetryEventName.LLM_COMPLETION,
|
||||
],
|
||||
};
|
||||
if (userId) {
|
||||
queryParams.userId = userId;
|
||||
}
|
||||
|
||||
const results = await analytics.query({
|
||||
query: `
|
||||
SELECT
|
||||
|
|
@ -152,18 +242,11 @@ export const getDeveloperUsage = async ({
|
|||
WHERE orgId = {orgId: String}
|
||||
AND timestamp >= toUnixTimestamp(now() - INTERVAL {timePeriod: Int32} DAY)
|
||||
AND type IN ({types: Array(String)})
|
||||
${userFilter}
|
||||
GROUP BY 1
|
||||
`,
|
||||
format: 'JSONEachRow',
|
||||
query_params: {
|
||||
orgId,
|
||||
timePeriod,
|
||||
types: [
|
||||
TelemetryEventName.TASK_CREATED,
|
||||
TelemetryEventName.TASK_COMPLETED,
|
||||
TelemetryEventName.LLM_COMPLETION,
|
||||
],
|
||||
},
|
||||
query_params: queryParams,
|
||||
});
|
||||
|
||||
const developerUsages = z
|
||||
|
|
@ -194,14 +277,36 @@ export type ModelUsage = z.infer<typeof modelUsageSchema>;
|
|||
export const getModelUsage = async ({
|
||||
orgId,
|
||||
timePeriod = 90,
|
||||
userId,
|
||||
}: {
|
||||
orgId?: string | null;
|
||||
timePeriod?: AnyTimePeriod;
|
||||
userId?: string | null;
|
||||
}): Promise<ModelUsage[]> => {
|
||||
await validateAnalyticsAccess({
|
||||
requestedOrgId: orgId,
|
||||
requestedUserId: userId,
|
||||
requireAdmin: true,
|
||||
});
|
||||
|
||||
if (!orgId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const userFilter = userId ? 'AND userId = {userId: String}' : '';
|
||||
const queryParams: Record<string, string | number | string[]> = {
|
||||
orgId: orgId!,
|
||||
timePeriod,
|
||||
types: [
|
||||
TelemetryEventName.TASK_CREATED,
|
||||
TelemetryEventName.TASK_COMPLETED,
|
||||
TelemetryEventName.LLM_COMPLETION,
|
||||
],
|
||||
};
|
||||
if (userId) {
|
||||
queryParams.userId = userId;
|
||||
}
|
||||
|
||||
const results = await analytics.query({
|
||||
query: `
|
||||
SELECT
|
||||
|
|
@ -215,18 +320,11 @@ export const getModelUsage = async ({
|
|||
orgId = {orgId: String}
|
||||
AND timestamp >= toUnixTimestamp(now() - INTERVAL {timePeriod: Int32} DAY)
|
||||
AND type IN ({types: Array(String)})
|
||||
${userFilter}
|
||||
GROUP BY 1, 2
|
||||
`,
|
||||
format: 'JSONEachRow',
|
||||
query_params: {
|
||||
orgId,
|
||||
timePeriod,
|
||||
types: [
|
||||
TelemetryEventName.TASK_CREATED,
|
||||
TelemetryEventName.TASK_COMPLETED,
|
||||
TelemetryEventName.LLM_COMPLETION,
|
||||
],
|
||||
},
|
||||
query_params: queryParams,
|
||||
});
|
||||
|
||||
return z.array(modelUsageSchema).parse(await results.json());
|
||||
|
|
@ -246,13 +344,36 @@ export type TaskWithUser = TaskWithTitle & { user: User };
|
|||
|
||||
export const getTasks = async ({
|
||||
orgId,
|
||||
userId,
|
||||
}: {
|
||||
orgId?: string | null;
|
||||
userId?: string | null;
|
||||
}): Promise<TaskWithUser[]> => {
|
||||
const { effectiveUserId } = await validateAnalyticsAccess({
|
||||
requestedOrgId: orgId,
|
||||
requestedUserId: userId,
|
||||
});
|
||||
|
||||
if (!orgId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const userFilter = effectiveUserId ? 'AND e.userId = {userId: String}' : '';
|
||||
const messageUserFilter = effectiveUserId
|
||||
? 'AND userId = {userId: String}'
|
||||
: '';
|
||||
const queryParams: Record<string, string | string[]> = {
|
||||
orgId: orgId!,
|
||||
types: [
|
||||
TelemetryEventName.TASK_CREATED,
|
||||
TelemetryEventName.TASK_COMPLETED,
|
||||
TelemetryEventName.LLM_COMPLETION,
|
||||
],
|
||||
};
|
||||
if (effectiveUserId) {
|
||||
queryParams.userId = effectiveUserId;
|
||||
}
|
||||
|
||||
const results = await analytics.query({
|
||||
query: `
|
||||
WITH first_messages AS (
|
||||
|
|
@ -262,6 +383,7 @@ export const getTasks = async ({
|
|||
argMin(mode, ts) as mode
|
||||
FROM messages
|
||||
WHERE orgId = {orgId: String}
|
||||
${messageUserFilter}
|
||||
GROUP BY taskId
|
||||
)
|
||||
SELECT
|
||||
|
|
@ -280,18 +402,12 @@ export const getTasks = async ({
|
|||
WHERE
|
||||
e.orgId = {orgId: String}
|
||||
AND e.type IN ({types: Array(String)})
|
||||
${userFilter}
|
||||
GROUP BY 1, 2
|
||||
ORDER BY timestamp DESC
|
||||
`,
|
||||
format: 'JSONEachRow',
|
||||
query_params: {
|
||||
orgId,
|
||||
types: [
|
||||
TelemetryEventName.TASK_CREATED,
|
||||
TelemetryEventName.TASK_COMPLETED,
|
||||
TelemetryEventName.LLM_COMPLETION,
|
||||
],
|
||||
},
|
||||
query_params: queryParams,
|
||||
});
|
||||
|
||||
const tasks = z.array(taskWithTitleSchema).parse(await results.json());
|
||||
|
|
@ -322,14 +438,35 @@ export type HourlyUsageByUser = z.infer<typeof hourlyUsageByUserSchema> & {
|
|||
export const getHourlyUsageByUser = async ({
|
||||
orgId,
|
||||
timePeriod = 90,
|
||||
userId,
|
||||
}: {
|
||||
orgId?: string | null;
|
||||
timePeriod?: AnyTimePeriod;
|
||||
userId?: string | null;
|
||||
}): Promise<HourlyUsageByUser[]> => {
|
||||
const { effectiveUserId } = await validateAnalyticsAccess({
|
||||
requestedOrgId: orgId,
|
||||
requestedUserId: userId,
|
||||
});
|
||||
|
||||
if (!orgId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const userFilter = effectiveUserId ? 'AND userId = {userId: String}' : '';
|
||||
const queryParams: Record<string, string | number | string[]> = {
|
||||
orgId: orgId!,
|
||||
timePeriod,
|
||||
types: [
|
||||
TelemetryEventName.TASK_CREATED,
|
||||
TelemetryEventName.TASK_COMPLETED,
|
||||
TelemetryEventName.LLM_COMPLETION,
|
||||
],
|
||||
};
|
||||
if (effectiveUserId) {
|
||||
queryParams.userId = effectiveUserId;
|
||||
}
|
||||
|
||||
const results = await analytics.query({
|
||||
query: `
|
||||
SELECT
|
||||
|
|
@ -343,19 +480,12 @@ export const getHourlyUsageByUser = async ({
|
|||
orgId = {orgId: String}
|
||||
AND timestamp >= toUnixTimestamp(now() - INTERVAL {timePeriod: Int32} DAY)
|
||||
AND type IN ({types: Array(String)})
|
||||
${userFilter}
|
||||
GROUP BY 1, 2
|
||||
ORDER BY hour_utc DESC, userId
|
||||
`,
|
||||
format: 'JSONEachRow',
|
||||
query_params: {
|
||||
orgId,
|
||||
timePeriod,
|
||||
types: [
|
||||
TelemetryEventName.TASK_CREATED,
|
||||
TelemetryEventName.TASK_COMPLETED,
|
||||
TelemetryEventName.LLM_COMPLETION,
|
||||
],
|
||||
},
|
||||
query_params: queryParams,
|
||||
});
|
||||
|
||||
const hourlyUsages = z
|
||||
|
|
|
|||
|
|
@ -1,33 +1,39 @@
|
|||
import { redirect } from 'next/navigation';
|
||||
import { auth } from '@clerk/nextjs/server';
|
||||
|
||||
import {
|
||||
NavbarHeader,
|
||||
NavbarMenu,
|
||||
Section,
|
||||
Connected,
|
||||
} from '@/components/layout';
|
||||
import { NavbarHeader, NavbarMenu, Section } from '@/components/layout';
|
||||
import { Usage } from './usage/Usage';
|
||||
|
||||
export default async function AuthenticatedLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { orgId, orgRole } = await auth();
|
||||
const { orgId, orgRole, userId } = await auth();
|
||||
|
||||
if (!orgId) {
|
||||
redirect('/select-org');
|
||||
}
|
||||
|
||||
// For now, a non-admin just get a "You're connected" message.
|
||||
if (orgRole !== 'org:admin') {
|
||||
return <Connected />;
|
||||
// Members get access to usage page only, filtered to their own data
|
||||
if (orgRole === 'org:member' || (orgRole && orgRole !== 'org:admin')) {
|
||||
return (
|
||||
<>
|
||||
<NavbarHeader className="h-[72px]" />
|
||||
<NavbarMenu className="h-[72px]" userRole="member" />
|
||||
<Section divider={false} className="min-h-[calc(100vh-72px-72px-2px)]">
|
||||
<div className="flex flex-col gap-8 py-8">
|
||||
<Usage userRole="member" currentUserId={userId} />
|
||||
</div>
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<NavbarHeader className="h-[72px]" />
|
||||
<NavbarMenu className="h-[72px]" />
|
||||
<NavbarMenu className="h-[72px]" userRole="admin" />
|
||||
<Section divider={false} className="min-h-[calc(100vh-72px-72px-2px)]">
|
||||
<div className="flex flex-col gap-8 py-8">{children}</div>
|
||||
</Section>
|
||||
|
|
|
|||
|
|
@ -13,16 +13,24 @@ export const Tasks = ({
|
|||
filter,
|
||||
onFilter,
|
||||
onTaskSelected,
|
||||
userRole = 'admin',
|
||||
currentUserId,
|
||||
}: {
|
||||
filter: Filter | null;
|
||||
onFilter: (filter: Filter) => void;
|
||||
onTaskSelected: (task: TaskWithUser) => void;
|
||||
userRole?: 'admin' | 'member';
|
||||
currentUserId?: string | null;
|
||||
}) => {
|
||||
const { orgId } = useAuth();
|
||||
|
||||
const { data = [], isPending } = useQuery({
|
||||
queryKey: ['getTasks', orgId],
|
||||
queryFn: () => getTasks({ orgId }),
|
||||
queryKey: ['getTasks', orgId, userRole === 'member' ? currentUserId : null],
|
||||
queryFn: () =>
|
||||
getTasks({
|
||||
orgId,
|
||||
userId: userRole === 'member' ? currentUserId : undefined,
|
||||
}),
|
||||
enabled: !!orgId,
|
||||
});
|
||||
|
||||
|
|
@ -64,7 +72,7 @@ export const Tasks = ({
|
|||
<TaskCard
|
||||
key={task.taskId}
|
||||
task={task}
|
||||
onFilter={onFilter}
|
||||
onFilter={userRole === 'member' ? undefined : onFilter}
|
||||
onTaskSelected={onTaskSelected}
|
||||
/>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,12 @@ import { Models } from './Models';
|
|||
import { Tasks } from './Tasks';
|
||||
import { TaskDrawer } from './TaskDrawer';
|
||||
|
||||
export const Usage = () => {
|
||||
type UsageProps = {
|
||||
userRole?: 'admin' | 'member';
|
||||
currentUserId?: string | null;
|
||||
};
|
||||
|
||||
export const Usage = ({ userRole = 'admin', currentUserId }: UsageProps) => {
|
||||
const t = useTranslations('Analytics');
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('tasks');
|
||||
const [filter, setFilter] = useState<Filter | null>(null);
|
||||
|
|
@ -25,24 +30,36 @@ export const Usage = () => {
|
|||
setViewMode('tasks');
|
||||
}, []);
|
||||
|
||||
// For members, automatically set filter to their user ID and hide other tabs
|
||||
const isMember = userRole === 'member';
|
||||
const availableViewModes = isMember ? (['tasks'] as const) : viewModes;
|
||||
|
||||
// Auto-apply user filter for members
|
||||
const effectiveFilter =
|
||||
isMember && currentUserId
|
||||
? { type: 'userId' as const, value: currentUserId, label: 'Your Tasks' }
|
||||
: filter;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-4 sm:gap-6">
|
||||
<UsageCard />
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{viewModes.map((mode) => (
|
||||
<Button
|
||||
key={mode}
|
||||
variant={viewMode === mode ? 'default' : 'secondary'}
|
||||
size="sm"
|
||||
onClick={() => setViewMode(mode)}
|
||||
className="min-w-[80px]"
|
||||
>
|
||||
{t(`view_mode_${mode}`)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
{filter && (
|
||||
<UsageCard userRole={userRole} currentUserId={currentUserId} />
|
||||
{!isMember && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{availableViewModes.map((mode) => (
|
||||
<Button
|
||||
key={mode}
|
||||
variant={viewMode === mode ? 'default' : 'secondary'}
|
||||
size="sm"
|
||||
onClick={() => setViewMode(mode)}
|
||||
className="min-w-[80px]"
|
||||
>
|
||||
{t(`view_mode_${mode}`)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{filter && !isMember && (
|
||||
<div className="flex flex-row">
|
||||
<Badge variant="outline">
|
||||
<span className="text-sm">
|
||||
|
|
@ -60,9 +77,11 @@ export const Usage = () => {
|
|||
)}
|
||||
{viewMode === 'tasks' ? (
|
||||
<Tasks
|
||||
filter={filter}
|
||||
onFilter={onFilter}
|
||||
filter={effectiveFilter}
|
||||
onFilter={isMember ? () => {} : onFilter}
|
||||
onTaskSelected={(task: TaskWithUser) => setTask(task)}
|
||||
userRole={userRole}
|
||||
currentUserId={currentUserId}
|
||||
/>
|
||||
) : viewMode === 'developers' ? (
|
||||
<Developers onFilter={onFilter} />
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { auth } from '@clerk/nextjs/server';
|
||||
import { Usage } from './Usage';
|
||||
|
||||
export default async function Page() {
|
||||
return <Usage />;
|
||||
const { userId } = await auth();
|
||||
|
||||
return <Usage userRole="admin" currentUserId={userId} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,12 @@ import { Tabs, TabsList, TabsTrigger } from '@/components/ui/ecosystem';
|
|||
|
||||
import { Section } from './Section';
|
||||
|
||||
type NavbarMenuProps = Omit<React.HTMLAttributes<HTMLDivElement>, 'children'>;
|
||||
type NavbarMenuProps = Omit<
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
'children'
|
||||
> & {
|
||||
userRole?: 'admin' | 'member';
|
||||
};
|
||||
|
||||
const tabValues = [
|
||||
'/usage',
|
||||
|
|
@ -23,7 +28,10 @@ type TabValue = (typeof tabValues)[number];
|
|||
const isTabValue = (value: string): value is TabValue =>
|
||||
tabValues.includes(value as TabValue);
|
||||
|
||||
export const NavbarMenu = (props: NavbarMenuProps) => {
|
||||
export const NavbarMenu = ({
|
||||
userRole = 'admin',
|
||||
...props
|
||||
}: NavbarMenuProps) => {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const [tabValue, setTabValue] = useState<TabValue | undefined>(undefined);
|
||||
|
|
@ -50,10 +58,14 @@ export const NavbarMenu = (props: NavbarMenuProps) => {
|
|||
>
|
||||
<TabsList>
|
||||
<TabsTrigger value="/usage">Usage</TabsTrigger>
|
||||
<TabsTrigger value="/audit-logs">Audit Logs</TabsTrigger>
|
||||
<TabsTrigger value="/providers">Providers</TabsTrigger>
|
||||
<TabsTrigger value="/telemetry">Telemetry</TabsTrigger>
|
||||
<TabsTrigger value="/org">Organization</TabsTrigger>
|
||||
{userRole === 'admin' && (
|
||||
<>
|
||||
<TabsTrigger value="/audit-logs">Audit Logs</TabsTrigger>
|
||||
<TabsTrigger value="/providers">Providers</TabsTrigger>
|
||||
<TabsTrigger value="/telemetry">Telemetry</TabsTrigger>
|
||||
<TabsTrigger value="/org">Organization</TabsTrigger>
|
||||
</>
|
||||
)}
|
||||
<TabsTrigger value="/hidden" className="hidden" />
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import { Status } from '@/app/(authenticated)/usage/Status';
|
|||
|
||||
type TaskCardProps = {
|
||||
task: TaskWithUser;
|
||||
onFilter: (filter: Filter) => void;
|
||||
onFilter?: (filter: Filter) => void;
|
||||
onTaskSelected: (task: TaskWithUser) => void;
|
||||
};
|
||||
|
||||
|
|
@ -82,13 +82,17 @@ export const TaskCard = ({ task, onFilter, onTaskSelected }: TaskCardProps) => {
|
|||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onFilter({
|
||||
type: 'userId',
|
||||
value: task.userId,
|
||||
label: task.user.name,
|
||||
});
|
||||
// Only allow filtering if onFilter is provided (disabled for members)
|
||||
if (onFilter) {
|
||||
onFilter({
|
||||
type: 'userId',
|
||||
value: task.userId,
|
||||
label: task.user.name,
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="px-0 h-auto text-xs font-normal text-muted-foreground hover:text-foreground"
|
||||
disabled={!onFilter}
|
||||
>
|
||||
{task.user.name}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -27,7 +27,15 @@ import { UsageChart } from './UsageChart';
|
|||
|
||||
type MetricType = 'tasks' | 'tokens' | 'cost';
|
||||
|
||||
export const UsageCard = () => {
|
||||
type UsageCardProps = {
|
||||
userRole?: 'admin' | 'member';
|
||||
currentUserId?: string | null;
|
||||
};
|
||||
|
||||
export const UsageCard = ({
|
||||
userRole = 'admin',
|
||||
currentUserId,
|
||||
}: UsageCardProps) => {
|
||||
const t = useTranslations('DashboardIndex');
|
||||
const { orgId } = useAuth();
|
||||
const [selectedPeriod, setSelectedPeriod] = useState<TimePeriodConfig>(
|
||||
|
|
@ -43,6 +51,7 @@ export const UsageCard = () => {
|
|||
orgId,
|
||||
selectedPeriod.value,
|
||||
selectedPeriod.granularity,
|
||||
userRole === 'member' ? currentUserId : null,
|
||||
],
|
||||
queryFn: () =>
|
||||
getUsage({
|
||||
|
|
@ -51,6 +60,7 @@ export const UsageCard = () => {
|
|||
selectedPeriod.granularity === 'daily'
|
||||
? (selectedPeriod.value as 7 | 30 | 90)
|
||||
: (selectedPeriod.value as 1), // Use 1 day for 24h view
|
||||
userId: userRole === 'member' ? currentUserId : undefined,
|
||||
}),
|
||||
enabled: !!orgId,
|
||||
});
|
||||
|
|
@ -169,6 +179,8 @@ export const UsageCard = () => {
|
|||
<UsageChart
|
||||
timePeriodConfig={selectedPeriod}
|
||||
selectedMetric={selectedMetric}
|
||||
userRole={userRole}
|
||||
currentUserId={currentUserId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -283,6 +283,8 @@ const processDailyDataForChart = (
|
|||
interface UsageChartProps {
|
||||
timePeriodConfig: TimePeriodConfig;
|
||||
selectedMetric?: MetricType;
|
||||
userRole?: 'admin' | 'member';
|
||||
currentUserId?: string | null;
|
||||
}
|
||||
|
||||
// Custom tick components for theme-aware labels
|
||||
|
|
@ -432,6 +434,8 @@ const CustomTooltip = ({
|
|||
export const UsageChart = ({
|
||||
timePeriodConfig,
|
||||
selectedMetric = 'tasks',
|
||||
userRole = 'admin',
|
||||
currentUserId,
|
||||
}: UsageChartProps) => {
|
||||
const { orgId } = useAuth();
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
|
|
@ -447,9 +451,14 @@ export const UsageChart = ({
|
|||
orgId,
|
||||
timePeriodConfig.value,
|
||||
timePeriodConfig.granularity,
|
||||
userRole === 'member' ? currentUserId : null,
|
||||
],
|
||||
queryFn: () =>
|
||||
getHourlyUsageByUser({ orgId, timePeriod: timePeriodConfig.value }),
|
||||
getHourlyUsageByUser({
|
||||
orgId,
|
||||
timePeriod: timePeriodConfig.value,
|
||||
userId: userRole === 'member' ? currentUserId : undefined,
|
||||
}),
|
||||
enabled: !!orgId,
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue