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 = ( - - } onClick={handleEditClick}> - Edit Environment - - {/* Add more menu items here if needed */} - - ); + 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} -
-
- -
-
+ {/* 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 + +
+
+ + + + + + +
+
+ + {/* 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: - -
- - - - -
-
- + {/* 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()}> + + + + + + + + + ), + } + ]; + + // 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 }) => ( + +
+
+
{title}
+
{value}
+
+
+ {icon} +
+
+
+ ); + + return ( +
+ {/* Header */} +
+
+ + <AppstoreOutlined style={{ marginRight: 10 }} /> Apps + +

Manage your workspace applications

+
+ +
+ + {/* 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()}> + + + + + + + + + ), + } + ]; + + // 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 }) => ( + +
+
+
{title}
+
{value}
+
+
+ {icon} +
+
+
+ ); + + return ( +
+ {/* Header */} +
+
+ + <DatabaseOutlined style={{ marginRight: 10 }} /> Data Sources + +

Manage your workspace data connections

+
+ +
+ + {/* 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`} - - -
- - {/* 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={[
+
+
+ +
+
+ + {environment.environmentName || "Unnamed Environment"} + +
+ + ID: {environment.environmentId} + + + {environment.environmentType} + + {environment.isMaster && ( + + Master + + )} +
+
+
+ + + + + + + ); +}; + +export default EnvironmentHeader; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentsTable.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentsTable.tsx index 0208932d7..16f9fc6fd 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentsTable.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentsTable.tsx @@ -1,127 +1,164 @@ import React from 'react'; -import { Table, Tag, Button, Tooltip, Space } from 'antd'; -import { EditOutlined, AuditOutlined} from '@ant-design/icons'; +import { Table, Tag, Button, Tooltip, Space, Card, Row, Col, Typography, Avatar } from 'antd'; +import { EditOutlined, AuditOutlined, LinkOutlined, EnvironmentOutlined, StarFilled, CloudServerOutlined } from '@ant-design/icons'; import { Environment } from '../types/environment.types'; +import { getEnvironmentTagColor, formatEnvironmentType } from '../utils/environmentUtils'; - +const { Text, Title } = Typography; interface EnvironmentsTableProps { environments: Environment[]; loading: boolean; onRowClick: (record: Environment) => void; - } /** - * Table component for displaying environments + * Modern card-based layout for displaying environments */ const EnvironmentsTable: React.FC = ({ environments, loading, onRowClick, }) => { - // Get color for environment type/stage - const getTypeColor = (type: string): string => { - if (!type) return 'default'; - - switch (type.toUpperCase()) { - case 'DEV': return 'blue'; - case 'TEST': return 'orange'; - case 'PREPROD': return 'purple'; - case 'PROD': return 'green'; - default: return 'default'; - } - }; - - // Open audit page in new tab - const openAuditPage = (environmentId: string, e: React.MouseEvent) => { + // Open audit page in new tab + const openAuditPage = (environmentId: string, e: React.MouseEvent) => { e.stopPropagation(); // Prevent row click from triggering const auditUrl = `/setting/audit?environmentId=${environmentId}`; window.open(auditUrl, '_blank'); }; + // Generate background color for environment avatar + const getAvatarColor = (name: string) => { + let hash = 0; + for (let i = 0; i < name.length; i++) { + hash = name.charCodeAt(i) + ((hash << 5) - hash); + } + + const type = name.toUpperCase(); + if (type === 'PROD') return '#f5222d'; + if (type === 'PREPROD') return '#fa8c16'; + if (type === 'TEST') return '#722ed1'; + if (type === 'DEV') return '#1890ff'; + + const hue = Math.abs(hash % 360); + return `hsl(${hue}, 70%, 50%)`; + }; - // Define table columns - const columns = [ - { - title: 'Name', - dataIndex: 'environmentName', - key: 'environmentName', - render: (name: string) => name || 'Unnamed Environment', - }, - { - title: 'Domain', - dataIndex: 'environmentFrontendUrl', - key: 'environmentFrontendUrl', - render: (url: string) => url || 'No URL', - }, - { - title: 'ID', - dataIndex: 'environmentId', - key: 'environmentId', - }, - { - title: 'Stage', - dataIndex: 'environmentType', - key: 'environmentType', - render: (type: string) => ( - - {type ? type.toUpperCase() : 'UNKNOWN'} - - ), - }, - { - title: 'Master', - dataIndex: 'isMaster', - key: 'isMaster', - render: (isMaster: boolean) => ( - - {isMaster ? 'Yes' : 'No'} - - ), - }, - { - title: 'Actions', - key: 'actions', - render: (_: any, record: Environment) => ( - e.stopPropagation()}> - - - - - ), - }, - ]; + // 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 && ( + <Tooltip title="Master Environment"> + <StarFilled style={{ color: '#faad14', marginLeft: '8px', fontSize: '14px' }} /> + </Tooltip> + )} + + + {formatEnvironmentType(env.environmentType)} + +
+
+
+ +
+
+ +
+
+
+ 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()}> + + + + + + + + + ), + } + ]; + + // Stat card component + const StatCard = ({ title, value, icon }: { title: string; value: number; icon: React.ReactNode }) => ( + +
+
+
{title}
+
{value}
+
+
+ {icon} +
+
+
+ ); + + return ( +
+ {/* Header */} +
+
+ + <ThunderboltOutlined style={{ marginRight: 10 }} /> Queries + +

Manage your workspace API queries

+
+ +
+ + {/* 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" + /> +
+ + {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 }) => ( + +
+
+
{title}
+
{value}
+
+
+ {icon} +
+
+
+ ); + + // 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 */} +
+
+ + <UsergroupAddOutlined style={{ marginRight: 10 }} /> User Groups + +

Manage user groups in this environment

+
+ +
+ + {/* 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 }) => ( + +
+
+
{title}
+
{value}
+
+
+ {icon} +
+
+
+ ); + + // 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()}> + + + + + ), + }, + ]; + + return ( +
+ {/* Header */} +
+
+ + <TeamOutlined style={{ marginRight: 10 }} /> Workspaces + +

Manage workspaces in this environment

+
+ +
+ + {/* 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" + /> +
+ + {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 ( - - - - ); - }, - }; -} - - -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 ( - - - - - - ); - }, - }; -} - -// 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