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 }) => (
-
-
-
-
- {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 = ({
-
+
{environment.environmentName || "Unnamed Environment"}
-
+
ID: {environment.environmentId}
{environment.environmentType}
{environment.isMaster && (
-
+
Master
)}
+ {environment.isLicensed === false && (
+
+ Unlicensed
+
+ )}
@@ -73,14 +88,10 @@ const EnvironmentHeader: React.FC = ({
}
onClick={onEditClick}
- type="default"
- size="large"
+ type="primary"
style={{
- background: 'white',
- color: '#1890ff',
- borderColor: 'white',
- fontWeight: 500,
- boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
+ fontWeight: '500',
+ borderRadius: '4px'
}}
>
Edit Environment
diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentsTable.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentsTable.tsx
index 509cb509a..2336c8d5d 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentsTable.tsx
+++ b/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentsTable.tsx
@@ -1,8 +1,9 @@
import React from 'react';
-import { Table, Tag, Button, Tooltip, Space, Card, Row, Col, Typography, Avatar, Spin, Alert } from 'antd';
-import { EditOutlined, AuditOutlined, LinkOutlined, EnvironmentOutlined, StarFilled, CloudServerOutlined, CheckCircleOutlined, CloseCircleOutlined, ExclamationCircleOutlined, SyncOutlined } from '@ant-design/icons';
+import { Table, Tag, Button, Tooltip, Space, Card, Row, Col, Typography, Avatar, Spin, Alert, Progress } from 'antd';
+import { EditOutlined, AuditOutlined, LinkOutlined, EnvironmentOutlined, StarFilled, CloudServerOutlined, CheckCircleOutlined, CloseCircleOutlined, ExclamationCircleOutlined, SyncOutlined, ApiOutlined } from '@ant-design/icons';
import { Environment } from '../types/environment.types';
import { getEnvironmentTagColor, formatEnvironmentType } from '../utils/environmentUtils';
+import { getAPICallsStatusColor } from '../services/license.service';
const { Text, Title } = Typography;
@@ -54,29 +55,29 @@ const EnvironmentsTable: React.FC = ({
case 'checking':
return {
icon: ,
- color: '#1890ff',
+ color: '#40a9ff',
text: 'Checking...',
status: 'processing' as const
};
case 'licensed':
return {
icon: ,
- color: '#52c41a',
+ color: '#73d13d',
text: 'Licensed',
status: 'success' as const
};
case 'unlicensed':
return {
icon: ,
- color: '#ff4d4f',
- text: 'Not Licensed',
- status: 'error' as const
+ color: '#ff7875',
+ text: 'License Required',
+ status: 'warning' as const
};
case 'error':
return {
icon: ,
- color: '#faad14',
- text: 'License Error',
+ color: '#ffc53d',
+ text: 'Setup Required',
status: 'warning' as const
};
default:
@@ -112,7 +113,7 @@ const EnvironmentsTable: React.FC = ({
border: '1px solid #f0f0f0',
position: 'relative'
}}
- bodyStyle={{ padding: '16px' }}
+ styles={{ body: { padding: '16px' } }}
onClick={() => handleRowClick(env)}
>
{/* Subtle overlay for unlicensed environments */}
@@ -179,8 +180,8 @@ const EnvironmentsTable: React.FC = ({
{licenseDisplay.text}
@@ -267,6 +268,37 @@ const EnvironmentsTable: React.FC = ({
+
+ {/* API Calls Information - show if license details are available */}
+ {env.licenseDetails && (
+
+
+
+
+ {env.licenseDetails.apiCallsUsage || 0}% used
+
+
+ )}
diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/ModernBreadcrumbs.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/ModernBreadcrumbs.tsx
index 1a3d35524..0403be24b 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/components/ModernBreadcrumbs.tsx
+++ b/client/packages/lowcoder/src/pages/setting/environments/components/ModernBreadcrumbs.tsx
@@ -2,7 +2,7 @@ import React, { ReactNode } from 'react';
import { Breadcrumb } from 'antd';
import { BreadcrumbProps } from 'antd/lib/breadcrumb';
-interface ModernBreadcrumbsProps extends BreadcrumbProps {
+interface ModernBreadcrumbsProps extends Omit {
/**
* Items to display in the breadcrumb
*/
@@ -17,36 +17,52 @@ interface ModernBreadcrumbsProps extends BreadcrumbProps {
* Modern styled breadcrumb component with consistent styling
*/
const ModernBreadcrumbs: React.FC = ({ items = [], ...props }) => {
+ // Convert custom items format to Antd's expected format
+ const breadcrumbItems = items.map(item => ({
+ key: item.key,
+ title: item.onClick ? (
+ {
+ e.currentTarget.style.color = '#096dd9';
+ e.currentTarget.style.textDecoration = 'underline';
+ }}
+ onMouseLeave={(e) => {
+ e.currentTarget.style.color = '#1890ff';
+ e.currentTarget.style.textDecoration = 'none';
+ }}
+ >
+ {item.title}
+
+ ) : (
+
+ {item.title}
+
+ )
+ }));
+
return (
-
- {items.map(item => (
-
- {item.onClick ? (
- e.currentTarget.style.textDecoration = 'underline'}
- onMouseLeave={(e) => e.currentTarget.style.textDecoration = 'none'}
- >
- {item.title}
-
- ) : (
-
- {item.title}
-
- )}
-
- ))}
-
+ /}
+ items={breadcrumbItems}
+ />
);
};
diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/StatsCard.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/StatsCard.tsx
new file mode 100644
index 000000000..214a60106
--- /dev/null
+++ b/client/packages/lowcoder/src/pages/setting/environments/components/StatsCard.tsx
@@ -0,0 +1,74 @@
+import React from 'react';
+import { Card } from 'antd';
+
+interface StatsCardProps {
+ title: string;
+ value: number | string;
+ icon?: React.ReactNode;
+ color?: string;
+ className?: string;
+ style?: React.CSSProperties;
+}
+
+/**
+ * Reusable StatsCard component for displaying environment statistics
+ * Used across all Environment pages for consistency
+ */
+const StatsCard: React.FC = ({
+ title,
+ value,
+ icon,
+ color = '#1890ff',
+ className = '',
+ style = {}
+}) => {
+ return (
+
+
+
+
+ {title}
+
+
+ {value}
+
+
+ {icon && (
+
+ {icon}
+
+ )}
+
+
+ );
+};
+
+export default StatsCard;
\ No newline at end of file
diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/UnlicensedEnvironmentView.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/UnlicensedEnvironmentView.tsx
index 6b61379dd..7388ae810 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/components/UnlicensedEnvironmentView.tsx
+++ b/client/packages/lowcoder/src/pages/setting/environments/components/UnlicensedEnvironmentView.tsx
@@ -6,10 +6,15 @@ import {
ArrowLeftOutlined,
CloseCircleOutlined,
ExclamationCircleOutlined,
- WarningOutlined
+ WarningOutlined,
+ CloudServerOutlined
} from '@ant-design/icons';
import { Environment } from '../types/environment.types';
import ContactLowcoderModal from './ContactLowcoderModal';
+import ModernBreadcrumbs from './ModernBreadcrumbs';
+import EnvironmentHeader from './EnvironmentHeader';
+import StatsCard from './StatsCard';
+import { Level1SettingPageContent } from "../../styled";
import history from "@lowcoder-ee/util/history";
const { Title, Text } = Typography;
@@ -20,7 +25,7 @@ interface UnlicensedEnvironmentViewProps {
}
/**
- * Modern UI for unlicensed environments
+ * Consistent UI for unlicensed environments matching other environment pages
*/
const UnlicensedEnvironmentView: React.FC = ({
environment,
@@ -31,118 +36,136 @@ const UnlicensedEnvironmentView: React.FC = ({
const getLicenseIcon = () => {
switch (environment.licenseStatus) {
case 'unlicensed':
- return ;
+ return ;
case 'error':
- return ;
+ return ;
default:
- return ;
+ return ;
}
};
const getLicenseTitle = () => {
- switch (environment.licenseStatus) {
- case 'unlicensed':
- return 'Environment Not Licensed';
- case 'error':
- return 'License Configuration Error';
- default:
- return 'License Issue';
- }
- };
+ return environment.licenseError;
+ }
+
+
const getLicenseDescription = () => {
- if (environment.licenseError) {
- return environment.licenseError;
- }
switch (environment.licenseStatus) {
case 'unlicensed':
- return 'This environment requires a valid license to access its features and functionality.';
+ return 'This environment needs a valid license to unlock its full capabilities and features. Please make sure your API Service URL is correctly configured and Plugin is installed.';
case 'error':
- return 'There was an error validating the license for this environment. Please check the configuration.';
+ return 'We encountered an issue while checking the license. Please review the configuration settings.';
default:
- return 'This environment has license-related issues that need to be resolved.';
+ return 'This environment requires license configuration to proceed.';
}
};
+ // Stats data consistent with other environment pages
+ const statsData = [
+ {
+ title: "Type",
+ value: environment.environmentType || "Unknown",
+ icon: ,
+ color: "#1890ff"
+ },
+ {
+ title: "Status",
+ value: "Unlicensed",
+ icon: ,
+ color: "#ff4d4f"
+ },
+ {
+ title: "Master Env",
+ value: environment.isMaster ? "Yes" : "No",
+ icon: ,
+ color: environment.isMaster ? "#722ed1" : "#8c8c8c"
+ },
+ {
+ title: "License Issue",
+ value: environment.licenseStatus === 'error' ? "Error" : "Missing",
+ icon: environment.licenseStatus === 'error' ? : ,
+ color: environment.licenseStatus === 'error' ? "#faad14" : "#ff4d4f"
+ }
+ ];
+
return (
-
-
-
-
- {/* Main Status Card */}
-
+
+ {/* Environment Header Component */}
+
+
+ {/* Stats Cards Row */}
+
+ {statsData.map((stat, index) => (
+
+
+
+ ))}
+
+
+ {/* Breadcrumbs */}
+ history.push('/setting/environments')
+ },
+ {
+ key: 'current',
+ title: environment.environmentName || "Environment Detail"
+ }
+ ]}
+ />
+
+ {/* License Issue Card */}
+
+
+
+
{/* Status Icon */}
{getLicenseIcon()}
- {/* Environment Info */}
-
-
- {getLicenseTitle()}
-
-
- {getLicenseDescription()}
-
-
- {/* Environment Details */}
-
- Environment:
-
- {environment.environmentName || 'Unnamed Environment'}
-
-
- ID: {environment.environmentId}
-
-
-
+ {/* License Issue Information */}
+
+ {getLicenseTitle()}
+
+
+ {getLicenseDescription()}
+
{/* Action Buttons */}
-
+
= ({
style={{
width: '100%',
height: '48px',
- borderRadius: '8px',
+ borderRadius: '4px',
fontSize: '16px',
- fontWeight: 500,
- background: 'linear-gradient(135deg, #1890ff 0%, #0050b3 100%)',
- border: 'none',
- boxShadow: '0 4px 12px rgba(24, 144, 255, 0.3)'
+ fontWeight: 500
}}
>
Contact Lowcoder Team
@@ -169,11 +189,9 @@ const UnlicensedEnvironmentView: React.FC = ({
style={{
width: '100%',
height: '48px',
- borderRadius: '8px',
+ borderRadius: '4px',
fontSize: '16px',
- fontWeight: 500,
- borderColor: '#d9d9d9',
- color: '#595959'
+ fontWeight: 500
}}
>
Edit Environment
@@ -186,31 +204,38 @@ const UnlicensedEnvironmentView: React.FC = ({
style={{
width: '100%',
height: '48px',
- borderRadius: '8px',
+ borderRadius: '4px',
fontSize: '16px',
- fontWeight: 500,
- borderColor: '#d9d9d9',
- color: '#8c8c8c'
+ fontWeight: 500
}}
>
Back to Environments
-
-
- {/* 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:
+
+
+
+ }
+ onClick={onDeploy}
+ disabled={!workspace.managed}
+ style={{
+ fontWeight: '500',
+ borderRadius: '4px'
+ }}
+ >
+ Deploy
+
+
+
+
+
+
);
};
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