diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx index 89f32f495..3f019f615 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx @@ -1,4 +1,4 @@ -import React, {useState} from "react"; +import React, {useState, useEffect} from "react"; import { Spin, Typography, @@ -9,6 +9,7 @@ import { Menu, Button, Tag, + Result, } from "antd"; import { LinkOutlined, @@ -16,10 +17,15 @@ import { AppstoreOutlined, UsergroupAddOutlined, EditOutlined, + CheckCircleOutlined, + CloseCircleOutlined, + ExclamationCircleOutlined, + SyncOutlined, } from "@ant-design/icons"; import { useSingleEnvironmentContext } from "./context/SingleEnvironmentContext"; import EditEnvironmentModal from "./components/EditEnvironmentModal"; +import UnlicensedEnvironmentView from "./components/UnlicensedEnvironmentView"; import { Environment } from "./types/environment.types"; import history from "@lowcoder-ee/util/history"; import WorkspacesTab from "./components/WorkspacesTab"; @@ -95,6 +101,29 @@ const EnvironmentDetail: React.FC = () => { ); } + // Check if environment is not licensed and show modern UI + if (environment.isLicensed === false) { + return ( + <> + + + {/* Edit Environment Modal */} + {environment && ( + + )} + + ); + } + const breadcrumbItems = [ { key: 'environments', @@ -114,7 +143,7 @@ const EnvironmentDetail: React.FC = () => { return (
{/* Environment Header Component */} { {environment.environmentType} + + {(() => { + switch (environment.licenseStatus) { + case 'checking': + return } color="blue" style={{ borderRadius: '12px' }}>Checking...; + case 'licensed': + return } color="green" style={{ borderRadius: '12px' }}>Licensed; + case 'unlicensed': + return } color="red" style={{ borderRadius: '12px' }}>Not Licensed; + case 'error': + return } color="orange" style={{ borderRadius: '12px' }}>License Error; + default: + return Unknown; + } + })()} + {environment.environmentApikey ? ( Configured diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx index cc7a9dc00..8d8fa2c43 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx @@ -1,252 +1,302 @@ -import React, { useState } from "react"; -import { Typography, Alert, Input, Button, Space, Empty, Card, Spin, Row, Col, Tooltip, Badge } from "antd"; -import { SearchOutlined, CloudServerOutlined, SyncOutlined} from "@ant-design/icons"; -import { useHistory } from "react-router-dom"; -import { useEnvironmentContext } from "./context/EnvironmentContext"; -import { Environment } from "./types/environment.types"; -import EnvironmentsTable from "./components/EnvironmentsTable"; -import { buildEnvironmentId } from "@lowcoder-ee/constants/routesURL"; -import { getEnvironmentTagColor } from "./utils/environmentUtils"; - -const { Title, Text } = Typography; - -/** - * Environment Listing Page Component - * Displays a table of environments - */ -const EnvironmentsList: React.FC = () => { - // Use the shared context instead of a local hook - const { - environments, - isLoading, - error, - refreshEnvironments - } = useEnvironmentContext(); - - // State for search input - const [searchText, setSearchText] = useState(""); - const [isRefreshing, setIsRefreshing] = useState(false); - - // Hook for navigation - const history = useHistory(); - - // Filter environments based on search text - const filteredEnvironments = environments.filter((env) => { - const searchLower = searchText.toLowerCase(); - return ( - (env.environmentName || "").toLowerCase().includes(searchLower) || - (env.environmentFrontendUrl || "").toLowerCase().includes(searchLower) || - env.environmentId.toLowerCase().includes(searchLower) || - env.environmentType.toLowerCase().includes(searchLower) - ); - }); - - // Handle row click to navigate to environment detail - const handleRowClick = (record: Environment) => { - history.push(buildEnvironmentId(record.environmentId)); - }; - - // Handle refresh - const handleRefresh = async () => { - setIsRefreshing(true); - await refreshEnvironments(); - setIsRefreshing(false); - }; - - // Count environment types - const environmentCounts = environments.reduce((counts, env) => { - const type = env.environmentType.toUpperCase(); - counts[type] = (counts[type] || 0) + 1; - return counts; - }, {} as Record); - - return ( -
- {/* Modern gradient header */} -
- - -
-
- -
-
- - Environments - - - Manage your deployment environments across dev, test, preprod, and production - -
-
- - - - - - -
-
- - {/* Environment type stats */} - {environments.length > 0 && ( - - - - - - -
-
- {environments.length} -
-
- Total Environments -
-
-
- - - {['PROD', 'PREPROD', 'TEST', 'DEV'].map(type => ( - - -
-
- {environmentCounts[type] || 0} -
-
- - {type} Environments -
-
-
- - ))} -
-
- -
- )} - - {/* Main content card */} - setSearchText(e.target.value)} - style={{ width: 250 }} - prefix={} - allowClear - /> - } - > - {/* Error handling */} - {error && ( - - )} - - {/* Loading, empty state or table */} - {isLoading ? ( -
- -
- ) : environments.length === 0 && !error ? ( - - ) : filteredEnvironments.length === 0 ? ( - - ) : ( - /* Table component */ - - )} - - {/* Results counter when searching */} - {searchText && filteredEnvironments.length !== environments.length && ( -
- Showing {filteredEnvironments.length} of {environments.length} environments -
- )} -
-
- ); -}; - +import React, { useState } from "react"; +import { Typography, Alert, Input, Button, Space, Empty, Card, Spin, Row, Col, Tooltip, Badge } from "antd"; +import { SearchOutlined, CloudServerOutlined, SyncOutlined, PlusOutlined} from "@ant-design/icons"; +import { useHistory } from "react-router-dom"; +import { useEnvironmentContext } from "./context/EnvironmentContext"; +import { Environment } from "./types/environment.types"; +import EnvironmentsTable from "./components/EnvironmentsTable"; +import CreateEnvironmentModal from "./components/CreateEnvironmentModal"; +import { buildEnvironmentId } from "@lowcoder-ee/constants/routesURL"; +import { getEnvironmentTagColor } from "./utils/environmentUtils"; +import { createEnvironment } from "./services/environments.service"; + +const { Title, Text } = Typography; + +/** + * Environment Listing Page Component + * Displays a table of environments + */ +const EnvironmentsList: React.FC = () => { + // Use the shared context instead of a local hook + const { + environments, + isLoading, + error, + refreshEnvironments + } = useEnvironmentContext(); + + // State for search input + const [searchText, setSearchText] = useState(""); + const [isRefreshing, setIsRefreshing] = useState(false); + const [isCreateModalVisible, setIsCreateModalVisible] = useState(false); + const [isCreating, setIsCreating] = useState(false); + + // Hook for navigation + const history = useHistory(); + + // Filter environments based on search text + const filteredEnvironments = environments.filter((env) => { + const searchLower = searchText.toLowerCase(); + return ( + (env.environmentName || "").toLowerCase().includes(searchLower) || + (env.environmentFrontendUrl || "").toLowerCase().includes(searchLower) || + env.environmentId.toLowerCase().includes(searchLower) || + env.environmentType.toLowerCase().includes(searchLower) + ); + }).sort((a, b) => { + // Sort by license status: licensed environments first + const aLicensed = a.isLicensed !== false; // licensed or unknown (default to licensed) + const bLicensed = b.isLicensed !== false; // licensed or unknown (default to licensed) + + if (aLicensed && !bLicensed) return -1; // a licensed, b unlicensed - a comes first + if (!aLicensed && bLicensed) return 1; // a unlicensed, b licensed - b comes first + + // If both have same license status, sort by environment name + return (a.environmentName || "").localeCompare(b.environmentName || ""); + }); + + // Handle row click to navigate to environment detail + const handleRowClick = (record: Environment) => { + // Allow navigation to all environments including unlicensed ones + history.push(buildEnvironmentId(record.environmentId)); + }; + + // Handle refresh + const handleRefresh = async () => { + setIsRefreshing(true); + await refreshEnvironments(); + setIsRefreshing(false); + }; + + // Handle create environment + const handleCreateEnvironment = async (environmentData: Partial) => { + setIsCreating(true); + try { + await createEnvironment(environmentData); + await refreshEnvironments(); // Refresh the list after creation + } catch (error) { + console.error("Failed to create environment:", error); + throw error; // Re-throw to let the modal handle the error display + } finally { + setIsCreating(false); + } + }; + + // Count environment types + const environmentCounts = environments.reduce((counts, env) => { + const type = env.environmentType.toUpperCase(); + counts[type] = (counts[type] || 0) + 1; + return counts; + }, {} as Record); + + return ( +
+ {/* Modern gradient header */} +
+ + +
+
+ +
+
+ + Environments + + + Manage your deployment environments across dev, test, preprod, and production + +
+
+ + + + + + + +
+
+ + {/* Environment type stats */} + {environments.length > 0 && ( + + + + + + +
+
+ {environments.length} +
+
+ Total Environments +
+
+
+ + + {['PROD', 'PREPROD', 'TEST', 'DEV'].map(type => ( + + +
+
+ {environmentCounts[type] || 0} +
+
+ + {type} Environments +
+
+
+ + ))} +
+
+ +
+ )} + + {/* Main content card */} + setSearchText(e.target.value)} + style={{ width: 250 }} + prefix={} + allowClear + /> + } + > + {/* Error handling */} + {error && ( + + )} + + {/* Loading, empty state or table */} + {isLoading ? ( +
+ +
+ ) : environments.length === 0 && !error ? ( + + ) : filteredEnvironments.length === 0 ? ( + + ) : ( + /* Table component */ + + )} + + {/* Results counter when searching */} + {searchText && filteredEnvironments.length !== environments.length && ( +
+ Showing {filteredEnvironments.length} of {environments.length} environments +
+ )} +
+ + {/* Create Environment Modal */} + setIsCreateModalVisible(false)} + onSave={handleCreateEnvironment} + loading={isCreating} + /> +
+ ); +}; + export default EnvironmentsList; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx index 0b69385b7..f48f046b2 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx @@ -4,8 +4,8 @@ import { Spin, Typography, Tabs, - message, } from "antd"; +import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"; import { AppstoreOutlined, DatabaseOutlined, @@ -46,9 +46,9 @@ const WorkspaceDetail: React.FC = () => { try { const success = await toggleManagedStatus(checked); if (success) { - message.success(`Workspace is now ${checked ? 'Managed' : 'Unmanaged'}`); + messageInstance.success(`Workspace is now ${checked ? 'Managed' : 'Unmanaged'}`); } else { - message.error('Failed to change managed status'); + messageInstance.error('Failed to change managed status'); } } finally { setIsToggling(false); diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/AppsTab.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/AppsTab.tsx index 6f9aea130..4e39062fc 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/AppsTab.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/AppsTab.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react'; -import { Card, Button, Divider, Alert, message, Table, Tag, Input, Space, Tooltip, Row, Col } from 'antd'; -import { SyncOutlined, CloudUploadOutlined, AuditOutlined, AppstoreOutlined, CheckCircleFilled, CloudServerOutlined, DisconnectOutlined, FilterOutlined } from '@ant-design/icons'; +import { Card, Button, Divider, Alert, Table, Tag, Input, Space, Tooltip, Row, Col } from 'antd'; +import { SyncOutlined, CloudUploadOutlined, AuditOutlined, AppstoreOutlined, CheckCircleFilled, CloudServerOutlined, DisconnectOutlined, FilterOutlined, DeleteOutlined } from '@ant-design/icons'; import Title from 'antd/lib/typography/Title'; import { Environment } from '../types/environment.types'; import { App, AppStats } from '../types/app.types'; @@ -10,6 +10,7 @@ import { ManagedObjectType, setManagedObject, unsetManagedObject } from '../serv import { useDeployModal } from '../context/DeployModalContext'; import { appsConfig } from '../config/apps.config'; import history from "@lowcoder-ee/util/history"; +import { messageInstance } from 'lowcoder-design/src/components/GlobalInstances'; const { Search } = Input; @@ -116,10 +117,10 @@ const AppsTab: React.FC = ({ environment, workspaceId }) => { unmanaged: prev.total - managed })); - message.success(`${app.name} is now ${checked ? 'Managed' : 'Unmanaged'}`); + messageInstance.success(`${app.name} is now ${checked ? 'Managed' : 'Unmanaged'}`); return true; } catch (error) { - message.error(`Failed to change managed status for ${app.name}`); + messageInstance.error(`Failed to change managed status for ${app.name}`); return false; } finally { setRefreshing(false); @@ -153,8 +154,20 @@ const AppsTab: React.FC = ({ environment, workspaceId }) => { > {app.name.charAt(0).toUpperCase()} -
-
{app.name}
+
+
+ {app.name} + {app.applicationStatus === 'RECYCLED' && ( + + + + )} +
{app.applicationId}
diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/ContactLowcoderModal.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/ContactLowcoderModal.tsx new file mode 100644 index 000000000..22c9ddd64 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/components/ContactLowcoderModal.tsx @@ -0,0 +1,132 @@ +import React, { useEffect } from 'react'; +import { Modal, Card, Row, Col, Typography, Divider } from 'antd'; +import { CustomerServiceOutlined, CloudServerOutlined } from '@ant-design/icons'; +import { useSelector, useDispatch } from 'react-redux'; +import { getDeploymentId } from 'redux/selectors/configSelectors'; +import { fetchDeploymentIdAction } from 'redux/reduxActions/configActions'; +import { Environment } from '../types/environment.types'; + +const { Title, Text } = Typography; + +interface ContactLowcoderModalProps { + visible: boolean; + onClose: () => void; + environment: Environment; +} + +/** + * Professional modal for contacting Lowcoder team with HubSpot form integration + */ +const ContactLowcoderModal: React.FC = ({ + visible, + onClose, + environment +}) => { + // Get deploymentId from Redux config provider + const deploymentId = useSelector(getDeploymentId); + const dispatch = useDispatch(); + + // Fetch deployment ID when modal opens if not already available + useEffect(() => { + if (visible && !deploymentId) { + dispatch(fetchDeploymentIdAction()); + } + }, [visible, deploymentId, dispatch]); + + return ( + + + Contact Lowcoder Team +
+ } + open={visible} + onCancel={onClose} + footer={null} + width={800} + centered + style={{ top: 20 }} + bodyStyle={{ padding: '24px' }} + > + {/* Environment Context Section */} + + + + + + +
+ + Environment: {environment.environmentName || 'Unnamed Environment'} + +
+ + Environment ID: {environment.environmentId} + +
+ + Deployment ID: {deploymentId || 'Loading...'} + +
+ +
+
+ + + + {/* HubSpot Form Integration Section */} +
+ + Get in Touch + + + + Our team is here to help you resolve licensing issues and get your environment up and running. + + + {/* HubSpot Form Container */} +
+ {/* HubSpot form will be integrated here */} +
+ +
Contact form will be integrated here
+
+
+ + {/* Environment data is available for form pre-filling */} + {/* Data available: environment.environmentName, environment.environmentId, deploymentId, + environment.licenseStatus, environment.environmentType, environment.licenseError */} +
+ + ); +}; + +export default ContactLowcoderModal; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/CreateEnvironmentModal.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/CreateEnvironmentModal.tsx new file mode 100644 index 000000000..dac35d7be --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/components/CreateEnvironmentModal.tsx @@ -0,0 +1,169 @@ +import React, { useState } from 'react'; +import { Modal, Form, Input, Select, Switch, Button, Alert } from 'antd'; +import { Environment } from '../types/environment.types'; + +const { Option } = Select; + +interface CreateEnvironmentModalProps { + visible: boolean; + onClose: () => void; + onSave: (data: Partial) => Promise; + loading?: boolean; +} + +const CreateEnvironmentModal: React.FC = ({ + visible, + onClose, + onSave, + loading = false +}) => { + const [form] = Form.useForm(); + const [submitLoading, setSubmitLoading] = useState(false); + + const handleSubmit = async () => { + try { + const values = await form.validateFields(); + setSubmitLoading(true); + + await onSave(values); + form.resetFields(); // Reset form after successful creation + onClose(); + } catch (error) { + if (error instanceof Error) { + console.error("Form validation or submission error:", error); + } + } finally { + setSubmitLoading(false); + } + }; + + const handleCancel = () => { + form.resetFields(); // Reset form when canceling + onClose(); + }; + + return ( + + Cancel + , + + ]} + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ); +}; + +export default CreateEnvironmentModal; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/DataSourcesTab.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/DataSourcesTab.tsx index ba13fd575..95535d590 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/DataSourcesTab.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/DataSourcesTab.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from 'react'; -import { Card, Button, Divider, Alert, message, Table, Tag, Input, Space, Tooltip, Row, Col, Avatar } from 'antd'; +import { Card, Button, Divider, Alert, Table, Tag, Input, Space, Tooltip, Row, Col, Avatar } from 'antd'; +import { messageInstance } from 'lowcoder-design/src/components/GlobalInstances'; import { SyncOutlined, CloudUploadOutlined, @@ -114,10 +115,10 @@ const DataSourcesTab: React.FC = ({ environment, workspaceI unmanaged: prev.total - managed })); - message.success(`${dataSource.name} is now ${checked ? 'Managed' : 'Unmanaged'}`); + messageInstance.success(`${dataSource.name} is now ${checked ? 'Managed' : 'Unmanaged'}`); return true; } catch (error) { - message.error(`Failed to change managed status for ${dataSource.name}`); + messageInstance.error(`Failed to change managed status for ${dataSource.name}`); return false; } finally { setRefreshing(false); diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/DeployItemModal.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/DeployItemModal.tsx index 31a0ce725..256589ac0 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/DeployItemModal.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/DeployItemModal.tsx @@ -1,6 +1,7 @@ // components/DeployItemModal.tsx import React, { useState, useEffect } from 'react'; -import { Modal, Form, Select, Checkbox, Button, message, Spin, Input, Tag, Space } from 'antd'; +import { Modal, Form, Select, Checkbox, Button, Spin, Input, Tag, Space } from 'antd'; +import { messageInstance } from 'lowcoder-design/src/components/GlobalInstances'; import { Environment } from '../types/environment.types'; import { DeployableItemConfig } from '../types/deployable-item.types'; import { useEnvironmentContext } from '../context/EnvironmentContext'; @@ -35,7 +36,7 @@ function DeployItemModal({ // Filter out source environment from target list const targetEnvironments = environments.filter( - (env: Environment) => env.environmentId !== sourceEnvironment.environmentId + (env: Environment) => env.environmentId !== sourceEnvironment.environmentId && env.isLicensed !== false ); const handleDeploy = async () => { @@ -46,7 +47,7 @@ function DeployItemModal({ const targetEnv = environments.find(env => env.environmentId === values.targetEnvId); if (!targetEnv) { - message.error('Target environment not found'); + messageInstance.error('Target environment not found'); return; } @@ -58,12 +59,12 @@ function DeployItemModal({ // Execute deployment await config.deploy.execute(params); - message.success(`Successfully deployed ${item.name} to target environment`); + messageInstance.success(`Successfully deployed ${item.name} to target environment`); if (onSuccess) onSuccess(); onClose(); } catch (error) { console.error('Deployment error:', error); - message.error(`Failed to deploy ${config.deploy.singularLabel.toLowerCase()}`); + messageInstance.error(`Failed to deploy ${config.deploy.singularLabel.toLowerCase()}`); } finally { setDeploying(false); } 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 82682075f..89ec73e39 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, message } from 'antd'; +import { Modal, Form, Input, Select, Switch, Button } from 'antd'; import { Environment } from '../types/environment.types'; const { Option } = Select; 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 16f9fc6fd..287c9ff00 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentsTable.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentsTable.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { Table, Tag, Button, Tooltip, Space, Card, Row, Col, Typography, Avatar } from 'antd'; -import { EditOutlined, AuditOutlined, LinkOutlined, EnvironmentOutlined, StarFilled, CloudServerOutlined } from '@ant-design/icons'; +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 { Environment } from '../types/environment.types'; import { getEnvironmentTagColor, formatEnvironmentType } from '../utils/environmentUtils'; @@ -27,6 +27,11 @@ const EnvironmentsTable: React.FC = ({ window.open(auditUrl, '_blank'); }; + // Handle row click - allow navigation to all environments including unlicensed + const handleRowClick = (env: Environment) => { + onRowClick(env); + }; + // Generate background color for environment avatar const getAvatarColor = (name: string) => { let hash = 0; @@ -44,6 +49,47 @@ const EnvironmentsTable: React.FC = ({ return `hsl(${hue}, 70%, 50%)`; }; + // Get license status icon and color + const getLicenseStatusDisplay = (env: Environment) => { + switch (env.licenseStatus) { + case 'checking': + return { + icon: , + color: '#1890ff', + text: 'Checking...', + status: 'processing' as const + }; + case 'licensed': + return { + icon: , + color: '#52c41a', + text: 'Licensed', + status: 'success' as const + }; + case 'unlicensed': + return { + icon: , + color: '#ff4d4f', + text: 'Not Licensed', + status: 'error' as const + }; + case 'error': + return { + icon: , + color: '#faad14', + text: 'License Error', + status: 'warning' as const + }; + default: + return { + icon: , + color: '#d9d9d9', + text: 'Unknown', + status: 'default' as const + }; + } + }; + // For card display, we'll use a custom layout instead of Table if (environments.length === 0) { return null; @@ -52,103 +98,185 @@ const EnvironmentsTable: React.FC = ({ return (
- {environments.map(env => ( - - onRowClick(env)} - > -
-
- { + const licenseDisplay = getLicenseStatusDisplay(env); + const isAccessible = env.isLicensed !== false; + + return ( + + handleRowClick(env)} + > + {/* Subtle overlay for unlicensed environments */} + {!isAccessible && ( +
+ {/* Not Licensed Badge */} +
} - /> -
- - {env.environmentName || 'Unnamed Environment'} - {env.isMaster && ( - <Tooltip title="Master Environment"> - <StarFilled style={{ color: '#faad14', marginLeft: '8px', fontSize: '14px' }} /> - </Tooltip> - )} - - - {formatEnvironmentType(env.environmentType)} - + gap: '6px', + boxShadow: '0 2px 6px rgba(0,0,0,0.15)' + }}> + {licenseDisplay.icon} + {licenseDisplay.text} +
-
-
- -
+ )} +
+ +
+
+
+ ID: + {isAccessible ? ( + + {env.environmentId} + + ) : ( + + {env.environmentId} + + )} +
+ +
+ Domain: + {env.environmentFrontendUrl ? ( + isAccessible ? ( + e.stopPropagation()} + style={{ fontSize: '13px' }} + > + {env.environmentFrontendUrl.replace(/^https?:\/\//, '')} + + + ) : ( + + {env.environmentFrontendUrl.replace(/^https?:\/\//, '')} + + ) + ) : ( + + )} +
+ +
+ Master: + + {env.isMaster ? 'Yes' : 'No'} + +
+ +
+ License: +
+ + {licenseDisplay.icon} + + + {licenseDisplay.text} + +
+
-
-
- - ))} + + + ); + })}
{environments.length > 10 && ( diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/QueriesTab.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/QueriesTab.tsx index a42f604c1..66d8d80c8 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/QueriesTab.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/QueriesTab.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from 'react'; -import { Card, Button, Divider, Alert, message, Table, Tag, Input, Space, Tooltip, Row, Col } from 'antd'; +import { Card, Button, Divider, Alert, Table, Tag, Input, Space, Tooltip, Row, Col } from 'antd'; +import { messageInstance } from 'lowcoder-design/src/components/GlobalInstances'; import { SyncOutlined, CloudUploadOutlined, @@ -115,10 +116,10 @@ const QueriesTab: React.FC = ({ environment, workspaceId }) => unmanaged: prev.total - managed })); - message.success(`${query.name} is now ${checked ? 'Managed' : 'Unmanaged'}`); + messageInstance.success(`${query.name} is now ${checked ? 'Managed' : 'Unmanaged'}`); return true; } catch (error) { - message.error(`Failed to change managed status for ${query.name}`); + messageInstance.error(`Failed to change managed status for ${query.name}`); return false; } finally { setRefreshing(false); diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/UnlicensedEnvironmentView.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/UnlicensedEnvironmentView.tsx new file mode 100644 index 000000000..6b61379dd --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/components/UnlicensedEnvironmentView.tsx @@ -0,0 +1,225 @@ +import React, { useState } from 'react'; +import { Button, Card, Space, Typography, Row, Col } from 'antd'; +import { + CustomerServiceOutlined, + EditOutlined, + ArrowLeftOutlined, + CloseCircleOutlined, + ExclamationCircleOutlined, + WarningOutlined +} from '@ant-design/icons'; +import { Environment } from '../types/environment.types'; +import ContactLowcoderModal from './ContactLowcoderModal'; +import history from "@lowcoder-ee/util/history"; + +const { Title, Text } = Typography; + +interface UnlicensedEnvironmentViewProps { + environment: Environment; + onEditClick: () => void; +} + +/** + * Modern UI for unlicensed environments + */ +const UnlicensedEnvironmentView: React.FC = ({ + environment, + onEditClick +}) => { + const [isContactModalVisible, setIsContactModalVisible] = useState(false); + + const getLicenseIcon = () => { + switch (environment.licenseStatus) { + case 'unlicensed': + return ; + case 'error': + return ; + default: + return ; + } + }; + + const getLicenseTitle = () => { + switch (environment.licenseStatus) { + case 'unlicensed': + return 'Environment Not Licensed'; + case 'error': + return 'License Configuration Error'; + default: + return 'License Issue'; + } + }; + + 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.'; + case 'error': + return 'There was an error validating the license for this environment. Please check the configuration.'; + default: + return 'This environment has license-related issues that need to be resolved.'; + } + }; + + return ( +
+ + +
+ {/* Main Status Card */} + + {/* Status Icon */} +
+ {getLicenseIcon()} +
+ + {/* Environment Info */} +
+ + {getLicenseTitle()} + + + {getLicenseDescription()} + + + {/* Environment Details */} +
+ Environment: + + {environment.environmentName || 'Unnamed Environment'} + + + ID: {environment.environmentId} + +
+
+ + {/* Action Buttons */} + + + + + + + +
+ + {/* Footer Help Text */} + + Need assistance? Contact our team for licensing support or edit the environment configuration to resolve this issue. + +
+ +
+ + {/* Contact Lowcoder Modal */} + setIsContactModalVisible(false)} + environment={environment} + /> +
+ ); +}; + +export default UnlicensedEnvironmentView; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/UserGroupsTab.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/UserGroupsTab.tsx index ff079a9df..0f11dac61 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/UserGroupsTab.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/UserGroupsTab.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Card, Button, Alert, message, Table, Tag, Input, Space, Row, Col, Avatar, Tooltip } from 'antd'; +import { Card, Button, Alert, Table, Tag, Input, Space, Row, Col, Avatar, Tooltip } from 'antd'; import { SyncOutlined, TeamOutlined, UserOutlined, UsergroupAddOutlined, SettingOutlined, CodeOutlined } from '@ant-design/icons'; import Title from 'antd/lib/typography/Title'; import { Environment } from '../types/environment.types'; diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/WorkspacesTab.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/WorkspacesTab.tsx index eaf8b2a17..006ffb1f6 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/WorkspacesTab.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/WorkspacesTab.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Card, Button, Divider, Alert, message, Table, Tag, Input, Space, Tooltip, Row, Col, Avatar } from 'antd'; +import { Card, Button, Divider, Alert, Table, Tag, Input, Space, Tooltip, Row, Col, Avatar } from 'antd'; import { SyncOutlined, AuditOutlined, TeamOutlined, CheckCircleFilled, CloudServerOutlined, DisconnectOutlined, FilterOutlined } from '@ant-design/icons'; import Title from 'antd/lib/typography/Title'; import { Environment } from '../types/environment.types'; diff --git a/client/packages/lowcoder/src/pages/setting/environments/context/EnvironmentContext.tsx b/client/packages/lowcoder/src/pages/setting/environments/context/EnvironmentContext.tsx index 9f9c7ec3c..7594aa404 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/context/EnvironmentContext.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/context/EnvironmentContext.tsx @@ -7,8 +7,8 @@ import React, { useCallback, ReactNode, } from "react"; -import { message } from "antd"; -import { getEnvironments } from "../services/environments.service"; +import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"; +import { getEnvironmentsWithLicenseStatus } from "../services/environments.service"; import { Environment } from "../types/environment.types"; interface EnvironmentContextState { @@ -59,12 +59,12 @@ export const EnvironmentProvider: React.FC = ({ setError(null); try { - const data = await getEnvironments(); + const data = await getEnvironmentsWithLicenseStatus(); setEnvironments(data); - } catch (err) { - const errorMessage = err instanceof Error ? err.message : "Failed to load environments list"; - setError(errorMessage); - message.error(errorMessage); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Failed to fetch environments'; + messageInstance.error(errorMessage); + console.error('Error fetching environments:', error); } finally { setIsLoading(false); } diff --git a/client/packages/lowcoder/src/pages/setting/environments/context/SingleEnvironmentContext.tsx b/client/packages/lowcoder/src/pages/setting/environments/context/SingleEnvironmentContext.tsx index bd93bf9f2..7ca5f0b70 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/context/SingleEnvironmentContext.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/context/SingleEnvironmentContext.tsx @@ -7,7 +7,7 @@ import React, { useCallback, ReactNode, } from "react"; - import { message } from "antd"; + import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"; import { useParams } from "react-router-dom"; import { getEnvironmentById, updateEnvironment } from "../services/environments.service"; import { Environment } from "../types/environment.types"; @@ -100,7 +100,7 @@ import React, { const updatedEnv = await updateEnvironment(environmentId, data); // Show success message - message.success("Environment updated successfully"); + messageInstance.success("Environment updated successfully"); // Refresh both the single environment and environments list await Promise.all([ @@ -111,7 +111,7 @@ import React, { return updatedEnv; } catch (err) { const errorMessage = err instanceof Error ? err.message : "Failed to update environment"; - message.error(errorMessage); + messageInstance.error(errorMessage); throw err; } }, [environment, environmentId, fetchEnvironment, refreshEnvironments]); diff --git a/client/packages/lowcoder/src/pages/setting/environments/context/WorkspaceContext.tsx b/client/packages/lowcoder/src/pages/setting/environments/context/WorkspaceContext.tsx index f2e16d442..988c11e5a 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/context/WorkspaceContext.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/context/WorkspaceContext.tsx @@ -7,7 +7,7 @@ import React, { useCallback, ReactNode, } from "react"; - import { message } from "antd"; + import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"; import { useParams } from "react-router-dom"; import { useSingleEnvironmentContext } from "./SingleEnvironmentContext"; import { fetchWorkspaceById } from "../services/environments.service"; @@ -96,8 +96,9 @@ import React, { ...workspaceData, managed: isManaged }); - } catch (err) { - const errorMessage = err instanceof Error ? err.message : "Workspace not found or failed to load"; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Failed to fetch workspace'; + messageInstance.error(errorMessage); setError(errorMessage); } finally { setIsLoading(false); @@ -135,7 +136,7 @@ import React, { return true; } catch (err) { const errorMessage = err instanceof Error ? err.message : "Failed to update managed status"; - message.error(errorMessage); + messageInstance.error(errorMessage); return false; } }, [workspace, environment]); diff --git a/client/packages/lowcoder/src/pages/setting/environments/services/apps.service.ts b/client/packages/lowcoder/src/pages/setting/environments/services/apps.service.ts index 8accb9db1..6d9d40eaf 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/services/apps.service.ts +++ b/client/packages/lowcoder/src/pages/setting/environments/services/apps.service.ts @@ -1,5 +1,5 @@ // services/appService.ts -import { message } from "antd"; +import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"; import { getWorkspaceApps } from "./environments.service"; import { getManagedApps } from "./enterprise.service"; import { App, AppStats } from "../types/app.types"; @@ -99,7 +99,7 @@ export async function getMergedWorkspaceApps( } catch (error) { const errorMessage = error instanceof Error ? error.message : "Failed to fetch apps"; - message.error(errorMessage); + messageInstance.error(errorMessage); throw error; } } 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 cca28176d..0e181a6da 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 @@ -1,6 +1,6 @@ // services/dataSources.service.ts import axios from 'axios'; -import { message } from "antd"; +import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"; import { DataSource, DataSourceWithMeta } from "../types/datasource.types"; import { getManagedObjects, ManagedObject, ManagedObjectType , transferManagedObject } from "./managed-objects.service"; @@ -66,7 +66,7 @@ export async function getWorkspaceDataSources( } catch (error) { // Handle and transform error const errorMessage = error instanceof Error ? error.message : 'Failed to fetch data sources'; - message.error(errorMessage); + messageInstance.error(errorMessage); throw error; } } @@ -145,7 +145,7 @@ export async function getMergedWorkspaceDataSources( } catch (error) { const errorMessage = error instanceof Error ? error.message : "Failed to fetch data sources"; - message.error(errorMessage); + messageInstance.error(errorMessage); throw error; } } @@ -171,7 +171,8 @@ export async function deployDataSource(params: DeployDataSourceParams): Promise< } return response.status === 200; } catch (error) { - console.error('Error deploying data source:', error); + const errorMessage = error instanceof Error ? error.message : 'Failed to deploy data source'; + messageInstance.error(errorMessage); throw error; } } \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts b/client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts index f0f9f1939..f676f1734 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts +++ b/client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts @@ -1,5 +1,5 @@ import axios from "axios"; -import { message } from "antd"; +import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"; import { ManagedOrg } from "../types/enterprise.types"; import { Query } from "../types/query.types"; @@ -26,7 +26,7 @@ export async function getManagedWorkspaces( return all.filter(org => org.environmentId === environmentId); } catch (err) { const errorMsg = err instanceof Error ? err.message : "Failed to fetch managed workspaces"; - message.error(errorMsg); + messageInstance.error(errorMsg); throw err; } } @@ -63,7 +63,7 @@ export async function connectManagedWorkspace( return res.data; } catch (err) { const errorMsg = err instanceof Error ? err.message : "Failed to connect org"; - message.error(errorMsg); + messageInstance.error(errorMsg); throw err; } } @@ -88,7 +88,7 @@ export async function unconnectManagedWorkspace(orgGid: string) { } catch (err) { const errorMsg = err instanceof Error ? err.message : "Failed to unconnect org"; - message.error(errorMsg); + messageInstance.error(errorMsg); throw err; } } @@ -124,7 +124,7 @@ export async function connectManagedApp( } catch (err) { const errorMsg = err instanceof Error ? err.message : "Failed to connect app"; - message.error(errorMsg); + messageInstance.error(errorMsg); throw err; } } @@ -137,7 +137,7 @@ export async function unconnectManagedApp(appGid: string) { }); } catch (err) { const errorMsg = err instanceof Error ? err.message : "Failed to unconnect app"; - message.error(errorMsg); + messageInstance.error(errorMsg); throw err; } } @@ -151,7 +151,8 @@ export const getManagedDataSources = async (environmentId: string): Promise })); } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Failed to fetch managed queries'; - message.error(errorMessage); + const errorMsg = error instanceof Error ? error.message : 'Failed to fetch queries'; + messageInstance.error(errorMsg); throw error; } } @@ -250,8 +253,8 @@ export async function connectManagedQuery( return response.status === 200; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Failed to connect query'; - message.error(errorMessage); + const errorMsg = error instanceof Error ? error.message : 'Failed to deploy query'; + messageInstance.error(errorMsg); throw error; } } @@ -272,8 +275,8 @@ export async function unconnectManagedQuery(queryGid: string): Promise return response.status === 200; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Failed to disconnect query'; - message.error(errorMessage); + const errorMsg = error instanceof Error ? error.message : 'Failed to disconnect query'; + messageInstance.error(errorMsg); throw error; } } \ No newline at end of file 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 e34c168b3..b0c489b7a 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 @@ -1,11 +1,12 @@ import axios from "axios"; -import { message } from "antd"; +import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"; import { Environment } from "../types/environment.types"; import { Workspace } from "../types/workspace.types"; import { UserGroup } from "../types/userGroup.types"; import {App} from "../types/app.types"; import { DataSourceWithMeta } from '../types/datasource.types'; import { Query, QueryResponse } from "../types/query.types"; +import { checkEnvironmentLicense } from './license.service'; @@ -39,12 +40,47 @@ export async function updateEnvironment( return res.data; } catch (err) { const errorMsg = err instanceof Error ? err.message : "Failed to update environment"; - message.error(errorMsg); + messageInstance.error(errorMsg); throw err; } } +/** + * Create a new environment manually + * @param environmentData - Environment data to create + * @returns Promise with created environment data + */ +export async function createEnvironment( + environmentData: Partial +): Promise { + try { + // Convert frontend model to API model + const payload = { + environment_description: environmentData.environmentDescription || "", + environment_icon: environmentData.environmentIcon || "", + environment_name: environmentData.environmentName || "", + environment_apikey: environmentData.environmentApikey || "", + environment_type: environmentData.environmentType || "", + environment_api_service_url: environmentData.environmentApiServiceUrl || "", + environment_frontend_url: environmentData.environmentFrontendUrl || "", + environment_node_service_url: environmentData.environmentNodeServiceUrl || "", + isMaster: environmentData.isMaster || false + }; + const res = await axios.post(`/api/plugins/enterprise/environments`, payload); + + if (res.data) { + messageInstance.success("Environment created successfully"); + return res.data; + } else { + throw new Error("Failed to create environment"); + } + } catch (err) { + const errorMsg = err instanceof Error ? err.message : "Failed to create environment"; + messageInstance.error(errorMsg); + throw err; + } +} /** * Fetch all environments @@ -62,7 +98,7 @@ export async function getEnvironments(): Promise { } catch (error) { const errorMessage = error instanceof Error ? error.message : "Failed to fetch environments"; - message.error(errorMessage); + messageInstance.error(errorMessage); throw error; } } @@ -82,11 +118,40 @@ export async function getEnvironmentById(id: string): Promise { throw new Error("Failed to fetch environment"); } - return response.data.data; + const environment = response.data.data; + + // Check license status for the environment + const envWithLicense: Environment = { + ...environment, + licenseStatus: 'checking' + }; + + try { + if (environment.environmentApiServiceUrl) { + const licenseInfo = await checkEnvironmentLicense( + environment.environmentApiServiceUrl, + environment.environmentApikey + ); + + envWithLicense.isLicensed = licenseInfo.isValid; + envWithLicense.licenseStatus = licenseInfo.isValid ? 'licensed' : 'unlicensed'; + envWithLicense.licenseError = licenseInfo.error; + } else { + envWithLicense.isLicensed = false; + envWithLicense.licenseStatus = 'error'; + envWithLicense.licenseError = 'API service URL not configured'; + } + } catch (error) { + envWithLicense.isLicensed = false; + envWithLicense.licenseStatus = 'error'; + envWithLicense.licenseError = error instanceof Error ? error.message : 'License check failed'; + } + + return envWithLicense; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Failed to fetch environment"; - message.error(errorMessage); + messageInstance.error(errorMessage); throw error; } } @@ -100,6 +165,7 @@ export async function getEnvironmentById(id: string): Promise { * Fetch workspaces for a specific environment * @param environmentId - ID of the environment * @param apiKey - API key for the environment + * @param apiServiceUrl - API service URL for the environment * @returns Promise with an array of workspaces */ export async function getEnvironmentWorkspaces( @@ -158,7 +224,7 @@ export async function getEnvironmentWorkspaces( // Handle and transform error const errorMessage = error instanceof Error ? error.message : "Failed to fetch workspaces"; - message.error(errorMessage); + messageInstance.error(errorMessage); throw error; } } @@ -208,7 +274,7 @@ export async function getEnvironmentUserGroups( } catch (error) { // Handle and transform error const errorMessage = error instanceof Error ? error.message : 'Failed to fetch user groups'; - message.error(errorMessage); + messageInstance.error(errorMessage); throw error; } } @@ -297,7 +363,10 @@ export async function getWorkspaceApps( // Then fetch applications without the orgId parameter const response = await axios.get(`${apiServiceUrl}/api/applications/list`, { - headers + headers, + params: { + withContainerSize: false + } }); // Check if response is valid @@ -305,12 +374,17 @@ export async function getWorkspaceApps( return []; } - return response.data.data; + // Filter out DELETED apps + const apps = response.data.data.filter((app: any) => + app.applicationStatus !== 'DELETED' + ); + + return apps; } catch (error) { // Handle and transform error - const errorMessage = error instanceof Error ? error.message : 'Failed to fetch apps'; - message.error(errorMessage); + const errorMessage = error instanceof Error ? error.message : 'Failed to fetch workspace apps'; + messageInstance.error(errorMessage); throw error; } } @@ -367,8 +441,8 @@ export async function getWorkspaceDataSources( return response.data.data ; } catch (error) { // Handle and transform error - const errorMessage = error instanceof Error ? error.message : 'Failed to fetch data sources'; - message.error(errorMessage); + const errorMessage = error instanceof Error ? error.message : 'Failed to fetch workspace data sources'; + messageInstance.error(errorMessage); throw error; } } @@ -449,8 +523,59 @@ export async function getWorkspaceQueries( } catch (error) { // Handle and transform error - const errorMessage = error instanceof Error ? error.message : 'Failed to fetch queries'; - message.error(errorMessage); + const errorMessage = error instanceof Error ? error.message : 'Failed to fetch workspace queries'; + messageInstance.error(errorMessage); + throw error; + } +} + +/** + * Fetch all environments and check their license status + * @returns Promise with environments data including license status + */ +export async function getEnvironmentsWithLicenseStatus(): Promise { + try { + // First fetch all environments + const environments = await getEnvironments(); + + // Check license status for each environment in parallel + const environmentsWithLicense = await Promise.all( + environments.map(async (env) => { + const envWithLicense: Environment = { + ...env, + licenseStatus: 'checking' + }; + + try { + if (env.environmentApiServiceUrl) { + const licenseInfo = await checkEnvironmentLicense( + env.environmentApiServiceUrl, + env.environmentApikey + ); + + envWithLicense.isLicensed = licenseInfo.isValid; + envWithLicense.licenseStatus = licenseInfo.isValid ? 'licensed' : 'unlicensed'; + envWithLicense.licenseError = licenseInfo.error; + } else { + envWithLicense.isLicensed = false; + envWithLicense.licenseStatus = 'error'; + envWithLicense.licenseError = 'API service URL not configured'; + } + } catch (error) { + envWithLicense.isLicensed = false; + envWithLicense.licenseStatus = 'error'; + envWithLicense.licenseError = error instanceof Error ? error.message : 'License check failed'; + } + + return envWithLicense; + }) + ); + + return environmentsWithLicense; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Failed to fetch environments"; + messageInstance.error(errorMessage); throw error; } } \ No newline at end of file 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 new file mode 100644 index 000000000..ac4a1fd96 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/services/license.service.ts @@ -0,0 +1,70 @@ +import axios from 'axios'; +import { EnvironmentLicense } from '../types/environment.types'; + +/** + * Check license status for an environment + * @param apiServiceUrl - API service URL for the environment + * @param apiKey - API key for the environment + * @returns Promise with license information + */ +export async function checkEnvironmentLicense( + apiServiceUrl: string, + apiKey?: string +): Promise { + try { + if (!apiServiceUrl) { + return { + isValid: false, + error: 'API service URL is required' + }; + } + + // Prepare headers with API key if available + const headers: Record = {}; + if (apiKey) { + headers.Authorization = `Bearer ${apiKey}`; + } + + // Make request to the license endpoint + const response = await axios.get( + `${apiServiceUrl}/api/plugins/enterprise/license`, + { + headers, + timeout: 5000 // 5 second timeout + } + ); + + // If we get a successful response, the license is valid + return { + isValid: true + }; + + } catch (error) { + // If the endpoint doesn't exist or returns an error, license is invalid + if (axios.isAxiosError(error)) { + if (error.response?.status === 404) { + return { + isValid: false, + error: 'License endpoint not found' + }; + } + if (error.response?.status === 401 || error.response?.status === 403) { + return { + isValid: false, + error: 'License authentication failed' + }; + } + if (error.code === 'ECONNABORTED') { + return { + isValid: false, + error: 'License check timeout' + }; + } + } + + return { + isValid: false, + error: error instanceof Error ? error.message : 'License check failed' + }; + } +} \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/services/managed-objects.service.ts b/client/packages/lowcoder/src/pages/setting/environments/services/managed-objects.service.ts index 589ea6a5a..0a4e0b14a 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/services/managed-objects.service.ts +++ b/client/packages/lowcoder/src/pages/setting/environments/services/managed-objects.service.ts @@ -1,5 +1,5 @@ import axios from "axios"; -import { message } from "antd"; +import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"; // Object types that can be managed export enum ManagedObjectType { @@ -51,7 +51,7 @@ export async function isManagedObject( } const errorMessage = error instanceof Error ? error.message : "Failed to check managed status"; - message.error(errorMessage); + messageInstance.error(errorMessage); throw error; } } @@ -88,7 +88,7 @@ export async function setManagedObject( return response.status === 200; } catch (error) { const errorMessage = error instanceof Error ? error.message : `Failed to set ${objType} as managed`; - message.error(errorMessage); + messageInstance.error(errorMessage); throw error; } } @@ -122,7 +122,7 @@ export async function unsetManagedObject( return response.status === 200; } catch (error) { const errorMessage = error instanceof Error ? error.message : `Failed to remove ${objType} from managed`; - message.error(errorMessage); + messageInstance.error(errorMessage); throw error; } } @@ -147,7 +147,7 @@ export async function getManagedObjects( return response.data.data; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Failed to fetch managed objects"; - message.error(errorMessage); + messageInstance.error(errorMessage); throw error; } } @@ -185,7 +185,7 @@ export async function getSingleManagedObject( } const errorMessage = error instanceof Error ? error.message : "Failed to fetch managed object"; - message.error(errorMessage); + messageInstance.error(errorMessage); throw error; } } 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 871c0e036..3683b97d7 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 @@ -1,5 +1,5 @@ // services/workspacesService.ts (or wherever makes sense in your structure) -import { message } from "antd"; +import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"; import { getEnvironmentWorkspaces } from "./environments.service"; import { getManagedObjects, ManagedObject, ManagedObjectType, transferManagedObject } from "./managed-objects.service"; import { Workspace } from "../types/workspace.types"; @@ -69,9 +69,8 @@ export async function getMergedEnvironmentWorkspaces( } }; } catch (error) { - const errorMessage = - error instanceof Error ? error.message : "Failed to fetch workspaces"; - message.error(errorMessage); + const errorMessage = error instanceof Error ? error.message : "Failed to fetch workspaces"; + messageInstance.error(errorMessage); throw error; } } 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 39766c1ea..388fbab02 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 @@ -14,4 +14,16 @@ export interface Environment { isMaster: boolean; createdAt: string; updatedAt: string; + // License-related properties + isLicensed?: boolean; + licenseStatus?: 'checking' | 'licensed' | 'unlicensed' | 'error'; + licenseError?: string; +} + +/** + * Interface representing license information for an environment + */ +export interface EnvironmentLicense { + isValid: boolean; + error?: string; } \ No newline at end of file