diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx index f65ca4f89..b772bf373 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx @@ -1,5 +1,4 @@ import React, {useState} from "react"; -import { useParams } from "react-router-dom"; import { Spin, Typography, @@ -8,25 +7,18 @@ import { Tabs, Alert, Descriptions, - Dropdown, Menu, Button, Breadcrumb, } from "antd"; import { - ReloadOutlined, LinkOutlined, - ClusterOutlined, TeamOutlined, - UserOutlined, - SyncOutlined, EditOutlined, - EllipsisOutlined, - MoreOutlined, HomeOutlined } from "@ant-design/icons"; -import { useEnvironmentContext } from "./context/EnvironmentContext"; +import { useSingleEnvironmentContext } from "./context/SingleEnvironmentContext"; import { workspaceConfig } from "./config/workspace.config"; import { userGroupsConfig } from "./config/usergroups.config"; import DeployableItemsTab from "./components/DeployableItemsTab"; @@ -37,21 +29,18 @@ import history from "@lowcoder-ee/util/history"; const { Title, Text } = Typography; const { TabPane } = Tabs; - /** * Environment Detail Page Component * Shows detailed information about a specific environment */ const EnvironmentDetail: React.FC = () => { - // Get environment ID from URL params + // Use the SingleEnvironmentContext instead of EnvironmentContext const { environment, - isLoadingEnvironment, + isLoading, error, updateEnvironmentData - } = useEnvironmentContext(); - - + } = useSingleEnvironmentContext(); const [isEditModalVisible, setIsEditModalVisible] = useState(false); const [isUpdating, setIsUpdating] = useState(false); @@ -67,11 +56,16 @@ const EnvironmentDetail: React.FC = () => { }; // Handle save environment - const handleSaveEnvironment = async (environmentId: string, data: Partial) => { + const handleSaveEnvironment = async (data: Partial) => { + if (!environment) return; + setIsUpdating(true); try { - await updateEnvironmentData(environmentId, data); + // Close the modal first, before the update completes handleCloseModal(); + + // Then update the environment data + await updateEnvironmentData(data); } catch (error) { console.error('Failed to update environment:', error); } finally { @@ -89,7 +83,7 @@ const EnvironmentDetail: React.FC = () => { ); - if (isLoadingEnvironment) { + if (isLoading) { return (
@@ -107,6 +101,7 @@ const EnvironmentDetail: React.FC = () => { /> ); } + return (
{ {environment.environmentName} - {/* Header with environment name and controls */} {/* Header with environment name and controls */}
{ /> + {/* Edit Environment Modal */} {environment && ( { ); }; -export default EnvironmentDetail; +export default EnvironmentDetail; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/Environments.tsx b/client/packages/lowcoder/src/pages/setting/environments/Environments.tsx index a1fb13e02..41081435d 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/Environments.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/Environments.tsx @@ -1,30 +1,28 @@ +// client/packages/lowcoder/src/pages/setting/environments/Environments.tsx import React from "react"; -import { Switch, Route } from "react-router-dom"; +import { Switch, Route, useRouteMatch } from "react-router-dom"; import { EnvironmentProvider } from "./context/EnvironmentContext"; +import EnvironmentRoutes from "./routes/EnvironmentRoutes"; import EnvironmentsList from "./EnvironmentsList"; -import EnvironmentScopedRoutes from "./components/EnvironmentScopedRoutes"; - -import { - ENVIRONMENT_SETTING, - ENVIRONMENT_DETAIL -} from "@lowcoder-ee/constants/routesURL"; /** - * Top-level Environments component that wraps all environment-related routes - * with the EnvironmentProvider for shared state management + * Top-level Environments component + * Provides the EnvironmentProvider at the top level */ const Environments: React.FC = () => { + const { path } = useRouteMatch(); + return ( - {/* Route that shows the list of environments */} - + {/* Environment list route */} + - - {/* All other routes under /environments/:envId */} - - + + {/* All routes that need a specific environment */} + + diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx index 09517016d..ead872249 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx @@ -18,7 +18,7 @@ const EnvironmentsList: React.FC = () => { // Use the shared context instead of a local hook const { environments, - isLoadingEnvironments, + isLoading, error, } = useEnvironmentContext(); @@ -83,7 +83,7 @@ const EnvironmentsList: React.FC = () => { )} {/* Empty state handling */} - {!isLoadingEnvironments && environments.length === 0 && !error ? ( + {!isLoading && environments.length === 0 && !error ? ( { /* Table component */ )} diff --git a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx index 2867171b0..79b861882 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx @@ -1,14 +1,10 @@ -import React, { useEffect, useState } from "react"; -import { useParams, useHistory } from "react-router-dom"; +import React, { useState } from "react"; import history from "@lowcoder-ee/util/history"; import { Spin, Typography, Card, - Row, - Col, Tabs, - Alert, Button, Breadcrumb, Space, @@ -26,101 +22,77 @@ import { ArrowLeftOutlined, CloudUploadOutlined } from "@ant-design/icons"; -import { useEnvironmentContext } from "./context/EnvironmentContext"; + +// Use the context hooks +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 { useDeployableItems } from "./hooks/useDeployableItems"; -import { workspaceConfig } from "./config/workspace.config"; -import { useDeployModal } from "./context/DeployModalContext"; const { Title, Text } = Typography; const { TabPane } = Tabs; - - const WorkspaceDetail: React.FC = () => { + // Use the context hooks + const { environment } = useSingleEnvironmentContext(); + const { workspace, isLoading, error, toggleManagedStatus } = useWorkspaceContext(); + const { openDeployModal } = useDeployModal(); - // Get parameters from URL - const { environmentId,workspaceId } = useParams<{ - workspaceId: string; - environmentId: string; - }>(); - const { - environment, - isLoadingEnvironment: envLoading, - error: envError, - } = useEnvironmentContext(); - - const {openDeployModal} = useDeployModal(); - - // Use our generic hook with the workspace config - const { - items: workspaces, - stats: workspaceStats, - loading: workspaceLoading, - error : workspaceError, - toggleManagedStatus, - refreshItems - } = useDeployableItems( - workspaceConfig, - environment, - { workspaceId } // Additional params if needed - ); - - // Find the current workspace in the items array - const workspace = workspaces.find(w => w.id === workspaceId); + const [isToggling, setIsToggling] = useState(false); + // Handle toggle managed status const handleToggleManaged = async (checked: boolean) => { if (!workspace) return; - const success = await toggleManagedStatus(workspace, checked); - if (success) { - message.success(`Workspace is now ${checked ? 'Managed' : 'Unmanaged'}`); - } else { - message.error('Failed to change managed status'); + setIsToggling(true); + try { + const success = await toggleManagedStatus(checked); + if (success) { + message.success(`Workspace is now ${checked ? 'Managed' : 'Unmanaged'}`); + } else { + message.error('Failed to change managed status'); + } + } finally { + setIsToggling(false); } }; - if (envLoading || workspaceLoading ) { - return ( -
- -
- ); - } + if (isLoading) { + return ( +
+ +
+ ); + } - if (!environment || !workspace) { - return ( -
- Workspace not found -
- ) - } + if (error || !environment || !workspace) { + return ( +
+ + {error || "Workspace not found"} + +
+ ); + } - return ( -
+
{/* Breadcrumb navigation */} - history.push("/setting/environments")} - > + history.push("/setting/environments")}> Environments - history.push(`/setting/environments/${environmentId}`) - } + onClick={() => history.push(`/setting/environments/${environment.environmentId}`)} > {environment.environmentName} @@ -129,29 +101,14 @@ const WorkspaceDetail: React.FC = () => { {/* Workspace header with details and actions */} - -
+ +
{/* Left section - Workspace info */}
{workspace.name} -
+
ID: {workspace.id} @@ -166,8 +123,9 @@ const WorkspaceDetail: React.FC = () => {
Managed: @@ -192,9 +150,7 @@ const WorkspaceDetail: React.FC = () => { @@ -204,58 +160,35 @@ const WorkspaceDetail: React.FC = () => { {/* Tabs for Apps, Data Sources, and Queries */} - - Apps - - } - key="apps" - > + Apps} key="apps"> - {/* Update the TabPane in WorkspaceDetail.tsx */} - - Data Sources - - } - key="dataSources" - > + Data Sources} key="dataSources"> - - Queries - - } - key="queries" - > + Queries} key="queries">
); - } - +}; -export default WorkspaceDetail \ No newline at end of file +export default WorkspaceDetail; \ 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 3ff4f284d..a58ac7d78 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/DeployItemModal.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/DeployItemModal.tsx @@ -4,7 +4,6 @@ import { Modal, Form, Select, Checkbox, Button, message, Spin, Input } from 'ant import { Environment } from '../types/environment.types'; import { DeployableItem, BaseStats, DeployableItemConfig } from '../types/deployable-item.types'; import { useEnvironmentContext } from '../context/EnvironmentContext'; - interface DeployItemModalProps { visible: boolean; item: T | null; @@ -23,7 +22,7 @@ function DeployItemModal({ onSuccess }: DeployItemModalProps) { const [form] = Form.useForm(); - const { environments, isLoadingEnvironments } = useEnvironmentContext(); + const { environments, isLoading } = useEnvironmentContext(); const [deploying, setDeploying] = useState(false); useEffect(() => { @@ -34,7 +33,7 @@ function DeployItemModal({ // Filter out source environment from target list const targetEnvironments = environments.filter( - env => env.environmentId !== sourceEnvironment.environmentId + (env: Environment) => env.environmentId !== sourceEnvironment.environmentId ); const handleDeploy = async () => { @@ -76,7 +75,7 @@ function DeployItemModal({ footer={null} destroyOnClose > - {isLoadingEnvironments ? ( + {isLoading ? (
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 5c09cc42b..88bdd852f 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/EditEnvironmentModal.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/EditEnvironmentModal.tsx @@ -8,7 +8,7 @@ interface EditEnvironmentModalProps { visible: boolean; environment: Environment | null; onClose: () => void; - onSave: (environmentId: string, data: Partial) => Promise; + onSave: (data: Partial) => Promise; // Updated signature loading?: boolean; } @@ -45,7 +45,7 @@ const EditEnvironmentModal: React.FC = ({ const values = await form.validateFields(); setSubmitLoading(true); - await onSave(environment.environmentId, values); + await onSave(values); // Call with only the data parameter onClose(); } catch (error) { if (error instanceof Error) { diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentScopedRoutes.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentScopedRoutes.tsx deleted file mode 100644 index e8a04d103..000000000 --- a/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentScopedRoutes.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React, { useEffect } from "react"; -import { Switch, Route, useParams } from "react-router-dom"; -import { useEnvironmentContext } from "../context/EnvironmentContext"; -import EnvironmentDetail from "../EnvironmentDetail"; -import WorkspaceDetail from "../WorkspaceDetail"; -import { DeployModalProvider } from "../context/DeployModalContext"; - -import { - ENVIRONMENT_DETAIL, - ENVIRONMENT_WORKSPACE_DETAIL, -} from "@lowcoder-ee/constants/routesURL"; - -/** - * Component for routes scoped to a specific environment - * Uses the environment ID from the URL parameters to fetch the specific environment - */ -const EnvironmentScopedRoutes: React.FC = () => { - const { environmentId } = useParams<{ environmentId: string }>(); - const { refreshEnvironment } = useEnvironmentContext(); - - // When the environmentId changes, fetch the specific environment - useEffect(() => { - if (environmentId) { - refreshEnvironment(environmentId); - } - }, [environmentId, refreshEnvironment]); - - return ( - - - - - - - - - - - - ); -}; - -export default EnvironmentScopedRoutes; \ 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 87d15ae3b..c6d3a7dc2 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 @@ -3,8 +3,9 @@ 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 { Environment } from '../types/environment.types'; import { buildEnvironmentWorkspaceId } from '@lowcoder-ee/constants/routesURL'; -import { getMergedEnvironmentWorkspaces } from '../services/workspace.service'; +import { getMergedEnvironmentWorkspaces, deployWorkspace } from '../services/workspace.service'; import { connectManagedWorkspace, unconnectManagedWorkspace } from '../services/enterprise.service'; import { createNameColumn, @@ -153,5 +154,24 @@ export const workspaceConfig: DeployableItemConfig = tooltip: 'View audit logs for this workspace', getAuditUrl: (item, environment) => `/setting/audit?environmentId=${environment.environmentId}&orgId=${item.id}&pageSize=100&pageNum=1` + }, + + // Deploy configuration + deploy: { + enabled: true, + fields: [], + prepareParams: (item: Workspace, values: any, sourceEnv: Environment, targetEnv: Environment) => { + if (!item.gid) { + console.error('Missing workspace.gid when deploying workspace:', item); + } + console.log('item.gid', item.gid); + + return { + envId: sourceEnv.environmentId, + targetEnvId: targetEnv.environmentId, + workspaceId: item.gid + }; + }, + execute: (params: any) => deployWorkspace(params) } }; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/context/EnvironmentContext.tsx b/client/packages/lowcoder/src/pages/setting/environments/context/EnvironmentContext.tsx index f8120ff71..9f9c7ec3c 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/context/EnvironmentContext.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/context/EnvironmentContext.tsx @@ -1,156 +1,91 @@ -import React, { - createContext, - useContext, - useEffect, - useState, - useCallback, - ReactNode, -} from "react"; -import { message } from "antd"; -import { - getEnvironmentById, - getEnvironments, - updateEnvironment, -} from "../services/environments.service"; -import { Environment } from "../types/environment.types"; - -interface EnvironmentContextState { - // Environment data - environment: Environment | null; - environments: Environment[]; - - // Loading states - isLoadingEnvironment: boolean; - isLoadingEnvironments: boolean; - - // Error state - error: string | null; - - // Functions - refreshEnvironment: (envId?: string) => Promise; - refreshEnvironments: () => Promise; - updateEnvironmentData: (envId: string, data: Partial) => Promise; -} - -const EnvironmentContext = createContext(undefined); - -export const useEnvironmentContext = () => { - const context = useContext(EnvironmentContext); - if (!context) { - throw new Error( - "useEnvironmentContext must be used within an EnvironmentProvider" - ); - } - return context; -}; - -interface ProviderProps { - children: ReactNode; -} - -export const EnvironmentProvider: React.FC = ({ - children, -}) => { - // State for environment data - const [environment, setEnvironment] = useState(null); - const [environments, setEnvironments] = useState([]); - - // Loading states - const [isLoadingEnvironment, setIsLoadingEnvironment] = useState(false); - const [isLoadingEnvironments, setIsLoadingEnvironments] = useState(true); - - // Error state - const [error, setError] = useState(null); - - // Function to fetch a specific environment by ID - const fetchEnvironment = useCallback(async (environmentId?: string) => { - // Only fetch if we have an environment ID - if (!environmentId) { - setEnvironment(null); - return; - } - - setIsLoadingEnvironment(true); - setError(null); - - try { - const data = await getEnvironmentById(environmentId); - console.log("Environment data:", data); - setEnvironment(data); - } catch (err) { - const errorMessage = err instanceof Error ? err.message : "Environment not found or failed to load"; - setError(errorMessage); - } finally { - setIsLoadingEnvironment(false); - } - }, []); - - // Function to fetch all environments - const fetchEnvironments = useCallback(async () => { - setIsLoadingEnvironments(true); - setError(null); - - try { - const data = await getEnvironments(); - console.log("Environments data:", data); - setEnvironments(data); - } catch (err) { - const errorMessage = err instanceof Error ? err.message : "Failed to load environments list"; - setError(errorMessage); - } finally { - setIsLoadingEnvironments(false); - } - }, []); - - // Function to update an environment -// Function to update an environment -const updateEnvironmentData = useCallback(async ( - environmentId: string, - data: Partial -): Promise => { - try { - const updatedEnv = await updateEnvironment(environmentId, data); - - // Show success message - message.success("Environment updated successfully"); - - // Refresh the environments list - fetchEnvironments(); - - // If we're viewing a single environment and it's the one we updated, - // refresh that environment data as well - if (environment && environment.environmentId === environmentId) { - fetchEnvironment(environmentId); - } - - return updatedEnv; - } catch (err) { - const errorMessage = err instanceof Error ? err.message : "Failed to update environment"; - message.error(errorMessage); - throw err; - } -}, [environment, fetchEnvironment, fetchEnvironments]); - - // Initial data loading - just fetch environments list - useEffect(() => { - fetchEnvironments(); - }, [fetchEnvironments]); - - // Create the context value - const value: EnvironmentContextState = { - environment, - environments, - isLoadingEnvironment, - isLoadingEnvironments, - error, - refreshEnvironment: fetchEnvironment, - refreshEnvironments: fetchEnvironments, - updateEnvironmentData, - }; - - return ( - - {children} - - ); +// client/packages/lowcoder/src/pages/setting/environments/context/EnvironmentContext.tsx +import React, { + createContext, + useContext, + useEffect, + useState, + useCallback, + ReactNode, +} from "react"; +import { message } from "antd"; +import { getEnvironments } from "../services/environments.service"; +import { Environment } from "../types/environment.types"; + +interface EnvironmentContextState { + // Environments list data + environments: Environment[]; + + // Loading state + isLoading: boolean; + + // Error state + error: string | null; + + // Functions + refreshEnvironments: () => Promise; +} + +const EnvironmentContext = createContext(undefined); + +export const useEnvironmentContext = () => { + const context = useContext(EnvironmentContext); + if (!context) { + throw new Error( + "useEnvironmentContext must be used within an EnvironmentProvider" + ); + } + return context; +}; + +interface ProviderProps { + children: ReactNode; +} + +export const EnvironmentProvider: React.FC = ({ + children, +}) => { + // State for environments list + const [environments, setEnvironments] = useState([]); + + // Loading state + const [isLoading, setIsLoading] = useState(true); + + // Error state + const [error, setError] = useState(null); + + // Function to fetch all environments + const fetchEnvironments = useCallback(async () => { + setIsLoading(true); + setError(null); + + try { + const data = await getEnvironments(); + setEnvironments(data); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : "Failed to load environments list"; + setError(errorMessage); + message.error(errorMessage); + } finally { + setIsLoading(false); + } + }, []); + + // Initial data loading + useEffect(() => { + fetchEnvironments(); + }, [fetchEnvironments]); + + // Create the context value + const value: EnvironmentContextState = { + environments, + isLoading, + error, + refreshEnvironments: fetchEnvironments, + }; + + return ( + + {children} + + ); }; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/context/SingleEnvironmentContext.tsx b/client/packages/lowcoder/src/pages/setting/environments/context/SingleEnvironmentContext.tsx new file mode 100644 index 000000000..bd93bf9f2 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/context/SingleEnvironmentContext.tsx @@ -0,0 +1,138 @@ +// client/packages/lowcoder/src/pages/setting/environments/context/SingleEnvironmentContext.tsx +import React, { + createContext, + useContext, + useEffect, + useState, + useCallback, + ReactNode, + } from "react"; + import { message } from "antd"; + import { useParams } from "react-router-dom"; + import { getEnvironmentById, updateEnvironment } from "../services/environments.service"; + import { Environment } from "../types/environment.types"; + import { useEnvironmentContext } from './EnvironmentContext'; + + interface SingleEnvironmentContextState { + // Environment data + environment: Environment | null; + + // Loading states + isLoading: boolean; + + // Error state + error: string | null; + + // Functions + refreshEnvironment: () => Promise; + updateEnvironmentData: (data: Partial) => Promise; + } + + const SingleEnvironmentContext = createContext(undefined); + + export const useSingleEnvironmentContext = () => { + const context = useContext(SingleEnvironmentContext); + if (!context) { + throw new Error( + "useSingleEnvironmentContext must be used within a SingleEnvironmentProvider" + ); + } + return context; + }; + + interface ProviderProps { + children: ReactNode; + environmentId?: string; // Optional prop-based ID + } + + export const SingleEnvironmentProvider: React.FC = ({ + children, + environmentId: propEnvironmentId, + }) => { + // Get environmentId from URL params if not provided as prop + const { envId } = useParams<{ envId: string }>(); + const environmentId = propEnvironmentId || envId; + + // Access the environments context to refresh the list + const { refreshEnvironments } = useEnvironmentContext(); + + // State for environment data + const [environment, setEnvironment] = useState(null); + + // Loading states + const [isLoading, setIsLoading] = useState(true); + + // Error state + const [error, setError] = useState(null); + + // Function to fetch environment by ID + const fetchEnvironment = useCallback(async () => { + // Only fetch if we have an environment ID + if (!environmentId) { + setEnvironment(null); + setIsLoading(false); + return; + } + + setIsLoading(true); + setError(null); + + try { + const data = await getEnvironmentById(environmentId); + setEnvironment(data); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : "Environment not found or failed to load"; + setError(errorMessage); + } finally { + setIsLoading(false); + } + }, [environmentId]); + + // Function to update the environment + const updateEnvironmentData = useCallback(async ( + data: Partial + ): Promise => { + if (!environmentId || !environment) { + throw new Error("No environment selected"); + } + + try { + const updatedEnv = await updateEnvironment(environmentId, data); + + // Show success message + message.success("Environment updated successfully"); + + // Refresh both the single environment and environments list + await Promise.all([ + fetchEnvironment(), // Refresh the current environment + refreshEnvironments() // Refresh the environments list + ]); + + return updatedEnv; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : "Failed to update environment"; + message.error(errorMessage); + throw err; + } + }, [environment, environmentId, fetchEnvironment, refreshEnvironments]); + + // Load environment data when the component mounts or environmentId changes + useEffect(() => { + fetchEnvironment(); + }, [fetchEnvironment]); + + // Create the context value + const value: SingleEnvironmentContextState = { + environment, + isLoading, + error, + refreshEnvironment: fetchEnvironment, + updateEnvironmentData, + }; + + return ( + + {children} + + ); + }; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/context/WorkspaceContext.tsx b/client/packages/lowcoder/src/pages/setting/environments/context/WorkspaceContext.tsx new file mode 100644 index 000000000..72c7ef356 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/context/WorkspaceContext.tsx @@ -0,0 +1,156 @@ +// client/packages/lowcoder/src/pages/setting/environments/context/WorkspaceContext.tsx +import React, { + createContext, + useContext, + useEffect, + useState, + useCallback, + ReactNode, + } from "react"; + import { message } from "antd"; + import { useParams } from "react-router-dom"; + import { useSingleEnvironmentContext } from "./SingleEnvironmentContext"; + import { fetchWorkspaceById } from "../services/environments.service"; + import { Workspace } from "../types/workspace.types"; + import { getManagedWorkspaces, connectManagedWorkspace, unconnectManagedWorkspace } from "../services/enterprise.service"; + + interface WorkspaceContextState { + // Workspace data + workspace: Workspace | null; + + // Loading states + isLoading: boolean; + + // Error state + error: string | null; + + // Functions + refreshWorkspace: () => Promise; + toggleManagedStatus: (checked: boolean) => Promise; + } + + const WorkspaceContext = createContext(undefined); + + export const useWorkspaceContext = () => { + const context = useContext(WorkspaceContext); + if (!context) { + throw new Error("useWorkspaceContext must be used within a WorkspaceProvider"); + } + return context; + }; + + interface ProviderProps { + children: ReactNode; + workspaceId?: string; + } + + export const WorkspaceProvider: React.FC = ({ + children, + workspaceId: propWorkspaceId, + }) => { + // Get workspaceId from URL params if not provided as prop + const { workspaceId: urlWorkspaceId } = useParams<{ workspaceId: string }>(); + const workspaceId = propWorkspaceId || urlWorkspaceId; + + // Get the environment context to access environment data + const { environment } = useSingleEnvironmentContext(); + + // State for workspace data + const [workspace, setWorkspace] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + // Function to fetch workspace by ID + const fetchWorkspace = useCallback(async () => { + // Only fetch if we have a workspace ID and environment + if (!workspaceId || !environment) { + setWorkspace(null); + setIsLoading(false); + return; + } + + setIsLoading(true); + setError(null); + + try { + // Fetch the workspace data + const workspaceData = await fetchWorkspaceById( + environment.environmentId, + workspaceId, + environment.environmentApikey, + environment.environmentApiServiceUrl! + ); + + if (!workspaceData) { + throw new Error("Workspace not found"); + } + + // Fetch managed workspaces to check if this one is managed + const managedWorkspaces = await getManagedWorkspaces(environment.environmentId); + + // Set the managed status + const isManaged = managedWorkspaces.some(org => org.orgGid === workspaceData.gid); + + // Update the workspace with managed status + setWorkspace({ + ...workspaceData, + managed: isManaged + }); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : "Workspace not found or failed to load"; + setError(errorMessage); + } finally { + setIsLoading(false); + } + }, [workspaceId, environment]); + + // Function to toggle managed status + const toggleManagedStatus = useCallback(async (checked: boolean): Promise => { + if (!workspace || !environment) { + return false; + } + + try { + if (checked) { + // Connect the workspace as managed + await connectManagedWorkspace( + environment.environmentId, + workspace.name, + workspace.gid! + ); + } else { + // Disconnect the managed workspace + await unconnectManagedWorkspace(workspace.gid!); + } + + // Update local state + setWorkspace(prev => prev ? { ...prev, managed: checked } : null); + + return true; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : "Failed to update managed status"; + message.error(errorMessage); + return false; + } + }, [workspace, environment]); + + // Load workspace data when the component mounts or dependencies change + useEffect(() => { + fetchWorkspace(); + }, [fetchWorkspace]); + + // Create the context value + const value: WorkspaceContextState = { + workspace, + isLoading, + error, + refreshWorkspace: fetchWorkspace, + toggleManagedStatus + }; + + return ( + + {children} + + ); + }; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/routes/EnvironmentRoutes.tsx b/client/packages/lowcoder/src/pages/setting/environments/routes/EnvironmentRoutes.tsx new file mode 100644 index 000000000..69e2addab --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/routes/EnvironmentRoutes.tsx @@ -0,0 +1,35 @@ +// client/packages/lowcoder/src/pages/setting/environments/routes/EnvironmentRoutes.tsx +import React from "react"; +import { Switch, Route, useRouteMatch } from "react-router-dom"; +import { SingleEnvironmentProvider } from "../context/SingleEnvironmentContext"; +import { DeployModalProvider } from "../context/DeployModalContext"; +import EnvironmentDetail from "../EnvironmentDetail"; +import WorkspaceRoutes from "./WorkspaceRoutes"; + +/** + * Routes that require a specific environment + * Provides the SingleEnvironmentProvider for all child routes + */ +const EnvironmentRoutes: React.FC = () => { + const { path } = useRouteMatch(); + + return ( + + + + {/* Environment detail route */} + + + + + {/* All routes that need a specific workspace */} + + + + + + + ); +}; + +export default EnvironmentRoutes; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/routes/WorkspaceRoutes.tsx b/client/packages/lowcoder/src/pages/setting/environments/routes/WorkspaceRoutes.tsx new file mode 100644 index 000000000..bc5f7a7b5 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/routes/WorkspaceRoutes.tsx @@ -0,0 +1,28 @@ +// client/packages/lowcoder/src/pages/setting/environments/routes/WorkspaceRoutes.tsx +import React from "react"; +import { Switch, Route, useRouteMatch } from "react-router-dom"; +import { WorkspaceProvider } from "../context/WorkspaceContext"; +import WorkspaceDetail from "../WorkspaceDetail"; + +/** + * Routes that require a specific workspace + * Provides the WorkspaceProvider for all child routes + */ +const WorkspaceRoutes: React.FC = () => { + const { path } = useRouteMatch(); + + return ( + + + {/* Workspace detail route */} + + + + + {/* You can add more workspace-specific routes here */} + + + ); +}; + +export default WorkspaceRoutes; \ 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 8c4c8785b..52528b4d0 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 @@ -109,13 +109,13 @@ export async function getMergedWorkspaceApps( export const deployApp = async (params: DeployAppParams): Promise => { try { const response = await axios.post( - `/api/plugins/enterprise/deploy`, + `/api/plugins/enterprise/app/deploy`, null, { params: { + applicationId: params.applicationId, envId: params.envId, targetEnvId: params.targetEnvId, - applicationId: params.applicationId, updateDependenciesIfNeeded: params.updateDependenciesIfNeeded ?? false, publishOnTarget: params.publishOnTarget ?? false, publicToAll: params.publicToAll ?? false, @@ -126,7 +126,8 @@ export const deployApp = async (params: DeployAppParams): Promise => { return response.status === 200; } catch (error) { - console.error('Error deploying app:', error); - throw error; + const errorMessage = error instanceof Error ? error.message : 'Failed to deploy app'; + // Don't show message directly, let the calling component handle it + throw new Error(errorMessage); } }; \ No newline at end of file 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 c56b978b5..43bc302a2 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 @@ -4,6 +4,7 @@ import { getEnvironmentWorkspaces } from "./environments.service"; import { getManagedWorkspaces } from "./enterprise.service"; import { Workspace } from "../types/workspace.types"; import { ManagedOrg } from "../types/enterprise.types"; +import axios from "axios"; export interface WorkspaceStats { total: number; @@ -73,4 +74,31 @@ export async function getMergedEnvironmentWorkspaces( message.error(errorMessage); throw error; } +} + +/** + * Deploy a workspace to another environment + * @param params Deployment parameters + * @returns Promise with boolean indicating success + */ +export async function deployWorkspace(params: { + envId: string; + targetEnvId: string; + workspaceId: string; +}): Promise { + try { + // Use the new endpoint format with only essential parameters + const response = await axios.post('/api/plugins/enterprise/org/deploy', null, { + params: { + orgGid: params.workspaceId, // Using workspaceId as orgGid + envId: params.envId, + targetEnvId: params.targetEnvId + } + }); + return response.status === 200; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Failed to deploy workspace'; + // Don't show message directly, let the calling component handle it + throw new Error(errorMessage); + } } \ No newline at end of file