diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx
index b772bf373..2106d8d3d 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx
+++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx
@@ -3,29 +3,30 @@ import {
Spin,
Typography,
Card,
- Tag,
Tabs,
Alert,
Descriptions,
Menu,
Button,
- Breadcrumb,
+ Tag,
} from "antd";
import {
LinkOutlined,
- TeamOutlined,
+ HomeOutlined,
+ AppstoreOutlined,
+ UsergroupAddOutlined,
EditOutlined,
- HomeOutlined
} from "@ant-design/icons";
import { useSingleEnvironmentContext } from "./context/SingleEnvironmentContext";
-import { workspaceConfig } from "./config/workspace.config";
-import { userGroupsConfig } from "./config/usergroups.config";
-import DeployableItemsTab from "./components/DeployableItemsTab";
import EditEnvironmentModal from "./components/EditEnvironmentModal";
import { Environment } from "./types/environment.types";
import history from "@lowcoder-ee/util/history";
-
+import WorkspacesTab from "./components/WorkspacesTab";
+import UserGroupsTab from "./components/UserGroupsTab";
+import EnvironmentHeader from "./components/EnvironmentHeader";
+import ModernBreadcrumbs from "./components/ModernBreadcrumbs";
+import { getEnvironmentTagColor } from "./utils/environmentUtils";
const { Title, Text } = Typography;
const { TabPane } = Tabs;
@@ -44,6 +45,7 @@ const EnvironmentDetail: React.FC = () => {
const [isEditModalVisible, setIsEditModalVisible] = useState(false);
const [isUpdating, setIsUpdating] = useState(false);
+ const [activeTab, setActiveTab] = useState('workspaces');
// Handle edit menu item click
const handleEditClick = () => {
@@ -73,15 +75,7 @@ const EnvironmentDetail: React.FC = () => {
}
};
- // Dropdown menu for environment actions
- const actionsMenu = (
-
- );
+
if (isLoading) {
return (
@@ -92,67 +86,81 @@ const EnvironmentDetail: React.FC = () => {
}
if (error || !environment) {
+ const errorItems = [
+ {
+ key: 'environments',
+ title: (
+
+ Environments
+
+ ),
+ onClick: () => history.push("/setting/environments")
+ },
+ {
+ key: 'notFound',
+ title: 'Not Found'
+ }
+ ];
+
return (
-
+
+
+
+
+
+
+ Environment Not Found
+
+
+ {error || "The environment you're looking for doesn't exist or you don't have permission to view it."}
+
+
+
+
+
);
}
-
+
+ const breadcrumbItems = [
+ {
+ key: 'environments',
+ title: (
+
+ Environments
+
+ ),
+ onClick: () => history.push("/setting/environments")
+ },
+ {
+ key: 'currentEnvironment',
+ title: environment.environmentName
+ }
+ ];
+
return (
-
-
- history.push("/setting/environments")}
- >
- Environments
-
-
- {environment.environmentName}
-
+ {/* Environment Header Component */}
+
- {/* Header with environment name and controls */}
-
-
-
- {environment.environmentName || "Unnamed Environment"}
-
- ID: {environment.environmentId}
-
-
- }
- onClick={handleEditClick}
- type="primary"
- >
- Edit Environment
-
-
-
+
{/* Basic Environment Information Card - improved responsiveness */}
Master}
+ style={{ marginBottom: "24px", borderRadius: '8px', boxShadow: '0 2px 8px rgba(0,0,0,0.05)' }}
+ className="environment-overview-card"
>
{
{environment.environmentType}
{environment.environmentApikey ? (
- Configured
+ Configured
) : (
- Not Configured
+ Not Configured
)}
@@ -199,33 +202,41 @@ const EnvironmentDetail: React.FC = () => {
+ {/* Modern Breadcrumbs navigation */}
+
{/* Tabs for Workspaces and User Groups */}
-
-
- {/* Using our new generic component with the workspace config */}
-
+
+
+ Workspaces
+
+ }
+ key="workspaces"
+ >
+ {/* Using our new standalone WorkspacesTab component */}
+
+
- User Groups
+ User Groups
}
key="userGroups"
>
- {/* Using our new generic component with the user group config */}
-
+ {/* Now using our standalone UserGroupsTab component */}
+
-
+
{/* Edit Environment Modal */}
{environment && (
{
const {
environments,
isLoading,
- error,
+ error,
+ refreshEnvironments
} = useEnvironmentContext();
- console.log("Environments:", environments);
-
// State for search input
const [searchText, setSearchText] = useState("");
+ const [isRefreshing, setIsRefreshing] = useState(false);
// Hook for navigation
const history = useHistory();
@@ -46,20 +46,157 @@ const EnvironmentsList: React.FC = () => {
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 (
-
- {/* Header section with title and controls */}
+
+ {/* Modern gradient header */}
-
Environments
-
+
+
+
+
+
+
+
+
+ Environments
+
+
+ Manage your deployment environments across dev, test, preprod, and production
+
+
+
+
+
+
+ }
+ onClick={handleRefresh}
+ loading={isLoading}
+ type="default"
+ style={{
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
+ borderColor: 'rgba(255, 255, 255, 0.4)',
+ color: 'white',
+ fontWeight: 500
+ }}
+ >
+ Refresh
+
+
+
+
+
+
+ {/* Environment type stats */}
+ {environments.length > 0 && (
+
+
+
+
+
+
+
+
+ {environments.length}
+
+
+ Total Environments
+
+
+
+
+
+ {['PROD', 'PREPROD', 'TEST', 'DEV'].map(type => (
+
+
+
+
+ {environmentCounts[type] || 0}
+
+
+
+ {type} Environments
+
+
+
+
+ ))}
+
+
+
+
+ )}
+
+ {/* Main content card */}
+
{
prefix={}
allowClear
/>
-
-
+ }
+ >
+ {/* Error handling */}
+ {error && (
+
+ )}
- {/* Error handling */}
- {error && (
-
- )}
+ {/* Loading, empty state or table */}
+ {isLoading ? (
+
+
+
+ ) : environments.length === 0 && !error ? (
+
+ ) : filteredEnvironments.length === 0 ? (
+
+ ) : (
+ /* Table component */
+
+ )}
- {/* Empty state handling */}
- {!isLoading && environments.length === 0 && !error ? (
-
- ) : (
- /* Table component */
-
- )}
+ {/* Results counter when searching */}
+ {searchText && filteredEnvironments.length !== environments.length && (
+
+ Showing {filteredEnvironments.length} of {environments.length} environments
+
+ )}
+
);
};
diff --git a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx
index 9079756af..b663b9ee5 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx
+++ b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx
@@ -3,15 +3,8 @@ import history from "@lowcoder-ee/util/history";
import {
Spin,
Typography,
- Card,
Tabs,
- Button,
- Breadcrumb,
- Space,
- Tag,
- Switch,
message,
- Tooltip
} from "antd";
import {
AppstoreOutlined,
@@ -19,8 +12,6 @@ import {
CodeOutlined,
HomeOutlined,
TeamOutlined,
- ArrowLeftOutlined,
- CloudUploadOutlined
} from "@ant-design/icons";
// Use the context hooks
@@ -28,13 +19,13 @@ import { useSingleEnvironmentContext } from "./context/SingleEnvironmentContext"
import { useWorkspaceContext } from "./context/WorkspaceContext";
import { useDeployModal } from "./context/DeployModalContext";
-import DeployableItemsTab from "./components/DeployableItemsTab";
import { workspaceConfig } from "./config/workspace.config";
-import { appsConfig } from "./config/apps.config";
-import { dataSourcesConfig } from "./config/data-sources.config";
-import { queryConfig } from "./config/query.config";
+import AppsTab from "./components/AppsTab";
+import DataSourcesTab from "./components/DataSourcesTab";
+import QueriesTab from "./components/QueriesTab";
+import ModernBreadcrumbs from "./components/ModernBreadcrumbs";
+import WorkspaceHeader from "./components/WorkspaceHeader";
-const { Title, Text } = Typography;
const { TabPane } = Tabs;
const WorkspaceDetail: React.FC = () => {
@@ -43,6 +34,8 @@ const WorkspaceDetail: React.FC = () => {
const { workspace, isLoading, error, toggleManagedStatus } = useWorkspaceContext();
const { openDeployModal } = useDeployModal();
+ console.log("workspace render", workspace);
+
const [isToggling, setIsToggling] = useState(false);
// Handle toggle managed status
@@ -80,115 +73,72 @@ const WorkspaceDetail: React.FC = () => {
);
}
- return (
-
- {/* Breadcrumb navigation */}
-
-
- history.push("/setting/environments")}>
- Environments
-
-
-
- history.push(`/setting/environments/${environment.environmentId}`)}
- >
- {environment.environmentName}
-
-
- {workspace.name}
-
+ const breadcrumbItems = [
+ {
+ key: 'environments',
+ title: (
+
+ Environments
+
+ ),
+ onClick: () => history.push("/setting/environments")
+ },
+ {
+ key: 'environment',
+ title: (
+
+ {environment.environmentName}
+
+ ),
+ onClick: () => history.push(`/setting/environments/${environment.environmentId}`)
+ },
+ {
+ key: 'workspace',
+ title: workspace.name
+ }
+ ];
- {/* Workspace header with details and actions */}
-
-
- {/* Left section - Workspace info */}
-
-
- {workspace.name}
-
-
-
- ID: {workspace.id}
-
-
- GID: {workspace.gid || 'N/A'}
-
-
- {workspace.managed ? "Managed" : "Unmanaged"}
-
-
-
+ return (
+
+ {/* New Workspace Header */}
+
openDeployModal(workspace, workspaceConfig, environment)}
+ />
- {/* Right section - Actions */}
-
-
- Managed:
-
-
-
- }
- onClick={() =>
- openDeployModal(workspace, workspaceConfig, environment)
- }
- disabled={!workspace.managed}
- >
- Deploy
-
-
- }
- onClick={() => history.push(`/setting/environments/${environment.environmentId}`)}
- >
- Back
-
-
-
-
+ {/* Modern Breadcrumbs navigation */}
+
{/* Tabs for Apps, Data Sources, and Queries */}
-
+
Apps} key="apps">
-
Data Sources} key="dataSources">
-
-
Queries} key="queries">
-
+
);
diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/AppsTab.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/AppsTab.tsx
new file mode 100644
index 000000000..6f9aea130
--- /dev/null
+++ b/client/packages/lowcoder/src/pages/setting/environments/components/AppsTab.tsx
@@ -0,0 +1,425 @@
+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 Title from 'antd/lib/typography/Title';
+import { Environment } from '../types/environment.types';
+import { App, AppStats } from '../types/app.types';
+import { getMergedWorkspaceApps } from '../services/apps.service';
+import { Switch, Spin, Empty, Avatar } from 'antd';
+import { ManagedObjectType, setManagedObject, unsetManagedObject } from '../services/managed-objects.service';
+import { useDeployModal } from '../context/DeployModalContext';
+import { appsConfig } from '../config/apps.config';
+import history from "@lowcoder-ee/util/history";
+
+const { Search } = Input;
+
+interface AppsTabProps {
+ environment: Environment;
+ workspaceId: string;
+}
+
+const AppsTab: React.FC = ({ environment, workspaceId }) => {
+ const [apps, setApps] = useState([]);
+ const [stats, setStats] = useState({
+ total: 0,
+ published: 0,
+ managed: 0,
+ unmanaged: 0
+ });
+ const [loading, setLoading] = useState(false);
+ const [refreshing, setRefreshing] = useState(false);
+ const [error, setError] = useState(null);
+ const [searchText, setSearchText] = useState('');
+ const { openDeployModal } = useDeployModal();
+ const [showManagedOnly, setShowManagedOnly] = useState(false);
+
+ // Fetch apps
+ const fetchApps = async () => {
+ if (!workspaceId || !environment) return;
+
+ setLoading(true);
+ setError(null);
+
+ try {
+ const result = await getMergedWorkspaceApps(
+ workspaceId,
+ environment.environmentId,
+ environment.environmentApikey,
+ environment.environmentApiServiceUrl!
+ );
+
+ setApps(result.apps);
+
+ // Calculate stats
+ const total = result.apps.length;
+ const published = result.apps.filter(app => app.published).length;
+ const managed = result.apps.filter(app => app.managed).length;
+
+ setStats({
+ total,
+ published,
+ managed,
+ unmanaged: total - managed
+ });
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to fetch apps");
+ } finally {
+ setLoading(false);
+ setRefreshing(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchApps();
+ }, [environment, workspaceId]);
+
+ // Handle refresh
+ const handleRefresh = () => {
+ setRefreshing(true);
+ fetchApps();
+ };
+
+ // Toggle managed status
+ const handleToggleManaged = async (app: App, checked: boolean) => {
+ setRefreshing(true);
+ try {
+ if (checked) {
+ await setManagedObject(
+ app.applicationGid,
+ environment.environmentId,
+ ManagedObjectType.APP,
+
+ );
+ } else {
+ await unsetManagedObject(
+ app.applicationGid,
+ environment.environmentId,
+ ManagedObjectType.APP
+ );
+ }
+
+ // Update the app in state
+ const updatedApps = apps.map(item => {
+ if (item.applicationId === app.applicationId) {
+ return { ...item, managed: checked };
+ }
+ return item;
+ });
+
+ setApps(updatedApps);
+
+ // Update stats
+ const managed = updatedApps.filter(app => app.managed).length;
+ setStats(prev => ({
+ ...prev,
+ managed,
+ unmanaged: prev.total - managed
+ }));
+
+ message.success(`${app.name} is now ${checked ? 'Managed' : 'Unmanaged'}`);
+ return true;
+ } catch (error) {
+ message.error(`Failed to change managed status for ${app.name}`);
+ return false;
+ } finally {
+ setRefreshing(false);
+ }
+ };
+
+ // Filter apps based on search
+ const filteredApps = searchText
+ ? apps.filter(app =>
+ app.name.toLowerCase().includes(searchText.toLowerCase()) ||
+ app.applicationId.toLowerCase().includes(searchText.toLowerCase()))
+ : apps;
+
+ const displayedApps = showManagedOnly
+ ? filteredApps.filter(app => app.managed)
+ : filteredApps;
+
+ // Table columns
+ const columns = [
+ {
+ title: 'App',
+ key: 'app',
+ render: (app: App) => (
+
+
+ {app.name.charAt(0).toUpperCase()}
+
+
+
{app.name}
+
+ {app.applicationId}
+
+
+
+ ),
+ },
+ {
+ title: 'Status',
+ key: 'status',
+ render: (app: App) => (
+
+
+ {app.published ? : null} {app.published ? 'Published' : 'Draft'}
+
+
+ {app.managed ? : } {app.managed ? 'Managed' : 'Unmanaged'}
+
+
+ ),
+ },
+ {
+ title: 'Managed',
+ key: 'managed',
+ render: (_: any, app: App) => (
+ handleToggleManaged(app, checked)}
+ loading={refreshing}
+ />
+ ),
+ },
+ {
+ title: 'Actions',
+ key: 'actions',
+ render: (_: any, app: App) => (
+ e.stopPropagation()}>
+
+
+ }
+ onClick={() => openDeployModal(app, appsConfig, environment)}
+ disabled={!app.managed}
+ >
+ Deploy
+
+
+
+ }
+ onClick={(e) => {
+ e.stopPropagation();
+ const auditUrl = `/setting/audit?environmentId=${environment.environmentId}&orgId=${workspaceId}&appId=${app.applicationId}&pageSize=100&pageNum=1`;
+ window.open(auditUrl, '_blank');
+ }}
+ >
+ Audit
+
+
+
+ ),
+ }
+ ];
+
+ // Helper function to generate colors from strings
+ const stringToColor = (str: string) => {
+ let hash = 0;
+ for (let i = 0; i < str.length; i++) {
+ hash = str.charCodeAt(i) + ((hash << 5) - hash);
+ }
+
+ const hue = Math.abs(hash % 360);
+ return `hsl(${hue}, 70%, 50%)`;
+ };
+
+ // Stat card component
+ const StatCard = ({ title, value, icon }: { title: string; value: number; icon: React.ReactNode }) => (
+
+
+
+ );
+
+ return (
+
+ {/* Header */}
+
+
+
+ Apps
+
+
Manage your workspace applications
+
+
}
+ onClick={handleRefresh}
+ loading={loading}
+ type="default"
+ style={{
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
+ borderColor: 'rgba(255, 255, 255, 0.4)',
+ color: 'white',
+ fontWeight: 500
+ }}
+ >
+ Refresh
+
+
+
+ {/* Error display */}
+ {error && (
+
+ )}
+
+ {/* Configuration warnings */}
+ {(!environment.environmentApikey || !environment.environmentApiServiceUrl) && !error && (
+
+ )}
+
+ {/* Stats display */}
+
+
+ }
+ />
+
+
+ }
+ />
+
+
+ }
+ />
+
+
+ }
+ />
+
+
+
+ {/* Content */}
+
+ {loading ? (
+
+
+
+ ) : apps.length === 0 ? (
+
+ ) : (
+ <>
+ {/* 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
+
+ )}
+
+ `${range[0]}-${range[1]} of ${total} apps`
+ }}
+ rowClassName={() => 'app-row'}
+ style={{
+ borderRadius: '8px',
+ overflow: 'hidden'
+ }}
+ />
+ >
+ )}
+
+
+ );
+};
+
+export default AppsTab;
\ 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
new file mode 100644
index 000000000..ba13fd575
--- /dev/null
+++ b/client/packages/lowcoder/src/pages/setting/environments/components/DataSourcesTab.tsx
@@ -0,0 +1,434 @@
+import React, { useState, useEffect } from 'react';
+import { Card, Button, Divider, Alert, message, Table, Tag, Input, Space, Tooltip, Row, Col, Avatar } from 'antd';
+import {
+ SyncOutlined,
+ CloudUploadOutlined,
+ DatabaseOutlined,
+ AuditOutlined,
+ ApiOutlined,
+ CheckCircleFilled,
+ CloudServerOutlined,
+ DisconnectOutlined,
+ FilterOutlined
+} from '@ant-design/icons';
+import Title from 'antd/lib/typography/Title';
+import { Environment } from '../types/environment.types';
+import { DataSource } from '../types/datasource.types';
+import { getMergedWorkspaceDataSources } from '../services/datasources.service';
+import { Switch, Spin, Empty } from 'antd';
+import { ManagedObjectType, setManagedObject, unsetManagedObject } from '../services/managed-objects.service';
+import { useDeployModal } from '../context/DeployModalContext';
+import { dataSourcesConfig } from '../config/data-sources.config';
+import history from "@lowcoder-ee/util/history";
+
+const { Search } = Input;
+
+interface DataSourcesTabProps {
+ environment: Environment;
+ workspaceId: string;
+}
+
+const DataSourcesTab: React.FC = ({ environment, workspaceId }) => {
+ const [dataSources, setDataSources] = useState([]);
+ const [stats, setStats] = useState({
+ total: 0,
+ types: 0,
+ managed: 0,
+ unmanaged: 0
+ });
+ const [loading, setLoading] = useState(false);
+ const [refreshing, setRefreshing] = useState(false);
+ const [error, setError] = useState(null);
+ const [searchText, setSearchText] = useState('');
+ const { openDeployModal } = useDeployModal();
+ const [showManagedOnly, setShowManagedOnly] = useState(false);
+
+ // Fetch data sources
+ const fetchDataSources = async () => {
+ if (!workspaceId || !environment) return;
+
+ setLoading(true);
+ setError(null);
+
+ try {
+ const result = await getMergedWorkspaceDataSources(
+ workspaceId,
+ environment.environmentId,
+ environment.environmentApikey,
+ environment.environmentApiServiceUrl!
+ );
+
+ setDataSources(result.dataSources);
+ setStats(result.stats);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to fetch data sources");
+ } finally {
+ setLoading(false);
+ setRefreshing(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchDataSources();
+ }, [environment, workspaceId]);
+
+ // Handle refresh
+ const handleRefresh = () => {
+ setRefreshing(true);
+ fetchDataSources();
+ };
+
+ // Toggle managed status
+ const handleToggleManaged = async (dataSource: DataSource, checked: boolean) => {
+ setRefreshing(true);
+ try {
+ if (checked) {
+ await setManagedObject(
+ dataSource.gid,
+ environment.environmentId,
+ ManagedObjectType.DATASOURCE,
+ );
+ } else {
+ await unsetManagedObject(
+ dataSource.gid,
+ environment.environmentId,
+ ManagedObjectType.DATASOURCE
+ );
+ }
+
+ // Update the data source in state
+ const updatedDataSources = dataSources.map(item => {
+ if (item.id === dataSource.id) {
+ return { ...item, managed: checked };
+ }
+ return item;
+ });
+
+ setDataSources(updatedDataSources);
+
+ // Update stats
+ const managed = updatedDataSources.filter(ds => ds.managed).length;
+ setStats(prev => ({
+ ...prev,
+ managed,
+ unmanaged: prev.total - managed
+ }));
+
+ message.success(`${dataSource.name} is now ${checked ? 'Managed' : 'Unmanaged'}`);
+ return true;
+ } catch (error) {
+ message.error(`Failed to change managed status for ${dataSource.name}`);
+ return false;
+ } finally {
+ setRefreshing(false);
+ }
+ };
+
+ // Filter data sources based on managed status and search
+ const filteredDataSources = searchText
+ ? dataSources.filter(ds =>
+ ds.name.toLowerCase().includes(searchText.toLowerCase()) ||
+ ds.id.toString().toLowerCase().includes(searchText.toLowerCase()))
+ : dataSources;
+
+ const displayedDataSources = showManagedOnly
+ ? filteredDataSources.filter(ds => ds.managed)
+ : filteredDataSources;
+
+ // Table columns
+ const columns = [
+ {
+ title: 'Data Source',
+ key: 'datasource',
+ render: (dataSource: DataSource) => (
+
+
}
+ />
+
+
{dataSource.name}
+
+ {dataSource.id}
+
+
+
+ ),
+ },
+ {
+ title: 'Type',
+ dataIndex: 'type',
+ key: 'type',
+ render: (type: string) => (
+
+ {type}
+
+ ),
+ },
+ {
+ title: 'Status',
+ key: 'status',
+ render: (dataSource: DataSource) => (
+
+ {dataSource.managed ? : } {dataSource.managed ? 'Managed' : 'Unmanaged'}
+
+ ),
+ },
+ {
+ title: 'Managed',
+ key: 'managed',
+ render: (_: any, dataSource: DataSource) => (
+ handleToggleManaged(dataSource, checked)}
+ loading={refreshing}
+ />
+ ),
+ },
+ {
+ title: 'Actions',
+ key: 'actions',
+ render: (_: any, dataSource: DataSource) => (
+ e.stopPropagation()}>
+
+
+ }
+ onClick={() => openDeployModal(dataSource, dataSourcesConfig, environment)}
+ disabled={!dataSource.managed}
+ >
+ Deploy
+
+
+
+ }
+ onClick={(e) => {
+ e.stopPropagation();
+ const auditUrl = `/setting/audit?environmentId=${environment.environmentId}&orgId=${workspaceId}&datasourceId=${dataSource.id}&pageSize=100&pageNum=1`;
+ window.open(auditUrl, '_blank');
+ }}
+ >
+ Audit
+
+
+
+ ),
+ }
+ ];
+
+ // Helper function to get color based on data source type
+ const getDataSourceColor = (type: string) => {
+ const colorMap: {[key: string]: string} = {
+ 'mysql': '#4479A1',
+ 'postgres': '#336791',
+ 'mongodb': '#4DB33D',
+ 'redis': '#DC382D',
+ 'rest': '#FF6C37',
+ 'graphql': '#E10098',
+ 'elasticsearch': '#005571',
+ 'oracle': '#F80000',
+ 'mssql': '#CC2927',
+ 'snowflake': '#29B5E8'
+ };
+
+ return colorMap[type.toLowerCase()] || '#1890ff';
+ };
+
+ // Stat card component
+ const StatCard = ({ title, value, icon }: { title: string; value: number; icon: React.ReactNode }) => (
+
+
+
+ );
+
+ return (
+
+ {/* Header */}
+
+
+
+ Data Sources
+
+
Manage your workspace data connections
+
+
}
+ onClick={handleRefresh}
+ loading={loading}
+ type="default"
+ style={{
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
+ borderColor: 'rgba(255, 255, 255, 0.4)',
+ color: 'white',
+ fontWeight: 500
+ }}
+ >
+ Refresh
+
+
+
+ {/* Error display */}
+ {error && (
+
+ )}
+
+ {/* Configuration warnings */}
+ {(!environment.environmentApikey || !environment.environmentApiServiceUrl) && !error && (
+
+ )}
+
+ {/* Stats display */}
+
+
+ }
+ />
+
+
+ }
+ />
+
+
+ }
+ />
+
+
+ }
+ />
+
+
+
+ {/* Content */}
+
+ {loading ? (
+
+
+
+ ) : dataSources.length === 0 ? (
+
+ ) : (
+ <>
+ {/* 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
+
+ )}
+
+ `${range[0]}-${range[1]} of ${total} data sources`
+ }}
+ rowClassName={() => 'datasource-row'}
+ style={{
+ borderRadius: '8px',
+ overflow: 'hidden'
+ }}
+ />
+ >
+ )}
+
+
+ );
+};
+
+export default DataSourcesTab;
\ No newline at end of file
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 a58ac7d78..5f5ebc67f 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/components/DeployItemModal.tsx
+++ b/client/packages/lowcoder/src/pages/setting/environments/components/DeployItemModal.tsx
@@ -1,26 +1,28 @@
// components/DeployItemModal.tsx
import React, { useState, useEffect } from 'react';
-import { Modal, Form, Select, Checkbox, Button, message, Spin, Input } from 'antd';
+import { Modal, Form, Select, Checkbox, Button, message, Spin, Input, Tag, Space } from 'antd';
import { Environment } from '../types/environment.types';
-import { DeployableItem, BaseStats, DeployableItemConfig } from '../types/deployable-item.types';
+import { DeployableItemConfig } from '../types/deployable-item.types';
import { useEnvironmentContext } from '../context/EnvironmentContext';
-interface DeployItemModalProps {
+import { getEnvironmentTagColor, formatEnvironmentType } from '../utils/environmentUtils';
+
+interface DeployItemModalProps {
visible: boolean;
- item: T | null;
+ item: any | null;
sourceEnvironment: Environment;
- config: DeployableItemConfig;
+ config: DeployableItemConfig;
onClose: () => void;
onSuccess?: () => void;
}
-function DeployItemModal({
+function DeployItemModal({
visible,
item,
sourceEnvironment,
config,
onClose,
onSuccess
-}: DeployItemModalProps) {
+}: DeployItemModalProps) {
const [form] = Form.useForm();
const { environments, isLoading } = useEnvironmentContext();
const [deploying, setDeploying] = useState(false);
@@ -37,7 +39,7 @@ function DeployItemModal({
);
const handleDeploy = async () => {
- if (!config.deploy?.enabled || !item) return;
+ if (!config.deploy || !item) return;
try {
const values = await form.validateFields();
@@ -61,7 +63,7 @@ function DeployItemModal({
onClose();
} catch (error) {
console.error('Deployment error:', error);
- message.error(`Failed to deploy ${config.singularLabel.toLowerCase()}`);
+ message.error(`Failed to deploy ${config.deploy.singularLabel.toLowerCase()}`);
} finally {
setDeploying(false);
}
@@ -69,7 +71,7 @@ function DeployItemModal({
return (
({
form={form}
layout="vertical"
>
+ {/* Source environment display */}
+
+
+ {sourceEnvironment.environmentName}
+ {sourceEnvironment.environmentType && (
+
+ {formatEnvironmentType(sourceEnvironment.environmentType)}
+
+ )}
+
+
+
({
diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsList.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsList.tsx
deleted file mode 100644
index 63f8dda72..000000000
--- a/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsList.tsx
+++ /dev/null
@@ -1,94 +0,0 @@
-// components/DeployableItemsList.tsx
-import React from 'react';
-import { Table, Tag, Empty, Spin, Switch, Space, Button, Tooltip } from 'antd';
-import { CloudUploadOutlined } from '@ant-design/icons';
-import history from '@lowcoder-ee/util/history';
-import { DeployableItem, BaseStats, DeployableItemConfig } from '../types/deployable-item.types';
-import { Environment } from '../types/environment.types';
-import { useDeployModal } from '../context/DeployModalContext';
-
-interface DeployableItemsListProps {
- items: T[];
- loading: boolean;
- refreshing: boolean;
- error?: string | null;
- environment: Environment;
- config: DeployableItemConfig;
- onToggleManaged?: (item: T, checked: boolean) => Promise;
- additionalParams?: Record;
-}
-
-function DeployableItemsList({
- items,
- loading,
- refreshing,
- error,
- environment,
- config,
- onToggleManaged,
- additionalParams = {}
-}: DeployableItemsListProps) {
-
- const { openDeployModal } = useDeployModal();
-
- // Handle row click for navigation
- const handleRowClick = (item: T) => {
- // Skip navigation if the route is just '#' (for non-navigable items)
- if (config.buildDetailRoute({}) === '#') return;
-
- // Build the route using the config and navigate
- const route = config.buildDetailRoute({
- environmentId: environment.environmentId,
- itemId: item[config.idField] as string,
- ...additionalParams
- });
-
- history.push(route);
- };
-
- // Get columns from config
- const columns = config.getColumns({
- environment,
- refreshing,
- onToggleManaged,
- openDeployModal,
- additionalParams
- })
-
-
- if (loading) {
- return (
-
-
-
- );
- }
-
- if (!items || items.length === 0 || error) {
- return (
-
- );
- }
-
- const hasNavigation = config.buildDetailRoute({}) !== '#';
-
- return (
- ({
- onClick: hasNavigation ? () => handleRowClick(record) : undefined,
- style: hasNavigation ? { cursor: 'pointer' } : undefined,
- })}
- />
- );
-}
-
-export default DeployableItemsList;
\ No newline at end of file
diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsTab.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsTab.tsx
deleted file mode 100644
index 4e50a873c..000000000
--- a/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsTab.tsx
+++ /dev/null
@@ -1,126 +0,0 @@
-// components/DeployableItemsTab.tsx
-import React from 'react';
-import { Card, Button, Divider, Alert, message } from 'antd';
-import { SyncOutlined } from '@ant-design/icons';
-import Title from 'antd/lib/typography/Title';
-import { Environment } from '../types/environment.types';
-import { DeployableItem, BaseStats, DeployableItemConfig } from '../types/deployable-item.types';
-import { useDeployableItems } from '../hooks/useDeployableItems';
-import DeployableItemsList from './DeployableItemsList';
-
-interface DeployableItemsTabProps {
- environment: Environment;
- config: DeployableItemConfig;
- additionalParams?: Record;
- title?: string;
-}
-
-function DeployableItemsTab({
- environment,
- config,
- additionalParams = {},
- title
-}: DeployableItemsTabProps) {
- // Use our generic hook with the provided config
- const {
- items,
- stats,
- loading,
- error,
- refreshing,
- toggleManagedStatus,
- refreshItems
- } = useDeployableItems(config, environment, additionalParams);
-
- // Handle toggling managed status
- const handleToggleManaged = async (item: T, checked: boolean) => {
- const success = await toggleManagedStatus(item, checked);
-
- if (success) {
- message.success(`${item.name} is now ${checked ? 'Managed' : 'Unmanaged'}`);
- } else {
- message.error(`Failed to toggle managed state for ${item.name}`);
- }
-
- return success;
- };
-
- // Handle refresh button click
- const handleRefresh = () => {
- refreshItems();
- message.info(`Refreshing ${config.pluralLabel.toLowerCase()}...`);
- };
-
- // Check for missing required environment properties
- const missingProps = config.requiredEnvProps.filter(
- prop => !environment[prop as keyof Environment]
- );
-
- return (
-
- {/* Header with refresh button */}
-
-
- {title || `${config.pluralLabel} in this Environment`}
-
- }
- onClick={handleRefresh}
- loading={loading}
- >
- Refresh
-
-
-
- {/* Render stats using the config's renderStats function */}
- {config.renderStats(stats)}
-
-
-
- {/* Show error if loading failed */}
- {error && (
-
- )}
-
- {/* Configuration warnings based on required props */}
- {missingProps.length > 0 && !error && (
-
- )}
-
- {/* Items List */}
-
-
- );
-}
-
-export default DeployableItemsTab;
\ No newline at end of file
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 88bdd852f..8f28fd26c 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/components/EditEnvironmentModal.tsx
+++ b/client/packages/lowcoder/src/pages/setting/environments/components/EditEnvironmentModal.tsx
@@ -61,7 +61,7 @@ const EditEnvironmentModal: React.FC = ({
title="Edit Environment"
open={visible}
onCancel={onClose}
- maskClosable={false}
+ maskClosable={true}
destroyOnClose={true}
footer={[
-
-
- ),
- },
- ];
+ // For card display, we'll use a custom layout instead of Table
+ if (environments.length === 0) {
+ return null;
+ }
return (
- ({
- onClick: () => onRowClick(record),
- style: {
- cursor: 'pointer',
- transition: 'background-color 0.3s',
- ':hover': {
- backgroundColor: '#f5f5f5',
- }
- }
- })}
- rowClassName={() => 'environment-row'}
- />
+
+
+ {environments.map(env => (
+
+ onRowClick(env)}
+ >
+
+
+
}
+ />
+
+
+ {env.environmentName || 'Unnamed Environment'}
+ {env.isMaster && (
+
+
+
+ )}
+
+
+ {formatEnvironmentType(env.environmentType)}
+
+
+
+
+
+ }
+ onClick={(e) => openAuditPage(env.environmentId, e)}
+ size="small"
+ style={{ borderRadius: '50%', width: '32px', height: '32px' }}
+ />
+
+
+
+
+
+
+
+ ID:
+
+ {env.environmentId}
+
+
+
+
+
+
+ Master:
+
+ {env.isMaster ? 'Yes' : 'No'}
+
+
+
+
+
+
+ ))}
+
+
+ {environments.length > 10 && (
+
+
+ Showing all {environments.length} environments
+
+
+ )}
+
);
};
diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/ModernBreadcrumbs.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/ModernBreadcrumbs.tsx
new file mode 100644
index 000000000..1a3d35524
--- /dev/null
+++ b/client/packages/lowcoder/src/pages/setting/environments/components/ModernBreadcrumbs.tsx
@@ -0,0 +1,54 @@
+import React, { ReactNode } from 'react';
+import { Breadcrumb } from 'antd';
+import { BreadcrumbProps } from 'antd/lib/breadcrumb';
+
+interface ModernBreadcrumbsProps extends BreadcrumbProps {
+ /**
+ * Items to display in the breadcrumb
+ */
+ items?: {
+ key: string;
+ title: ReactNode;
+ onClick?: () => void;
+ }[];
+}
+
+/**
+ * Modern styled breadcrumb component with consistent styling
+ */
+const ModernBreadcrumbs: React.FC = ({ items = [], ...props }) => {
+ return (
+
+
+ {items.map(item => (
+
+ {item.onClick ? (
+ e.currentTarget.style.textDecoration = 'underline'}
+ onMouseLeave={(e) => e.currentTarget.style.textDecoration = 'none'}
+ >
+ {item.title}
+
+ ) : (
+
+ {item.title}
+
+ )}
+
+ ))}
+
+
+ );
+};
+
+export default ModernBreadcrumbs;
\ No newline at end of file
diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/QueriesTab.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/QueriesTab.tsx
new file mode 100644
index 000000000..a42f604c1
--- /dev/null
+++ b/client/packages/lowcoder/src/pages/setting/environments/components/QueriesTab.tsx
@@ -0,0 +1,427 @@
+import React, { useState, useEffect } from 'react';
+import { Card, Button, Divider, Alert, message, Table, Tag, Input, Space, Tooltip, Row, Col } from 'antd';
+import {
+ SyncOutlined,
+ CloudUploadOutlined,
+ CodeOutlined,
+ AuditOutlined,
+ UserOutlined,
+ CloudServerOutlined,
+ DisconnectOutlined,
+ ApiOutlined,
+ ThunderboltOutlined,
+ FilterOutlined
+} from '@ant-design/icons';
+import Title from 'antd/lib/typography/Title';
+import { Environment } from '../types/environment.types';
+import { Workspace } from '../types/workspace.types';
+import { Query } from '../types/query.types';
+import { getMergedWorkspaceQueries } from '../services/query.service';
+import { Switch, Spin, Empty, Avatar } from 'antd';
+import { ManagedObjectType, setManagedObject, unsetManagedObject } from '../services/managed-objects.service';
+import { useDeployModal } from '../context/DeployModalContext';
+import { queryConfig } from '../config/query.config';
+import history from "@lowcoder-ee/util/history";
+
+const { Search } = Input;
+
+interface QueriesTabProps {
+ environment: Environment;
+ workspaceId: string;
+}
+
+const QueriesTab: React.FC = ({ environment, workspaceId }) => {
+ const [queries, setQueries] = useState([]);
+ const [stats, setStats] = useState({
+ total: 0,
+ managed: 0,
+ unmanaged: 0
+ });
+ const [loading, setLoading] = useState(false);
+ const [refreshing, setRefreshing] = useState(false);
+ const [error, setError] = useState(null);
+ const [searchText, setSearchText] = useState('');
+ const { openDeployModal } = useDeployModal();
+ const [showManagedOnly, setShowManagedOnly] = useState(false);
+
+ // Fetch queries
+ const fetchQueries = async () => {
+ if (!workspaceId || !environment) return;
+
+ setLoading(true);
+ setError(null);
+
+ try {
+ const result = await getMergedWorkspaceQueries(
+ workspaceId,
+ environment.environmentId,
+ environment.environmentApikey,
+ environment.environmentApiServiceUrl!
+ );
+
+ setQueries(result.queries);
+ setStats(result.stats);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to fetch queries");
+ } finally {
+ setLoading(false);
+ setRefreshing(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchQueries();
+ }, [environment, workspaceId]);
+
+ // Handle refresh
+ const handleRefresh = () => {
+ setRefreshing(true);
+ fetchQueries();
+ };
+
+ // Toggle managed status
+ const handleToggleManaged = async (query: Query, checked: boolean) => {
+ setRefreshing(true);
+ try {
+ if (checked) {
+ await setManagedObject(
+ query.gid,
+ environment.environmentId,
+ ManagedObjectType.QUERY,
+ );
+ } else {
+ await unsetManagedObject(
+ query.gid,
+ environment.environmentId,
+ ManagedObjectType.QUERY
+ );
+ }
+
+ // Update the query in state
+ const updatedQueries = queries.map(item => {
+ if (item.id === query.id) {
+ return { ...item, managed: checked };
+ }
+ return item;
+ });
+
+ setQueries(updatedQueries);
+
+ // Update stats
+ const managed = updatedQueries.filter(q => q.managed).length;
+ setStats(prev => ({
+ ...prev,
+ managed,
+ unmanaged: prev.total - managed
+ }));
+
+ message.success(`${query.name} is now ${checked ? 'Managed' : 'Unmanaged'}`);
+ return true;
+ } catch (error) {
+ message.error(`Failed to change managed status for ${query.name}`);
+ return false;
+ } finally {
+ setRefreshing(false);
+ }
+ };
+
+ // Filter queries based on search
+ const filteredQueries = searchText
+ ? queries.filter(query =>
+ query.name.toLowerCase().includes(searchText.toLowerCase()) ||
+ query.id.toLowerCase().includes(searchText.toLowerCase()))
+ : queries;
+
+ const displayedQueries = showManagedOnly
+ ? filteredQueries.filter(query => query.managed)
+ : filteredQueries;
+
+ // Helper function to generate colors from strings
+ const stringToColor = (str: string) => {
+ let hash = 0;
+ for (let i = 0; i < str.length; i++) {
+ hash = str.charCodeAt(i) + ((hash << 5) - hash);
+ }
+
+ const hue = Math.abs(hash % 360);
+ return `hsl(${hue}, 70%, 50%)`;
+ };
+
+ // Table columns
+ const columns = [
+ {
+ title: 'Query',
+ key: 'query',
+ render: (query: Query) => (
+
+
}
+ >
+
+
+
{query.name}
+
+ {query.id}
+
+
+
+ ),
+ },
+ {
+ title: 'Creator',
+ dataIndex: 'creatorName',
+ key: 'creatorName',
+ render: (creatorName: string) => (
+
+
}
+ style={{ backgroundColor: '#1890ff' }}
+ />
+ {creatorName}
+
+ )
+ },
+ {
+ title: 'Status',
+ key: 'status',
+ render: (query: Query) => (
+
+ {query.managed ? : } {query.managed ? 'Managed' : 'Unmanaged'}
+
+ ),
+ },
+ {
+ title: 'Managed',
+ key: 'managed',
+ render: (_: any, query: Query) => (
+ handleToggleManaged(query, checked)}
+ loading={refreshing}
+ />
+ ),
+ },
+ {
+ title: 'Actions',
+ key: 'actions',
+ render: (_: any, query: Query) => (
+ e.stopPropagation()}>
+
+
+ }
+ onClick={() => openDeployModal(query, queryConfig, environment)}
+ disabled={!query.managed}
+ >
+ Deploy
+
+
+
+ }
+ onClick={(e) => {
+ e.stopPropagation();
+ const auditUrl = `/setting/audit?environmentId=${environment.environmentId}&orgId=${workspaceId}&queryId=${query.id}&pageSize=100&pageNum=1`;
+ window.open(auditUrl, '_blank');
+ }}
+ >
+ Audit
+
+
+
+ ),
+ }
+ ];
+
+ // Stat card component
+ const StatCard = ({ title, value, icon }: { title: string; value: number; icon: React.ReactNode }) => (
+
+
+
+ );
+
+ return (
+
+ {/* Header */}
+
+
+
+ Queries
+
+
Manage your workspace API queries
+
+
}
+ onClick={handleRefresh}
+ loading={loading}
+ type="default"
+ style={{
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
+ borderColor: 'rgba(255, 255, 255, 0.4)',
+ color: 'white',
+ fontWeight: 500
+ }}
+ >
+ Refresh
+
+
+
+ {/* Error display */}
+ {error && (
+
+ )}
+
+ {/* Configuration warnings */}
+ {(!environment.environmentApikey || !environment.environmentApiServiceUrl) && !error && (
+
+ )}
+
+ {/* Stats display */}
+
+
+ }
+ />
+
+
+ }
+ />
+
+
+ }
+ />
+
+
+
+ {/* Content */}
+
+ {loading ? (
+
+
+
+ ) : queries.length === 0 ? (
+
+ ) : (
+ <>
+ {/* Search and Filter Bar */}
+
+ setSearchText(value)}
+ onChange={e => setSearchText(e.target.value)}
+ style={{ width: 300 }}
+ size="large"
+ />
+ setShowManagedOnly(!showManagedOnly)}
+ type="default"
+ icon={}
+ style={{
+ marginLeft: '8px',
+ backgroundColor: showManagedOnly ? '#1890ff' : 'white',
+ color: showManagedOnly ? 'white' : '#1890ff',
+ borderColor: '#1890ff'
+ }}
+ />
+
+
+ {searchText && displayedQueries.length !== queries.length && (
+
+ Showing {displayedQueries.length} of {queries.length} queries
+
+ )}
+
+ `${range[0]}-${range[1]} of ${total} queries`
+ }}
+ rowClassName={() => 'query-row'}
+ style={{
+ borderRadius: '8px',
+ overflow: 'hidden'
+ }}
+ />
+ >
+ )}
+
+
+ );
+};
+
+export default QueriesTab;
\ 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
new file mode 100644
index 000000000..ff079a9df
--- /dev/null
+++ b/client/packages/lowcoder/src/pages/setting/environments/components/UserGroupsTab.tsx
@@ -0,0 +1,359 @@
+import React, { useState, useEffect } from 'react';
+import { Card, Button, Alert, message, 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';
+import { UserGroup, UserGroupsTabStats } from '../types/userGroup.types';
+import { getEnvironmentUserGroups } from '../services/environments.service';
+import { Spin, Empty } from 'antd';
+
+const { Search } = Input;
+
+interface UserGroupsTabProps {
+ environment: Environment;
+}
+
+const UserGroupsTab: React.FC = ({ environment }) => {
+ const [userGroups, setUserGroups] = useState([]);
+ const [stats, setStats] = useState({
+ total: 0,
+ allUsers: 0,
+ developers: 0,
+ custom: 0
+ });
+ const [loading, setLoading] = useState(false);
+ const [refreshing, setRefreshing] = useState(false);
+ const [error, setError] = useState(null);
+ const [searchText, setSearchText] = useState('');
+
+ // Fetch user groups
+ const fetchUserGroups = async () => {
+ if (!environment) return;
+
+ setLoading(true);
+ setError(null);
+
+ try {
+ // Check for required environment properties
+ if (!environment.environmentApikey || !environment.environmentApiServiceUrl) {
+ setError('Missing required configuration: API key or API service URL');
+ setLoading(false);
+ return;
+ }
+
+ const response = await getEnvironmentUserGroups(
+ environment.environmentId,
+ environment.environmentApikey,
+ environment.environmentApiServiceUrl
+ );
+
+ // Extract the groups from the data array in the response
+ const groups = response|| [];
+
+ setUserGroups(groups);
+
+ // Calculate stats
+ const total = groups.length;
+ const allUsers = groups.filter((group: UserGroup) => group.allUsersGroup).length;
+ const developers = groups.filter((group: UserGroup) => group.devGroup).length;
+ const custom = total - (allUsers + developers);
+
+ setStats({
+ total,
+ allUsers,
+ developers,
+ custom
+ });
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to fetch user groups");
+ } finally {
+ setLoading(false);
+ setRefreshing(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchUserGroups();
+ }, [environment]);
+
+ // Handle refresh
+ const handleRefresh = () => {
+ setRefreshing(true);
+ fetchUserGroups();
+ };
+
+ // Filter user groups based on search
+ const filteredUserGroups = searchText
+ ? userGroups.filter(group =>
+ group.groupName.toLowerCase().includes(searchText.toLowerCase()) ||
+ group.groupId.toLowerCase().includes(searchText.toLowerCase()))
+ : userGroups;
+
+ // Helper function to generate colors from strings
+ const stringToColor = (str: string) => {
+ let hash = 0;
+ for (let i = 0; i < str.length; i++) {
+ hash = str.charCodeAt(i) + ((hash << 5) - hash);
+ }
+
+ const hue = Math.abs(hash % 360);
+ return `hsl(${hue}, 70%, 50%)`;
+ };
+
+ // Stat card component
+ const StatCard = ({ title, value, icon }: { title: string; value: number; icon: React.ReactNode }) => (
+
+
+
+ );
+
+ // Table columns
+ const columns = [
+ {
+ title: 'User Group',
+ key: 'group',
+ render: (group: UserGroup) => (
+
+
+ {group.groupName.charAt(0).toUpperCase()}
+
+
+
{group.groupName}
+
+ {group.groupId}
+
+
+
+ ),
+ },
+ {
+ title: 'Type',
+ key: 'type',
+ render: (_: any, group: UserGroup) => {
+ if (group.allUsersGroup) return (
+
+ All Users
+
+ );
+ if (group.devGroup) return (
+
+ Developers
+
+ );
+ return (
+
+ Custom
+
+ );
+ },
+ },
+ {
+ title: 'Members',
+ key: 'members',
+ render: (_: any, group: UserGroup) => (
+
+
+ {group.stats?.userCount || 0}
+
+
+ ),
+ },
+ {
+ title: 'Admin Members',
+ key: 'adminMembers',
+ render: (_: any, group: UserGroup) => (
+
+
+ {group.stats?.adminUserCount || 0}
+
+
+ ),
+ },
+ {
+ title: 'Created',
+ dataIndex: 'createTime',
+ key: 'createTime',
+ render: (createTime: number) => (
+
+ {new Date(createTime).toLocaleDateString()}
+
+ ),
+ }
+ ];
+
+ return (
+
+ {/* Header */}
+
+
+
+ User Groups
+
+
Manage user groups in this environment
+
+
}
+ onClick={handleRefresh}
+ loading={loading}
+ type="default"
+ style={{
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
+ borderColor: 'rgba(255, 255, 255, 0.4)',
+ color: 'white',
+ fontWeight: 500
+ }}
+ >
+ Refresh
+
+
+
+ {/* Error display */}
+ {error && (
+
+ )}
+
+ {/* Configuration warnings */}
+ {(!environment.environmentApikey || !environment.environmentApiServiceUrl) && !error && (
+
+ )}
+
+ {/* Stats display */}
+
+
+ }
+ />
+
+
+ }
+ />
+
+
+ }
+ />
+
+
+ }
+ />
+
+
+
+ {/* Content */}
+
+ {loading ? (
+
+
+
+ ) : userGroups.length === 0 ? (
+
+ ) : (
+ <>
+ {/* 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
+
+ )}
+
+
+ `${range[0]}-${range[1]} of ${total} user groups`
+ }}
+ style={{
+ borderRadius: '8px',
+ overflow: 'hidden'
+ }}
+ rowClassName={() => 'group-row'}
+ />
+ >
+ )}
+
+
+ );
+};
+
+export default UserGroupsTab;
\ No newline at end of file
diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/WorkspaceHeader.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/WorkspaceHeader.tsx
new file mode 100644
index 000000000..9cc2bc61f
--- /dev/null
+++ b/client/packages/lowcoder/src/pages/setting/environments/components/WorkspaceHeader.tsx
@@ -0,0 +1,261 @@
+import React, { useState } from "react";
+import {
+ Typography,
+ Switch,
+ Button,
+ Tag,
+ Tooltip,
+ Row,
+ Col,
+ Statistic,
+ Avatar,
+ Space,
+ Divider,
+ Card,
+ Dropdown,
+ Menu
+} from "antd";
+import {
+ CloudUploadOutlined,
+ SettingOutlined,
+ TeamOutlined,
+ AppstoreOutlined,
+ DatabaseOutlined,
+ CodeOutlined,
+ CloudServerOutlined,
+ ClockCircleOutlined,
+ MoreOutlined,
+ StarOutlined,
+ StarFilled
+} from "@ant-design/icons";
+import { Environment } from "../types/environment.types";
+import { Workspace } from "../types/workspace.types";
+import styled from "styled-components";
+
+const { Title, Text } = Typography;
+
+// Styled components for custom design
+const HeaderWrapper = styled.div`
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+ position: relative;
+ margin-bottom: 24px;
+`;
+
+const GradientBanner = styled.div<{ avatarColor: string }>`
+ background: linear-gradient(135deg, ${props => props.avatarColor} 0%, #feb47b 100%);
+ height: 140px;
+ position: relative;
+ overflow: hidden;
+ transition: background 1s ease-in-out;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: -50%;
+ left: -50%;
+ width: 200%;
+ height: 200%;
+ background: repeating-linear-gradient(
+ 45deg,
+ rgba(255,255,255,0.1),
+ rgba(255,255,255,0.1) 1px,
+ transparent 1px,
+ transparent 10px
+ );
+ animation: moveBackground 30s linear infinite;
+ }
+
+ @keyframes moveBackground {
+ 0% {
+ transform: translate(0, 0);
+ }
+ 100% {
+ transform: translate(100px, 100px);
+ }
+ }
+
+ &:hover {
+ background: linear-gradient(135deg, #feb47b 0%, ${props => props.avatarColor} 100%);
+ transition: background 1s ease-in-out;
+ }
+`;
+
+const ContentContainer = styled.div`
+ background-color: white;
+ padding: 24px;
+ position: relative;
+ transition: transform 0.3s ease-in-out;
+
+ &:hover {
+ transform: translateY(-5px);
+ }
+`;
+
+const AvatarContainer = styled.div`
+ position: absolute;
+ top: -50px;
+ left: 24px;
+ background: white;
+ padding: 4px;
+ border-radius: 16px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+`;
+
+const StatusBadge = styled(Tag)<{ $active?: boolean }>`
+ position: absolute;
+ top: 12px;
+ right: 12px;
+ font-weight: 600;
+ font-size: 12px;
+ padding: 4px 12px;
+ border-radius: 20px;
+ 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'};
+`;
+
+const StatCard = styled(Card)`
+ border-radius: 8px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+ transition: all 0.3s;
+
+ &:hover {
+ transform: translateY(-3px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
+ }
+`;
+
+const ActionButton = styled(Button)`
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 38px;
+`;
+
+const FavoriteButton = styled(Button)`
+ position: absolute;
+ top: 12px;
+ right: 80px;
+ border: none;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+`;
+
+interface WorkspaceHeaderProps {
+ workspace: Workspace;
+ environment: Environment;
+ isToggling: boolean;
+ onToggleManagedStatus: (checked: boolean) => Promise;
+ onDeploy: () => void;
+}
+
+const WorkspaceHeader: React.FC = ({
+ workspace,
+ environment,
+ isToggling,
+ onToggleManagedStatus,
+ onDeploy
+}) => {
+
+ // Generate a consistent color for the workspace avatar
+ const getAvatarColor = (name: string) => {
+ 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%)`;
+ };
+
+ // Format date for last updated
+ const formatDate = (date: number | undefined) => {
+ if (!date) return "N/A";
+ return new Date(date).toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ year: "numeric"
+ });
+ };
+
+
+
+
+
+ return (
+
+
+
+ {workspace.managed ? "Managed" : "Unmanaged"}
+
+
+
+
+
+
+
+ {workspace.name.charAt(0).toUpperCase()}
+
+
+
+
+
+
+ {workspace.name}
+
+
+ ID: {workspace.id}
+
+
+ created on {formatDate(workspace.creationDate)}
+
+
+ {environment.environmentName}
+
+
+
+
+
+
+
+ Managed:
+
+
+
+ }
+ onClick={onDeploy}
+ disabled={!workspace.managed}
+ >
+ Deploy
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default WorkspaceHeader;
\ No newline at end of file
diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/WorkspacesTab.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/WorkspacesTab.tsx
new file mode 100644
index 000000000..eaf8b2a17
--- /dev/null
+++ b/client/packages/lowcoder/src/pages/setting/environments/components/WorkspacesTab.tsx
@@ -0,0 +1,363 @@
+import React, { useState, useEffect } from 'react';
+import { Card, Button, Divider, Alert, message, 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';
+import { Workspace } from '../types/workspace.types';
+import { getMergedEnvironmentWorkspaces } from '../services/workspace.service';
+import { Spin, Empty } from 'antd';
+
+import history from '@lowcoder-ee/util/history';
+
+const { Search } = Input;
+
+interface WorkspacesTabProps {
+ environment: Environment;
+}
+
+const WorkspacesTab: React.FC = ({ environment }) => {
+ const [workspaces, setWorkspaces] = useState([]);
+ const [stats, setStats] = useState({
+ total: 0,
+ managed: 0,
+ unmanaged: 0
+ });
+ const [loading, setLoading] = useState(false);
+ const [refreshing, setRefreshing] = useState(false);
+ const [error, setError] = useState(null);
+ const [searchText, setSearchText] = useState('');
+ const [showManagedOnly, setShowManagedOnly] = useState(false);
+
+ // Fetch workspaces
+ const fetchWorkspaces = async () => {
+ if (!environment) return;
+
+ setLoading(true);
+ setError(null);
+
+ try {
+ // Check for required environment properties
+ if (!environment.environmentApikey || !environment.environmentApiServiceUrl) {
+ setError('Missing required configuration: API key or API service URL');
+ setLoading(false);
+ return;
+ }
+
+ const result = await getMergedEnvironmentWorkspaces(
+ environment.environmentId,
+ environment.environmentApikey,
+ environment.environmentApiServiceUrl
+ );
+
+ setWorkspaces(result.workspaces);
+ setStats(result.stats);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to fetch workspaces");
+ } finally {
+ setLoading(false);
+ setRefreshing(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchWorkspaces();
+ }, [environment]);
+
+ // Handle refresh
+ const handleRefresh = () => {
+ setRefreshing(true);
+ fetchWorkspaces();
+ };
+
+ // Handle row click for navigation
+ const handleRowClick = (workspace: Workspace) => {
+ history.push(`/setting/environments/${environment.environmentId}/workspaces/${workspace.id}`);
+ };
+
+ // Filter workspaces based on search and managed status
+ const filteredWorkspaces = searchText
+ ? workspaces.filter(workspace =>
+ workspace.name.toLowerCase().includes(searchText.toLowerCase()) ||
+ workspace.id.toLowerCase().includes(searchText.toLowerCase()))
+ : workspaces;
+
+ const displayedWorkspaces = showManagedOnly
+ ? filteredWorkspaces.filter(workspace => workspace.managed)
+ : filteredWorkspaces;
+
+ // Helper function to generate colors from strings
+ const stringToColor = (str: string) => {
+ let hash = 0;
+ for (let i = 0; i < str.length; i++) {
+ hash = str.charCodeAt(i) + ((hash << 5) - hash);
+ }
+
+ const hue = Math.abs(hash % 360);
+ return `hsl(${hue}, 70%, 50%)`;
+ };
+
+ // Stat card component
+ const StatCard = ({ title, value, icon }: { title: string; value: number; icon: React.ReactNode }) => (
+
+
+
+ );
+
+ // Table columns
+ const columns = [
+ {
+ title: 'Workspace',
+ key: 'workspace',
+ render: (workspace: Workspace) => (
+
+
+ {workspace.name.charAt(0).toUpperCase()}
+
+
+
{workspace.name}
+
+ {workspace.id}
+
+
+
+ ),
+ },
+ {
+ title: 'Role',
+ dataIndex: 'role',
+ key: 'role',
+ },
+ {
+ title: 'Status',
+ dataIndex: 'status',
+ key: 'status',
+ render: (status: string) => (
+
+ {status === 'ACTIVE' ? : null}
+ {status}
+
+ ),
+ },
+ {
+ title: 'Managed',
+ key: 'managed',
+ render: (_: any, workspace: Workspace) => (
+
+ {workspace.managed
+ ?
+ :
+ }
+ {workspace.managed ? 'Managed' : 'Unmanaged'}
+
+ ),
+ },
+ {
+ title: 'Actions',
+ key: 'actions',
+ render: (_: any, workspace: Workspace) => (
+ e.stopPropagation()}>
+
+ }
+ onClick={(e) => {
+ e.stopPropagation();
+ const auditUrl = `/setting/audit?environmentId=${environment.environmentId}&orgId=${workspace.id}&pageSize=100&pageNum=1`;
+ window.open(auditUrl, '_blank');
+ }}
+ >
+ Audit
+
+
+
+ ),
+ },
+ ];
+
+ return (
+
+ {/* Header */}
+
+
+
+ Workspaces
+
+
Manage workspaces in this environment
+
+
}
+ onClick={handleRefresh}
+ loading={loading}
+ type="default"
+ style={{
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
+ borderColor: 'rgba(255, 255, 255, 0.4)',
+ color: 'white',
+ fontWeight: 500
+ }}
+ >
+ Refresh
+
+
+
+ {/* Error display */}
+ {error && (
+
+ )}
+
+ {/* Configuration warnings */}
+ {(!environment.environmentApikey || !environment.environmentApiServiceUrl) && !error && (
+
+ )}
+
+ {/* Stats display */}
+
+
+ }
+ />
+
+
+ }
+ />
+
+
+ }
+ />
+
+
+
+ {/* Content */}
+
+ {loading ? (
+
+
+
+ ) : workspaces.length === 0 ? (
+
+ ) : (
+ <>
+ {/* Search and Filter Bar */}
+
+ setSearchText(value)}
+ onChange={e => setSearchText(e.target.value)}
+ style={{ width: 300 }}
+ size="large"
+ />
+ setShowManagedOnly(!showManagedOnly)}
+ type="default"
+ icon={}
+ style={{
+ marginLeft: '8px',
+ backgroundColor: showManagedOnly ? '#52c41a' : 'white',
+ color: showManagedOnly ? 'white' : '#52c41a',
+ borderColor: '#52c41a'
+ }}
+ />
+
+
+ {searchText && displayedWorkspaces.length !== workspaces.length && (
+
+ Showing {displayedWorkspaces.length} of {workspaces.length} workspaces
+
+ )}
+
+ `${range[0]}-${range[1]} of ${total} workspaces`
+ }}
+ style={{
+ borderRadius: '8px',
+ overflow: 'hidden'
+ }}
+ onRow={(record) => ({
+ onClick: () => handleRowClick(record),
+ style: { cursor: 'pointer' }
+ })}
+ rowClassName={() => 'workspace-row'}
+ />
+ >
+ )}
+
+
+ );
+};
+
+export default WorkspacesTab;
\ No newline at end of file
diff --git a/client/packages/lowcoder/src/pages/setting/environments/config/apps.config.tsx b/client/packages/lowcoder/src/pages/setting/environments/config/apps.config.tsx
index 0ba63e593..1a09691c2 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/config/apps.config.tsx
+++ b/client/packages/lowcoder/src/pages/setting/environments/config/apps.config.tsx
@@ -1,182 +1,20 @@
// config/apps.config.tsx
-import React from 'react';
-import { Row, Col, Statistic, Tag, Space, Button, Tooltip } from 'antd';
-import { AppstoreOutlined, AuditOutlined } from '@ant-design/icons';
-import {DeployableItemConfig } from '../types/deployable-item.types';
+import { DeployableItemConfig } from '../types/deployable-item.types';
import { Environment } from '../types/environment.types';
-import { getMergedWorkspaceApps, deployApp } from '../services/apps.service';
-import { connectManagedApp, unconnectManagedApp } from '../services/enterprise.service';
-import { App, AppStats } from '../types/app.types';
+import { deployApp } from '../services/apps.service';
+import { App } from '../types/app.types';
-import {
- createNameColumn,
- createDescriptionColumn,
- createPublishedColumn,
- createManagedColumn,
- createDeployColumn,
- createAuditColumn,
- createIdColumn,
- createApplicationGidColumn
-} from '../utils/columnFactories';
-
-// Define AppStats interface if not already defined
-export const appsConfig: DeployableItemConfig = {
- // Basic info
- type: 'apps',
- singularLabel: 'App',
- pluralLabel: 'Apps',
- icon: ,
- idField: 'id', // or applicationId if you prefer to use that directly
-
- // Navigation
- buildDetailRoute: () => '#',
+// Define AppStats interface if not already defined
-
- // Configuration
- requiredEnvProps: ['environmentApikey', 'environmentApiServiceUrl'],
-
- // Stats rendering
- renderStats: (stats) => (
-
-
- } />
-
-
- } />
-
-
- } />
-
-
- } />
-
-
- ),
-
- // Stats calculation
- calculateStats: (apps) => {
- const total = apps.length;
- const published = apps.filter(app => app.published).length;
- const managed = apps.filter(app => app.managed).length;
-
- return {
- total,
- published,
- managed,
- unmanaged: total - managed
- };
- },
-
- // Table configuration
- getColumns: ({ environment, refreshing, onToggleManaged, openDeployModal, additionalParams }) => {
- const columns = [
- createIdColumn(),
- createApplicationGidColumn(),
- createNameColumn(),
- createPublishedColumn(),
- ];
-
- // Add managed column if enabled
- if (appsConfig.enableManaged && onToggleManaged) {
- columns.push(createManagedColumn(onToggleManaged, refreshing));
- }
-
- // Add deploy column if enabled
- if (appsConfig.deploy?.enabled && openDeployModal) {
- columns.push(createDeployColumn(appsConfig, environment, openDeployModal));
- }
-
- // Add audit column if enabled
- if (appsConfig.audit?.enabled) {
- columns.push(createAuditColumn(appsConfig, environment, additionalParams));
- }
-
- return columns;
- },
- columns: [
- {
- title: 'Name',
- dataIndex: 'name',
- key: 'name',
- },
- {
- title: 'ID',
- dataIndex: 'id',
- key: 'id',
- ellipsis: true,
- },
- {
- title: 'Role',
- dataIndex: 'role',
- key: 'role',
- render: (role: string) => {role},
- },
-
- {
- title: 'Status',
- dataIndex: 'status',
- key: 'status',
- render: (status: string) => (
-
- {status}
-
- ),
- }
- ],
-
- // Deployment options
- enableManaged: true,
-
- // Service functions
- fetchItems: async ({ environment, workspaceId }) => {
- if (!workspaceId) {
- throw new Error("Workspace ID is required to fetch apps");
- }
-
- const result = await getMergedWorkspaceApps(
- workspaceId,
- environment.environmentId,
- environment.environmentApikey,
- environment.environmentApiServiceUrl!
- );
-
- // Map to ensure proper id field
- return result.apps.map(app => ({
- ...app,
- id: app.applicationId // Map applicationId to id for DeployableItem compatibility
- }));
- },
- audit: {
- enabled: true,
- icon: ,
- label: 'Audit',
- tooltip: 'View audit logs for this app',
- getAuditUrl: (item, environment, additionalParams) => {
- console.log("Additional params:", additionalParams);
- return `/setting/audit?environmentId=${environment.environmentId}&orgId=${item.id}&appId=${additionalParams?.workspaceId}&pageSize=100&pageNum=1`
- }
- },
- toggleManaged: async ({ item, checked, environment }) => {
- try {
- if (checked) {
- await connectManagedApp(environment.environmentId, item.name, item.applicationGid!);
- } else {
- await unconnectManagedApp(item.applicationGid!);
- }
- return true;
- } catch (error) {
- console.error('Error toggling managed status:', error);
- return false;
- }
- },
- // deployment options
+export const appsConfig: DeployableItemConfig = {
+
deploy: {
- enabled: true,
+ singularLabel: 'App',
fields: [
{
name: 'updateDependenciesIfNeeded',
diff --git a/client/packages/lowcoder/src/pages/setting/environments/config/data-sources.config.tsx b/client/packages/lowcoder/src/pages/setting/environments/config/data-sources.config.tsx
index d31b0fa3d..11e9e54fc 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/config/data-sources.config.tsx
+++ b/client/packages/lowcoder/src/pages/setting/environments/config/data-sources.config.tsx
@@ -1,169 +1,15 @@
// config/data-sources.config.tsx
import React from 'react';
-import { Row, Col, Statistic, Tag, Space, Button, Tooltip } from 'antd';
-import { DatabaseOutlined, CloudUploadOutlined } from '@ant-design/icons';
import { DeployableItemConfig } from '../types/deployable-item.types';
-import { DataSource, DataSourceStats } from '../types/datasource.types';
+import { DataSource} from '../types/datasource.types';
import { Environment } from '../types/environment.types';
-import { getMergedWorkspaceDataSources, deployDataSource } from '../services/datasources.service';
-import { connectManagedDataSource, unconnectManagedDataSource } from '../services/enterprise.service';
-import {
- createNameColumn,
- createTypeColumn,
- createDatabaseColumn,
- createDatasourceStatusColumn,
- createManagedColumn,
- createDeployColumn,
- createAuditColumn,
- createGidColumn
-} from '../utils/columnFactories';
+import { deployDataSource, DataSourceStats } from '../services/datasources.service';
-export const dataSourcesConfig: DeployableItemConfig = {
- // Basic info
- type: 'dataSources',
- singularLabel: 'Data Source',
- pluralLabel: 'Data Sources',
- icon: ,
- idField: 'id',
-
- // Navigation
- buildDetailRoute: (params) => "#",
-
- // Configuration
- requiredEnvProps: ['environmentApikey', 'environmentApiServiceUrl'],
-
- // Stats rendering
- renderStats: (stats) => (
-
-
- } />
-
-
- } />
-
-
- } />
-
-
- ),
-
- // Stats calculation
- calculateStats: (dataSources) => {
- const total = dataSources.length;
- const managed = dataSources.filter(ds => ds.managed).length;
-
- // Calculate counts by type
- const byType = dataSources.reduce((acc, ds) => {
- const type = ds.type || 'Unknown';
- acc[type] = (acc[type] || 0) + 1;
- return acc;
- }, {} as Record);
-
- return {
- total,
- managed,
- unmanaged: total - managed,
- byType
- };
- },
-
- // Table configuration - Customize based on your existing UI
- columns: [
- {
- title: 'Name',
- dataIndex: 'name',
- key: 'name',
- },
- {
- title: 'Type',
- dataIndex: 'type',
- key: 'type',
- render: (type: string) => (
- {type || 'Unknown'}
- ),
- },
- {
- title: 'Database',
- key: 'database',
- render: (_, record: DataSource) => (
- {record.datasourceConfig?.database || 'N/A'}
- ),
- },
- {
- title: 'Status',
- dataIndex: 'datasourceStatus',
- key: 'status',
- render: (status: string) => (
-
- {status}
-
- ),
- },
- ],
-
- // Deployment options
- enableManaged: true,
-
- // Service functions
- fetchItems: async ({ environment, workspaceId }) => {
- if (!workspaceId) {
- throw new Error("Workspace ID is required to fetch data sources");
- }
-
- const result = await getMergedWorkspaceDataSources(
- workspaceId,
- environment.environmentId,
- environment.environmentApikey,
- environment.environmentApiServiceUrl!
- );
-
- return result.dataSources;
- },
- getColumns: ({ environment, refreshing, onToggleManaged, openDeployModal, additionalParams }) => {
- const columns = [
- createNameColumn(),
- createGidColumn(),
- createTypeColumn(),
- createDatabaseColumn(),
- createDatasourceStatusColumn(),
- ];
-
- // Add managed column if enabled
- if (dataSourcesConfig.enableManaged && onToggleManaged) {
- columns.push(createManagedColumn(onToggleManaged, refreshing));
- }
-
- // Add deploy column if enabled
- if (dataSourcesConfig.deploy?.enabled && openDeployModal) {
- columns.push(createDeployColumn(dataSourcesConfig, environment, openDeployModal));
- }
-
- // Add audit column if enabled
- if (dataSourcesConfig.audit?.enabled) {
- columns.push(createAuditColumn(dataSourcesConfig, environment, additionalParams));
- }
-
- return columns;
- },
-
-
- toggleManaged: async ({ item, checked, environment }) => {
- try {
- if (checked) {
- await connectManagedDataSource(environment.environmentId, item.name, item.gid);
- } else {
- await unconnectManagedDataSource(item.gid);
- }
- return true;
- } catch (error) {
- console.error('Error toggling managed status:', error);
- return false;
- }
- },
+export const dataSourcesConfig: DeployableItemConfig = {
deploy: {
- enabled: true,
+ singularLabel: 'Data Source',
fields: [
{
name: 'updateDependenciesIfNeeded',
diff --git a/client/packages/lowcoder/src/pages/setting/environments/config/query.config.tsx b/client/packages/lowcoder/src/pages/setting/environments/config/query.config.tsx
index 3940a5e58..5396bd877 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/config/query.config.tsx
+++ b/client/packages/lowcoder/src/pages/setting/environments/config/query.config.tsx
@@ -1,164 +1,16 @@
// config/query.config.tsx
-import React from 'react';
-import { Row, Col, Statistic, Tag } from 'antd';
-import { ApiOutlined } from '@ant-design/icons';
import { DeployableItemConfig } from '../types/deployable-item.types';
import { Query } from '../types/query.types';
-import { connectManagedQuery, unconnectManagedQuery } from '../services/enterprise.service';
-import { getMergedWorkspaceQueries, deployQuery } from '../services/query.service';
+import { deployQuery } from '../services/query.service';
import { Environment } from '../types/environment.types';
-import {
- createNameColumn,
- createCreatorColumn,
- createDateColumn,
- createQueryTypeColumn,
- createManagedColumn,
- createDeployColumn,
- createAuditColumn,
- createGidColumn
-} from '../utils/columnFactories';
-// Define QueryStats interface
-export interface QueryStats {
- total: number;
- managed: number;
- unmanaged: number;
-}
-export const queryConfig: DeployableItemConfig = {
- // Basic info
- type: 'queries',
- singularLabel: 'Query',
- pluralLabel: 'Queries',
- icon: ,
- idField: 'id',
-
- // Navigation - queries don't have detail pages in this implementation
- buildDetailRoute: () => '#',
-
- // Configuration
- requiredEnvProps: ['environmentApikey', 'environmentApiServiceUrl'],
-
- // Stats rendering
- renderStats: (stats) => (
-
-
- } />
-
-
- } />
-
-
- } />
-
-
- ),
-
- // Stats calculation
- calculateStats: (queries) => {
- const total = queries.length;
- const managed = queries.filter(q => q.managed).length;
-
- return {
- total,
- managed,
- unmanaged: total - managed
- };
- },
- columns: [
- {
- title: 'Name',
- dataIndex: 'name',
- key: 'name',
- },
- {
- title: 'Type',
- dataIndex: 'type',
- key: 'type',
- render: (type: string) => (
- {type || 'Unknown'}
- ),
- },
- {
- title: 'Database',
- key: 'database',
- render: (_, record: Query) => (
- {record.datasourceConfig?.database || 'N/A'}
- ),
- },
- {
- title: 'Status',
- dataIndex: 'datasourceStatus',
- key: 'status',
- render: (status: string) => (
-
- {status}
-
- ),
- },
- ],
- getColumns: ({ environment, refreshing, onToggleManaged, openDeployModal, additionalParams }) => {
- const columns = [
- createNameColumn(),
- createGidColumn(),
- createCreatorColumn(),
- createDateColumn('createTime', 'Creation Date'),
- createQueryTypeColumn(),
- ];
-
- // Add managed column if enabled
- if (queryConfig.enableManaged && onToggleManaged) {
- columns.push(createManagedColumn(onToggleManaged, refreshing));
- }
-
- // Add deploy column if enabled
- if (queryConfig.deploy?.enabled && openDeployModal) {
- columns.push(createDeployColumn(queryConfig, environment, openDeployModal));
- }
-
- // Add audit column if enabled
- if (queryConfig.audit?.enabled) {
- columns.push(createAuditColumn(queryConfig, environment, additionalParams));
- }
-
- return columns;
- },
-
- // Deployment options
- enableManaged: true,
-
- // Service functions
- fetchItems: async ({ environment, workspaceId }) => {
- if (!workspaceId) {
- throw new Error("Workspace ID is required to fetch queries");
- }
-
- const result = await getMergedWorkspaceQueries(
- workspaceId,
- environment.environmentId,
- environment.environmentApikey,
- environment.environmentApiServiceUrl!
- );
-
- return result.queries;
- },
+
+export const queryConfig: DeployableItemConfig = {
- toggleManaged: async ({ item, checked, environment }) => {
- try {
- if (checked) {
- await connectManagedQuery(environment.environmentId, item.name, item.gid);
- } else {
- await unconnectManagedQuery(item.gid);
- }
- return true;
- } catch (error) {
- console.error('Error toggling managed status:', error);
- return false;
- }
- },
deploy: {
- enabled: true,
+ singularLabel: 'Query',
fields: [
{
name: 'updateDependenciesIfNeeded',
diff --git a/client/packages/lowcoder/src/pages/setting/environments/config/usergroups.config.tsx b/client/packages/lowcoder/src/pages/setting/environments/config/usergroups.config.tsx
deleted file mode 100644
index 8ae041320..000000000
--- a/client/packages/lowcoder/src/pages/setting/environments/config/usergroups.config.tsx
+++ /dev/null
@@ -1,169 +0,0 @@
-// config/usergroups.config.tsx
-import React from 'react';
-import { Row, Col, Statistic, Tag, Badge } from 'antd';
-import { TeamOutlined, UserOutlined } from '@ant-design/icons';
-import { getEnvironmentUserGroups } from '../services/environments.service';
-import { UserGroup, UserGroupStats } from '../types/userGroup.types';
-import { DeployableItemConfig } from '../types/deployable-item.types';
-import {
- createUserGroupNameColumn,
- createGroupIdColumn,
- createUserCountColumn,
- createDateColumn,
- createGroupTypeColumn,
- createAuditColumn
-} from '../utils/columnFactories';
-
-const formatDate = (timestamp: number): string => {
- if (!timestamp) return 'N/A';
- const date = new Date(timestamp);
- return `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`;
-};
-
-
-export const userGroupsConfig: DeployableItemConfig = {
- // Basic info
- type: 'userGroups',
- singularLabel: 'User Group',
- pluralLabel: 'User Groups',
- icon: ,
- idField: 'id',
-
- // Navigation - No navigation for user groups, provide a dummy function
- buildDetailRoute: () => '#',
-
- // Configuration
- requiredEnvProps: ['environmentApikey', 'environmentApiServiceUrl'],
-
- // Stats rendering - Custom for user groups
- renderStats: (stats) => (
-
-
- } />
-
-
- } />
-
-
- } />
-
-
- ),
-
- // Stats calculation - Custom for user groups
- calculateStats: (userGroups) => {
- const total = userGroups.length;
- const totalUsers = userGroups.reduce(
- (sum, group) => sum + (group.stats?.userCount ?? 0),
- 0
- );
- const adminUsers = userGroups.reduce(
- (sum, group) => sum + (group.stats?.adminUserCount ?? 0),
- 0
- );
-
- return {
- total,
- managed: 0, // User groups don't have managed/unmanaged state
- unmanaged: 0, // User groups don't have managed/unmanaged state
- totalUsers,
- adminUsers
- };
- },
-
- // Table configuration
- columns: [
- {
- title: 'Name',
- dataIndex: 'groupName',
- key: 'groupName',
- render: (name: string, record: UserGroup) => (
-
- {record.groupName}
- {record.allUsersGroup && (
- All Users
- )}
- {record.devGroup && (
- Dev
- )}
-
- ),
- },
- {
- title: 'ID',
- dataIndex: 'groupId',
- key: 'groupId',
- ellipsis: true,
- },
- {
- title: 'Users',
- key: 'userCount',
- render: (_, record: UserGroup) => (
-
-
-
- ({record.stats.adminUserCount} admin{record.stats.adminUserCount !== 1 ? 's' : ''})
-
-
- ),
- },
- {
- title: 'Created',
- key: 'createTime',
- render: (_, record: UserGroup) => formatDate(record.createTime),
- },
- {
- title: 'Type',
- key: 'type',
- render: (_, record: UserGroup) => {
- if (record.allUsersGroup) return Global;
- if (record.devGroup) return Dev;
- if (record.syncGroup) return Sync;
- return Standard;
- },
- }
- ],
-
- // No managed status for user groups
- enableManaged: false,
-
- getColumns: ({ environment, additionalParams }) => {
- const columns = [
- createGroupIdColumn(),
- createUserGroupNameColumn(),
-
- createUserCountColumn(),
- createDateColumn('createTime', 'Created'),
- createGroupTypeColumn(),
- ];
-
- // User groups aren't managed, so we don't add the managed column
-
- // Add audit column if enabled
- if (userGroupsConfig.audit?.enabled) {
- columns.push(createAuditColumn(userGroupsConfig, environment, additionalParams));
- }
-
- return columns;
- },
- // Service functions
- fetchItems: async ({ environment }) => {
- const userGroups = await getEnvironmentUserGroups(
- environment.environmentId,
- environment.environmentApikey,
- environment.environmentApiServiceUrl!
- );
-
- // Map the required properties to satisfy DeployableItem interface
- return userGroups.map(group => ({
- ...group,
- id: group.groupId, // Map groupId to id
- name: group.groupName // Map groupName to name
- }));
- },
-
- // Dummy function for toggleManaged (will never be called since enableManaged is false)
- toggleManaged: async () => {
- return false;
- }
-};
\ No newline at end of file
diff --git a/client/packages/lowcoder/src/pages/setting/environments/config/workspace.config.tsx b/client/packages/lowcoder/src/pages/setting/environments/config/workspace.config.tsx
index c07a19210..4b55b4257 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/config/workspace.config.tsx
+++ b/client/packages/lowcoder/src/pages/setting/environments/config/workspace.config.tsx
@@ -1,166 +1,17 @@
// config/workspace.config.tsx
-import React from 'react';
-import { Row, Col, Statistic, Tag } from 'antd';
-import { ClusterOutlined, AuditOutlined } from '@ant-design/icons';
-import { Workspace, WorkspaceStats, DeployableItemConfig } from '../types/deployable-item.types';
+
+import { DeployableItemConfig } from '../types/deployable-item.types';
import { Environment } from '../types/environment.types';
-import { buildEnvironmentWorkspaceId } from '@lowcoder-ee/constants/routesURL';
-import { getMergedEnvironmentWorkspaces, deployWorkspace } from '../services/workspace.service';
-import { connectManagedWorkspace, unconnectManagedWorkspace } from '../services/enterprise.service';
-import {
- createNameColumn,
- createIdColumn,
- createRoleColumn,
- createDateColumn,
- createStatusColumn,
- createManagedColumn,
- createAuditColumn,
- createGidColumn
-} from '../utils/columnFactories';
+import { deployWorkspace } from '../services/workspace.service';
+import { Workspace } from '../types/workspace.types';
-export const workspaceConfig: DeployableItemConfig = {
- // Basic info
- type: 'workspaces',
- singularLabel: 'Workspace',
- pluralLabel: 'Workspaces',
- icon: ,
- idField: 'id',
-
- // Navigation
- buildDetailRoute: (params) => buildEnvironmentWorkspaceId(params.environmentId, params.itemId),
-
- // Configuration
- requiredEnvProps: ['environmentApikey', 'environmentApiServiceUrl'],
-
- // Stats rendering
- renderStats: (stats) => (
-
-
- } />
-
-
- } />
-
-
- } />
-
-
- ),
-
- // Stats calculation
- calculateStats: (workspaces) => {
- const total = workspaces.length;
- const managed = workspaces.filter(w => w.managed).length;
- return {
- total,
- managed,
- unmanaged: total - managed
- };
- },
-
- // Original columns for backward compatibility
- columns: [
- {
- title: 'Name',
- dataIndex: 'name',
- key: 'name',
- },
- {
- title: 'ID',
- dataIndex: 'id',
- key: 'id',
- ellipsis: true,
- },
- {
- title: 'Role',
- dataIndex: 'role',
- key: 'role',
- render: (role: string) => {role},
- },
- {
- title: 'Creation Date',
- key: 'creationDate',
- render: (_, record: Workspace) => {
- if (!record.creationDate) return 'N/A';
- const date = new Date(record.creationDate);
- return `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`;
- },
- },
- {
- title: 'Status',
- dataIndex: 'status',
- key: 'status',
- render: (status: string) => (
-
- {status}
-
- ),
- }
- ],
-
- // New getColumns method
- getColumns: ({ environment, refreshing, onToggleManaged, additionalParams }) => {
- const columns = [
- createIdColumn(),
- createGidColumn(),
- createNameColumn(),
- createRoleColumn(),
- createManagedColumn(),
- createDateColumn('creationDate', 'Creation Date'),
- createStatusColumn()
- ];
-
- // Add audit column if enabled
- if (workspaceConfig.audit?.enabled) {
- columns.push(createAuditColumn(workspaceConfig, environment, additionalParams));
- }
-
- return columns;
- },
-
- // Enable managed functionality
- enableManaged: true,
-
- // Fetch function
- fetchItems: async ({ environment }) => {
- const result = await getMergedEnvironmentWorkspaces(
- environment.environmentId,
- environment.environmentApikey,
- environment.environmentApiServiceUrl!
- );
- return result.workspaces;
- },
-
- // Toggle managed status
- toggleManaged: async ({ item, checked, environment }) => {
- try {
- if (checked) {
- await connectManagedWorkspace(environment.environmentId, item.name, item.gid!);
- } else {
- await unconnectManagedWorkspace(item.gid!);
- }
- return true;
- } catch (error) {
- console.error('Error toggling managed status:', error);
- return false;
- }
- },
-
- // Audit configuration
- audit: {
- enabled: true,
- icon: ,
- label: 'Audit',
- tooltip: 'View audit logs for this workspace',
- getAuditUrl: (item, environment) =>
- `/setting/audit?environmentId=${environment.environmentId}&orgId=${item.id}&pageSize=100&pageNum=1`
- },
+export const workspaceConfig: DeployableItemConfig = {
// Deploy configuration
deploy: {
- enabled: true,
+ singularLabel: 'Workspace',
fields: [],
prepareParams: (item: Workspace, values: any, sourceEnv: Environment, targetEnv: Environment) => {
if (!item.gid) {
diff --git a/client/packages/lowcoder/src/pages/setting/environments/context/DeployModalContext.tsx b/client/packages/lowcoder/src/pages/setting/environments/context/DeployModalContext.tsx
index 7084e9405..904ab62c6 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/context/DeployModalContext.tsx
+++ b/client/packages/lowcoder/src/pages/setting/environments/context/DeployModalContext.tsx
@@ -1,13 +1,13 @@
// context/DeployModalContext.tsx
import React, { createContext, useContext, useState } from 'react';
-import { DeployableItem, BaseStats, DeployableItemConfig } from '../types/deployable-item.types';
+import { DeployableItemConfig } from '../types/deployable-item.types';
import { Environment } from '../types/environment.types';
import DeployItemModal from '../components/DeployItemModal';
interface DeployModalContextType {
- openDeployModal: (
- item: T,
- config: DeployableItemConfig,
+ openDeployModal: (
+ item: any,
+ config: DeployableItemConfig,
sourceEnvironment: Environment,
onSuccess?: () => void
) => void;
@@ -18,8 +18,8 @@ const DeployModalContext = createContext(und
export const DeployModalProvider: React.FC<{children: React.ReactNode}> = ({ children }) => {
const [modalState, setModalState] = useState<{
visible: boolean;
- item: DeployableItem | null;
- config: DeployableItemConfig | null;
+ item: any | null;
+ config: DeployableItemConfig | null;
sourceEnvironment: Environment | null;
onSuccess?: () => void;
}>({
@@ -29,9 +29,9 @@ export const DeployModalProvider: React.FC<{children: React.ReactNode}> = ({ chi
sourceEnvironment: null
});
- const openDeployModal = (
- item: T,
- config: DeployableItemConfig,
+ const openDeployModal = (
+ item: any,
+ config: DeployableItemConfig,
sourceEnvironment: Environment,
onSuccess?: () => void
) => {
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 72c7ef356..f2e16d442 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/context/WorkspaceContext.tsx
+++ b/client/packages/lowcoder/src/pages/setting/environments/context/WorkspaceContext.tsx
@@ -12,7 +12,7 @@ import React, {
import { useSingleEnvironmentContext } from "./SingleEnvironmentContext";
import { fetchWorkspaceById } from "../services/environments.service";
import { Workspace } from "../types/workspace.types";
- import { getManagedWorkspaces, connectManagedWorkspace, unconnectManagedWorkspace } from "../services/enterprise.service";
+ import { getManagedObjects, ManagedObjectType, setManagedObject, unsetManagedObject } from "../services/managed-objects.service";
interface WorkspaceContextState {
// Workspace data
@@ -86,10 +86,10 @@ import React, {
}
// Fetch managed workspaces to check if this one is managed
- const managedWorkspaces = await getManagedWorkspaces(environment.environmentId);
+ const managedWorkspaces = await getManagedObjects(environment.environmentId, ManagedObjectType.ORG);
// Set the managed status
- const isManaged = managedWorkspaces.some(org => org.orgGid === workspaceData.gid);
+ const isManaged = managedWorkspaces.some(org => org.objGid === workspaceData.gid);
// Update the workspace with managed status
setWorkspace({
@@ -113,14 +113,20 @@ import React, {
try {
if (checked) {
// Connect the workspace as managed
- await connectManagedWorkspace(
+ await setManagedObject(
+ workspace.gid!,
environment.environmentId,
- workspace.name,
- workspace.gid!
+ ManagedObjectType.ORG,
+
+
);
} else {
// Disconnect the managed workspace
- await unconnectManagedWorkspace(workspace.gid!);
+ await unsetManagedObject(
+ workspace.gid!,
+ environment.environmentId,
+ ManagedObjectType.ORG
+ );
}
// Update local state
diff --git a/client/packages/lowcoder/src/pages/setting/environments/hooks/useDeployableItems.ts b/client/packages/lowcoder/src/pages/setting/environments/hooks/useDeployableItems.ts
deleted file mode 100644
index bb04cf54f..000000000
--- a/client/packages/lowcoder/src/pages/setting/environments/hooks/useDeployableItems.ts
+++ /dev/null
@@ -1,146 +0,0 @@
-// hooks/useDeployableItems.ts
-import { useState, useEffect, useCallback } from "react";
-import { DeployableItem, BaseStats, DeployableItemConfig } from "../types/deployable-item.types";
-import { Environment } from "../types/environment.types";
-
-interface UseDeployableItemsState {
- items: T[];
- stats: S;
- loading: boolean;
- error: string | null;
- refreshing: boolean;
-}
-
-export interface UseDeployableItemsResult {
- items: T[];
- stats: S;
- loading: boolean;
- error: string | null;
- refreshing: boolean;
- toggleManagedStatus: (item: T, checked: boolean) => Promise;
- refreshItems: () => Promise;
-}
-
-export const useDeployableItems = (
- config: DeployableItemConfig,
- environment: Environment | null,
- additionalParams: Record = {}
-): UseDeployableItemsResult => {
- // Create a default empty stats object based on the config's calculateStats method
- const createEmptyStats = (): S => {
- return config.calculateStats([]) as S;
- };
-
- const [state, setState] = useState>({
- items: [],
- stats: createEmptyStats(),
- loading: false,
- error: null,
- refreshing: false
- });
-
- const fetchItems = useCallback(async () => {
- if (!environment) return;
-
- // Check for required environment properties
- const missingProps = config.requiredEnvProps.filter(prop => !environment[prop as keyof Environment]);
-
- if (missingProps.length > 0) {
- setState(prev => ({
- ...prev,
- loading: false,
- error: `Missing required configuration: ${missingProps.join(', ')}`
- }));
- return;
- }
-
- setState(prev => ({ ...prev, loading: true, error: null }));
-
- try {
- // Call the fetchItems function from the config
- const items = await config.fetchItems({
- environment,
- ...additionalParams
- });
-
- // Calculate stats using the config's function
- const stats = config.calculateStats(items);
-
- // Update state with items and stats
- setState({
- items,
- stats,
- loading: false,
- error: null,
- refreshing: false
- });
- } catch (err) {
- setState(prev => ({
- ...prev,
- loading: false,
- refreshing: false,
- error: err instanceof Error ? err.message : "Failed to fetch items"
- }));
- }
- }, [environment, config]);
-
- useEffect(() => {
- if (environment) {
- fetchItems();
- }
- }, [environment, fetchItems]);
-
- const toggleManagedStatus = async (item: T, checked: boolean): Promise => {
- if (!config.enableManaged) return false;
- if (!environment) return false;
-
- setState(prev => ({ ...prev, refreshing: true }));
-
- try {
- // Call the toggleManaged function from the config
- const success = await config.toggleManaged({
- item,
- checked,
- environment
- });
-
- if (success) {
- // Optimistically update the state
- setState(prev => {
- // Update items with the new managed status
- const updatedItems = prev.items.map(i =>
- i[config.idField] === item[config.idField] ? { ...i, managed: checked } : i
- );
-
- // Recalculate stats
- const stats = config.calculateStats(updatedItems);
-
- return {
- ...prev,
- items: updatedItems,
- stats,
- refreshing: false
- };
- });
- } else {
- setState(prev => ({ ...prev, refreshing: false }));
- }
-
- return success;
- } catch (err) {
- setState(prev => ({ ...prev, refreshing: false }));
- return false;
- }
- };
-
- const refreshItems = async (): Promise => {
- setState(prev => ({ ...prev, refreshing: true }));
- await fetchItems();
- };
-
- return {
- ...state,
- toggleManagedStatus,
- refreshItems
- };
-};
\ No newline at end of file
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 52528b4d0..bfd7ae348 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
@@ -2,15 +2,11 @@
import { message } from "antd";
import { getWorkspaceApps } from "./environments.service";
import { getManagedApps } from "./enterprise.service";
-import { App } from "../types/app.types";
+import { App, AppStats } from "../types/app.types";
import axios from "axios";
+import { getManagedObjects, ManagedObject } from "./managed-objects.service";
+import { ManagedObjectType, transferManagedObject } from "./managed-objects.service";
-export interface AppStats {
- total: number;
- published: number;
- managed: number;
- unmanaged: number;
-}
export interface MergedAppsResult {
apps: App[];
@@ -30,10 +26,13 @@ export interface DeployAppParams {
// Use your existing merge function with slight modification
-export const getMergedApps = (standardApps: App[], managedApps: any[]): App[] => {
+export const getMergedApps = (standardApps: App[], managedObjects: ManagedObject[]): App[] => {
return standardApps.map((app) => ({
...app,
- managed: managedApps.some((managedApp) => managedApp.appGid === app.applicationGid),
+ managed: managedObjects.some((managedObj) =>
+ managedObj.objGid === app.applicationGid &&
+ managedObj.objType === "APP"
+ ),
}));
};
@@ -77,17 +76,17 @@ export async function getMergedWorkspaceApps(
};
}
- // Only fetch managed apps if we have regular apps
- let managedApps = [];
+ // Fetch managed objects instead of managed apps
+ let managedObjects: ManagedObject[] = [];
try {
- managedApps = await getManagedApps(environmentId);
+ managedObjects = await getManagedObjects(environmentId);
} catch (error) {
- console.error("Failed to fetch managed apps:", error);
+ console.error("Failed to fetch managed objects:", error);
// Continue with empty managed list
}
- // Use your existing merge function
- const mergedApps = getMergedApps(regularApps, managedApps);
+ // Use the updated merge function
+ const mergedApps = getMergedApps(regularApps, managedObjects);
// Calculate stats
const stats = calculateAppStats(mergedApps);
@@ -124,6 +123,15 @@ export const deployApp = async (params: DeployAppParams): Promise => {
}
);
+ if (response.status === 200) {
+ await transferManagedObject(
+ params.applicationId,
+ params.envId,
+ params.targetEnvId,
+ ManagedObjectType.APP
+ );
+ }
+
return response.status === 200;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to deploy app';
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 71d6929ac..694c28dd1 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
@@ -2,7 +2,7 @@
import axios from 'axios';
import { message } from "antd";
import { DataSource, DataSourceWithMeta } from "../types/datasource.types";
-import { getManagedDataSources } from "./enterprise.service";
+import { getManagedObjects, ManagedObject, ManagedObjectType , transferManagedObject } from "./managed-objects.service";
export interface DataSourceStats {
total: number;
@@ -71,12 +71,12 @@ export async function getWorkspaceDataSources(
}
// Function to merge regular and managed data sources
-export const getMergedDataSources = (standardDataSources: DataSourceWithMeta[], managedDataSources: any[]): DataSource[] => {
+export const getMergedDataSources = (standardDataSources: DataSourceWithMeta[], managedObjects: ManagedObject[]): DataSource[] => {
return standardDataSources.map((dataSourceWithMeta) => {
const dataSource = dataSourceWithMeta.datasource;
return {
...dataSource,
- managed: managedDataSources.some((managedDs) => managedDs.datasourceGid === dataSource.gid),
+ managed: managedObjects.some((obj) => obj.objGid === dataSource.gid && obj.objType === ManagedObjectType.DATASOURCE),
};
});
};
@@ -123,16 +123,16 @@ export async function getMergedWorkspaceDataSources(
}
// Only fetch managed data sources if we have regular data sources
- let managedDataSources = [];
+ let managedObjects: ManagedObject[] = [];
try {
- managedDataSources = await getManagedDataSources(environmentId);
+ managedObjects = await getManagedObjects(environmentId, ManagedObjectType.DATASOURCE);
} catch (error) {
console.error("Failed to fetch managed data sources:", error);
// Continue with empty managed list
}
// Use the merge function
- const mergedDataSources = getMergedDataSources(regularDataSourcesWithMeta, managedDataSources);
+ const mergedDataSources = getMergedDataSources(regularDataSourcesWithMeta, managedObjects);
// Calculate stats
const stats = calculateDataSourceStats(mergedDataSources);
@@ -160,6 +160,14 @@ export async function deployDataSource(params: DeployDataSourceParams): Promise<
updateDependenciesIfNeeded: params.updateDependenciesIfNeeded ?? false
}
});
+ if (response.status === 200) {
+ await transferManagedObject(
+ params.datasourceId,
+ params.envId,
+ params.targetEnvId,
+ ManagedObjectType.DATASOURCE
+ );
+ }
return response.status === 200;
} catch (error) {
console.error('Error deploying data source:', error);
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
new file mode 100644
index 000000000..589ea6a5a
--- /dev/null
+++ b/client/packages/lowcoder/src/pages/setting/environments/services/managed-objects.service.ts
@@ -0,0 +1,207 @@
+import axios from "axios";
+import { message } from "antd";
+
+// Object types that can be managed
+export enum ManagedObjectType {
+ ORG = "ORG",
+ APP = "APP",
+ QUERY = "QUERY",
+ DATASOURCE = "DATASOURCE"
+}
+
+// Add this interface after the ManagedObjectType enum
+export interface ManagedObject {
+ id: string;
+ managedId: string;
+ objGid: string;
+ environmentId: string;
+ objType: ManagedObjectType;
+}
+
+/**
+ * Check if an object is managed
+ * @param objGid - Object's global ID
+ * @param environmentId - Environment ID
+ * @param objType - Object type (ORG, APP, QUERY, DATASOURCE)
+ * @returns Promise with boolean indicating if object is managed
+ */
+export async function isManagedObject(
+ objGid: string,
+ environmentId: string,
+ objType: ManagedObjectType
+): Promise {
+ try {
+ if (!objGid || !environmentId || !objType) {
+ throw new Error("Missing required parameters");
+ }
+
+ const response = await axios.get(`/api/plugins/enterprise/managed-obj`, {
+ params: {
+ objGid,
+ environmentId,
+ objType
+ }
+ });
+
+ return response.data.managed === true;
+ } catch (error) {
+ // If the object doesn't exist as managed, it's not an error
+ if (axios.isAxiosError(error) && error.response?.status === 404) {
+ return false;
+ }
+
+ const errorMessage = error instanceof Error ? error.message : "Failed to check managed status";
+ message.error(errorMessage);
+ throw error;
+ }
+}
+
+/**
+ * Set an object as managed
+ * @param objGid - Object's global ID
+ * @param environmentId - Environment ID
+ * @param objType - Object type (ORG, APP, QUERY, DATASOURCE)
+ * @param objName - Object name (optional)
+ * @param objTags - Object tags (optional)
+ * @returns Promise with operation result
+ */
+export async function setManagedObject(
+ objGid: string,
+ environmentId: string,
+ objType: ManagedObjectType,
+ managedId?: string
+): Promise {
+ try {
+ if (!objGid || !environmentId || !objType) {
+ throw new Error("Missing required parameters");
+ }
+
+ const requestBody = {
+ objGid,
+ environmentId,
+ objType,
+ ...(managedId && { managedId })
+ };
+
+ const response = await axios.post(`/api/plugins/enterprise/managed-obj`, requestBody);
+
+ return response.status === 200;
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : `Failed to set ${objType} as managed`;
+ message.error(errorMessage);
+ throw error;
+ }
+}
+
+
+/**
+ * Set an object as unmanaged
+ * @param objGid - Object's global ID
+ * @param environmentId - Environment ID
+ * @param objType - Object type (ORG, APP, QUERY, DATASOURCE)
+ * @returns Promise with operation result
+ */
+export async function unsetManagedObject(
+ objGid: string,
+ environmentId: string,
+ objType: ManagedObjectType
+): Promise {
+ try {
+ if (!objGid || !environmentId || !objType) {
+ throw new Error("Missing required parameters");
+ }
+
+ const response = await axios.delete(`/api/plugins/enterprise/managed-obj`, {
+ params: {
+ objGid,
+ environmentId,
+ objType
+ }
+ });
+
+ return response.status === 200;
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : `Failed to remove ${objType} from managed`;
+ message.error(errorMessage);
+ throw error;
+ }
+}
+
+// Add this new function
+export async function getManagedObjects(
+ environmentId: string,
+ objType?: ManagedObjectType
+): Promise {
+ try {
+ if (!environmentId) {
+ throw new Error("Missing environment ID");
+ }
+
+ const response = await axios.get(`/api/plugins/enterprise/managed-obj/list`, {
+ params: {
+ environmentId,
+ ...(objType && { objType }) // Only include objType in params if it's provided
+ }
+ });
+
+ return response.data.data;
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : "Failed to fetch managed objects";
+ message.error(errorMessage);
+ throw error;
+ }
+}
+
+/**
+ * Get a single managed object by its parameters
+ * @param objGid - Object's global ID
+ * @param environmentId - Environment ID
+ * @param objType - Object type (ORG, APP, QUERY, DATASOURCE)
+ * @returns Promise with ManagedObject if found
+ */
+export async function getSingleManagedObject(
+ objGid: string,
+ environmentId: string,
+ objType: ManagedObjectType
+): Promise {
+ try {
+ if (!objGid || !environmentId || !objType) {
+ throw new Error("Missing required parameters");
+ }
+
+ const response = await axios.get(`/api/plugins/enterprise/managed-obj`, {
+ params: {
+ objGid,
+ environmentId,
+ objType
+ }
+ });
+
+ return response.data.data || null;
+ } catch (error) {
+ // If the object doesn't exist as managed, return null instead of throwing
+ if (axios.isAxiosError(error) && error.response?.status === 404) {
+ return null;
+ }
+
+ const errorMessage = error instanceof Error ? error.message : "Failed to fetch managed object";
+ message.error(errorMessage);
+ throw error;
+ }
+}
+
+
+export async function transferManagedObject(objGid: string, sourceEnvId: string, targetEnvId: string, objType: ManagedObjectType): Promise {
+ try {
+ const managedObject = await getSingleManagedObject(objGid, sourceEnvId, objType);
+ if (managedObject) {
+ await setManagedObject(managedObject.objGid, targetEnvId, objType, managedObject.managedId);
+ } else {
+ throw new Error(`Managed object not found for objGid: ${objGid}`);
+ }
+ } catch (error) {
+ console.error('Error transferring managed object:', error);
+ throw error;
+ }
+}
+
diff --git a/client/packages/lowcoder/src/pages/setting/environments/services/query.service.ts b/client/packages/lowcoder/src/pages/setting/environments/services/query.service.ts
index 8f5ad6892..6801097be 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/services/query.service.ts
+++ b/client/packages/lowcoder/src/pages/setting/environments/services/query.service.ts
@@ -2,16 +2,12 @@
* Get merged queries (both regular and managed) for a workspace
*/
import axios from 'axios';
-import { getManagedQueries } from './enterprise.service';
+import { getManagedObjects, ManagedObjectType, transferManagedObject } from './managed-objects.service';
import { getWorkspaceQueries } from './environments.service';
-import { Query } from '../types/query.types';
+import { Query, QueryStats } from '../types/query.types';
export interface MergedQueriesResult {
queries: Query[];
- stats: {
- total: number;
- managed: number;
- unmanaged: number;
- };
+ stats: QueryStats;
}
export interface DeployQueryParams {
@@ -34,11 +30,11 @@ export interface MergedQueriesResult {
const regularQueries = await getWorkspaceQueries(workspaceId, apiKey, apiServiceUrl);
console.log("Regular queries response:", regularQueries);
- const managedQueries = await getManagedQueries(environmentId);
- console.log("Managed queries response:", managedQueries);
+ const managedObjects = await getManagedObjects(environmentId, ManagedObjectType.QUERY);
+ console.log("Managed queries response:", managedObjects);
- // Create a map of managed queries by GID for quick lookup
- const managedQueryGids = new Set(managedQueries.map(query => query.gid));
+ // Create a set of managed query GIDs for quick lookup
+ const managedQueryGids = new Set(managedObjects.map(obj => obj.objGid));
console.log("Managed query GIDs:", Array.from(managedQueryGids));
// Mark regular queries as managed if they exist in managed queries
@@ -86,6 +82,14 @@ export interface MergedQueriesResult {
updateDependenciesIfNeeded: params.updateDependenciesIfNeeded ?? false
}
});
+ if (response.status === 200) {
+ await transferManagedObject(
+ params.queryId,
+ params.envId,
+ params.targetEnvId,
+ ManagedObjectType.QUERY
+ );
+ }
return response.status === 200;
} catch (error) {
console.error('Error deploying query:', 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 43bc302a2..4f7978f3b 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,7 +1,7 @@
// services/workspacesService.ts (or wherever makes sense in your structure)
import { message } from "antd";
import { getEnvironmentWorkspaces } from "./environments.service";
-import { getManagedWorkspaces } from "./enterprise.service";
+import { getManagedObjects, ManagedObject, ManagedObjectType, transferManagedObject } from "./managed-objects.service";
import { Workspace } from "../types/workspace.types";
import { ManagedOrg } from "../types/enterprise.types";
import axios from "axios";
@@ -43,9 +43,9 @@ export async function getMergedEnvironmentWorkspaces(
}
// Only fetch managed workspaces if we have regular workspaces
- let managedOrgs: ManagedOrg[] = [];
+ let managedObjects: ManagedObject[] = [];
try {
- managedOrgs = await getManagedWorkspaces(environmentId);
+ managedObjects = await getManagedObjects(environmentId, ManagedObjectType.ORG);
} catch (error) {
console.error("Failed to fetch managed workspaces:", error);
// Continue with empty managed list
@@ -54,7 +54,7 @@ export async function getMergedEnvironmentWorkspaces(
// Merge the workspaces
const mergedWorkspaces = regularWorkspaces.map(ws => ({
...ws,
- managed: managedOrgs.some(org => org.orgGid === ws.gid)
+ managed: managedObjects.some(obj => obj.objGid === ws.gid && obj.objType === ManagedObjectType.ORG)
}));
// Calculate stats
@@ -95,6 +95,18 @@ export async function deployWorkspace(params: {
targetEnvId: params.targetEnvId
}
});
+
+ // After successful deployment, set the managed object in target environment
+ if (response.status === 200) {
+ await transferManagedObject(
+ params.workspaceId,
+ params.envId,
+ params.targetEnvId,
+ ManagedObjectType.ORG
+ );
+ }
+
+
return response.status === 200;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to deploy workspace';
diff --git a/client/packages/lowcoder/src/pages/setting/environments/types/app.types.ts b/client/packages/lowcoder/src/pages/setting/environments/types/app.types.ts
index b3af252b5..775a589fb 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/types/app.types.ts
+++ b/client/packages/lowcoder/src/pages/setting/environments/types/app.types.ts
@@ -1,6 +1,5 @@
-import { DeployableItem, BaseStats } from "./deployable-item.types";
-export interface App extends DeployableItem {
+export interface App {
orgId: string;
applicationId: string;
applicationGid: string;
@@ -28,6 +27,9 @@ export interface App extends DeployableItem {
id: string
}
- export interface AppStats extends BaseStats {
- published: number
+ export interface AppStats {
+ total: number;
+ published: number;
+ managed: number;
+ unmanaged: number;
}
\ No newline at end of file
diff --git a/client/packages/lowcoder/src/pages/setting/environments/types/datasource.types.ts b/client/packages/lowcoder/src/pages/setting/environments/types/datasource.types.ts
index f4f03072d..b33e14e86 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/types/datasource.types.ts
+++ b/client/packages/lowcoder/src/pages/setting/environments/types/datasource.types.ts
@@ -1,8 +1,6 @@
/**
* Represents a DataSource configuration
*/
-
-import { DeployableItem, BaseStats } from "./deployable-item.types";
export interface DataSourceConfig {
usingUri: boolean;
srvMode: boolean;
@@ -18,7 +16,7 @@ export interface DataSourceConfig {
/**
* Represents a DataSource entity
*/
- export interface DataSource extends DeployableItem {
+ export interface DataSource {
id: string;
createdBy: string;
gid: string;
@@ -41,7 +39,3 @@ export interface DataSourceConfig {
edit: boolean;
creatorName: string;
}
-
- export interface DataSourceStats extends BaseStats {
- byType: Record; // Count by each type
- }
\ No newline at end of file
diff --git a/client/packages/lowcoder/src/pages/setting/environments/types/deployable-item.types.ts b/client/packages/lowcoder/src/pages/setting/environments/types/deployable-item.types.ts
index ac223c63d..55964eefe 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/types/deployable-item.types.ts
+++ b/client/packages/lowcoder/src/pages/setting/environments/types/deployable-item.types.ts
@@ -1,45 +1,7 @@
// types/deployable-item.types.ts
-import { ReactNode } from 'react';
import { Environment } from './environment.types';
-import { ColumnType } from 'antd/lib/table';
-// Base interface for all deployable items
-export interface AuditConfig {
- enabled: boolean;
- icon?: React.ReactNode;
- label?: string;
- tooltip?: string;
- getAuditUrl: (item: any, environment: Environment, additionalParams?: Record) => string;
-}
-export interface DeployableItem {
- id: string;
- name: string;
- managed?: boolean;
- [key: string]: any; // Allow for item-specific properties
-}
-
-// Workspace specific implementation
-export interface Workspace extends DeployableItem {
- id: string;
- name: string;
- role?: string;
- creationDate?: number;
- status?: string;
- managed?: boolean;
- gid?: string;
-}
-
-// Stats interface that can be extended for specific item types
-// Base interface for stats
-export interface BaseStats {
- total: number;
- managed: number;
- unmanaged: number;
- [key: string]: any;
-}
-export interface WorkspaceStats extends BaseStats {}
-
export interface DeployField {
name: string;
@@ -50,56 +12,11 @@ export interface DeployField {
options?: Array<{label: string, value: any}>; // For select fields
}
// Configuration for each deployable item type
-export interface DeployableItemConfig {
- // Identifying info
- type: string; // e.g., 'workspaces'
- singularLabel: string; // e.g., 'Workspace'
- pluralLabel: string; // e.g., 'Workspaces'
-
- // UI elements
- icon: ReactNode; // Icon to use in stats
-
- // Navigation
- buildDetailRoute: (params: Record) => string;
-
- // Configuration
- requiredEnvProps: string[]; // Required environment properties
-
- // Customization
- idField: string; // Field to use as the ID (e.g., 'id')
-
- // Stats
- renderStats: (stats: S) => ReactNode;
- calculateStats: (items: T[]) => S;
-
- // Original columns (will be deprecated)
- columns: ColumnType[];
-
- // New method to generate columns
- getColumns: (params: {
- environment: Environment;
- refreshing: boolean;
- onToggleManaged?: (item: T, checked: boolean) => Promise;
- openDeployModal?: (item: T, config: DeployableItemConfig, environment: Environment) => void;
- additionalParams?: Record;
- }) => ColumnType[];
-
- // Add audit configuration
- audit?: AuditConfig;
-
-
-
- // Deployable configuration
- enableManaged: boolean;
-
- // Service functions
- fetchItems: (params: { environment: Environment, [key: string]: any }) => Promise;
- toggleManaged: (params: { item: T; checked: boolean; environment: Environment }) => Promise;
-
- deploy?: {
- enabled: boolean;
+export interface DeployableItemConfig {
+ deploy: {
+ singularLabel: string;
fields: DeployField[];
- prepareParams: (item: T, values: any, sourceEnv: Environment, targetEnv: Environment) => any;
+ prepareParams: (item: any, values: any, sourceEnv: Environment, targetEnv: Environment) => any;
execute: (params: any) => Promise;
};
}
\ No newline at end of file
diff --git a/client/packages/lowcoder/src/pages/setting/environments/types/query.types.ts b/client/packages/lowcoder/src/pages/setting/environments/types/query.types.ts
index 5d38385b0..212efeaac 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/types/query.types.ts
+++ b/client/packages/lowcoder/src/pages/setting/environments/types/query.types.ts
@@ -1,5 +1,4 @@
// types/query.types.ts
-import { DeployableItem, BaseStats } from './deployable-item.types';
export interface LibraryQueryDSL {
query: {
@@ -32,11 +31,10 @@ export interface LibraryQueryDSL {
cancelPrevious: boolean;
depQueryName: string;
delayTime: string;
- managed?: boolean;
};
}
-export interface Query extends DeployableItem {
+export interface Query {
id: string;
gid: string;
organizationId: string;
@@ -44,9 +42,10 @@ export interface Query extends DeployableItem {
libraryQueryDSL: LibraryQueryDSL;
createTime: number;
creatorName: string;
+ managed?: boolean;
}
-export interface QueryStats extends BaseStats {
+export interface QueryStats {
total: number;
managed: number;
unmanaged: number;
diff --git a/client/packages/lowcoder/src/pages/setting/environments/types/userGroup.types.ts b/client/packages/lowcoder/src/pages/setting/environments/types/userGroup.types.ts
index 6a1938bcc..5791204c8 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/types/userGroup.types.ts
+++ b/client/packages/lowcoder/src/pages/setting/environments/types/userGroup.types.ts
@@ -1,34 +1,26 @@
-/**
- * Represents a User Group entity in an environment
-*/
+export interface UserGroupStats {
+ users: string[];
+ adminUserCount: number;
+ userCount: number;
+}
-import { DeployableItem, BaseStats } from "./deployable-item.types";
+export interface UserGroup {
+ groupId: string;
+ groupGid: string;
+ groupName: string;
+ allUsersGroup: boolean;
+ visitorRole: string;
+ createTime: number;
+ dynamicRule: any;
+ stats: UserGroupStats;
+ syncDelete: boolean;
+ devGroup: boolean;
+ syncGroup: boolean;
+}
-export interface UserGroup extends DeployableItem {
- groupId: string;
- groupGid: string;
- groupName: string;
- allUsersGroup: boolean;
- visitorRole: string;
- createTime: number;
- dynamicRule: any;
- stats: {
- users: string[];
- userCount: number;
- adminUserCount: number;
- };
- syncDelete: boolean;
- devGroup: boolean;
- syncGroup: boolean;
- id: string;
- name: string;
- }
-
-
- /**
- * Statistics for User Groups
- */
-export interface UserGroupStats extends BaseStats {
- totalUsers: number;
- adminUsers: number;
+export interface UserGroupsTabStats {
+ total: number;
+ allUsers: number;
+ developers: number;
+ custom: number;
}
\ No newline at end of file
diff --git a/client/packages/lowcoder/src/pages/setting/environments/utils/columnFactories.tsx b/client/packages/lowcoder/src/pages/setting/environments/utils/columnFactories.tsx
deleted file mode 100644
index e0e89465e..000000000
--- a/client/packages/lowcoder/src/pages/setting/environments/utils/columnFactories.tsx
+++ /dev/null
@@ -1,327 +0,0 @@
-// utils/columnFactories.tsx
-import React from 'react';
-import { Tag, Space, Switch, Button, Tooltip, Badge} from 'antd';
-import { CloudUploadOutlined, AuditOutlined } from '@ant-design/icons';
-import { ColumnType } from 'antd/lib/table';
-import { DeployableItem, DeployableItemConfig, BaseStats } from '../types/deployable-item.types';
-import { Environment } from '../types/environment.types';
-
-// Base columns for workspace
-export function createNameColumn(): ColumnType {
- return {
- title: 'Name',
- dataIndex: 'name',
- key: 'name',
- };
-}
-
-export function createIdColumn(): ColumnType {
- return {
- title: 'ID',
- dataIndex: 'id',
- key: 'id',
- ellipsis: true,
- };
-}
-
-export function createGidColumn(): ColumnType {
- return {
- title: 'GID',
- dataIndex: 'gid',
- key: 'gid',
- ellipsis: true,
- };
-}
-
-export function createApplicationGidColumn(): ColumnType {
- return {
- title: 'GID',
- dataIndex: 'applicationGid',
- key: 'applicationGid',
- ellipsis: true,
- };
-}
-
-export function createRoleColumn(): ColumnType {
- return {
- title: 'Role',
- dataIndex: 'role',
- key: 'role',
- render: (role: string) => {role},
- };
-}
-
-export function createDateColumn(
- dateField: string,
- title: string
-): ColumnType {
- return {
- title: title,
- key: dateField,
- render: (_, record: any) => {
- if (!record[dateField]) return 'N/A';
- const date = new Date(record[dateField]);
- return `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`;
- },
- };
-}
-
-export function createStatusColumn(): ColumnType {
- return {
- title: 'Status',
- dataIndex: 'status',
- key: 'status',
- render: (status: string) => (
-
- {status}
-
- ),
- };
-}
-
-// Feature columns
-export function createManagedColumn(
- onToggleManaged?: (item: T, checked: boolean) => Promise,
- refreshing: boolean = false
-): ColumnType {
- return {
- title: 'Managed',
- key: 'managed',
- render: (_, record: T) => (
-
-
- {record.managed ? 'Managed' : 'Unmanaged'}
-
- {onToggleManaged && (
- {
- e.stopPropagation(); // Stop row click event
- onToggleManaged(record, checked);
- }}
- onChange={() => {}}
- />
- )}
-
- ),
- };
-}
-
-export function createAuditColumn(
- config: DeployableItemConfig,
- environment: Environment,
- additionalParams: Record = {}
-): ColumnType {
- return {
- title: 'Audit',
- key: 'audit',
- render: (_, record: T) => {
- const openAuditPage = (e: React.MouseEvent) => {
- e.stopPropagation();
- if (config.audit?.getAuditUrl) {
- const auditUrl = config.audit.getAuditUrl(record, environment, additionalParams);
- window.open(auditUrl, '_blank');
- }
- };
-
- return (
-
- }
- onClick={openAuditPage}
- >
- {config.audit?.label || 'Audit'}
-
-
- );
- },
- };
-}
-
-
-export function createDescriptionColumn(): ColumnType {
- return {
- title: 'Description',
- dataIndex: 'description',
- key: 'description',
- ellipsis: true,
- };
-}
-
-
-export function createDeployColumn(
- config: DeployableItemConfig,
- environment: Environment,
- openDeployModal: (item: T, config: DeployableItemConfig, environment: Environment) => void
-): ColumnType {
- return {
- title: 'Actions',
- key: 'actions',
- render: (_, record: T) => {
- // Check if the item is managed
- const isManaged = record.managed === true;
-
- return (
-
-
- }
- onClick={(e) => {
- e.stopPropagation(); // Prevent row click navigation
- if (isManaged) {
- openDeployModal(record, config, environment);
- }
- }}
- type="primary"
- ghost
- disabled={!isManaged}
- >
- Deploy
-
-
-
- );
- },
- };
-}
-
-// App-specific columns
-export function createPublishedColumn(): ColumnType {
- return {
- title: 'Status',
- dataIndex: 'published',
- key: 'published',
- render: (published: boolean) => (
-
- {published ? 'Published' : 'Unpublished'}
-
- ),
- };
-}
-
-// Data Source specific columns
-export function createTypeColumn(): ColumnType {
- return {
- title: 'Type',
- dataIndex: 'type',
- key: 'type',
- render: (type: string) => (
- {type || 'Unknown'}
- ),
- };
-}
-
-export function createDatabaseColumn(): ColumnType {
- return {
- title: 'Database',
- key: 'database',
- render: (_, record: T) => (
- {record.datasourceConfig?.database || 'N/A'}
- ),
- };
-}
-
-export function createDatasourceStatusColumn(): ColumnType {
- return {
- title: 'Status',
- dataIndex: 'datasourceStatus',
- key: 'status',
- render: (status: string) => (
-
- {status}
-
- ),
- };
-}
-
-
-// Query-specific column factories to add to columnFactories.tsx
-export function createCreatorColumn(): ColumnType {
- return {
- title: 'Creator',
- dataIndex: 'creatorName',
- key: 'creatorName',
- };
-}
-
-export function createQueryTypeColumn(): ColumnType {
- return {
- title: 'Query Type',
- key: 'queryType',
- render: (_, record: T) => {
- const queryType = record.libraryQueryDSL?.query?.compType || 'Unknown';
- return {queryType};
- },
- };
-}
-
-export function createUserGroupNameColumn(): ColumnType {
- return {
- title: 'Name',
- dataIndex: 'groupName',
- key: 'groupName',
- render: (name: string, record: T) => (
-
- {record.groupName}
- {record.allUsersGroup && (
- All Users
- )}
- {record.devGroup && (
- Dev
- )}
-
- ),
- };
-}
-
-export function createGroupIdColumn(): ColumnType {
- return {
- title: 'ID',
- dataIndex: 'groupId',
- key: 'groupId',
- ellipsis: true,
- };
-}
-
-export function createUserCountColumn(): ColumnType {
- return {
- title: 'Users',
- key: 'userCount',
- render: (_, record: T) => (
-
-
-
- ({record.stats?.adminUserCount || 0} admin{(record.stats?.adminUserCount || 0) !== 1 ? 's' : ''})
-
-
- ),
- };
-}
-
-export function createGroupTypeColumn(): ColumnType {
- return {
- title: 'Type',
- key: 'type',
- render: (_, record: T) => {
- if (record.allUsersGroup) return Global;
- if (record.devGroup) return Dev;
- if (record.syncGroup) return Sync;
- return Standard;
- },
- };
-}
\ No newline at end of file
diff --git a/client/packages/lowcoder/src/pages/setting/environments/utils/environmentUtils.ts b/client/packages/lowcoder/src/pages/setting/environments/utils/environmentUtils.ts
new file mode 100644
index 000000000..59a21f859
--- /dev/null
+++ b/client/packages/lowcoder/src/pages/setting/environments/utils/environmentUtils.ts
@@ -0,0 +1,71 @@
+/**
+ * Utility functions for environment-related features
+ */
+
+/**
+ * Get the appropriate color for an environment tag based on its type
+ * @param envType The environment type/stage (DEV, TEST, PREPROD, PROD)
+ * @returns A color string to use with Ant Design's Tag component
+ */
+export const getEnvironmentTagColor = (envType: string | undefined): string => {
+ if (!envType) return 'default';
+
+ // Normalize to uppercase for consistent comparison
+ const type = envType.toUpperCase();
+
+ switch (type) {
+ case 'PROD':
+ return 'red'; // Red for production - indicates caution
+
+ case 'PREPROD':
+ return 'orange'; // Orange for pre-production
+
+ case 'TEST':
+ return 'purple'; // Purple for test environment
+
+ case 'DEV':
+ return 'blue'; // Blue for development
+
+ default:
+ return 'default'; // Default gray for unknown types
+ }
+};
+
+/**
+ * Get the appropriate background gradient for an environment based on its type
+ * @param envType The environment type/stage (DEV, TEST, PREPROD, PROD)
+ * @returns A CSS linear gradient string for the background
+ */
+export const getEnvironmentHeaderGradient = (envType: string | undefined): string => {
+ if (!envType) return 'linear-gradient(135deg, #1890ff 0%, #096dd9 100%)';
+
+ // Normalize to uppercase for consistent comparison
+ const type = envType.toUpperCase();
+
+ switch (type) {
+ case 'PROD':
+ return 'linear-gradient(135deg, #f5222d 0%, #fa8c16 100%)';
+
+ case 'PREPROD':
+ return 'linear-gradient(135deg, #fa8c16 0%, #faad14 100%)';
+
+ case 'TEST':
+ return 'linear-gradient(135deg, #722ed1 0%, #b37feb 100%)';
+
+ case 'DEV':
+ return 'linear-gradient(135deg, #1890ff 0%, #096dd9 100%)';
+
+ default:
+ return 'linear-gradient(135deg, #1890ff 0%, #096dd9 100%)';
+ }
+};
+
+/**
+ * Format an environment type for display
+ * @param envType The environment type string
+ * @returns Formatted environment type string
+ */
+export const formatEnvironmentType = (envType: string | undefined): string => {
+ if (!envType) return 'UNKNOWN';
+ return envType.toUpperCase();
+};
\ No newline at end of file