diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx index 041997779..245e12971 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx @@ -10,6 +10,10 @@ import { Button, Tag, Result, + Row, + Col, + Statistic, + Progress, } from "antd"; import { LinkOutlined, @@ -21,6 +25,11 @@ import { CloseCircleOutlined, ExclamationCircleOutlined, SyncOutlined, + CloudServerOutlined, + UserOutlined, + SafetyOutlined, + CrownOutlined, + ApiOutlined, } from "@ant-design/icons"; import { useSingleEnvironmentContext } from "./context/SingleEnvironmentContext"; @@ -31,10 +40,12 @@ import history from "@lowcoder-ee/util/history"; import WorkspacesTab from "./components/WorkspacesTab"; import UserGroupsTab from "./components/UserGroupsTab"; import EnvironmentHeader from "./components/EnvironmentHeader"; +import StatsCard from "./components/StatsCard"; import ModernBreadcrumbs from "./components/ModernBreadcrumbs"; import { getEnvironmentTagColor } from "./utils/environmentUtils"; +import { formatAPICalls, getAPICallsStatusColor } from "./services/license.service"; import ErrorComponent from './components/ErrorComponent'; -const { TabPane } = Tabs; +import { Level1SettingPageContent } from "../styled"; /** * Environment Detail Page Component @@ -124,33 +135,80 @@ const EnvironmentDetail: React.FC = () => { ); } - const breadcrumbItems = [ + // Stats data for the cards + const statsData = [ { - key: 'environments', - title: ( + title: "Type", + value: environment.environmentType || "Unknown", + icon: , + color: getEnvironmentTagColor(environment.environmentType) + }, + { + title: "Status", + value: environment.isLicensed ? "Licensed" : "Unlicensed", + icon: environment.isLicensed ? : , + color: environment.isLicensed ? "#52c41a" : "#ff4d4f" + }, + { + title: "API Key", + value: environment.environmentApikey ? "Configured" : "Not Set", + icon: , + color: environment.environmentApikey ? "#1890ff" : "#faad14" + }, + { + title: "Master Env", + value: environment.isMaster ? "Yes" : "No", + icon: , + color: environment.isMaster ? "#722ed1" : "#8c8c8c" + } + ]; + + const tabItems = [ + { + key: 'workspaces', + label: ( - Environments + Workspaces ), - onClick: () => history.push("/setting/environments") + children: }, { - key: 'currentEnvironment', - title: environment.environmentName + key: 'userGroups', + label: ( + + User Groups + + ), + children: } ]; return ( -
+ + {/* Breadcrumbs */} + + {/* Environment Header Component */} + {/* Stats Cards Row */} + + {statsData.map((stat, index) => ( + + + + ))} + + {/* Basic Environment Information Card */} { "No domain set" )} - - - {environment.environmentType} - + + + {environment.environmentId} + {(() => { @@ -196,29 +251,178 @@ const EnvironmentDetail: React.FC = () => { case 'licensed': return } color="green" style={{ borderRadius: '4px' }}>Licensed; case 'unlicensed': - return } color="red" style={{ borderRadius: '4px' }}>Not Licensed; + return } color="orange" style={{ borderRadius: '4px' }}>License Needed; case 'error': - return } color="orange" style={{ borderRadius: '4px' }}>License Error; + return } color="orange" style={{ borderRadius: '4px' }}>Setup Required; default: return Unknown; } })()} - - {environment.environmentApikey ? ( - Configured - ) : ( - Not Configured - )} - - - {environment.isMaster ? "Yes" : "No"} + + {environment.createdAt ? new Date(environment.createdAt).toLocaleDateString() : "Unknown"} - {/* Modern Breadcrumbs navigation */} - + history.push('/setting/environments') + }, + { + key: 'current', + title: environment.environmentName || "Environment Detail" + } + ]} + /> + {/* Detailed License Information Card - only show for licensed environments with details */} + {environment.isLicensed && environment.licenseDetails && ( + + + License Details + + } + style={{ + marginBottom: "24px", + borderRadius: '4px', + border: '1px solid #f0f0f0' + }} + className="license-details-card" + > + + {/* API Calls Status */} + + + ( + + {value?.toLocaleString()} + + )} + prefix={} + /> +
+ +
+ {environment.licenseDetails.apiCallsUsage || 0}% used +
+
+
+ + + {/* Total License Limit */} + + + value?.toLocaleString()} + prefix={} + /> + + {environment.licenseDetails.eeLicenses.length} License{environment.licenseDetails.eeLicenses.length !== 1 ? 's' : ''} + + + + + {/* Enterprise Edition Status */} + + + ( + : } + > + {value} + + )} + /> + + +
+ + {/* License Details */} +
+ + + License Information + + + + {environment.licenseDetails.eeLicenses.map((license, index) => ( + + +
+ + {license.customerName} + +
+
+ ID: {license.customerId} +
+
+ UUID: {license.uuid.substring(0, 8)}... +
+ + {license.apiCallsLimit.toLocaleString()} calls + +
+ + ))} +
+
+
+ )} + {/* Tabs for Workspaces and User Groups */} { onChange={setActiveTab} className="modern-tabs" type="line" - > - - Workspaces - - } - key="workspaces" - > - - - - - User Groups - - } - key="userGroups" - > - - - + items={tabItems} + /> {/* Edit Environment Modal */} {environment && ( @@ -261,7 +444,7 @@ const EnvironmentDetail: React.FC = () => { loading={isUpdating} /> )} -
+ ); }; diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx index 7b041b81f..36be33166 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from "react"; -import { Alert, Empty, Spin, Card } from "antd"; +import { Alert, Empty, Spin, Card, Row, Col } from "antd"; import { SyncOutlined, CloudServerOutlined } from "@ant-design/icons"; import { AddIcon, Search, TacoButton } from "lowcoder-design"; import { useHistory } from "react-router-dom"; @@ -9,6 +9,7 @@ import { fetchEnvironments } from "redux/reduxActions/enterpriseActions"; import { Environment } from "./types/environment.types"; import EnvironmentsTable from "./components/EnvironmentsTable"; import CreateEnvironmentModal from "./components/CreateEnvironmentModal"; +import StatsCard from "./components/StatsCard"; import { buildEnvironmentId } from "@lowcoder-ee/constants/routesURL"; import { createEnvironment } from "./services/environments.service"; import { getEnvironmentTagColor } from "./utils/environmentUtils"; @@ -107,37 +108,6 @@ const EnvironmentsList: React.FC = () => { return ; }; - // Stat card component - const StatCard = ({ title, value, color }: { title: string; value: number; color: string }) => ( - -
-
-
{title}
-
{value}
-
-
- {getEnvironmentIcon(title)} -
-
-
- ); - // Filter environments based on search text const filteredEnvironments = environments.filter((env) => { const searchLower = searchText.toLowerCase(); @@ -201,75 +171,65 @@ const EnvironmentsList: React.FC = () => { > Refresh - setIsCreateModalVisible(true)}> - New Environment + } + onClick={() => setIsCreateModalVisible(true)} + > + Add Environment - {/* Environment Type Statistics */} - {!isLoading && environments.length > 0 && ( - -
- {environmentStats.map(([type, count]) => ( -
- -
- ))} -
-
- )} + {/* Environment Statistics Cards */} + + + {environmentStats.map(([type, count]) => ( + + + + ))} + + - {/* Error handling */} {error && ( )} - {/* Loading, empty state or table */} - {isLoading ? ( -
- -
- ) : environments.length === 0 && !error ? ( - - ) : filteredEnvironments.length === 0 ? ( - - ) : ( - /* Table component */ + )} + + {(filteredEnvironments.length > 0 || isLoading) && ( )} - - {/* Results counter when searching */} - {searchText && filteredEnvironments.length !== environments.length && ( -
- Showing {filteredEnvironments.length} of {environments.length} environments -
- )}
- {/* Create Environment Modal */} setIsCreateModalVisible(false)} diff --git a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx index 6ed3a1427..8ca8d3294 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx @@ -4,6 +4,8 @@ import { Spin, Typography, Tabs, + Row, + Col, } from "antd"; import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"; import { @@ -12,6 +14,8 @@ import { CodeOutlined, HomeOutlined, TeamOutlined, + CloudServerOutlined, + CheckCircleOutlined, } from "@ant-design/icons"; // Use the context hooks @@ -25,9 +29,9 @@ import DataSourcesTab from "./components/DataSourcesTab"; import QueriesTab from "./components/QueriesTab"; import ModernBreadcrumbs from "./components/ModernBreadcrumbs"; import WorkspaceHeader from "./components/WorkspaceHeader"; +import StatsCard from "./components/StatsCard"; import ErrorComponent from "./components/ErrorComponent"; - -const { TabPane } = Tabs; +import { Level1SettingPageContent } from "../styled"; const WorkspaceDetail: React.FC = () => { // Use the context hooks @@ -35,7 +39,6 @@ const WorkspaceDetail: React.FC = () => { const { workspace, isLoading, error, toggleManagedStatus } = useWorkspaceContext(); const { openDeployModal } = useDeployModal(); - const [isToggling, setIsToggling] = useState(false); // Handle toggle managed status @@ -58,7 +61,17 @@ const WorkspaceDetail: React.FC = () => { if (isLoading) { return (
- + +
+ Loading workspace details... +
+
); } @@ -98,13 +111,53 @@ const WorkspaceDetail: React.FC = () => { } ]; + const tabItems = [ + { + key: 'apps', + label: ( + + Apps + + ), + children: ( + + ) + }, + { + key: 'dataSources', + label: ( + + Data Sources + + ), + children: ( + + ) + }, + { + key: 'queries', + label: ( + + Queries + + ), + children: ( + + ) + } + ]; + return ( -
+ {/* New Workspace Header */} { onDeploy={() => openDeployModal(workspace, workspaceConfig, environment)} /> + {/* Stats Cards Row */} + + + : } + color={workspace.managed ? "#52c41a" : "#faad14"} + /> + + + } + color="#1890ff" + /> + + + } + color="#722ed1" + /> + + + } + color="#52c41a" + /> + + + {/* Modern Breadcrumbs navigation */} @@ -122,29 +211,9 @@ const WorkspaceDetail: React.FC = () => { defaultActiveKey="apps" className="modern-tabs" type="line" - > - Apps} key="apps"> - - - - Data Sources} key="dataSources"> - - - Queries} key="queries"> - - - - -
+ items={tabItems} + /> + ); }; diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/ContactLowcoderModal.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/ContactLowcoderModal.tsx index 145b08e98..34ab0526f 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/ContactLowcoderModal.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/ContactLowcoderModal.tsx @@ -100,7 +100,7 @@ const ContactLowcoderModal: React.FC = ({ width={800} centered style={{ top: 20 }} - bodyStyle={{ padding: '24px' }} + styles={{ body: { padding: '24px' } }} > {/* Environment Context Section */} = ({ background: '#fafafa', border: '1px solid #f0f0f0' }} - bodyStyle={{ padding: '16px' }} + styles={{ body: { padding: '16px' } }} > diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/CreateEnvironmentModal.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/CreateEnvironmentModal.tsx index 421a001e4..8a6132c0a 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/CreateEnvironmentModal.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/CreateEnvironmentModal.tsx @@ -192,9 +192,9 @@ const CreateEnvironmentModal: React.FC = ({ diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/EditEnvironmentModal.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/EditEnvironmentModal.tsx index 593e4be2e..3b90bc826 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/EditEnvironmentModal.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/EditEnvironmentModal.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Modal, Form, Input, Select, Switch, Button, Tooltip } from 'antd'; +import { Modal, Form, Input, Select, Switch, Button, Alert, Tooltip } from 'antd'; import { useSelector } from 'react-redux'; import { selectMasterEnvironment, selectHasMasterEnvironment } from 'redux/selectors/enterpriseSelectors'; import { Environment } from '../types/environment.types'; @@ -191,7 +191,13 @@ const EditEnvironmentModal: React.FC = ({ - + ); diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentHeader.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentHeader.tsx index 0a999129e..43ecb5fc0 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentHeader.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentHeader.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { Button, Tag, Typography, Row, Col } from 'antd'; import { EditOutlined, CloudServerOutlined } from '@ant-design/icons'; import { Environment } from '../types/environment.types'; -import { getEnvironmentTagColor, getEnvironmentHeaderGradient } from '../utils/environmentUtils'; +import { getEnvironmentTagColor } from '../utils/environmentUtils'; const { Title, Text } = Typography; @@ -24,11 +24,11 @@ const EnvironmentHeader: React.FC = ({ className="environment-header" style={{ marginBottom: "24px", - background: getEnvironmentHeaderGradient(environment.environmentType), + background: '#fff', padding: '20px 24px', borderRadius: '8px', - color: 'white', - boxShadow: '0 2px 8px rgba(0,0,0,0.1)' + border: '1px solid #f0f0f0', + boxShadow: '0 2px 8px rgba(0,0,0,0.06)' }} > @@ -36,35 +36,50 @@ const EnvironmentHeader: React.FC = ({
- + <Title level={3} style={{ margin: '0 0 8px 0', color: '#222222', fontWeight: '500' }}> {environment.environmentName || "Unnamed Environment"}
- + ID: {environment.environmentId} {environment.environmentType} {environment.isMaster && ( - + Master )} + {environment.isLicensed === false && ( + + Unlicensed + + )}
@@ -73,14 +88,10 @@ const EnvironmentHeader: React.FC = ({ -
- - {/* Footer Help Text */} - - Need assistance? Contact our team for licensing support or edit the environment configuration to resolve this issue. - - - - + + + + + + {/* Help Text */} + + + Need assistance? Contact our team for licensing support or edit the environment configuration to resolve this issue. + + {/* Contact Lowcoder Modal */} = ({ onClose={() => setIsContactModalVisible(false)} environment={environment} /> - + ); }; diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/WorkspaceHeader.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/WorkspaceHeader.tsx index f4c00c22e..462e6bf35 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/WorkspaceHeader.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/WorkspaceHeader.tsx @@ -7,144 +7,20 @@ import { Tooltip, Row, Col, - Statistic, Avatar, Space, - Divider, - Card, - Dropdown, - Menu } from "antd"; import { CloudUploadOutlined, - SettingOutlined, TeamOutlined, - AppstoreOutlined, - DatabaseOutlined, - CodeOutlined, CloudServerOutlined, ClockCircleOutlined, - MoreOutlined, - StarOutlined, - StarFilled } from "@ant-design/icons"; import { Environment } from "../types/environment.types"; import { Workspace } from "../types/workspace.types"; -import styled from "styled-components"; const { Title, Text } = Typography; -// Styled components for custom design -const HeaderWrapper = styled.div` - border-radius: 12px; - overflow: hidden; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); - position: relative; - margin-bottom: 24px; -`; - -const GradientBanner = styled.div<{ avatarColor: string }>` - background: linear-gradient(135deg, ${props => props.avatarColor} 0%, rgba(24, 144, 255, 0.8) 100%); - height: 140px; - position: relative; - overflow: hidden; - transition: background 1s ease-in-out; - - &::before { - content: ''; - position: absolute; - top: -50%; - left: -50%; - width: 200%; - height: 200%; - background: repeating-linear-gradient( - 45deg, - rgba(255,255,255,0.1), - rgba(255,255,255,0.1) 1px, - transparent 1px, - transparent 10px - ); - animation: moveBackground 30s linear infinite; - } - - @keyframes moveBackground { - 0% { - transform: translate(0, 0); - } - 100% { - transform: translate(100px, 100px); - } - } - - &:hover { - background: linear-gradient(135deg, rgba(24, 144, 255, 0.8) 0%, ${props => props.avatarColor} 100%); - transition: background 1s ease-in-out; - } -`; - -const ContentContainer = styled.div` - background-color: white; - padding: 24px; - position: relative; - transition: transform 0.3s ease-in-out; - - &:hover { - transform: translateY(-2px); - } -`; - -const AvatarContainer = styled.div` - position: absolute; - top: -50px; - left: 24px; - background: white; - padding: 4px; - border-radius: 8px; - border: 1px solid #f0f0f0; -`; - -const StatusBadge = styled(Tag)<{ $active?: boolean }>` - position: absolute; - top: 12px; - right: 12px; - font-weight: 500; - font-size: 12px; - padding: 4px 12px; - border-radius: 4px; - border: none; - background: ${props => props.$active ? '#52c41a' : '#f5f5f5'}; - color: ${props => props.$active ? 'white' : '#8c8c8c'}; -`; - -const StatCard = styled(Card)` - border-radius: 4px; - border: 1px solid #f0f0f0; - transition: all 0.3s; - - &:hover { - transform: translateY(-2px); - border-color: #d9d9d9; - } -`; - -const ActionButton = styled(Button)` - border-radius: 4px; - display: flex; - align-items: center; - justify-content: center; - height: 32px; -`; - -const FavoriteButton = styled(Button)` - position: absolute; - top: 12px; - right: 80px; - border: none; - border-radius: 4px; - background: rgba(255, 255, 255, 0.9); - color: #722ed1; -`; - interface WorkspaceHeaderProps { workspace: Workspace; environment: Environment; @@ -172,7 +48,7 @@ const WorkspaceHeader: React.FC = ({ }; // Format date for last updated - const formatDate = (date: number | undefined) => { + const formatDate = (date: number | undefined) => { if (!date) return "N/A"; return new Date(date).toLocaleDateString("en-US", { month: "short", @@ -181,81 +57,97 @@ const WorkspaceHeader: React.FC = ({ }); }; - - - - return ( - - - - {workspace.managed ? "Managed" : "Unmanaged"} - - - - - - - - {workspace.name.charAt(0).toUpperCase()} - - - - - - - {workspace.name} - - - ID: {workspace.id} - - - created on {formatDate(workspace.creationDate)} - - - {environment.environmentName} - - - - - -
-
- Managed: - -
- - } - onClick={onDeploy} - disabled={!workspace.managed} +
+ + +
+ + {workspace.name.charAt(0).toUpperCase()} + +
+ + {workspace.name} + +
+ + ID: {workspace.id} + + + + {formatDate(workspace.creationDate)} + + + + {environment.environmentName} + + - Deploy - - + {workspace.managed ? 'Managed' : 'Unmanaged'} + +
- - - - - - - +
+ + +
+
+ Managed: + +
+ + + +
+ +
+
); }; diff --git a/client/packages/lowcoder/src/pages/setting/environments/config/data-sources.config.tsx b/client/packages/lowcoder/src/pages/setting/environments/config/data-sources.config.tsx index 591621862..ce802e8de 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/config/data-sources.config.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/config/data-sources.config.tsx @@ -5,18 +5,10 @@ import { DataSource} from '../types/datasource.types'; import { Environment } from '../types/environment.types'; import { deployDataSource, DataSourceStats } from '../services/datasources.service'; - - export const dataSourcesConfig: DeployableItemConfig = { deploy: { singularLabel: 'Data Source', fields: [ - { - name: 'updateDependenciesIfNeeded', - label: 'Update Dependencies If Needed', - type: 'checkbox', - defaultValue: false - }, { name: 'deployCredential', label: 'Overwrite Credentials', @@ -29,7 +21,6 @@ export const dataSourcesConfig: DeployableItemConfig = { envId: sourceEnv.environmentId, targetEnvId: targetEnv.environmentId, datasourceId: item.id, - updateDependenciesIfNeeded: values.updateDependenciesIfNeeded, datasourceGid: item.gid, deployCredential: values.deployCredential ?? false }; diff --git a/client/packages/lowcoder/src/pages/setting/environments/config/query.config.tsx b/client/packages/lowcoder/src/pages/setting/environments/config/query.config.tsx index 35189c8c9..7495e5371 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/config/query.config.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/config/query.config.tsx @@ -4,17 +4,14 @@ import { Query } from '../types/query.types'; import { deployQuery } from '../services/query.service'; import { Environment } from '../types/environment.types'; - - - export const queryConfig: DeployableItemConfig = { deploy: { singularLabel: 'Query', fields: [ { - name: 'updateDependenciesIfNeeded', - label: 'Update Dependencies If Needed', + name: 'deployCredential', + label: 'Overwrite Credentials', type: 'checkbox', defaultValue: false } @@ -24,8 +21,8 @@ export const queryConfig: DeployableItemConfig = { envId: sourceEnv.environmentId, targetEnvId: targetEnv.environmentId, queryId: item.id, - updateDependenciesIfNeeded: values.updateDependenciesIfNeeded, queryGid: item.gid, + deployCredential: values.deployCredential ?? false }; }, execute: (params: any) => deployQuery(params) diff --git a/client/packages/lowcoder/src/pages/setting/environments/config/workspace.config.tsx b/client/packages/lowcoder/src/pages/setting/environments/config/workspace.config.tsx index 6689931f9..e35423f2f 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/config/workspace.config.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/config/workspace.config.tsx @@ -5,32 +5,23 @@ import { Environment } from '../types/environment.types'; import { deployWorkspace } from '../services/workspace.service'; import { Workspace } from '../types/workspace.types'; - - export const workspaceConfig: DeployableItemConfig = { // Deploy configuration deploy: { singularLabel: 'Workspace', fields: [ - { - name: 'deployCredential', - label: 'Overwrite Credentials', - type: 'checkbox', - defaultValue: false - } + // Removed deployCredential field as workspaces don't need credential overwrite ], prepareParams: (item: Workspace, values: any, sourceEnv: Environment, targetEnv: Environment) => { if (!item.gid) { console.error('Missing workspace.gid when deploying workspace:', item); } - console.log('item.gid', item.gid); return { envId: sourceEnv.environmentId, targetEnvId: targetEnv.environmentId, workspaceId: item.gid, - deployCredential: values.deployCredential ?? false }; }, execute: (params: any) => deployWorkspace(params) diff --git a/client/packages/lowcoder/src/pages/setting/environments/services/datasources.service.ts b/client/packages/lowcoder/src/pages/setting/environments/services/datasources.service.ts index e743179b0..2fc492028 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/services/datasources.service.ts +++ b/client/packages/lowcoder/src/pages/setting/environments/services/datasources.service.ts @@ -21,7 +21,6 @@ export interface DeployDataSourceParams { targetEnvId: string; datasourceId: string; datasourceGid: string; - updateDependenciesIfNeeded?: boolean; deployCredential: boolean; } // Get data sources for a workspace - using your correct implementation @@ -158,7 +157,6 @@ export async function deployDataSource(params: DeployDataSourceParams): Promise< envId: params.envId, targetEnvId: params.targetEnvId, datasourceId: params.datasourceId, - updateDependenciesIfNeeded: params.updateDependenciesIfNeeded ?? false, deployCredential: params.deployCredential } }); diff --git a/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts b/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts index eb11609f5..b3ccb5314 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts +++ b/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts @@ -136,6 +136,7 @@ export async function getEnvironmentById(id: string): Promise { envWithLicense.isLicensed = licenseInfo.isValid; envWithLicense.licenseStatus = licenseInfo.isValid ? 'licensed' : 'unlicensed'; envWithLicense.licenseError = licenseInfo.error; + envWithLicense.licenseDetails = licenseInfo.details; } else { envWithLicense.isLicensed = false; envWithLicense.licenseStatus = 'error'; @@ -556,6 +557,7 @@ export async function getEnvironmentsWithLicenseStatus(): Promise envWithLicense.isLicensed = licenseInfo.isValid; envWithLicense.licenseStatus = licenseInfo.isValid ? 'licensed' : 'unlicensed'; envWithLicense.licenseError = licenseInfo.error; + envWithLicense.licenseDetails = licenseInfo.details; } else { envWithLicense.isLicensed = false; envWithLicense.licenseStatus = 'error'; diff --git a/client/packages/lowcoder/src/pages/setting/environments/services/license.service.ts b/client/packages/lowcoder/src/pages/setting/environments/services/license.service.ts index ff0be0ce6..ebc0ae46a 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/services/license.service.ts +++ b/client/packages/lowcoder/src/pages/setting/environments/services/license.service.ts @@ -1,11 +1,11 @@ import axios from 'axios'; -import { EnvironmentLicense } from '../types/environment.types'; +import { EnvironmentLicense, DetailedLicenseInfo } from '../types/environment.types'; /** - * Check if license endpoint exists for an environment + * Check license and fetch detailed license information for an environment * @param apiServiceUrl - API service URL for the environment * @param apiKey - API key for the environment - * @returns Promise with license information + * @returns Promise with license information including detailed data */ export async function checkEnvironmentLicense( apiServiceUrl: string, @@ -25,8 +25,8 @@ export async function checkEnvironmentLicense( headers.Authorization = `Bearer ${apiKey}`; } - // Use GET request to check endpoint existence - await axios.get( + // Fetch detailed license information + const response = await axios.get( `${apiServiceUrl}/api/plugins/enterprise/license`, { headers, @@ -34,16 +34,84 @@ export async function checkEnvironmentLicense( } ); - // If we get a successful response, the endpoint exists + // Parse the license response + const licenseData = response.data; + + // Calculate total API calls limit and usage percentage + const totalAPICallsLimit = licenseData.eeLicenses?.reduce( + (sum: number, license: any) => sum + (license.apiCallsLimit || 0), + 0 + ) || 0; + + const apiCallsUsage = totalAPICallsLimit > 0 + ? Math.round(((totalAPICallsLimit - licenseData.remainingAPICalls) / totalAPICallsLimit) * 100) + : 0; + + const licenseDetails: DetailedLicenseInfo = { + eeActive: licenseData.eeActive || false, + remainingAPICalls: licenseData.remainingAPICalls || 0, + eeLicenses: licenseData.eeLicenses || [], + totalAPICallsLimit, + apiCallsUsage + }; + + // Determine if license is valid based on enterprise edition status and remaining calls + const isValid = licenseDetails.eeActive && licenseDetails.remainingAPICalls > 0; + return { - isValid: true + isValid, + details: licenseDetails }; } catch (error) { - // Any error means the endpoint doesn't exist or isn't accessible + // Determine the specific error type + let errorMessage = 'License information unavailable'; + + if (axios.isAxiosError(error)) { + if (error.code === 'ECONNABORTED') { + errorMessage = 'License check took too long'; + } else if (error.response?.status === 404) { + errorMessage = 'License service not available'; + } else if (error.response?.status === 401) { + errorMessage = 'Authentication required - please check API key'; + } else if (error.response && error.response.status >= 500) { + errorMessage = 'License service temporarily unavailable'; + } + } + return { isValid: false, - error: 'License not available' + error: errorMessage }; } +} + +/** + * Format API calls for display + * @param remaining - Remaining API calls + * @param total - Total API calls limit + * @returns Formatted string + */ +export function formatAPICalls(remaining: number, total: number): string { + const used = total - remaining; + const percentage = total > 0 ? Math.round((used / total) * 100) : 0; + + return `${remaining.toLocaleString()} remaining (${used.toLocaleString()}/${total.toLocaleString()} used, ${percentage}%)`; +} + +/** + * Get API calls status color based on usage percentage - using softer, less aggressive colors + * @param remainingCalls - Remaining API calls + * @param totalCalls - Total API calls limit + * @returns Color string for UI components + */ +export function getAPICallsStatusColor(remainingCalls: number, totalCalls: number): string { + if (totalCalls === 0) return '#d9d9d9'; // Unknown + + const usagePercentage = ((totalCalls - remainingCalls) / totalCalls) * 100; + + if (usagePercentage >= 90) return '#ff7875'; // Soft red - High usage + if (usagePercentage >= 75) return '#ffc53d'; // Soft orange - Moderate usage + if (usagePercentage >= 50) return '#40a9ff'; // Soft blue - Normal usage + return '#73d13d'; // Soft green - Low usage } \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/services/query.service.ts b/client/packages/lowcoder/src/pages/setting/environments/services/query.service.ts index 4cf7a29be..20b79f4ee 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/services/query.service.ts +++ b/client/packages/lowcoder/src/pages/setting/environments/services/query.service.ts @@ -14,8 +14,8 @@ export interface MergedQueriesResult { envId: string; targetEnvId: string; queryId: string; - updateDependenciesIfNeeded?: boolean; queryGid: string; + deployCredential: boolean; } @@ -71,7 +71,7 @@ export interface MergedQueriesResult { envId: params.envId, targetEnvId: params.targetEnvId, queryId: params.queryId, - updateDependenciesIfNeeded: params.updateDependenciesIfNeeded ?? false + deployCredential: params.deployCredential } }); if (response.status === 200) { diff --git a/client/packages/lowcoder/src/pages/setting/environments/services/workspace.service.ts b/client/packages/lowcoder/src/pages/setting/environments/services/workspace.service.ts index dec7c7cf7..b7cf4a37c 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/services/workspace.service.ts +++ b/client/packages/lowcoder/src/pages/setting/environments/services/workspace.service.ts @@ -84,7 +84,6 @@ export async function deployWorkspace(params: { envId: string; targetEnvId: string; workspaceId: string; - deployCredential: boolean; // Mandatory parameter }): Promise { try { // Use the new endpoint format with only essential parameters @@ -93,7 +92,6 @@ export async function deployWorkspace(params: { orgGid: params.workspaceId, // Using workspaceId as orgGid envId: params.envId, targetEnvId: params.targetEnvId, - deployCredential: params.deployCredential } }); @@ -107,7 +105,6 @@ export async function deployWorkspace(params: { ); } - return response.status === 200; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Failed to deploy workspace'; diff --git a/client/packages/lowcoder/src/pages/setting/environments/types/environment.types.ts b/client/packages/lowcoder/src/pages/setting/environments/types/environment.types.ts index 4660ea38b..1cca58f7e 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/types/environment.types.ts +++ b/client/packages/lowcoder/src/pages/setting/environments/types/environment.types.ts @@ -18,6 +18,8 @@ export interface Environment { isLicensed?: boolean; licenseStatus?: 'checking' | 'licensed' | 'unlicensed' | 'error'; licenseError?: string; + // Enhanced license details + licenseDetails?: DetailedLicenseInfo; } /** @@ -26,4 +28,28 @@ export interface Environment { export interface EnvironmentLicense { isValid: boolean; error?: string; + // Enhanced license details + details?: DetailedLicenseInfo; +} + +/** + * Interface representing detailed license information from the license endpoint + */ +export interface DetailedLicenseInfo { + eeActive: boolean; + remainingAPICalls: number; + eeLicenses: LicenseEntry[]; + // Calculated fields + totalAPICallsLimit?: number; + apiCallsUsage?: number; // percentage used +} + +/** + * Interface representing a single license entry + */ +export interface LicenseEntry { + uuid: string; + customerId: string; + customerName: string; + apiCallsLimit: number; } \ No newline at end of file