Skip to content

Commit d2425d7

Browse files
authored
feat: adding rate limiting tab to user settings (#1707)
1 parent 8d8bab4 commit d2425d7

File tree

16 files changed

+647
-246
lines changed

16 files changed

+647
-246
lines changed
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/******************************************************************************
2+
* Copyright (c) 2026 Contributors to the Eclipse Foundation.
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information regarding copyright ownership.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* https://www.eclipse.org/legal/epl-2.0.
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*****************************************************************************/
13+
14+
import { FC, type ReactNode } from 'react';
15+
import {
16+
Box,
17+
Typography,
18+
Paper,
19+
type PaperProps,
20+
Chip,
21+
Stack,
22+
Divider,
23+
Grid,
24+
} from '@mui/material';
25+
import type { Customer } from '../../../extension-registry-types';
26+
27+
const sectionPaperProps: PaperProps = { elevation: 1, sx: { p: 3, mb: 3 } };
28+
29+
export interface GeneralDetailsProps {
30+
customer: Customer;
31+
headerAction?: ReactNode;
32+
}
33+
34+
export const GeneralDetails: FC<GeneralDetailsProps> = ({ customer, headerAction }) => {
35+
const tier = customer.tier;
36+
return (
37+
<Paper {...sectionPaperProps}>
38+
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
39+
<Typography variant='h6'>General Information</Typography>
40+
{headerAction && <Box sx={{ ml: 'auto' }}>{headerAction}</Box>}
41+
</Box>
42+
<Divider sx={{ mb: 2 }} />
43+
<Grid container spacing={2}>
44+
<Grid item xs={12} sm={6} md={4}>
45+
<Typography variant='subtitle2' color='text.secondary'>Name</Typography>
46+
<Typography variant='body1'>{customer.name}</Typography>
47+
</Grid>
48+
<Grid item xs={12} sm={6} md={4}>
49+
<Typography variant='subtitle2' color='text.secondary'>State</Typography>
50+
<Box sx={{ mt: 0.5 }}>
51+
<Chip
52+
label={customer.state}
53+
size='small'
54+
color='secondary'
55+
/>
56+
</Box>
57+
</Grid>
58+
{tier ? (
59+
<>
60+
<Grid item xs={12} sm={6} md={4}>
61+
<Typography variant='subtitle2' color='text.secondary'>Tier</Typography>
62+
<Box sx={{ mt: 0.5 }}>
63+
<Chip label={tier.name} size='small' />
64+
</Box>
65+
</Grid>
66+
<Grid item xs={12} sm={6} md={4}>
67+
<Typography variant='subtitle2' color='text.secondary'>Tier Type</Typography>
68+
<Typography variant='body2'>{tier.tierType}</Typography>
69+
</Grid>
70+
<Grid item xs={12} sm={6} md={4}>
71+
<Typography variant='subtitle2' color='text.secondary'>Capacity</Typography>
72+
<Typography variant='body2'>{tier.capacity} requests / {tier.duration}s</Typography>
73+
</Grid>
74+
<Grid item xs={12} sm={6} md={4}>
75+
<Typography variant='subtitle2' color='text.secondary'>Refill Strategy</Typography>
76+
<Typography variant='body2'>{tier.refillStrategy}</Typography>
77+
</Grid>
78+
{tier.description && (
79+
<Grid item xs={12}>
80+
<Typography variant='subtitle2' color='text.secondary'>Tier Description</Typography>
81+
<Typography variant='body2'>{tier.description}</Typography>
82+
</Grid>
83+
)}
84+
</>
85+
) : (
86+
<Grid item xs={12} sm={6} md={4}>
87+
<Typography variant='subtitle2' color='text.secondary'>Tier</Typography>
88+
<Typography variant='body2' color='text.secondary'>No tier assigned</Typography>
89+
</Grid>
90+
)}
91+
<Grid item xs={12}>
92+
<Typography variant='subtitle2' color='text.secondary'>CIDR Blocks</Typography>
93+
{customer.cidrBlocks.length > 0 ? (
94+
<Stack direction='row' spacing={0.5} sx={{ mt: 0.5 }} flexWrap='wrap' useFlexGap>
95+
{customer.cidrBlocks.map((cidr) => (
96+
<Chip key={cidr} label={cidr} size='small' variant='outlined' />
97+
))}
98+
</Stack>
99+
) : (
100+
<Typography variant='body2' color='text.secondary'>None configured</Typography>
101+
)}
102+
</Grid>
103+
</Grid>
104+
</Paper>
105+
);
106+
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/******************************************************************************
2+
* Copyright (c) 2026 Contributors to the Eclipse Foundation.
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information regarding copyright ownership.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* https://www.eclipse.org/legal/epl-2.0.
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*****************************************************************************/
13+
14+
export { GeneralDetails } from './general-details';
15+
export type { GeneralDetailsProps } from './general-details';
16+
17+
export { Members } from './members';
18+
export type { MembersProps } from './members';
19+
20+
export { UsageStats } from './usage-stats';
21+
export type { UsageStatsProps } from './usage-stats';
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/******************************************************************************
2+
* Copyright (c) 2026 Contributors to the Eclipse Foundation.
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information regarding copyright ownership.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* https://www.eclipse.org/legal/epl-2.0.
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*****************************************************************************/
13+
14+
import { FC, type ReactNode } from 'react';
15+
import {
16+
Box,
17+
Typography,
18+
Paper,
19+
type PaperProps,
20+
Divider,
21+
Avatar,
22+
List,
23+
ListItem,
24+
ListItemAvatar,
25+
ListItemText,
26+
} from '@mui/material';
27+
import type { UserData } from '../../../extension-registry-types';
28+
29+
const sectionPaperProps: PaperProps = { elevation: 1, sx: { p: 3, mb: 3 } };
30+
31+
export interface MembersProps {
32+
users: UserData[];
33+
headerAction?: ReactNode;
34+
renderUserAction?: (user: UserData) => ReactNode;
35+
renderUserPrimary?: (user: UserData) => ReactNode;
36+
}
37+
38+
export const Members: FC<MembersProps> = ({ users, headerAction, renderUserAction, renderUserPrimary }) => (
39+
<Paper {...sectionPaperProps}>
40+
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
41+
<Typography variant='h6'>Members</Typography>
42+
{headerAction && <Box sx={{ ml: 'auto' }}>{headerAction}</Box>}
43+
</Box>
44+
<Divider sx={{ mb: 1 }} />
45+
{users.length === 0 ? (
46+
<Typography variant='body2' color='text.secondary' sx={{ py: 1 }}>
47+
No members assigned to this customer.
48+
</Typography>
49+
) : (
50+
<List dense disablePadding>
51+
{users.map(user => (
52+
<ListItem
53+
key={`${user.loginName}-${user.provider}`}
54+
secondaryAction={renderUserAction?.(user)}
55+
>
56+
<ListItemAvatar>
57+
<Avatar src={user.avatarUrl} sx={{ width: 32, height: 32 }} />
58+
</ListItemAvatar>
59+
<ListItemText
60+
primary={renderUserPrimary ? renderUserPrimary(user) : user.loginName}
61+
secondary={user.fullName}
62+
/>
63+
</ListItem>
64+
))}
65+
</List>
66+
)}
67+
</Paper>
68+
);
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/******************************************************************************
2+
* Copyright (c) 2026 Contributors to the Eclipse Foundation.
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information regarding copyright ownership.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* https://www.eclipse.org/legal/epl-2.0.
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*****************************************************************************/
13+
14+
import { FC } from 'react';
15+
import {
16+
Typography,
17+
Paper,
18+
type PaperProps,
19+
Divider,
20+
} from '@mui/material';
21+
import type { Customer, UsageStats as UsageStatsType } from '../../../extension-registry-types';
22+
import type { DateTime } from 'luxon';
23+
import { UsageStatsChart } from '../usage-stats/usage-stats-chart';
24+
25+
const sectionPaperProps: PaperProps = { elevation: 1, sx: { p: 3, mb: 3 } };
26+
27+
export interface UsageStatsProps {
28+
usageStats: readonly UsageStatsType[];
29+
customer: Customer;
30+
startDate: DateTime;
31+
onStartDateChange: (date: DateTime) => void;
32+
compact?: boolean;
33+
}
34+
35+
export const UsageStats: FC<UsageStatsProps> = ({ usageStats, customer, startDate, onStartDateChange, compact }) => (
36+
<Paper {...sectionPaperProps}>
37+
<Typography variant='h6' gutterBottom>
38+
Usage Statistics
39+
</Typography>
40+
<Divider sx={{ mb: 2 }} />
41+
<UsageStatsChart
42+
usageStats={usageStats}
43+
customer={customer}
44+
startDate={startDate}
45+
onStartDateChange={onStartDateChange}
46+
embedded
47+
compact={compact}
48+
/>
49+
</Paper>
50+
);

webui/src/pages/admin-dashboard/usage-stats/usage-stats-chart.tsx renamed to webui/src/components/rate-limiting/usage-stats/usage-stats-chart.tsx

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,16 @@ interface UsageStatsChartProps {
4242
startDate: DateTime;
4343
onStartDateChange: (date: DateTime) => void;
4444
embedded?: boolean;
45+
compact?: boolean;
4546
}
4647

4748
export const UsageStatsChart: FC<UsageStatsChartProps> = ({
4849
usageStats,
4950
customer,
5051
startDate,
5152
onStartDateChange,
52-
embedded = false
53+
embedded = false,
54+
compact = false
5355
}) => {
5456
const dayStart = startDate.startOf('day').toMillis() / 1000;
5557
const dayEnd = startDate.endOf('day').toMillis() / 1000;
@@ -134,8 +136,8 @@ export const UsageStatsChart: FC<UsageStatsChartProps> = ({
134136
color: 'lightgray',
135137
}]}
136138

137-
height={400}
138-
margin={{ top: 10 }}
139+
height={compact ? 300 : 400}
140+
margin={{ top: 30 }}
139141
xAxis={[
140142
{
141143
id: 'date',
@@ -172,11 +174,13 @@ export const UsageStatsChart: FC<UsageStatsChartProps> = ({
172174
label='Time (UTC)'
173175
position='bottom'
174176
axisId='date'
175-
tickInterval={(value, index) => {
176-
return new Date(value).getMinutes() === 0;
177+
tickInterval={(value) => {
178+
const d = new Date(value);
179+
return d.getMinutes() === 0 && (!compact || d.getHours() % 3 === 0);
177180
}}
178-
tickLabelInterval={(value, index) => {
179-
return new Date(value).getMinutes() === 0;
181+
tickLabelInterval={(value) => {
182+
const d = new Date(value);
183+
return d.getMinutes() === 0 && (!compact || d.getHours() % 3 === 0);
180184
}}
181185
tickLabelStyle={{
182186
fontSize: 10,
@@ -202,4 +206,4 @@ export const UsageStatsChart: FC<UsageStatsChartProps> = ({
202206
}
203207
</LocalizationProvider>
204208
);
205-
};
209+
};

webui/src/pages/admin-dashboard/usage-stats/usage-stats-utils.ts renamed to webui/src/components/rate-limiting/usage-stats/usage-stats-utils.ts

File renamed without changes.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/******************************************************************************
2+
* Copyright (c) 2026 Contributors to the Eclipse Foundation.
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information regarding copyright ownership.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* https://www.eclipse.org/legal/epl-2.0.
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*****************************************************************************/
13+
14+
import { useContext, useState, useEffect, useRef, useCallback } from "react";
15+
import { MainContext } from "../../../context";
16+
import type { UsageStats } from "../../../extension-registry-types";
17+
import { handleError } from "../../../utils";
18+
import { getDefaultStartDate } from "./usage-stats-utils";
19+
import { DateTime } from "luxon";
20+
21+
export const useUsageStats = (customerName: string | undefined) => {
22+
const abortController = useRef(new AbortController());
23+
const { service } = useContext(MainContext);
24+
25+
const [usageStats, setUsageStats] = useState<readonly UsageStats[]>([]);
26+
const [loading, setLoading] = useState(false);
27+
const [error, setError] = useState<string | null>(null);
28+
const [internalStartDate, setInternalStartDate] = useState<DateTime>(getDefaultStartDate);
29+
30+
const startDateRef = useRef(internalStartDate);
31+
startDateRef.current = internalStartDate;
32+
33+
const fetchUsageStats = useCallback(async (date: DateTime) => {
34+
if (!customerName) {
35+
setUsageStats([]);
36+
setLoading(false);
37+
return;
38+
}
39+
40+
try {
41+
setLoading(true);
42+
setError(null);
43+
const data = await service.getUsageStatsForUser(
44+
abortController.current,
45+
customerName,
46+
date.toJSDate()
47+
);
48+
setUsageStats(data.stats);
49+
} catch (err) {
50+
setError(handleError(err as Error));
51+
} finally {
52+
setLoading(false);
53+
}
54+
}, [service, customerName]);
55+
56+
const setStartDate = useCallback((date: DateTime) => {
57+
setInternalStartDate(date);
58+
fetchUsageStats(date);
59+
}, [fetchUsageStats]);
60+
61+
useEffect(() => {
62+
fetchUsageStats(startDateRef.current);
63+
return () => {
64+
abortController.current.abort();
65+
abortController.current = new AbortController();
66+
};
67+
}, [fetchUsageStats]);
68+
69+
return { usageStats, loading, error, startDate: internalStartDate, setStartDate };
70+
};

0 commit comments

Comments
 (0)