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 61a73fe24..864e615e5 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 000000000..b125e4125 --- /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 000000000..785e694a3 --- /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 000000000..39766c1ea --- /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 c98d6540d..8eabd126a 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 6bec5c190..a675ec649 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 864e615e5..bea8d1250 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 000000000..713a2b251 --- /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 713a2b251..5af0a5c3d 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 000000000..1e7326057 --- /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 000000000..34fe98348 --- /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 bea8d1250..07d0086a4 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 5af0a5c3d..b0a6c7274 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 785e694a3..f75557933 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 34fe98348..6bc12a1c7 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 000000000..f347e79a0 --- /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 6bc12a1c7..8f040e828 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 000000000..a52deb488 --- /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 f347e79a0..000000000 --- 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 f75557933..39f728794 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 000000000..b3ec8fdda --- /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 39f728794..1205087d9 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 8f040e828..362ff097b 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 000000000..7a191784e --- /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 a52deb488..7e4d9f37e 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 1205087d9..fc34d51a0 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 000000000..cd5ec1ec8 --- /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 07d0086a4..2095dd835 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 000000000..ab6255b49 --- /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 000000000..8c5ea581f --- /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 000000000..803a8f341 --- /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 fc34d51a0..b7378fdad 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 000000000..984228d6e --- /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 362ff097b..360b4c5e7 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 c5357e957..051b51ef5 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 b7378fdad..0df2f3f2e 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 ab6255b49..b6218559a 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 000000000..be6677f21 --- /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 803a8f341..a8dacbfcd 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 0df2f3f2e..6898afd16 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 000000000..220986803 --- /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 6898afd16..ccff1975f 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 2095dd835..7d5eaa521 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 000000000..f8ffd4470 --- /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 000000000..c81500ae3 --- /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 360b4c5e7..1bd1bc05e 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 000000000..0f949037b --- /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 b6218559a..003103874 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 000000000..0520fea0d --- /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 003103874..7b3d9479a 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 000000000..d909a42be --- /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 000000000..d323b5777 --- /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 1bd1bc05e..489309a05 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 7e4d9f37e..000000000 --- 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 a8dacbfcd..000000000 --- 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 000000000..0ec3e4021 --- /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 000000000..1992ea287 --- /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 000000000..4ce934e9f --- /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 489309a05..b8e273e18 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 0ec3e4021..be539629b 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 1992ea287..0bfae6fce 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 b8e273e18..1e1867268 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 4ce934e9f..e51a78740 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 b3ec8fdda..15f1e7dfb 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 000000000..f661786bd --- /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 1e1867268..ca74b8145 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 f3cb0faf1..9d3aa2296 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 0bfae6fce..d156d5142 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 d156d5142..69099dc6e 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 7b3d9479a..64bf13ca9 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 8c5ea581f..8ac6fb31e 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 000000000..8f8a4bc8e --- /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 69099dc6e..8fd6d17fd 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 984228d6e..67c445f92 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 000000000..0e09fb3dd --- /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 ca74b8145..6266f617f 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 64bf13ca9..75fba87a6 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 c81500ae3..afd5c50da 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 6266f617f..b24d07aea 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 000000000..65b1aba94 --- /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 000000000..70883961c --- /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 b24d07aea..1a4d338ca 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 65b1aba94..244f37b0c 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 1a4d338ca..3beb11ce2 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 70883961c..3e2348bc8 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 000000000..afd8565ae --- /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 000000000..c56b978b5 --- /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 3beb11ce2..0a37c5672 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 75fba87a6..eaf2f24c3 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 000000000..b96d02d53 --- /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 d909a42be..6d8718a34 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 000000000..cf94f5cde --- /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 8ac6fb31e..40048f4eb 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 b96d02d53..9d8c8f923 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 000000000..408f5ae69 --- /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 cf94f5cde..8c4c8785b 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 000000000..e3a3a8fd8 --- /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 8fd6d17fd..e0bc1efd2 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 220986803..09dc193ac 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 eaf2f24c3..72ec132d9 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 000000000..e7d4d39c4 --- /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 000000000..29a41c095 --- /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 d323b5777..61a93fea0 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 e3a3a8fd8..1454a2cf4 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 0a37c5672..5268a5358 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 000000000..4e50a873c --- /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 000000000..21eee9f87 --- /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 000000000..bb04cf54f --- /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 000000000..bb303e89f --- /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 5268a5358..00ce1636a 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 a04687ffc..4918f4e82 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 000000000..bcf83c3e3 --- /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 cd5ec1ec8..6a1938bcc 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 72ec132d9..2c8348f8a 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 000000000..75f274709 --- /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 67c445f92..b3af252b5 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 2c8348f8a..9255472d8 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 09dc193ac..f4f03072d 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 e0bc1efd2..215e35586 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 215e35586..fe2433034 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 ccff1975f..5171750af 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 000000000..c10de3023 --- /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 000000000..a65352210 --- /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 9255472d8..82c4564d4 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 000000000..1efce6267 --- /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 5171750af..1ef501000 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 c10de3023..90889f191 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 a65352210..5d38385b0 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 000000000..3ff4f284d --- /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 4918f4e82..b207d98d5 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 f8ffd4470..4c75ccf68 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 75f274709..cc1d285d4 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 000000000..7084e9405 --- /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 bb303e89f..d0c7139c2 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 82c4564d4..eda5d5708 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 9fee65c4a..e84f07293 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 e7d4d39c4..7a5562578 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 29a41c095..000000000 --- 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 dd9b7f8ea..f0c45c73c 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 1454a2cf4..b1fe06745 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 1efce6267..8c274baca 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 90889f191..39eda0235 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 00ce1636a..e625f75c3 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 7d5eaa521..a1fb13e02 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 b0a6c7274..40ee18e07 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 4c75ccf68..e8a04d103 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 afd5c50da..dc5245b90 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 40ee18e07..cea220c12 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 000000000..5c09cc42b --- /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 1e7326057..9aec452ed 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 cea220c12..d34e0bd52 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 9aec452ed..5e9da24f6 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 5e9da24f6..0208932d7 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 b207d98d5..258739e2d 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 cc1d285d4..8cea1a0e1 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 21eee9f87..189ba44f1 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 d0c7139c2..a670c1837 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 bccb866cc..fedcf2508 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 258739e2d..001568b46 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 189ba44f1..c1e9c8e6c 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 a670c1837..941fced56 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 000000000..976c352da --- /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 001568b46..0009e549b 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 941fced56..ac223c63d 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 8cea1a0e1..90b673f34 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 fee0d9105..567e460a7 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 8c274baca..00721f033 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 bcf83c3e3..8ae041320 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 c1e9c8e6c..298888ccd 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 976c352da..5609f5b18 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 5609f5b18..b33685ab7 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 298888ccd..87d15ae3b 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 e76ba8145..f33bbe4c1 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 9850cb39f..d22af82e8 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 36ef2fc71..631dfa8e7 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 0009e549b..63f8dda72 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 631dfa8e7..d16f52c24 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 79dfa0372..2867171b0 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 40048f4eb..000000000 --- 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 9d8c8f923..000000000 --- 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 e84f07293..000000000 --- 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 7a5562578..000000000 --- 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 408f5ae69..000000000 --- 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 7a191784e..000000000 --- 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 244f37b0c..000000000 --- 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 9d3aa2296..000000000 --- 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 3e2348bc8..000000000 --- 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 8f8a4bc8e..000000000 --- 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 be539629b..000000000 --- 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 0923932c2..000000000 --- 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 0f949037b..000000000 --- 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 b125e4125..000000000 --- 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 0520fea0d..000000000 --- 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 6d8718a34..000000000 --- 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 61a93fea0..000000000 --- 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 afd8565ae..000000000 --- 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 0e09fb3dd..000000000 --- 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 f661786bd..000000000 --- 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, - }, - }; -}