From 0697c938606f393862bf2ad10af3e402b740c604 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 27 May 2025 23:39:53 +0500 Subject: [PATCH 01/10] Update UI for the EnvironmentsListing Page --- .../setting/environments/EnvironmentsList.tsx | 246 ++++++------------ 1 file changed, 73 insertions(+), 173 deletions(-) diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx index 3f9f51a1f..060b1dbad 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx @@ -1,6 +1,7 @@ -import React, { useState, useEffect, useRef } 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 React, { useState, useEffect } from "react"; +import { Alert, Empty, Spin } from "antd"; +import { SyncOutlined } from "@ant-design/icons"; +import { AddIcon, Search, TacoButton } from "lowcoder-design"; import { useHistory } from "react-router-dom"; import { useSelector, useDispatch } from "react-redux"; import { selectEnvironments, selectEnvironmentsLoading, selectEnvironmentsError } from "redux/selectors/enterpriseSelectors"; @@ -9,10 +10,49 @@ 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; +import styled from "styled-components"; + +const EnvironmentsWrapper = styled.div` + display: flex; + flex-direction: column; + width: 100%; + height: 100%; +`; + +const HeaderWrapper = styled.div` + display: flex; + align-items: center; + height: 92px; + padding: 28px 36px; + width: 100%; +`; + +const Title = styled.div` + font-weight: 500; + font-size: 18px; + color: #222222; + line-height: 18px; + flex-grow: 1; +`; + +const AddBtn = styled(TacoButton)` + min-width: 96px; + width: fit-content; + height: 32px; +`; + +const RefreshBtn = styled(TacoButton)` + width: fit-content; + height: 32px; + margin-right: 12px; +`; + +const BodyWrapper = styled.div` + width: 100%; + flex-grow: 1; + padding: 0 24px; +`; /** * Environment Listing Page Component @@ -29,13 +69,10 @@ const EnvironmentsList: React.FC = () => { const [searchText, setSearchText] = useState(""); 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(); @@ -82,167 +119,30 @@ const EnvironmentsList: React.FC = () => { } }; - // 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 - /> - } - > + + + Environments + setSearchText(e.target.value)} + style={{ width: "192px", height: "32px", margin: "0 12px 0 0" }} + /> + } + onClick={handleRefresh} + loading={isLoading} + > + Refresh + + setIsCreateModalVisible(true)}> + New Environment + + + + {/* Error handling */} {error && ( { description={error} type="error" showIcon - style={{ marginBottom: "24px" }} + style={{ marginBottom: "16px" }} /> )} {/* Loading, empty state or table */} {isLoading ? (
- +
) : environments.length === 0 && !error ? ( { Showing {filteredEnvironments.length} of {environments.length} environments
)} - + {/* Create Environment Modal */} { onSave={handleCreateEnvironment} loading={isCreating} /> - + ); }; From 0abe597e12a1118c40e9b4c84122cce41267cd0f Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 27 May 2025 23:44:11 +0500 Subject: [PATCH 02/10] Update Environments Table Card UI --- .../components/EnvironmentsTable.tsx | 94 +++++++++---------- 1 file changed, 45 insertions(+), 49 deletions(-) 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 287c9ff00..0a1e9d98e 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentsTable.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentsTable.tsx @@ -13,7 +13,7 @@ interface EnvironmentsTableProps { } /** - * Modern card-based layout for displaying environments + * Clean card-based layout for displaying environments consistent with app design */ const EnvironmentsTable: React.FC = ({ environments, @@ -34,17 +34,16 @@ const EnvironmentsTable: React.FC = ({ // Generate background color for environment avatar const getAvatarColor = (name: string) => { - let hash = 0; - for (let i = 0; i < name.length; i++) { - hash = name.charCodeAt(i) + ((hash << 5) - hash); - } - const type = name.toUpperCase(); if (type === 'PROD') return '#f5222d'; if (type === 'PREPROD') return '#fa8c16'; if (type === 'TEST') return '#722ed1'; if (type === 'DEV') return '#1890ff'; + let hash = 0; + for (let i = 0; i < name.length; i++) { + hash = name.charCodeAt(i) + ((hash << 5) - hash); + } const hue = Math.abs(hash % 360); return `hsl(${hue}, 70%, 50%)`; }; @@ -90,7 +89,6 @@ const EnvironmentsTable: React.FC = ({ } }; - // For card display, we'll use a custom layout instead of Table if (environments.length === 0) { return null; } @@ -107,16 +105,14 @@ const EnvironmentsTable: React.FC = ({ handleRowClick(env)} > {/* Subtle overlay for unlicensed environments */} @@ -127,7 +123,7 @@ const EnvironmentsTable: React.FC = ({ left: 0, right: 0, bottom: 0, - background: 'rgba(255, 255, 255, 0.7)', + background: 'rgba(255, 255, 255, 0.8)', zIndex: 1, display: 'flex', alignItems: 'flex-start', @@ -138,14 +134,13 @@ const EnvironmentsTable: React.FC = ({
{licenseDisplay.icon} {licenseDisplay.text} @@ -160,25 +155,24 @@ const EnvironmentsTable: React.FC = ({ backgroundColor: getAvatarColor(env.environmentType), display: 'flex', alignItems: 'center', - justifyContent: 'center', - fontSize: '20px' + justifyContent: 'center' }} - size={48} + size={40} icon={} />
- + <Title level={5} style={{ margin: 0, marginBottom: '4px', fontSize: '14px' }}> {env.environmentName || 'Unnamed Environment'} {env.isMaster && ( <Tooltip title="Master Environment"> - <StarFilled style={{ color: '#faad14', marginLeft: '8px', fontSize: '14px' }} /> + <StarFilled style={{ color: '#faad14', marginLeft: '6px', fontSize: '12px' }} /> </Tooltip> )} {formatEnvironmentType(env.environmentType)} @@ -187,7 +181,7 @@ const EnvironmentsTable: React.FC = ({ color={licenseDisplay.status === 'success' ? 'green' : licenseDisplay.status === 'error' ? 'red' : licenseDisplay.status === 'warning' ? 'orange' : 'blue'} - style={{ borderRadius: '12px' }} + style={{ fontSize: '11px', borderRadius: '4px' }} > {licenseDisplay.text} @@ -204,9 +198,11 @@ const EnvironmentsTable: React.FC = ({ onClick={(e) => openAuditPage(env.environmentId, e)} size="small" style={{ - borderRadius: '50%', - width: '32px', - height: '32px' + width: '28px', + height: '28px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center' }} /> @@ -214,23 +210,23 @@ const EnvironmentsTable: React.FC = ({ )}
-
-
-
- ID: +
+
+
+ ID: {isAccessible ? ( - + {env.environmentId} ) : ( - + {env.environmentId} )}
-
- Domain: +
+ Domain: {env.environmentFrontendUrl ? ( isAccessible ? ( = ({ target="_blank" rel="noopener noreferrer" onClick={(e) => e.stopPropagation()} - style={{ fontSize: '13px' }} + style={{ fontSize: '12px' }} > {env.environmentFrontendUrl.replace(/^https?:\/\//, '')} - + ) : ( - + {env.environmentFrontendUrl.replace(/^https?:\/\//, '')} ) ) : ( - + )}
-
- Master: - +
+ Master: + {env.isMaster ? 'Yes' : 'No'}
-
- License: +
+ License:
- + {licenseDisplay.icon} - + {licenseDisplay.text}
@@ -280,8 +276,8 @@ const EnvironmentsTable: React.FC = ({ {environments.length > 10 && ( -
- +
+ Showing all {environments.length} environments
From d2d7ede5f9be4b1d59325f99cf656fae5dcecad1 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 28 May 2025 12:28:00 +0500 Subject: [PATCH 03/10] Update Environments UI --- .../environments/EnvironmentDetail.tsx | 45 ++++++---- .../setting/environments/EnvironmentsList.tsx | 86 ++++++++++++++++++- .../setting/environments/WorkspaceDetail.tsx | 12 ++- .../environments/components/AppsTab.tsx | 79 +++++++---------- .../components/DataSourcesTab.tsx | 83 ++++++++---------- .../environments/components/QueriesTab.tsx | 81 +++++++---------- .../environments/components/UserGroupsTab.tsx | 79 ++++++++--------- .../components/WorkspaceHeader.tsx | 39 +++++---- .../environments/components/WorkspacesTab.tsx | 85 ++++++++---------- .../environments/services/license.service.ts | 35 ++------ 10 files changed, 316 insertions(+), 308 deletions(-) diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx index 3f019f615..437a5511b 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx @@ -85,8 +85,8 @@ const EnvironmentDetail: React.FC = () => { if (isLoading) { return ( -
- +
+
); } @@ -151,19 +151,21 @@ const EnvironmentDetail: React.FC = () => { onEditClick={handleEditClick} /> - - - {/* Basic Environment Information Card - improved responsiveness */} + {/* Basic Environment Information Card */} {environment.environmentFrontendUrl ? ( @@ -181,7 +183,7 @@ const EnvironmentDetail: React.FC = () => { {environment.environmentType} @@ -190,23 +192,23 @@ const EnvironmentDetail: React.FC = () => { {(() => { switch (environment.licenseStatus) { case 'checking': - return } color="blue" style={{ borderRadius: '12px' }}>Checking...; + return } color="blue" style={{ borderRadius: '4px' }}>Checking...; case 'licensed': - return } color="green" style={{ borderRadius: '12px' }}>Licensed; + return } color="green" style={{ borderRadius: '4px' }}>Licensed; case 'unlicensed': - return } color="red" style={{ borderRadius: '12px' }}>Not Licensed; + return } color="red" style={{ borderRadius: '4px' }}>Not Licensed; case 'error': - return } color="orange" style={{ borderRadius: '12px' }}>License Error; + return } color="orange" style={{ borderRadius: '4px' }}>License Error; default: - return Unknown; + return Unknown; } })()} {environment.environmentApikey ? ( - Configured + Configured ) : ( - Not Configured + Not Configured )} @@ -217,13 +219,20 @@ const EnvironmentDetail: React.FC = () => { {/* Modern Breadcrumbs navigation */} + {/* Tabs for Workspaces and User Groups */} { } key="workspaces" > - {/* Using our new standalone WorkspacesTab component */} @@ -245,7 +253,6 @@ const EnvironmentDetail: React.FC = () => { } key="userGroups" > - {/* Now using our standalone UserGroupsTab component */} diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx index 060b1dbad..a8cd4bda8 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from "react"; -import { Alert, Empty, Spin } from "antd"; -import { SyncOutlined } from "@ant-design/icons"; +import { Alert, Empty, Spin, Row, Col, Card } from "antd"; +import { SyncOutlined, CloudServerOutlined } from "@ant-design/icons"; import { AddIcon, Search, TacoButton } from "lowcoder-design"; import { useHistory } from "react-router-dom"; import { useSelector, useDispatch } from "react-redux"; @@ -11,6 +11,7 @@ import EnvironmentsTable from "./components/EnvironmentsTable"; import CreateEnvironmentModal from "./components/CreateEnvironmentModal"; import { buildEnvironmentId } from "@lowcoder-ee/constants/routesURL"; import { createEnvironment } from "./services/environments.service"; +import { getEnvironmentTagColor } from "./utils/environmentUtils"; import styled from "styled-components"; const EnvironmentsWrapper = styled.div` @@ -54,6 +55,10 @@ const BodyWrapper = styled.div` padding: 0 24px; `; +const StatsWrapper = styled.div` + margin-bottom: 20px; +`; + /** * Environment Listing Page Component * Displays a table of environments @@ -73,6 +78,65 @@ const EnvironmentsList: React.FC = () => { // Hook for navigation const history = useHistory(); + // Calculate environment type statistics + const environmentStats = React.useMemo(() => { + const stats = environments.reduce((acc, env) => { + const type = env.environmentType.toUpperCase(); + acc[type] = (acc[type] || 0) + 1; + return acc; + }, {} as Record); + + // Sort by common environment types first + const typeOrder = ['PROD', 'PREPROD', 'TEST', 'DEV']; + const sortedStats = Object.entries(stats).sort(([a], [b]) => { + const aIndex = typeOrder.indexOf(a); + const bIndex = typeOrder.indexOf(b); + + if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex; + if (aIndex !== -1) return -1; + if (bIndex !== -1) return 1; + return a.localeCompare(b); + }); + + return sortedStats; + }, [environments]); + + // Get icon for environment type + const getEnvironmentIcon = (type: string) => { + return ; + }; + + // Stat card component + const StatCard = ({ title, value, color }: { title: string; value: number; color: string }) => ( + +
+
+
{title}
+
{value}
+
+
+ {getEnvironmentIcon(title)} +
+
+
+ ); + // Filter environments based on search text const filteredEnvironments = environments.filter((env) => { const searchLower = searchText.toLowerCase(); @@ -133,7 +197,6 @@ const EnvironmentsList: React.FC = () => { buttonType="normal" icon={} onClick={handleRefresh} - loading={isLoading} > Refresh @@ -143,6 +206,23 @@ const EnvironmentsList: React.FC = () => { + {/* Environment Type Statistics */} + {!isLoading && environments.length > 0 && ( + + + {environmentStats.map(([type, count]) => ( + + + + ))} + + + )} + {/* Error handling */} {error && ( { {/* Tabs for Apps, Data Sources, and Queries */} - + Apps} key="apps"> = ({ environment, workspaceId }) => { key: 'status', render: (app: App) => ( - + {app.published ? : null} {app.published ? 'Published' : 'Draft'} {app.managed ? : } {app.managed ? 'Managed' : 'Unmanaged'} @@ -252,22 +252,22 @@ const AppsTab: React.FC = ({ environment, workspaceId }) => {
-
{title}
-
{value}
+
{title}
+
{value}
= ({ environment, workspaceId }) => { ); return ( -
+
{/* Header */}
- - <AppstoreOutlined style={{ marginRight: 10 }} /> Apps + <Title level={4} style={{ margin: 0, marginBottom: '4px' }}> + <AppstoreOutlined style={{ marginRight: 8 }} /> Apps -

Manage your workspace applications

+

+ Manage workspace applications +

@@ -320,7 +311,7 @@ const AppsTab: React.FC = ({ environment, workspaceId }) => { description={error} type="error" showIcon - style={{ marginBottom: "20px" }} + style={{ marginBottom: "16px" }} /> )} @@ -331,12 +322,12 @@ const AppsTab: React.FC = ({ environment, workspaceId }) => { description="Missing required configuration: API key or API service URL" type="warning" showIcon - style={{ marginBottom: "20px" }} + style={{ marginBottom: "16px" }} /> )} {/* Stats display */} - + = ({ environment, workspaceId }) => { {/* Content */} {loading ? (
- +
) : apps.length === 0 ? ( = ({ environment, workspaceId }) => { ) : ( <> {/* Search and Filter Bar */} -
+
setSearchText(value)} onChange={e => setSearchText(e.target.value)} style={{ width: 300 }} - size="large" />
{searchText && displayedApps.length !== apps.length && ( -
+
Showing {displayedApps.length} of {apps.length} apps
)} @@ -420,13 +407,11 @@ const AppsTab: React.FC = ({ environment, workspaceId }) => { rowKey="applicationId" pagination={{ pageSize: 10, - showTotal: (total, range) => `${range[0]}-${range[1]} of ${total} apps` + showTotal: (total, range) => `${range[0]}-${range[1]} of ${total} apps`, + size: 'small' }} + size="middle" rowClassName={() => 'app-row'} - style={{ - borderRadius: '8px', - overflow: 'hidden' - }} /> )} 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 95535d590..9cdb0b55a 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/DataSourcesTab.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/DataSourcesTab.tsx @@ -165,7 +165,7 @@ const DataSourcesTab: React.FC = ({ environment, workspaceI dataIndex: 'type', key: 'type', render: (type: string) => ( - + {type} ), @@ -176,7 +176,7 @@ const DataSourcesTab: React.FC = ({ environment, workspaceI render: (dataSource: DataSource) => ( {dataSource.managed ? : } {dataSource.managed ? 'Managed' : 'Unmanaged'} @@ -249,22 +249,22 @@ const DataSourcesTab: React.FC = ({ environment, workspaceI
-
{title}
-
{value}
+
{title}
+
{value}
= ({ environment, workspaceI ); return ( -
+
{/* Header */}
- - <DatabaseOutlined style={{ marginRight: 10 }} /> Data Sources + <Title level={4} style={{ margin: 0, marginBottom: '4px' }}> + <DatabaseOutlined style={{ marginRight: 8 }} /> Data Sources -

Manage your workspace data connections

+

+ Manage workspace data connections +

@@ -317,7 +308,7 @@ const DataSourcesTab: React.FC = ({ environment, workspaceI description={error} type="error" showIcon - style={{ marginBottom: "20px" }} + style={{ marginBottom: "16px" }} /> )} @@ -328,12 +319,12 @@ const DataSourcesTab: React.FC = ({ environment, workspaceI description="Missing required configuration: API key or API service URL" type="warning" showIcon - style={{ marginBottom: "20px" }} + style={{ marginBottom: "16px" }} /> )} {/* Stats display */} - + = ({ environment, workspaceI {/* Content */} {loading ? (
- +
) : dataSources.length === 0 ? ( = ({ environment, workspaceI ) : ( <> {/* Search and Filter Bar */} -
+
setSearchText(value)} onChange={e => setSearchText(e.target.value)} style={{ width: 300 }} - size="large" />
{searchText && displayedDataSources.length !== dataSources.length && ( -
+
Showing {displayedDataSources.length} of {dataSources.length} data sources
)} @@ -417,13 +404,11 @@ const DataSourcesTab: React.FC = ({ environment, workspaceI rowKey="id" pagination={{ pageSize: 10, - showTotal: (total, range) => `${range[0]}-${range[1]} of ${total} data sources` + showTotal: (total, range) => `${range[0]}-${range[1]} of ${total} data sources`, + size: 'small' }} + size="middle" rowClassName={() => 'datasource-row'} - style={{ - borderRadius: '8px', - overflow: 'hidden' - }} /> )} 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 66d8d80c8..0d754025f 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/QueriesTab.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/QueriesTab.tsx @@ -194,7 +194,7 @@ const QueriesTab: React.FC = ({ environment, workspaceId }) => render: (query: Query) => ( {query.managed ? : } {query.managed ? 'Managed' : 'Unmanaged'} @@ -249,22 +249,22 @@ const QueriesTab: React.FC = ({ environment, workspaceId }) =>
-
{title}
-
{value}
+
{title}
+
{value}
= ({ environment, workspaceId }) => ); return ( -
+
{/* Header */}
- - <ThunderboltOutlined style={{ marginRight: 10 }} /> Queries + <Title level={4} style={{ margin: 0, marginBottom: '4px' }}> + <ThunderboltOutlined style={{ marginRight: 8 }} /> Queries -

Manage your workspace API queries

+

+ Manage workspace API queries +

@@ -317,7 +308,7 @@ const QueriesTab: React.FC = ({ environment, workspaceId }) => description={error} type="error" showIcon - style={{ marginBottom: "20px" }} + style={{ marginBottom: "16px" }} /> )} @@ -328,12 +319,12 @@ const QueriesTab: React.FC = ({ environment, workspaceId }) => description="Missing required configuration: API key or API service URL" type="warning" showIcon - style={{ marginBottom: "20px" }} + style={{ marginBottom: "16px" }} /> )} {/* Stats display */} - + = ({ environment, workspaceId }) => {/* Content */} {loading ? (
- +
) : queries.length === 0 ? ( = ({ environment, workspaceId }) => ) : ( <> {/* Search and Filter Bar */} -
+
setSearchText(value)} onChange={e => setSearchText(e.target.value)} style={{ width: 300 }} - size="large" />
{searchText && displayedQueries.length !== queries.length && ( -
+
Showing {displayedQueries.length} of {queries.length} queries
)} @@ -410,13 +397,11 @@ const QueriesTab: React.FC = ({ environment, workspaceId }) => rowKey="id" pagination={{ pageSize: 10, - showTotal: (total, range) => `${range[0]}-${range[1]} of ${total} queries` + showTotal: (total, range) => `${range[0]}-${range[1]} of ${total} queries`, + size: 'small' }} + size="middle" rowClassName={() => 'query-row'} - style={{ - borderRadius: '8px', - overflow: 'hidden' - }} /> )} 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 0f11dac61..e9f781b3c 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/UserGroupsTab.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/UserGroupsTab.tsx @@ -105,22 +105,22 @@ const UserGroupsTab: React.FC = ({ environment }) => {
-
{title}
-
{value}
+
{title}
+
{value}
= ({ environment }) => { marginRight: 12 }} shape="square" + size="small" > {group.groupName.charAt(0).toUpperCase()}
-
{group.groupName}
-
+
{group.groupName}
+
{group.groupId}
@@ -161,17 +162,17 @@ const UserGroupsTab: React.FC = ({ environment }) => { key: 'type', render: (_: any, group: UserGroup) => { if (group.allUsersGroup) return ( - + All Users ); if (group.devGroup) return ( - + Developers ); return ( - + Custom ); @@ -182,7 +183,7 @@ const UserGroupsTab: React.FC = ({ environment }) => { key: 'members', render: (_: any, group: UserGroup) => ( - + {group.stats?.userCount || 0} @@ -193,7 +194,7 @@ const UserGroupsTab: React.FC = ({ environment }) => { key: 'adminMembers', render: (_: any, group: UserGroup) => ( - + {group.stats?.adminUserCount || 0} @@ -204,7 +205,7 @@ const UserGroupsTab: React.FC = ({ environment }) => { dataIndex: 'createTime', key: 'createTime', render: (createTime: number) => ( - + {new Date(createTime).toLocaleDateString()} ), @@ -212,35 +213,26 @@ const UserGroupsTab: React.FC = ({ environment }) => { ]; return ( -
+
{/* Header */}
- - <UsergroupAddOutlined style={{ marginRight: 10 }} /> User Groups + <Title level={4} style={{ margin: 0, marginBottom: '4px' }}> + <UsergroupAddOutlined style={{ marginRight: 8 }} /> User Groups -

Manage user groups in this environment

+

+ Manage user groups in this environment +

@@ -253,7 +245,7 @@ const UserGroupsTab: React.FC = ({ environment }) => { description={error} type="error" showIcon - style={{ marginBottom: "20px" }} + style={{ marginBottom: "16px" }} /> )} @@ -264,12 +256,12 @@ const UserGroupsTab: React.FC = ({ environment }) => { description="Missing required configuration: API key or API service URL" type="warning" showIcon - style={{ marginBottom: "20px" }} + style={{ marginBottom: "16px" }} /> )} {/* Stats display */} - + = ({ environment }) => { {/* Content */} {loading ? (
- +
) : userGroups.length === 0 ? ( = ({ environment }) => { ) : ( <> {/* Search Bar */} -
+
setSearchText(value)} onChange={e => setSearchText(e.target.value)} style={{ width: 300 }} - size="large" /> {searchText && filteredUserGroups.length !== userGroups.length && ( -
+
Showing {filteredUserGroups.length} of {userGroups.length} user groups
)} @@ -341,12 +332,10 @@ const UserGroupsTab: React.FC = ({ environment }) => { rowKey="groupId" pagination={{ pageSize: 10, - showTotal: (total, range) => `${range[0]}-${range[1]} of ${total} user groups` - }} - style={{ - borderRadius: '8px', - overflow: 'hidden' + showTotal: (total, range) => `${range[0]}-${range[1]} of ${total} user groups`, + size: 'small' }} + size="middle" rowClassName={() => 'group-row'} /> 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 9cc2bc61f..f4c00c22e 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/WorkspaceHeader.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/WorkspaceHeader.tsx @@ -44,7 +44,7 @@ const HeaderWrapper = styled.div` `; const GradientBanner = styled.div<{ avatarColor: string }>` - background: linear-gradient(135deg, ${props => props.avatarColor} 0%, #feb47b 100%); + background: linear-gradient(135deg, ${props => props.avatarColor} 0%, rgba(24, 144, 255, 0.8) 100%); height: 140px; position: relative; overflow: hidden; @@ -77,7 +77,7 @@ const GradientBanner = styled.div<{ avatarColor: string }>` } &:hover { - background: linear-gradient(135deg, #feb47b 0%, ${props => props.avatarColor} 100%); + background: linear-gradient(135deg, rgba(24, 144, 255, 0.8) 0%, ${props => props.avatarColor} 100%); transition: background 1s ease-in-out; } `; @@ -89,7 +89,7 @@ const ContentContainer = styled.div` transition: transform 0.3s ease-in-out; &:hover { - transform: translateY(-5px); + transform: translateY(-2px); } `; @@ -99,41 +99,40 @@ const AvatarContainer = styled.div` left: 24px; background: white; padding: 4px; - border-radius: 16px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + border-radius: 8px; + border: 1px solid #f0f0f0; `; const StatusBadge = styled(Tag)<{ $active?: boolean }>` position: absolute; top: 12px; right: 12px; - font-weight: 600; + font-weight: 500; font-size: 12px; padding: 4px 12px; - border-radius: 20px; + border-radius: 4px; border: none; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); - background: ${props => props.$active ? 'linear-gradient(135deg, #52c41a, #389e0d)' : '#f0f0f0'}; - color: ${props => props.$active ? 'white' : '#666'}; + background: ${props => props.$active ? '#52c41a' : '#f5f5f5'}; + color: ${props => props.$active ? 'white' : '#8c8c8c'}; `; const StatCard = styled(Card)` - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + border-radius: 4px; + border: 1px solid #f0f0f0; transition: all 0.3s; &:hover { - transform: translateY(-3px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); + transform: translateY(-2px); + border-color: #d9d9d9; } `; const ActionButton = styled(Button)` - border-radius: 8px; + border-radius: 4px; display: flex; align-items: center; justify-content: center; - height: 38px; + height: 32px; `; const FavoriteButton = styled(Button)` @@ -141,7 +140,9 @@ const FavoriteButton = styled(Button)` top: 12px; right: 80px; border: none; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + border-radius: 4px; + background: rgba(255, 255, 255, 0.9); + color: #722ed1; `; interface WorkspaceHeaderProps { @@ -167,7 +168,7 @@ const WorkspaceHeader: React.FC = ({ hash = name.charCodeAt(i) + ((hash << 5) - hash); } const hue = Math.abs(hash % 360); - return `hsl(${hue}, 70%, 50%)`; + return `hsl(${hue}, 45%, 55%)`; }; // Format date for last updated @@ -214,7 +215,7 @@ const WorkspaceHeader: React.FC = ({ created on {formatDate(workspace.creationDate)} - + {environment.environmentName} 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 006ffb1f6..26732c76a 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/WorkspacesTab.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/WorkspacesTab.tsx @@ -101,22 +101,22 @@ const WorkspacesTab: React.FC = ({ environment }) => {
-
{title}
-
{value}
+
{title}
+
{value}
= ({ environment }) => { marginRight: 12 }} shape="square" + size="small" > {workspace.name.charAt(0).toUpperCase()}
-
{workspace.name}
-
+
{workspace.name}
+
{workspace.id}
@@ -162,7 +163,7 @@ const WorkspacesTab: React.FC = ({ environment }) => { dataIndex: 'status', key: 'status', render: (status: string) => ( - + {status === 'ACTIVE' ? : null} {status} @@ -174,7 +175,7 @@ const WorkspacesTab: React.FC = ({ environment }) => { render: (_: any, workspace: Workspace) => ( {workspace.managed ? @@ -192,6 +193,7 @@ const WorkspacesTab: React.FC = ({ environment }) => { @@ -248,7 +241,7 @@ const WorkspacesTab: React.FC = ({ environment }) => { description={error} type="error" showIcon - style={{ marginBottom: "20px" }} + style={{ marginBottom: "16px" }} /> )} @@ -259,12 +252,12 @@ const WorkspacesTab: React.FC = ({ environment }) => { description="Missing required configuration: API key or API service URL" type="warning" showIcon - style={{ marginBottom: "20px" }} + style={{ marginBottom: "16px" }} /> )} {/* Stats display */} - + = ({ environment }) => { {/* Content */} {loading ? (
- +
) : workspaces.length === 0 ? ( = ({ environment }) => { ) : ( <> {/* Search and Filter Bar */} -
+
setSearchText(value)} onChange={e => setSearchText(e.target.value)} style={{ width: 300 }} - size="large" />
{searchText && displayedWorkspaces.length !== workspaces.length && ( -
+
Showing {displayedWorkspaces.length} of {workspaces.length} workspaces
)} @@ -341,12 +330,10 @@ const WorkspacesTab: React.FC = ({ environment }) => { rowKey="id" pagination={{ pageSize: 10, - showTotal: (total, range) => `${range[0]}-${range[1]} of ${total} workspaces` - }} - style={{ - borderRadius: '8px', - overflow: 'hidden' + showTotal: (total, range) => `${range[0]}-${range[1]} of ${total} workspaces`, + size: 'small' }} + size="middle" onRow={(record) => ({ onClick: () => handleRowClick(record), style: { cursor: 'pointer' } 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 ac4a1fd96..ff0be0ce6 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 @@ -2,7 +2,7 @@ import axios from 'axios'; import { EnvironmentLicense } from '../types/environment.types'; /** - * Check license status for an environment + * Check if license endpoint exists for an environment * @param apiServiceUrl - API service URL for the environment * @param apiKey - API key for the environment * @returns Promise with license information @@ -25,46 +25,25 @@ export async function checkEnvironmentLicense( headers.Authorization = `Bearer ${apiKey}`; } - // Make request to the license endpoint - const response = await axios.get( + // Use GET request to check endpoint existence + await axios.get( `${apiServiceUrl}/api/plugins/enterprise/license`, { headers, - timeout: 5000 // 5 second timeout + timeout: 500 // Very short timeout for immediate failure when endpoint doesn't exist } ); - // If we get a successful response, the license is valid + // If we get a successful response, the endpoint exists 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' - }; - } - } - + // Any error means the endpoint doesn't exist or isn't accessible return { isValid: false, - error: error instanceof Error ? error.message : 'License check failed' + error: 'License not available' }; } } \ No newline at end of file From d36c5dcf36b27d9f403ef1074f347b9682255f4c Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 28 May 2025 16:00:18 +0500 Subject: [PATCH 04/10] Fix layout issue for Environments List --- .../pages/setting/environments/EnvironmentsList.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx index a8cd4bda8..c6f739c51 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, Row, Col, Card } from "antd"; +import { Alert, Empty, Spin, Card } from "antd"; import { SyncOutlined, CloudServerOutlined } from "@ant-design/icons"; import { AddIcon, Search, TacoButton } from "lowcoder-design"; import { useHistory } from "react-router-dom"; @@ -19,6 +19,7 @@ const EnvironmentsWrapper = styled.div` flex-direction: column; width: 100%; height: 100%; + min-width: 1000px; `; const HeaderWrapper = styled.div` @@ -209,17 +210,17 @@ const EnvironmentsList: React.FC = () => { {/* Environment Type Statistics */} {!isLoading && environments.length > 0 && ( - +
{environmentStats.map(([type, count]) => ( - +
- +
))} - +
)} From ec7fd1df1f4d87cde77f182632a952fdef7ff35e Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 28 May 2025 16:10:04 +0500 Subject: [PATCH 05/10] remove tabs borders --- .../src/pages/setting/environments/EnvironmentDetail.tsx | 6 ------ .../src/pages/setting/environments/WorkspaceDetail.tsx | 6 ------ 2 files changed, 12 deletions(-) diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx index 437a5511b..041997779 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx @@ -227,12 +227,6 @@ const EnvironmentDetail: React.FC = () => { onChange={setActiveTab} className="modern-tabs" type="line" - style={{ - background: '#fff', - borderRadius: '4px', - border: '1px solid #f0f0f0', - padding: '0' - }} > { defaultActiveKey="apps" className="modern-tabs" type="line" - style={{ - background: '#fff', - borderRadius: '4px', - border: '1px solid #f0f0f0', - padding: '0' - }} > Apps} key="apps"> Date: Wed, 28 May 2025 20:00:44 +0500 Subject: [PATCH 06/10] Add hubspot modal for the unlicensed environment --- .../components/ContactLowcoderModal.tsx | 303 ++++++++++-------- .../services/environments.service.ts | 43 +++ 2 files changed, 215 insertions(+), 131 deletions(-) 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 22c9ddd64..a970a4116 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/ContactLowcoderModal.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/ContactLowcoderModal.tsx @@ -1,132 +1,173 @@ -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 */} -
- - ); -}; - +import React, { useState, useEffect } from 'react'; +import { Modal, Card, Row, Col, Typography, Divider, Spin, Alert } from 'antd'; +import { CustomerServiceOutlined, CloudServerOutlined } from '@ant-design/icons'; +import { Environment } from '../types/environment.types'; +import { getEnvironmentDeploymentId } from '../services/environments.service'; +import { HubspotModal } from '../../hubspotModal'; + +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 +}) => { + const [deploymentId, setDeploymentId] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [showHubspotModal, setShowHubspotModal] = useState(false); + + // Fetch deployment ID when modal opens + useEffect(() => { + if (visible && environment.environmentApiServiceUrl && environment.environmentApikey) { + setIsLoading(true); + setError(null); + + getEnvironmentDeploymentId( + environment.environmentApiServiceUrl, + environment.environmentApikey + ) + .then((id) => { + setDeploymentId(id); + setShowHubspotModal(true); + }) + .catch((err) => { + console.error('Failed to fetch deployment ID:', err); + setError(err instanceof Error ? err.message : 'Failed to fetch deployment ID'); + }) + .finally(() => { + setIsLoading(false); + }); + } else if (visible) { + setError('Environment API service URL or API key not configured'); + } + }, [visible, environment.environmentApiServiceUrl, environment.environmentApikey]); + + // Handle HubSpot modal close + const handleHubspotClose = () => { + setShowHubspotModal(false); + onClose(); + }; + + // Handle main modal close + const handleClose = () => { + setShowHubspotModal(false); + setDeploymentId(null); + setError(null); + onClose(); + }; + + // Show HubSpot modal if we have deployment ID + if (showHubspotModal && deploymentId) { + return ( + + ); + } + + return ( + + + Contact Lowcoder Team +
+ } + open={visible} + onCancel={handleClose} + 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: {isLoading ? 'Loading...' : deploymentId || 'Not available'} + +
+ +
+
+ + + + {/* Loading, Error, or Success State */} +
+ {isLoading && ( +
+ + + Fetching deployment information... + +
+ )} + + {error && ( + + )} + + {!isLoading && !error && !deploymentId && ( +
+ +
Please ensure the environment is properly configured to contact support.
+
+ )} +
+ + ); +}; + export default ContactLowcoderModal; \ 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 b0c489b7a..eb11609f5 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 @@ -578,4 +578,47 @@ export async function getEnvironmentsWithLicenseStatus(): Promise messageInstance.error(errorMessage); throw error; } +} + +/** + * Fetch deployment ID from a specific environment + * @param apiServiceUrl - API service URL for the environment + * @param apiKey - API key for the environment + * @returns Promise with deployment ID string + */ +export async function getEnvironmentDeploymentId( + apiServiceUrl: string, + apiKey: string +): Promise { + try { + // Check if required parameters are provided + if (!apiServiceUrl) { + throw new Error('API service URL is required'); + } + + if (!apiKey) { + throw new Error('API key is required to fetch deployment ID'); + } + + // Set up headers with the Bearer token format + const headers = { + Authorization: `Bearer ${apiKey}` + }; + + // Make the API request to get deployment ID + const response = await axios.get(`${apiServiceUrl}/api/configs/deploymentId`, { headers }); + + // Check if response is valid + if (!response.data) { + throw new Error('Failed to fetch deployment ID'); + } + + // The response should return a string directly + return response.data; + } catch (error) { + // Handle and transform error + const errorMessage = error instanceof Error ? error.message : 'Failed to fetch deployment ID'; + messageInstance.error(errorMessage); + throw error; + } } \ No newline at end of file From 0b505e09f5ee88ff3a617e8bf9857efad36841a6 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 28 May 2025 20:15:03 +0500 Subject: [PATCH 07/10] update to try/catch and async/await --- .../components/ContactLowcoderModal.tsx | 43 +++++++++++-------- 1 file changed, 24 insertions(+), 19 deletions(-) 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 a970a4116..73d31c277 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/ContactLowcoderModal.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/ContactLowcoderModal.tsx @@ -28,28 +28,33 @@ const ContactLowcoderModal: React.FC = ({ // Fetch deployment ID when modal opens useEffect(() => { - if (visible && environment.environmentApiServiceUrl && environment.environmentApikey) { + const fetchDeploymentId = async () => { + if (!visible || !environment.environmentApiServiceUrl || !environment.environmentApikey) { + if (visible) { + setError('Environment API service URL or API key not configured'); + } + return; + } + setIsLoading(true); setError(null); - getEnvironmentDeploymentId( - environment.environmentApiServiceUrl, - environment.environmentApikey - ) - .then((id) => { - setDeploymentId(id); - setShowHubspotModal(true); - }) - .catch((err) => { - console.error('Failed to fetch deployment ID:', err); - setError(err instanceof Error ? err.message : 'Failed to fetch deployment ID'); - }) - .finally(() => { - setIsLoading(false); - }); - } else if (visible) { - setError('Environment API service URL or API key not configured'); - } + try { + const id = await getEnvironmentDeploymentId( + environment.environmentApiServiceUrl, + environment.environmentApikey + ); + setDeploymentId(id); + setShowHubspotModal(true); + } catch (err) { + console.error('Failed to fetch deployment ID:', err); + setError(err instanceof Error ? err.message : 'Failed to fetch deployment ID'); + } finally { + setIsLoading(false); + } + }; + + fetchDeploymentId(); }, [visible, environment.environmentApiServiceUrl, environment.environmentApikey]); // Handle HubSpot modal close From ae24fed566880f097ecb32ebb6d596320a97e8f4 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 28 May 2025 21:16:47 +0500 Subject: [PATCH 08/10] fix orgId --- .../setting/environments/components/ContactLowcoderModal.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 73d31c277..145b08e98 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/ContactLowcoderModal.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/ContactLowcoderModal.tsx @@ -1,9 +1,11 @@ import React, { useState, useEffect } from 'react'; import { Modal, Card, Row, Col, Typography, Divider, Spin, Alert } from 'antd'; import { CustomerServiceOutlined, CloudServerOutlined } from '@ant-design/icons'; +import { useSelector } from 'react-redux'; import { Environment } from '../types/environment.types'; import { getEnvironmentDeploymentId } from '../services/environments.service'; import { HubspotModal } from '../../hubspotModal'; +import { getUser } from 'redux/selectors/usersSelectors'; const { Title, Text } = Typography; @@ -25,6 +27,7 @@ const ContactLowcoderModal: React.FC = ({ const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [showHubspotModal, setShowHubspotModal] = useState(false); + const user = useSelector(getUser); // Fetch deployment ID when modal opens useEffect(() => { @@ -77,7 +80,7 @@ const ContactLowcoderModal: React.FC = ({ ); From 68e39e56f975779aad4878977ce794da46e68a73 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 28 May 2025 23:30:09 +0500 Subject: [PATCH 09/10] Only 1 master environment can be created --- .../components/CreateEnvironmentModal.tsx | 24 +++++++++++++++++-- .../redux/selectors/enterpriseSelectors.ts | 10 ++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) 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 dac35d7be..0e2a71175 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/CreateEnvironmentModal.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/CreateEnvironmentModal.tsx @@ -1,5 +1,7 @@ import React, { useState } from 'react'; -import { Modal, Form, Input, Select, Switch, Button, Alert } 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'; const { Option } = Select; @@ -20,6 +22,10 @@ const CreateEnvironmentModal: React.FC = ({ const [form] = Form.useForm(); const [submitLoading, setSubmitLoading] = useState(false); + // Redux selectors to check for existing master environment + const hasMasterEnvironment = useSelector(selectHasMasterEnvironment); + const masterEnvironment = useSelector(selectMasterEnvironment); + const handleSubmit = async () => { try { const values = await form.validateFields(); @@ -151,9 +157,23 @@ const CreateEnvironmentModal: React.FC = ({ label="Master Environment" valuePropName="checked" > - + + + + {hasMasterEnvironment && ( + + )} + { return environments.filter(env => env.isLicensed !== false); }; +export const selectMasterEnvironment = (state: AppState) => { + const environments = state.ui.enterprise?.environments ?? []; + return environments.find(env => env.isMaster) ?? null; +}; + +export const selectHasMasterEnvironment = (state: AppState) => { + const environments = state.ui.enterprise?.environments ?? []; + return environments.some(env => env.isMaster); +}; + From 4c6a81b6c77a8bf4a7b7afbd67d571a518fe43f1 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 29 May 2025 00:14:02 +0500 Subject: [PATCH 10/10] Fix only one master environment --- .../components/CreateEnvironmentModal.tsx | 67 ++++++++++++------- .../components/EditEnvironmentModal.tsx | 61 ++++++++++++++--- 2 files changed, 93 insertions(+), 35 deletions(-) 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 0e2a71175..ace410bab 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/CreateEnvironmentModal.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/CreateEnvironmentModal.tsx @@ -21,18 +21,33 @@ const CreateEnvironmentModal: React.FC = ({ }) => { const [form] = Form.useForm(); const [submitLoading, setSubmitLoading] = useState(false); + const [isMaster, setIsMaster] = useState(false); // Redux selectors to check for existing master environment const hasMasterEnvironment = useSelector(selectHasMasterEnvironment); const masterEnvironment = useSelector(selectMasterEnvironment); + const handleMasterChange = (checked: boolean) => { + // Only allow enabling master if no master environment exists + if (checked && hasMasterEnvironment) { + return; // Do nothing if trying to enable master when one already exists + } + setIsMaster(checked); + }; + const handleSubmit = async () => { try { const values = await form.validateFields(); setSubmitLoading(true); - await onSave(values); - form.resetFields(); // Reset form after successful creation + const submitData = { + ...values, + isMaster + }; + + await onSave(submitData); + form.resetFields(); + setIsMaster(false); // Reset master state onClose(); } catch (error) { if (error instanceof Error) { @@ -44,7 +59,8 @@ const CreateEnvironmentModal: React.FC = ({ }; const handleCancel = () => { - form.resetFields(); // Reset form when canceling + form.resetFields(); + setIsMaster(false); // Reset master state onClose(); }; @@ -74,8 +90,7 @@ const CreateEnvironmentModal: React.FC = ({ layout="vertical" name="create_environment_form" initialValues={{ - environmentType: "DEV", - isMaster: false + environmentType: "DEV" }} > = ({ /> - - - - + +
+ + + + {isMaster && ( + + Will be Master + + )} +
- {hasMasterEnvironment && ( - - )} - = ({ }) => { const [form] = Form.useForm(); const [submitLoading, setSubmitLoading] = useState(false); + const [isMaster, setIsMaster] = useState(false); + + // Redux selectors to check for existing master environment + const hasMasterEnvironment = useSelector(selectHasMasterEnvironment); + const masterEnvironment = useSelector(selectMasterEnvironment); + + // Check if another environment is master (not this one) + const hasOtherMaster = hasMasterEnvironment && masterEnvironment?.environmentId !== environment?.environmentId; // Initialize form with environment data when it changes useEffect(() => { if (environment) { + setIsMaster(environment.isMaster); form.setFieldsValue({ environmentName: environment.environmentName || '', environmentDescription: environment.environmentDescription || '', @@ -32,12 +43,19 @@ const EditEnvironmentModal: React.FC = ({ environmentApiServiceUrl: environment.environmentApiServiceUrl || '', environmentFrontendUrl: environment.environmentFrontendUrl || '', environmentNodeServiceUrl: environment.environmentNodeServiceUrl || '', - environmentApikey: environment.environmentApikey || '', - isMaster: environment.isMaster + environmentApikey: environment.environmentApikey || '' }); } }, [environment, form]); + const handleMasterChange = (checked: boolean) => { + // Only allow enabling master if no other environment is master + if (checked && hasOtherMaster) { + return; // Do nothing if trying to enable master when another exists + } + setIsMaster(checked); + }; + const handleSubmit = async () => { if (!environment) return; @@ -45,7 +63,12 @@ const EditEnvironmentModal: React.FC = ({ const values = await form.validateFields(); setSubmitLoading(true); - await onSave(values); // Call with only the data parameter + const submitData = { + ...values, + isMaster + }; + + await onSave(submitData); onClose(); } catch (error) { if (error instanceof Error) { @@ -144,13 +167,31 @@ const EditEnvironmentModal: React.FC = ({ />
- - + +
+ + + + {isMaster && ( + + Currently Master + + )} +
+ + );