Skip to content

Commit 0ee3f1f

Browse files
committed
Add api calls count
1 parent 702a961 commit 0ee3f1f

File tree

5 files changed

+294
-11
lines changed

5 files changed

+294
-11
lines changed

client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ import {
1010
Button,
1111
Tag,
1212
Result,
13+
Progress,
14+
Statistic,
15+
Row,
16+
Col,
17+
Tooltip,
1318
} from "antd";
1419
import {
1520
LinkOutlined,
@@ -21,6 +26,9 @@ import {
2126
CloseCircleOutlined,
2227
ExclamationCircleOutlined,
2328
SyncOutlined,
29+
ApiOutlined,
30+
UserOutlined,
31+
CrownOutlined,
2432
} from "@ant-design/icons";
2533

2634
import { useSingleEnvironmentContext } from "./context/SingleEnvironmentContext";
@@ -33,6 +41,7 @@ import UserGroupsTab from "./components/UserGroupsTab";
3341
import EnvironmentHeader from "./components/EnvironmentHeader";
3442
import ModernBreadcrumbs from "./components/ModernBreadcrumbs";
3543
import { getEnvironmentTagColor } from "./utils/environmentUtils";
44+
import { formatAPICalls, getAPICallsStatusColor } from "./services/license.service";
3645
import ErrorComponent from './components/ErrorComponent';
3746
const { TabPane } = Tabs;
3847

@@ -217,6 +226,151 @@ const EnvironmentDetail: React.FC = () => {
217226
</Descriptions>
218227
</Card>
219228

229+
{/* Detailed License Information Card - only show for licensed environments with details */}
230+
{environment.isLicensed && environment.licenseDetails && (
231+
<Card
232+
title={
233+
<span>
234+
<CrownOutlined style={{ color: '#52c41a', marginRight: '8px' }} />
235+
License Details
236+
</span>
237+
}
238+
style={{
239+
marginBottom: "24px",
240+
borderRadius: '4px',
241+
border: '1px solid #f0f0f0'
242+
}}
243+
className="license-details-card"
244+
>
245+
<Row gutter={[24, 16]}>
246+
{/* API Calls Status */}
247+
<Col xs={24} sm={12} md={8}>
248+
<Card
249+
size="small"
250+
style={{ height: '100%', textAlign: 'center' }}
251+
bodyStyle={{ padding: '16px' }}
252+
>
253+
<Statistic
254+
title="API Calls Remaining"
255+
value={environment.licenseDetails.remainingAPICalls}
256+
formatter={(value) => (
257+
<span style={{
258+
color: getAPICallsStatusColor(
259+
environment.licenseDetails?.remainingAPICalls || 0,
260+
environment.licenseDetails?.totalAPICallsLimit || 0
261+
)
262+
}}>
263+
{value?.toLocaleString()}
264+
</span>
265+
)}
266+
prefix={<ApiOutlined />}
267+
/>
268+
<div style={{ marginTop: '12px' }}>
269+
<Progress
270+
percent={100 - (environment.licenseDetails.apiCallsUsage || 0)}
271+
strokeColor={getAPICallsStatusColor(
272+
environment.licenseDetails.remainingAPICalls,
273+
environment.licenseDetails.totalAPICallsLimit || 0
274+
)}
275+
size="small"
276+
showInfo={false}
277+
/>
278+
<div style={{
279+
fontSize: '12px',
280+
color: '#8c8c8c',
281+
marginTop: '4px'
282+
}}>
283+
{environment.licenseDetails.apiCallsUsage || 0}% used
284+
</div>
285+
</div>
286+
</Card>
287+
</Col>
288+
289+
{/* Total License Limit */}
290+
<Col xs={24} sm={12} md={8}>
291+
<Card
292+
size="small"
293+
style={{ height: '100%', textAlign: 'center' }}
294+
bodyStyle={{ padding: '16px' }}
295+
>
296+
<Statistic
297+
title="Total API Calls Limit"
298+
value={environment.licenseDetails.totalAPICallsLimit}
299+
formatter={(value) => value?.toLocaleString()}
300+
prefix={<ApiOutlined />}
301+
/>
302+
<Tag
303+
color="blue"
304+
style={{ marginTop: '12px' }}
305+
>
306+
{environment.licenseDetails.eeLicenses.length} License{environment.licenseDetails.eeLicenses.length !== 1 ? 's' : ''}
307+
</Tag>
308+
</Card>
309+
</Col>
310+
311+
{/* Enterprise Edition Status */}
312+
<Col xs={24} sm={12} md={8}>
313+
<Card
314+
size="small"
315+
style={{ height: '100%', textAlign: 'center' }}
316+
bodyStyle={{ padding: '16px' }}
317+
>
318+
<Statistic
319+
title="Enterprise Edition"
320+
value={environment.licenseDetails.eeActive ? "Active" : "Inactive"}
321+
formatter={(value) => (
322+
<Tag
323+
color={environment.licenseDetails?.eeActive ? "green" : "red"}
324+
icon={environment.licenseDetails?.eeActive ? <CheckCircleOutlined /> : <CloseCircleOutlined />}
325+
>
326+
{value}
327+
</Tag>
328+
)}
329+
/>
330+
</Card>
331+
</Col>
332+
</Row>
333+
334+
{/* License Details */}
335+
<div style={{ marginTop: '24px' }}>
336+
<Typography.Title level={5} style={{ marginBottom: '16px' }}>
337+
<UserOutlined style={{ marginRight: '8px' }} />
338+
License Information
339+
</Typography.Title>
340+
341+
<Row gutter={[16, 16]}>
342+
{environment.licenseDetails.eeLicenses.map((license, index) => (
343+
<Col xs={24} sm={12} md={8} key={license.uuid}>
344+
<Card
345+
size="small"
346+
style={{
347+
border: '1px solid #f0f0f0',
348+
borderRadius: '6px'
349+
}}
350+
bodyStyle={{ padding: '12px' }}
351+
>
352+
<div style={{ marginBottom: '8px' }}>
353+
<strong style={{ color: '#262626' }}>
354+
{license.customerName}
355+
</strong>
356+
</div>
357+
<div style={{ fontSize: '12px', color: '#8c8c8c', marginBottom: '8px' }}>
358+
ID: {license.customerId}
359+
</div>
360+
<div style={{ fontSize: '12px', color: '#8c8c8c', marginBottom: '8px' }}>
361+
UUID: <span style={{ fontFamily: 'monospace' }}>{license.uuid.substring(0, 8)}...</span>
362+
</div>
363+
<Tag color="blue">
364+
{license.apiCallsLimit.toLocaleString()} calls
365+
</Tag>
366+
</Card>
367+
</Col>
368+
))}
369+
</Row>
370+
</div>
371+
</Card>
372+
)}
373+
220374
{/* Modern Breadcrumbs navigation */}
221375
<ModernBreadcrumbs items={breadcrumbItems} />
222376

client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentsTable.tsx

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import React from 'react';
2-
import { Table, Tag, Button, Tooltip, Space, Card, Row, Col, Typography, Avatar, Spin, Alert } from 'antd';
3-
import { EditOutlined, AuditOutlined, LinkOutlined, EnvironmentOutlined, StarFilled, CloudServerOutlined, CheckCircleOutlined, CloseCircleOutlined, ExclamationCircleOutlined, SyncOutlined } from '@ant-design/icons';
2+
import { Table, Tag, Button, Tooltip, Space, Card, Row, Col, Typography, Avatar, Spin, Alert, Progress } from 'antd';
3+
import { EditOutlined, AuditOutlined, LinkOutlined, EnvironmentOutlined, StarFilled, CloudServerOutlined, CheckCircleOutlined, CloseCircleOutlined, ExclamationCircleOutlined, SyncOutlined, ApiOutlined } from '@ant-design/icons';
44
import { Environment } from '../types/environment.types';
55
import { getEnvironmentTagColor, formatEnvironmentType } from '../utils/environmentUtils';
6+
import { getAPICallsStatusColor } from '../services/license.service';
67

78
const { Text, Title } = Typography;
89

@@ -267,6 +268,38 @@ const EnvironmentsTable: React.FC<EnvironmentsTableProps> = ({
267268
</Text>
268269
</div>
269270
</div>
271+
272+
{/* API Calls Information - show if license details are available */}
273+
{env.licenseDetails && (
274+
<div style={{ marginTop: '8px', padding: '8px', background: '#fafafa', borderRadius: '4px' }}>
275+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '6px' }}>
276+
<Text type="secondary" style={{ fontSize: '11px' }}>
277+
<ApiOutlined style={{ marginRight: '4px' }} />
278+
API Calls
279+
</Text>
280+
281+
</div>
282+
<Progress
283+
percent={100 - (env.licenseDetails.apiCallsUsage || 0)}
284+
strokeColor={getAPICallsStatusColor(
285+
env.licenseDetails.remainingAPICalls,
286+
env.licenseDetails.totalAPICallsLimit || 0
287+
)}
288+
size="small"
289+
showInfo={false}
290+
strokeWidth={4}
291+
/>
292+
<div style={{
293+
display: 'flex',
294+
justifyContent: 'space-between',
295+
marginTop: '4px',
296+
fontSize: '10px',
297+
color: '#8c8c8c'
298+
}}>
299+
<span>{env.licenseDetails.apiCallsUsage || 0}% used</span>
300+
</div>
301+
</div>
302+
)}
270303
</div>
271304
</div>
272305
</Card>

client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ export async function getEnvironmentById(id: string): Promise<Environment> {
136136
envWithLicense.isLicensed = licenseInfo.isValid;
137137
envWithLicense.licenseStatus = licenseInfo.isValid ? 'licensed' : 'unlicensed';
138138
envWithLicense.licenseError = licenseInfo.error;
139+
envWithLicense.licenseDetails = licenseInfo.details;
139140
} else {
140141
envWithLicense.isLicensed = false;
141142
envWithLicense.licenseStatus = 'error';
@@ -556,6 +557,7 @@ export async function getEnvironmentsWithLicenseStatus(): Promise<Environment[]>
556557
envWithLicense.isLicensed = licenseInfo.isValid;
557558
envWithLicense.licenseStatus = licenseInfo.isValid ? 'licensed' : 'unlicensed';
558559
envWithLicense.licenseError = licenseInfo.error;
560+
envWithLicense.licenseDetails = licenseInfo.details;
559561
} else {
560562
envWithLicense.isLicensed = false;
561563
envWithLicense.licenseStatus = 'error';

client/packages/lowcoder/src/pages/setting/environments/services/license.service.ts

Lines changed: 77 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import axios from 'axios';
2-
import { EnvironmentLicense } from '../types/environment.types';
2+
import { EnvironmentLicense, DetailedLicenseInfo } from '../types/environment.types';
33

44
/**
5-
* Check if license endpoint exists for an environment
5+
* Check license and fetch detailed license information for an environment
66
* @param apiServiceUrl - API service URL for the environment
77
* @param apiKey - API key for the environment
8-
* @returns Promise with license information
8+
* @returns Promise with license information including detailed data
99
*/
1010
export async function checkEnvironmentLicense(
1111
apiServiceUrl: string,
@@ -25,25 +25,93 @@ export async function checkEnvironmentLicense(
2525
headers.Authorization = `Bearer ${apiKey}`;
2626
}
2727

28-
// Use GET request to check endpoint existence
29-
await axios.get(
28+
// Fetch detailed license information
29+
const response = await axios.get(
3030
`${apiServiceUrl}/api/plugins/enterprise/license`,
3131
{
3232
headers,
3333
timeout: 500 // Very short timeout for immediate failure when endpoint doesn't exist
3434
}
3535
);
3636

37-
// If we get a successful response, the endpoint exists
37+
// Parse the license response
38+
const licenseData = response.data;
39+
40+
// Calculate total API calls limit and usage percentage
41+
const totalAPICallsLimit = licenseData.eeLicenses?.reduce(
42+
(sum: number, license: any) => sum + (license.apiCallsLimit || 0),
43+
0
44+
) || 0;
45+
46+
const apiCallsUsage = totalAPICallsLimit > 0
47+
? Math.round(((totalAPICallsLimit - licenseData.remainingAPICalls) / totalAPICallsLimit) * 100)
48+
: 0;
49+
50+
const licenseDetails: DetailedLicenseInfo = {
51+
eeActive: licenseData.eeActive || false,
52+
remainingAPICalls: licenseData.remainingAPICalls || 0,
53+
eeLicenses: licenseData.eeLicenses || [],
54+
totalAPICallsLimit,
55+
apiCallsUsage
56+
};
57+
58+
// Determine if license is valid based on enterprise edition status and remaining calls
59+
const isValid = licenseDetails.eeActive && licenseDetails.remainingAPICalls > 0;
60+
3861
return {
39-
isValid: true
62+
isValid,
63+
details: licenseDetails
4064
};
4165

4266
} catch (error) {
43-
// Any error means the endpoint doesn't exist or isn't accessible
67+
// Determine the specific error type
68+
let errorMessage = 'License not available';
69+
70+
if (axios.isAxiosError(error)) {
71+
if (error.code === 'ECONNABORTED') {
72+
errorMessage = 'License check timed out';
73+
} else if (error.response?.status === 404) {
74+
errorMessage = 'License endpoint not found';
75+
} else if (error.response?.status === 401) {
76+
errorMessage = 'Unauthorized - check API key';
77+
} else if (error.response && error.response.status >= 500) {
78+
errorMessage = 'License server error';
79+
}
80+
}
81+
4482
return {
4583
isValid: false,
46-
error: 'License not available'
84+
error: errorMessage
4785
};
4886
}
87+
}
88+
89+
/**
90+
* Format API calls for display
91+
* @param remaining - Remaining API calls
92+
* @param total - Total API calls limit
93+
* @returns Formatted string
94+
*/
95+
export function formatAPICalls(remaining: number, total: number): string {
96+
const used = total - remaining;
97+
const percentage = total > 0 ? Math.round((used / total) * 100) : 0;
98+
99+
return `${remaining.toLocaleString()} remaining (${used.toLocaleString()}/${total.toLocaleString()} used, ${percentage}%)`;
100+
}
101+
102+
/**
103+
* Get API calls status color based on usage percentage
104+
* @param remainingCalls - Remaining API calls
105+
* @param totalCalls - Total API calls limit
106+
* @returns Color string for UI components
107+
*/
108+
export function getAPICallsStatusColor(remainingCalls: number, totalCalls: number): string {
109+
if (totalCalls === 0) return '#d9d9d9'; // Unknown
110+
111+
const usagePercentage = ((totalCalls - remainingCalls) / totalCalls) * 100;
112+
113+
if (usagePercentage >= 90) return '#ff4d4f'; // Red - Critical
114+
if (usagePercentage >= 75) return '#faad14'; // Orange - Warning
115+
if (usagePercentage >= 50) return '#1890ff'; // Blue - Moderate
116+
return '#52c41a'; // Green - Good
49117
}

0 commit comments

Comments
 (0)