From 349fdf120d640b2262b689c79df32332c1367974 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 10 Apr 2025 15:53:16 +0500 Subject: [PATCH 01/68] Add Env Listing Page Structure --- .../setting/environments/Environments.tsx | 139 +++++++++++++++++- .../environments/hooks/useEnvironments.ts | 65 ++++++++ .../services/environments.service.ts | 42 ++++++ .../environments/types/environment.types.ts | 17 +++ .../src/pages/setting/settingHome.tsx | 2 +- 5 files changed, 261 insertions(+), 4 deletions(-) create mode 100644 client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironments.ts create mode 100644 client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts create mode 100644 client/packages/lowcoder/src/pages/setting/environments/types/environment.types.ts diff --git a/client/packages/lowcoder/src/pages/setting/environments/Environments.tsx b/client/packages/lowcoder/src/pages/setting/environments/Environments.tsx index 61a73fe24d..864e615e5d 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/Environments.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/Environments.tsx @@ -1,3 +1,136 @@ -export function Environments() { - return <>; -} +import React, { useState } from 'react'; +import { Table, Typography, Alert, Input, Button, Space, Empty } from 'antd'; +import { SearchOutlined, ReloadOutlined } from '@ant-design/icons'; +import { useHistory } from 'react-router-dom'; +import { useEnvironments } from './hooks/useEnvironments'; +import { Environment } from './types/environment.types'; + +const { Title } = Typography; + +/** + * Environment Listing Page Component + * Displays a basic table of environments + */ +const Environments: React.FC = () => { + // Use our custom hook to get environments data and states + const { environments, loading, error, refresh } = useEnvironments(); + + // State for search input + const [searchText, setSearchText] = useState(''); + + // Hook for navigation (using history instead of navigate) + const history = useHistory(); + + // Filter environments based on search text + const filteredEnvironments = environments.filter(env => { + const searchLower = searchText.toLowerCase(); + return ( + (env.environmentName || '').toLowerCase().includes(searchLower) || + (env.environmentFrontendUrl || '').toLowerCase().includes(searchLower) || + env.environmentId.toLowerCase().includes(searchLower) || + env.environmentType.toLowerCase().includes(searchLower) + ); + }); + + // Define table columns - updated to match the actual data structure + 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', + }, + { + title: 'Master', + dataIndex: 'isMaster', + key: 'isMaster', + render: (isMaster: boolean) => isMaster ? 'Yes' : 'No', + }, + ]; + + // Handle row click to navigate to environment detail + const handleRowClick = (record: Environment) => { + history.push(`/home/settings/environments/${record.environmentId}`); + }; + + return ( +
+ {/* Header section with title and controls */} +
+ Environments + + setSearchText(e.target.value)} + style={{ width: 250 }} + prefix={} + allowClear + /> + + +
+ + {/* Error handling */} + {error && ( + + )} + + {/* Empty state handling */} + {!loading && environments.length === 0 && !error ? ( + + ) : ( + /* Table component */ + ({ + onClick: () => handleRowClick(record), + style: { cursor: 'pointer' } + })} + /> + )} + + ); +}; + +export default Environments; diff --git a/client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironments.ts b/client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironments.ts new file mode 100644 index 0000000000..b125e4125c --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironments.ts @@ -0,0 +1,65 @@ +import { useState, useEffect, useCallback } from 'react'; +import { Environment } from '../types/environment.types'; +import { getEnvironments } from '../services/environments.service'; + +/** + * Interface for the state managed by this hook + */ +interface EnvironmentsState { + environments: Environment[]; + loading: boolean; + error: string | null; +} + +/** + * Custom hook for fetching and managing environments data + * @returns Object containing environments data, loading state, error state, and refresh function + */ +export const useEnvironments = () => { + // Initialize state with loading true + const [state, setState] = useState({ + environments: [], + loading: true, + error: null, + }); + + /** + * Function to fetch environments from the API + */ + const fetchEnvironments = useCallback(async () => { + // Set loading state + setState(prev => ({ ...prev, loading: true, error: null })); + + try { + // Call the API service + const environments = await getEnvironments(); + + // Update state with fetched data + setState({ + environments, + loading: false, + error: null, + }); + } catch (error) { + // Handle error state + setState(prev => ({ + ...prev, + loading: false, + error: error instanceof Error ? error.message : 'An unknown error occurred', + })); + } + }, []); + + // Fetch environments on component mount + useEffect(() => { + fetchEnvironments(); + }, [fetchEnvironments]); + + // Return state values and refresh function + return { + environments: state.environments, + loading: state.loading, + error: state.error, + refresh: fetchEnvironments + }; +}; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts b/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts new file mode 100644 index 0000000000..785e694a35 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts @@ -0,0 +1,42 @@ +import axios from 'axios'; +import { message } from 'antd'; +import { Environment } from '../types/environment.types'; + +/** + * Fetch all environments + * @returns Promise with environments data + */ +export async function getEnvironments(): Promise { + try { + // The response contains the data array directly in response.data + const response = await axios.get('/api/plugins/enterprise/environments/list'); + + // Return the data array directly from response.data + return response.data || []; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Failed to fetch environments'; + message.error(errorMessage); + throw error; + } +} + +/** + * Fetch a single environment by ID + * @param id Environment ID + * @returns Promise with environment data + */ +export async function getEnvironmentById(id: string): Promise { + try { + const response = await axios.get(`/api/plugins/enterprise/environments/${id}`); + + if (!response.data) { + throw new Error('Failed to fetch environment'); + } + + return response.data; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Failed to fetch environment'; + message.error(errorMessage); + throw error; + } +} \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/types/environment.types.ts b/client/packages/lowcoder/src/pages/setting/environments/types/environment.types.ts new file mode 100644 index 0000000000..39766c1eae --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/types/environment.types.ts @@ -0,0 +1,17 @@ +/** + * Interface representing an Environment entity + */ +export interface Environment { + environmentId: string; + environmentName?: string; + environmentDescription?: string; + environmentIcon?: string; + environmentType: string; + environmentApiServiceUrl?: string; + environmentNodeServiceUrl?: string; + environmentFrontendUrl?: string; + environmentApikey: string; + isMaster: boolean; + createdAt: string; + updatedAt: string; +} \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/settingHome.tsx b/client/packages/lowcoder/src/pages/setting/settingHome.tsx index c98d6540df..8eabd126aa 100644 --- a/client/packages/lowcoder/src/pages/setting/settingHome.tsx +++ b/client/packages/lowcoder/src/pages/setting/settingHome.tsx @@ -25,7 +25,7 @@ import { getUser } from "redux/selectors/usersSelectors"; import history from "util/history"; import { useParams } from "react-router-dom"; import { BrandingSetting } from "@lowcoder-ee/pages/setting/branding/BrandingSetting"; -import { Environments } from "@lowcoder-ee/pages/setting/environments/Environments"; +import Environments from "@lowcoder-ee/pages/setting/environments/Environments"; import { AppUsage } from "@lowcoder-ee/pages/setting/appUsage"; import { AuditLog } from "@lowcoder-ee/pages/setting/audit"; import { IdSourceHome } from "@lowcoder-ee/pages/setting/idSource"; From cc015e6215cce52b28ced4e48211742eea1f11ac Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 10 Apr 2025 16:06:10 +0500 Subject: [PATCH 02/68] Add routing structure for Environments --- .../lowcoder/src/constants/routesURL.ts | 8 + .../setting/environments/Environments.tsx | 138 ++---------------- .../setting/environments/EnvironmentsList.tsx | 136 +++++++++++++++++ 3 files changed, 154 insertions(+), 128 deletions(-) create mode 100644 client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx diff --git a/client/packages/lowcoder/src/constants/routesURL.ts b/client/packages/lowcoder/src/constants/routesURL.ts index 6bec5c190a..a675ec6495 100644 --- a/client/packages/lowcoder/src/constants/routesURL.ts +++ b/client/packages/lowcoder/src/constants/routesURL.ts @@ -24,6 +24,10 @@ export const AUDIT_LOG_DETAIL = "/setting/audit/:eventId/detail"; export const APP_USAGE_DASHBOARD = "/setting/app-usage"; export const APP_USAGE_DETAIL = "/setting/app-usage/:eventId/detail"; +export const ENVIRONMENT_SETTING = "/setting/environments"; +export const ENVIRONMENT_DETAIL = `${ENVIRONMENT_SETTING}/:environmentId`; +export const ENVIRONMENT_WORKSPACE_DETAIL = `${ENVIRONMENT_DETAIL}/workspaces/:workspaceId`; + export const OAUTH_PROVIDER_SETTING = "/setting/oauth-provider"; export const OAUTH_PROVIDER_DETAIL = "/setting/oauth-provider/detail"; @@ -120,3 +124,7 @@ export const buildSubscriptionSettingsLink = (subscriptionId: string, productId export const buildSubscriptionInfoLink = (productId: string) => `${SUBSCRIPTION_SETTING}/info/${productId}`; export const buildSupportTicketLink = (ticketId: string) => `${SUPPORT_URL}/details/${ticketId}`; + +export const buildEnvironmentId = (environmentId: string) => `${ENVIRONMENT_SETTING}/${environmentId}`; +export const buildEnvironmentWorkspaceId = (environmentId: string, workspaceId: string) => + `${ENVIRONMENT_SETTING}/${environmentId}/workspaces/${workspaceId}`; diff --git a/client/packages/lowcoder/src/pages/setting/environments/Environments.tsx b/client/packages/lowcoder/src/pages/setting/environments/Environments.tsx index 864e615e5d..bea8d1250a 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/Environments.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/Environments.tsx @@ -1,135 +1,17 @@ -import React, { useState } from 'react'; -import { Table, Typography, Alert, Input, Button, Space, Empty } from 'antd'; -import { SearchOutlined, ReloadOutlined } from '@ant-design/icons'; -import { useHistory } from 'react-router-dom'; -import { useEnvironments } from './hooks/useEnvironments'; -import { Environment } from './types/environment.types'; +// environments/Environments.tsx +import React from "react"; +import { Switch, Route, useRouteMatch } from "react-router-dom"; +import EnvironmentsList from "./EnvironmentsList"; // Rename your current component -const { Title } = Typography; +import { ENVIRONMENT_SETTING } from "@lowcoder-ee/constants/routesURL"; -/** - * Environment Listing Page Component - * Displays a basic table of environments - */ const Environments: React.FC = () => { - // Use our custom hook to get environments data and states - const { environments, loading, error, refresh } = useEnvironments(); - - // State for search input - const [searchText, setSearchText] = useState(''); - - // Hook for navigation (using history instead of navigate) - const history = useHistory(); - - // Filter environments based on search text - const filteredEnvironments = environments.filter(env => { - const searchLower = searchText.toLowerCase(); - return ( - (env.environmentName || '').toLowerCase().includes(searchLower) || - (env.environmentFrontendUrl || '').toLowerCase().includes(searchLower) || - env.environmentId.toLowerCase().includes(searchLower) || - env.environmentType.toLowerCase().includes(searchLower) - ); - }); - - // Define table columns - updated to match the actual data structure - 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', - }, - { - title: 'Master', - dataIndex: 'isMaster', - key: 'isMaster', - render: (isMaster: boolean) => isMaster ? 'Yes' : 'No', - }, - ]; - - // Handle row click to navigate to environment detail - const handleRowClick = (record: Environment) => { - history.push(`/home/settings/environments/${record.environmentId}`); - }; - return ( -
- {/* Header section with title and controls */} -
- Environments - - setSearchText(e.target.value)} - style={{ width: 250 }} - prefix={} - allowClear - /> - - -
- - {/* Error handling */} - {error && ( - - )} - - {/* Empty state handling */} - {!loading && environments.length === 0 && !error ? ( - - ) : ( - /* Table component */ -
({ - onClick: () => handleRowClick(record), - style: { cursor: 'pointer' } - })} - /> - )} - + + + + + ); }; diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx new file mode 100644 index 0000000000..713a2b251b --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx @@ -0,0 +1,136 @@ +import React, { useState } from 'react'; +import { Table, Typography, Alert, Input, Button, Space, Empty } from 'antd'; +import { SearchOutlined, ReloadOutlined } from '@ant-design/icons'; +import { useHistory } from 'react-router-dom'; +import { useEnvironments } from './hooks/useEnvironments'; +import { Environment } from './types/environment.types'; + +const { Title } = Typography; + +/** + * Environment Listing Page Component + * Displays a basic table of environments + */ +const EnvironmentsList: React.FC = () => { + // Use our custom hook to get environments data and states + const { environments, loading, error, refresh } = useEnvironments(); + + // State for search input + const [searchText, setSearchText] = useState(''); + + // Hook for navigation (using history instead of navigate) + const history = useHistory(); + + // Filter environments based on search text + const filteredEnvironments = environments.filter(env => { + const searchLower = searchText.toLowerCase(); + return ( + (env.environmentName || '').toLowerCase().includes(searchLower) || + (env.environmentFrontendUrl || '').toLowerCase().includes(searchLower) || + env.environmentId.toLowerCase().includes(searchLower) || + env.environmentType.toLowerCase().includes(searchLower) + ); + }); + + // Define table columns - updated to match the actual data structure + 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', + }, + { + title: 'Master', + dataIndex: 'isMaster', + key: 'isMaster', + render: (isMaster: boolean) => isMaster ? 'Yes' : 'No', + }, + ]; + + // Handle row click to navigate to environment detail + const handleRowClick = (record: Environment) => { + history.push(`/home/settings/environments/${record.environmentId}`); + }; + + return ( +
+ {/* Header section with title and controls */} +
+ Environments + + setSearchText(e.target.value)} + style={{ width: 250 }} + prefix={} + allowClear + /> + + +
+ + {/* Error handling */} + {error && ( + + )} + + {/* Empty state handling */} + {!loading && environments.length === 0 && !error ? ( + + ) : ( + /* Table component */ +
({ + onClick: () => handleRowClick(record), + style: { cursor: 'pointer' } + })} + /> + )} + + ); +}; + +export default EnvironmentsList; \ No newline at end of file From 27451ab5eae40e88013697340c63fc0e655763bb Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 10 Apr 2025 16:18:30 +0500 Subject: [PATCH 03/68] Add Environments Table component --- .../setting/environments/EnvironmentsList.tsx | 104 +++++++++--------- .../components/EnvironmentsTable.tsx | 96 ++++++++++++++++ 2 files changed, 145 insertions(+), 55 deletions(-) create mode 100644 client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentsTable.tsx diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx index 713a2b251b..5af0a5c3db 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx @@ -1,9 +1,10 @@ -import React, { useState } from 'react'; -import { Table, Typography, Alert, Input, Button, Space, Empty } from 'antd'; -import { SearchOutlined, ReloadOutlined } from '@ant-design/icons'; -import { useHistory } from 'react-router-dom'; -import { useEnvironments } from './hooks/useEnvironments'; -import { Environment } from './types/environment.types'; +import React, { useState } from "react"; +import { Table, Typography, Alert, Input, Button, Space, Empty } from "antd"; +import { SearchOutlined, ReloadOutlined } from "@ant-design/icons"; +import { useHistory } from "react-router-dom"; +import { useEnvironments } from "./hooks/useEnvironments"; +import { Environment } from "./types/environment.types"; +import EnvironmentsTable from "./components/EnvironmentsTable"; const { Title } = Typography; @@ -14,19 +15,19 @@ const { Title } = Typography; const EnvironmentsList: React.FC = () => { // Use our custom hook to get environments data and states const { environments, loading, error, refresh } = useEnvironments(); - + // State for search input - const [searchText, setSearchText] = useState(''); - + const [searchText, setSearchText] = useState(""); + // Hook for navigation (using history instead of navigate) const history = useHistory(); // Filter environments based on search text - const filteredEnvironments = environments.filter(env => { + const filteredEnvironments = environments.filter((env) => { const searchLower = searchText.toLowerCase(); return ( - (env.environmentName || '').toLowerCase().includes(searchLower) || - (env.environmentFrontendUrl || '').toLowerCase().includes(searchLower) || + (env.environmentName || "").toLowerCase().includes(searchLower) || + (env.environmentFrontendUrl || "").toLowerCase().includes(searchLower) || env.environmentId.toLowerCase().includes(searchLower) || env.environmentType.toLowerCase().includes(searchLower) ); @@ -35,64 +36,63 @@ const EnvironmentsList: React.FC = () => { // Define table columns - updated to match the actual data structure const columns = [ { - title: 'Name', - dataIndex: 'environmentName', - key: 'environmentName', - render: (name: string) => name || 'Unnamed Environment', + 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: "Domain", + dataIndex: "environmentFrontendUrl", + key: "environmentFrontendUrl", + render: (url: string) => url || "No URL", }, { - title: 'ID', - dataIndex: 'environmentId', - key: 'environmentId', + title: "ID", + dataIndex: "environmentId", + key: "environmentId", }, { - title: 'Stage', - dataIndex: 'environmentType', - key: 'environmentType', + title: "Stage", + dataIndex: "environmentType", + key: "environmentType", }, { - title: 'Master', - dataIndex: 'isMaster', - key: 'isMaster', - render: (isMaster: boolean) => isMaster ? 'Yes' : 'No', + title: "Master", + dataIndex: "isMaster", + key: "isMaster", + render: (isMaster: boolean) => (isMaster ? "Yes" : "No"), }, ]; - + // Handle row click to navigate to environment detail const handleRowClick = (record: Environment) => { history.push(`/home/settings/environments/${record.environmentId}`); }; return ( -
+
{/* Header section with title and controls */} -
+
Environments setSearchText(e.target.value)} + onChange={(e) => setSearchText(e.target.value)} style={{ width: 250 }} prefix={} allowClear /> - @@ -105,7 +105,7 @@ const EnvironmentsList: React.FC = () => { description={error} type="error" showIcon - style={{ marginBottom: '24px' }} + style={{ marginBottom: "24px" }} /> )} @@ -117,20 +117,14 @@ const EnvironmentsList: React.FC = () => { /> ) : ( /* Table component */ -
({ - onClick: () => handleRowClick(record), - style: { cursor: 'pointer' } - })} + onRowClick={handleRowClick} /> )} ); }; -export default EnvironmentsList; \ No newline at end of file +export default EnvironmentsList; diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentsTable.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentsTable.tsx new file mode 100644 index 0000000000..1e73260579 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentsTable.tsx @@ -0,0 +1,96 @@ +import React from 'react'; +import { Table, Tag } from 'antd'; +import { Environment } from '../types/environment.types'; + +interface EnvironmentsTableProps { + environments: Environment[]; + loading: boolean; + onRowClick: (record: Environment) => void; +} + +/** + * Table component for displaying environments + */ +const EnvironmentsTable: React.FC = ({ + environments, + loading, + onRowClick +}) => { + // Get color for environment type/stage + const getTypeColor = (type: string): string => { + switch (type.toUpperCase()) { + case 'DEV': return 'blue'; + case 'TEST': return 'orange'; + case 'PROD': return 'green'; + default: return 'default'; + } + }; + + // 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.toUpperCase()} + + ), + }, + { + title: 'Master', + dataIndex: 'isMaster', + key: 'isMaster', + render: (isMaster: boolean) => ( + + {isMaster ? 'Yes' : 'No'} + + ), + }, + ]; + + return ( +
({ + onClick: () => onRowClick(record), + style: { + cursor: 'pointer', + transition: 'background-color 0.3s', + ':hover': { + backgroundColor: '#f5f5f5', + } + } + })} + rowClassName={() => 'environment-row'} + /> + ); +}; + +export default EnvironmentsTable; \ No newline at end of file From 4be0dadf52680715d1986d92d88899caf8e3a2f5 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 10 Apr 2025 17:27:30 +0500 Subject: [PATCH 04/68] Add Env Detail Page --- .../environments/EnvironmentDetail.tsx | 233 ++++++++++++++++++ .../setting/environments/Environments.tsx | 8 +- .../setting/environments/EnvironmentsList.tsx | 3 +- .../services/environments.service.ts | 2 +- 4 files changed, 243 insertions(+), 3 deletions(-) create mode 100644 client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx new file mode 100644 index 0000000000..34fe98348d --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx @@ -0,0 +1,233 @@ +import React, { useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; +import { + Spin, + Typography, + Card, + Row, + Col, + Tag, + Tabs, + Alert, + Descriptions, + Button, + Statistic +} from "antd"; +import { + ReloadOutlined, + LinkOutlined, + ClusterOutlined, + TeamOutlined, + UserOutlined +} from "@ant-design/icons"; +import { getEnvironmentById } from "./services/environments.service"; +import { Environment } from "./types/environment.types"; + +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 + const { environmentId: id } = useParams<{ environmentId: string }>(); + console.log(id); + + // State for environment data and loading state + const [environment, setEnvironment] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Fetch environment data on mount and when ID changes + useEffect(() => { + fetchEnvironmentData(); + }, [id]); + + // Function to fetch environment data + const fetchEnvironmentData = async () => { + setLoading(true); + setError(null); + + try { + const data = await getEnvironmentById(id); + setEnvironment(data); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to fetch environment details'); + } finally { + setLoading(false); + } + }; + + // Handle refresh button click + const handleRefresh = () => { + fetchEnvironmentData(); + }; + + // If loading, show spinner + if (loading) { + return ( +
+ +
+ ); + } + + // If error, show error message + if (error) { + return ( + } onClick={handleRefresh}> + Try Again + + } + /> + ); + } + + // If no environment data, show message + if (!environment) { + return ( + + ); + } + + return ( +
+ {/* Header with environment name and controls */} +
+
+ {environment.environmentName || 'Unnamed Environment'} + ID: {environment.environmentId} +
+ +
+ + {/* Basic Environment Information Card */} + Master} + > + + + {environment.environmentFrontendUrl ? ( + + {environment.environmentFrontendUrl} + + ) : ( + 'No domain set' + )} + + + + {environment.environmentType} + + + + {environment.environmentApikey ? Configured : Not Configured} + + + {environment.isMaster ? 'Yes' : 'No'} + + + + + {/* Tabs for Workspaces and User Groups */} + + Workspaces} + key="workspaces" + > + + {/* Placeholder for workspace statistics */} + +
+ } + /> + + + } + /> + + + } + /> + + + + {/* Placeholder for workspace list */} + + + + + User Groups} + key="userGroups" + > + + {/* Placeholder for user group statistics */} + + + } + /> + + + } + /> + + + + {/* Placeholder for user group list */} + + + + + + ); +}; + +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 bea8d1250a..07d0086a49 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/Environments.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/Environments.tsx @@ -2,8 +2,10 @@ import React from "react"; import { Switch, Route, useRouteMatch } from "react-router-dom"; import EnvironmentsList from "./EnvironmentsList"; // Rename your current component +import EnvironmentDetail from "./EnvironmentDetail"; + +import { ENVIRONMENT_SETTING, ENVIRONMENT_DETAIL } from "@lowcoder-ee/constants/routesURL"; -import { ENVIRONMENT_SETTING } from "@lowcoder-ee/constants/routesURL"; const Environments: React.FC = () => { return ( @@ -11,6 +13,10 @@ const Environments: React.FC = () => { + + + + ); }; diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx index 5af0a5c3db..b0a6c7274b 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx @@ -5,6 +5,7 @@ import { useHistory } from "react-router-dom"; import { useEnvironments } from "./hooks/useEnvironments"; import { Environment } from "./types/environment.types"; import EnvironmentsTable from "./components/EnvironmentsTable"; +import { buildEnvironmentId } from "@lowcoder-ee/constants/routesURL"; const { Title } = Typography; @@ -67,7 +68,7 @@ const EnvironmentsList: React.FC = () => { // Handle row click to navigate to environment detail const handleRowClick = (record: Environment) => { - history.push(`/home/settings/environments/${record.environmentId}`); + history.push(buildEnvironmentId(record.environmentId)); }; return ( diff --git a/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts b/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts index 785e694a35..f75557933d 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts +++ b/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts @@ -27,7 +27,7 @@ export async function getEnvironments(): Promise { */ export async function getEnvironmentById(id: string): Promise { try { - const response = await axios.get(`/api/plugins/enterprise/environments/${id}`); + const response = await axios.get(`/api/plugins/enterprise/environments?environmentId=${id}`); if (!response.data) { throw new Error('Failed to fetch environment'); From 2e88e9fd63dce98dd2058ff252b24dee605c3665 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 10 Apr 2025 17:32:00 +0500 Subject: [PATCH 05/68] Create useEnvironmentDetail Hook and add in Detail Page --- .../environments/EnvironmentDetail.tsx | 41 +++------------- .../hooks/useEnvironmentDetail.tsx | 48 +++++++++++++++++++ 2 files changed, 54 insertions(+), 35 deletions(-) create mode 100644 client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironmentDetail.tsx diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx index 34fe98348d..6bc12a1c76 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React from "react"; import { useParams } from "react-router-dom"; import { Spin, @@ -20,8 +20,7 @@ import { TeamOutlined, UserOutlined } from "@ant-design/icons"; -import { getEnvironmentById } from "./services/environments.service"; -import { Environment } from "./types/environment.types"; +import { useEnvironmentDetail } from './hooks/useEnvironmentDetail'; const { Title, Text } = Typography; const { TabPane } = Tabs; @@ -33,37 +32,9 @@ const { TabPane } = Tabs; const EnvironmentDetail: React.FC = () => { // Get environment ID from URL params const { environmentId: id } = useParams<{ environmentId: string }>(); - console.log(id); - // State for environment data and loading state - const [environment, setEnvironment] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - // Fetch environment data on mount and when ID changes - useEffect(() => { - fetchEnvironmentData(); - }, [id]); - - // Function to fetch environment data - const fetchEnvironmentData = async () => { - setLoading(true); - setError(null); - - try { - const data = await getEnvironmentById(id); - setEnvironment(data); - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to fetch environment details'); - } finally { - setLoading(false); - } - }; - - // Handle refresh button click - const handleRefresh = () => { - fetchEnvironmentData(); - }; + // Use the custom hook to handle data fetching and state management + const { environment, loading, error, refresh } = useEnvironmentDetail(id); // If loading, show spinner if (loading) { @@ -84,7 +55,7 @@ const EnvironmentDetail: React.FC = () => { showIcon style={{ margin: '24px' }} action={ - } @@ -115,7 +86,7 @@ const EnvironmentDetail: React.FC = () => { diff --git a/client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironmentDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironmentDetail.tsx new file mode 100644 index 0000000000..f347e79a0d --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironmentDetail.tsx @@ -0,0 +1,48 @@ +import { useState, useEffect, useCallback } from 'react'; +import { getEnvironmentById } from '../services/environments.service'; +import { Environment } from '../types/environment.types'; + +/** + * Custom hook to fetch and manage environment detail data + * @param id - Environment ID to fetch + * @returns Object containing environment data, loading state, error state, and refresh function + */ +export const useEnvironmentDetail = (id: string) => { + const [environment, setEnvironment] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Function to fetch environment data + const fetchEnvironmentData = useCallback(async () => { + if (!id) { + setError('No environment ID provided'); + setLoading(false); + return; + } + + setLoading(true); + setError(null); + + try { + const data = await getEnvironmentById(id); + setEnvironment(data); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to fetch environment details'); + } finally { + setLoading(false); + } + }, [id]); + + // Fetch environment data on mount and when ID changes + useEffect(() => { + fetchEnvironmentData(); + }, [fetchEnvironmentData]); + + // Return the state and a function to refresh data + return { + environment, + loading, + error, + refresh: fetchEnvironmentData, + }; +}; \ No newline at end of file From eb401ee94d95126940d0878256851a33d83bf5cb Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 10 Apr 2025 19:03:26 +0500 Subject: [PATCH 06/68] add workspaces list --- .../environments/EnvironmentDetail.tsx | 280 +++++++++++++----- .../components/WorkspacesList.tsx | 94 ++++++ .../hooks/useEnvironmentDetail.ts | 125 ++++++++ .../hooks/useEnvironmentDetail.tsx | 48 --- .../services/environments.service.ts | 103 ++++++- .../environments/types/workspace.types.ts | 15 + 6 files changed, 524 insertions(+), 141 deletions(-) create mode 100644 client/packages/lowcoder/src/pages/setting/environments/components/WorkspacesList.tsx create mode 100644 client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironmentDetail.ts delete mode 100644 client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironmentDetail.tsx create mode 100644 client/packages/lowcoder/src/pages/setting/environments/types/workspace.types.ts diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx index 6bc12a1c76..8f040e8285 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx @@ -1,26 +1,29 @@ import React from "react"; import { useParams } from "react-router-dom"; -import { - Spin, - Typography, - Card, - Row, - Col, - Tag, - Tabs, - Alert, - Descriptions, +import { + Spin, + Typography, + Card, + Row, + Col, + Tag, + Tabs, + Alert, + Descriptions, Button, - Statistic + Statistic, + Divider, } from "antd"; -import { - ReloadOutlined, - LinkOutlined, - ClusterOutlined, - TeamOutlined, - UserOutlined +import { + ReloadOutlined, + LinkOutlined, + ClusterOutlined, + TeamOutlined, + UserOutlined, + SyncOutlined, } from "@ant-design/icons"; -import { useEnvironmentDetail } from './hooks/useEnvironmentDetail'; +import { useEnvironmentDetail } from "./hooks/useEnvironmentDetail"; +import WorkspacesList from "./components/WorkspacesList"; const { Title, Text } = Typography; const { TabPane } = Tabs; @@ -32,19 +35,37 @@ const { TabPane } = Tabs; const EnvironmentDetail: React.FC = () => { // Get environment ID from URL params const { environmentId: id } = useParams<{ environmentId: string }>(); - + + // Use the custom hook to handle data fetching and state management // Use the custom hook to handle data fetching and state management - const { environment, loading, error, refresh } = useEnvironmentDetail(id); - + const { + environment, + loading, + error, + refresh, + workspaces, + workspacesLoading, + workspacesError, + refreshWorkspaces, + workspaceStats, + } = useEnvironmentDetail(id); // If loading, show spinner if (loading) { return ( -
+
); } - + // If error, show error message if (error) { return ( @@ -53,7 +74,7 @@ const EnvironmentDetail: React.FC = () => { description={error} type="error" showIcon - style={{ margin: '24px' }} + style={{ margin: "24px" }} action={
- + {/* Basic Environment Information Card */} - Master} > - + {environment.environmentFrontendUrl ? ( - + {environment.environmentFrontendUrl} ) : ( - 'No domain set' + "No domain set" )} - + {environment.environmentType} - {environment.environmentApikey ? Configured : Not Configured} + {environment.environmentApikey ? ( + Configured + ) : ( + Not Configured + )} - {environment.isMaster ? 'Yes' : 'No'} + {environment.isMaster ? "Yes" : "No"} - + {/* Tabs for Workspaces and User Groups */} - Workspaces} + + Workspaces + + } key="workspaces" > - {/* Placeholder for workspace statistics */} - + {/* Header with refresh button */} +
+ Workspaces in this Environment + +
+ + {/* Workspace Statistics */} +
- } + } /> - } + } /> - } + } /> - - {/* Placeholder for workspace list */} - + + {/* Show error if workspace loading failed */} + {workspacesError && ( + + API Key Required + + ) : ( + + ) + } + /> + )} + + {(!environment.environmentApikey || + !environment.environmentApiServiceUrl) && + !workspacesError && ( + + )} + + {/* Workspaces List */} + - - User Groups} + + + User Groups + + } key="userGroups" > {/* Placeholder for user group statistics */} - + - } + } /> - } + } /> - + {/* Placeholder for user group list */} = ({ + workspaces, + loading, + error, +}) => { + // Format timestamp to date string + const formatDate = (timestamp?: number): string => { + if (!timestamp) return 'N/A'; + const date = new Date(timestamp); + return `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`; + }; + + // Table columns definition + const 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) => formatDate(record.creationDate), + }, + { + title: 'Status', + dataIndex: 'status', + key: 'status', + render: (status: string) => ( + + {status} + + ), + } + ]; + + // If loading, show spinner + if (loading) { + return ( +
+ +
+ ); + } + + // If no workspaces or error, show empty state + if (!workspaces || workspaces.length === 0 || error) { + return ( + + ); + } + + return ( +
+ ); +}; + +export default WorkspacesList; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironmentDetail.ts b/client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironmentDetail.ts new file mode 100644 index 0000000000..a52deb4888 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironmentDetail.ts @@ -0,0 +1,125 @@ +import { useState, useEffect, useCallback } from "react"; +import { + getEnvironmentById, + getEnvironmentWorkspaces, +} from "../services/environments.service"; +import { Environment } from "../types/environment.types"; +import { Workspace } from "../types/workspace.types"; +/** + * Custom hook to fetch and manage environment detail data + * @param id - Environment ID to fetch + * @returns Object containing environment data, loading state, error state, and refresh function + */ +export const useEnvironmentDetail = (id: string) => { + const [environment, setEnvironment] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Workspaces state + const [workspaces, setWorkspaces] = useState([]); + const [workspacesLoading, setWorkspacesLoading] = useState(false); + const [workspacesError, setWorkspacesError] = useState(null); + + // Function to fetch environment data + const fetchEnvironmentData = useCallback(async () => { + if (!id) { + setError("No environment ID provided"); + setLoading(false); + return; + } + + setLoading(true); + setError(null); + + try { + const data = await getEnvironmentById(id); + setEnvironment(data); + } catch (err) { + setError( + err instanceof Error + ? err.message + : "Failed to fetch environment details" + ); + } finally { + setLoading(false); + } + }, [id]); + + // Function to fetch workspaces for the environment + const fetchWorkspaces = useCallback(async () => { + // Don't fetch workspaces if environment is not loaded yet + if (!environment) { + return; + } + + setWorkspacesLoading(true); + setWorkspacesError(null); + + try { + // Get the API key from the environment + const apiKey = environment.environmentApikey; + const apiServiceUrl = environment.environmentApiServiceUrl; + + if (!apiKey) { + setWorkspacesError( + "No API key configured for this environment. Workspaces cannot be fetched." + ); + setWorkspacesLoading(false); + return; + } + if (!apiServiceUrl) { + setWorkspacesError('No API service URL configured for this environment. Workspaces cannot be fetched.'); + setWorkspacesLoading(false); + return; + } + + // Call the function with environment ID and API key + const data = await getEnvironmentWorkspaces(id, apiKey, apiServiceUrl); + console.log(data); + setWorkspaces(data); + } catch (err) { + setWorkspacesError( + err instanceof Error ? err.message : "Failed to fetch workspaces" + ); + } finally { + setWorkspacesLoading(false); + } + }, [environment, id]); + + // Fetch environment data on mount and when ID changes + useEffect(() => { + fetchEnvironmentData(); + }, [fetchEnvironmentData]); + + // Fetch workspaces when environment is loaded + useEffect(() => { + if (environment) { + fetchWorkspaces(); + } + }, [environment, fetchWorkspaces]); + + // Calculate workspace statistics + const workspaceStats = { + total: workspaces.length, + managed: 0, // To be implemented later + unmanaged: workspaces.length, // To be implemented later + apiKeyConfigured: !!environment?.environmentApikey, + apiServiceUrlConfigured: !!environment?.environmentApiServiceUrl + }; + + // Return the state and functions to refresh data + return { + // Environment data + environment, + loading, + error, + refresh: fetchEnvironmentData, + + // Workspaces data + workspaces, + workspacesLoading, + workspacesError, + refreshWorkspaces: fetchWorkspaces, + workspaceStats, + }; +}; diff --git a/client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironmentDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironmentDetail.tsx deleted file mode 100644 index f347e79a0d..0000000000 --- a/client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironmentDetail.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { useState, useEffect, useCallback } from 'react'; -import { getEnvironmentById } from '../services/environments.service'; -import { Environment } from '../types/environment.types'; - -/** - * Custom hook to fetch and manage environment detail data - * @param id - Environment ID to fetch - * @returns Object containing environment data, loading state, error state, and refresh function - */ -export const useEnvironmentDetail = (id: string) => { - const [environment, setEnvironment] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - // Function to fetch environment data - const fetchEnvironmentData = useCallback(async () => { - if (!id) { - setError('No environment ID provided'); - setLoading(false); - return; - } - - setLoading(true); - setError(null); - - try { - const data = await getEnvironmentById(id); - setEnvironment(data); - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to fetch environment details'); - } finally { - setLoading(false); - } - }, [id]); - - // Fetch environment data on mount and when ID changes - useEffect(() => { - fetchEnvironmentData(); - }, [fetchEnvironmentData]); - - // Return the state and a function to refresh data - return { - environment, - loading, - error, - refresh: fetchEnvironmentData, - }; -}; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts b/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts index f75557933d..39f7287947 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts +++ b/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts @@ -1,6 +1,7 @@ -import axios from 'axios'; -import { message } from 'antd'; -import { Environment } from '../types/environment.types'; +import axios from "axios"; +import { message } from "antd"; +import { Environment } from "../types/environment.types"; +import { Workspace } from "../types/workspace.types"; /** * Fetch all environments @@ -9,12 +10,15 @@ import { Environment } from '../types/environment.types'; export async function getEnvironments(): Promise { try { // The response contains the data array directly in response.data - const response = await axios.get('/api/plugins/enterprise/environments/list'); - + const response = await axios.get( + "/api/plugins/enterprise/environments/list" + ); + // Return the data array directly from response.data return response.data || []; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Failed to fetch environments'; + const errorMessage = + error instanceof Error ? error.message : "Failed to fetch environments"; message.error(errorMessage); throw error; } @@ -27,16 +31,91 @@ export async function getEnvironments(): Promise { */ export async function getEnvironmentById(id: string): Promise { try { - const response = await axios.get(`/api/plugins/enterprise/environments?environmentId=${id}`); - + const response = await axios.get( + `/api/plugins/enterprise/environments?environmentId=${id}` + ); + if (!response.data) { - throw new Error('Failed to fetch environment'); + throw new Error("Failed to fetch environment"); } - + return response.data; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Failed to fetch environment'; + const errorMessage = + error instanceof Error ? error.message : "Failed to fetch environment"; + message.error(errorMessage); + throw error; + } +} + +/* ================================================================================ + +=============================== ENVIRONMENT WORKSPACES ============================ +*/ + +/** + * Fetch workspaces for a specific environment + * @param environmentId - ID of the environment + * @param apiKey - API key for the environment + * @returns Promise with an array of workspaces + */ +export async function getEnvironmentWorkspaces( + environmentId: string, + apiKey: string, + apiServiceUrl: string +): Promise { + try { + // Check if required parameters are provided + if (!environmentId) { + throw new Error("Environment ID is required"); + } + + if (!apiKey) { + throw new Error("API key is required to fetch workspaces"); + } + if (!apiServiceUrl) { + throw new Error('API service URL is required to fetch workspaces'); + } + + // Set up headers with the API key + const headers = { + "X-API-Key": apiKey, + }; + + // Make the API request to get user data which includes workspaces + const response = await axios.get(`${apiServiceUrl}/api/users/me`, { headers }); + + // Check if response is valid + if (!response.data || !response.data.success) { + throw new Error(response.data?.message || "Failed to fetch workspaces"); + } + + // Extract workspaces from the response + const userData = response.data.data; + + if (!userData.orgAndRoles || !Array.isArray(userData.orgAndRoles)) { + return []; + } + + // Transform the data to match our Workspace interface + const workspaces: Workspace[] = userData.orgAndRoles.map((item:any) => ({ + id: item.org.id, + name: item.org.name, + role: item.role, + creationDate: item.org.createTime, + status: item.org.state, + gid: item.org.gid, + createdBy: item.org.createdBy, + isAutoGeneratedOrganization: item.org.isAutoGeneratedOrganization, + logoUrl: item.org.logoUrl || "", + })); + + return workspaces; + } catch (error) { + // Handle and transform error + const errorMessage = + error instanceof Error ? error.message : "Failed to fetch workspaces"; message.error(errorMessage); throw error; } -} \ No newline at end of file +} diff --git a/client/packages/lowcoder/src/pages/setting/environments/types/workspace.types.ts b/client/packages/lowcoder/src/pages/setting/environments/types/workspace.types.ts new file mode 100644 index 0000000000..b3ec8fddaa --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/types/workspace.types.ts @@ -0,0 +1,15 @@ +/** + * Represents a Workspace entity in an environment + */ +export interface Workspace { + id: string; + name: string; + role: string; // 'admin', 'member', etc. + creationDate?: number; // timestamp + status: string; // 'ACTIVE', 'INACTIVE', etc. + // Optional fields + gid?: string; + createdBy?: string; + isAutoGeneratedOrganization?: boolean | null; + logoUrl?: string; + } \ No newline at end of file From 33262149255dff8ce6355462fd56c56b5564d421 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 10 Apr 2025 19:11:18 +0500 Subject: [PATCH 07/68] fix Auth headers --- .../pages/setting/environments/services/environments.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts b/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts index 39f7287947..1205087d94 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts +++ b/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts @@ -79,7 +79,7 @@ export async function getEnvironmentWorkspaces( // Set up headers with the API key const headers = { - "X-API-Key": apiKey, + Authorization: `Bearer ${apiKey}` }; // Make the API request to get user data which includes workspaces From c66a04884286ba36a3f371d2a48e0bd38c34a068 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 10 Apr 2025 20:12:50 +0500 Subject: [PATCH 08/68] Add user groups in Env Detail Page --- .../environments/EnvironmentDetail.tsx | 100 ++++++++++++++-- .../components/UserGroupsList.tsx | 109 ++++++++++++++++++ .../hooks/useEnvironmentDetail.ts | 97 ++++++++++++++-- .../services/environments.service.ts | 52 +++++++++ .../environments/types/userGroup.types.ts | 20 ++++ 5 files changed, 358 insertions(+), 20 deletions(-) create mode 100644 client/packages/lowcoder/src/pages/setting/environments/components/UserGroupsList.tsx create mode 100644 client/packages/lowcoder/src/pages/setting/environments/types/userGroup.types.ts diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx index 8f040e8285..362ff097b9 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx @@ -24,6 +24,7 @@ import { } from "@ant-design/icons"; import { useEnvironmentDetail } from "./hooks/useEnvironmentDetail"; import WorkspacesList from "./components/WorkspacesList"; +import UserGroupsList from "./components/UserGroupsList"; const { Title, Text } = Typography; const { TabPane } = Tabs; @@ -48,6 +49,12 @@ const EnvironmentDetail: React.FC = () => { workspacesError, refreshWorkspaces, workspaceStats, + userGroups, + userGroupsLoading, + userGroupsError, + refreshUserGroups, + userGroupStats + } = useEnvironmentDetail(id); // If loading, show spinner if (loading) { @@ -277,7 +284,6 @@ const EnvironmentDetail: React.FC = () => { /> - @@ -287,30 +293,102 @@ const EnvironmentDetail: React.FC = () => { key="userGroups" > - {/* Placeholder for user group statistics */} + {/* Header with refresh button */} +
+ User Groups in this Environment + +
+ + {/* User Group Statistics */}
} /> } + /> + + + } /> - {/* Placeholder for user group list */} - + + {/* Show error if user group loading failed */} + {userGroupsError && ( + + Configuration Required + + ) : ( + + ) + } + /> + )} + + {/* Show warning if no API key or API service URL is configured */} + {(!environment.environmentApikey || + !environment.environmentApiServiceUrl) && + !userGroupsError && ( + + )} + + {/* User Groups List */} + @@ -319,4 +397,4 @@ const EnvironmentDetail: React.FC = () => { ); }; -export default EnvironmentDetail; \ No newline at end of file +export default EnvironmentDetail; diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/UserGroupsList.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/UserGroupsList.tsx new file mode 100644 index 0000000000..7a191784e9 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/components/UserGroupsList.tsx @@ -0,0 +1,109 @@ +import React from 'react'; +import { Table, Tag, Empty, Spin, Badge } from 'antd'; +import { UserGroup } from '../types/userGroup.types'; + +interface UserGroupsListProps { + userGroups: UserGroup[]; + loading: boolean; + error?: string | null; +} + +/** + * Component to display a list of user groups in a table + */ +const UserGroupsList: React.FC = ({ + userGroups, + loading, + error, +}) => { + // Format timestamp to date string + const formatDate = (timestamp?: number): string => { + if (!timestamp) return 'N/A'; + const date = new Date(timestamp); + return `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`; + }; + + // Table columns definition + const columns = [ + { + title: 'Name', + dataIndex: 'groupName', + key: 'groupName', + render: (name: string, record: UserGroup) => ( +
+ {name} + {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; + }, + } + ]; + + // If loading, show spinner + if (loading) { + return ( +
+ +
+ ); + } + + // If no user groups or error, show empty state + if (!userGroups || userGroups.length === 0 || error) { + return ( + + ); + } + + return ( +
+ ); +}; + +export default UserGroupsList; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironmentDetail.ts b/client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironmentDetail.ts index a52deb4888..7e4d9f37e8 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironmentDetail.ts +++ b/client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironmentDetail.ts @@ -2,15 +2,19 @@ import { useState, useEffect, useCallback } from "react"; import { getEnvironmentById, getEnvironmentWorkspaces, + getEnvironmentUserGroups, } from "../services/environments.service"; import { Environment } from "../types/environment.types"; import { Workspace } from "../types/workspace.types"; +import { UserGroup } from "../types/userGroup.types"; + /** * Custom hook to fetch and manage environment detail data * @param id - Environment ID to fetch - * @returns Object containing environment data, loading state, error state, and refresh function + * @returns Object containing environment data, workspaces, user groups, loading states, error states, and refresh functions */ export const useEnvironmentDetail = (id: string) => { + // Environment state const [environment, setEnvironment] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -20,6 +24,11 @@ export const useEnvironmentDetail = (id: string) => { const [workspacesLoading, setWorkspacesLoading] = useState(false); const [workspacesError, setWorkspacesError] = useState(null); + // User Groups state + const [userGroups, setUserGroups] = useState([]); + const [userGroupsLoading, setUserGroupsLoading] = useState(false); + const [userGroupsError, setUserGroupsError] = useState(null); + // Function to fetch environment data const fetchEnvironmentData = useCallback(async () => { if (!id) { @@ -45,7 +54,7 @@ export const useEnvironmentDetail = (id: string) => { } }, [id]); - // Function to fetch workspaces for the environment + // Function to fetch workspaces const fetchWorkspaces = useCallback(async () => { // Don't fetch workspaces if environment is not loaded yet if (!environment) { @@ -56,10 +65,11 @@ export const useEnvironmentDetail = (id: string) => { setWorkspacesError(null); try { - // Get the API key from the environment + // Get the API key and API service URL from the environment const apiKey = environment.environmentApikey; const apiServiceUrl = environment.environmentApiServiceUrl; + // Check if both API key and API service URL are configured if (!apiKey) { setWorkspacesError( "No API key configured for this environment. Workspaces cannot be fetched." @@ -67,15 +77,17 @@ export const useEnvironmentDetail = (id: string) => { setWorkspacesLoading(false); return; } + if (!apiServiceUrl) { - setWorkspacesError('No API service URL configured for this environment. Workspaces cannot be fetched.'); + setWorkspacesError( + "No API service URL configured for this environment. Workspaces cannot be fetched." + ); setWorkspacesLoading(false); return; } - // Call the function with environment ID and API key + // Call the function with environment ID, API key, and API service URL const data = await getEnvironmentWorkspaces(id, apiKey, apiServiceUrl); - console.log(data); setWorkspaces(data); } catch (err) { setWorkspacesError( @@ -86,17 +98,62 @@ export const useEnvironmentDetail = (id: string) => { } }, [environment, id]); + // Function to fetch user groups + const fetchUserGroups = useCallback(async () => { + // Don't fetch user groups if environment is not loaded yet + if (!environment) { + return; + } + + setUserGroupsLoading(true); + setUserGroupsError(null); + + try { + // Get the API key and API service URL from the environment + const apiKey = environment.environmentApikey; + const apiServiceUrl = environment.environmentApiServiceUrl; + + // Check if both API key and API service URL are configured + if (!apiKey) { + setUserGroupsError( + "No API key configured for this environment. User groups cannot be fetched." + ); + setUserGroupsLoading(false); + return; + } + + if (!apiServiceUrl) { + setUserGroupsError( + "No API service URL configured for this environment. User groups cannot be fetched." + ); + setUserGroupsLoading(false); + return; + } + + // Call the function with environment ID, API key, and API service URL + const data = await getEnvironmentUserGroups(id, apiKey, apiServiceUrl); + setUserGroups(data); + } catch (err) { + setUserGroupsError( + err instanceof Error ? err.message : "Failed to fetch user groups" + ); + } finally { + setUserGroupsLoading(false); + } + }, [environment, id]); + // Fetch environment data on mount and when ID changes useEffect(() => { fetchEnvironmentData(); }, [fetchEnvironmentData]); - // Fetch workspaces when environment is loaded + // Fetch workspaces and user groups after environment is loaded useEffect(() => { if (environment) { fetchWorkspaces(); + fetchUserGroups(); } - }, [environment, fetchWorkspaces]); + }, [environment, fetchWorkspaces, fetchUserGroups]); // Calculate workspace statistics const workspaceStats = { @@ -104,7 +161,22 @@ export const useEnvironmentDetail = (id: string) => { managed: 0, // To be implemented later unmanaged: workspaces.length, // To be implemented later apiKeyConfigured: !!environment?.environmentApikey, - apiServiceUrlConfigured: !!environment?.environmentApiServiceUrl + apiServiceUrlConfigured: !!environment?.environmentApiServiceUrl, + }; + + // Calculate user group statistics + const userGroupStats = { + total: userGroups.length, + totalUsers: userGroups.reduce( + (acc, group) => acc + group.stats.userCount, + 0 + ), + adminUsers: userGroups.reduce( + (acc, group) => acc + group.stats.adminUserCount, + 0 + ), + apiKeyConfigured: !!environment?.environmentApikey, + apiServiceUrlConfigured: !!environment?.environmentApiServiceUrl, }; // Return the state and functions to refresh data @@ -121,5 +193,12 @@ export const useEnvironmentDetail = (id: string) => { workspacesError, refreshWorkspaces: fetchWorkspaces, workspaceStats, + + // User Groups data + userGroups, + userGroupsLoading, + userGroupsError, + refreshUserGroups: fetchUserGroups, + userGroupStats, }; }; diff --git a/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts b/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts index 1205087d94..fc34d51a04 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts +++ b/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts @@ -2,6 +2,7 @@ import axios from "axios"; import { message } from "antd"; import { Environment } from "../types/environment.types"; import { Workspace } from "../types/workspace.types"; +import { UserGroup } from "../types/userGroup.types"; /** * Fetch all environments @@ -119,3 +120,54 @@ export async function getEnvironmentWorkspaces( throw error; } } + + + +/* ================================================================================ + +=============================== ENVIRONMENT USER GROUPS ============================ */ + +export async function getEnvironmentUserGroups( + environmentId: string, + apiKey: string, + apiServiceUrl: string +): Promise { + try { + // Check if required parameters are provided + if (!environmentId) { + throw new Error('Environment ID is required'); + } + + if (!apiKey) { + throw new Error('API key is required to fetch user groups'); + } + + if (!apiServiceUrl) { + throw new Error('API service URL is required to fetch user groups'); + } + + // Set up headers with the Bearer token format + const headers = { + Authorization: `Bearer ${apiKey}` + }; + + // Make the API request to get user groups + const response = await axios.get(`${apiServiceUrl}/api/groups/list`, { headers }); + console.log(response); + + // Check if response is valid + if (!response.data) { + throw new Error('Failed to fetch user groups'); + } + + // The response data is already an array of user groups + const userGroups: UserGroup[] = response.data.data || []; + + return userGroups; + } catch (error) { + // Handle and transform error + const errorMessage = error instanceof Error ? error.message : 'Failed to fetch user groups'; + message.error(errorMessage); + throw error; + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..cd5ec1ec8b --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/types/userGroup.types.ts @@ -0,0 +1,20 @@ +/** + * Represents a User Group entity in an environment + */ +export interface UserGroup { + 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; + } \ No newline at end of file From 2d6ec6b473053ef9868cf85b901479a76ffddd40 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 10 Apr 2025 22:06:31 +0500 Subject: [PATCH 09/68] setup workspace detail page --- .../setting/environments/Environments.tsx | 17 +- .../setting/environments/WorkspaceDetail.tsx | 221 ++++++++++++++++++ .../environments/components/AppsList.tsx | 125 ++++++++++ .../environments/hooks/useWorkspaceDetail.ts | 145 ++++++++++++ .../services/environments.service.ts | 98 +++++++- .../setting/environments/types/app.types.ts | 25 ++ 6 files changed, 625 insertions(+), 6 deletions(-) create mode 100644 client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx create mode 100644 client/packages/lowcoder/src/pages/setting/environments/components/AppsList.tsx create mode 100644 client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaceDetail.ts create mode 100644 client/packages/lowcoder/src/pages/setting/environments/types/app.types.ts diff --git a/client/packages/lowcoder/src/pages/setting/environments/Environments.tsx b/client/packages/lowcoder/src/pages/setting/environments/Environments.tsx index 07d0086a49..2095dd835d 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/Environments.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/Environments.tsx @@ -3,20 +3,27 @@ import React from "react"; import { Switch, Route, useRouteMatch } from "react-router-dom"; import EnvironmentsList from "./EnvironmentsList"; // Rename your current component import EnvironmentDetail from "./EnvironmentDetail"; +import WorkspaceDetail from "./WorkspaceDetail"; +import { ENVIRONMENT_WORKSPACE_DETAIL } from "@lowcoder-ee/constants/routesURL"; -import { ENVIRONMENT_SETTING, ENVIRONMENT_DETAIL } from "@lowcoder-ee/constants/routesURL"; - +import { + ENVIRONMENT_SETTING, + ENVIRONMENT_DETAIL, +} from "@lowcoder-ee/constants/routesURL"; const Environments: React.FC = () => { return ( - - + + + - + + + ); }; diff --git a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx new file mode 100644 index 0000000000..ab6255b49b --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx @@ -0,0 +1,221 @@ +import React, { useEffect, useState } from "react"; +import { useParams, useHistory } from "react-router-dom"; +import history from "@lowcoder-ee/util/history"; +import { useWorkspaceDetail } from "./hooks/useWorkspaceDetail"; + + +import { + Spin, + Typography, + Card, + Row, + Col, + Tabs, + Alert, + Button, + Statistic, + Divider, + Breadcrumb +} from "antd"; +import { + ReloadOutlined, + AppstoreOutlined, + DatabaseOutlined, + CodeOutlined, + HomeOutlined, + TeamOutlined, + SyncOutlined, + ArrowLeftOutlined +} from "@ant-design/icons"; +import { getEnvironmentById } from './services/environments.service'; +import AppsList from './components/AppsList'; +import { Workspace } from './types/workspace.types'; +import { Environment } from './types/environment.types'; +import { App } from './types/app.types'; + +const { Title, Text } = Typography; +const { TabPane } = Tabs; + + +const WorkspaceDetail: React.FC = () => { + // Get parameters from URL + const { environmentId, workspaceId } = useParams<{ + environmentId: string; + workspaceId: string; + }>(); + + // Use the custom hook + const { + environment, + workspace, + apps, + appsLoading, + appsError, + refreshApps, + appStats, + isLoading, + hasError + } = useWorkspaceDetail(environmentId, workspaceId); + + // Handle loading/error states + if (isLoading) { + return ; + } + + // Handle loading/error states +if (isLoading) { + return ( +
+ +
+ ); + } + + // Handle error state + if (hasError || !environment || !workspace) { + return ( + history.push(`/home/settings/environments/${environmentId}`)}> + Back to Environment + + } + /> + ); + } + + return ( +
+ {/* Breadcrumb navigation */} + + history.push('/home/settings/environments')}> + Environments + + history.push(`/home/settings/environments/${environmentId}`)}> + {environment.environmentName} + + + {workspace.name} + + + + {/* Header with workspace name and controls */} +
+
+ + {workspace.name} + ID: {workspace.id} +
+
+ + {/* Tabs for Apps, Data Sources, and Queries */} + + Apps} + key="apps" + > + + {/* Header with refresh button */} +
+ Apps in this Workspace + +
+ + {/* App Statistics */} + +
+ } + /> + + + } + /> + + + + + + {/* Show error if apps loading failed */} + {appsError && ( + + Try Again + + } + /> + )} + + {/* Apps List */} + + + + + Data Sources} + key="dataSources" + > + + + + + + Queries} + key="queries" + > + + + + + + + ); + } + + +export default WorkspaceDetail \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/AppsList.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/AppsList.tsx new file mode 100644 index 0000000000..8c5ea581f7 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/components/AppsList.tsx @@ -0,0 +1,125 @@ +import React from 'react'; +import { Table, Tag, Empty, Spin, Avatar, Tooltip } from 'antd'; +import { + AppstoreOutlined, + UserOutlined, + CheckCircleOutlined, + CloseCircleOutlined +} from '@ant-design/icons'; +import { App } from '../types/app.types'; + +interface AppsListProps { + apps: App[]; + loading: boolean; + error?: string | null; +} + +/** + * Component to display a list of apps in a table + */ +const AppsList: React.FC = ({ + apps, + loading, + error, +}) => { + // Format timestamp to date string + const formatDate = (timestamp?: number): string => { + if (!timestamp) return 'N/A'; + const date = new Date(timestamp); + return `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`; + }; + + // Table columns definition + const columns = [ + { + title: 'Title', + key: 'title', + render: (record: App) => ( +
+ } + src={record.icon || undefined} + style={{ marginRight: 8 }} + /> + {record.title || record.name} +
+ ), + }, + { + title: 'Created By', + dataIndex: 'createBy', + key: 'createBy', + render: (createBy: string) => ( +
+ } style={{ marginRight: 8 }} /> + {createBy} +
+ ), + }, + { + title: 'Created', + key: 'createAt', + render: (record: App) => formatDate(record.createAt), + }, + { + title: 'Last Modified', + key: 'lastModifyTime', + render: (record: App) => formatDate(record.lastModifyTime), + }, + { + title: 'Published', + dataIndex: 'published', + key: 'published', + render: (published: boolean) => ( + + {published ? + : + + } + + ), + }, + { + title: 'Status', + dataIndex: 'applicationStatus', + key: 'applicationStatus', + render: (status: string) => ( + + {status} + + ), + } + ]; + + // If loading, show spinner + if (loading) { + return ( +
+ +
+ ); + } + + // If no apps or error, show empty state + if (!apps || apps.length === 0 || error) { + return ( + + ); + } + + return ( +
+ ); +}; + +export default AppsList; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaceDetail.ts b/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaceDetail.ts new file mode 100644 index 0000000000..803a8f3411 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaceDetail.ts @@ -0,0 +1,145 @@ +import { useState, useEffect, useCallback } from "react"; +import { getEnvironmentById, fetchWorkspaceById, getWorkspaceApps} from "../services/environments.service"; +import { Environment } from "../types/environment.types"; +import { Workspace } from "../types/workspace.types"; +import { App } from "../types/app.types"; +export const useWorkspaceDetail = ( + environmentId: string, + workspaceId: string +) => { + // Environment state + const [environment, setEnvironment] = useState(null); + const [environmentLoading, setEnvironmentLoading] = useState(true); + const [environmentError, setEnvironmentError] = useState(null); + + // Workspace state + const [workspace, setWorkspace] = useState(null); + const [workspaceLoading, setWorkspaceLoading] = useState(true); + const [workspaceError, setWorkspaceError] = useState(null); + + // Apps state + const [apps, setApps] = useState([]); + const [appsLoading, setAppsLoading] = useState(false); + const [appsError, setAppsError] = useState(null); + + // Function to fetch environment data + const fetchEnvironmentData = useCallback(async () => { + // Similar to your existing function + setEnvironmentLoading(true); + try { + const data = await getEnvironmentById(environmentId); + setEnvironment(data); + } catch (err) { + setEnvironmentError( + err instanceof Error ? err.message : "Failed to fetch environment" + ); + } finally { + setEnvironmentLoading(false); + } + }, [environmentId]); + + // Function to fetch workspace data using your fetchWorkspaceById + const fetchWorkspaceData = useCallback(async () => { + if (!environment) return; + + setWorkspaceLoading(true); + try { + const apiKey = environment.environmentApikey; + const apiServiceUrl = environment.environmentApiServiceUrl; + + if (!apiKey || !apiServiceUrl) { + setWorkspaceError("Missing API key or service URL"); + return; + } + + const data = await fetchWorkspaceById( + environmentId, + workspaceId, + apiKey, + apiServiceUrl + ); + if (data) { + setWorkspace(data); + } else { + setWorkspaceError("Workspace not found"); + } + } catch (err) { + setWorkspaceError( + err instanceof Error ? err.message : "Failed to fetch workspace" + ); + } finally { + setWorkspaceLoading(false); + } + }, [environment, environmentId, workspaceId]); + + // Function to fetch apps + const fetchAppsData = useCallback(async () => { + if (!environment || !workspace) return; + + setAppsLoading(true); + try { + const apiKey = environment.environmentApikey; + const apiServiceUrl = environment.environmentApiServiceUrl; + + if (!apiKey || !apiServiceUrl) { + setAppsError("Missing API key or service URL"); + return; + } + + const data = await getWorkspaceApps(workspace.id, apiKey, apiServiceUrl); + setApps(data); + } catch (err) { + setAppsError(err instanceof Error ? err.message : "Failed to fetch apps"); + } finally { + setAppsLoading(false); + } + }, [environment, workspace]); + + // Chain the useEffects to sequence the data fetching + useEffect(() => { + fetchEnvironmentData(); + }, [fetchEnvironmentData]); + + useEffect(() => { + if (environment) { + fetchWorkspaceData(); + } + }, [environment, fetchWorkspaceData]); + + useEffect(() => { + if (environment && workspace) { + fetchAppsData(); + } + }, [environment, workspace, fetchAppsData]); + + // App statistics + const appStats = { + total: apps.length, + published: apps.filter((app) => app.published).length, + }; + + return { + // Environment data + environment, + environmentLoading, + environmentError, + refreshEnvironment: fetchEnvironmentData, + + // Workspace data + workspace, + workspaceLoading, + workspaceError, + refreshWorkspace: fetchWorkspaceData, + + // Apps data + apps, + appsLoading, + appsError, + refreshApps: fetchAppsData, + appStats, + + // Overall loading state + isLoading: environmentLoading || workspaceLoading, + hasError: !!(environmentError || workspaceError), + }; +}; diff --git a/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts b/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts index fc34d51a04..b7378fdad7 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts +++ b/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts @@ -3,6 +3,7 @@ import { message } from "antd"; import { Environment } from "../types/environment.types"; import { Workspace } from "../types/workspace.types"; import { UserGroup } from "../types/userGroup.types"; +import {App} from "../types/app.types"; /** * Fetch all environments @@ -170,4 +171,99 @@ export async function getEnvironmentUserGroups( message.error(errorMessage); throw error; } -} \ No newline at end of file +} + + + + +/* ================================================================================ + +=============================== WorkSpace Details ============================ */ + + +/** + * Get a specific workspace by ID from the list of workspaces + * @param workspaces - Array of workspaces + * @param workspaceId - ID of the workspace to find + * @returns The found workspace or null if not found + */ +export function getWorkspaceById(workspaces: Workspace[], workspaceId: string): Workspace | null { + if (!workspaces || !workspaceId) { + return null; + } + + return workspaces.find(workspace => workspace.id === workspaceId) || null; +} + +/** + * Fetch a specific workspace from an environment + * @param environmentId - ID of the environment + * @param workspaceId - ID of the workspace to fetch + * @param apiKey - API key for the environment + * @param apiServiceUrl - API service URL for the environment + * @returns Promise with the workspace or null if not found + */ +export async function fetchWorkspaceById( + environmentId: string, + workspaceId: string, + apiKey: string, + apiServiceUrl: string +): Promise { + try { + // First fetch all workspaces for the environment + const workspaces = await getEnvironmentWorkspaces(environmentId, apiKey, apiServiceUrl); + + // Then find the specific workspace by ID + return getWorkspaceById(workspaces, workspaceId); + } catch (error) { + throw error; + } +} + + +export async function getWorkspaceApps( + workspaceId: string, + apiKey: string, + apiServiceUrl: string +): Promise { + try { + // Check if required parameters are provided + if (!workspaceId) { + throw new Error('Workspace ID is required'); + } + + if (!apiKey) { + throw new Error('API key is required to fetch apps'); + } + + if (!apiServiceUrl) { + throw new Error('API service URL is required to fetch apps'); + } + + // Set up headers with the Bearer token format + const headers = { + Authorization: `Bearer ${apiKey}` + }; + + // Make the API request to get apps + // Include the orgId as a query parameter if needed + const response = await axios.get(`${apiServiceUrl}/api/applications/list`, { + headers, + params: { + orgId: workspaceId + } + }); + + // Check if response is valid + if (!response.data || !response.data.data) { + return []; + } + + return response.data.data; + } catch (error) { + // Handle and transform error + const errorMessage = error instanceof Error ? error.message : 'Failed to fetch apps'; + message.error(errorMessage); + throw error; + } +} 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 new file mode 100644 index 0000000000..984228d6e2 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/types/app.types.ts @@ -0,0 +1,25 @@ +export interface App { + orgId: string; + applicationId: string; + applicationGid: string; + name: string; + createAt: number; + createBy: string; + role: string; + applicationType: number; + applicationStatus: string; + folderId: string | null; + lastViewTime: number; + lastModifyTime: number; + lastEditedAt: number; + publicToAll: boolean; + publicToMarketplace: boolean; + agencyProfile: boolean; + editingUserId: string | null; + title: string; + description: string; + category: string; + icon: string; + published: boolean; + folder: boolean; + } \ No newline at end of file From fe7e3c1fdf7d811c8f2228a7c3877427c56ad3c5 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 10 Apr 2025 22:17:29 +0500 Subject: [PATCH 10/68] add click on workspace list --- .../pages/setting/environments/EnvironmentDetail.tsx | 1 + .../environments/components/WorkspacesList.tsx | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx index 362ff097b9..360b4c5e7e 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx @@ -281,6 +281,7 @@ const EnvironmentDetail: React.FC = () => { workspaces={workspaces} loading={workspacesLoading && !workspacesError} error={workspacesError} + environmentId={environment.environmentId} /> diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/WorkspacesList.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/WorkspacesList.tsx index c5357e9576..051b51ef5a 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/WorkspacesList.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/WorkspacesList.tsx @@ -1,11 +1,14 @@ import React from 'react'; import { Table, Tag, Empty, Spin } from 'antd'; import { Workspace } from '../types/workspace.types'; +import history from '@lowcoder-ee/util/history'; +import { buildEnvironmentWorkspaceId } from '@lowcoder-ee/constants/routesURL'; interface WorkspacesListProps { workspaces: Workspace[]; loading: boolean; error?: string | null; + environmentId: string } /** @@ -15,6 +18,7 @@ const WorkspacesList: React.FC = ({ workspaces, loading, error, + environmentId }) => { // Format timestamp to date string const formatDate = (timestamp?: number): string => { @@ -23,6 +27,10 @@ const WorkspacesList: React.FC = ({ return `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`; }; + const handleRowClick = (workspace: Workspace) => { + history.push(`${buildEnvironmentWorkspaceId(environmentId, workspace.id)}`); + }; + // Table columns definition const columns = [ { @@ -87,6 +95,10 @@ const WorkspacesList: React.FC = ({ rowKey="id" pagination={{ pageSize: 10 }} size="middle" + onRow={(record) => ({ + onClick: () => handleRowClick(record), + style: { cursor: 'pointer' } // Add pointer cursor to indicate clickable rows + })} /> ); }; From d39b6a09176288498e0facde7f3ada4a553b645b Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 10 Apr 2025 22:27:59 +0500 Subject: [PATCH 11/68] fix filtering of apps --- .../setting/environments/services/environments.service.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts b/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts index b7378fdad7..0df2f3f2e6 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts +++ b/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts @@ -259,7 +259,10 @@ export async function getWorkspaceApps( return []; } - return response.data.data; + const filteredApps = response.data.data.filter((app: App) => app.orgId === workspaceId); + + return filteredApps; + } catch (error) { // Handle and transform error const errorMessage = error instanceof Error ? error.message : 'Failed to fetch apps'; From f5987e2f947c4e483aa7cd028db454196f61cadd Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 10 Apr 2025 22:53:38 +0500 Subject: [PATCH 12/68] setup data sources structure --- .../setting/environments/WorkspaceDetail.tsx | 68 ++++++++- .../components/DataSourcesList.tsx | 141 ++++++++++++++++++ .../environments/hooks/useWorkspaceDetail.ts | 53 ++++++- .../services/environments.service.ts | 65 ++++++++ .../environments/types/datasource.types.ts | 40 +++++ 5 files changed, 360 insertions(+), 7 deletions(-) create mode 100644 client/packages/lowcoder/src/pages/setting/environments/components/DataSourcesList.tsx create mode 100644 client/packages/lowcoder/src/pages/setting/environments/types/datasource.types.ts diff --git a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx index ab6255b49b..b6218559a7 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx @@ -2,6 +2,8 @@ import React, { useEffect, useState } from "react"; import { useParams, useHistory } from "react-router-dom"; import history from "@lowcoder-ee/util/history"; import { useWorkspaceDetail } from "./hooks/useWorkspaceDetail"; +import DataSourcesList from './components/DataSourcesList'; + import { @@ -44,7 +46,6 @@ const WorkspaceDetail: React.FC = () => { workspaceId: string; }>(); - // Use the custom hook const { environment, workspace, @@ -53,6 +54,11 @@ const WorkspaceDetail: React.FC = () => { appsError, refreshApps, appStats, + dataSources, + dataSourcesLoading, + dataSourcesError, + refreshDataSources, + dataSourceStats, isLoading, hasError } = useWorkspaceDetail(environmentId, workspaceId); @@ -185,16 +191,66 @@ if (isLoading) { + {/* Update the TabPane in WorkspaceDetail.tsx */} Data Sources} key="dataSources" > - + Data Sources in this Workspace + + + + {/* Data Source Statistics */} + + + } + /> + + + } + /> + + + + + + {/* Show error if data sources loading failed */} + {dataSourcesError && ( + + Try Again + + } + /> + )} + + {/* Data Sources List */} + diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/DataSourcesList.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/DataSourcesList.tsx new file mode 100644 index 0000000000..be6677f211 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/components/DataSourcesList.tsx @@ -0,0 +1,141 @@ +import React from 'react'; +import { Table, Tag, Empty, Spin, Badge, Tooltip } from 'antd'; +import { + DatabaseOutlined, + UserOutlined, + CheckCircleOutlined, + CloseCircleOutlined +} from '@ant-design/icons'; +import { DataSourceWithMeta } from '../types/datasource.types'; + +interface DataSourcesListProps { + dataSources: DataSourceWithMeta[]; + loading: boolean; + error?: string | null; +} + +/** + * Component to display a list of data sources in a table + */ +const DataSourcesList: React.FC = ({ + dataSources, + loading, + error, +}) => { + // Format timestamp to date string + const formatDate = (timestamp?: number): string => { + if (!timestamp) return 'N/A'; + const date = new Date(timestamp); + return `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`; + }; + + // Get icon for data source type + const getDataSourceTypeIcon = (type: string) => { + return ; + }; + + // Get color for data source status + const getStatusColor = (status: string) => { + switch (status) { + case 'NORMAL': + return 'green'; + case 'ERROR': + return 'red'; + case 'WARNING': + return 'orange'; + default: + return 'default'; + } + }; + + // Table columns definition + const columns = [ + { + title: 'Name', + key: 'name', + render: (record: DataSourceWithMeta) => ( +
+ {getDataSourceTypeIcon(record.datasource.type)} + {record.datasource.name} +
+ ), + }, + { + title: 'Type', + dataIndex: ['datasource', 'type'], + key: 'type', + render: (type: string) => ( + {type.toUpperCase()} + ), + }, + { + title: 'Created By', + dataIndex: 'creatorName', + key: 'creatorName', + render: (creatorName: string) => ( +
+ + {creatorName} +
+ ), + }, + { + title: 'Created', + key: 'createTime', + render: (record: DataSourceWithMeta) => formatDate(record.datasource.createTime), + }, + { + title: 'Status', + key: 'status', + render: (record: DataSourceWithMeta) => ( + + {record.datasource.datasourceStatus} + + ), + }, + { + title: 'Edit Access', + dataIndex: 'edit', + key: 'edit', + render: (edit: boolean) => ( + + {edit ? + : + + } + + ), + }, + ]; + + // If loading, show spinner + if (loading) { + return ( +
+ +
+ ); + } + + // If no data sources or error, show empty state + if (!dataSources || dataSources.length === 0 || error) { + return ( + + ); + } + + return ( +
record.datasource.id} + pagination={{ pageSize: 10 }} + size="middle" + /> + ); +}; + +export default DataSourcesList; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaceDetail.ts b/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaceDetail.ts index 803a8f3411..a8dacbfcde 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaceDetail.ts +++ b/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaceDetail.ts @@ -1,8 +1,9 @@ import { useState, useEffect, useCallback } from "react"; -import { getEnvironmentById, fetchWorkspaceById, getWorkspaceApps} from "../services/environments.service"; +import { getEnvironmentById, fetchWorkspaceById, getWorkspaceApps, getWorkspaceDataSources} from "../services/environments.service"; import { Environment } from "../types/environment.types"; import { Workspace } from "../types/workspace.types"; import { App } from "../types/app.types"; +import { DataSourceWithMeta } from '../types/datasource.types'; export const useWorkspaceDetail = ( environmentId: string, workspaceId: string @@ -22,6 +23,11 @@ export const useWorkspaceDetail = ( const [appsLoading, setAppsLoading] = useState(false); const [appsError, setAppsError] = useState(null); + // Data Sources state + const [dataSources, setDataSources] = useState([]); + const [dataSourcesLoading, setDataSourcesLoading] = useState(false); + const [dataSourcesError, setDataSourcesError] = useState(null); + // Function to fetch environment data const fetchEnvironmentData = useCallback(async () => { // Similar to your existing function @@ -95,6 +101,36 @@ export const useWorkspaceDetail = ( } }, [environment, workspace]); + + // Function to fetch data sources + const fetchDataSourcesData = useCallback(async () => { + if (!environment || !workspace) return; + + setDataSourcesLoading(true); + setDataSourcesError(null); + + try { + const apiKey = environment.environmentApikey; + const apiServiceUrl = environment.environmentApiServiceUrl; + + if (!apiKey || !apiServiceUrl) { + setDataSourcesError("Missing API key or service URL"); + setDataSourcesLoading(false); + return; + } + + const data = await getWorkspaceDataSources(workspace.id, apiKey, apiServiceUrl); + setDataSources(data); + } catch (err) { + setDataSourcesError(err instanceof Error ? err.message : "Failed to fetch data sources"); + } finally { + setDataSourcesLoading(false); + } + }, [environment, workspace]); + + + + // Chain the useEffects to sequence the data fetching useEffect(() => { fetchEnvironmentData(); @@ -109,6 +145,8 @@ export const useWorkspaceDetail = ( useEffect(() => { if (environment && workspace) { fetchAppsData(); + fetchDataSourcesData(); + } }, [environment, workspace, fetchAppsData]); @@ -118,6 +156,12 @@ export const useWorkspaceDetail = ( published: apps.filter((app) => app.published).length, }; + // Data Source statistics + const dataSourceStats = { + total: dataSources.length, + types: [...new Set(dataSources.map(ds => ds.datasource.type))].length, + }; + return { // Environment data environment, @@ -138,6 +182,13 @@ export const useWorkspaceDetail = ( refreshApps: fetchAppsData, appStats, + // Data Sources data + dataSources, + dataSourcesLoading, + dataSourcesError, + refreshDataSources: fetchDataSourcesData, + dataSourceStats, + // Overall loading state isLoading: environmentLoading || workspaceLoading, hasError: !!(environmentError || workspaceError), diff --git a/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts b/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts index 0df2f3f2e6..6898afd162 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts +++ b/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts @@ -4,6 +4,8 @@ import { Environment } from "../types/environment.types"; import { Workspace } from "../types/workspace.types"; import { UserGroup } from "../types/userGroup.types"; import {App} from "../types/app.types"; +import { DataSourceWithMeta } from '../types/datasource.types'; + /** * Fetch all environments @@ -220,6 +222,11 @@ export async function fetchWorkspaceById( } } +/* ================================================================================ + +=============================== WorkSpace Apps ============================ */ + + export async function getWorkspaceApps( workspaceId: string, @@ -270,3 +277,61 @@ export async function getWorkspaceApps( throw error; } } + + +/* ================================================================================ + +=============================== WorkSpace Data Source ============================ */ + +/** + * Fetch data sources for a specific workspace + * @param workspaceId - ID of the workspace (orgId) + * @param apiKey - API key for the environment + * @param apiServiceUrl - API service URL for the environment + * @returns Promise with an array of data sources + */ +export async function getWorkspaceDataSources( + workspaceId: string, + apiKey: string, + apiServiceUrl: string +): Promise { + try { + // Check if required parameters are provided + if (!workspaceId) { + throw new Error('Workspace ID is required'); + } + + if (!apiKey) { + throw new Error('API key is required to fetch data sources'); + } + + if (!apiServiceUrl) { + throw new Error('API service URL is required to fetch data sources'); + } + + // Set up headers with the Bearer token format + const headers = { + Authorization: `Bearer ${apiKey}` + }; + + // Make the API request to get data sources + const response = await axios.get(`${apiServiceUrl}/api/datasources/listByOrg`, { + headers, + params: { + orgId: workspaceId + } + }); + + // Check if response is valid + if (!response.data) { + return []; + } + + return response.data; + } catch (error) { + // Handle and transform error + const errorMessage = error instanceof Error ? error.message : 'Failed to fetch data sources'; + message.error(errorMessage); + throw error; + } +} \ 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 new file mode 100644 index 0000000000..2209868030 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/types/datasource.types.ts @@ -0,0 +1,40 @@ +/** + * Represents a DataSource configuration + */ +export interface DataSourceConfig { + usingUri: boolean; + srvMode: boolean; + ssl: boolean; + endpoints: any[]; + host: string | null; + port: number; + database: string | null; + username: string; + authMechanism: string | null; + } + + /** + * Represents a DataSource entity + */ + export interface DataSource { + id: string; + createdBy: string; + gid: string; + name: string; + type: string; + organizationId: string; + creationSource: number; + datasourceStatus: string; + pluginDefinition: any | null; + createTime: number; + datasourceConfig: DataSourceConfig; + } + + /** + * Represents a DataSource with additional metadata + */ + export interface DataSourceWithMeta { + datasource: DataSource; + edit: boolean; + creatorName: string; + } \ No newline at end of file From 8c11ef5c0404d8657d65e2fd0424f898f42093c2 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 10 Apr 2025 23:44:39 +0500 Subject: [PATCH 13/68] fix data source error --- .../setting/environments/services/environments.service.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts b/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts index 6898afd162..ccff1975f0 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts +++ b/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts @@ -315,19 +315,20 @@ export async function getWorkspaceDataSources( }; // Make the API request to get data sources - const response = await axios.get(`${apiServiceUrl}/api/datasources/listByOrg`, { + const response = await axios.get<{data:DataSourceWithMeta[]}>(`${apiServiceUrl}/api/datasources/listByOrg`, { headers, params: { orgId: workspaceId } }); - + console.log("data source response",response); + // Check if response is valid if (!response.data) { return []; } - return response.data; + return response.data.data ; } catch (error) { // Handle and transform error const errorMessage = error instanceof Error ? error.message : 'Failed to fetch data sources'; From d9f7dd5017dda9cff6be49e56a0635f91ae876d4 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Fri, 11 Apr 2025 01:34:10 +0500 Subject: [PATCH 14/68] Add Environment Context --- .../setting/environments/Environments.tsx | 22 +++--- .../components/EnvironmentScopedRoutes.tsx | 31 ++++++++ .../context/EnvironmentContext.tsx | 74 +++++++++++++++++++ 3 files changed, 114 insertions(+), 13 deletions(-) create mode 100644 client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentScopedRoutes.tsx create mode 100644 client/packages/lowcoder/src/pages/setting/environments/context/EnvironmentContext.tsx diff --git a/client/packages/lowcoder/src/pages/setting/environments/Environments.tsx b/client/packages/lowcoder/src/pages/setting/environments/Environments.tsx index 2095dd835d..7d5eaa5218 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/Environments.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/Environments.tsx @@ -1,28 +1,24 @@ -// environments/Environments.tsx import React from "react"; -import { Switch, Route, useRouteMatch } from "react-router-dom"; -import EnvironmentsList from "./EnvironmentsList"; // Rename your current component -import EnvironmentDetail from "./EnvironmentDetail"; -import WorkspaceDetail from "./WorkspaceDetail"; -import { ENVIRONMENT_WORKSPACE_DETAIL } from "@lowcoder-ee/constants/routesURL"; +import { Switch, Route } from "react-router-dom"; +import EnvironmentsList from "./EnvironmentsList"; +import EnvironmentScopedRoutes from "./components/EnvironmentScopedRoutes"; import { ENVIRONMENT_SETTING, - ENVIRONMENT_DETAIL, + ENVIRONMENT_DETAIL } from "@lowcoder-ee/constants/routesURL"; const Environments: React.FC = () => { return ( - - + {/* Route that shows the list of environments */} + + + {/* All other routes under /environments/:envId */} - - - - + ); diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentScopedRoutes.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentScopedRoutes.tsx new file mode 100644 index 0000000000..f8ffd44705 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentScopedRoutes.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import { Switch, Route, useParams } from "react-router-dom"; +import { EnvironmentProvider } from "../context/EnvironmentContext"; + +import EnvironmentDetail from "../EnvironmentDetail"; +import WorkspaceDetail from "../WorkspaceDetail"; + +import { + ENVIRONMENT_DETAIL, + ENVIRONMENT_WORKSPACE_DETAIL, +} from "@lowcoder-ee/constants/routesURL"; + +const EnvironmentScopedRoutes: React.FC = () => { + const { environmentId } = useParams<{ environmentId: string }>(); + + return ( + + + + + + + + + + + + ); +}; + +export default EnvironmentScopedRoutes; diff --git a/client/packages/lowcoder/src/pages/setting/environments/context/EnvironmentContext.tsx b/client/packages/lowcoder/src/pages/setting/environments/context/EnvironmentContext.tsx new file mode 100644 index 0000000000..c81500ae32 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/context/EnvironmentContext.tsx @@ -0,0 +1,74 @@ +// src/contexts/EnvironmentContext.tsx +import React, { + createContext, + useContext, + useEffect, + useState, + useCallback, + ReactNode, + } from "react"; + import { useHistory } from "react-router-dom"; + import { getEnvironmentById } from "../services/environments.service"; + import { Environment } from "../types/environment.types"; + + interface EnvironmentContextType { + environment: Environment | null; + loading: boolean; + error: string | null; + refresh: () => void; + } + + 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 { + envId: string; + children: ReactNode; + } + + export const EnvironmentProvider: React.FC = ({ envId, children }) => { + const [environment, setEnvironment] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const history = useHistory(); + + const fetchEnvironment = useCallback(async () => { + setLoading(true); + setError(null); + try { + const data = await getEnvironmentById(envId); + console.log("Environment data:", data); + setEnvironment(data); + } catch (err) { + setError("Environment not found or failed to load"); + history.push("/404"); // or a centralized error route + } finally { + setLoading(false); + } + }, [envId, history]); + + useEffect(() => { + fetchEnvironment(); + }, [fetchEnvironment]); + + const value: EnvironmentContextType = { + environment, + loading, + error, + refresh: fetchEnvironment, + }; + + return ( + + {children} + + ); + }; + \ No newline at end of file From 46911b0e65c70188e4dd0128f4d0fccfd649ceb2 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Fri, 11 Apr 2025 02:19:06 +0500 Subject: [PATCH 15/68] Add Workspace and UserGroup hooks --- .../environments/EnvironmentDetail.tsx | 45 ++++++----- .../hooks/useEnvironmentUserGroups.ts | 81 +++++++++++++++++++ .../hooks/useEnvironmentWorkspaces.ts | 81 +++++++++++++++++++ 3 files changed, 189 insertions(+), 18 deletions(-) create mode 100644 client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironmentUserGroups.ts create mode 100644 client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironmentWorkspaces.ts diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx index 360b4c5e7e..1bd1bc05e8 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx @@ -25,6 +25,9 @@ import { import { useEnvironmentDetail } from "./hooks/useEnvironmentDetail"; import WorkspacesList from "./components/WorkspacesList"; import UserGroupsList from "./components/UserGroupsList"; +import { useEnvironmentContext } from "./context/EnvironmentContext"; +import { useEnvironmentWorkspaces } from "./hooks/useEnvironmentWorkspaces"; +import { useEnvironmentUserGroups } from "./hooks/useEnvironmentUserGroups"; const { Title, Text } = Typography; const { TabPane } = Tabs; @@ -35,29 +38,35 @@ const { TabPane } = Tabs; */ const EnvironmentDetail: React.FC = () => { // Get environment ID from URL params - const { environmentId: id } = useParams<{ environmentId: string }>(); - - // Use the custom hook to handle data fetching and state management - // Use the custom hook to handle data fetching and state management const { environment, - loading, - error, + loading: envLoading, + error: envError, refresh, + } = useEnvironmentContext(); + + + const { workspaces, - workspacesLoading, - workspacesError, - refreshWorkspaces, + loading: workspacesLoading, + error: workspacesError, + refresh: refreshWorkspaces, workspaceStats, + } = useEnvironmentWorkspaces(environment); + + const { userGroups, - userGroupsLoading, - userGroupsError, - refreshUserGroups, - userGroupStats - - } = useEnvironmentDetail(id); + loading: userGroupsLoading, + error: userGroupsError, + refresh: refreshUserGroups, + userGroupStats, + } = useEnvironmentUserGroups(environment); + + // Use the custom hook to handle data fetching and state management + // Use the custom hook to handle data fetching and state management + // If loading, show spinner - if (loading) { + if (envLoading) { return (
{ } // If error, show error message - if (error) { + if (envError) { return ( { + const [userGroups, setUserGroups] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchUserGroups = useCallback(async () => { + if (!environment) return; + + setLoading(true); + setError(null); + + try { + const { environmentId, environmentApikey, environmentApiServiceUrl } = environment; + + if (!environmentApikey) { + setError("No API key configured for this environment. User groups cannot be fetched."); + setLoading(false); + return; + } + + if (!environmentApiServiceUrl) { + setError("No API service URL configured for this environment. User groups cannot be fetched."); + setLoading(false); + return; + } + + const data = await getEnvironmentUserGroups( + environmentId, + environmentApikey, + environmentApiServiceUrl + ); + + setUserGroups(data); + } catch (err) { + setError( + err instanceof Error + ? err.message + : "Failed to fetch user groups" + ); + } finally { + setLoading(false); + } + }, [environment]); + + useEffect(() => { + if (environment) { + fetchUserGroups(); + } + }, [environment, fetchUserGroups]); + + const userGroupStats: UserGroupStats = { + total: userGroups.length, + totalUsers: userGroups.reduce((sum, group) => sum + (group.stats?.userCount ?? 0), 0), + adminUsers: userGroups.reduce((sum, group) => sum + (group.stats?.adminUserCount ?? 0), 0), + apiKeyConfigured: !!environment?.environmentApikey, + apiServiceUrlConfigured: !!environment?.environmentApiServiceUrl, + }; + + return { + userGroups, + loading, + error, + refresh: fetchUserGroups, + userGroupStats, + }; +}; diff --git a/client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironmentWorkspaces.ts b/client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironmentWorkspaces.ts new file mode 100644 index 0000000000..0f949037bf --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironmentWorkspaces.ts @@ -0,0 +1,81 @@ +import { useState, useEffect, useCallback } from "react"; +import { getEnvironmentWorkspaces } from "../services/environments.service"; +import { Environment } from "../types/environment.types"; +import { Workspace } from "../types/workspace.types"; + +interface WorkspaceStats { + total: number; + managed: number; + unmanaged: number; + apiKeyConfigured: boolean; + apiServiceUrlConfigured: boolean; +} + +export const useEnvironmentWorkspaces = ( + environment: Environment | null +) => { + const [workspaces, setWorkspaces] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchWorkspaces = useCallback(async () => { + if (!environment) return; + + setLoading(true); + setError(null); + + try { + const { environmentId, environmentApikey, environmentApiServiceUrl } = environment; + + if (!environmentApikey) { + setError("No API key configured for this environment. Workspaces cannot be fetched."); + setLoading(false); + return; + } + + if (!environmentApiServiceUrl) { + setError("No API service URL configured for this environment. Workspaces cannot be fetched."); + setLoading(false); + return; + } + + const data = await getEnvironmentWorkspaces( + environmentId, + environmentApikey, + environmentApiServiceUrl + ); + + setWorkspaces(data); + } catch (err) { + setError( + err instanceof Error + ? err.message + : "Failed to fetch workspaces" + ); + } finally { + setLoading(false); + } + }, [environment]); + + useEffect(() => { + if (environment) { + fetchWorkspaces(); + } + }, [environment, fetchWorkspaces]); + + const workspaceStats: WorkspaceStats = { + total: workspaces.length, + managed: 0, // logic to be added later + unmanaged: workspaces.length, // logic to be added later + apiKeyConfigured: !!environment?.environmentApikey, + apiServiceUrlConfigured: !!environment?.environmentApiServiceUrl, + }; + + return { + workspaces, + loading, + error, + refresh: fetchWorkspaces, + workspaceStats, + }; +}; From 9f2c181ecdc0bb12abc55cac1dcfd09f4bf40773 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Fri, 11 Apr 2025 02:54:47 +0500 Subject: [PATCH 16/68] Add useWorkspace hook --- .../setting/environments/WorkspaceDetail.tsx | 92 +++++++++---------- .../environments/hooks/useWorkspace.ts | 65 +++++++++++++ 2 files changed, 107 insertions(+), 50 deletions(-) create mode 100644 client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspace.ts diff --git a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx index b6218559a7..0031038749 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx @@ -29,71 +29,63 @@ import { SyncOutlined, ArrowLeftOutlined } from "@ant-design/icons"; -import { getEnvironmentById } from './services/environments.service'; import AppsList from './components/AppsList'; -import { Workspace } from './types/workspace.types'; -import { Environment } from './types/environment.types'; -import { App } from './types/app.types'; +import { useEnvironmentContext } from "./context/EnvironmentContext"; +import { useWorkspace } from "./hooks/useWorkspace"; + + const { Title, Text } = Typography; const { TabPane } = Tabs; const WorkspaceDetail: React.FC = () => { + // Get parameters from URL - const { environmentId, workspaceId } = useParams<{ - environmentId: string; + const { environmentId,workspaceId } = useParams<{ workspaceId: string; + environmentId: string; }>(); - const { environment, + loading: envLoading, + error: envError, + refresh: refreshEnvironment, + } = useEnvironmentContext(); + + const { workspace, - apps, - appsLoading, - appsError, - refreshApps, - appStats, - dataSources, - dataSourcesLoading, - dataSourcesError, - refreshDataSources, - dataSourceStats, - isLoading, - hasError - } = useWorkspaceDetail(environmentId, workspaceId); + loading: workspaceLoading, + error: workspaceError, + refresh: refreshWorkspace + } = useWorkspace(environment, workspaceId); + + - // Handle loading/error states - if (isLoading) { - return ; + if (envLoading || workspaceLoading) { + return ( +
+ +
+ ); + } + + if (envError || workspaceError || !environment || !workspace) { + return ( + history.push(`/home/settings/environments/${environmentId}`)}> + Back to Environment + + } + /> + ); } - - // Handle loading/error states -if (isLoading) { - return ( -
- -
- ); - } - - // Handle error state - if (hasError || !environment || !workspace) { - return ( - history.push(`/home/settings/environments/${environmentId}`)}> - Back to Environment - - } - /> - ); - } return (
diff --git a/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspace.ts b/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspace.ts new file mode 100644 index 0000000000..0520fea0d4 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspace.ts @@ -0,0 +1,65 @@ +import { useState, useEffect, useCallback } from "react"; +import { useHistory } from "react-router-dom"; +import { fetchWorkspaceById } from "../services/environments.service"; +import { Environment } from "../types/environment.types"; +import { Workspace } from "../types/workspace.types"; + +export const useWorkspace = ( + environment: Environment | null, + workspaceId: string +) => { + const [workspace, setWorkspace] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const history = useHistory(); + + const fetchWorkspace = useCallback(async () => { + if (!environment) return; + + setLoading(true); + setError(null); + + try { + const { environmentId, environmentApikey, environmentApiServiceUrl } = environment; + + if (!environmentApikey || !environmentApiServiceUrl) { + setError("Missing API key or service URL for this environment."); + setLoading(false); + return; + } + + const data = await fetchWorkspaceById( + environmentId, + workspaceId, + environmentApikey, + environmentApiServiceUrl + ); + + setWorkspace(data); + } catch (err) { + setError( + err instanceof Error + ? err.message + : "Failed to fetch workspace details" + ); + + // Optional: redirect to environment detail if workspace fetch fails + // history.push(`/home/settings/environments/${environment.environmentId}`); + } finally { + setLoading(false); + } + }, [environment, workspaceId, history]); + + useEffect(() => { + if (environment) { + fetchWorkspace(); + } + }, [environment, fetchWorkspace]); + + return { + workspace, + loading, + error, + refresh: fetchWorkspace, + }; +}; From bf39d50e406b1f6627ec0b6bbfb26961248b983b Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Fri, 11 Apr 2025 03:14:33 +0500 Subject: [PATCH 17/68] refactor workspace detail page --- .../setting/environments/WorkspaceDetail.tsx | 24 +++++-- .../environments/hooks/useWorkspaceApps.ts | 68 ++++++++++++++++++ .../hooks/useWorkspaceDataSources.ts | 70 +++++++++++++++++++ 3 files changed, 156 insertions(+), 6 deletions(-) create mode 100644 client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaceApps.ts create mode 100644 client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaceDataSources.ts diff --git a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx index 0031038749..7b3d9479a8 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx @@ -1,11 +1,7 @@ import React, { useEffect, useState } from "react"; import { useParams, useHistory } from "react-router-dom"; import history from "@lowcoder-ee/util/history"; -import { useWorkspaceDetail } from "./hooks/useWorkspaceDetail"; import DataSourcesList from './components/DataSourcesList'; - - - import { Spin, Typography, @@ -20,7 +16,6 @@ import { Breadcrumb } from "antd"; import { - ReloadOutlined, AppstoreOutlined, DatabaseOutlined, CodeOutlined, @@ -32,7 +27,8 @@ import { import AppsList from './components/AppsList'; import { useEnvironmentContext } from "./context/EnvironmentContext"; import { useWorkspace } from "./hooks/useWorkspace"; - +import { useWorkspaceApps } from "./hooks/useWorkspaceApps"; +import { useWorkspaceDataSources } from "./hooks/useWorkspaceDataSources"; const { Title, Text } = Typography; @@ -59,6 +55,22 @@ const WorkspaceDetail: React.FC = () => { error: workspaceError, refresh: refreshWorkspace } = useWorkspace(environment, workspaceId); + + const { + apps, + loading: appsLoading, + error: appsError, + refresh: refreshApps, + appStats, + } = useWorkspaceApps(environment, workspaceId); + + const { + dataSources, + loading: dataSourcesLoading, + error: dataSourcesError, + refresh: refreshDataSources, + dataSourceStats, + } = useWorkspaceDataSources(environment, workspaceId); diff --git a/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaceApps.ts b/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaceApps.ts new file mode 100644 index 0000000000..d909a42bef --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaceApps.ts @@ -0,0 +1,68 @@ +import { useState, useEffect, useCallback } from "react"; +import { getWorkspaceApps } from "../services/environments.service"; +import { Environment } from "../types/environment.types"; +import { App } from "../types/app.types"; + +interface AppStats { + total: number; + published: number; +} + +export const useWorkspaceApps = ( + environment: Environment | null, + workspaceId: string +) => { + const [apps, setApps] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchApps = useCallback(async () => { + if (!environment || !workspaceId) return; + + setLoading(true); + setError(null); + + try { + const { environmentId, environmentApikey, environmentApiServiceUrl } = environment; + + if (!environmentApikey || !environmentApiServiceUrl) { + setError("Missing API key or service URL for this environment. Apps cannot be fetched."); + setLoading(false); + return; + } + + const data = await getWorkspaceApps( + workspaceId, + environmentApikey, + environmentApiServiceUrl + ); + + setApps(data); + } catch (err) { + setError( + err instanceof Error ? err.message : "Failed to fetch apps" + ); + } finally { + setLoading(false); + } + }, [environment, workspaceId]); + + useEffect(() => { + if (environment) { + fetchApps(); + } + }, [environment, fetchApps]); + + const appStats: AppStats = { + total: apps.length, + published: apps.filter(app => app.published).length, + }; + + return { + apps, + loading, + error, + refresh: fetchApps, + appStats, + }; +}; diff --git a/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaceDataSources.ts b/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaceDataSources.ts new file mode 100644 index 0000000000..d323b57775 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaceDataSources.ts @@ -0,0 +1,70 @@ +import { useState, useEffect, useCallback } from "react"; +import { getWorkspaceDataSources } from "../services/environments.service"; +import { Environment } from "../types/environment.types"; +import { DataSourceWithMeta } from "../types/datasource.types"; + +interface DataSourceStats { + total: number; + types: number; // unique types +} + +export const useWorkspaceDataSources = ( + environment: Environment | null, + workspaceId: string +) => { + const [dataSources, setDataSources] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchDataSources = useCallback(async () => { + if (!environment || !workspaceId) return; + + setLoading(true); + setError(null); + + try { + const { environmentApikey, environmentApiServiceUrl } = environment; + + if (!environmentApikey || !environmentApiServiceUrl) { + setError("Missing API key or service URL. Data sources cannot be fetched."); + setLoading(false); + return; + } + + const data = await getWorkspaceDataSources( + workspaceId, + environmentApikey, + environmentApiServiceUrl + ); + + setDataSources(data); + } catch (err) { + setError( + err instanceof Error ? err.message : "Failed to fetch data sources" + ); + } finally { + setLoading(false); + } + }, [environment, workspaceId]); + + useEffect(() => { + if (environment) { + fetchDataSources(); + } + }, [environment, fetchDataSources]); + + const uniqueTypes = new Set(dataSources.map(ds => ds.datasource.type)); + + const dataSourceStats: DataSourceStats = { + total: dataSources.length, + types: uniqueTypes.size, + }; + + return { + dataSources, + loading, + error, + refresh: fetchDataSources, + dataSourceStats, + }; +}; From 204890a5bd3402e1b52169def27272ddf1397379 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Fri, 11 Apr 2025 13:19:41 +0500 Subject: [PATCH 18/68] remove unused files --- .../environments/EnvironmentDetail.tsx | 1 - .../hooks/useEnvironmentDetail.ts | 204 ------------------ .../environments/hooks/useWorkspaceDetail.ts | 196 ----------------- 3 files changed, 401 deletions(-) delete mode 100644 client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironmentDetail.ts delete mode 100644 client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaceDetail.ts diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx index 1bd1bc05e8..489309a053 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx @@ -22,7 +22,6 @@ import { UserOutlined, SyncOutlined, } from "@ant-design/icons"; -import { useEnvironmentDetail } from "./hooks/useEnvironmentDetail"; import WorkspacesList from "./components/WorkspacesList"; import UserGroupsList from "./components/UserGroupsList"; import { useEnvironmentContext } from "./context/EnvironmentContext"; diff --git a/client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironmentDetail.ts b/client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironmentDetail.ts deleted file mode 100644 index 7e4d9f37e8..0000000000 --- a/client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironmentDetail.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { useState, useEffect, useCallback } from "react"; -import { - getEnvironmentById, - getEnvironmentWorkspaces, - getEnvironmentUserGroups, -} from "../services/environments.service"; -import { Environment } from "../types/environment.types"; -import { Workspace } from "../types/workspace.types"; -import { UserGroup } from "../types/userGroup.types"; - -/** - * Custom hook to fetch and manage environment detail data - * @param id - Environment ID to fetch - * @returns Object containing environment data, workspaces, user groups, loading states, error states, and refresh functions - */ -export const useEnvironmentDetail = (id: string) => { - // Environment state - const [environment, setEnvironment] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - // Workspaces state - const [workspaces, setWorkspaces] = useState([]); - const [workspacesLoading, setWorkspacesLoading] = useState(false); - const [workspacesError, setWorkspacesError] = useState(null); - - // User Groups state - const [userGroups, setUserGroups] = useState([]); - const [userGroupsLoading, setUserGroupsLoading] = useState(false); - const [userGroupsError, setUserGroupsError] = useState(null); - - // Function to fetch environment data - const fetchEnvironmentData = useCallback(async () => { - if (!id) { - setError("No environment ID provided"); - setLoading(false); - return; - } - - setLoading(true); - setError(null); - - try { - const data = await getEnvironmentById(id); - setEnvironment(data); - } catch (err) { - setError( - err instanceof Error - ? err.message - : "Failed to fetch environment details" - ); - } finally { - setLoading(false); - } - }, [id]); - - // Function to fetch workspaces - const fetchWorkspaces = useCallback(async () => { - // Don't fetch workspaces if environment is not loaded yet - if (!environment) { - return; - } - - setWorkspacesLoading(true); - setWorkspacesError(null); - - try { - // Get the API key and API service URL from the environment - const apiKey = environment.environmentApikey; - const apiServiceUrl = environment.environmentApiServiceUrl; - - // Check if both API key and API service URL are configured - if (!apiKey) { - setWorkspacesError( - "No API key configured for this environment. Workspaces cannot be fetched." - ); - setWorkspacesLoading(false); - return; - } - - if (!apiServiceUrl) { - setWorkspacesError( - "No API service URL configured for this environment. Workspaces cannot be fetched." - ); - setWorkspacesLoading(false); - return; - } - - // Call the function with environment ID, API key, and API service URL - const data = await getEnvironmentWorkspaces(id, apiKey, apiServiceUrl); - setWorkspaces(data); - } catch (err) { - setWorkspacesError( - err instanceof Error ? err.message : "Failed to fetch workspaces" - ); - } finally { - setWorkspacesLoading(false); - } - }, [environment, id]); - - // Function to fetch user groups - const fetchUserGroups = useCallback(async () => { - // Don't fetch user groups if environment is not loaded yet - if (!environment) { - return; - } - - setUserGroupsLoading(true); - setUserGroupsError(null); - - try { - // Get the API key and API service URL from the environment - const apiKey = environment.environmentApikey; - const apiServiceUrl = environment.environmentApiServiceUrl; - - // Check if both API key and API service URL are configured - if (!apiKey) { - setUserGroupsError( - "No API key configured for this environment. User groups cannot be fetched." - ); - setUserGroupsLoading(false); - return; - } - - if (!apiServiceUrl) { - setUserGroupsError( - "No API service URL configured for this environment. User groups cannot be fetched." - ); - setUserGroupsLoading(false); - return; - } - - // Call the function with environment ID, API key, and API service URL - const data = await getEnvironmentUserGroups(id, apiKey, apiServiceUrl); - setUserGroups(data); - } catch (err) { - setUserGroupsError( - err instanceof Error ? err.message : "Failed to fetch user groups" - ); - } finally { - setUserGroupsLoading(false); - } - }, [environment, id]); - - // Fetch environment data on mount and when ID changes - useEffect(() => { - fetchEnvironmentData(); - }, [fetchEnvironmentData]); - - // Fetch workspaces and user groups after environment is loaded - useEffect(() => { - if (environment) { - fetchWorkspaces(); - fetchUserGroups(); - } - }, [environment, fetchWorkspaces, fetchUserGroups]); - - // Calculate workspace statistics - const workspaceStats = { - total: workspaces.length, - managed: 0, // To be implemented later - unmanaged: workspaces.length, // To be implemented later - apiKeyConfigured: !!environment?.environmentApikey, - apiServiceUrlConfigured: !!environment?.environmentApiServiceUrl, - }; - - // Calculate user group statistics - const userGroupStats = { - total: userGroups.length, - totalUsers: userGroups.reduce( - (acc, group) => acc + group.stats.userCount, - 0 - ), - adminUsers: userGroups.reduce( - (acc, group) => acc + group.stats.adminUserCount, - 0 - ), - apiKeyConfigured: !!environment?.environmentApikey, - apiServiceUrlConfigured: !!environment?.environmentApiServiceUrl, - }; - - // Return the state and functions to refresh data - return { - // Environment data - environment, - loading, - error, - refresh: fetchEnvironmentData, - - // Workspaces data - workspaces, - workspacesLoading, - workspacesError, - refreshWorkspaces: fetchWorkspaces, - workspaceStats, - - // User Groups data - userGroups, - userGroupsLoading, - userGroupsError, - refreshUserGroups: fetchUserGroups, - userGroupStats, - }; -}; diff --git a/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaceDetail.ts b/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaceDetail.ts deleted file mode 100644 index a8dacbfcde..0000000000 --- a/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaceDetail.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { useState, useEffect, useCallback } from "react"; -import { getEnvironmentById, fetchWorkspaceById, getWorkspaceApps, getWorkspaceDataSources} from "../services/environments.service"; -import { Environment } from "../types/environment.types"; -import { Workspace } from "../types/workspace.types"; -import { App } from "../types/app.types"; -import { DataSourceWithMeta } from '../types/datasource.types'; -export const useWorkspaceDetail = ( - environmentId: string, - workspaceId: string -) => { - // Environment state - const [environment, setEnvironment] = useState(null); - const [environmentLoading, setEnvironmentLoading] = useState(true); - const [environmentError, setEnvironmentError] = useState(null); - - // Workspace state - const [workspace, setWorkspace] = useState(null); - const [workspaceLoading, setWorkspaceLoading] = useState(true); - const [workspaceError, setWorkspaceError] = useState(null); - - // Apps state - const [apps, setApps] = useState([]); - const [appsLoading, setAppsLoading] = useState(false); - const [appsError, setAppsError] = useState(null); - - // Data Sources state - const [dataSources, setDataSources] = useState([]); - const [dataSourcesLoading, setDataSourcesLoading] = useState(false); - const [dataSourcesError, setDataSourcesError] = useState(null); - - // Function to fetch environment data - const fetchEnvironmentData = useCallback(async () => { - // Similar to your existing function - setEnvironmentLoading(true); - try { - const data = await getEnvironmentById(environmentId); - setEnvironment(data); - } catch (err) { - setEnvironmentError( - err instanceof Error ? err.message : "Failed to fetch environment" - ); - } finally { - setEnvironmentLoading(false); - } - }, [environmentId]); - - // Function to fetch workspace data using your fetchWorkspaceById - const fetchWorkspaceData = useCallback(async () => { - if (!environment) return; - - setWorkspaceLoading(true); - try { - const apiKey = environment.environmentApikey; - const apiServiceUrl = environment.environmentApiServiceUrl; - - if (!apiKey || !apiServiceUrl) { - setWorkspaceError("Missing API key or service URL"); - return; - } - - const data = await fetchWorkspaceById( - environmentId, - workspaceId, - apiKey, - apiServiceUrl - ); - if (data) { - setWorkspace(data); - } else { - setWorkspaceError("Workspace not found"); - } - } catch (err) { - setWorkspaceError( - err instanceof Error ? err.message : "Failed to fetch workspace" - ); - } finally { - setWorkspaceLoading(false); - } - }, [environment, environmentId, workspaceId]); - - // Function to fetch apps - const fetchAppsData = useCallback(async () => { - if (!environment || !workspace) return; - - setAppsLoading(true); - try { - const apiKey = environment.environmentApikey; - const apiServiceUrl = environment.environmentApiServiceUrl; - - if (!apiKey || !apiServiceUrl) { - setAppsError("Missing API key or service URL"); - return; - } - - const data = await getWorkspaceApps(workspace.id, apiKey, apiServiceUrl); - setApps(data); - } catch (err) { - setAppsError(err instanceof Error ? err.message : "Failed to fetch apps"); - } finally { - setAppsLoading(false); - } - }, [environment, workspace]); - - - // Function to fetch data sources - const fetchDataSourcesData = useCallback(async () => { - if (!environment || !workspace) return; - - setDataSourcesLoading(true); - setDataSourcesError(null); - - try { - const apiKey = environment.environmentApikey; - const apiServiceUrl = environment.environmentApiServiceUrl; - - if (!apiKey || !apiServiceUrl) { - setDataSourcesError("Missing API key or service URL"); - setDataSourcesLoading(false); - return; - } - - const data = await getWorkspaceDataSources(workspace.id, apiKey, apiServiceUrl); - setDataSources(data); - } catch (err) { - setDataSourcesError(err instanceof Error ? err.message : "Failed to fetch data sources"); - } finally { - setDataSourcesLoading(false); - } - }, [environment, workspace]); - - - - - // Chain the useEffects to sequence the data fetching - useEffect(() => { - fetchEnvironmentData(); - }, [fetchEnvironmentData]); - - useEffect(() => { - if (environment) { - fetchWorkspaceData(); - } - }, [environment, fetchWorkspaceData]); - - useEffect(() => { - if (environment && workspace) { - fetchAppsData(); - fetchDataSourcesData(); - - } - }, [environment, workspace, fetchAppsData]); - - // App statistics - const appStats = { - total: apps.length, - published: apps.filter((app) => app.published).length, - }; - - // Data Source statistics - const dataSourceStats = { - total: dataSources.length, - types: [...new Set(dataSources.map(ds => ds.datasource.type))].length, - }; - - return { - // Environment data - environment, - environmentLoading, - environmentError, - refreshEnvironment: fetchEnvironmentData, - - // Workspace data - workspace, - workspaceLoading, - workspaceError, - refreshWorkspace: fetchWorkspaceData, - - // Apps data - apps, - appsLoading, - appsError, - refreshApps: fetchAppsData, - appStats, - - // Data Sources data - dataSources, - dataSourcesLoading, - dataSourcesError, - refreshDataSources: fetchDataSourcesData, - dataSourceStats, - - // Overall loading state - isLoading: environmentLoading || workspaceLoading, - hasError: !!(environmentError || workspaceError), - }; -}; From 058db378d5a21cd99b19aa02055f32537190f022 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Fri, 11 Apr 2025 16:29:51 +0500 Subject: [PATCH 19/68] add enterprise managed workspaces hook --- .../hooks/enterprise/useManagedWorkspaces.ts | 49 ++++++++++ .../services/enterprise.service.ts | 94 +++++++++++++++++++ .../environments/types/enterprise.types.ts | 9 ++ 3 files changed, 152 insertions(+) create mode 100644 client/packages/lowcoder/src/pages/setting/environments/hooks/enterprise/useManagedWorkspaces.ts create mode 100644 client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts create mode 100644 client/packages/lowcoder/src/pages/setting/environments/types/enterprise.types.ts diff --git a/client/packages/lowcoder/src/pages/setting/environments/hooks/enterprise/useManagedWorkspaces.ts b/client/packages/lowcoder/src/pages/setting/environments/hooks/enterprise/useManagedWorkspaces.ts new file mode 100644 index 0000000000..0ec3e40218 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/hooks/enterprise/useManagedWorkspaces.ts @@ -0,0 +1,49 @@ +import { useEffect, useState } from "react"; +import { ManagedOrg } from "../../types/enterprise.types"; +import { + getManagedWorkspaces, +} from "../../services/enterprise.service"; +import { Environment } from "../../types/environment.types"; + +export function useManagedWorkspaces( + environment: Environment | null +) { + const [managed, setManaged] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchManaged = async () => { + if (!environment) return; + setLoading(true); + setError(null); + + + try { + const { environmentId, environmentApikey, environmentApiServiceUrl } = environment; + + if (!environmentApikey || !environmentApiServiceUrl) { + setError("Missing API key or service URL for this environment."); + setLoading(false); + return; + } + + const result = await getManagedWorkspaces(environmentId, environmentApiServiceUrl); + setManaged(result); + } catch (err: any) { + setError(err.message ?? "Failed to load managed workspaces"); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchManaged(); + }, [environment, fetchManaged]); + + return { + managedWorkspaces: managed, + managedLoading: loading, + managedError: error, + refreshManagedWorkspaces: fetchManaged, + }; +} diff --git a/client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts b/client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts new file mode 100644 index 0000000000..1992ea2877 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts @@ -0,0 +1,94 @@ +import axios from "axios"; +import { message } from "antd"; +import { ManagedOrg } from "../types/enterprise.types"; + +/** + * Fetch workspaces for a specific environment + * @param apiServiceUrl - API service URL for the environment + * @param environmentId - ID of the environment + * + * + */ + +export async function getManagedWorkspaces( + environmentId: string, + apiServiceUrl: string +): Promise { + if (!environmentId || !apiServiceUrl) { + throw new Error("Missing environmentId or apiServiceUrl"); + } + + try { + const res = await axios.get(`${apiServiceUrl}/api/plugins/enterprise/org`); + const all: ManagedOrg[] = res.data; + return all.filter(org => org.environmentId === environmentId); + } catch (err) { + const errorMsg = err instanceof Error ? err.message : "Failed to fetch managed workspaces"; + message.error(errorMsg); + throw err; + } +} + + +/** + * Fetch workspaces for a specific environment + * @param apiServiceUrl - API service URL for the environment + * @param environmentId - ID of the environment + * @param orgName - Name of the workspace + * @param orgTags - Tags of the workspace + * + */ + +export async function connectManagedWorkspace( + environmentId: string, + apiServiceUrl: string, + orgName: string, + orgTags: string[] = [] +) { + if (!environmentId || !apiServiceUrl || !orgName) { + throw new Error("Missing required params to connect org"); + } + + try { + const payload = { + environment_id: environmentId, + org_name: orgName, + org_tags: orgTags, + }; + + const res = await axios.post(`${apiServiceUrl}/api/plugins/enterprise/org`, payload); + return res.data; + } catch (err) { + const errorMsg = err instanceof Error ? err.message : "Failed to connect org"; + message.error(errorMsg); + throw err; + } +} + + + + + + +/** + * Fetch workspaces for a specific environment + * @param apiServiceUrl - API service URL for the environment + * @param orgId - ID of the workspace + * + */ +export async function unconnectManagedWorkspace( + apiServiceUrl: string, + orgId: string +) { + if (!apiServiceUrl || !orgId) { + throw new Error("Missing apiServiceUrl or orgId"); + } + + try { + await axios.delete(`${apiServiceUrl}/api/plugins/enterprise/org/${orgId}`); + } catch (err) { + const errorMsg = err instanceof Error ? err.message : "Failed to unconnect org"; + message.error(errorMsg); + throw err; + } +} diff --git a/client/packages/lowcoder/src/pages/setting/environments/types/enterprise.types.ts b/client/packages/lowcoder/src/pages/setting/environments/types/enterprise.types.ts new file mode 100644 index 0000000000..4ce934e9f7 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/types/enterprise.types.ts @@ -0,0 +1,9 @@ +export interface ManagedOrg { + orgGid: string; + environmentId: string; + orgName: string; + orgTags: string[]; + createdAt: string; + updatedAt: string; + } + \ No newline at end of file From 1348756c6cbbb6d80f58dd55b13ca438a9d81128 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Fri, 11 Apr 2025 17:12:22 +0500 Subject: [PATCH 20/68] fix managed workspaces endpoint --- .../setting/environments/EnvironmentDetail.tsx | 8 ++++++++ .../hooks/enterprise/useManagedWorkspaces.ts | 15 +++++++++------ .../environments/services/enterprise.service.ts | 8 ++++---- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx index 489309a053..b8e273e18a 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx @@ -27,6 +27,7 @@ import UserGroupsList from "./components/UserGroupsList"; import { useEnvironmentContext } from "./context/EnvironmentContext"; import { useEnvironmentWorkspaces } from "./hooks/useEnvironmentWorkspaces"; import { useEnvironmentUserGroups } from "./hooks/useEnvironmentUserGroups"; +import { useManagedWorkspaces } from "./hooks/enterprise/useManagedWorkspaces"; const { Title, Text } = Typography; const { TabPane } = Tabs; @@ -53,6 +54,13 @@ const EnvironmentDetail: React.FC = () => { workspaceStats, } = useEnvironmentWorkspaces(environment); + const { + managedWorkspaces, + managedLoading, + managedError, + refreshManagedWorkspaces, + } = useManagedWorkspaces(environment); + const { userGroups, loading: userGroupsLoading, diff --git a/client/packages/lowcoder/src/pages/setting/environments/hooks/enterprise/useManagedWorkspaces.ts b/client/packages/lowcoder/src/pages/setting/environments/hooks/enterprise/useManagedWorkspaces.ts index 0ec3e40218..be539629b0 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/hooks/enterprise/useManagedWorkspaces.ts +++ b/client/packages/lowcoder/src/pages/setting/environments/hooks/enterprise/useManagedWorkspaces.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useState, useCallback } from "react"; import { ManagedOrg } from "../../types/enterprise.types"; import { getManagedWorkspaces, @@ -12,7 +12,7 @@ export function useManagedWorkspaces( const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const fetchManaged = async () => { + const fetchManaged = useCallback(async () => { if (!environment) return; setLoading(true); setError(null); @@ -21,23 +21,26 @@ export function useManagedWorkspaces( try { const { environmentId, environmentApikey, environmentApiServiceUrl } = environment; - if (!environmentApikey || !environmentApiServiceUrl) { + if (!environmentApikey) { setError("Missing API key or service URL for this environment."); setLoading(false); return; } - const result = await getManagedWorkspaces(environmentId, environmentApiServiceUrl); + const result = await getManagedWorkspaces(environmentId); + console.log("Managed workspaces:", result); setManaged(result); } catch (err: any) { setError(err.message ?? "Failed to load managed workspaces"); } finally { setLoading(false); } - }; + } , [environment]); useEffect(() => { - fetchManaged(); + if(environment) { + fetchManaged(); + } }, [environment, fetchManaged]); return { diff --git a/client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts b/client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts index 1992ea2877..0bfae6fce6 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts +++ b/client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts @@ -12,14 +12,14 @@ import { ManagedOrg } from "../types/enterprise.types"; export async function getManagedWorkspaces( environmentId: string, - apiServiceUrl: string + ): Promise { - if (!environmentId || !apiServiceUrl) { - throw new Error("Missing environmentId or apiServiceUrl"); + if (!environmentId) { + throw new Error("Missing environmentId"); } try { - const res = await axios.get(`${apiServiceUrl}/api/plugins/enterprise/org`); + const res = await axios.get(`/api/plugins/enterprise/org/list`); const all: ManagedOrg[] = res.data; return all.filter(org => org.environmentId === environmentId); } catch (err) { From c96b993e64e6a82d1aa2f121a1ae600e9f90c2c8 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Fri, 11 Apr 2025 18:02:58 +0500 Subject: [PATCH 21/68] add utility function for merge workspaces --- .../environments/EnvironmentDetail.tsx | 7 ++-- .../components/WorkspacesList.tsx | 21 ++++++++---- .../environments/types/enterprise.types.ts | 5 ++- .../environments/types/workspace.types.ts | 1 + .../environments/utils/getMergedWorkspaces.ts | 33 +++++++++++++++++++ 5 files changed, 58 insertions(+), 9 deletions(-) create mode 100644 client/packages/lowcoder/src/pages/setting/environments/utils/getMergedWorkspaces.ts diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx index b8e273e18a..1e18672680 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx @@ -28,6 +28,8 @@ import { useEnvironmentContext } from "./context/EnvironmentContext"; import { useEnvironmentWorkspaces } from "./hooks/useEnvironmentWorkspaces"; import { useEnvironmentUserGroups } from "./hooks/useEnvironmentUserGroups"; import { useManagedWorkspaces } from "./hooks/enterprise/useManagedWorkspaces"; +import { getMergedWorkspaces } from "./utils/getMergedWorkspaces"; + const { Title, Text } = Typography; const { TabPane } = Tabs; @@ -51,7 +53,6 @@ const EnvironmentDetail: React.FC = () => { loading: workspacesLoading, error: workspacesError, refresh: refreshWorkspaces, - workspaceStats, } = useEnvironmentWorkspaces(environment); const { @@ -120,6 +121,8 @@ const EnvironmentDetail: React.FC = () => { ); } + const { merged, stats: workspaceStats } = getMergedWorkspaces(workspaces, managedWorkspaces); + return (
{/* Header with environment name and controls */} @@ -294,7 +297,7 @@ const EnvironmentDetail: React.FC = () => { {/* Workspaces List */} = ({ workspaces, loading, error, - environmentId + environmentId, }) => { // Format timestamp to date string const formatDate = (timestamp?: number): string => { @@ -66,7 +66,16 @@ const WorkspacesList: React.FC = ({ {status} ), - } + }, + { + title: 'Managed', + key: 'managed', + render: (record: Workspace) => ( + + {record.managed ? 'Managed' : 'Unmanaged'} + + ), + }, ]; // If loading, show spinner @@ -82,7 +91,7 @@ const WorkspacesList: React.FC = ({ if (!workspaces || workspaces.length === 0 || error) { return ( ); @@ -97,10 +106,10 @@ const WorkspacesList: React.FC = ({ size="middle" onRow={(record) => ({ onClick: () => handleRowClick(record), - style: { cursor: 'pointer' } // Add pointer cursor to indicate clickable rows + style: { cursor: 'pointer' }, // Add pointer cursor to indicate clickable rows })} /> ); }; -export default WorkspacesList; \ No newline at end of file +export default WorkspacesList; diff --git a/client/packages/lowcoder/src/pages/setting/environments/types/enterprise.types.ts b/client/packages/lowcoder/src/pages/setting/environments/types/enterprise.types.ts index 4ce934e9f7..e51a787403 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/types/enterprise.types.ts +++ b/client/packages/lowcoder/src/pages/setting/environments/types/enterprise.types.ts @@ -1,3 +1,4 @@ +import { Workspace } from "../types/workspace.types"; export interface ManagedOrg { orgGid: string; environmentId: string; @@ -6,4 +7,6 @@ export interface ManagedOrg { createdAt: string; updatedAt: string; } - \ No newline at end of file + + + export type MergedWorkspace = Workspace & { managed: boolean }; diff --git a/client/packages/lowcoder/src/pages/setting/environments/types/workspace.types.ts b/client/packages/lowcoder/src/pages/setting/environments/types/workspace.types.ts index b3ec8fddaa..15f1e7dfbc 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/types/workspace.types.ts +++ b/client/packages/lowcoder/src/pages/setting/environments/types/workspace.types.ts @@ -12,4 +12,5 @@ export interface Workspace { createdBy?: string; isAutoGeneratedOrganization?: boolean | null; logoUrl?: string; + managed?: boolean; } \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/utils/getMergedWorkspaces.ts b/client/packages/lowcoder/src/pages/setting/environments/utils/getMergedWorkspaces.ts new file mode 100644 index 0000000000..f661786bd9 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/utils/getMergedWorkspaces.ts @@ -0,0 +1,33 @@ +import { Workspace } from "../types/workspace.types"; +import { ManagedOrg } from "../types/enterprise.types"; + + +export interface MergedWorkspaceResult { + merged: Workspace[]; + stats: { + total: number; + managed: number; + unmanaged: number; + }; +} + +export function getMergedWorkspaces( + standard: Workspace[], + managed: ManagedOrg[] +): MergedWorkspaceResult { + const merged = standard.map((ws) => ({ + ...ws, + managed: managed.some((m) => m.orgGid === ws.gid), + })); + + const managedCount = merged.filter((ws) => ws.managed).length; + + return { + merged, + stats: { + total: merged.length, + managed: managedCount, + unmanaged: merged.length - managedCount, + }, + }; +} From 19c99c7355bb25a0f962ad431d6e77ef020736b1 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Fri, 11 Apr 2025 22:33:13 +0500 Subject: [PATCH 22/68] add managed/unmanged workspaces --- .../environments/EnvironmentDetail.tsx | 64 ++++++++++++++++++- .../components/WorkspacesList.tsx | 39 ++++++----- .../services/enterprise.service.ts | 28 ++++---- 3 files changed, 98 insertions(+), 33 deletions(-) diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx index 1e18672680..ca74b81451 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, {useState} from "react"; import { useParams } from "react-router-dom"; import { Spin, @@ -13,6 +13,7 @@ import { Button, Statistic, Divider, + message } from "antd"; import { ReloadOutlined, @@ -29,6 +30,8 @@ import { useEnvironmentWorkspaces } from "./hooks/useEnvironmentWorkspaces"; import { useEnvironmentUserGroups } from "./hooks/useEnvironmentUserGroups"; import { useManagedWorkspaces } from "./hooks/enterprise/useManagedWorkspaces"; import { getMergedWorkspaces } from "./utils/getMergedWorkspaces"; +import { Workspace } from "./types/workspace.types"; +import { connectManagedWorkspace, unconnectManagedWorkspace } from "./services/enterprise.service"; const { Title, Text } = Typography; @@ -38,6 +41,12 @@ const { TabPane } = Tabs; * Environment Detail Page Component * Shows detailed information about a specific environment */ + +type WorkspaceStats = { + total: number; + managed: number; + unmanaged: number; +}; const EnvironmentDetail: React.FC = () => { // Get environment ID from URL params const { @@ -72,6 +81,22 @@ const EnvironmentDetail: React.FC = () => { // Use the custom hook to handle data fetching and state management // Use the custom hook to handle data fetching and state management + + const [mergedWorkspaces, setMergedWorkspaces] = useState([]); + const [workspaceStats, setWorkspaceStats] = useState({ + total: 0, + managed: 0, + unmanaged: 0, + }); + + + React.useEffect(() => { + if (workspaces && managedWorkspaces) { + const { merged, stats } = getMergedWorkspaces(workspaces, managedWorkspaces); + setMergedWorkspaces(merged); + setWorkspaceStats(stats); + } + }, [workspaces, managedWorkspaces]); // If loading, show spinner if (envLoading) { @@ -121,7 +146,39 @@ const EnvironmentDetail: React.FC = () => { ); } - const { merged, stats: workspaceStats } = getMergedWorkspaces(workspaces, managedWorkspaces); + const { merged, stats: initialStats } = getMergedWorkspaces(workspaces, managedWorkspaces); + + + + const handleToggleManaged = async (workspace: Workspace, checked: boolean) => { + try { + console.log("WORKSPACE", workspace); + if (checked) { + await connectManagedWorkspace(environment.environmentId, workspace.name, workspace.gid!); + } else { + await unconnectManagedWorkspace(workspace.gid!); + } + + // Optimistically update the local state + const updatedList = mergedWorkspaces.map((w) => + w.id === workspace.id ? { ...w, managed: checked } : w + ); + + const updatedManagedCount = updatedList.filter((w) => w.managed).length; + + setMergedWorkspaces(updatedList); + setWorkspaceStats({ + total: updatedList.length, + managed: updatedManagedCount, + unmanaged: updatedList.length - updatedManagedCount, + }); + + message.success(`${workspace.name} is now ${checked ? 'Managed' : 'Unmanaged'}`); + } catch (err) { + message.error(`Failed to toggle managed state for ${workspace.name}`); + } + }; + return (
@@ -297,10 +354,11 @@ const EnvironmentDetail: React.FC = () => { {/* Workspaces List */} diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/WorkspacesList.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/WorkspacesList.tsx index f3cb0faf1b..9d3aa22963 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/WorkspacesList.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/WorkspacesList.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Table, Tag, Empty, Spin } from 'antd'; +import { Table, Tag, Empty, Spin, Switch, Space } from 'antd'; import { Workspace } from '../types/workspace.types'; import history from '@lowcoder-ee/util/history'; import { buildEnvironmentWorkspaceId } from '@lowcoder-ee/constants/routesURL'; @@ -9,18 +9,18 @@ interface WorkspacesListProps { loading: boolean; error?: string | null; environmentId: string; + onToggleManaged?: (workspace: Workspace, checked: boolean) => void; + refreshing?: boolean; } -/** - * Component to display a list of workspaces in a table - */ const WorkspacesList: React.FC = ({ workspaces, loading, error, environmentId, + onToggleManaged, + refreshing = false, }) => { - // Format timestamp to date string const formatDate = (timestamp?: number): string => { if (!timestamp) return 'N/A'; const date = new Date(timestamp); @@ -31,7 +31,6 @@ const WorkspacesList: React.FC = ({ history.push(`${buildEnvironmentWorkspaceId(environmentId, workspace.id)}`); }; - // Table columns definition const columns = [ { title: 'Name', @@ -48,9 +47,7 @@ const WorkspacesList: React.FC = ({ title: 'Role', dataIndex: 'role', key: 'role', - render: (role: string) => ( - {role} - ), + render: (role: string) => {role}, }, { title: 'Creation Date', @@ -71,14 +68,27 @@ const WorkspacesList: React.FC = ({ title: 'Managed', key: 'managed', render: (record: Workspace) => ( - - {record.managed ? 'Managed' : 'Unmanaged'} - + + + {record.managed ? 'Managed' : 'Unmanaged'} + + {onToggleManaged && ( + { + e.stopPropagation(); // ✅ THIS STOPS the row from being triggered + onToggleManaged(record, checked); + }} + onChange={() => {}} + /> + )} + ), }, ]; - // If loading, show spinner if (loading) { return (
@@ -87,7 +97,6 @@ const WorkspacesList: React.FC = ({ ); } - // If no workspaces or error, show empty state if (!workspaces || workspaces.length === 0 || error) { return ( = ({ size="middle" onRow={(record) => ({ onClick: () => handleRowClick(record), - style: { cursor: 'pointer' }, // Add pointer cursor to indicate clickable rows + style: { cursor: 'pointer' }, })} /> ); diff --git a/client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts b/client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts index 0bfae6fce6..d156d5142c 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts +++ b/client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts @@ -41,11 +41,11 @@ export async function getManagedWorkspaces( export async function connectManagedWorkspace( environmentId: string, - apiServiceUrl: string, orgName: string, - orgTags: string[] = [] + org_gid: string, // ✅ not optional + orgTags: string[] = [], ) { - if (!environmentId || !apiServiceUrl || !orgName) { + if (!environmentId || !orgName || !org_gid) { throw new Error("Missing required params to connect org"); } @@ -54,9 +54,10 @@ export async function connectManagedWorkspace( environment_id: environmentId, org_name: orgName, org_tags: orgTags, + org_gid, }; - const res = await axios.post(`${apiServiceUrl}/api/plugins/enterprise/org`, payload); + const res = await axios.post(`/api/plugins/enterprise/org`, payload); return res.data; } catch (err) { const errorMsg = err instanceof Error ? err.message : "Failed to connect org"; @@ -67,27 +68,24 @@ export async function connectManagedWorkspace( - - - /** * Fetch workspaces for a specific environment * @param apiServiceUrl - API service URL for the environment * @param orgId - ID of the workspace * */ -export async function unconnectManagedWorkspace( - apiServiceUrl: string, - orgId: string -) { - if (!apiServiceUrl || !orgId) { - throw new Error("Missing apiServiceUrl or orgId"); +export async function unconnectManagedWorkspace(orgGid: string) { + if (!orgGid) { + throw new Error("Missing orgGid to unconnect workspace"); } try { - await axios.delete(`${apiServiceUrl}/api/plugins/enterprise/org/${orgId}`); + await axios.delete(`/api/plugins/enterprise/org`, { + params: { orgGid }, // ✅ pass as query param + }); } catch (err) { - const errorMsg = err instanceof Error ? err.message : "Failed to unconnect org"; + const errorMsg = + err instanceof Error ? err.message : "Failed to unconnect org"; message.error(errorMsg); throw err; } From 82ce3c3ff90093ea420f7eee262465380bee311a Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Fri, 11 Apr 2025 22:48:24 +0500 Subject: [PATCH 23/68] add app enterprise methods --- .../services/enterprise.service.ts | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts b/client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts index d156d5142c..69099dc6e2 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts +++ b/client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts @@ -90,3 +90,54 @@ export async function unconnectManagedWorkspace(orgGid: string) { throw err; } } + + + + +// FOR APPS + +export async function getManagedApps(environmentId: string) { + const res = await axios.get(`/api/plugins/enterprise/app/list`); + const allApps = res.data; + return allApps.filter((app: any) => app.environmentId === environmentId); +} + +// Connect an app +export async function connectManagedApp( + environmentId: string, + app_name: string, + app_version: string, + app_gid: string, + app_tags: string[] = [] +) { + try { + const payload = { + environment_id: environmentId, + app_name, + app_version, + app_gid, + app_tags, + }; + + const res = await axios.post(`/api/plugins/enterprise/app`, payload); + return res.data; + } catch (err) { + const errorMsg = + err instanceof Error ? err.message : "Failed to connect app"; + message.error(errorMsg); + throw err; + } +} + +// Unconnect an app +export async function unconnectManagedApp(appGid: string) { + try { + await axios.delete(`/api/plugins/enterprise/app`, { + params: { appGid }, + }); + } catch (err) { + const errorMsg = err instanceof Error ? err.message : "Failed to unconnect app"; + message.error(errorMsg); + throw err; + } +} \ No newline at end of file From 06b382250ba5c1d60d0da19b1406cb572eaa2117 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Fri, 11 Apr 2025 23:22:47 +0500 Subject: [PATCH 24/68] add managed/unmaged for apps --- .../setting/environments/WorkspaceDetail.tsx | 45 ++++++++++++++++--- .../environments/components/AppsList.tsx | 27 ++++++++++- .../hooks/enterprise/useManagedApps.ts | 26 +++++++++++ .../services/enterprise.service.ts | 2 - .../setting/environments/types/app.types.ts | 1 + .../environments/utils/getMergedApps.ts | 9 ++++ 6 files changed, 100 insertions(+), 10 deletions(-) create mode 100644 client/packages/lowcoder/src/pages/setting/environments/hooks/enterprise/useManagedApps.ts create mode 100644 client/packages/lowcoder/src/pages/setting/environments/utils/getMergedApps.ts diff --git a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx index 7b3d9479a8..64bf13ca93 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx @@ -13,7 +13,8 @@ import { Button, Statistic, Divider, - Breadcrumb + Breadcrumb, + message } from "antd"; import { AppstoreOutlined, @@ -29,12 +30,16 @@ import { useEnvironmentContext } from "./context/EnvironmentContext"; import { useWorkspace } from "./hooks/useWorkspace"; import { useWorkspaceApps } from "./hooks/useWorkspaceApps"; import { useWorkspaceDataSources } from "./hooks/useWorkspaceDataSources"; - +import { useManagedApps } from "./hooks/enterprise/useManagedApps"; +import { App } from "./types/app.types"; +import { getMergedApps } from "./utils/getMergedApps"; +import { connectManagedApp, unconnectManagedApp } from "./services/enterprise.service"; const { Title, Text } = Typography; const { TabPane } = Tabs; + const WorkspaceDetail: React.FC = () => { // Get parameters from URL @@ -72,8 +77,35 @@ const WorkspaceDetail: React.FC = () => { dataSourceStats, } = useWorkspaceDataSources(environment, workspaceId); - - + const { managedApps } = useManagedApps(environmentId); + const [mergedApps, setMergedApps] = useState([]); + + useEffect(() => { + setMergedApps(getMergedApps(apps, managedApps)); + }, [apps, managedApps]); + + + + + const handleToggleManagedApp = async (app: App, checked: boolean) => { + try { + if (checked) { + await connectManagedApp(environmentId, app.name, app.applicationGid!); + } else { + await unconnectManagedApp(app.applicationGid!); + } + + setMergedApps((currentApps) => + currentApps.map((a) => + a.applicationId === app.applicationId ? { ...a, managed: checked } : a + ) + ); + + message.success(`${app.name} is now ${checked ? "Managed" : "Unmanaged"}`); + } catch { + message.error(`Failed to toggle ${app.name}`); + } + }; if (envLoading || workspaceLoading) { return (
@@ -187,10 +219,11 @@ const WorkspaceDetail: React.FC = () => { )} {/* Apps List */} - diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/AppsList.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/AppsList.tsx index 8c5ea581f7..8ac6fb31e0 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/AppsList.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/AppsList.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Table, Tag, Empty, Spin, Avatar, Tooltip } from 'antd'; +import { Table, Tag, Empty, Spin, Avatar, Tooltip, Switch, Space } from 'antd'; import { AppstoreOutlined, UserOutlined, @@ -12,6 +12,8 @@ interface AppsListProps { apps: App[]; loading: boolean; error?: string | null; + onToggleManaged?: (app: App, checked: boolean) => void; + } /** @@ -21,6 +23,8 @@ const AppsList: React.FC = ({ apps, loading, error, + onToggleManaged + }) => { // Format timestamp to date string const formatDate = (timestamp?: number): string => { @@ -89,7 +93,26 @@ const AppsList: React.FC = ({ {status} ), - } + }, + { + title: 'Managed', + key: 'managed', + render: (record: App) => ( + + + {record.managed ? 'Managed' : 'Unmanaged'} + + { + e.stopPropagation(); // Prevent navigation + onToggleManaged?.(record, checked); + }} + /> + + ), + }, ]; // If loading, show spinner diff --git a/client/packages/lowcoder/src/pages/setting/environments/hooks/enterprise/useManagedApps.ts b/client/packages/lowcoder/src/pages/setting/environments/hooks/enterprise/useManagedApps.ts new file mode 100644 index 0000000000..8f8a4bc8ee --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/hooks/enterprise/useManagedApps.ts @@ -0,0 +1,26 @@ +import { useState, useEffect } from 'react'; +import { getManagedApps } from '../../services/enterprise.service'; + +export const useManagedApps = (environmentId: string) => { + const [managedApps, setManagedApps] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchManagedApps = async () => { + setLoading(true); + try { + const apps = await getManagedApps(environmentId); + setManagedApps(apps); + } catch (err: any) { + setError(err.message || 'Failed to fetch managed apps'); + } finally { + setLoading(false); + } + }; + + fetchManagedApps(); + }, [environmentId]); + + return { managedApps, loading, error }; +}; diff --git a/client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts b/client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts index 69099dc6e2..8fd6d17fd4 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts +++ b/client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts @@ -106,7 +106,6 @@ export async function getManagedApps(environmentId: string) { export async function connectManagedApp( environmentId: string, app_name: string, - app_version: string, app_gid: string, app_tags: string[] = [] ) { @@ -114,7 +113,6 @@ export async function connectManagedApp( const payload = { environment_id: environmentId, app_name, - app_version, app_gid, app_tags, }; 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 984228d6e2..67c445f92b 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 @@ -22,4 +22,5 @@ export interface App { icon: string; published: boolean; folder: boolean; + managed?: boolean; } \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/utils/getMergedApps.ts b/client/packages/lowcoder/src/pages/setting/environments/utils/getMergedApps.ts new file mode 100644 index 0000000000..0e09fb3dd0 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/utils/getMergedApps.ts @@ -0,0 +1,9 @@ +import { App } from '../types/app.types'; + + +export const getMergedApps = (standardApps: App[], managedApps: any[]): App[] => { + return standardApps.map((app) => ({ + ...app, + managed: managedApps.some((managedApp) => managedApp.appGid === app.applicationGid), + })); +}; From a758d7e3f43df2aa94c67094f68c12abd45b8c07 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Mon, 14 Apr 2025 17:31:59 +0500 Subject: [PATCH 25/68] Add environments list in Context --- .../environments/EnvironmentDetail.tsx | 11 +- .../setting/environments/WorkspaceDetail.tsx | 3 +- .../context/EnvironmentContext.tsx | 170 +++++++++++------- 3 files changed, 103 insertions(+), 81 deletions(-) diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx index ca74b81451..6266f617f8 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx @@ -51,9 +51,8 @@ const EnvironmentDetail: React.FC = () => { // Get environment ID from URL params const { environment, - loading: envLoading, + isLoadingEnvironment: envLoading, error: envError, - refresh, } = useEnvironmentContext(); @@ -124,11 +123,6 @@ const EnvironmentDetail: React.FC = () => { type="error" showIcon style={{ margin: "24px" }} - action={ - - } /> ); } @@ -198,9 +192,6 @@ const EnvironmentDetail: React.FC = () => { ID: {environment.environmentId}
-
{/* Basic Environment Information Card */} diff --git a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx index 64bf13ca93..75fba87a64 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx @@ -49,9 +49,8 @@ const WorkspaceDetail: React.FC = () => { }>(); const { environment, - loading: envLoading, + isLoadingEnvironment: envLoading, error: envError, - refresh: refreshEnvironment, } = useEnvironmentContext(); const { 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 c81500ae32..afd5c50da9 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/context/EnvironmentContext.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/context/EnvironmentContext.tsx @@ -1,74 +1,106 @@ // src/contexts/EnvironmentContext.tsx import React, { - createContext, - useContext, - useEffect, - useState, - useCallback, - ReactNode, - } from "react"; - import { useHistory } from "react-router-dom"; - import { getEnvironmentById } from "../services/environments.service"; - import { Environment } from "../types/environment.types"; - - interface EnvironmentContextType { - environment: Environment | null; - loading: boolean; - error: string | null; - refresh: () => void; + createContext, + useContext, + useEffect, + useState, + useCallback, + ReactNode, +} from "react"; +import { useHistory } from "react-router-dom"; +import { + getEnvironmentById, + getEnvironments, +} from "../services/environments.service"; +import { Environment } from "../types/environment.types"; + +interface EnvironmentContextState { + environment: Environment | null; + environments: Environment[]; + isLoadingEnvironment: boolean; + isLoadingEnvironments: boolean; + error: string | null; +} + +const EnvironmentContext = createContext( + undefined +); + +export const useEnvironmentContext = () => { + const context = useContext(EnvironmentContext); + if (!context) { + throw new Error( + "useEnvironmentContext must be used within an EnvironmentProvider" + ); } - - 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 { + envId: string; + children: ReactNode; +} + +export const EnvironmentProvider: React.FC = ({ + envId, + children, +}) => { + const [environment, setEnvironment] = useState(null); + const [environments, setEnvironments] = useState([]); + + // Separate loading states + const [isLoadingEnvironment, setIsLoadingEnvironment] = + useState(true); + const [isLoadingEnvironments, setIsLoadingEnvironments] = + useState(true); + + const [error, setError] = useState(null); + const history = useHistory(); + + const fetchEnvironment = useCallback(async () => { + setIsLoadingEnvironment(true); + try { + const data = await getEnvironmentById(envId); + console.log("Environment data:", data); + setEnvironment(data); + } catch (err) { + setError("Environment not found or failed to load"); + history.push("/404"); // or a centralized error route + } finally { + setIsLoadingEnvironment(false); } - return context; - }; - - interface ProviderProps { - envId: string; - children: ReactNode; - } - - export const EnvironmentProvider: React.FC = ({ envId, children }) => { - const [environment, setEnvironment] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const history = useHistory(); - - const fetchEnvironment = useCallback(async () => { - setLoading(true); - setError(null); - try { - const data = await getEnvironmentById(envId); - console.log("Environment data:", data); - setEnvironment(data); - } catch (err) { - setError("Environment not found or failed to load"); - history.push("/404"); // or a centralized error route - } finally { - setLoading(false); - } - }, [envId, history]); - - useEffect(() => { - fetchEnvironment(); - }, [fetchEnvironment]); - - const value: EnvironmentContextType = { - environment, - loading, - error, - refresh: fetchEnvironment, - }; - - return ( - - {children} - - ); + }, [envId, history]); + + const fetchEnvironments = useCallback(async () => { + setIsLoadingEnvironments(true); + try { + const data = await getEnvironments(); + console.log("Environments data:", data); + setEnvironments(data); + } catch (err) { + setError("Failed to load environments list"); + } finally { + setIsLoadingEnvironments(false); + } + }, []); + + useEffect(() => { + fetchEnvironment(); + fetchEnvironments(); + }, [fetchEnvironment, fetchEnvironments]); + + + const value: EnvironmentContextState = { + environment, + environments, + isLoadingEnvironment, + isLoadingEnvironments, + error, }; - \ No newline at end of file + + return ( + + {children} + + ); +}; From ce9b5bac5c880c12b760803f1dad9f5f233d4c0a Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Mon, 14 Apr 2025 18:36:26 +0500 Subject: [PATCH 26/68] Move Workspace/User-Group tabs to the seperate component --- .../environments/EnvironmentDetail.tsx | 204 +----------------- .../environments/components/UserGroupsTab.tsx | 126 +++++++++++ .../environments/components/WorkspacesTab.tsx | 162 ++++++++++++++ 3 files changed, 293 insertions(+), 199 deletions(-) create mode 100644 client/packages/lowcoder/src/pages/setting/environments/components/UserGroupsTab.tsx create mode 100644 client/packages/lowcoder/src/pages/setting/environments/components/WorkspacesTab.tsx diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx index 6266f617f8..b24d07aeaa 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx @@ -32,6 +32,8 @@ import { useManagedWorkspaces } from "./hooks/enterprise/useManagedWorkspaces"; import { getMergedWorkspaces } from "./utils/getMergedWorkspaces"; import { Workspace } from "./types/workspace.types"; import { connectManagedWorkspace, unconnectManagedWorkspace } from "./services/enterprise.service"; +import WorkspacesTab from "./components/WorkspacesTab"; +import UserGroupsTab from "./components/UserGroupsTab"; const { Title, Text } = Typography; @@ -253,105 +255,7 @@ const EnvironmentDetail: React.FC = () => { } key="workspaces" > - - {/* Header with refresh button */} -
- Workspaces in this Environment - -
- - {/* Workspace Statistics */} - -
- } - /> - - - } - /> - - - } - /> - - - - - - {/* Show error if workspace loading failed */} - {workspacesError && ( - - API Key Required - - ) : ( - - ) - } - /> - )} - - {(!environment.environmentApikey || - !environment.environmentApiServiceUrl) && - !workspacesError && ( - - )} - - {/* Workspaces List */} - - + { } key="userGroups" - > - - {/* Header with refresh button */} -
- User Groups in this Environment - -
- - {/* User Group Statistics */} - -
- } - /> - - - } - /> - - - } - /> - - - - - - {/* Show error if user group loading failed */} - {userGroupsError && ( - - Configuration Required - - ) : ( - - ) - } - /> - )} - - {/* Show warning if no API key or API service URL is configured */} - {(!environment.environmentApikey || - !environment.environmentApiServiceUrl) && - !userGroupsError && ( - - )} - - {/* User Groups List */} - - + > + 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 0000000000..65b1aba946 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/components/UserGroupsTab.tsx @@ -0,0 +1,126 @@ +// components/UserGroupsTab.tsx +import React from 'react'; +import { Card, Button, Row, Col, Statistic, Divider, Alert } from 'antd'; +import { TeamOutlined, UserOutlined, SyncOutlined } from '@ant-design/icons'; +import Title from 'antd/lib/typography/Title'; +import { Environment } from '../types/environment.types'; +import { useEnvironmentUserGroups } from '../hooks/useEnvironmentUserGroups'; +import UserGroupsList from './UserGroupsList'; + +interface UserGroupsTabProps { + environment: Environment; +} + +const UserGroupsTab: React.FC = ({ environment }) => { + const { + userGroups, + loading: userGroupsLoading, + error: userGroupsError, + refresh: refreshUserGroups, + userGroupStats, + } = useEnvironmentUserGroups(environment); + + return ( + + {/* Header with refresh button */} +
+ User Groups in this Environment + +
+ + {/* User Group Statistics */} + +
+ } + /> + + + } + /> + + + } + /> + + + + + + {/* Show error if user group loading failed */} + {userGroupsError && ( + + Configuration Required + + ) : ( + + ) + } + /> + )} + + {/* Show warning if no API key or API service URL is configured */} + {(!environment.environmentApikey || + !environment.environmentApiServiceUrl) && + !userGroupsError && ( + + )} + + {/* User Groups List */} + + + ); +}; + +export default UserGroupsTab; \ 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 0000000000..70883961cb --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/components/WorkspacesTab.tsx @@ -0,0 +1,162 @@ +// components/WorkspacesTab.tsx +import React from 'react'; +import { Card, Button, Row, Col, Statistic, Divider, Alert, message } from 'antd'; +import { ClusterOutlined, SyncOutlined } from '@ant-design/icons'; +import Title from 'antd/lib/typography/Title'; +import { Environment } from '../types/environment.types'; +import { Workspace } from '../types/workspace.types'; +import { useEnvironmentWorkspaces } from '../hooks/useEnvironmentWorkspaces'; +import { useManagedWorkspaces } from '../hooks/enterprise/useManagedWorkspaces'; +import WorkspacesList from './WorkspacesList'; +import { connectManagedWorkspace, unconnectManagedWorkspace } from '../services/enterprise.service'; +import { getMergedWorkspaces } from '../utils/getMergedWorkspaces'; + +interface WorkspaceStats { + total: number; + managed: number; + unmanaged: number; +} + +interface WorkspacesTabProps { + environment: Environment; +} + +const WorkspacesTab: React.FC = ({ environment }) => { + // Keep the existing hooks for now - we'll optimize these later + const { + workspaces, + loading: workspacesLoading, + error: workspacesError, + } = useEnvironmentWorkspaces(environment); + + const { + managedWorkspaces, + managedLoading, + managedError, + } = useManagedWorkspaces(environment); + + // Keep the merging logic for now - we'll optimize this later + const [mergedWorkspaces, setMergedWorkspaces] = React.useState([]); + const [workspaceStats, setWorkspaceStats] = React.useState({ + total: 0, + managed: 0, + unmanaged: 0, + }); + + React.useEffect(() => { + if (workspaces && managedWorkspaces) { + const { merged, stats } = getMergedWorkspaces(workspaces, managedWorkspaces); + setMergedWorkspaces(merged); + setWorkspaceStats(stats); + } + }, [workspaces, managedWorkspaces]); + + const handleToggleManaged = async (workspace: Workspace, checked: boolean) => { + try { + if (checked) { + await connectManagedWorkspace(environment.environmentId, workspace.name, workspace.gid!); + } else { + await unconnectManagedWorkspace(workspace.gid!); + } + + // Optimistically update the local state + const updatedList = mergedWorkspaces.map((w) => + w.id === workspace.id ? { ...w, managed: checked } : w + ); + + const updatedManagedCount = updatedList.filter((w) => w.managed).length; + + setMergedWorkspaces(updatedList); + setWorkspaceStats({ + total: updatedList.length, + managed: updatedManagedCount, + unmanaged: updatedList.length - updatedManagedCount, + }); + + message.success(`${workspace.name} is now ${checked ? 'Managed' : 'Unmanaged'}`); + } catch (err) { + message.error(`Failed to toggle managed state for ${workspace.name}`); + } + }; + + return ( + + {/* Header with refresh button */} +
+ Workspaces in this Environment +
+ + {/* Workspace Statistics */} + +
+ } + /> + + + } + /> + + + } + /> + + + + + + {/* Show error if workspace loading failed */} + {workspacesError && ( + + )} + + {(!environment.environmentApikey || + !environment.environmentApiServiceUrl) && + !workspacesError && ( + + )} + + {/* Workspaces List */} + + + ); +}; + +export default WorkspacesTab; \ No newline at end of file From a9fbce84b68b9f7fd8f7b03d2cf29f788599f2e3 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Mon, 14 Apr 2025 18:45:26 +0500 Subject: [PATCH 27/68] remove button from user-groups --- .../environments/EnvironmentDetail.tsx | 8 +----- .../environments/components/UserGroupsTab.tsx | 25 ------------------- 2 files changed, 1 insertion(+), 32 deletions(-) diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx index b24d07aeaa..1a4d338ca2 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx @@ -72,13 +72,7 @@ const EnvironmentDetail: React.FC = () => { refreshManagedWorkspaces, } = useManagedWorkspaces(environment); - const { - userGroups, - loading: userGroupsLoading, - error: userGroupsError, - refresh: refreshUserGroups, - userGroupStats, - } = useEnvironmentUserGroups(environment); + // Use the custom hook to handle data fetching and state management // Use the custom hook to handle data fetching and state management diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/UserGroupsTab.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/UserGroupsTab.tsx index 65b1aba946..244f37b0cb 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/UserGroupsTab.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/UserGroupsTab.tsx @@ -16,7 +16,6 @@ const UserGroupsTab: React.FC = ({ environment }) => { userGroups, loading: userGroupsLoading, error: userGroupsError, - refresh: refreshUserGroups, userGroupStats, } = useEnvironmentUserGroups(environment); @@ -32,14 +31,6 @@ const UserGroupsTab: React.FC = ({ environment }) => { }} > User Groups in this Environment - {/* User Group Statistics */} @@ -77,22 +68,6 @@ const UserGroupsTab: React.FC = ({ environment }) => { type="error" showIcon style={{ marginBottom: "16px" }} - action={ - userGroupsError.includes("No API key configured") || - userGroupsError.includes("No API service URL configured") ? ( - - ) : ( - - ) - } /> )} From aa27421d412e551b5efa88da921a2c6267680ec4 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Mon, 14 Apr 2025 19:11:47 +0500 Subject: [PATCH 28/68] remove unnecessary code from the Environment Detail page --- .../environments/EnvironmentDetail.tsx | 70 +------------------ 1 file changed, 1 insertion(+), 69 deletions(-) diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx index 1a4d338ca2..3beb11ce20 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx @@ -58,40 +58,6 @@ const EnvironmentDetail: React.FC = () => { } = useEnvironmentContext(); - const { - workspaces, - loading: workspacesLoading, - error: workspacesError, - refresh: refreshWorkspaces, - } = useEnvironmentWorkspaces(environment); - - const { - managedWorkspaces, - managedLoading, - managedError, - refreshManagedWorkspaces, - } = useManagedWorkspaces(environment); - - - - // Use the custom hook to handle data fetching and state management - // Use the custom hook to handle data fetching and state management - - const [mergedWorkspaces, setMergedWorkspaces] = useState([]); - const [workspaceStats, setWorkspaceStats] = useState({ - total: 0, - managed: 0, - unmanaged: 0, - }); - - - React.useEffect(() => { - if (workspaces && managedWorkspaces) { - const { merged, stats } = getMergedWorkspaces(workspaces, managedWorkspaces); - setMergedWorkspaces(merged); - setWorkspaceStats(stats); - } - }, [workspaces, managedWorkspaces]); // If loading, show spinner if (envLoading) { @@ -135,41 +101,7 @@ const EnvironmentDetail: React.FC = () => { /> ); } - - const { merged, stats: initialStats } = getMergedWorkspaces(workspaces, managedWorkspaces); - - - - const handleToggleManaged = async (workspace: Workspace, checked: boolean) => { - try { - console.log("WORKSPACE", workspace); - if (checked) { - await connectManagedWorkspace(environment.environmentId, workspace.name, workspace.gid!); - } else { - await unconnectManagedWorkspace(workspace.gid!); - } - - // Optimistically update the local state - const updatedList = mergedWorkspaces.map((w) => - w.id === workspace.id ? { ...w, managed: checked } : w - ); - - const updatedManagedCount = updatedList.filter((w) => w.managed).length; - - setMergedWorkspaces(updatedList); - setWorkspaceStats({ - total: updatedList.length, - managed: updatedManagedCount, - unmanaged: updatedList.length - updatedManagedCount, - }); - - message.success(`${workspace.name} is now ${checked ? 'Managed' : 'Unmanaged'}`); - } catch (err) { - message.error(`Failed to toggle managed state for ${workspace.name}`); - } - }; - - + return (
{/* Header with environment name and controls */} From 2690ddffafc28a43a557381ea21bebdd26424bb1 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Mon, 14 Apr 2025 20:02:23 +0500 Subject: [PATCH 29/68] add useWorkspaces hook that returns merged workspaces --- .../environments/components/WorkspacesTab.tsx | 92 ++++--------- .../environments/hooks/useWorkspaces.ts | 121 ++++++++++++++++++ .../services/workspace.service.ts | 76 +++++++++++ 3 files changed, 220 insertions(+), 69 deletions(-) create mode 100644 client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaces.ts create mode 100644 client/packages/lowcoder/src/pages/setting/environments/services/workspace.service.ts diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/WorkspacesTab.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/WorkspacesTab.tsx index 70883961cb..3e2348bc88 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/WorkspacesTab.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/WorkspacesTab.tsx @@ -4,78 +4,32 @@ import { Card, Button, Row, Col, Statistic, Divider, Alert, message } from 'antd import { ClusterOutlined, SyncOutlined } from '@ant-design/icons'; import Title from 'antd/lib/typography/Title'; import { Environment } from '../types/environment.types'; +import { useWorkspaces } from '../hooks/useWorkspaces'; +import WorkspacesList from './WorkspacesList'; import { Workspace } from '../types/workspace.types'; -import { useEnvironmentWorkspaces } from '../hooks/useEnvironmentWorkspaces'; -import { useManagedWorkspaces } from '../hooks/enterprise/useManagedWorkspaces'; -import WorkspacesList from './WorkspacesList'; -import { connectManagedWorkspace, unconnectManagedWorkspace } from '../services/enterprise.service'; -import { getMergedWorkspaces } from '../utils/getMergedWorkspaces'; - -interface WorkspaceStats { - total: number; - managed: number; - unmanaged: number; -} interface WorkspacesTabProps { environment: Environment; } const WorkspacesTab: React.FC = ({ environment }) => { - // Keep the existing hooks for now - we'll optimize these later + // Use the new hook that handles both regular and managed workspaces const { workspaces, - loading: workspacesLoading, - error: workspacesError, - } = useEnvironmentWorkspaces(environment); - - const { - managedWorkspaces, - managedLoading, - managedError, - } = useManagedWorkspaces(environment); - - // Keep the merging logic for now - we'll optimize this later - const [mergedWorkspaces, setMergedWorkspaces] = React.useState([]); - const [workspaceStats, setWorkspaceStats] = React.useState({ - total: 0, - managed: 0, - unmanaged: 0, - }); - - React.useEffect(() => { - if (workspaces && managedWorkspaces) { - const { merged, stats } = getMergedWorkspaces(workspaces, managedWorkspaces); - setMergedWorkspaces(merged); - setWorkspaceStats(stats); - } - }, [workspaces, managedWorkspaces]); + stats, + loading, + error, + toggleManagedStatus + } = useWorkspaces(environment); - const handleToggleManaged = async (workspace: Workspace, checked: boolean) => { - try { - if (checked) { - await connectManagedWorkspace(environment.environmentId, workspace.name, workspace.gid!); - } else { - await unconnectManagedWorkspace(workspace.gid!); - } - - // Optimistically update the local state - const updatedList = mergedWorkspaces.map((w) => - w.id === workspace.id ? { ...w, managed: checked } : w - ); - - const updatedManagedCount = updatedList.filter((w) => w.managed).length; - - setMergedWorkspaces(updatedList); - setWorkspaceStats({ - total: updatedList.length, - managed: updatedManagedCount, - unmanaged: updatedList.length - updatedManagedCount, - }); - + const handleToggleManaged = async (workspace: Workspace, checked:boolean) => { + const success = await toggleManagedStatus(workspace, checked); + + if (success) { message.success(`${workspace.name} is now ${checked ? 'Managed' : 'Unmanaged'}`); - } catch (err) { + } else { message.error(`Failed to toggle managed state for ${workspace.name}`); + // Optionally refresh to ensure UI is in sync with backend } }; @@ -98,21 +52,21 @@ const WorkspacesTab: React.FC = ({ environment }) => {
} /> } /> } /> @@ -121,10 +75,10 @@ const WorkspacesTab: React.FC = ({ environment }) => { {/* Show error if workspace loading failed */} - {workspacesError && ( + {error && ( = ({ environment }) => { {(!environment.environmentApikey || !environment.environmentApiServiceUrl) && - !workspacesError && ( + !error && ( = ({ environment }) => { {/* Workspaces List */} diff --git a/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaces.ts b/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaces.ts new file mode 100644 index 0000000000..afd8565aef --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaces.ts @@ -0,0 +1,121 @@ +// hooks/useWorkspaces.ts +import { useState, useEffect, useCallback } from "react"; +import { getMergedEnvironmentWorkspaces, MergedWorkspacesResult } from "../services/workspace.service"; +import { connectManagedWorkspace, unconnectManagedWorkspace } from "../services/enterprise.service"; +import { Environment } from "../types/environment.types"; +import { Workspace } from "../types/workspace.types"; + +interface WorkspacesState extends MergedWorkspacesResult { + loading: boolean; + error: string | null; +} + +export const useWorkspaces = (environment: Environment | null) => { + const [state, setState] = useState({ + workspaces: [], + stats: { + total: 0, + managed: 0, + unmanaged: 0, + }, + loading: false, + error: null + }); + + const fetchWorkspaces = useCallback(async () => { + if (!environment) return; + + setState(prev => ({ ...prev, loading: true, error: null })); + + try { + const { environmentId, environmentApikey, environmentApiServiceUrl } = environment; + + // Validate required configuration + if (!environmentApikey) { + setState(prev => ({ + ...prev, + loading: false, + error: "No API key configured for this environment. Workspaces cannot be fetched." + })); + return; + } + + if (!environmentApiServiceUrl) { + setState(prev => ({ + ...prev, + loading: false, + error: "No API service URL configured for this environment. Workspaces cannot be fetched." + })); + return; + } + + // Use the merged utility function + const result = await getMergedEnvironmentWorkspaces( + environmentId, + environmentApikey, + environmentApiServiceUrl + ); + + // Update state with result + setState({ + ...result, + loading: false, + error: null + }); + } catch (err) { + setState(prev => ({ + ...prev, + loading: false, + error: err instanceof Error ? err.message : "Failed to fetch workspaces" + })); + } + }, [environment]); + + useEffect(() => { + if (environment) { + fetchWorkspaces(); + } + }, [environment, fetchWorkspaces]); + + const toggleManagedStatus = async (workspace: Workspace, checked: boolean) => { + try { + if (!environment) return false; + + if (checked) { + await connectManagedWorkspace(environment.environmentId, workspace.name, workspace.gid!); + } else { + await unconnectManagedWorkspace(workspace.gid!); + } + + // Optimistically update the state + setState(prev => { + // Update workspaces with the new managed status + const updatedWorkspaces = prev.workspaces.map(w => + w.id === workspace.id ? { ...w, managed: checked } : w + ); + + // Recalculate stats + const managedCount = updatedWorkspaces.filter(w => w.managed).length; + + return { + ...prev, + workspaces: updatedWorkspaces, + stats: { + total: updatedWorkspaces.length, + managed: managedCount, + unmanaged: updatedWorkspaces.length - managedCount + } + }; + }); + + return true; // Success indicator + } catch (err) { + return false; // Failure indicator + } + }; + + return { + ...state, + toggleManagedStatus, + }; +}; \ 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 new file mode 100644 index 0000000000..c56b978b55 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/services/workspace.service.ts @@ -0,0 +1,76 @@ +// 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 { Workspace } from "../types/workspace.types"; +import { ManagedOrg } from "../types/enterprise.types"; + +export interface WorkspaceStats { + total: number; + managed: number; + unmanaged: number; +} + +export interface MergedWorkspacesResult { + workspaces: Workspace[]; + stats: WorkspaceStats; +} + +export async function getMergedEnvironmentWorkspaces( + environmentId: string, + apiKey: string, + apiServiceUrl: string +): Promise { + try { + // First, get regular workspaces + const regularWorkspaces = await getEnvironmentWorkspaces( + environmentId, + apiKey, + apiServiceUrl + ); + + // If no workspaces, return early with empty result + if (!regularWorkspaces.length) { + return { + workspaces: [], + stats: { + total: 0, + managed: 0, + unmanaged: 0 + } + }; + } + + // Only fetch managed workspaces if we have regular workspaces + let managedOrgs: ManagedOrg[] = []; + try { + managedOrgs = await getManagedWorkspaces(environmentId); + } catch (error) { + console.error("Failed to fetch managed workspaces:", error); + // Continue with empty managed list + } + + // Merge the workspaces + const mergedWorkspaces = regularWorkspaces.map(ws => ({ + ...ws, + managed: managedOrgs.some(org => org.orgGid === ws.gid) + })); + + // Calculate stats + const managedCount = mergedWorkspaces.filter(ws => ws.managed).length; + + return { + workspaces: mergedWorkspaces, + stats: { + total: mergedWorkspaces.length, + managed: managedCount, + unmanaged: mergedWorkspaces.length - managedCount + } + }; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Failed to fetch workspaces"; + message.error(errorMessage); + throw error; + } +} \ No newline at end of file From aafb5f5cd841a6dc4c1bed00a593ff477fa4173c Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Mon, 14 Apr 2025 20:05:08 +0500 Subject: [PATCH 30/68] remove unnecessary imports --- .../environments/EnvironmentDetail.tsx | 21 +------------------ 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx index 3beb11ce20..0a37c56726 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx @@ -4,16 +4,10 @@ import { Spin, Typography, Card, - Row, - Col, Tag, Tabs, Alert, Descriptions, - Button, - Statistic, - Divider, - message } from "antd"; import { ReloadOutlined, @@ -23,15 +17,8 @@ import { UserOutlined, SyncOutlined, } from "@ant-design/icons"; -import WorkspacesList from "./components/WorkspacesList"; -import UserGroupsList from "./components/UserGroupsList"; + import { useEnvironmentContext } from "./context/EnvironmentContext"; -import { useEnvironmentWorkspaces } from "./hooks/useEnvironmentWorkspaces"; -import { useEnvironmentUserGroups } from "./hooks/useEnvironmentUserGroups"; -import { useManagedWorkspaces } from "./hooks/enterprise/useManagedWorkspaces"; -import { getMergedWorkspaces } from "./utils/getMergedWorkspaces"; -import { Workspace } from "./types/workspace.types"; -import { connectManagedWorkspace, unconnectManagedWorkspace } from "./services/enterprise.service"; import WorkspacesTab from "./components/WorkspacesTab"; import UserGroupsTab from "./components/UserGroupsTab"; @@ -43,12 +30,6 @@ const { TabPane } = Tabs; * Environment Detail Page Component * Shows detailed information about a specific environment */ - -type WorkspaceStats = { - total: number; - managed: number; - unmanaged: number; -}; const EnvironmentDetail: React.FC = () => { // Get environment ID from URL params const { From 0d87a217c025ef855abb71db4ce1b16a9a91bf73 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Mon, 14 Apr 2025 21:00:02 +0500 Subject: [PATCH 31/68] Make a seperate AppsTab components and unified managed/unmanged apps --- .../setting/environments/WorkspaceDetail.tsx | 97 +------------- .../environments/components/AppsTab.tsx | 115 ++++++++++++++++ .../environments/hooks/useWorkspaceApps.ts | 125 +++++++++++++----- .../environments/services/apps.service.ts | 92 +++++++++++++ 4 files changed, 299 insertions(+), 130 deletions(-) create mode 100644 client/packages/lowcoder/src/pages/setting/environments/components/AppsTab.tsx create mode 100644 client/packages/lowcoder/src/pages/setting/environments/services/apps.service.ts diff --git a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx index 75fba87a64..eaf2f24c3a 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx @@ -34,6 +34,7 @@ import { useManagedApps } from "./hooks/enterprise/useManagedApps"; import { App } from "./types/app.types"; import { getMergedApps } from "./utils/getMergedApps"; import { connectManagedApp, unconnectManagedApp } from "./services/enterprise.service"; +import AppsTab from "./components/AppsTab"; const { Title, Text } = Typography; const { TabPane } = Tabs; @@ -60,14 +61,6 @@ const WorkspaceDetail: React.FC = () => { refresh: refreshWorkspace } = useWorkspace(environment, workspaceId); - const { - apps, - loading: appsLoading, - error: appsError, - refresh: refreshApps, - appStats, - } = useWorkspaceApps(environment, workspaceId); - const { dataSources, loading: dataSourcesLoading, @@ -76,35 +69,6 @@ const WorkspaceDetail: React.FC = () => { dataSourceStats, } = useWorkspaceDataSources(environment, workspaceId); - const { managedApps } = useManagedApps(environmentId); - const [mergedApps, setMergedApps] = useState([]); - - useEffect(() => { - setMergedApps(getMergedApps(apps, managedApps)); - }, [apps, managedApps]); - - - - - const handleToggleManagedApp = async (app: App, checked: boolean) => { - try { - if (checked) { - await connectManagedApp(environmentId, app.name, app.applicationGid!); - } else { - await unconnectManagedApp(app.applicationGid!); - } - - setMergedApps((currentApps) => - currentApps.map((a) => - a.applicationId === app.applicationId ? { ...a, managed: checked } : a - ) - ); - - message.success(`${app.name} is now ${checked ? "Managed" : "Unmanaged"}`); - } catch { - message.error(`Failed to toggle ${app.name}`); - } - }; if (envLoading || workspaceLoading) { return (
@@ -167,64 +131,7 @@ const WorkspaceDetail: React.FC = () => { tab={ Apps} key="apps" > - - {/* Header with refresh button */} -
- Apps in this Workspace - -
- - {/* App Statistics */} - -
- } - /> - - - } - /> - - - - - - {/* Show error if apps loading failed */} - {appsError && ( - - Try Again - - } - /> - )} - - {/* Apps List */} - - + {/* Update the TabPane in WorkspaceDetail.tsx */} 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 0000000000..b96d02d53a --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/components/AppsTab.tsx @@ -0,0 +1,115 @@ +// components/AppsTab.tsx +import React from 'react'; +import { Card, Button, Row, Col, Statistic, Divider, Alert, message } from 'antd'; +import { AppstoreOutlined, SyncOutlined } from '@ant-design/icons'; +import Title from 'antd/lib/typography/Title'; +import { Environment } from '../types/environment.types'; +import { useWorkspaceApps } from '../hooks/useWorkspaceApps'; +import AppsList from './AppsList'; +import { App } from '../types/app.types'; + +interface AppsTabProps { + environment: Environment; + workspaceId: string; +} + +const AppsTab: React.FC = ({ environment, workspaceId }) => { + const { + apps, + stats, + loading, + error, + toggleManagedStatus + } = useWorkspaceApps(environment, workspaceId); + + const handleToggleManagedApp = async (app: App, checked: boolean) => { + const success = await toggleManagedStatus(app, checked); + + if (success) { + message.success(`${app.name} is now ${checked ? "Managed" : "Unmanaged"}`); + } else { + message.error(`Failed to toggle ${app.name}`); + } + }; + + return ( + + {/* Header with refresh button */} +
+ Apps in this Workspace +
+ + {/* App Statistics */} + +
+ } + /> + + + } + /> + + + } + /> + + + } + /> + + + + + + {/* Show error if apps loading failed */} + {error && ( + + )} + + {/* Configuration warning */} + {(!environment.environmentApikey || + !environment.environmentApiServiceUrl) && + !error && ( + + )} + + {/* Apps List */} + + + ); +}; + +export default AppsTab; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaceApps.ts b/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaceApps.ts index d909a42bef..6d8718a34e 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaceApps.ts +++ b/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaceApps.ts @@ -1,68 +1,123 @@ +// hooks/useWorkspaceApps.ts import { useState, useEffect, useCallback } from "react"; -import { getWorkspaceApps } from "../services/environments.service"; +import { getMergedWorkspaceApps, MergedAppsResult } from "../services/apps.service"; +import { connectManagedApp, unconnectManagedApp } from "../services/enterprise.service"; import { Environment } from "../types/environment.types"; import { App } from "../types/app.types"; -interface AppStats { - total: number; - published: number; +interface AppState extends MergedAppsResult { + loading: boolean; + error: string | null; } -export const useWorkspaceApps = ( - environment: Environment | null, - workspaceId: string -) => { - const [apps, setApps] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); +export const useWorkspaceApps = (environment: Environment | null, workspaceId: string) => { + const [state, setState] = useState({ + apps: [], + stats: { + total: 0, + published: 0, + managed: 0, + unmanaged: 0 + }, + loading: false, + error: null + }); const fetchApps = useCallback(async () => { if (!environment || !workspaceId) return; - setLoading(true); - setError(null); + setState(prev => ({ ...prev, loading: true, error: null })); try { const { environmentId, environmentApikey, environmentApiServiceUrl } = environment; - if (!environmentApikey || !environmentApiServiceUrl) { - setError("Missing API key or service URL for this environment. Apps cannot be fetched."); - setLoading(false); + // Validate required configuration + if (!environmentApikey) { + setState(prev => ({ + ...prev, + loading: false, + error: "No API key configured for this environment. Apps cannot be fetched." + })); return; } - const data = await getWorkspaceApps( + if (!environmentApiServiceUrl) { + setState(prev => ({ + ...prev, + loading: false, + error: "No API service URL configured for this environment. Apps cannot be fetched." + })); + return; + } + + // Use the service function to get merged apps + const result = await getMergedWorkspaceApps( workspaceId, + environmentId, environmentApikey, environmentApiServiceUrl ); - - setApps(data); + + // Update state with result + setState({ + ...result, + loading: false, + error: null + }); } catch (err) { - setError( - err instanceof Error ? err.message : "Failed to fetch apps" - ); - } finally { - setLoading(false); + setState(prev => ({ + ...prev, + loading: false, + error: err instanceof Error ? err.message : "Failed to fetch apps" + })); } }, [environment, workspaceId]); useEffect(() => { - if (environment) { + if (environment && workspaceId) { fetchApps(); } - }, [environment, fetchApps]); + }, [environment, workspaceId, fetchApps]); - const appStats: AppStats = { - total: apps.length, - published: apps.filter(app => app.published).length, + const toggleManagedStatus = async (app: App, checked: boolean) => { + try { + if (!environment) return false; + + if (checked) { + await connectManagedApp(environment.environmentId, app.name, app.applicationGid!); + } else { + await unconnectManagedApp(app.applicationGid!); + } + + // Optimistically update the state + setState(prev => { + // Update apps with the new managed status + const updatedApps = prev.apps.map(a => + a.applicationId === app.applicationId ? { ...a, managed: checked } : a + ); + + // Recalculate stats + const managedCount = updatedApps.filter(a => a.managed).length; + + return { + ...prev, + apps: updatedApps, + stats: { + ...prev.stats, + managed: managedCount, + unmanaged: updatedApps.length - managedCount + } + }; + }); + + return true; // Success indicator + } catch (err) { + return false; // Failure indicator + } }; return { - apps, - loading, - error, - refresh: fetchApps, - appStats, + ...state, + toggleManagedStatus, }; -}; +}; \ 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 new file mode 100644 index 0000000000..cf94f5cdeb --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/services/apps.service.ts @@ -0,0 +1,92 @@ +// services/appService.ts +import { message } from "antd"; +import { getWorkspaceApps } from "./environments.service"; +import { getManagedApps } from "./enterprise.service"; +import { App } from "../types/app.types"; + +export interface AppStats { + total: number; + published: number; + managed: number; + unmanaged: number; +} + +export interface MergedAppsResult { + apps: App[]; + stats: AppStats; +} + +// Use your existing merge function with slight modification +export const getMergedApps = (standardApps: App[], managedApps: any[]): App[] => { + return standardApps.map((app) => ({ + ...app, + managed: managedApps.some((managedApp) => managedApp.appGid === app.applicationGid), + })); +}; + +// Calculate app statistics +export const calculateAppStats = (apps: App[]): AppStats => { + const publishedCount = apps.filter(app => app.published).length; + const managedCount = apps.filter(app => app.managed).length; + + return { + total: apps.length, + published: publishedCount, + managed: managedCount, + unmanaged: apps.length - managedCount + }; +}; + +export async function getMergedWorkspaceApps( + workspaceId: string, + environmentId: string, + apiKey: string, + apiServiceUrl: string +): Promise { + try { + // First, get regular apps for the workspace + const regularApps = await getWorkspaceApps( + workspaceId, + apiKey, + apiServiceUrl + ); + + // If no apps, return early with empty result + if (!regularApps.length) { + return { + apps: [], + stats: { + total: 0, + published: 0, + managed: 0, + unmanaged: 0 + } + }; + } + + // Only fetch managed apps if we have regular apps + let managedApps = []; + try { + managedApps = await getManagedApps(environmentId); + } catch (error) { + console.error("Failed to fetch managed apps:", error); + // Continue with empty managed list + } + + // Use your existing merge function + const mergedApps = getMergedApps(regularApps, managedApps); + + // Calculate stats + const stats = calculateAppStats(mergedApps); + + return { + apps: mergedApps, + stats + }; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Failed to fetch apps"; + message.error(errorMessage); + throw error; + } +} \ No newline at end of file From 4280e67939c64149ec1aeada7cd80dd145c49273 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Mon, 14 Apr 2025 22:29:51 +0500 Subject: [PATCH 32/68] add deploy modal --- .../environments/components/AppsList.tsx | 194 ++++++++---------- .../environments/components/AppsTab.tsx | 3 +- .../components/DeployAppModal.tsx | 148 +++++++++++++ .../environments/services/apps.service.ts | 42 +++- 4 files changed, 278 insertions(+), 109 deletions(-) create mode 100644 client/packages/lowcoder/src/pages/setting/environments/components/DeployAppModal.tsx diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/AppsList.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/AppsList.tsx index 8ac6fb31e0..40048f4eb4 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/AppsList.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/AppsList.tsx @@ -1,147 +1,127 @@ -import React from 'react'; -import { Table, Tag, Empty, Spin, Avatar, Tooltip, Switch, Space } from 'antd'; -import { - AppstoreOutlined, - UserOutlined, - CheckCircleOutlined, - CloseCircleOutlined -} from '@ant-design/icons'; +// components/AppsList.tsx +import React, { useState } from 'react'; +import { Table, Switch, Button, Space, Tooltip, Tag } from 'antd'; +import { CloudUploadOutlined } from '@ant-design/icons'; import { App } from '../types/app.types'; +import { Environment } from '../types/environment.types'; +import DeployAppModal from './DeployAppModal'; +import { ColumnsType } from 'antd/lib/table'; interface AppsListProps { apps: App[]; loading: boolean; - error?: string | null; - onToggleManaged?: (app: App, checked: boolean) => void; - + error: string | null; + environment: Environment; + onToggleManaged: (app: App, checked: boolean) => Promise; + onRefresh?: () => void; // Make this optional since your current implementation doesn't have it } -/** - * Component to display a list of apps in a table - */ const AppsList: React.FC = ({ apps, loading, error, - onToggleManaged - + environment, + onToggleManaged, + onRefresh, }) => { - // Format timestamp to date string - const formatDate = (timestamp?: number): string => { - if (!timestamp) return 'N/A'; - const date = new Date(timestamp); - return `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`; + const [deployModalVisible, setDeployModalVisible] = useState(false); + const [selectedApp, setSelectedApp] = useState(null); + + const handleDeploy = (app: App) => { + setSelectedApp(app); + setDeployModalVisible(true); }; - // Table columns definition - const columns = [ - { - title: 'Title', - key: 'title', - render: (record: App) => ( -
- } - src={record.icon || undefined} - style={{ marginRight: 8 }} - /> - {record.title || record.name} -
- ), - }, + // Cast the value to boolean in onFilter to fix the type issue + const columns: ColumnsType = [ { - title: 'Created By', - dataIndex: 'createBy', - key: 'createBy', - render: (createBy: string) => ( -
- } style={{ marginRight: 8 }} /> - {createBy} -
- ), - }, - { - title: 'Created', - key: 'createAt', - render: (record: App) => formatDate(record.createAt), + title: 'Name', + dataIndex: 'name', + key: 'name', + sorter: (a: App, b: App) => a.name.localeCompare(b.name), }, { - title: 'Last Modified', - key: 'lastModifyTime', - render: (record: App) => formatDate(record.lastModifyTime), + title: 'Description', + dataIndex: 'description', + key: 'description', + ellipsis: true, }, { - title: 'Published', + title: 'Status', dataIndex: 'published', key: 'published', render: (published: boolean) => ( - - {published ? - : - - } - - ), - }, - { - title: 'Status', - dataIndex: 'applicationStatus', - key: 'applicationStatus', - render: (status: string) => ( - - {status} + + {published ? 'Published' : 'Unpublished'} ), + filters: [ + { text: 'Published', value: true }, + { text: 'Unpublished', value: false }, + ], + onFilter: (value, record: App) => record.published === Boolean(value), }, { title: 'Managed', + dataIndex: 'managed', key: 'managed', - render: (record: App) => ( + render: (managed: boolean, record: App) => ( - - {record.managed ? 'Managed' : 'Unmanaged'} - { - e.stopPropagation(); // Prevent navigation - onToggleManaged?.(record, checked); - }} + checked={managed} + onChange={(checked) => onToggleManaged(record, checked)} /> + + {managed ? 'Managed' : 'Unmanaged'} + + + ), + filters: [ + { text: 'Managed', value: true }, + { text: 'Unmanaged', value: false }, + ], + onFilter: (value, record: App) => record.managed === Boolean(value), + }, + { + title: 'Actions', + key: 'actions', + render: (_, record: App) => ( + + + + ), }, ]; - // If loading, show spinner - if (loading) { - return ( -
- -
- ); - } - - // If no apps or error, show empty state - if (!apps || apps.length === 0 || error) { - return ( - - ); - } - return ( -
+ <> +
+ + setDeployModalVisible(false)} + /> + ); }; diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/AppsTab.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/AppsTab.tsx index b96d02d53a..9d8c8f923e 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/AppsTab.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/AppsTab.tsx @@ -106,8 +106,9 @@ const AppsTab: React.FC = ({ environment, workspaceId }) => { apps={apps} loading={loading && !error} error={error} + environment={environment} onToggleManaged={handleToggleManagedApp} - /> + /> ); }; diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/DeployAppModal.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/DeployAppModal.tsx new file mode 100644 index 0000000000..408f5ae694 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/components/DeployAppModal.tsx @@ -0,0 +1,148 @@ +// components/DeployAppModal.tsx +import React, { useState, useEffect } from 'react'; +import { Modal, Form, Select, Checkbox, Button, message, Spin } from 'antd'; +import { Environment } from '../types/environment.types'; +import { App } from '../types/app.types'; +import { deployApp } from '../services/apps.service'; +import { useEnvironmentContext } from '../context/EnvironmentContext'; + +interface DeployAppModalProps { + visible: boolean; + app: App | null; + currentEnvironment: Environment; + onClose: () => void; +} + +const DeployAppModal: React.FC = ({ + visible, + app, + currentEnvironment, + onClose, +}) => { + const [form] = Form.useForm(); + const { environments, isLoadingEnvironments } = useEnvironmentContext(); + console.log('environments data modal', environments); + const [deploying, setDeploying] = useState(false); + + // Reset form when modal becomes visible + useEffect(() => { + if (visible) { + form.resetFields(); + } + }, [visible, form]); + + // Filter out current environment from the list + const targetEnvironments = environments.filter( + (env) => env.environmentId !== currentEnvironment.environmentId + ); + + const handleDeploy = async () => { + try { + const values = await form.validateFields(); + + if (!app) return; + + setDeploying(true); + + await deployApp( + { + envId: currentEnvironment.environmentId, + targetEnvId: values.targetEnvId, + applicationId: app.applicationId!, + updateDependenciesIfNeeded: values.updateDependenciesIfNeeded, + publishOnTarget: values.publishOnTarget, + publicToAll: values.publicToAll, + publicToMarketplace: values.publicToMarketplace, + }, + ); + + message.success(`Successfully deployed ${app.name} to target environment`); + onClose(); + } catch (error) { + console.error('Deployment error:', error); + message.error('Failed to deploy app'); + } finally { + setDeploying(false); + } + }; + + return ( + + {isLoadingEnvironments ? ( +
+ +
+ ) : ( +
+ + + + + + Update Dependencies If Needed + + + + Publish On Target + + + + Public To All + + + + Public To Marketplace + + + + + + + + )} +
+ ); +}; + +export default DeployAppModal; \ 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 cf94f5cdeb..8c4c8785b9 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 @@ -3,6 +3,7 @@ import { message } from "antd"; import { getWorkspaceApps } from "./environments.service"; import { getManagedApps } from "./enterprise.service"; import { App } from "../types/app.types"; +import axios from "axios"; export interface AppStats { total: number; @@ -16,6 +17,18 @@ export interface MergedAppsResult { stats: AppStats; } + +export interface DeployAppParams { + envId: string; + targetEnvId: string; + applicationId: string; + updateDependenciesIfNeeded?: boolean; + publishOnTarget?: boolean; + publicToAll?: boolean; + publicToMarketplace?: boolean; +} + + // Use your existing merge function with slight modification export const getMergedApps = (standardApps: App[], managedApps: any[]): App[] => { return standardApps.map((app) => ({ @@ -89,4 +102,31 @@ export async function getMergedWorkspaceApps( message.error(errorMessage); throw error; } -} \ No newline at end of file +} + + + +export const deployApp = async (params: DeployAppParams): Promise => { + try { + const response = await axios.post( + `/api/plugins/enterprise/deploy`, + null, + { + params: { + envId: params.envId, + targetEnvId: params.targetEnvId, + applicationId: params.applicationId, + updateDependenciesIfNeeded: params.updateDependenciesIfNeeded ?? false, + publishOnTarget: params.publishOnTarget ?? false, + publicToAll: params.publicToAll ?? false, + publicToMarketplace: params.publicToMarketplace ?? false + } + } + ); + + return response.status === 200; + } catch (error) { + console.error('Error deploying app:', error); + throw error; + } +}; \ No newline at end of file From d8ed7158321c92d4cc47e24e051d58e6e85fc41b Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 15 Apr 2025 01:02:28 +0500 Subject: [PATCH 33/68] add datasource functions --- .../services/datasources.service.ts | 150 ++++++++++++++++++ .../services/enterprise.service.ts | 46 +++++- .../environments/types/datasource.types.ts | 1 + 3 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 client/packages/lowcoder/src/pages/setting/environments/services/datasources.service.ts 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 new file mode 100644 index 0000000000..e3a3a8fd8a --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/services/datasources.service.ts @@ -0,0 +1,150 @@ +// services/dataSources.service.ts +// Create this new file + +import axios from 'axios'; +import { message } from "antd"; +import { DataSource } from "../types/datasource.types"; +import { getManagedDataSources } from './enterprise.service'; + +export interface DataSourceStats { + total: number; + types: number; + managed: number; + unmanaged: number; +} + +export interface MergedDataSourcesResult { + dataSources: DataSource[]; + stats: DataSourceStats; +} + +// Get data sources for a workspace +export const getWorkspaceDataSources = async ( + workspaceId: string, + apiKey: string, + apiServiceUrl: string +): Promise => { + try { + const response = await axios.get( + `${apiServiceUrl}/api/workspace/${workspaceId}/datasources`, + { + headers: { + 'Authorization': `Bearer ${apiKey}` + } + } + ); + return response.data || []; + } catch (error) { + console.error("Error fetching workspace data sources:", error); + throw error; + } +}; + +// Function to merge regular and managed data sources +export const getMergedDataSources = (standardDataSources: DataSource[], managedDataSources: any[]): DataSource[] => { + return standardDataSources.map((dataSource) => ({ + ...dataSource, + managed: managedDataSources.some((managedDs) => managedDs.datasourceGid === dataSource.gid), + })); +}; + +// Calculate data source statistics +export const calculateDataSourceStats = (dataSources: DataSource[]): DataSourceStats => { + const uniqueTypes = new Set(dataSources.map(ds => ds.type)).size; + const managedCount = dataSources.filter(ds => ds.managed).length; + + return { + total: dataSources.length, + types: uniqueTypes, + managed: managedCount, + unmanaged: dataSources.length - managedCount + }; +}; + +// Get and merge data sources from a workspace +export async function getMergedWorkspaceDataSources( + workspaceId: string, + environmentId: string, + apiKey: string, + apiServiceUrl: string +): Promise { + try { + // First, get regular data sources for the workspace + const regularDataSources = await getWorkspaceDataSources( + workspaceId, + apiKey, + apiServiceUrl + ); + + // If no data sources, return early with empty result + if (!regularDataSources.length) { + return { + dataSources: [], + stats: { + total: 0, + types: 0, + managed: 0, + unmanaged: 0 + } + }; + } + + // Only fetch managed data sources if we have regular data sources + let managedDataSources = []; + try { + managedDataSources = await getManagedDataSources(environmentId); + } catch (error) { + console.error("Failed to fetch managed data sources:", error); + // Continue with empty managed list + } + + // Use the merge function + const mergedDataSources = getMergedDataSources(regularDataSources, managedDataSources); + + // Calculate stats + const stats = calculateDataSourceStats(mergedDataSources); + + return { + dataSources: mergedDataSources, + stats + }; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Failed to fetch data sources"; + message.error(errorMessage); + throw error; + } +} + +// Function to deploy a data source to another environment +export interface DeployDataSourceParams { + envId: string; + targetEnvId: string; + datasourceId: string; + updateDependenciesIfNeeded?: boolean; +} + +export const deployDataSource = async ( + params: DeployDataSourceParams, + apiServiceUrl: string +): Promise => { + try { + const response = await axios.post( + `${apiServiceUrl}/api/plugins/enterprise/deploy-datasource`, + null, + { + params: { + envId: params.envId, + targetEnvId: params.targetEnvId, + datasourceId: params.datasourceId, + updateDependenciesIfNeeded: params.updateDependenciesIfNeeded ?? false + } + } + ); + + return response.status === 200; + } catch (error) { + console.error('Error deploying data source:', error); + throw error; + } +}; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts b/client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts index 8fd6d17fd4..e0bc1efd2b 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts +++ b/client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts @@ -138,4 +138,48 @@ export async function unconnectManagedApp(appGid: string) { message.error(errorMsg); throw err; } -} \ No newline at end of file +} + +// data sources + +export const getManagedDataSources = async (environmentId: string): Promise => { + try { + const response = await axios.get( + `/api/plugins/enterprise/datasource/list?environmentId=${environmentId}` + ); + return response.data || []; + } catch (error) { + console.error("Error fetching managed data sources:", error); + throw error; + } +}; + +// Connect a data source to be managed +export const connectManagedDataSource = async ( + environmentId: string, + name: string, + datasourceGid: string +): Promise => { + try { + await axios.post(`/api/plugins/enterprise/datasource`, { + environmentId, + name, + datasourceGid + }); + } catch (error) { + console.error("Error connecting managed data source:", error); + throw error; + } +}; + +// Disconnect a managed data source +export const unconnectManagedDataSource = async ( + datasourceGid: string +): Promise => { + try { + await axios.delete(`/api/plugins/enterprise/datasource?datasourceGid=${datasourceGid}`); + } catch (error) { + console.error("Error disconnecting managed data source:", error); + throw error; + } +}; \ 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 2209868030..09dc193ac3 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 @@ -28,6 +28,7 @@ export interface DataSourceConfig { pluginDefinition: any | null; createTime: number; datasourceConfig: DataSourceConfig; + managed?: boolean; } /** From a5205547da53f88f0a263265bf2c09b84a3134bd Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 15 Apr 2025 01:31:36 +0500 Subject: [PATCH 34/68] fix data sources tab --- .../setting/environments/WorkspaceDetail.tsx | 68 +----- .../components/DataSourcesList.tsx | 199 +++++++++--------- .../components/DataSourcesTab.tsx | 119 +++++++++++ .../components/DeployDataSourceModal.tsx | 126 +++++++++++ .../hooks/useWorkspaceDataSources.ts | 102 ++++++--- .../services/datasources.service.ts | 79 ++++--- 6 files changed, 468 insertions(+), 225 deletions(-) create mode 100644 client/packages/lowcoder/src/pages/setting/environments/components/DataSourcesTab.tsx create mode 100644 client/packages/lowcoder/src/pages/setting/environments/components/DeployDataSourceModal.tsx diff --git a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx index eaf2f24c3a..72ec132d95 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx @@ -35,6 +35,7 @@ import { App } from "./types/app.types"; import { getMergedApps } from "./utils/getMergedApps"; import { connectManagedApp, unconnectManagedApp } from "./services/enterprise.service"; import AppsTab from "./components/AppsTab"; +import DataSourcesTab from "./components/DataSourcesTab"; const { Title, Text } = Typography; const { TabPane } = Tabs; @@ -58,16 +59,7 @@ const WorkspaceDetail: React.FC = () => { workspace, loading: workspaceLoading, error: workspaceError, - refresh: refreshWorkspace } = useWorkspace(environment, workspaceId); - - const { - dataSources, - loading: dataSourcesLoading, - error: dataSourcesError, - refresh: refreshDataSources, - dataSourceStats, - } = useWorkspaceDataSources(environment, workspaceId); if (envLoading || workspaceLoading) { return ( @@ -139,63 +131,7 @@ const WorkspaceDetail: React.FC = () => { tab={ Data Sources} key="dataSources" > - - {/* Header with refresh button */} -
- Data Sources in this Workspace - -
- - {/* Data Source Statistics */} - -
- } - /> - - - } - /> - - - - - - {/* Show error if data sources loading failed */} - {dataSourcesError && ( - - Try Again - - } - /> - )} - - {/* Data Sources List */} - - + Promise; + onRefresh?: () => void; } -/** - * Component to display a list of data sources in a table - */ const DataSourcesList: React.FC = ({ dataSources, loading, error, + environment, + onToggleManaged, + onRefresh, }) => { - // Format timestamp to date string - const formatDate = (timestamp?: number): string => { - if (!timestamp) return 'N/A'; - const date = new Date(timestamp); - return `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`; - }; + const [deployModalVisible, setDeployModalVisible] = useState(false); + const [selectedDataSource, setSelectedDataSource] = useState(null); - // Get icon for data source type - const getDataSourceTypeIcon = (type: string) => { - return ; + const handleDeploy = (dataSource: DataSource) => { + setSelectedDataSource(dataSource); + setDeployModalVisible(true); }; - // Get color for data source status - const getStatusColor = (status: string) => { - switch (status) { - case 'NORMAL': - return 'green'; - case 'ERROR': - return 'red'; - case 'WARNING': - return 'orange'; - default: - return 'default'; - } - }; - - // Table columns definition - const columns = [ + const columns: ColumnsType = [ { title: 'Name', + dataIndex: 'name', key: 'name', - render: (record: DataSourceWithMeta) => ( -
- {getDataSourceTypeIcon(record.datasource.type)} - {record.datasource.name} -
- ), + sorter: (a, b) => a.name.localeCompare(b.name), }, { title: 'Type', - dataIndex: ['datasource', 'type'], + dataIndex: 'type', key: 'type', - render: (type: string) => ( - {type.toUpperCase()} - ), + filters: Array.from(new Set(dataSources.map(ds => ds.type))) + .map(type => ({ text: type, value: type })), + onFilter: (value, record) => record.type === value, }, { - title: 'Created By', - dataIndex: 'creatorName', - key: 'creatorName', - render: (creatorName: string) => ( -
- - {creatorName} -
+ title: 'Status', + dataIndex: 'datasourceStatus', + key: 'status', + render: (status: string) => ( + + {status} + ), + filters: Array.from(new Set(dataSources.map(ds => ds.datasourceStatus))) + .map(status => ({ text: status, value: status })), + onFilter: (value, record) => record.datasourceStatus === value, }, { - title: 'Created', - key: 'createTime', - render: (record: DataSourceWithMeta) => formatDate(record.datasource.createTime), + title: 'DB Name', + dataIndex: ['datasourceConfig', 'database'], + key: 'database', + render: (database: string | null) => database || 'N/A', }, { - title: 'Status', - key: 'status', - render: (record: DataSourceWithMeta) => ( - - {record.datasource.datasourceStatus} - + title: 'Managed', + dataIndex: 'managed', + key: 'managed', + render: (managed: boolean, record: DataSource) => ( + + onToggleManaged(record, checked)} + /> + + {managed ? 'Managed' : 'Unmanaged'} + + ), + filters: [ + { text: 'Managed', value: true }, + { text: 'Unmanaged', value: false }, + ], + onFilter: (value, record) => record.managed === Boolean(value), }, { - title: 'Edit Access', - dataIndex: 'edit', - key: 'edit', - render: (edit: boolean) => ( - - {edit ? - : - - } - + title: 'Actions', + key: 'actions', + render: (_, record: DataSource) => ( + + + + + ), }, ]; - // If loading, show spinner - if (loading) { - return ( -
- -
- ); - } - - // If no data sources or error, show empty state - if (!dataSources || dataSources.length === 0 || error) { - return ( - - ); - } - return ( -
record.datasource.id} - pagination={{ pageSize: 10 }} - size="middle" - /> + <> +
+ + setDeployModalVisible(false)} + onSuccess={onRefresh} + /> + ); }; 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 0000000000..e7d4d39c49 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/components/DataSourcesTab.tsx @@ -0,0 +1,119 @@ +// components/DataSourcesTab.tsx +// Create this new file + +import React from 'react'; +import { Card, Button, Row, Col, Statistic, Divider, Alert, message } from 'antd'; +import { DatabaseOutlined, SyncOutlined } from '@ant-design/icons'; +import Title from 'antd/lib/typography/Title'; +import { Environment } from '../types/environment.types'; +import { useWorkspaceDataSources } from '../hooks/useWorkspaceDataSources'; +import DataSourcesList from './DataSourcesList'; +import { DataSource } from '../types/datasource.types'; + +interface DataSourcesTabProps { + environment: Environment; + workspaceId: string; +} + +const DataSourcesTab: React.FC = ({ environment, workspaceId }) => { + const { + dataSources, + stats, + loading, + error, + toggleManagedStatus + } = useWorkspaceDataSources(environment, workspaceId); + + const handleToggleManagedDataSource = async (dataSource: DataSource, checked: boolean) => { + const success = await toggleManagedStatus(dataSource, checked); + + if (success) { + message.success(`${dataSource.name} is now ${checked ? "Managed" : "Unmanaged"}`); + } else { + message.error(`Failed to toggle ${dataSource.name}`); + } + }; + + return ( + + {/* Header with refresh button */} +
+ Data Sources in this Workspace +
+ + {/* Data Source Statistics */} + +
+ } + /> + + + } + /> + + + } + /> + + + } + /> + + + + + + {/* Show error if data sources loading failed */} + {error && ( + + )} + + {/* Configuration warning */} + {(!environment.environmentApikey || + !environment.environmentApiServiceUrl) && + !error && ( + + )} + + {/* Data Sources List */} + + + ); +}; + +export default DataSourcesTab; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/DeployDataSourceModal.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/DeployDataSourceModal.tsx new file mode 100644 index 0000000000..29a41c0954 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/components/DeployDataSourceModal.tsx @@ -0,0 +1,126 @@ +// components/DeployDataSourceModal.tsx +// Create this new file + +import React, { useState, useEffect } from 'react'; +import { Modal, Form, Select, Checkbox, Button, message, Spin } from 'antd'; +import { Environment } from '../types/environment.types'; +import { DataSource } from '../types/datasource.types'; +import { deployDataSource } from '../services/datasources.service'; +import { useEnvironmentContext } from '../context/EnvironmentContext'; + +interface DeployDataSourceModalProps { + visible: boolean; + dataSource: DataSource | null; + currentEnvironment: Environment; + onClose: () => void; + onSuccess?: () => void; +} + +const DeployDataSourceModal: React.FC = ({ + visible, + dataSource, + currentEnvironment, + onClose, + onSuccess, +}) => { + const [form] = Form.useForm(); + const { environments, isLoadingEnvironments } = useEnvironmentContext(); + const [deploying, setDeploying] = useState(false); + + // Reset form when modal becomes visible + useEffect(() => { + if (visible) { + form.resetFields(); + } + }, [visible, form]); + + // Filter out current environment from the list + const targetEnvironments = environments.filter( + (env) => env.environmentId !== currentEnvironment.environmentId + ); + + const handleDeploy = async () => { + try { + const values = await form.validateFields(); + + if (!dataSource) return; + + setDeploying(true); + + await deployDataSource( + { + envId: currentEnvironment.environmentId, + targetEnvId: values.targetEnvId, + datasourceId: dataSource.gid, + updateDependenciesIfNeeded: values.updateDependenciesIfNeeded + }, + currentEnvironment.environmentApiServiceUrl! + ); + + message.success(`Successfully deployed ${dataSource.name} to target environment`); + if (onSuccess) onSuccess(); + onClose(); + } catch (error) { + console.error('Deployment error:', error); + message.error('Failed to deploy data source'); + } finally { + setDeploying(false); + } + }; + + return ( + + {isLoadingEnvironments ? ( +
+ +
+ ) : ( +
+ + + + + + Update Dependencies If Needed + + + + + + + + )} +
+ ); +}; + +export default DeployDataSourceModal; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaceDataSources.ts b/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaceDataSources.ts index d323b57775..61a93fea0e 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaceDataSources.ts +++ b/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaceDataSources.ts @@ -1,20 +1,22 @@ +// hooks/useWorkspaceDataSources.ts +// Create this new file + import { useState, useEffect, useCallback } from "react"; -import { getWorkspaceDataSources } from "../services/environments.service"; +import { getMergedWorkspaceDataSources } from "../services/datasources.service"; +import { connectManagedDataSource, unconnectManagedDataSource } from "../services/enterprise.service"; import { Environment } from "../types/environment.types"; -import { DataSourceWithMeta } from "../types/datasource.types"; - -interface DataSourceStats { - total: number; - types: number; // unique types -} +import { DataSource } from "../types/datasource.types"; -export const useWorkspaceDataSources = ( - environment: Environment | null, - workspaceId: string -) => { - const [dataSources, setDataSources] = useState([]); +export const useWorkspaceDataSources = (environment: Environment | null, workspaceId: string) => { + const [dataSources, setDataSources] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + const [stats, setStats] = useState({ + total: 0, + types: 0, + managed: 0, + unmanaged: 0 + }); const fetchDataSources = useCallback(async () => { if (!environment || !workspaceId) return; @@ -23,48 +25,86 @@ export const useWorkspaceDataSources = ( setError(null); try { - const { environmentApikey, environmentApiServiceUrl } = environment; + const { environmentId, environmentApikey, environmentApiServiceUrl } = environment; + + // Validate required configuration + if (!environmentApikey) { + setError("No API key configured for this environment. Data sources cannot be fetched."); + setLoading(false); + return; + } - if (!environmentApikey || !environmentApiServiceUrl) { - setError("Missing API key or service URL. Data sources cannot be fetched."); + if (!environmentApiServiceUrl) { + setError("No API service URL configured for this environment. Data sources cannot be fetched."); setLoading(false); return; } - const data = await getWorkspaceDataSources( + // Get merged data sources + const result = await getMergedWorkspaceDataSources( workspaceId, + environmentId, environmentApikey, environmentApiServiceUrl ); - - setDataSources(data); + + // Update state with results + setDataSources(result.dataSources); + setStats(result.stats); + setLoading(false); } catch (err) { - setError( - err instanceof Error ? err.message : "Failed to fetch data sources" - ); - } finally { + setError(err instanceof Error ? err.message : "Failed to fetch data sources"); setLoading(false); } }, [environment, workspaceId]); useEffect(() => { - if (environment) { + if (environment && workspaceId) { fetchDataSources(); } - }, [environment, fetchDataSources]); + }, [environment, workspaceId, fetchDataSources]); + + const toggleManagedStatus = async (dataSource: DataSource, checked: boolean) => { + try { + if (!environment) return false; + + if (checked) { + await connectManagedDataSource(environment.environmentId, dataSource.name, dataSource.gid); + } else { + await unconnectManagedDataSource(dataSource.gid); + } - const uniqueTypes = new Set(dataSources.map(ds => ds.datasource.type)); + // Optimistically update the state + setDataSources(prevDataSources => + prevDataSources.map(ds => + ds.id === dataSource.id ? { ...ds, managed: checked } : ds + ) + ); + + // Update stats + const updatedDataSources = dataSources.map(ds => + ds.id === dataSource.id ? { ...ds, managed: checked } : ds + ); + + const managedCount = updatedDataSources.filter(ds => ds.managed).length; + + setStats(prevStats => ({ + ...prevStats, + managed: managedCount, + unmanaged: updatedDataSources.length - managedCount + })); - const dataSourceStats: DataSourceStats = { - total: dataSources.length, - types: uniqueTypes.size, + return true; // Success indicator + } catch (err) { + return false; // Failure indicator + } }; return { dataSources, loading, error, - refresh: fetchDataSources, - dataSourceStats, + stats, + toggleManagedStatus, }; -}; +}; \ No newline at end of file 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 e3a3a8fd8a..1454a2cf41 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/services/datasources.service.ts +++ b/client/packages/lowcoder/src/pages/setting/environments/services/datasources.service.ts @@ -1,10 +1,8 @@ // services/dataSources.service.ts -// Create this new file - import axios from 'axios'; import { message } from "antd"; -import { DataSource } from "../types/datasource.types"; -import { getManagedDataSources } from './enterprise.service'; +import { DataSource, DataSourceWithMeta } from "../types/datasource.types"; +import { getManagedDataSources } from "./enterprise.service"; export interface DataSourceStats { total: number; @@ -18,34 +16,63 @@ export interface MergedDataSourcesResult { stats: DataSourceStats; } -// Get data sources for a workspace -export const getWorkspaceDataSources = async ( - workspaceId: string, +// Get data sources for a workspace - using your correct implementation +export async function getWorkspaceDataSources( + workspaceId: string, apiKey: string, apiServiceUrl: string -): Promise => { +): Promise { try { - const response = await axios.get( - `${apiServiceUrl}/api/workspace/${workspaceId}/datasources`, - { - headers: { - 'Authorization': `Bearer ${apiKey}` - } + // Check if required parameters are provided + if (!workspaceId) { + throw new Error('Workspace ID is required'); + } + + if (!apiKey) { + throw new Error('API key is required to fetch data sources'); + } + + if (!apiServiceUrl) { + throw new Error('API service URL is required to fetch data sources'); + } + + // Set up headers with the Bearer token format + const headers = { + Authorization: `Bearer ${apiKey}` + }; + + // Make the API request to get data sources + const response = await axios.get<{data:DataSourceWithMeta[]}>(`${apiServiceUrl}/api/datasources/listByOrg`, { + headers, + params: { + orgId: workspaceId } - ); - return response.data || []; + }); + console.log("data source response",response); + + // Check if response is valid + if (!response.data) { + return []; + } + + return response.data.data; } catch (error) { - console.error("Error fetching workspace data sources:", error); + // Handle and transform error + const errorMessage = error instanceof Error ? error.message : 'Failed to fetch data sources'; + message.error(errorMessage); throw error; } -}; +} // Function to merge regular and managed data sources -export const getMergedDataSources = (standardDataSources: DataSource[], managedDataSources: any[]): DataSource[] => { - return standardDataSources.map((dataSource) => ({ - ...dataSource, - managed: managedDataSources.some((managedDs) => managedDs.datasourceGid === dataSource.gid), - })); +export const getMergedDataSources = (standardDataSources: DataSourceWithMeta[], managedDataSources: any[]): DataSource[] => { + return standardDataSources.map((dataSourceWithMeta) => { + const dataSource = dataSourceWithMeta.datasource; + return { + ...dataSource, + managed: managedDataSources.some((managedDs) => managedDs.datasourceGid === dataSource.gid), + }; + }); }; // Calculate data source statistics @@ -70,14 +97,14 @@ export async function getMergedWorkspaceDataSources( ): Promise { try { // First, get regular data sources for the workspace - const regularDataSources = await getWorkspaceDataSources( + const regularDataSourcesWithMeta = await getWorkspaceDataSources( workspaceId, apiKey, apiServiceUrl ); // If no data sources, return early with empty result - if (!regularDataSources.length) { + if (!regularDataSourcesWithMeta.length) { return { dataSources: [], stats: { @@ -99,7 +126,7 @@ export async function getMergedWorkspaceDataSources( } // Use the merge function - const mergedDataSources = getMergedDataSources(regularDataSources, managedDataSources); + const mergedDataSources = getMergedDataSources(regularDataSourcesWithMeta, managedDataSources); // Calculate stats const stats = calculateDataSourceStats(mergedDataSources); From 10b7140ba7f50d027001214b290dc1c59ec2a20a Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 15 Apr 2025 14:50:02 +0500 Subject: [PATCH 35/68] test generic approach --- .../environments/EnvironmentDetail.tsx | 19 +-- .../components/DeployableItemsList.tsx | 103 ++++++++++++ .../components/DeployableItemsTab.tsx | 126 +++++++++++++++ .../environments/config/workspace.config.tsx | 116 ++++++++++++++ .../environments/hooks/useDeployableItems.ts | 146 ++++++++++++++++++ .../types/deployable-item.types.ts | 71 +++++++++ 6 files changed, 572 insertions(+), 9 deletions(-) create mode 100644 client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsList.tsx create mode 100644 client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsTab.tsx create mode 100644 client/packages/lowcoder/src/pages/setting/environments/config/workspace.config.tsx create mode 100644 client/packages/lowcoder/src/pages/setting/environments/hooks/useDeployableItems.ts create mode 100644 client/packages/lowcoder/src/pages/setting/environments/types/deployable-item.types.ts diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx index 0a37c56726..5268a53587 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx @@ -21,6 +21,9 @@ import { import { useEnvironmentContext } from "./context/EnvironmentContext"; import WorkspacesTab from "./components/WorkspacesTab"; import UserGroupsTab from "./components/UserGroupsTab"; +import { workspaceConfig } from "./config/workspace.config"; +import DeployableItemsTab from "./components/DeployableItemsTab"; + const { Title, Text } = Typography; @@ -154,15 +157,13 @@ const EnvironmentDetail: React.FC = () => { {/* Tabs for Workspaces and User Groups */} - - Workspaces - - } - key="workspaces" - > - + + {/* Using our new generic component with the workspace config */} + { + 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) { + // Handle row click for navigation + const handleRowClick = (item: T) => { + // 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); + }; + + // Generate columns based on config + let columns = [...config.columns]; + + // Add managed column if enabled + if (config.enableManaged) { + columns.push({ + title: 'Managed', + key: 'managed', + render: (_, record: T) => ( + + + {record.managed ? 'Managed' : 'Unmanaged'} + + {onToggleManaged && ( + { + e.stopPropagation(); // Stop row click event + onToggleManaged(record, checked); + }} + onChange={() => {}} + /> + )} + + ), + }); + } + + if (loading) { + return ( +
+ +
+ ); + } + + if (!items || items.length === 0 || error) { + return ( + + ); + } + + return ( +
({ + onClick: () => handleRowClick(record), + style: { cursor: 'pointer' }, + })} + /> + ); +} + +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 new file mode 100644 index 0000000000..4e50a873c7 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsTab.tsx @@ -0,0 +1,126 @@ +// 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/config/workspace.config.tsx b/client/packages/lowcoder/src/pages/setting/environments/config/workspace.config.tsx new file mode 100644 index 0000000000..21eee9f870 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/config/workspace.config.tsx @@ -0,0 +1,116 @@ +// config/workspace.config.tsx +import React from 'react'; +import { Row, Col, Statistic, Tag } from 'antd'; +import { ClusterOutlined } from '@ant-design/icons'; +import { Workspace, WorkspaceStats, DeployableItemConfig } from '../types/deployable-item.types'; +import { buildEnvironmentWorkspaceId } from '@lowcoder-ee/constants/routesURL'; +import { getMergedEnvironmentWorkspaces } from '../services/workspace.service'; +import { connectManagedWorkspace, unconnectManagedWorkspace } from '../services/enterprise.service'; + +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 + }; + }, + + // Table configuration + 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} + + ), + } + ], + + // Deployment options + enableManaged: true, + + // Service functions + fetchItems: async ({ environment }) => { + const result = await getMergedEnvironmentWorkspaces( + environment.environmentId, + environment.environmentApikey, + environment.environmentApiServiceUrl! + ); + return result.workspaces; + }, + + 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; + } + } +}; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/hooks/useDeployableItems.ts b/client/packages/lowcoder/src/pages/setting/environments/hooks/useDeployableItems.ts new file mode 100644 index 0000000000..bb04cf54fd --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/hooks/useDeployableItems.ts @@ -0,0 +1,146 @@ +// 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/types/deployable-item.types.ts b/client/packages/lowcoder/src/pages/setting/environments/types/deployable-item.types.ts new file mode 100644 index 0000000000..bb303e89f6 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/types/deployable-item.types.ts @@ -0,0 +1,71 @@ +// types/deployable-item.types.ts +import { ReactNode } from 'react'; +import { Environment } from './environment.types'; + +// Base interface for all deployable items +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 +export interface BaseStats { + total: number; + managed: number; + unmanaged: number; +} + +export interface WorkspaceStats extends BaseStats {} + +// 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; + + // Table configuration + columns: Array<{ + title: string; + dataIndex?: string; + key: string; + render?: (value: any, record: T) => ReactNode; + ellipsis?: boolean; + }>; + + // Deployable configuration + enableManaged: boolean; + + // Service functions + fetchItems: (params: { environment: Environment, [key: string]: any }) => Promise; + toggleManaged: (params: { item: T; checked: boolean; environment: Environment }) => Promise; +} \ No newline at end of file From 23b8462a630165cd1dacc887ebaedf6b2e2e2f2d Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 15 Apr 2025 18:25:56 +0500 Subject: [PATCH 36/68] add user groups tab generic --- .../environments/EnvironmentDetail.tsx | 9 +- .../components/DeployableItemsList.tsx | 47 +++--- .../environments/config/usergroups.config.tsx | 143 ++++++++++++++++++ .../environments/types/userGroup.types.ts | 20 ++- 4 files changed, 195 insertions(+), 24 deletions(-) create mode 100644 client/packages/lowcoder/src/pages/setting/environments/config/usergroups.config.tsx diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx index 5268a53587..00ce1636aa 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx @@ -22,6 +22,7 @@ import { useEnvironmentContext } from "./context/EnvironmentContext"; import WorkspacesTab from "./components/WorkspacesTab"; import UserGroupsTab from "./components/UserGroupsTab"; import { workspaceConfig } from "./config/workspace.config"; +import { userGroupsConfig } from "./config/usergroups.config"; import DeployableItemsTab from "./components/DeployableItemsTab"; @@ -173,7 +174,13 @@ const EnvironmentDetail: React.FC = () => { } key="userGroups" > - + {/* Using our new generic component with the user group config */} + + diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsList.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsList.tsx index a04687ffcc..4918f4e82b 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsList.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsList.tsx @@ -27,16 +27,20 @@ function DeployableItemsList({ additionalParams = {} }: DeployableItemsListProps) { // Handle row click for navigation - const handleRowClick = (item: T) => { - // 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); - }; + // 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); +}; // Generate columns based on config let columns = [...config.columns]; @@ -85,18 +89,21 @@ function DeployableItemsList({ ); } + const hasNavigation = config.buildDetailRoute({}) !== '#'; + + return (
({ - onClick: () => handleRowClick(record), - style: { cursor: 'pointer' }, - })} - /> + columns={columns} + dataSource={items} + rowKey={config.idField} + pagination={{ pageSize: 10 }} + size="middle" + onRow={(record) => ({ + onClick: hasNavigation ? () => handleRowClick(record) : undefined, + style: hasNavigation ? { cursor: 'pointer' } : undefined, + })} + /> ); } 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 new file mode 100644 index 0000000000..bcf83c3e3c --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/config/usergroups.config.tsx @@ -0,0 +1,143 @@ +// 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'; + + +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, + + // 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/types/userGroup.types.ts b/client/packages/lowcoder/src/pages/setting/environments/types/userGroup.types.ts index cd5ec1ec8b..6a1938bcc3 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,7 +1,10 @@ /** * Represents a User Group entity in an environment - */ -export interface UserGroup { +*/ + +import { DeployableItem, BaseStats } from "./deployable-item.types"; + +export interface UserGroup extends DeployableItem { groupId: string; groupGid: string; groupName: string; @@ -17,4 +20,15 @@ export interface UserGroup { syncDelete: boolean; devGroup: boolean; syncGroup: boolean; - } \ No newline at end of file + id: string; + name: string; + } + + + /** + * Statistics for User Groups + */ +export interface UserGroupStats extends BaseStats { + totalUsers: number; + adminUsers: number; +} \ No newline at end of file From ed1f8da37cd481c893532bb4882283313819a510 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 15 Apr 2025 18:44:40 +0500 Subject: [PATCH 37/68] add generic tab for apps --- .../setting/environments/WorkspaceDetail.tsx | 20 +-- .../environments/config/apps.config.tsx | 143 ++++++++++++++++++ .../setting/environments/types/app.types.ts | 9 +- 3 files changed, 159 insertions(+), 13 deletions(-) create mode 100644 client/packages/lowcoder/src/pages/setting/environments/config/apps.config.tsx diff --git a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx index 72ec132d95..2c8348f8a4 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx @@ -11,10 +11,7 @@ import { Tabs, Alert, Button, - Statistic, - Divider, Breadcrumb, - message } from "antd"; import { AppstoreOutlined, @@ -22,20 +19,14 @@ import { CodeOutlined, HomeOutlined, TeamOutlined, - SyncOutlined, ArrowLeftOutlined } from "@ant-design/icons"; -import AppsList from './components/AppsList'; import { useEnvironmentContext } from "./context/EnvironmentContext"; import { useWorkspace } from "./hooks/useWorkspace"; -import { useWorkspaceApps } from "./hooks/useWorkspaceApps"; -import { useWorkspaceDataSources } from "./hooks/useWorkspaceDataSources"; -import { useManagedApps } from "./hooks/enterprise/useManagedApps"; -import { App } from "./types/app.types"; -import { getMergedApps } from "./utils/getMergedApps"; -import { connectManagedApp, unconnectManagedApp } from "./services/enterprise.service"; import AppsTab from "./components/AppsTab"; import DataSourcesTab from "./components/DataSourcesTab"; +import DeployableItemsTab from "./components/DeployableItemsTab"; +import { appsConfig } from "./config/apps.config"; const { Title, Text } = Typography; const { TabPane } = Tabs; @@ -123,7 +114,12 @@ const WorkspaceDetail: React.FC = () => { tab={ Apps} key="apps" > - + {/* Update the TabPane in WorkspaceDetail.tsx */} 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 new file mode 100644 index 0000000000..75f274709a --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/config/apps.config.tsx @@ -0,0 +1,143 @@ +// config/apps.config.tsx +import React from 'react'; +import { Row, Col, Statistic, Tag, Space, Button, Tooltip } from 'antd'; +import { AppstoreOutlined, CloudUploadOutlined } from '@ant-design/icons'; +import {DeployableItemConfig } from '../types/deployable-item.types'; +import { Environment } from '../types/environment.types'; +import { getMergedWorkspaceApps } from '../services/apps.service'; +import { connectManagedApp, unconnectManagedApp } from '../services/enterprise.service'; +import { App, AppStats } from '../types/app.types'; + +// 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: () => '#', + + + // 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 + columns: [ + { + title: 'Name', + dataIndex: 'name', + key: 'name', + }, + { + title: 'Description', + dataIndex: 'description', + key: 'description', + ellipsis: true, + }, + { + title: 'Status', + dataIndex: 'published', + key: 'published', + render: (published: boolean) => ( + + {published ? 'Published' : 'Unpublished'} + + ), + }, + { + title: 'Actions', + key: 'actions', + render: (_, record: App) => ( + + + + + + ), + } + ], + + // 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 + })); + }, + + 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; + } + } +}; \ No newline at end of file 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 67c445f92b..b3af252b58 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,4 +1,6 @@ -export interface App { +import { DeployableItem, BaseStats } from "./deployable-item.types"; + +export interface App extends DeployableItem { orgId: string; applicationId: string; applicationGid: string; @@ -23,4 +25,9 @@ export interface App { published: boolean; folder: boolean; managed?: boolean; + id: string + } + + export interface AppStats extends BaseStats { + published: number } \ No newline at end of file From 2934db2497f12aec36c7c22b144e96a3b03e3631 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 15 Apr 2025 19:17:07 +0500 Subject: [PATCH 38/68] setup data sources generic --- .../setting/environments/WorkspaceDetail.tsx | 8 +- .../config/data-sources.config.tsx | 148 ++++++++++++++++++ .../environments/types/datasource.types.ts | 8 +- 3 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 client/packages/lowcoder/src/pages/setting/environments/config/data-sources.config.tsx diff --git a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx index 2c8348f8a4..9255472d83 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx @@ -27,6 +27,7 @@ import AppsTab from "./components/AppsTab"; import DataSourcesTab from "./components/DataSourcesTab"; import DeployableItemsTab from "./components/DeployableItemsTab"; import { appsConfig } from "./config/apps.config"; +import { dataSourcesConfig } from "./config/data-sources.config"; const { Title, Text } = Typography; const { TabPane } = Tabs; @@ -127,7 +128,12 @@ const WorkspaceDetail: React.FC = () => { tab={ Data Sources} key="dataSources" > - + = { + // 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} + + ), + }, + { + title: 'Actions', + key: 'actions', + render: (_, record: DataSource) => ( + + + + + + ), + } + ], + + // 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; + }, + + 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; + } + } +}; \ 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 09dc193ac3..f4f03072db 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,6 +1,8 @@ /** * Represents a DataSource configuration */ + +import { DeployableItem, BaseStats } from "./deployable-item.types"; export interface DataSourceConfig { usingUri: boolean; srvMode: boolean; @@ -16,7 +18,7 @@ export interface DataSourceConfig { /** * Represents a DataSource entity */ - export interface DataSource { + export interface DataSource extends DeployableItem { id: string; createdBy: string; gid: string; @@ -38,4 +40,8 @@ export interface DataSourceConfig { datasource: DataSource; edit: boolean; creatorName: string; + } + + export interface DataSourceStats extends BaseStats { + byType: Record; // Count by each type } \ No newline at end of file From b704492ba4f9a2330d2b5fbd67bfc9c589336e8b Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 15 Apr 2025 19:54:16 +0500 Subject: [PATCH 39/68] fix data source payload --- .../environments/services/enterprise.service.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts b/client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts index e0bc1efd2b..215e35586e 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts +++ b/client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts @@ -161,11 +161,14 @@ export const connectManagedDataSource = async ( datasourceGid: string ): Promise => { try { - await axios.post(`/api/plugins/enterprise/datasource`, { - environmentId, + const payload = { + environment_id: environmentId, name, - datasourceGid - }); + datasource_gid: datasourceGid, + }; + + + await axios.post(`/api/plugins/enterprise/datasource`, payload); } catch (error) { console.error("Error connecting managed data source:", error); throw error; From f2dd6a5b92f18ae8b267d93b0d59d7593fde42e2 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 15 Apr 2025 20:45:36 +0500 Subject: [PATCH 40/68] add query services --- .../services/enterprise.service.ts | 93 ++++++++++++++++++- .../services/environments.service.ts | 85 +++++++++++++++++ .../environments/services/query.service.ts | 54 +++++++++++ .../setting/environments/types/query.types.ts | 57 ++++++++++++ 4 files changed, 288 insertions(+), 1 deletion(-) create mode 100644 client/packages/lowcoder/src/pages/setting/environments/services/query.service.ts create mode 100644 client/packages/lowcoder/src/pages/setting/environments/types/query.types.ts diff --git a/client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts b/client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts index 215e35586e..fe24330349 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts +++ b/client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts @@ -1,6 +1,8 @@ import axios from "axios"; import { message } from "antd"; import { ManagedOrg } from "../types/enterprise.types"; +import { Query } from "../types/query.types"; + /** * Fetch workspaces for a specific environment @@ -185,4 +187,93 @@ export const unconnectManagedDataSource = async ( console.error("Error disconnecting managed data source:", error); throw error; } -}; \ No newline at end of file +}; + + + + +export async function getManagedQueries(environmentId: string): Promise { + try { + if (!environmentId) { + throw new Error('Environment ID is required'); + } + + // Get managed queries from the enterprise endpoint + const response = await axios.get(`/api/plugins/enterprise/qlQuery/list`, { + params: { + environmentId + } + }); + + if (!response.data || !Array.isArray(response.data)) { + return []; + } + + // Map the response to match our Query interface + // Note: You may need to adjust this mapping based on the actual response structure + return response.data.map((item: any) => ({ + id: item.id || item.qlQueryId, + gid: item.qlQueryGid, + name: item.qlQueryName, + organizationId: item.orgId, + libraryQueryDSL: item.libraryQueryDSL || {}, + createTime: item.createTime, + creatorName: item.creatorName || '', + managed: true // These are managed queries + })); + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Failed to fetch managed queries'; + message.error(errorMessage); + throw error; + } +} + + +export async function connectManagedQuery( + environmentId: string, + queryName: string, + queryGid: string +): Promise { + try { + if (!environmentId || !queryGid) { + throw new Error('Environment ID and Query GID are required'); + } + + const response = await axios.post('/api/plugins/enterprise/qlQuery', { + environment_id: environmentId, + ql_query_name: queryName, + ql_query_tags: [], + ql_query_gid: queryGid + }); + + return response.status === 200; + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Failed to connect query'; + message.error(errorMessage); + throw error; + } +} + + +export async function unconnectManagedQuery(queryGid: string): Promise { + try { + if (!queryGid) { + throw new Error('Query GID is required'); + } + + const response = await axios.delete(`/api/plugins/enterprise/qlQuery`, { + params: { + qlQueryGid: queryGid + } + }); + + return response.status === 200; + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Failed to disconnect query'; + message.error(errorMessage); + throw error; + } +} \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts b/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts index ccff1975f0..5171750afc 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts +++ b/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts @@ -5,6 +5,7 @@ import { Workspace } from "../types/workspace.types"; import { UserGroup } from "../types/userGroup.types"; import {App} from "../types/app.types"; import { DataSourceWithMeta } from '../types/datasource.types'; +import { Query, QueryResponse } from "../types/query.types"; /** @@ -335,4 +336,88 @@ export async function getWorkspaceDataSources( message.error(errorMessage); throw error; } +} + + + +/** + * Fetch queries for a specific workspace + * @param workspaceId - ID of the workspace (orgId) + * @param apiKey - API key for the environment + * @param apiServiceUrl - API service URL for the environment + * @param options - Additional options (name filter, pagination) + * @returns Promise with an array of queries and metadata + */ +export async function getWorkspaceQueries( + workspaceId: string, + apiKey: string, + apiServiceUrl: string, + options: { + name?: string; + pageNum?: number; + pageSize?: number; + } = {} +): Promise<{ queries: Query[], total: number }> { + try { + // Check if required parameters are provided + if (!workspaceId) { + throw new Error('Workspace ID is required'); + } + + if (!apiKey) { + throw new Error('API key is required to fetch queries'); + } + + if (!apiServiceUrl) { + throw new Error('API service URL is required to fetch queries'); + } + + // Set up headers with the Bearer token format + const headers = { + Authorization: `Bearer ${apiKey}` + }; + + // Prepare query parameters + const params: any = { + orgId: workspaceId + }; + + // Add optional parameters if provided + if (options.name) params.name = options.name; + if (options.pageNum !== undefined) params.pageNum = options.pageNum; + if (options.pageSize !== undefined) params.pageSize = options.pageSize; + + // Make the API request to get queries + const response = await axios.get(`${apiServiceUrl}/api/library-queries/listByOrg`, { + headers, + params + }); + + // Check if response is valid + if (!response.data || !response.data.success === false) { + return { queries: [], total: 0 }; + } + + // Map the response to include id field required by DeployableItem + const queries = response.data.data.map(query => ({ + ...query, + // Map to DeployableItem fields if not already present + id: query.id, + name: query.name, + managed: false // Default to unmanaged + })); + + console.log("queries",queries); + + return { + queries, + total: response.data.total + }; + + } catch (error) { + // Handle and transform error + const errorMessage = error instanceof Error ? error.message : 'Failed to fetch queries'; + message.error(errorMessage); + throw error; + } } \ No newline at end of file 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 new file mode 100644 index 0000000000..c10de30230 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/services/query.service.ts @@ -0,0 +1,54 @@ +/** + * Get merged queries (both regular and managed) for a workspace + */ +import { getManagedQueries } from './enterprise.service'; +import { getWorkspaceQueries } from './environments.service'; +import { Query } from '../types/query.types'; +export interface MergedQueriesResult { + queries: Query[]; + stats: { + total: number; + managed: number; + unmanaged: number; + }; + } + + export async function getMergedWorkspaceQueries( + workspaceId: string, + environmentId: string, + apiKey: string, + apiServiceUrl: string + ): Promise { + try { + // Fetch both regular and managed queries + const [regularQueries, managedQueries] = await Promise.all([ + getWorkspaceQueries(workspaceId, apiKey, apiServiceUrl), + getManagedQueries(environmentId) + ]); + + // Create a map of managed queries by GID for quick lookup + const managedQueryGids = new Set(managedQueries.map(query => query.gid)); + + // Mark regular queries as managed if they exist in managed queries + const mergedQueries = regularQueries.queries.map((query: Query ) => ({ + ...query, + managed: managedQueryGids.has(query.gid) + })); + + // Calculate stats + const total = mergedQueries.length; + const managed = mergedQueries.filter(query => query.managed).length; + + return { + queries: mergedQueries, + stats: { + total, + managed, + unmanaged: total - managed + } + }; + + } catch (error) { + throw error; + } + } \ 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 new file mode 100644 index 0000000000..a653522100 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/types/query.types.ts @@ -0,0 +1,57 @@ +// types/query.types.ts +import { DeployableItem } from './deployable-item.types'; + +export interface LibraryQueryDSL { + query: { + compType: string; + comp: { + bodyType: string; + body: string; + httpMethod: string; + path: string; + headers: Array<{ key: string; value: string }>; + params: Array<{ key: string; value: string }>; + bodyFormData: Array<{ key: string; value: string; type: string }>; + }; + id: string; + name: string; + order: number; + datasourceId: string; + triggerType: string; + onEvent: any[]; + notification: { + showSuccess: boolean; + showFail: boolean; + fail: any[]; + }; + timeout: string; + confirmationModal: any; + variables: any[]; + periodic: boolean; + periodicTime: string; + cancelPrevious: boolean; + depQueryName: string; + delayTime: string; + }; +} + +export interface Query extends DeployableItem { + id: string; + gid: string; + organizationId: string; + name: string; + libraryQueryDSL: LibraryQueryDSL; + createTime: number; + creatorName: string; + managed?: boolean; +} + +export interface QueryResponse { + code: number; + message: string; + data: Query[]; + pageNum: number; + pageSize: number; + total: number; + success: boolean; +} \ No newline at end of file From b4b8c1c019e6856f31a010b22162ae7927a53fce Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 15 Apr 2025 21:18:06 +0500 Subject: [PATCH 41/68] add query service tab --- .../setting/environments/WorkspaceDetail.tsx | 15 +-- .../environments/config/query.config.tsx | 121 ++++++++++++++++++ .../services/environments.service.ts | 6 +- .../environments/services/query.service.ts | 32 +++-- .../setting/environments/types/query.types.ts | 10 +- 5 files changed, 162 insertions(+), 22 deletions(-) create mode 100644 client/packages/lowcoder/src/pages/setting/environments/config/query.config.tsx diff --git a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx index 9255472d83..82c4564d40 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx @@ -28,6 +28,7 @@ import DataSourcesTab from "./components/DataSourcesTab"; import DeployableItemsTab from "./components/DeployableItemsTab"; import { appsConfig } from "./config/apps.config"; import { dataSourcesConfig } from "./config/data-sources.config"; +import { queryConfig } from "./config/query.config"; const { Title, Text } = Typography; const { TabPane } = Tabs; @@ -140,14 +141,12 @@ const WorkspaceDetail: React.FC = () => { tab={ Queries} key="queries" > - - - + 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 new file mode 100644 index 0000000000..1efce62671 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/config/query.config.tsx @@ -0,0 +1,121 @@ +// 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 } from '../services/query.service'; + +// 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 + }; + }, + + // Table configuration + columns: [ + { + title: 'Name', + dataIndex: 'name', + key: 'name', + }, + { + title: 'Creator', + dataIndex: 'creatorName', + key: 'creatorName', + }, + { + title: 'Creation Date', + key: 'createTime', + render: (_, record: Query) => { + if (!record.createTime) return 'N/A'; + const date = new Date(record.createTime); + return `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`; + }, + }, + { + title: 'Query Type', + key: 'queryType', + render: (_, record: Query) => { + const queryType = record.libraryQueryDSL?.query?.compType || 'Unknown'; + return {queryType}; + }, + } + ], + + // 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; + }, + + 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; + } + } +}; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts b/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts index 5171750afc..1ef5010001 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts +++ b/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts @@ -392,12 +392,12 @@ export async function getWorkspaceQueries( headers, params }); - + debugger // Check if response is valid - if (!response.data || !response.data.success === false) { + if (!response.data) { return { queries: [], total: 0 }; } - + console.log("RESPONSE DATA QUERIES",response.data.data); // Map the response to include id field required by DeployableItem const queries = response.data.data.map(query => ({ ...query, 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 c10de30230..90889f1913 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 @@ -20,24 +20,37 @@ export interface MergedQueriesResult { apiServiceUrl: string ): Promise { try { - // Fetch both regular and managed queries - const [regularQueries, managedQueries] = await Promise.all([ - getWorkspaceQueries(workspaceId, apiKey, apiServiceUrl), - getManagedQueries(environmentId) - ]); + // Fetch regular queries + + const regularQueries = await getWorkspaceQueries(workspaceId, apiKey, apiServiceUrl); + console.log("Regular queries response:", regularQueries); + + const managedQueries = await getManagedQueries(environmentId); + console.log("Managed queries response:", managedQueries); // Create a map of managed queries by GID for quick lookup const managedQueryGids = new Set(managedQueries.map(query => query.gid)); + console.log("Managed query GIDs:", Array.from(managedQueryGids)); // Mark regular queries as managed if they exist in managed queries - const mergedQueries = regularQueries.queries.map((query: Query ) => ({ - ...query, - managed: managedQueryGids.has(query.gid) - })); + const mergedQueries = regularQueries.queries.map((query: Query) => { + const isManaged = managedQueryGids.has(query.gid); + console.log(`Query ${query.name} (gid: ${query.gid}) is ${isManaged ? "managed" : "not managed"}`); + + return { + ...query, + managed: isManaged + }; + }); // Calculate stats const total = mergedQueries.length; const managed = mergedQueries.filter(query => query.managed).length; + console.log("Generated stats:", { + total, + managed, + unmanaged: total - managed + }); return { queries: mergedQueries, @@ -49,6 +62,7 @@ export interface MergedQueriesResult { }; } catch (error) { + console.error("Error in getMergedWorkspaceQueries:", error); throw error; } } \ 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 a653522100..5d38385b0e 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,5 @@ // types/query.types.ts -import { DeployableItem } from './deployable-item.types'; +import { DeployableItem, BaseStats } from './deployable-item.types'; export interface LibraryQueryDSL { query: { @@ -32,6 +32,7 @@ export interface LibraryQueryDSL { cancelPrevious: boolean; depQueryName: string; delayTime: string; + managed?: boolean; }; } @@ -43,7 +44,12 @@ export interface Query extends DeployableItem { libraryQueryDSL: LibraryQueryDSL; createTime: number; creatorName: string; - managed?: boolean; +} + +export interface QueryStats extends BaseStats { + total: number; + managed: number; + unmanaged: number; } export interface QueryResponse { From 83973afa51696744ec8d1d55693d0d75d285eabd Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 15 Apr 2025 23:33:11 +0500 Subject: [PATCH 42/68] add DeployModal in context --- .../components/DeployItemModal.tsx | 165 ++++++++++++++++++ .../components/DeployableItemsList.tsx | 34 +++- .../components/EnvironmentScopedRoutes.tsx | 4 + .../environments/config/apps.config.tsx | 66 ++++--- .../context/DeployModalContext.tsx | 75 ++++++++ .../types/deployable-item.types.ts | 16 ++ 6 files changed, 337 insertions(+), 23 deletions(-) create mode 100644 client/packages/lowcoder/src/pages/setting/environments/components/DeployItemModal.tsx create mode 100644 client/packages/lowcoder/src/pages/setting/environments/context/DeployModalContext.tsx diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/DeployItemModal.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/DeployItemModal.tsx new file mode 100644 index 0000000000..3ff4f284de --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/components/DeployItemModal.tsx @@ -0,0 +1,165 @@ +// components/DeployItemModal.tsx +import React, { useState, useEffect } from 'react'; +import { Modal, Form, Select, Checkbox, Button, message, Spin, Input } from 'antd'; +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; + sourceEnvironment: Environment; + config: DeployableItemConfig; + onClose: () => void; + onSuccess?: () => void; +} + +function DeployItemModal({ + visible, + item, + sourceEnvironment, + config, + onClose, + onSuccess +}: DeployItemModalProps) { + const [form] = Form.useForm(); + const { environments, isLoadingEnvironments } = useEnvironmentContext(); + const [deploying, setDeploying] = useState(false); + + useEffect(() => { + if (visible) { + form.resetFields(); + } + }, [visible, form]); + + // Filter out source environment from target list + const targetEnvironments = environments.filter( + env => env.environmentId !== sourceEnvironment.environmentId + ); + + const handleDeploy = async () => { + if (!config.deploy?.enabled || !item) return; + + try { + const values = await form.validateFields(); + const targetEnv = environments.find(env => env.environmentId === values.targetEnvId); + + if (!targetEnv) { + message.error('Target environment not found'); + return; + } + + setDeploying(true); + + // Prepare parameters based on item type + const params = config.deploy.prepareParams(item, values, sourceEnvironment, targetEnv); + + // Execute deployment + await config.deploy.execute(params); + + message.success(`Successfully deployed ${item.name} to target environment`); + if (onSuccess) onSuccess(); + onClose(); + } catch (error) { + console.error('Deployment error:', error); + message.error(`Failed to deploy ${config.singularLabel.toLowerCase()}`); + } finally { + setDeploying(false); + } + }; + + return ( + + {isLoadingEnvironments ? ( +
+ +
+ ) : ( +
+ + + + + {/* Render dynamic fields based on config */} + {config.deploy?.fields.map(field => { + switch (field.type) { + case 'checkbox': + return ( + + {field.label} + + ); + case 'select': + return ( + + + + ); + case 'input': + return ( + + + + ); + default: + return null; + } + })} + + + + + + + )} +
+ ); +} + +export default DeployItemModal; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsList.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsList.tsx index 4918f4e82b..b207d98d58 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsList.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsList.tsx @@ -1,9 +1,11 @@ // components/DeployableItemsList.tsx import React from 'react'; -import { Table, Tag, Empty, Spin, Switch, Space } from 'antd'; +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[]; @@ -26,6 +28,10 @@ function DeployableItemsList({ onToggleManaged, additionalParams = {} }: DeployableItemsListProps) { + + const { openDeployModal } = useDeployModal(); + + // Handle row click for navigation // Handle row click for navigation const handleRowClick = (item: T) => { @@ -72,6 +78,32 @@ const handleRowClick = (item: T) => { }); } + // Add deploy action column if enabled + if (config.deploy?.enabled) { + columns.push({ + title: 'Actions', + key: 'actions', + render: (_, record: T) => ( + + + + + + ), + }); + } + + if (loading) { return (
diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentScopedRoutes.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentScopedRoutes.tsx index f8ffd44705..4c75ccf688 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentScopedRoutes.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentScopedRoutes.tsx @@ -4,6 +4,7 @@ import { EnvironmentProvider } from "../context/EnvironmentContext"; import EnvironmentDetail from "../EnvironmentDetail"; import WorkspaceDetail from "../WorkspaceDetail"; +import { DeployModalProvider } from "../context/DeployModalContext"; import { ENVIRONMENT_DETAIL, @@ -15,6 +16,8 @@ const EnvironmentScopedRoutes: React.FC = () => { return ( + + @@ -24,6 +27,7 @@ const EnvironmentScopedRoutes: React.FC = () => { + ); }; 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 75f274709a..cc1d285d45 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 @@ -4,7 +4,7 @@ import { Row, Col, Statistic, Tag, Space, Button, Tooltip } from 'antd'; import { AppstoreOutlined, CloudUploadOutlined } from '@ant-design/icons'; import {DeployableItemConfig } from '../types/deployable-item.types'; import { Environment } from '../types/environment.types'; -import { getMergedWorkspaceApps } from '../services/apps.service'; +import { getMergedWorkspaceApps, deployApp } from '../services/apps.service'; import { connectManagedApp, unconnectManagedApp } from '../services/enterprise.service'; import { App, AppStats } from '../types/app.types'; @@ -81,27 +81,6 @@ export const appsConfig: DeployableItemConfig = { ), }, - { - title: 'Actions', - key: 'actions', - render: (_, record: App) => ( - - - - - - ), - } ], // Deployment options @@ -139,5 +118,48 @@ export const appsConfig: DeployableItemConfig = { console.error('Error toggling managed status:', error); return false; } + }, + // deployment options + + deploy: { + enabled: true, + fields: [ + { + name: 'updateDependenciesIfNeeded', + label: 'Update Dependencies If Needed', + type: 'checkbox', + defaultValue: false + }, + { + name: 'publishOnTarget', + label: 'Publish On Target', + type: 'checkbox', + defaultValue: false + }, + { + name: 'publicToAll', + label: 'Public To All', + type: 'checkbox', + defaultValue: false + }, + { + name: 'publicToMarketplace', + label: 'Public To Marketplace', + type: 'checkbox', + defaultValue: false + } + ], + prepareParams: (item: App, values: any, sourceEnv: Environment, targetEnv: Environment) => { + return { + envId: sourceEnv.environmentId, + targetEnvId: targetEnv.environmentId, + applicationId: item.applicationId, + updateDependenciesIfNeeded: values.updateDependenciesIfNeeded, + publishOnTarget: values.publishOnTarget, + publicToAll: values.publicToAll, + publicToMarketplace: values.publicToMarketplace, + }; + }, + execute: (params: any) => deployApp(params) } }; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/context/DeployModalContext.tsx b/client/packages/lowcoder/src/pages/setting/environments/context/DeployModalContext.tsx new file mode 100644 index 0000000000..7084e94058 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/context/DeployModalContext.tsx @@ -0,0 +1,75 @@ +// context/DeployModalContext.tsx +import React, { createContext, useContext, useState } from 'react'; +import { DeployableItem, BaseStats, DeployableItemConfig } from '../types/deployable-item.types'; +import { Environment } from '../types/environment.types'; +import DeployItemModal from '../components/DeployItemModal'; + +interface DeployModalContextType { + openDeployModal: ( + item: T, + config: DeployableItemConfig, + sourceEnvironment: Environment, + onSuccess?: () => void + ) => void; +} + +const DeployModalContext = createContext(undefined); + +export const DeployModalProvider: React.FC<{children: React.ReactNode}> = ({ children }) => { + const [modalState, setModalState] = useState<{ + visible: boolean; + item: DeployableItem | null; + config: DeployableItemConfig | null; + sourceEnvironment: Environment | null; + onSuccess?: () => void; + }>({ + visible: false, + item: null, + config: null, + sourceEnvironment: null + }); + + const openDeployModal = ( + item: T, + config: DeployableItemConfig, + sourceEnvironment: Environment, + onSuccess?: () => void + ) => { + setModalState({ + visible: true, + item, + config, + sourceEnvironment, + onSuccess + }); + }; + + const closeDeployModal = () => { + setModalState(prev => ({ ...prev, visible: false })); + }; + + return ( + + {children} + + {modalState.config && modalState.sourceEnvironment && ( + + )} + + ); +}; + +export const useDeployModal = () => { + const context = useContext(DeployModalContext); + if (context === undefined) { + throw new Error('useDeployModal must be used within a DeployModalProvider'); + } + return context; +}; \ 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 bb303e89f6..d0c7139c28 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 @@ -30,6 +30,15 @@ export interface BaseStats { export interface WorkspaceStats extends BaseStats {} + +export interface DeployField { + name: string; + label: string; + type: 'checkbox' | 'select' | 'input'; + defaultValue?: any; + required?: boolean; + options?: Array<{label: string, value: any}>; // For select fields +} // Configuration for each deployable item type export interface DeployableItemConfig { // Identifying info @@ -68,4 +77,11 @@ export interface DeployableItemConfig Promise; toggleManaged: (params: { item: T; checked: boolean; environment: Environment }) => Promise; + + deploy?: { + enabled: boolean; + fields: DeployField[]; + prepareParams: (item: T, values: any, sourceEnv: Environment, targetEnv: Environment) => any; + execute: (params: any) => Promise; + }; } \ No newline at end of file From 8527dd918a4a0986e578245be0e87334ab53b688 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 15 Apr 2025 23:55:46 +0500 Subject: [PATCH 43/68] Add deployment config for Datasource --- .../setting/environments/WorkspaceDetail.tsx | 3 - .../components/DataSourcesList.tsx | 5 +- .../components/DataSourcesTab.tsx | 5 +- .../components/DeployDataSourceModal.tsx | 126 ------------------ .../config/data-sources.config.tsx | 22 ++- .../services/datasources.service.ts | 34 ++--- 6 files changed, 34 insertions(+), 161 deletions(-) delete mode 100644 client/packages/lowcoder/src/pages/setting/environments/components/DeployDataSourceModal.tsx diff --git a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx index 82c4564d40..eda5d57084 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useState } from "react"; import { useParams, useHistory } from "react-router-dom"; import history from "@lowcoder-ee/util/history"; -import DataSourcesList from './components/DataSourcesList'; import { Spin, Typography, @@ -23,8 +22,6 @@ import { } from "@ant-design/icons"; import { useEnvironmentContext } from "./context/EnvironmentContext"; import { useWorkspace } from "./hooks/useWorkspace"; -import AppsTab from "./components/AppsTab"; -import DataSourcesTab from "./components/DataSourcesTab"; import DeployableItemsTab from "./components/DeployableItemsTab"; import { appsConfig } from "./config/apps.config"; import { dataSourcesConfig } from "./config/data-sources.config"; diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/DataSourcesList.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/DataSourcesList.tsx index 9fee65c4a5..e84f072934 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/DataSourcesList.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/DataSourcesList.tsx @@ -7,7 +7,6 @@ import { CloudUploadOutlined } from '@ant-design/icons'; import { DataSource } from '../types/datasource.types'; import { Environment } from '../types/environment.types'; import { ColumnsType } from 'antd/lib/table'; -import DeployDataSourceModal from './DeployDataSourceModal'; interface DataSourcesListProps { dataSources: DataSource[]; @@ -122,13 +121,13 @@ const DataSourcesList: React.FC = ({ }} /> - setDeployModalVisible(false)} onSuccess={onRefresh} - /> + /> */} ); }; diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/DataSourcesTab.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/DataSourcesTab.tsx index e7d4d39c49..7a5562578d 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/DataSourcesTab.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/DataSourcesTab.tsx @@ -7,7 +7,6 @@ import { DatabaseOutlined, SyncOutlined } from '@ant-design/icons'; import Title from 'antd/lib/typography/Title'; import { Environment } from '../types/environment.types'; import { useWorkspaceDataSources } from '../hooks/useWorkspaceDataSources'; -import DataSourcesList from './DataSourcesList'; import { DataSource } from '../types/datasource.types'; interface DataSourcesTabProps { @@ -105,13 +104,13 @@ const DataSourcesTab: React.FC = ({ environment, workspaceI )} {/* Data Sources List */} - + /> */} ); }; diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/DeployDataSourceModal.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/DeployDataSourceModal.tsx deleted file mode 100644 index 29a41c0954..0000000000 --- a/client/packages/lowcoder/src/pages/setting/environments/components/DeployDataSourceModal.tsx +++ /dev/null @@ -1,126 +0,0 @@ -// components/DeployDataSourceModal.tsx -// Create this new file - -import React, { useState, useEffect } from 'react'; -import { Modal, Form, Select, Checkbox, Button, message, Spin } from 'antd'; -import { Environment } from '../types/environment.types'; -import { DataSource } from '../types/datasource.types'; -import { deployDataSource } from '../services/datasources.service'; -import { useEnvironmentContext } from '../context/EnvironmentContext'; - -interface DeployDataSourceModalProps { - visible: boolean; - dataSource: DataSource | null; - currentEnvironment: Environment; - onClose: () => void; - onSuccess?: () => void; -} - -const DeployDataSourceModal: React.FC = ({ - visible, - dataSource, - currentEnvironment, - onClose, - onSuccess, -}) => { - const [form] = Form.useForm(); - const { environments, isLoadingEnvironments } = useEnvironmentContext(); - const [deploying, setDeploying] = useState(false); - - // Reset form when modal becomes visible - useEffect(() => { - if (visible) { - form.resetFields(); - } - }, [visible, form]); - - // Filter out current environment from the list - const targetEnvironments = environments.filter( - (env) => env.environmentId !== currentEnvironment.environmentId - ); - - const handleDeploy = async () => { - try { - const values = await form.validateFields(); - - if (!dataSource) return; - - setDeploying(true); - - await deployDataSource( - { - envId: currentEnvironment.environmentId, - targetEnvId: values.targetEnvId, - datasourceId: dataSource.gid, - updateDependenciesIfNeeded: values.updateDependenciesIfNeeded - }, - currentEnvironment.environmentApiServiceUrl! - ); - - message.success(`Successfully deployed ${dataSource.name} to target environment`); - if (onSuccess) onSuccess(); - onClose(); - } catch (error) { - console.error('Deployment error:', error); - message.error('Failed to deploy data source'); - } finally { - setDeploying(false); - } - }; - - return ( - - {isLoadingEnvironments ? ( -
- -
- ) : ( -
- - - - - - Update Dependencies If Needed - - - - - - - - )} -
- ); -}; - -export default DeployDataSourceModal; \ No newline at end of file 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 dd9b7f8ea3..f0c45c73c3 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 @@ -5,7 +5,7 @@ import { DatabaseOutlined, CloudUploadOutlined } from '@ant-design/icons'; import { DeployableItemConfig } from '../types/deployable-item.types'; import { DataSource, DataSourceStats } from '../types/datasource.types'; import { Environment } from '../types/environment.types'; -import { getMergedWorkspaceDataSources } from '../services/datasources.service'; +import { getMergedWorkspaceDataSources, deployDataSource } from '../services/datasources.service'; import { connectManagedDataSource, unconnectManagedDataSource } from '../services/enterprise.service'; @@ -144,5 +144,25 @@ export const dataSourcesConfig: DeployableItemConfig { + return { + envId: sourceEnv.environmentId, + targetEnvId: targetEnv.environmentId, + datasourceId: item.id, + updateDependenciesIfNeeded: values.updateDependenciesIfNeeded + }; + }, + execute: (params: any) => deployDataSource(params) } }; \ No newline at end of file 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 1454a2cf41..b1fe06745f 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 @@ -16,6 +16,12 @@ export interface MergedDataSourcesResult { stats: DataSourceStats; } +export interface DeployDataSourceParams { + envId: string; + targetEnvId: string; + datasourceId: string; + updateDependenciesIfNeeded?: boolean; +} // Get data sources for a workspace - using your correct implementation export async function getWorkspaceDataSources( workspaceId: string, @@ -144,34 +150,12 @@ export async function getMergedWorkspaceDataSources( } // Function to deploy a data source to another environment -export interface DeployDataSourceParams { - envId: string; - targetEnvId: string; - datasourceId: string; - updateDependenciesIfNeeded?: boolean; -} - -export const deployDataSource = async ( - params: DeployDataSourceParams, - apiServiceUrl: string -): Promise => { +export async function deployDataSource(params: DeployDataSourceParams): Promise { try { - const response = await axios.post( - `${apiServiceUrl}/api/plugins/enterprise/deploy-datasource`, - null, - { - params: { - envId: params.envId, - targetEnvId: params.targetEnvId, - datasourceId: params.datasourceId, - updateDependenciesIfNeeded: params.updateDependenciesIfNeeded ?? false - } - } - ); - + const response = await axios.post('/api/plugins/enterprise/datasource/deploy', params); return response.status === 200; } catch (error) { console.error('Error deploying data source:', error); throw error; } -}; \ No newline at end of file +} \ No newline at end of file From 17bb62af8a344bb7cb0cad5fc289d05eb43a9491 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 16 Apr 2025 00:25:45 +0500 Subject: [PATCH 44/68] Add deployment config for Query Library --- .../environments/config/query.config.tsx | 23 ++++++++++++++++++- .../environments/services/query.service.ts | 19 +++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) 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 1efce62671..8c274baca7 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 @@ -5,7 +5,8 @@ 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 } from '../services/query.service'; +import { getMergedWorkspaceQueries, deployQuery } from '../services/query.service'; +import { Environment } from '../types/environment.types'; // Define QueryStats interface export interface QueryStats { @@ -117,5 +118,25 @@ export const queryConfig: DeployableItemConfig = { console.error('Error toggling managed status:', error); return false; } + }, + deploy: { + enabled: true, + fields: [ + { + name: 'updateDependenciesIfNeeded', + label: 'Update Dependencies If Needed', + type: 'checkbox', + defaultValue: false + } + ], + prepareParams: (item: Query, values: any, sourceEnv: Environment, targetEnv: Environment) => { + return { + envId: sourceEnv.environmentId, + targetEnvId: targetEnv.environmentId, + queryId: item.id, + updateDependenciesIfNeeded: values.updateDependenciesIfNeeded + }; + }, + execute: (params: any) => deployQuery(params) } }; \ No newline at end of file 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 90889f1913..39eda02355 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 @@ -1,6 +1,7 @@ /** * Get merged queries (both regular and managed) for a workspace */ +import axios from 'axios'; import { getManagedQueries } from './enterprise.service'; import { getWorkspaceQueries } from './environments.service'; import { Query } from '../types/query.types'; @@ -12,6 +13,14 @@ export interface MergedQueriesResult { unmanaged: number; }; } + + export interface DeployQueryParams { + envId: string; + targetEnvId: string; + queryId: string; + updateDependenciesIfNeeded?: boolean; + } + export async function getMergedWorkspaceQueries( workspaceId: string, @@ -65,4 +74,14 @@ export interface MergedQueriesResult { console.error("Error in getMergedWorkspaceQueries:", error); throw error; } + } + + export async function deployQuery(params: DeployQueryParams): Promise { + try { + const response = await axios.post('/api/plugins/enterprise/qlQuery/deploy', params); + return response.status === 200; + } catch (error) { + console.error('Error deploying query:', error); + throw error; + } } \ No newline at end of file From 5f393b09c38860a04a963d5f9b1c006d0470adaa Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 16 Apr 2025 15:15:10 +0500 Subject: [PATCH 45/68] wrap the provider --- .../environments/EnvironmentDetail.tsx | 43 +++---------- .../setting/environments/Environments.tsx | 29 +++++---- .../setting/environments/EnvironmentsList.tsx | 56 +++++------------ .../components/EnvironmentScopedRoutes.tsx | 28 ++++++--- .../context/EnvironmentContext.tsx | 61 ++++++++++++------- 5 files changed, 98 insertions(+), 119 deletions(-) diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx index 00ce1636aa..e625f75c3e 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx @@ -19,8 +19,6 @@ import { } from "@ant-design/icons"; import { useEnvironmentContext } from "./context/EnvironmentContext"; -import WorkspacesTab from "./components/WorkspacesTab"; -import UserGroupsTab from "./components/UserGroupsTab"; import { workspaceConfig } from "./config/workspace.config"; import { userGroupsConfig } from "./config/usergroups.config"; import DeployableItemsTab from "./components/DeployableItemsTab"; @@ -38,55 +36,30 @@ const EnvironmentDetail: React.FC = () => { // Get environment ID from URL params const { environment, - isLoadingEnvironment: envLoading, - error: envError, + isLoadingEnvironment, + error } = useEnvironmentContext(); - // If loading, show spinner - if (envLoading) { + if (isLoadingEnvironment) { return ( -
- +
+
); } - // If error, show error message - if (envError) { + if (error || !environment) { return ( - ); - } - - // If no environment data, show message - if (!environment) { - return ( - ); } - return (
{/* Header with environment name and controls */} diff --git a/client/packages/lowcoder/src/pages/setting/environments/Environments.tsx b/client/packages/lowcoder/src/pages/setting/environments/Environments.tsx index 7d5eaa5218..a1fb13e020 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/Environments.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/Environments.tsx @@ -1,5 +1,6 @@ import React from "react"; import { Switch, Route } from "react-router-dom"; +import { EnvironmentProvider } from "./context/EnvironmentContext"; import EnvironmentsList from "./EnvironmentsList"; import EnvironmentScopedRoutes from "./components/EnvironmentScopedRoutes"; @@ -8,20 +9,26 @@ import { ENVIRONMENT_DETAIL } from "@lowcoder-ee/constants/routesURL"; +/** + * Top-level Environments component that wraps all environment-related routes + * with the EnvironmentProvider for shared state management + */ const Environments: React.FC = () => { return ( - - {/* Route that shows the list of environments */} - - - + + + {/* Route that shows the list of environments */} + + + - {/* All other routes under /environments/:envId */} - - - - + {/* All other routes under /environments/:envId */} + + + + + ); }; -export default Environments; +export default Environments; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx index b0a6c7274b..40ee18e07d 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx @@ -1,8 +1,8 @@ import React, { useState } from "react"; -import { Table, Typography, Alert, Input, Button, Space, Empty } from "antd"; +import { Typography, Alert, Input, Button, Space, Empty } from "antd"; import { SearchOutlined, ReloadOutlined } from "@ant-design/icons"; import { useHistory } from "react-router-dom"; -import { useEnvironments } from "./hooks/useEnvironments"; +import { useEnvironmentContext } from "./context/EnvironmentContext"; import { Environment } from "./types/environment.types"; import EnvironmentsTable from "./components/EnvironmentsTable"; import { buildEnvironmentId } from "@lowcoder-ee/constants/routesURL"; @@ -11,16 +11,16 @@ const { Title } = Typography; /** * Environment Listing Page Component - * Displays a basic table of environments + * Displays a table of environments */ const EnvironmentsList: React.FC = () => { - // Use our custom hook to get environments data and states - const { environments, loading, error, refresh } = useEnvironments(); + // Use the shared context instead of a local hook + const { environments, isLoadingEnvironments, error, refreshEnvironments } = useEnvironmentContext(); // State for search input const [searchText, setSearchText] = useState(""); - // Hook for navigation (using history instead of navigate) + // Hook for navigation const history = useHistory(); // Filter environments based on search text @@ -34,38 +34,6 @@ const EnvironmentsList: React.FC = () => { ); }); - // Define table columns - updated to match the actual data structure - 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", - }, - { - title: "Master", - dataIndex: "isMaster", - key: "isMaster", - render: (isMaster: boolean) => (isMaster ? "Yes" : "No"), - }, - ]; - // Handle row click to navigate to environment detail const handleRowClick = (record: Environment) => { history.push(buildEnvironmentId(record.environmentId)); @@ -93,7 +61,11 @@ const EnvironmentsList: React.FC = () => { prefix={} allowClear /> - @@ -111,7 +83,7 @@ const EnvironmentsList: React.FC = () => { )} {/* Empty state handling */} - {!loading && environments.length === 0 && !error ? ( + {!isLoadingEnvironments && environments.length === 0 && !error ? ( { /* Table component */ )} @@ -128,4 +100,4 @@ const EnvironmentsList: React.FC = () => { ); }; -export default EnvironmentsList; +export default EnvironmentsList; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentScopedRoutes.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentScopedRoutes.tsx index 4c75ccf688..e8a04d103b 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentScopedRoutes.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentScopedRoutes.tsx @@ -1,7 +1,6 @@ -import React from "react"; +import React, { useEffect } from "react"; import { Switch, Route, useParams } from "react-router-dom"; -import { EnvironmentProvider } from "../context/EnvironmentContext"; - +import { useEnvironmentContext } from "../context/EnvironmentContext"; import EnvironmentDetail from "../EnvironmentDetail"; import WorkspaceDetail from "../WorkspaceDetail"; import { DeployModalProvider } from "../context/DeployModalContext"; @@ -11,13 +10,23 @@ import { 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 { environmentId } = useParams<{ environmentId: string }>(); + const { refreshEnvironment } = useEnvironmentContext(); + + // When the environmentId changes, fetch the specific environment + useEffect(() => { + if (environmentId) { + refreshEnvironment(environmentId); + } + }, [environmentId, refreshEnvironment]); return ( - - - + @@ -27,9 +36,8 @@ const EnvironmentScopedRoutes: React.FC = () => { - - + ); }; -export default EnvironmentScopedRoutes; +export default EnvironmentScopedRoutes; \ 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 afd5c50da9..dc5245b904 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/context/EnvironmentContext.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/context/EnvironmentContext.tsx @@ -15,16 +15,23 @@ import { 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; } -const EnvironmentContext = createContext( - undefined -); +const EnvironmentContext = createContext(undefined); export const useEnvironmentContext = () => { const context = useContext(EnvironmentContext); @@ -37,65 +44,77 @@ export const useEnvironmentContext = () => { }; interface ProviderProps { - envId: string; children: ReactNode; } export const EnvironmentProvider: React.FC = ({ - envId, children, }) => { + // State for environment data const [environment, setEnvironment] = useState(null); const [environments, setEnvironments] = useState([]); - // Separate loading states - const [isLoadingEnvironment, setIsLoadingEnvironment] = - useState(true); - const [isLoadingEnvironments, setIsLoadingEnvironments] = - useState(true); + // Loading states + const [isLoadingEnvironment, setIsLoadingEnvironment] = useState(false); + const [isLoadingEnvironments, setIsLoadingEnvironments] = useState(true); + // Error state const [error, setError] = useState(null); - const history = useHistory(); - const fetchEnvironment = useCallback(async () => { + // 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(envId); + const data = await getEnvironmentById(environmentId); console.log("Environment data:", data); setEnvironment(data); } catch (err) { - setError("Environment not found or failed to load"); - history.push("/404"); // or a centralized error route + const errorMessage = err instanceof Error ? err.message : "Environment not found or failed to load"; + setError(errorMessage); } finally { setIsLoadingEnvironment(false); } - }, [envId, history]); + }, []); + // 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) { - setError("Failed to load environments list"); + const errorMessage = err instanceof Error ? err.message : "Failed to load environments list"; + setError(errorMessage); } finally { setIsLoadingEnvironments(false); } }, []); + // Initial data loading - just fetch environments list useEffect(() => { - fetchEnvironment(); fetchEnvironments(); - }, [fetchEnvironment, fetchEnvironments]); - + }, [fetchEnvironments]); + // Create the context value const value: EnvironmentContextState = { environment, environments, isLoadingEnvironment, isLoadingEnvironments, error, + refreshEnvironment: fetchEnvironment, + refreshEnvironments: fetchEnvironments, }; return ( @@ -103,4 +122,4 @@ export const EnvironmentProvider: React.FC = ({ {children} ); -}; +}; \ No newline at end of file From d5035d6f0d5c4c6065d6ca0b3bed69065928fccb Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 16 Apr 2025 16:15:14 +0500 Subject: [PATCH 46/68] Test update environment --- .../setting/environments/EnvironmentsList.tsx | 52 +++++- .../components/EditEnvironmentModal.tsx | 159 ++++++++++++++++++ .../components/EnvironmentsTable.tsx | 35 +++- .../context/EnvironmentContext.tsx | 35 +++- .../services/environments.service.ts | 38 +++++ 5 files changed, 312 insertions(+), 7 deletions(-) create mode 100644 client/packages/lowcoder/src/pages/setting/environments/components/EditEnvironmentModal.tsx diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx index 40ee18e07d..cea220c127 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx @@ -6,6 +6,7 @@ import { useEnvironmentContext } from "./context/EnvironmentContext"; import { Environment } from "./types/environment.types"; import EnvironmentsTable from "./components/EnvironmentsTable"; import { buildEnvironmentId } from "@lowcoder-ee/constants/routesURL"; +import EditEnvironmentModal from "./components/EditEnvironmentModal"; const { Title } = Typography; @@ -15,7 +16,19 @@ const { Title } = Typography; */ const EnvironmentsList: React.FC = () => { // Use the shared context instead of a local hook - const { environments, isLoadingEnvironments, error, refreshEnvironments } = useEnvironmentContext(); + const { + environments, + isLoadingEnvironments, + error, + refreshEnvironments, + updateEnvironmentData + } = useEnvironmentContext(); + + // State for edit modal + const [isEditModalVisible, setIsEditModalVisible] = useState(false); + const [selectedEnvironment, setSelectedEnvironment] = useState(null); + const [isUpdating, setIsUpdating] = useState(false); + // State for search input const [searchText, setSearchText] = useState(""); @@ -39,6 +52,33 @@ const EnvironmentsList: React.FC = () => { history.push(buildEnvironmentId(record.environmentId)); }; + + // Handle edit button click + const handleEditClick = (environment: Environment) => { + setSelectedEnvironment(environment); + setIsEditModalVisible(true); + }; + + // Handle modal close + const handleCloseModal = () => { + setIsEditModalVisible(false); + setSelectedEnvironment(null); + }; + + // Handle save environment + const handleSaveEnvironment = async (environmentId: string, data: Partial) => { + setIsUpdating(true); + try { + // Use the context function to update the environment + // This will automatically update both the environments list and the detail view + await updateEnvironmentData(environmentId, data); + } catch (error) { + console.error('Failed to update environment:', error); + } finally { + setIsUpdating(false); + } + }; + return (
{/* Header section with title and controls */} @@ -94,8 +134,18 @@ const EnvironmentsList: React.FC = () => { environments={filteredEnvironments} loading={isLoadingEnvironments} onRowClick={handleRowClick} + onEditClick={handleEditClick} /> )} + + {/* Edit Environment Modal */} +
); }; diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/EditEnvironmentModal.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/EditEnvironmentModal.tsx new file mode 100644 index 0000000000..5c09cc42b5 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/components/EditEnvironmentModal.tsx @@ -0,0 +1,159 @@ +import React, { useState, useEffect } from 'react'; +import { Modal, Form, Input, Select, Switch, Button, message } from 'antd'; +import { Environment } from '../types/environment.types'; + +const { Option } = Select; + +interface EditEnvironmentModalProps { + visible: boolean; + environment: Environment | null; + onClose: () => void; + onSave: (environmentId: string, data: Partial) => Promise; + loading?: boolean; +} + +const EditEnvironmentModal: React.FC = ({ + visible, + environment, + onClose, + onSave, + loading = false +}) => { + const [form] = Form.useForm(); + const [submitLoading, setSubmitLoading] = useState(false); + + // Initialize form with environment data when it changes + useEffect(() => { + if (environment) { + form.setFieldsValue({ + environmentName: environment.environmentName || '', + environmentDescription: environment.environmentDescription || '', + environmentType: environment.environmentType, + environmentApiServiceUrl: environment.environmentApiServiceUrl || '', + environmentFrontendUrl: environment.environmentFrontendUrl || '', + environmentNodeServiceUrl: environment.environmentNodeServiceUrl || '', + environmentApikey: environment.environmentApikey || '', + isMaster: environment.isMaster + }); + } + }, [environment, form]); + + const handleSubmit = async () => { + if (!environment) return; + + try { + const values = await form.validateFields(); + setSubmitLoading(true); + + await onSave(environment.environmentId, values); + onClose(); + } catch (error) { + if (error instanceof Error) { + console.error("Form validation or submission error:", error); + } + } finally { + setSubmitLoading(false); + } + }; + + return ( + + Cancel + , + + ]} + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ); +}; + +export default EditEnvironmentModal; \ 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 1e73260579..9aec452ed1 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentsTable.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentsTable.tsx @@ -1,11 +1,16 @@ import React from 'react'; -import { Table, Tag } from 'antd'; +import { Table, Tag, Button, Tooltip } from 'antd'; +import { EditOutlined } from '@ant-design/icons'; import { Environment } from '../types/environment.types'; + + interface EnvironmentsTableProps { environments: Environment[]; loading: boolean; onRowClick: (record: Environment) => void; + onEditClick: (record: Environment) => void; + } /** @@ -14,13 +19,17 @@ interface EnvironmentsTableProps { const EnvironmentsTable: React.FC = ({ environments, loading, - onRowClick + onRowClick, + onEditClick, }) => { // 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'; } @@ -50,8 +59,8 @@ const EnvironmentsTable: React.FC = ({ dataIndex: 'environmentType', key: 'environmentType', render: (type: string) => ( - - {type.toUpperCase()} + + {type ? type.toUpperCase() : 'UNKNOWN'} ), }, @@ -65,6 +74,24 @@ const EnvironmentsTable: React.FC = ({ ), }, + { + title: 'Actions', + key: 'actions', + width: 100, + render: (_: any, record: Environment) => ( + +
{/* Basic Environment Information Card */} @@ -156,6 +207,17 @@ const EnvironmentDetail: React.FC = () => { + {/* Edit Environment Modal */} + {environment && ( + + )} +
); }; From 221be50742abc6646047f742c548708bd73dad69 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 16 Apr 2025 17:14:23 +0500 Subject: [PATCH 48/68] remove edit from environments table --- .../setting/environments/EnvironmentsList.tsx | 52 +------------------ .../components/EnvironmentsTable.tsx | 20 ------- 2 files changed, 1 insertion(+), 71 deletions(-) diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx index cea220c127..d34e0bd524 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx @@ -20,15 +20,9 @@ const EnvironmentsList: React.FC = () => { environments, isLoadingEnvironments, error, - refreshEnvironments, - updateEnvironmentData } = useEnvironmentContext(); - // State for edit modal - const [isEditModalVisible, setIsEditModalVisible] = useState(false); - const [selectedEnvironment, setSelectedEnvironment] = useState(null); - const [isUpdating, setIsUpdating] = useState(false); - + console.log("Environments:", environments); // State for search input const [searchText, setSearchText] = useState(""); @@ -52,33 +46,6 @@ const EnvironmentsList: React.FC = () => { history.push(buildEnvironmentId(record.environmentId)); }; - - // Handle edit button click - const handleEditClick = (environment: Environment) => { - setSelectedEnvironment(environment); - setIsEditModalVisible(true); - }; - - // Handle modal close - const handleCloseModal = () => { - setIsEditModalVisible(false); - setSelectedEnvironment(null); - }; - - // Handle save environment - const handleSaveEnvironment = async (environmentId: string, data: Partial) => { - setIsUpdating(true); - try { - // Use the context function to update the environment - // This will automatically update both the environments list and the detail view - await updateEnvironmentData(environmentId, data); - } catch (error) { - console.error('Failed to update environment:', error); - } finally { - setIsUpdating(false); - } - }; - return (
{/* Header section with title and controls */} @@ -101,13 +68,6 @@ const EnvironmentsList: React.FC = () => { prefix={} allowClear /> -
@@ -134,18 +94,8 @@ const EnvironmentsList: React.FC = () => { environments={filteredEnvironments} loading={isLoadingEnvironments} onRowClick={handleRowClick} - onEditClick={handleEditClick} /> )} - - {/* Edit Environment Modal */} -
); }; 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 9aec452ed1..5e9da24f6b 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentsTable.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentsTable.tsx @@ -9,7 +9,6 @@ interface EnvironmentsTableProps { environments: Environment[]; loading: boolean; onRowClick: (record: Environment) => void; - onEditClick: (record: Environment) => void; } @@ -20,7 +19,6 @@ const EnvironmentsTable: React.FC = ({ environments, loading, onRowClick, - onEditClick, }) => { // Get color for environment type/stage const getTypeColor = (type: string): string => { @@ -74,24 +72,6 @@ const EnvironmentsTable: React.FC = ({ ), }, - { - title: 'Actions', - key: 'actions', - width: 100, - render: (_: any, record: Environment) => ( - - + + + +
- - - ), - } ], // Deployment options From 04c1a76d3926f4da74ad9dc4bf234883935dca0c Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 16 Apr 2025 22:00:00 +0500 Subject: [PATCH 53/68] add audit link in environments table --- .../components/EnvironmentsTable.tsx | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentsTable.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentsTable.tsx index 5e9da24f6b..0208932d74 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentsTable.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentsTable.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { Table, Tag, Button, Tooltip } from 'antd'; -import { EditOutlined } from '@ant-design/icons'; +import { Table, Tag, Button, Tooltip, Space } from 'antd'; +import { EditOutlined, AuditOutlined} from '@ant-design/icons'; import { Environment } from '../types/environment.types'; @@ -33,6 +33,14 @@ const EnvironmentsTable: React.FC = ({ } }; + // 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'); + }; + + // Define table columns const columns = [ { @@ -72,6 +80,23 @@ const EnvironmentsTable: React.FC = ({ ), }, + { + title: 'Actions', + key: 'actions', + render: (_: any, record: Environment) => ( + e.stopPropagation()}> + + + + + ), + }, ]; return ( From 3ad52c0cea967a51d88bee425787be92297f1a8b Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 16 Apr 2025 22:50:48 +0500 Subject: [PATCH 54/68] add audit logs --- .../components/DeployableItemsList.tsx | 28 +++++++++++++++++++ .../environments/config/apps.config.tsx | 13 +++++++-- .../environments/config/workspace.config.tsx | 11 +++++++- .../types/deployable-item.types.ts | 11 ++++++++ 4 files changed, 60 insertions(+), 3 deletions(-) diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsList.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsList.tsx index b207d98d58..258739e2dc 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsList.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsList.tsx @@ -31,6 +31,15 @@ function DeployableItemsList({ const { openDeployModal } = useDeployModal(); + // Open audit page + const openAuditPage = (item: T, e: React.MouseEvent) => { + e.stopPropagation(); + if (config.audit?.getAuditUrl) { + const auditUrl = config.audit.getAuditUrl(item, environment, additionalParams); + window.open(auditUrl, '_blank'); + } + }; + // Handle row click for navigation // Handle row click for navigation @@ -103,6 +112,25 @@ const handleRowClick = (item: T) => { }); } + const hasAudit = config.audit?.enabled; + +// Add audit column if enabled - SEPARATE CONDITION + if (config.audit?.enabled) { + columns.push({ + title: 'Audit', + key: 'audit', + render: (_, record: T) => ( + + + + ), + }); + } if (loading) { return ( 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 cc1d285d45..8cea1a0e1a 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,7 +1,7 @@ // config/apps.config.tsx import React from 'react'; import { Row, Col, Statistic, Tag, Space, Button, Tooltip } from 'antd'; -import { AppstoreOutlined, CloudUploadOutlined } from '@ant-design/icons'; +import { AppstoreOutlined, AuditOutlined } from '@ant-design/icons'; import {DeployableItemConfig } from '../types/deployable-item.types'; import { Environment } from '../types/environment.types'; import { getMergedWorkspaceApps, deployApp } from '../services/apps.service'; @@ -105,7 +105,16 @@ export const appsConfig: DeployableItemConfig = { 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) { 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 21eee9f870..189ba44f1f 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,7 +1,7 @@ // config/workspace.config.tsx import React from 'react'; import { Row, Col, Statistic, Tag } from 'antd'; -import { ClusterOutlined } from '@ant-design/icons'; +import { ClusterOutlined, AuditOutlined } from '@ant-design/icons'; import { Workspace, WorkspaceStats, DeployableItemConfig } from '../types/deployable-item.types'; import { buildEnvironmentWorkspaceId } from '@lowcoder-ee/constants/routesURL'; import { getMergedEnvironmentWorkspaces } from '../services/workspace.service'; @@ -99,6 +99,15 @@ export const workspaceConfig: DeployableItemConfig = ); return result.workspaces; }, + + 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` + }, toggleManaged: async ({ item, checked, environment }) => { try { 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 d0c7139c28..a670c1837a 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 @@ -3,6 +3,13 @@ import { ReactNode } from 'react'; import { Environment } from './environment.types'; // 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; @@ -61,6 +68,10 @@ export interface DeployableItemConfig ReactNode; calculateStats: (items: T[]) => S; + + // Add audit configuration + audit?: AuditConfig; + // Table configuration columns: Array<{ From 29c2ae2abfe3091c835d555a58f5a2627dbe3006 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 17 Apr 2025 00:13:16 +0500 Subject: [PATCH 55/68] Fixed workspace detail page header --- .../setting/environments/WorkspaceDetail.tsx | 139 ++++++++++++------ 1 file changed, 90 insertions(+), 49 deletions(-) diff --git a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx index bccb866ccb..fedcf25081 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx @@ -109,68 +109,101 @@ const WorkspaceDetail: React.FC = () => { } return ( -
+
{/* Breadcrumb navigation */} - - history.push('/home/settings/environments')}> + + history.push("/home/settings/environments")} + > Environments - history.push(`/home/settings/environments/${environmentId}`)}> + + history.push(`/home/settings/environments/${environmentId}`) + } + > {environment.environmentName} - - {workspace.name} - + {workspace.name} - - {/* Header with workspace name and controls */} -
-
+ {/* Workspace header with details and actions */} + +
+ {/* Left section - Workspace info */}
- {workspace.name} - - {workspace.managed ? 'Managed' : 'Unmanaged'} - + + {workspace.name} + +
+ + ID: {workspace.id} + + + {workspace.managed ? "Managed" : "Unmanaged"} + +
- - - + + {/* Right section - Actions */} + +
+ Managed: + +
+
+
- -
- - {workspace.name} - ID: {workspace.id} -
-
- {/* Tabs for Apps, Data Sources, and Queries */} - Apps} + + Apps + + } key="apps" > { config={appsConfig} additionalParams={{ workspaceId }} title="Apps in this Workspace" - /> + /> - - {/* Update the TabPane in WorkspaceDetail.tsx */} - Data Sources} + + {/* Update the TabPane in WorkspaceDetail.tsx */} + + Data Sources + + } key="dataSources" > { title="Data Sources in this Workspace" /> - - Queries} + + + Queries + + } key="queries" > Date: Thu, 17 Apr 2025 01:35:33 +0500 Subject: [PATCH 56/68] move all columns to config --- .../components/DeployableItemsList.tsx | 174 +++++++++--------- .../environments/config/workspace.config.tsx | 54 ++++-- .../types/deployable-item.types.ts | 25 ++- .../environments/utils/columnFactories.tsx | 123 +++++++++++++ 4 files changed, 273 insertions(+), 103 deletions(-) create mode 100644 client/packages/lowcoder/src/pages/setting/environments/utils/columnFactories.tsx diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsList.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsList.tsx index 258739e2dc..001568b467 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsList.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsList.tsx @@ -40,83 +40,91 @@ function DeployableItemsList({ } }; - // Handle row click for navigation - // 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); -}; - - // Generate columns based on config - let columns = [...config.columns]; - - // Add managed column if enabled - if (config.enableManaged) { - columns.push({ - title: 'Managed', - key: 'managed', - render: (_, record: T) => ( - - - {record.managed ? 'Managed' : 'Unmanaged'} - - {onToggleManaged && ( - { - e.stopPropagation(); // Stop row click event - onToggleManaged(record, checked); - }} - onChange={() => {}} - /> - )} - - ), + 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); + }; - // Add deploy action column if enabled - if (config.deploy?.enabled) { - columns.push({ - title: 'Actions', - key: 'actions', - render: (_, record: T) => ( - - - - - - ), - }); - } + // Determine columns - Use new getColumns method if available, fall back to old approach + const columns = config.getColumns ? + config.getColumns({ + environment, + refreshing, + onToggleManaged, + openDeployModal, + additionalParams + }) : + generateLegacyColumns(); + + // Legacy column generation for backward compatibility + function generateLegacyColumns() { + let legacyColumns = [...config.columns]; + + // Add managed column if enabled + if (config.enableManaged) { + legacyColumns.push({ + title: 'Managed', + key: 'managed', + render: (_, record: T) => ( + + + {record.managed ? 'Managed' : 'Unmanaged'} + + {onToggleManaged && ( + { + e.stopPropagation(); // Stop row click event + onToggleManaged(record, checked); + }} + onChange={() => {}} + /> + )} + + ), + }); + } - const hasAudit = config.audit?.enabled; + // Add deploy action column if enabled + if (config.deploy?.enabled) { + legacyColumns.push({ + title: 'Actions', + key: 'actions', + render: (_, record: T) => ( + + + + + + ), + }); + } -// Add audit column if enabled - SEPARATE CONDITION + // Add audit column if enabled if (config.audit?.enabled) { - columns.push({ + legacyColumns.push({ title: 'Audit', key: 'audit', render: (_, record: T) => ( @@ -131,6 +139,9 @@ const handleRowClick = (item: T) => { ), }); } + + return legacyColumns; + } if (loading) { return ( @@ -151,19 +162,18 @@ const handleRowClick = (item: T) => { const hasNavigation = config.buildDetailRoute({}) !== '#'; - return (
({ - onClick: hasNavigation ? () => handleRowClick(record) : undefined, - style: hasNavigation ? { cursor: 'pointer' } : undefined, - })} - /> + columns={columns} + dataSource={items} + rowKey={config.idField} + pagination={{ pageSize: 10 }} + size="middle" + onRow={(record) => ({ + onClick: hasNavigation ? () => handleRowClick(record) : undefined, + style: hasNavigation ? { cursor: 'pointer' } : undefined, + })} + /> ); } 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 189ba44f1f..c1e9c8e6cd 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 @@ -6,6 +6,15 @@ import { Workspace, WorkspaceStats, DeployableItemConfig } from '../types/deploy import { buildEnvironmentWorkspaceId } from '@lowcoder-ee/constants/routesURL'; import { getMergedEnvironmentWorkspaces } from '../services/workspace.service'; import { connectManagedWorkspace, unconnectManagedWorkspace } from '../services/enterprise.service'; +import { + createNameColumn, + createIdColumn, + createRoleColumn, + createDateColumn, + createStatusColumn, + createManagedColumn, + createAuditColumn +} from '../utils/columnFactories'; export const workspaceConfig: DeployableItemConfig = { // Basic info @@ -47,7 +56,7 @@ export const workspaceConfig: DeployableItemConfig = }; }, - // Table configuration + // Original columns for backward compatibility columns: [ { title: 'Name', @@ -87,10 +96,29 @@ export const workspaceConfig: DeployableItemConfig = } ], - // Deployment options + // New getColumns method + getColumns: ({ environment, refreshing, onToggleManaged, additionalParams }) => { + const columns = [ + createNameColumn(), + createIdColumn(), + createRoleColumn(), + 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, - // Service functions + // Fetch function fetchItems: async ({ environment }) => { const result = await getMergedEnvironmentWorkspaces( environment.environmentId, @@ -99,16 +127,8 @@ export const workspaceConfig: DeployableItemConfig = ); return result.workspaces; }, - - 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` - }, + // Toggle managed status toggleManaged: async ({ item, checked, environment }) => { try { if (checked) { @@ -121,5 +141,15 @@ export const workspaceConfig: DeployableItemConfig = 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` } }; \ 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 a670c1837a..941fced56e 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,6 +1,8 @@ // 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 { @@ -29,12 +31,13 @@ export interface Workspace extends DeployableItem { } // 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 {} @@ -69,18 +72,22 @@ export interface DeployableItemConfig 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; - // Table configuration - columns: Array<{ - title: string; - dataIndex?: string; - key: string; - render?: (value: any, record: T) => ReactNode; - ellipsis?: boolean; - }>; // Deployable configuration enableManaged: boolean; diff --git a/client/packages/lowcoder/src/pages/setting/environments/utils/columnFactories.tsx b/client/packages/lowcoder/src/pages/setting/environments/utils/columnFactories.tsx new file mode 100644 index 0000000000..976c352da7 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/utils/columnFactories.tsx @@ -0,0 +1,123 @@ +// utils/columnFactories.tsx +import React from 'react'; +import { Tag, Space, Switch, Button, Tooltip } 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 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 ( + + + + ); + }, + }; +} \ No newline at end of file From 860f5bfbeef48fc48fa4d3d60a78a7277d2304d9 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 17 Apr 2025 01:43:28 +0500 Subject: [PATCH 57/68] remove legacy columns from Deployitemslist --- .../components/DeployableItemsList.tsx | 91 +------------------ .../types/deployable-item.types.ts | 2 +- 2 files changed, 3 insertions(+), 90 deletions(-) diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsList.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsList.tsx index 001568b467..0009e549bb 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsList.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsList.tsx @@ -31,15 +31,6 @@ function DeployableItemsList({ const { openDeployModal } = useDeployModal(); - // Open audit page - const openAuditPage = (item: T, e: React.MouseEvent) => { - e.stopPropagation(); - if (config.audit?.getAuditUrl) { - const auditUrl = config.audit.getAuditUrl(item, environment, additionalParams); - window.open(auditUrl, '_blank'); - } - }; - // Handle row click for navigation const handleRowClick = (item: T) => { // Skip navigation if the route is just '#' (for non-navigable items) @@ -56,92 +47,14 @@ function DeployableItemsList({ }; // Determine columns - Use new getColumns method if available, fall back to old approach - const columns = config.getColumns ? - config.getColumns({ + const columns = config.getColumns({ environment, refreshing, onToggleManaged, openDeployModal, additionalParams - }) : - generateLegacyColumns(); + }) - // Legacy column generation for backward compatibility - function generateLegacyColumns() { - let legacyColumns = [...config.columns]; - - // Add managed column if enabled - if (config.enableManaged) { - legacyColumns.push({ - title: 'Managed', - key: 'managed', - render: (_, record: T) => ( - - - {record.managed ? 'Managed' : 'Unmanaged'} - - {onToggleManaged && ( - { - e.stopPropagation(); // Stop row click event - onToggleManaged(record, checked); - }} - onChange={() => {}} - /> - )} - - ), - }); - } - - // Add deploy action column if enabled - if (config.deploy?.enabled) { - legacyColumns.push({ - title: 'Actions', - key: 'actions', - render: (_, record: T) => ( - - - - - - ), - }); - } - - // Add audit column if enabled - if (config.audit?.enabled) { - legacyColumns.push({ - title: 'Audit', - key: 'audit', - render: (_, record: T) => ( - - - - ), - }); - } - - return legacyColumns; - } if (loading) { return ( 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 941fced56e..ac223c63d9 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 @@ -76,7 +76,7 @@ export interface DeployableItemConfig[]; // New method to generate columns - getColumns?: (params: { + getColumns: (params: { environment: Environment; refreshing: boolean; onToggleManaged?: (item: T, checked: boolean) => Promise; From cae995aae8c15f837a31f42e922be47aa2c7357d Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 17 Apr 2025 01:57:45 +0500 Subject: [PATCH 58/68] move all columns to config --- .../environments/config/apps.config.tsx | 61 +++++- .../config/data-sources.config.tsx | 37 +++- .../environments/config/query.config.tsx | 74 ++++++-- .../environments/config/usergroups.config.tsx | 28 ++- .../environments/config/workspace.config.tsx | 2 +- .../environments/utils/columnFactories.tsx | 178 +++++++++++++++++- 6 files changed, 348 insertions(+), 32 deletions(-) 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 8cea1a0e1a..90b673f346 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 @@ -8,6 +8,17 @@ import { getMergedWorkspaceApps, deployApp } from '../services/apps.service'; import { connectManagedApp, unconnectManagedApp } from '../services/enterprise.service'; import { App, AppStats } from '../types/app.types'; + +import { + createNameColumn, + createDescriptionColumn, + createPublishedColumn, + createManagedColumn, + createDeployColumn, + createAuditColumn, + createIdColumn +} from '../utils/columnFactories'; + // Define AppStats interface if not already defined @@ -59,6 +70,31 @@ export const appsConfig: DeployableItemConfig = { }, // Table configuration + getColumns: ({ environment, refreshing, onToggleManaged, openDeployModal, additionalParams }) => { + const columns = [ + createIdColumn(), + 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', @@ -66,21 +102,28 @@ export const appsConfig: DeployableItemConfig = { key: 'name', }, { - title: 'Description', - dataIndex: 'description', - key: 'description', + title: 'ID', + dataIndex: 'id', + key: 'id', ellipsis: true, }, + { + title: 'Role', + dataIndex: 'role', + key: 'role', + render: (role: string) => {role}, + }, + { title: 'Status', - dataIndex: 'published', - key: 'published', - render: (published: boolean) => ( - - {published ? 'Published' : 'Unpublished'} + dataIndex: 'status', + key: 'status', + render: (status: string) => ( + + {status} ), - }, + } ], // Deployment options 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 fee0d91058..567e460a79 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 @@ -7,7 +7,15 @@ import { DataSource, DataSourceStats } 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 +} from '../utils/columnFactories'; export const dataSourcesConfig: DeployableItemConfig = { @@ -111,6 +119,33 @@ export const dataSourcesConfig: DeployableItemConfig { + const columns = [ + createNameColumn(), + 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 { 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 8c274baca7..00721f0331 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 @@ -8,6 +8,16 @@ import { connectManagedQuery, unconnectManagedQuery } from '../services/enterpri import { getMergedWorkspaceQueries, deployQuery } from '../services/query.service'; import { Environment } from '../types/environment.types'; +import { + createNameColumn, + createCreatorColumn, + createDateColumn, + createQueryTypeColumn, + createManagedColumn, + createDeployColumn, + createAuditColumn +} from '../utils/columnFactories'; + // Define QueryStats interface export interface QueryStats { total: number; @@ -55,8 +65,6 @@ export const queryConfig: DeployableItemConfig = { unmanaged: total - managed }; }, - - // Table configuration columns: [ { title: 'Name', @@ -64,28 +72,56 @@ export const queryConfig: DeployableItemConfig = { key: 'name', }, { - title: 'Creator', - dataIndex: 'creatorName', - key: 'creatorName', + title: 'Type', + dataIndex: 'type', + key: 'type', + render: (type: string) => ( + {type || 'Unknown'} + ), }, { - title: 'Creation Date', - key: 'createTime', - render: (_, record: Query) => { - if (!record.createTime) return 'N/A'; - const date = new Date(record.createTime); - return `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`; - }, + title: 'Database', + key: 'database', + render: (_, record: Query) => ( + {record.datasourceConfig?.database || 'N/A'} + ), }, { - title: 'Query Type', - key: 'queryType', - render: (_, record: Query) => { - const queryType = record.libraryQueryDSL?.query?.compType || 'Unknown'; - return {queryType}; - }, - } + title: 'Status', + dataIndex: 'datasourceStatus', + key: 'status', + render: (status: string) => ( + + {status} + + ), + }, ], + getColumns: ({ environment, refreshing, onToggleManaged, openDeployModal, additionalParams }) => { + const columns = [ + createNameColumn(), + 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, 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 index bcf83c3e3c..8ae0413202 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/config/usergroups.config.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/config/usergroups.config.tsx @@ -5,7 +5,14 @@ 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'; @@ -120,6 +127,25 @@ export const userGroupsConfig: DeployableItemConfig = // 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( 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 c1e9c8e6cd..298888ccdf 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 @@ -99,8 +99,8 @@ export const workspaceConfig: DeployableItemConfig = // New getColumns method getColumns: ({ environment, refreshing, onToggleManaged, additionalParams }) => { const columns = [ - createNameColumn(), createIdColumn(), + createNameColumn(), createRoleColumn(), createDateColumn('creationDate', 'Creation Date'), createStatusColumn() diff --git a/client/packages/lowcoder/src/pages/setting/environments/utils/columnFactories.tsx b/client/packages/lowcoder/src/pages/setting/environments/utils/columnFactories.tsx index 976c352da7..5609f5b187 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/utils/columnFactories.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/utils/columnFactories.tsx @@ -1,6 +1,6 @@ // utils/columnFactories.tsx import React from 'react'; -import { Tag, Space, Switch, Button, Tooltip } from 'antd'; +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'; @@ -120,4 +120,180 @@ export function createAuditColumn( ); }, }; +} + + +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) => ( + + + + + + ), + }; +} + + +// 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 From 7a6626cd200998fe0276d5bb32a9973dabcc36a4 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 17 Apr 2025 14:30:40 +0500 Subject: [PATCH 59/68] disable button unless managed --- .../environments/utils/columnFactories.tsx | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/client/packages/lowcoder/src/pages/setting/environments/utils/columnFactories.tsx b/client/packages/lowcoder/src/pages/setting/environments/utils/columnFactories.tsx index 5609f5b187..b33685ab70 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/utils/columnFactories.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/utils/columnFactories.tsx @@ -141,27 +141,37 @@ export function createDeployColumn ( - - - - - - ), + + + + ); + }, }; } - // App-specific columns export function createPublishedColumn(): ColumnType { return { From 72cfeab9ba7b3d91e8b6c66c8681bcd1ea25a0d6 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 17 Apr 2025 14:35:30 +0500 Subject: [PATCH 60/68] add managed/unmanged tags in workspace table --- .../src/pages/setting/environments/config/workspace.config.tsx | 2 ++ 1 file changed, 2 insertions(+) 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 298888ccdf..87d15ae3b5 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 @@ -102,8 +102,10 @@ export const workspaceConfig: DeployableItemConfig = createIdColumn(), createNameColumn(), createRoleColumn(), + createManagedColumn(), createDateColumn('creationDate', 'Creation Date'), createStatusColumn() + ]; From b40147fe2adec61e90ecd65767cc37f5dd05fe8f Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 17 Apr 2025 16:03:13 +0500 Subject: [PATCH 61/68] fix breadcrumbs --- .../environments/EnvironmentDetail.tsx | 17 ++++++++++++ .../setting/environments/WorkspaceDetail.tsx | 26 ++++++++++++------- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx index e76ba8145f..f33bbe4c1d 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx @@ -11,6 +11,7 @@ import { Dropdown, Menu, Button, + Breadcrumb, } from "antd"; import { ReloadOutlined, @@ -22,6 +23,7 @@ import { EditOutlined, EllipsisOutlined, MoreOutlined, + HomeOutlined } from "@ant-design/icons"; import { useEnvironmentContext } from "./context/EnvironmentContext"; @@ -30,6 +32,7 @@ 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"; const { Title, Text } = Typography; const { TabPane } = Tabs; @@ -106,6 +109,20 @@ const EnvironmentDetail: React.FC = () => { } return (
+ + + + history.push("/setting/environments")} + > + Environments + + + {environment.environmentName} + + + {/* Header with environment name and controls */}
{ > {/* Breadcrumb navigation */} - history.push("/home/settings/environments")} - > - Environments + + history.push("/setting/environments")} + > + Environments + - - history.push(`/home/settings/environments/${environmentId}`) - } - > - {environment.environmentName} + + + history.push(`/setting/environments/${environmentId}`) + } + > + {environment.environmentName} + {workspace.name} From de46a00abd9d44ea5842457c4a3e246918fdbed9 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 17 Apr 2025 16:07:30 +0500 Subject: [PATCH 62/68] fix back links --- .../src/pages/setting/environments/WorkspaceDetail.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx index 9850cb39f5..d22af82e86 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx @@ -100,7 +100,7 @@ const WorkspaceDetail: React.FC = () => { showIcon style={{ margin: '24px' }} action={ - } @@ -193,7 +193,7 @@ const WorkspaceDetail: React.FC = () => {
- + + - } - /> - ); +
+ Workspace not found +
+ ) } + return (
Date: Thu, 17 Apr 2025 19:02:09 +0500 Subject: [PATCH 65/68] fix responsiveness detail page --- .../environments/EnvironmentDetail.tsx | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx index 36ef2fc716..631dfa8e7d 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx @@ -109,11 +109,13 @@ const EnvironmentDetail: React.FC = () => { ); } return ( -
- - +
+ - history.push("/setting/environments")} > @@ -122,7 +124,6 @@ const EnvironmentDetail: React.FC = () => { {environment.environmentName} - {/* Header with environment name and controls */}
{ marginBottom: "24px", display: "flex", justifyContent: "space-between", - alignItems: "center", + alignItems: "flex-start", // Changed from center to allow wrapping + flexWrap: "wrap", // Allow wrapping on small screens + gap: "16px", // Add spacing between wrapped elements }} > -
- + <div style={{ flex: "1 1 auto", minWidth: "200px" }}> + <Title level={3} style={{ margin: 0, wordBreak: "break-word" }}> {environment.environmentName || "Unnamed Environment"} ID: {environment.environmentId}
- -
- {/* Basic Environment Information Card */} + {/* Basic Environment Information Card - improved responsiveness */} { > {environment.environmentFrontendUrl ? ( @@ -200,7 +203,7 @@ const EnvironmentDetail: React.FC = () => { {/* Tabs for Workspaces and User Groups */} - + {/* Using our new generic component with the workspace config */} { } key="userGroups" - > + > {/* Using our new generic component with the user group config */} - {/* Edit Environment Modal */} @@ -235,7 +237,6 @@ const EnvironmentDetail: React.FC = () => { loading={isUpdating} /> )} -
); }; From e15f9b6d0d94194f9b0061adda37f04bcaee1d12 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 17 Apr 2025 20:27:43 +0500 Subject: [PATCH 66/68] fix tabs UI --- .../environments/components/DeployableItemsList.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsList.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsList.tsx index 0009e549bb..63f8dda721 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsList.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsList.tsx @@ -46,13 +46,13 @@ function DeployableItemsList({ history.push(route); }; - // Determine columns - Use new getColumns method if available, fall back to old approach + // Get columns from config const columns = config.getColumns({ - environment, - refreshing, - onToggleManaged, - openDeployModal, - additionalParams + environment, + refreshing, + onToggleManaged, + openDeployModal, + additionalParams }) @@ -82,6 +82,7 @@ function DeployableItemsList({ rowKey={config.idField} pagination={{ pageSize: 10 }} size="middle" + scroll={{ x: 'max-content' }} onRow={(record) => ({ onClick: hasNavigation ? () => handleRowClick(record) : undefined, style: hasNavigation ? { cursor: 'pointer' } : undefined, From 6c047141abfe36ddc64a4a2e8971a40b2ae89bde Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 17 Apr 2025 20:45:15 +0500 Subject: [PATCH 67/68] replace edit dropdown with button --- .../setting/environments/EnvironmentDetail.tsx | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx index 631dfa8e7d..d16f52c242 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx @@ -125,6 +125,7 @@ const EnvironmentDetail: React.FC = () => { {environment.environmentName} + {/* Header with environment name and controls */} {/* Header with environment name and controls */}
{ marginBottom: "24px", display: "flex", justifyContent: "space-between", - alignItems: "flex-start", // Changed from center to allow wrapping - flexWrap: "wrap", // Allow wrapping on small screens - gap: "16px", // Add spacing between wrapped elements + alignItems: "flex-start", + flexWrap: "wrap", + gap: "16px", }} >
@@ -144,9 +145,13 @@ const EnvironmentDetail: React.FC = () => { ID: {environment.environmentId}
- -
From a3dac7e859ef9303c78c6e82b33187aeecd0e257 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Fri, 18 Apr 2025 15:57:47 +0500 Subject: [PATCH 68/68] remove unused files/code --- .../setting/environments/WorkspaceDetail.tsx | 1 - .../environments/components/AppsList.tsx | 128 --------------- .../environments/components/AppsTab.tsx | 116 -------------- .../components/DataSourcesList.tsx | 135 ---------------- .../components/DataSourcesTab.tsx | 118 -------------- .../components/DeployAppModal.tsx | 148 ------------------ .../components/UserGroupsList.tsx | 109 ------------- .../environments/components/UserGroupsTab.tsx | 101 ------------ .../components/WorkspacesList.tsx | 124 --------------- .../environments/components/WorkspacesTab.tsx | 116 -------------- .../hooks/enterprise/useManagedApps.ts | 26 --- .../hooks/enterprise/useManagedWorkspaces.ts | 52 ------ .../hooks/useEnvironmentUserGroups.ts | 81 ---------- .../hooks/useEnvironmentWorkspaces.ts | 81 ---------- .../environments/hooks/useEnvironments.ts | 65 -------- .../environments/hooks/useWorkspace.ts | 65 -------- .../environments/hooks/useWorkspaceApps.ts | 123 --------------- .../hooks/useWorkspaceDataSources.ts | 110 ------------- .../environments/hooks/useWorkspaces.ts | 121 -------------- .../environments/utils/getMergedApps.ts | 9 -- .../environments/utils/getMergedWorkspaces.ts | 33 ---- 21 files changed, 1862 deletions(-) delete mode 100644 client/packages/lowcoder/src/pages/setting/environments/components/AppsList.tsx delete mode 100644 client/packages/lowcoder/src/pages/setting/environments/components/AppsTab.tsx delete mode 100644 client/packages/lowcoder/src/pages/setting/environments/components/DataSourcesList.tsx delete mode 100644 client/packages/lowcoder/src/pages/setting/environments/components/DataSourcesTab.tsx delete mode 100644 client/packages/lowcoder/src/pages/setting/environments/components/DeployAppModal.tsx delete mode 100644 client/packages/lowcoder/src/pages/setting/environments/components/UserGroupsList.tsx delete mode 100644 client/packages/lowcoder/src/pages/setting/environments/components/UserGroupsTab.tsx delete mode 100644 client/packages/lowcoder/src/pages/setting/environments/components/WorkspacesList.tsx delete mode 100644 client/packages/lowcoder/src/pages/setting/environments/components/WorkspacesTab.tsx delete mode 100644 client/packages/lowcoder/src/pages/setting/environments/hooks/enterprise/useManagedApps.ts delete mode 100644 client/packages/lowcoder/src/pages/setting/environments/hooks/enterprise/useManagedWorkspaces.ts delete mode 100644 client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironmentUserGroups.ts delete mode 100644 client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironmentWorkspaces.ts delete mode 100644 client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironments.ts delete mode 100644 client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspace.ts delete mode 100644 client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaceApps.ts delete mode 100644 client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaceDataSources.ts delete mode 100644 client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaces.ts delete mode 100644 client/packages/lowcoder/src/pages/setting/environments/utils/getMergedApps.ts delete mode 100644 client/packages/lowcoder/src/pages/setting/environments/utils/getMergedWorkspaces.ts diff --git a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx index 79dfa0372d..2867171b0a 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx @@ -27,7 +27,6 @@ import { CloudUploadOutlined } from "@ant-design/icons"; import { useEnvironmentContext } from "./context/EnvironmentContext"; -import { useWorkspace } from "./hooks/useWorkspace"; import DeployableItemsTab from "./components/DeployableItemsTab"; import { appsConfig } from "./config/apps.config"; import { dataSourcesConfig } from "./config/data-sources.config"; diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/AppsList.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/AppsList.tsx deleted file mode 100644 index 40048f4eb4..0000000000 --- a/client/packages/lowcoder/src/pages/setting/environments/components/AppsList.tsx +++ /dev/null @@ -1,128 +0,0 @@ -// components/AppsList.tsx -import React, { useState } from 'react'; -import { Table, Switch, Button, Space, Tooltip, Tag } from 'antd'; -import { CloudUploadOutlined } from '@ant-design/icons'; -import { App } from '../types/app.types'; -import { Environment } from '../types/environment.types'; -import DeployAppModal from './DeployAppModal'; -import { ColumnsType } from 'antd/lib/table'; - -interface AppsListProps { - apps: App[]; - loading: boolean; - error: string | null; - environment: Environment; - onToggleManaged: (app: App, checked: boolean) => Promise; - onRefresh?: () => void; // Make this optional since your current implementation doesn't have it -} - -const AppsList: React.FC = ({ - apps, - loading, - error, - environment, - onToggleManaged, - onRefresh, -}) => { - const [deployModalVisible, setDeployModalVisible] = useState(false); - const [selectedApp, setSelectedApp] = useState(null); - - const handleDeploy = (app: App) => { - setSelectedApp(app); - setDeployModalVisible(true); - }; - - // Cast the value to boolean in onFilter to fix the type issue - const columns: ColumnsType = [ - { - title: 'Name', - dataIndex: 'name', - key: 'name', - sorter: (a: App, b: App) => a.name.localeCompare(b.name), - }, - { - title: 'Description', - dataIndex: 'description', - key: 'description', - ellipsis: true, - }, - { - title: 'Status', - dataIndex: 'published', - key: 'published', - render: (published: boolean) => ( - - {published ? 'Published' : 'Unpublished'} - - ), - filters: [ - { text: 'Published', value: true }, - { text: 'Unpublished', value: false }, - ], - onFilter: (value, record: App) => record.published === Boolean(value), - }, - { - title: 'Managed', - dataIndex: 'managed', - key: 'managed', - render: (managed: boolean, record: App) => ( - - onToggleManaged(record, checked)} - /> - - {managed ? 'Managed' : 'Unmanaged'} - - - ), - filters: [ - { text: 'Managed', value: true }, - { text: 'Unmanaged', value: false }, - ], - onFilter: (value, record: App) => record.managed === Boolean(value), - }, - { - title: 'Actions', - key: 'actions', - render: (_, record: App) => ( - - - - - - ), - }, - ]; - - return ( - <> -
- - setDeployModalVisible(false)} - /> - - ); -}; - -export default AppsList; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/AppsTab.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/AppsTab.tsx deleted file mode 100644 index 9d8c8f923e..0000000000 --- a/client/packages/lowcoder/src/pages/setting/environments/components/AppsTab.tsx +++ /dev/null @@ -1,116 +0,0 @@ -// components/AppsTab.tsx -import React from 'react'; -import { Card, Button, Row, Col, Statistic, Divider, Alert, message } from 'antd'; -import { AppstoreOutlined, SyncOutlined } from '@ant-design/icons'; -import Title from 'antd/lib/typography/Title'; -import { Environment } from '../types/environment.types'; -import { useWorkspaceApps } from '../hooks/useWorkspaceApps'; -import AppsList from './AppsList'; -import { App } from '../types/app.types'; - -interface AppsTabProps { - environment: Environment; - workspaceId: string; -} - -const AppsTab: React.FC = ({ environment, workspaceId }) => { - const { - apps, - stats, - loading, - error, - toggleManagedStatus - } = useWorkspaceApps(environment, workspaceId); - - const handleToggleManagedApp = async (app: App, checked: boolean) => { - const success = await toggleManagedStatus(app, checked); - - if (success) { - message.success(`${app.name} is now ${checked ? "Managed" : "Unmanaged"}`); - } else { - message.error(`Failed to toggle ${app.name}`); - } - }; - - return ( - - {/* Header with refresh button */} -
- Apps in this Workspace -
- - {/* App Statistics */} - -
- } - /> - - - } - /> - - - } - /> - - - } - /> - - - - - - {/* Show error if apps loading failed */} - {error && ( - - )} - - {/* Configuration warning */} - {(!environment.environmentApikey || - !environment.environmentApiServiceUrl) && - !error && ( - - )} - - {/* Apps List */} - - - ); -}; - -export default AppsTab; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/DataSourcesList.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/DataSourcesList.tsx deleted file mode 100644 index e84f072934..0000000000 --- a/client/packages/lowcoder/src/pages/setting/environments/components/DataSourcesList.tsx +++ /dev/null @@ -1,135 +0,0 @@ -// components/DataSourcesList.tsx -// Create this new file - -import React, { useState } from 'react'; -import { Table, Switch, Button, Space, Tooltip, Tag } from 'antd'; -import { CloudUploadOutlined } from '@ant-design/icons'; -import { DataSource } from '../types/datasource.types'; -import { Environment } from '../types/environment.types'; -import { ColumnsType } from 'antd/lib/table'; - -interface DataSourcesListProps { - dataSources: DataSource[]; - loading: boolean; - error: string | null; - environment: Environment; - onToggleManaged: (dataSource: DataSource, checked: boolean) => Promise; - onRefresh?: () => void; -} - -const DataSourcesList: React.FC = ({ - dataSources, - loading, - error, - environment, - onToggleManaged, - onRefresh, -}) => { - const [deployModalVisible, setDeployModalVisible] = useState(false); - const [selectedDataSource, setSelectedDataSource] = useState(null); - - const handleDeploy = (dataSource: DataSource) => { - setSelectedDataSource(dataSource); - setDeployModalVisible(true); - }; - - const columns: ColumnsType = [ - { - title: 'Name', - dataIndex: 'name', - key: 'name', - sorter: (a, b) => a.name.localeCompare(b.name), - }, - { - title: 'Type', - dataIndex: 'type', - key: 'type', - filters: Array.from(new Set(dataSources.map(ds => ds.type))) - .map(type => ({ text: type, value: type })), - onFilter: (value, record) => record.type === value, - }, - { - title: 'Status', - dataIndex: 'datasourceStatus', - key: 'status', - render: (status: string) => ( - - {status} - - ), - filters: Array.from(new Set(dataSources.map(ds => ds.datasourceStatus))) - .map(status => ({ text: status, value: status })), - onFilter: (value, record) => record.datasourceStatus === value, - }, - { - title: 'DB Name', - dataIndex: ['datasourceConfig', 'database'], - key: 'database', - render: (database: string | null) => database || 'N/A', - }, - { - title: 'Managed', - dataIndex: 'managed', - key: 'managed', - render: (managed: boolean, record: DataSource) => ( - - onToggleManaged(record, checked)} - /> - - {managed ? 'Managed' : 'Unmanaged'} - - - ), - filters: [ - { text: 'Managed', value: true }, - { text: 'Unmanaged', value: false }, - ], - onFilter: (value, record) => record.managed === Boolean(value), - }, - { - title: 'Actions', - key: 'actions', - render: (_, record: DataSource) => ( - - - - - - ), - }, - ]; - - return ( - <> -
- - {/* setDeployModalVisible(false)} - onSuccess={onRefresh} - /> */} - - ); -}; - -export default DataSourcesList; \ 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 deleted file mode 100644 index 7a5562578d..0000000000 --- a/client/packages/lowcoder/src/pages/setting/environments/components/DataSourcesTab.tsx +++ /dev/null @@ -1,118 +0,0 @@ -// components/DataSourcesTab.tsx -// Create this new file - -import React from 'react'; -import { Card, Button, Row, Col, Statistic, Divider, Alert, message } from 'antd'; -import { DatabaseOutlined, SyncOutlined } from '@ant-design/icons'; -import Title from 'antd/lib/typography/Title'; -import { Environment } from '../types/environment.types'; -import { useWorkspaceDataSources } from '../hooks/useWorkspaceDataSources'; -import { DataSource } from '../types/datasource.types'; - -interface DataSourcesTabProps { - environment: Environment; - workspaceId: string; -} - -const DataSourcesTab: React.FC = ({ environment, workspaceId }) => { - const { - dataSources, - stats, - loading, - error, - toggleManagedStatus - } = useWorkspaceDataSources(environment, workspaceId); - - const handleToggleManagedDataSource = async (dataSource: DataSource, checked: boolean) => { - const success = await toggleManagedStatus(dataSource, checked); - - if (success) { - message.success(`${dataSource.name} is now ${checked ? "Managed" : "Unmanaged"}`); - } else { - message.error(`Failed to toggle ${dataSource.name}`); - } - }; - - return ( - - {/* Header with refresh button */} -
- Data Sources in this Workspace -
- - {/* Data Source Statistics */} - -
- } - /> - - - } - /> - - - } - /> - - - } - /> - - - - - - {/* Show error if data sources loading failed */} - {error && ( - - )} - - {/* Configuration warning */} - {(!environment.environmentApikey || - !environment.environmentApiServiceUrl) && - !error && ( - - )} - - {/* Data Sources List */} - {/* */} - - ); -}; - -export default DataSourcesTab; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/DeployAppModal.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/DeployAppModal.tsx deleted file mode 100644 index 408f5ae694..0000000000 --- a/client/packages/lowcoder/src/pages/setting/environments/components/DeployAppModal.tsx +++ /dev/null @@ -1,148 +0,0 @@ -// components/DeployAppModal.tsx -import React, { useState, useEffect } from 'react'; -import { Modal, Form, Select, Checkbox, Button, message, Spin } from 'antd'; -import { Environment } from '../types/environment.types'; -import { App } from '../types/app.types'; -import { deployApp } from '../services/apps.service'; -import { useEnvironmentContext } from '../context/EnvironmentContext'; - -interface DeployAppModalProps { - visible: boolean; - app: App | null; - currentEnvironment: Environment; - onClose: () => void; -} - -const DeployAppModal: React.FC = ({ - visible, - app, - currentEnvironment, - onClose, -}) => { - const [form] = Form.useForm(); - const { environments, isLoadingEnvironments } = useEnvironmentContext(); - console.log('environments data modal', environments); - const [deploying, setDeploying] = useState(false); - - // Reset form when modal becomes visible - useEffect(() => { - if (visible) { - form.resetFields(); - } - }, [visible, form]); - - // Filter out current environment from the list - const targetEnvironments = environments.filter( - (env) => env.environmentId !== currentEnvironment.environmentId - ); - - const handleDeploy = async () => { - try { - const values = await form.validateFields(); - - if (!app) return; - - setDeploying(true); - - await deployApp( - { - envId: currentEnvironment.environmentId, - targetEnvId: values.targetEnvId, - applicationId: app.applicationId!, - updateDependenciesIfNeeded: values.updateDependenciesIfNeeded, - publishOnTarget: values.publishOnTarget, - publicToAll: values.publicToAll, - publicToMarketplace: values.publicToMarketplace, - }, - ); - - message.success(`Successfully deployed ${app.name} to target environment`); - onClose(); - } catch (error) { - console.error('Deployment error:', error); - message.error('Failed to deploy app'); - } finally { - setDeploying(false); - } - }; - - return ( - - {isLoadingEnvironments ? ( -
- -
- ) : ( -
- - - - - - Update Dependencies If Needed - - - - Publish On Target - - - - Public To All - - - - Public To Marketplace - - - - - - - - )} -
- ); -}; - -export default DeployAppModal; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/UserGroupsList.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/UserGroupsList.tsx deleted file mode 100644 index 7a191784e9..0000000000 --- a/client/packages/lowcoder/src/pages/setting/environments/components/UserGroupsList.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import React from 'react'; -import { Table, Tag, Empty, Spin, Badge } from 'antd'; -import { UserGroup } from '../types/userGroup.types'; - -interface UserGroupsListProps { - userGroups: UserGroup[]; - loading: boolean; - error?: string | null; -} - -/** - * Component to display a list of user groups in a table - */ -const UserGroupsList: React.FC = ({ - userGroups, - loading, - error, -}) => { - // Format timestamp to date string - const formatDate = (timestamp?: number): string => { - if (!timestamp) return 'N/A'; - const date = new Date(timestamp); - return `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`; - }; - - // Table columns definition - const columns = [ - { - title: 'Name', - dataIndex: 'groupName', - key: 'groupName', - render: (name: string, record: UserGroup) => ( -
- {name} - {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; - }, - } - ]; - - // If loading, show spinner - if (loading) { - return ( -
- -
- ); - } - - // If no user groups or error, show empty state - if (!userGroups || userGroups.length === 0 || error) { - return ( - - ); - } - - return ( -
- ); -}; - -export default UserGroupsList; \ 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 deleted file mode 100644 index 244f37b0cb..0000000000 --- a/client/packages/lowcoder/src/pages/setting/environments/components/UserGroupsTab.tsx +++ /dev/null @@ -1,101 +0,0 @@ -// components/UserGroupsTab.tsx -import React from 'react'; -import { Card, Button, Row, Col, Statistic, Divider, Alert } from 'antd'; -import { TeamOutlined, UserOutlined, SyncOutlined } from '@ant-design/icons'; -import Title from 'antd/lib/typography/Title'; -import { Environment } from '../types/environment.types'; -import { useEnvironmentUserGroups } from '../hooks/useEnvironmentUserGroups'; -import UserGroupsList from './UserGroupsList'; - -interface UserGroupsTabProps { - environment: Environment; -} - -const UserGroupsTab: React.FC = ({ environment }) => { - const { - userGroups, - loading: userGroupsLoading, - error: userGroupsError, - userGroupStats, - } = useEnvironmentUserGroups(environment); - - return ( - - {/* Header with refresh button */} -
- User Groups in this Environment -
- - {/* User Group Statistics */} - -
- } - /> - - - } - /> - - - } - /> - - - - - - {/* Show error if user group loading failed */} - {userGroupsError && ( - - )} - - {/* Show warning if no API key or API service URL is configured */} - {(!environment.environmentApikey || - !environment.environmentApiServiceUrl) && - !userGroupsError && ( - - )} - - {/* User Groups List */} - - - ); -}; - -export default UserGroupsTab; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/WorkspacesList.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/WorkspacesList.tsx deleted file mode 100644 index 9d3aa22963..0000000000 --- a/client/packages/lowcoder/src/pages/setting/environments/components/WorkspacesList.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import React from 'react'; -import { Table, Tag, Empty, Spin, Switch, Space } from 'antd'; -import { Workspace } from '../types/workspace.types'; -import history from '@lowcoder-ee/util/history'; -import { buildEnvironmentWorkspaceId } from '@lowcoder-ee/constants/routesURL'; - -interface WorkspacesListProps { - workspaces: Workspace[]; - loading: boolean; - error?: string | null; - environmentId: string; - onToggleManaged?: (workspace: Workspace, checked: boolean) => void; - refreshing?: boolean; -} - -const WorkspacesList: React.FC = ({ - workspaces, - loading, - error, - environmentId, - onToggleManaged, - refreshing = false, -}) => { - const formatDate = (timestamp?: number): string => { - if (!timestamp) return 'N/A'; - const date = new Date(timestamp); - return `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`; - }; - - const handleRowClick = (workspace: Workspace) => { - history.push(`${buildEnvironmentWorkspaceId(environmentId, workspace.id)}`); - }; - - const 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) => formatDate(record.creationDate), - }, - { - title: 'Status', - dataIndex: 'status', - key: 'status', - render: (status: string) => ( - - {status} - - ), - }, - { - title: 'Managed', - key: 'managed', - render: (record: Workspace) => ( - - - {record.managed ? 'Managed' : 'Unmanaged'} - - {onToggleManaged && ( - { - e.stopPropagation(); // ✅ THIS STOPS the row from being triggered - onToggleManaged(record, checked); - }} - onChange={() => {}} - /> - )} - - ), - }, - ]; - - if (loading) { - return ( -
- -
- ); - } - - if (!workspaces || workspaces.length === 0 || error) { - return ( - - ); - } - - return ( -
({ - onClick: () => handleRowClick(record), - style: { cursor: 'pointer' }, - })} - /> - ); -}; - -export default WorkspacesList; diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/WorkspacesTab.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/WorkspacesTab.tsx deleted file mode 100644 index 3e2348bc88..0000000000 --- a/client/packages/lowcoder/src/pages/setting/environments/components/WorkspacesTab.tsx +++ /dev/null @@ -1,116 +0,0 @@ -// components/WorkspacesTab.tsx -import React from 'react'; -import { Card, Button, Row, Col, Statistic, Divider, Alert, message } from 'antd'; -import { ClusterOutlined, SyncOutlined } from '@ant-design/icons'; -import Title from 'antd/lib/typography/Title'; -import { Environment } from '../types/environment.types'; -import { useWorkspaces } from '../hooks/useWorkspaces'; -import WorkspacesList from './WorkspacesList'; -import { Workspace } from '../types/workspace.types'; - -interface WorkspacesTabProps { - environment: Environment; -} - -const WorkspacesTab: React.FC = ({ environment }) => { - // Use the new hook that handles both regular and managed workspaces - const { - workspaces, - stats, - loading, - error, - toggleManagedStatus - } = useWorkspaces(environment); - - const handleToggleManaged = async (workspace: Workspace, checked:boolean) => { - const success = await toggleManagedStatus(workspace, checked); - - if (success) { - message.success(`${workspace.name} is now ${checked ? 'Managed' : 'Unmanaged'}`); - } else { - message.error(`Failed to toggle managed state for ${workspace.name}`); - // Optionally refresh to ensure UI is in sync with backend - } - }; - - return ( - - {/* Header with refresh button */} -
- Workspaces in this Environment -
- - {/* Workspace Statistics */} - -
- } - /> - - - } - /> - - - } - /> - - - - - - {/* Show error if workspace loading failed */} - {error && ( - - )} - - {(!environment.environmentApikey || - !environment.environmentApiServiceUrl) && - !error && ( - - )} - - {/* Workspaces List */} - - - ); -}; - -export default WorkspacesTab; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/hooks/enterprise/useManagedApps.ts b/client/packages/lowcoder/src/pages/setting/environments/hooks/enterprise/useManagedApps.ts deleted file mode 100644 index 8f8a4bc8ee..0000000000 --- a/client/packages/lowcoder/src/pages/setting/environments/hooks/enterprise/useManagedApps.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { useState, useEffect } from 'react'; -import { getManagedApps } from '../../services/enterprise.service'; - -export const useManagedApps = (environmentId: string) => { - const [managedApps, setManagedApps] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - useEffect(() => { - const fetchManagedApps = async () => { - setLoading(true); - try { - const apps = await getManagedApps(environmentId); - setManagedApps(apps); - } catch (err: any) { - setError(err.message || 'Failed to fetch managed apps'); - } finally { - setLoading(false); - } - }; - - fetchManagedApps(); - }, [environmentId]); - - return { managedApps, loading, error }; -}; diff --git a/client/packages/lowcoder/src/pages/setting/environments/hooks/enterprise/useManagedWorkspaces.ts b/client/packages/lowcoder/src/pages/setting/environments/hooks/enterprise/useManagedWorkspaces.ts deleted file mode 100644 index be539629b0..0000000000 --- a/client/packages/lowcoder/src/pages/setting/environments/hooks/enterprise/useManagedWorkspaces.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { useEffect, useState, useCallback } from "react"; -import { ManagedOrg } from "../../types/enterprise.types"; -import { - getManagedWorkspaces, -} from "../../services/enterprise.service"; -import { Environment } from "../../types/environment.types"; - -export function useManagedWorkspaces( - environment: Environment | null -) { - const [managed, setManaged] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const fetchManaged = useCallback(async () => { - if (!environment) return; - setLoading(true); - setError(null); - - - try { - const { environmentId, environmentApikey, environmentApiServiceUrl } = environment; - - if (!environmentApikey) { - setError("Missing API key or service URL for this environment."); - setLoading(false); - return; - } - - const result = await getManagedWorkspaces(environmentId); - console.log("Managed workspaces:", result); - setManaged(result); - } catch (err: any) { - setError(err.message ?? "Failed to load managed workspaces"); - } finally { - setLoading(false); - } - } , [environment]); - - useEffect(() => { - if(environment) { - fetchManaged(); - } - }, [environment, fetchManaged]); - - return { - managedWorkspaces: managed, - managedLoading: loading, - managedError: error, - refreshManagedWorkspaces: fetchManaged, - }; -} diff --git a/client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironmentUserGroups.ts b/client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironmentUserGroups.ts deleted file mode 100644 index 0923932c27..0000000000 --- a/client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironmentUserGroups.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { useState, useEffect, useCallback } from "react"; -import { getEnvironmentUserGroups } from "../services/environments.service"; -import { Environment } from "../types/environment.types"; -import { UserGroup } from "../types/userGroup.types"; - -interface UserGroupStats { - total: number; - totalUsers: number; - adminUsers: number; - apiKeyConfigured: boolean; - apiServiceUrlConfigured: boolean; -} - -export const useEnvironmentUserGroups = ( - environment: Environment | null -) => { - const [userGroups, setUserGroups] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const fetchUserGroups = useCallback(async () => { - if (!environment) return; - - setLoading(true); - setError(null); - - try { - const { environmentId, environmentApikey, environmentApiServiceUrl } = environment; - - if (!environmentApikey) { - setError("No API key configured for this environment. User groups cannot be fetched."); - setLoading(false); - return; - } - - if (!environmentApiServiceUrl) { - setError("No API service URL configured for this environment. User groups cannot be fetched."); - setLoading(false); - return; - } - - const data = await getEnvironmentUserGroups( - environmentId, - environmentApikey, - environmentApiServiceUrl - ); - - setUserGroups(data); - } catch (err) { - setError( - err instanceof Error - ? err.message - : "Failed to fetch user groups" - ); - } finally { - setLoading(false); - } - }, [environment]); - - useEffect(() => { - if (environment) { - fetchUserGroups(); - } - }, [environment, fetchUserGroups]); - - const userGroupStats: UserGroupStats = { - total: userGroups.length, - totalUsers: userGroups.reduce((sum, group) => sum + (group.stats?.userCount ?? 0), 0), - adminUsers: userGroups.reduce((sum, group) => sum + (group.stats?.adminUserCount ?? 0), 0), - apiKeyConfigured: !!environment?.environmentApikey, - apiServiceUrlConfigured: !!environment?.environmentApiServiceUrl, - }; - - return { - userGroups, - loading, - error, - refresh: fetchUserGroups, - userGroupStats, - }; -}; diff --git a/client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironmentWorkspaces.ts b/client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironmentWorkspaces.ts deleted file mode 100644 index 0f949037bf..0000000000 --- a/client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironmentWorkspaces.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { useState, useEffect, useCallback } from "react"; -import { getEnvironmentWorkspaces } from "../services/environments.service"; -import { Environment } from "../types/environment.types"; -import { Workspace } from "../types/workspace.types"; - -interface WorkspaceStats { - total: number; - managed: number; - unmanaged: number; - apiKeyConfigured: boolean; - apiServiceUrlConfigured: boolean; -} - -export const useEnvironmentWorkspaces = ( - environment: Environment | null -) => { - const [workspaces, setWorkspaces] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const fetchWorkspaces = useCallback(async () => { - if (!environment) return; - - setLoading(true); - setError(null); - - try { - const { environmentId, environmentApikey, environmentApiServiceUrl } = environment; - - if (!environmentApikey) { - setError("No API key configured for this environment. Workspaces cannot be fetched."); - setLoading(false); - return; - } - - if (!environmentApiServiceUrl) { - setError("No API service URL configured for this environment. Workspaces cannot be fetched."); - setLoading(false); - return; - } - - const data = await getEnvironmentWorkspaces( - environmentId, - environmentApikey, - environmentApiServiceUrl - ); - - setWorkspaces(data); - } catch (err) { - setError( - err instanceof Error - ? err.message - : "Failed to fetch workspaces" - ); - } finally { - setLoading(false); - } - }, [environment]); - - useEffect(() => { - if (environment) { - fetchWorkspaces(); - } - }, [environment, fetchWorkspaces]); - - const workspaceStats: WorkspaceStats = { - total: workspaces.length, - managed: 0, // logic to be added later - unmanaged: workspaces.length, // logic to be added later - apiKeyConfigured: !!environment?.environmentApikey, - apiServiceUrlConfigured: !!environment?.environmentApiServiceUrl, - }; - - return { - workspaces, - loading, - error, - refresh: fetchWorkspaces, - workspaceStats, - }; -}; diff --git a/client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironments.ts b/client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironments.ts deleted file mode 100644 index b125e4125c..0000000000 --- a/client/packages/lowcoder/src/pages/setting/environments/hooks/useEnvironments.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { useState, useEffect, useCallback } from 'react'; -import { Environment } from '../types/environment.types'; -import { getEnvironments } from '../services/environments.service'; - -/** - * Interface for the state managed by this hook - */ -interface EnvironmentsState { - environments: Environment[]; - loading: boolean; - error: string | null; -} - -/** - * Custom hook for fetching and managing environments data - * @returns Object containing environments data, loading state, error state, and refresh function - */ -export const useEnvironments = () => { - // Initialize state with loading true - const [state, setState] = useState({ - environments: [], - loading: true, - error: null, - }); - - /** - * Function to fetch environments from the API - */ - const fetchEnvironments = useCallback(async () => { - // Set loading state - setState(prev => ({ ...prev, loading: true, error: null })); - - try { - // Call the API service - const environments = await getEnvironments(); - - // Update state with fetched data - setState({ - environments, - loading: false, - error: null, - }); - } catch (error) { - // Handle error state - setState(prev => ({ - ...prev, - loading: false, - error: error instanceof Error ? error.message : 'An unknown error occurred', - })); - } - }, []); - - // Fetch environments on component mount - useEffect(() => { - fetchEnvironments(); - }, [fetchEnvironments]); - - // Return state values and refresh function - return { - environments: state.environments, - loading: state.loading, - error: state.error, - refresh: fetchEnvironments - }; -}; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspace.ts b/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspace.ts deleted file mode 100644 index 0520fea0d4..0000000000 --- a/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspace.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { useState, useEffect, useCallback } from "react"; -import { useHistory } from "react-router-dom"; -import { fetchWorkspaceById } from "../services/environments.service"; -import { Environment } from "../types/environment.types"; -import { Workspace } from "../types/workspace.types"; - -export const useWorkspace = ( - environment: Environment | null, - workspaceId: string -) => { - const [workspace, setWorkspace] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const history = useHistory(); - - const fetchWorkspace = useCallback(async () => { - if (!environment) return; - - setLoading(true); - setError(null); - - try { - const { environmentId, environmentApikey, environmentApiServiceUrl } = environment; - - if (!environmentApikey || !environmentApiServiceUrl) { - setError("Missing API key or service URL for this environment."); - setLoading(false); - return; - } - - const data = await fetchWorkspaceById( - environmentId, - workspaceId, - environmentApikey, - environmentApiServiceUrl - ); - - setWorkspace(data); - } catch (err) { - setError( - err instanceof Error - ? err.message - : "Failed to fetch workspace details" - ); - - // Optional: redirect to environment detail if workspace fetch fails - // history.push(`/home/settings/environments/${environment.environmentId}`); - } finally { - setLoading(false); - } - }, [environment, workspaceId, history]); - - useEffect(() => { - if (environment) { - fetchWorkspace(); - } - }, [environment, fetchWorkspace]); - - return { - workspace, - loading, - error, - refresh: fetchWorkspace, - }; -}; diff --git a/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaceApps.ts b/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaceApps.ts deleted file mode 100644 index 6d8718a34e..0000000000 --- a/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaceApps.ts +++ /dev/null @@ -1,123 +0,0 @@ -// hooks/useWorkspaceApps.ts -import { useState, useEffect, useCallback } from "react"; -import { getMergedWorkspaceApps, MergedAppsResult } from "../services/apps.service"; -import { connectManagedApp, unconnectManagedApp } from "../services/enterprise.service"; -import { Environment } from "../types/environment.types"; -import { App } from "../types/app.types"; - -interface AppState extends MergedAppsResult { - loading: boolean; - error: string | null; -} - -export const useWorkspaceApps = (environment: Environment | null, workspaceId: string) => { - const [state, setState] = useState({ - apps: [], - stats: { - total: 0, - published: 0, - managed: 0, - unmanaged: 0 - }, - loading: false, - error: null - }); - - const fetchApps = useCallback(async () => { - if (!environment || !workspaceId) return; - - setState(prev => ({ ...prev, loading: true, error: null })); - - try { - const { environmentId, environmentApikey, environmentApiServiceUrl } = environment; - - // Validate required configuration - if (!environmentApikey) { - setState(prev => ({ - ...prev, - loading: false, - error: "No API key configured for this environment. Apps cannot be fetched." - })); - return; - } - - if (!environmentApiServiceUrl) { - setState(prev => ({ - ...prev, - loading: false, - error: "No API service URL configured for this environment. Apps cannot be fetched." - })); - return; - } - - // Use the service function to get merged apps - const result = await getMergedWorkspaceApps( - workspaceId, - environmentId, - environmentApikey, - environmentApiServiceUrl - ); - - // Update state with result - setState({ - ...result, - loading: false, - error: null - }); - } catch (err) { - setState(prev => ({ - ...prev, - loading: false, - error: err instanceof Error ? err.message : "Failed to fetch apps" - })); - } - }, [environment, workspaceId]); - - useEffect(() => { - if (environment && workspaceId) { - fetchApps(); - } - }, [environment, workspaceId, fetchApps]); - - const toggleManagedStatus = async (app: App, checked: boolean) => { - try { - if (!environment) return false; - - if (checked) { - await connectManagedApp(environment.environmentId, app.name, app.applicationGid!); - } else { - await unconnectManagedApp(app.applicationGid!); - } - - // Optimistically update the state - setState(prev => { - // Update apps with the new managed status - const updatedApps = prev.apps.map(a => - a.applicationId === app.applicationId ? { ...a, managed: checked } : a - ); - - // Recalculate stats - const managedCount = updatedApps.filter(a => a.managed).length; - - return { - ...prev, - apps: updatedApps, - stats: { - ...prev.stats, - managed: managedCount, - unmanaged: updatedApps.length - managedCount - } - }; - }); - - return true; // Success indicator - } catch (err) { - return false; // Failure indicator - } - }; - - return { - ...state, - toggleManagedStatus, - }; -}; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaceDataSources.ts b/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaceDataSources.ts deleted file mode 100644 index 61a93fea0e..0000000000 --- a/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaceDataSources.ts +++ /dev/null @@ -1,110 +0,0 @@ -// hooks/useWorkspaceDataSources.ts -// Create this new file - -import { useState, useEffect, useCallback } from "react"; -import { getMergedWorkspaceDataSources } from "../services/datasources.service"; -import { connectManagedDataSource, unconnectManagedDataSource } from "../services/enterprise.service"; -import { Environment } from "../types/environment.types"; -import { DataSource } from "../types/datasource.types"; - -export const useWorkspaceDataSources = (environment: Environment | null, workspaceId: string) => { - const [dataSources, setDataSources] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [stats, setStats] = useState({ - total: 0, - types: 0, - managed: 0, - unmanaged: 0 - }); - - const fetchDataSources = useCallback(async () => { - if (!environment || !workspaceId) return; - - setLoading(true); - setError(null); - - try { - const { environmentId, environmentApikey, environmentApiServiceUrl } = environment; - - // Validate required configuration - if (!environmentApikey) { - setError("No API key configured for this environment. Data sources cannot be fetched."); - setLoading(false); - return; - } - - if (!environmentApiServiceUrl) { - setError("No API service URL configured for this environment. Data sources cannot be fetched."); - setLoading(false); - return; - } - - // Get merged data sources - const result = await getMergedWorkspaceDataSources( - workspaceId, - environmentId, - environmentApikey, - environmentApiServiceUrl - ); - - // Update state with results - setDataSources(result.dataSources); - setStats(result.stats); - setLoading(false); - } catch (err) { - setError(err instanceof Error ? err.message : "Failed to fetch data sources"); - setLoading(false); - } - }, [environment, workspaceId]); - - useEffect(() => { - if (environment && workspaceId) { - fetchDataSources(); - } - }, [environment, workspaceId, fetchDataSources]); - - const toggleManagedStatus = async (dataSource: DataSource, checked: boolean) => { - try { - if (!environment) return false; - - if (checked) { - await connectManagedDataSource(environment.environmentId, dataSource.name, dataSource.gid); - } else { - await unconnectManagedDataSource(dataSource.gid); - } - - // Optimistically update the state - setDataSources(prevDataSources => - prevDataSources.map(ds => - ds.id === dataSource.id ? { ...ds, managed: checked } : ds - ) - ); - - // Update stats - const updatedDataSources = dataSources.map(ds => - ds.id === dataSource.id ? { ...ds, managed: checked } : ds - ); - - const managedCount = updatedDataSources.filter(ds => ds.managed).length; - - setStats(prevStats => ({ - ...prevStats, - managed: managedCount, - unmanaged: updatedDataSources.length - managedCount - })); - - return true; // Success indicator - } catch (err) { - return false; // Failure indicator - } - }; - - return { - dataSources, - loading, - error, - stats, - toggleManagedStatus, - }; -}; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaces.ts b/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaces.ts deleted file mode 100644 index afd8565aef..0000000000 --- a/client/packages/lowcoder/src/pages/setting/environments/hooks/useWorkspaces.ts +++ /dev/null @@ -1,121 +0,0 @@ -// hooks/useWorkspaces.ts -import { useState, useEffect, useCallback } from "react"; -import { getMergedEnvironmentWorkspaces, MergedWorkspacesResult } from "../services/workspace.service"; -import { connectManagedWorkspace, unconnectManagedWorkspace } from "../services/enterprise.service"; -import { Environment } from "../types/environment.types"; -import { Workspace } from "../types/workspace.types"; - -interface WorkspacesState extends MergedWorkspacesResult { - loading: boolean; - error: string | null; -} - -export const useWorkspaces = (environment: Environment | null) => { - const [state, setState] = useState({ - workspaces: [], - stats: { - total: 0, - managed: 0, - unmanaged: 0, - }, - loading: false, - error: null - }); - - const fetchWorkspaces = useCallback(async () => { - if (!environment) return; - - setState(prev => ({ ...prev, loading: true, error: null })); - - try { - const { environmentId, environmentApikey, environmentApiServiceUrl } = environment; - - // Validate required configuration - if (!environmentApikey) { - setState(prev => ({ - ...prev, - loading: false, - error: "No API key configured for this environment. Workspaces cannot be fetched." - })); - return; - } - - if (!environmentApiServiceUrl) { - setState(prev => ({ - ...prev, - loading: false, - error: "No API service URL configured for this environment. Workspaces cannot be fetched." - })); - return; - } - - // Use the merged utility function - const result = await getMergedEnvironmentWorkspaces( - environmentId, - environmentApikey, - environmentApiServiceUrl - ); - - // Update state with result - setState({ - ...result, - loading: false, - error: null - }); - } catch (err) { - setState(prev => ({ - ...prev, - loading: false, - error: err instanceof Error ? err.message : "Failed to fetch workspaces" - })); - } - }, [environment]); - - useEffect(() => { - if (environment) { - fetchWorkspaces(); - } - }, [environment, fetchWorkspaces]); - - const toggleManagedStatus = async (workspace: Workspace, checked: boolean) => { - try { - if (!environment) return false; - - if (checked) { - await connectManagedWorkspace(environment.environmentId, workspace.name, workspace.gid!); - } else { - await unconnectManagedWorkspace(workspace.gid!); - } - - // Optimistically update the state - setState(prev => { - // Update workspaces with the new managed status - const updatedWorkspaces = prev.workspaces.map(w => - w.id === workspace.id ? { ...w, managed: checked } : w - ); - - // Recalculate stats - const managedCount = updatedWorkspaces.filter(w => w.managed).length; - - return { - ...prev, - workspaces: updatedWorkspaces, - stats: { - total: updatedWorkspaces.length, - managed: managedCount, - unmanaged: updatedWorkspaces.length - managedCount - } - }; - }); - - return true; // Success indicator - } catch (err) { - return false; // Failure indicator - } - }; - - return { - ...state, - toggleManagedStatus, - }; -}; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/utils/getMergedApps.ts b/client/packages/lowcoder/src/pages/setting/environments/utils/getMergedApps.ts deleted file mode 100644 index 0e09fb3dd0..0000000000 --- a/client/packages/lowcoder/src/pages/setting/environments/utils/getMergedApps.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { App } from '../types/app.types'; - - -export const getMergedApps = (standardApps: App[], managedApps: any[]): App[] => { - return standardApps.map((app) => ({ - ...app, - managed: managedApps.some((managedApp) => managedApp.appGid === app.applicationGid), - })); -}; diff --git a/client/packages/lowcoder/src/pages/setting/environments/utils/getMergedWorkspaces.ts b/client/packages/lowcoder/src/pages/setting/environments/utils/getMergedWorkspaces.ts deleted file mode 100644 index f661786bd9..0000000000 --- a/client/packages/lowcoder/src/pages/setting/environments/utils/getMergedWorkspaces.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Workspace } from "../types/workspace.types"; -import { ManagedOrg } from "../types/enterprise.types"; - - -export interface MergedWorkspaceResult { - merged: Workspace[]; - stats: { - total: number; - managed: number; - unmanaged: number; - }; -} - -export function getMergedWorkspaces( - standard: Workspace[], - managed: ManagedOrg[] -): MergedWorkspaceResult { - const merged = standard.map((ws) => ({ - ...ws, - managed: managed.some((m) => m.orgGid === ws.gid), - })); - - const managedCount = merged.filter((ws) => ws.managed).length; - - return { - merged, - stats: { - total: merged.length, - managed: managedCount, - unmanaged: merged.length - managedCount, - }, - }; -} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy