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/EnvironmentDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx new file mode 100644 index 000000000..d16f52c24 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx @@ -0,0 +1,249 @@ +import React, {useState} from "react"; +import { useParams } from "react-router-dom"; +import { + Spin, + Typography, + Card, + Tag, + Tabs, + Alert, + Descriptions, + Dropdown, + Menu, + Button, + Breadcrumb, +} from "antd"; +import { + ReloadOutlined, + LinkOutlined, + ClusterOutlined, + TeamOutlined, + UserOutlined, + SyncOutlined, + EditOutlined, + EllipsisOutlined, + MoreOutlined, + HomeOutlined +} from "@ant-design/icons"; + +import { useEnvironmentContext } from "./context/EnvironmentContext"; +import { workspaceConfig } from "./config/workspace.config"; +import { userGroupsConfig } from "./config/usergroups.config"; +import DeployableItemsTab from "./components/DeployableItemsTab"; +import EditEnvironmentModal from "./components/EditEnvironmentModal"; +import { Environment } from "./types/environment.types"; +import history from "@lowcoder-ee/util/history"; + +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 { + environment, + isLoadingEnvironment, + error, + updateEnvironmentData + } = useEnvironmentContext(); + + + + const [isEditModalVisible, setIsEditModalVisible] = useState(false); + const [isUpdating, setIsUpdating] = useState(false); + + // Handle edit menu item click + const handleEditClick = () => { + setIsEditModalVisible(true); + }; + + // Handle modal close + const handleCloseModal = () => { + setIsEditModalVisible(false); + }; + + // Handle save environment + const handleSaveEnvironment = async (environmentId: string, data: Partial) => { + setIsUpdating(true); + try { + await updateEnvironmentData(environmentId, data); + handleCloseModal(); + } catch (error) { + console.error('Failed to update environment:', error); + } finally { + setIsUpdating(false); + } + }; + + // Dropdown menu for environment actions + const actionsMenu = ( + + } onClick={handleEditClick}> + Edit Environment + + {/* Add more menu items here if needed */} + + ); + debugger + + if (isLoadingEnvironment) { + return ( +
+ +
+ ); + } + + if (error || !environment) { + return ( + + ); + } + return ( +
+ + + history.push("/setting/environments")} + > + Environments + + + {environment.environmentName} + + + {/* Header with environment name and controls */} + {/* Header with environment name and controls */} +
+
+ + {environment.environmentName || "Unnamed Environment"} + + ID: {environment.environmentId} +
+
+ +
+
+ + {/* Basic Environment Information Card - improved responsiveness */} + 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 */} + + + {/* Using our new generic component with the workspace config */} + + + + User Groups + + } + key="userGroups" + > + {/* Using our new generic component with the user group config */} + + + + {/* Edit Environment Modal */} + {environment && ( + + )} +
+ ); +}; + +export default EnvironmentDetail; diff --git a/client/packages/lowcoder/src/pages/setting/environments/Environments.tsx b/client/packages/lowcoder/src/pages/setting/environments/Environments.tsx index 61a73fe24..a1fb13e02 100644 --- a/client/packages/lowcoder/src/pages/setting/environments/Environments.tsx +++ b/client/packages/lowcoder/src/pages/setting/environments/Environments.tsx @@ -1,3 +1,34 @@ -export function Environments() { - return <>; -} +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"; + +import { + ENVIRONMENT_SETTING, + ENVIRONMENT_DETAIL +} from "@lowcoder-ee/constants/routesURL"; + +/** + * Top-level Environments component that wraps all environment-related routes + * with the EnvironmentProvider for shared state management + */ +const Environments: React.FC = () => { + return ( + + + {/* Route that shows the list of environments */} + + + + + {/* All other routes under /environments/:envId */} + + + + + + ); +}; + +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 new file mode 100644 index 000000000..09517016d --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx @@ -0,0 +1,103 @@ +import React, { useState } from "react"; +import { Typography, Alert, Input, Button, Space, Empty } from "antd"; +import { SearchOutlined, ReloadOutlined } from "@ant-design/icons"; +import { useHistory } from "react-router-dom"; +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; + +/** + * Environment Listing Page Component + * Displays a table of environments + */ +const EnvironmentsList: React.FC = () => { + // Use the shared context instead of a local hook + const { + environments, + isLoadingEnvironments, + error, + } = useEnvironmentContext(); + + console.log("Environments:", environments); + + // State for search input + const [searchText, setSearchText] = useState(""); + + // Hook for navigation + 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) + ); + }); + + // Handle row click to navigate to environment detail + const handleRowClick = (record: Environment) => { + history.push(buildEnvironmentId(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 */} + {!isLoadingEnvironments && environments.length === 0 && !error ? ( + + ) : ( + /* Table component */ + + )} +
+ ); +}; + +export default EnvironmentsList; \ No newline at end of file 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..2867171b0 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx @@ -0,0 +1,261 @@ +import React, { useEffect, useState } from "react"; +import { useParams, useHistory } from "react-router-dom"; +import history from "@lowcoder-ee/util/history"; +import { + Spin, + Typography, + Card, + Row, + Col, + Tabs, + Alert, + Button, + Breadcrumb, + Space, + Tag, + Switch, + message, + Tooltip +} from "antd"; +import { + AppstoreOutlined, + DatabaseOutlined, + CodeOutlined, + HomeOutlined, + TeamOutlined, + ArrowLeftOutlined, + CloudUploadOutlined +} from "@ant-design/icons"; +import { useEnvironmentContext } from "./context/EnvironmentContext"; +import DeployableItemsTab from "./components/DeployableItemsTab"; +import { appsConfig } from "./config/apps.config"; +import { dataSourcesConfig } from "./config/data-sources.config"; +import { queryConfig } from "./config/query.config"; +import { useDeployableItems } from "./hooks/useDeployableItems"; +import { workspaceConfig } from "./config/workspace.config"; +import { useDeployModal } from "./context/DeployModalContext"; + +const { Title, Text } = Typography; +const { TabPane } = Tabs; + + + +const WorkspaceDetail: React.FC = () => { + + // Get parameters from URL + const { environmentId,workspaceId } = useParams<{ + workspaceId: string; + environmentId: string; + }>(); + const { + environment, + isLoadingEnvironment: envLoading, + error: envError, + } = useEnvironmentContext(); + + const {openDeployModal} = useDeployModal(); + + // Use our generic hook with the workspace config + const { + items: workspaces, + stats: workspaceStats, + loading: workspaceLoading, + error : workspaceError, + toggleManagedStatus, + refreshItems + } = useDeployableItems( + workspaceConfig, + environment, + { workspaceId } // Additional params if needed + ); + + // Find the current workspace in the items array + const workspace = workspaces.find(w => w.id === workspaceId); + + const handleToggleManaged = async (checked: boolean) => { + if (!workspace) return; + + const success = await toggleManagedStatus(workspace, checked); + if (success) { + message.success(`Workspace is now ${checked ? 'Managed' : 'Unmanaged'}`); + } else { + message.error('Failed to change managed status'); + } + }; + + if (envLoading || workspaceLoading ) { + return ( +
+ +
+ ); + } + + if (!environment || !workspace) { + return ( +
+ Workspace not found +
+ ) + } + + + return ( +
+ {/* Breadcrumb navigation */} + + + history.push("/setting/environments")} + > + Environments + + + + + history.push(`/setting/environments/${environmentId}`) + } + > + {environment.environmentName} + + + {workspace.name} + + + {/* Workspace header with details and actions */} + +
+ {/* Left section - Workspace info */} +
+ + {workspace.name} + +
+ + ID: {workspace.id} + + + {workspace.managed ? "Managed" : "Unmanaged"} + +
+
+ + {/* Right section - Actions */} + +
+ Managed: + +
+ + + + +
+
+
+ + {/* Tabs for Apps, Data Sources, and Queries */} + + + Apps + + } + key="apps" + > + + + + {/* Update the TabPane in WorkspaceDetail.tsx */} + + 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/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 new file mode 100644 index 000000000..63f8dda72 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsList.tsx @@ -0,0 +1,94 @@ +// components/DeployableItemsList.tsx +import React from 'react'; +import { Table, Tag, Empty, Spin, Switch, Space, Button, Tooltip } from 'antd'; +import { CloudUploadOutlined } from '@ant-design/icons'; +import history from '@lowcoder-ee/util/history'; +import { DeployableItem, BaseStats, DeployableItemConfig } from '../types/deployable-item.types'; +import { Environment } from '../types/environment.types'; +import { useDeployModal } from '../context/DeployModalContext'; + +interface DeployableItemsListProps { + items: T[]; + loading: boolean; + refreshing: boolean; + error?: string | null; + environment: Environment; + config: DeployableItemConfig; + onToggleManaged?: (item: T, checked: boolean) => Promise; + additionalParams?: Record; +} + +function DeployableItemsList({ + items, + loading, + refreshing, + error, + environment, + config, + onToggleManaged, + additionalParams = {} +}: DeployableItemsListProps) { + + const { openDeployModal } = useDeployModal(); + + // Handle row click for navigation + const handleRowClick = (item: T) => { + // Skip navigation if the route is just '#' (for non-navigable items) + if (config.buildDetailRoute({}) === '#') return; + + // Build the route using the config and navigate + const route = config.buildDetailRoute({ + environmentId: environment.environmentId, + itemId: item[config.idField] as string, + ...additionalParams + }); + + history.push(route); + }; + + // Get columns from config + const columns = config.getColumns({ + environment, + refreshing, + onToggleManaged, + openDeployModal, + additionalParams + }) + + + if (loading) { + return ( +
+ +
+ ); + } + + if (!items || items.length === 0 || error) { + return ( + + ); + } + + const hasNavigation = config.buildDetailRoute({}) !== '#'; + + return ( + ({ + onClick: hasNavigation ? () => handleRowClick(record) : undefined, + style: hasNavigation ? { cursor: 'pointer' } : undefined, + })} + /> + ); +} + +export default DeployableItemsList; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsTab.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsTab.tsx 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/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/EnvironmentScopedRoutes.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentScopedRoutes.tsx new file mode 100644 index 000000000..e8a04d103 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentScopedRoutes.tsx @@ -0,0 +1,43 @@ +import React, { useEffect } from "react"; +import { Switch, Route, useParams } from "react-router-dom"; +import { useEnvironmentContext } from "../context/EnvironmentContext"; +import EnvironmentDetail from "../EnvironmentDetail"; +import WorkspaceDetail from "../WorkspaceDetail"; +import { DeployModalProvider } from "../context/DeployModalContext"; + +import { + ENVIRONMENT_DETAIL, + ENVIRONMENT_WORKSPACE_DETAIL, +} from "@lowcoder-ee/constants/routesURL"; + +/** + * Component for routes scoped to a specific environment + * Uses the environment ID from the URL parameters to fetch the specific environment + */ +const EnvironmentScopedRoutes: React.FC = () => { + const { environmentId } = useParams<{ environmentId: string }>(); + const { refreshEnvironment } = useEnvironmentContext(); + + // When the environmentId changes, fetch the specific environment + useEffect(() => { + if (environmentId) { + refreshEnvironment(environmentId); + } + }, [environmentId, refreshEnvironment]); + + return ( + + + + + + + + + + + + ); +}; + +export default EnvironmentScopedRoutes; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentsTable.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentsTable.tsx new file mode 100644 index 000000000..0208932d7 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentsTable.tsx @@ -0,0 +1,128 @@ +import React from 'react'; +import { Table, Tag, Button, Tooltip, Space } from 'antd'; +import { EditOutlined, AuditOutlined} from '@ant-design/icons'; +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 => { + if (!type) return 'default'; + + switch (type.toUpperCase()) { + case 'DEV': return 'blue'; + case 'TEST': return 'orange'; + case 'PREPROD': return 'purple'; + case 'PROD': return 'green'; + default: return 'default'; + } + }; + + // Open audit page in new tab + const openAuditPage = (environmentId: string, e: React.MouseEvent) => { + e.stopPropagation(); // Prevent row click from triggering + const auditUrl = `/setting/audit?environmentId=${environmentId}`; + window.open(auditUrl, '_blank'); + }; + + + // Define table columns + const columns = [ + { + title: 'Name', + dataIndex: 'environmentName', + key: 'environmentName', + render: (name: string) => name || 'Unnamed Environment', + }, + { + title: 'Domain', + dataIndex: 'environmentFrontendUrl', + key: 'environmentFrontendUrl', + render: (url: string) => url || 'No URL', + }, + { + title: 'ID', + dataIndex: 'environmentId', + key: 'environmentId', + }, + { + title: 'Stage', + dataIndex: 'environmentType', + key: 'environmentType', + render: (type: string) => ( + + {type ? type.toUpperCase() : 'UNKNOWN'} + + ), + }, + { + title: 'Master', + dataIndex: 'isMaster', + key: 'isMaster', + render: (isMaster: boolean) => ( + + {isMaster ? 'Yes' : 'No'} + + ), + }, + { + title: 'Actions', + key: 'actions', + render: (_: any, record: Environment) => ( + e.stopPropagation()}> + + + + + ), + }, + ]; + + 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 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..90b673f34 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/config/apps.config.tsx @@ -0,0 +1,217 @@ +// config/apps.config.tsx +import React from 'react'; +import { Row, Col, Statistic, Tag, Space, Button, Tooltip } from 'antd'; +import { AppstoreOutlined, AuditOutlined } from '@ant-design/icons'; +import {DeployableItemConfig } from '../types/deployable-item.types'; +import { Environment } from '../types/environment.types'; +import { getMergedWorkspaceApps, deployApp } from '../services/apps.service'; +import { connectManagedApp, unconnectManagedApp } from '../services/enterprise.service'; +import { App, AppStats } from '../types/app.types'; + + +import { + createNameColumn, + createDescriptionColumn, + createPublishedColumn, + createManagedColumn, + createDeployColumn, + createAuditColumn, + createIdColumn +} from '../utils/columnFactories'; + +// Define AppStats interface if not already defined + + +export const appsConfig: DeployableItemConfig = { + // Basic info + type: 'apps', + singularLabel: 'App', + pluralLabel: 'Apps', + icon: , + idField: 'id', // or applicationId if you prefer to use that directly + + // Navigation + buildDetailRoute: () => '#', + + + // Configuration + requiredEnvProps: ['environmentApikey', 'environmentApiServiceUrl'], + + // Stats rendering + renderStats: (stats) => ( + + + } /> + + + } /> + + + } /> + + + } /> + + + ), + + // Stats calculation + calculateStats: (apps) => { + const total = apps.length; + const published = apps.filter(app => app.published).length; + const managed = apps.filter(app => app.managed).length; + + return { + total, + published, + managed, + unmanaged: total - managed + }; + }, + + // Table configuration + getColumns: ({ environment, refreshing, onToggleManaged, openDeployModal, additionalParams }) => { + const columns = [ + createIdColumn(), + createNameColumn(), + createPublishedColumn(), + ]; + + // Add managed column if enabled + if (appsConfig.enableManaged && onToggleManaged) { + columns.push(createManagedColumn(onToggleManaged, refreshing)); + } + + // Add deploy column if enabled + if (appsConfig.deploy?.enabled && openDeployModal) { + columns.push(createDeployColumn(appsConfig, environment, openDeployModal)); + } + + // Add audit column if enabled + if (appsConfig.audit?.enabled) { + columns.push(createAuditColumn(appsConfig, environment, additionalParams)); + } + + return columns; + }, + + columns: [ + { + title: 'Name', + dataIndex: 'name', + key: 'name', + }, + { + title: 'ID', + dataIndex: 'id', + key: 'id', + ellipsis: true, + }, + { + title: 'Role', + dataIndex: 'role', + key: 'role', + render: (role: string) => {role}, + }, + + { + title: 'Status', + dataIndex: 'status', + key: 'status', + render: (status: string) => ( + + {status} + + ), + } + ], + + // Deployment options + enableManaged: true, + + // Service functions + fetchItems: async ({ environment, workspaceId }) => { + if (!workspaceId) { + throw new Error("Workspace ID is required to fetch apps"); + } + + const result = await getMergedWorkspaceApps( + workspaceId, + environment.environmentId, + environment.environmentApikey, + environment.environmentApiServiceUrl! + ); + + // Map to ensure proper id field + return result.apps.map(app => ({ + ...app, + id: app.applicationId // Map applicationId to id for DeployableItem compatibility + })); + }, + audit: { + enabled: true, + icon: , + label: 'Audit', + tooltip: 'View audit logs for this app', + getAuditUrl: (item, environment, additionalParams) => { + console.log("Additional params:", additionalParams); + return `/setting/audit?environmentId=${environment.environmentId}&orgId=${item.id}&appId=${additionalParams?.workspaceId}&pageSize=100&pageNum=1` + } + }, + toggleManaged: async ({ item, checked, environment }) => { + try { + if (checked) { + await connectManagedApp(environment.environmentId, item.name, item.applicationGid!); + } else { + await unconnectManagedApp(item.applicationGid!); + } + return true; + } catch (error) { + console.error('Error toggling managed status:', error); + return false; + } + }, + // deployment options + + 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/config/data-sources.config.tsx b/client/packages/lowcoder/src/pages/setting/environments/config/data-sources.config.tsx new file mode 100644 index 000000000..567e460a7 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/config/data-sources.config.tsx @@ -0,0 +1,183 @@ +// config/data-sources.config.tsx +import React from 'react'; +import { Row, Col, Statistic, Tag, Space, Button, Tooltip } from 'antd'; +import { DatabaseOutlined, CloudUploadOutlined } from '@ant-design/icons'; +import { DeployableItemConfig } from '../types/deployable-item.types'; +import { DataSource, DataSourceStats } from '../types/datasource.types'; +import { 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 = { + // Basic info + type: 'dataSources', + singularLabel: 'Data Source', + pluralLabel: 'Data Sources', + icon: , + idField: 'id', + + // Navigation + buildDetailRoute: (params) => "#", + + // Configuration + requiredEnvProps: ['environmentApikey', 'environmentApiServiceUrl'], + + // Stats rendering + renderStats: (stats) => ( + + + } /> + + + } /> + + + } /> + + + ), + + // Stats calculation + calculateStats: (dataSources) => { + const total = dataSources.length; + const managed = dataSources.filter(ds => ds.managed).length; + + // Calculate counts by type + const byType = dataSources.reduce((acc, ds) => { + const type = ds.type || 'Unknown'; + acc[type] = (acc[type] || 0) + 1; + return acc; + }, {} as Record); + + return { + total, + managed, + unmanaged: total - managed, + byType + }; + }, + + // Table configuration - Customize based on your existing UI + columns: [ + { + title: 'Name', + dataIndex: 'name', + key: 'name', + }, + { + title: 'Type', + dataIndex: 'type', + key: 'type', + render: (type: string) => ( + {type || 'Unknown'} + ), + }, + { + title: 'Database', + key: 'database', + render: (_, record: DataSource) => ( + {record.datasourceConfig?.database || 'N/A'} + ), + }, + { + title: 'Status', + dataIndex: 'datasourceStatus', + key: 'status', + render: (status: string) => ( + + {status} + + ), + }, + ], + + // Deployment options + enableManaged: true, + + // Service functions + fetchItems: async ({ environment, workspaceId }) => { + if (!workspaceId) { + throw new Error("Workspace ID is required to fetch data sources"); + } + + const result = await getMergedWorkspaceDataSources( + workspaceId, + environment.environmentId, + environment.environmentApikey, + environment.environmentApiServiceUrl! + ); + + return result.dataSources; + }, + + getColumns: ({ environment, refreshing, onToggleManaged, openDeployModal, additionalParams }) => { + const columns = [ + createNameColumn(), + createTypeColumn(), + createDatabaseColumn(), + createDatasourceStatusColumn(), + ]; + + // Add managed column if enabled + if (dataSourcesConfig.enableManaged && onToggleManaged) { + columns.push(createManagedColumn(onToggleManaged, refreshing)); + } + + // Add deploy column if enabled + if (dataSourcesConfig.deploy?.enabled && openDeployModal) { + columns.push(createDeployColumn(dataSourcesConfig, environment, openDeployModal)); + } + + // Add audit column if enabled + if (dataSourcesConfig.audit?.enabled) { + columns.push(createAuditColumn(dataSourcesConfig, environment, additionalParams)); + } + + return columns; + }, + + + toggleManaged: async ({ item, checked, environment }) => { + try { + if (checked) { + await connectManagedDataSource(environment.environmentId, item.name, item.gid); + } else { + await unconnectManagedDataSource(item.gid); + } + return true; + } catch (error) { + console.error('Error toggling managed status:', error); + return false; + } + }, + deploy: { + enabled: true, + fields: [ + { + name: 'updateDependenciesIfNeeded', + label: 'Update Dependencies If Needed', + type: 'checkbox', + defaultValue: false + } + ], + prepareParams: (item: DataSource, values: any, sourceEnv: Environment, targetEnv: Environment) => { + 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/config/query.config.tsx b/client/packages/lowcoder/src/pages/setting/environments/config/query.config.tsx new file mode 100644 index 000000000..00721f033 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/config/query.config.tsx @@ -0,0 +1,178 @@ +// config/query.config.tsx +import React from 'react'; +import { Row, Col, Statistic, Tag } from 'antd'; +import { ApiOutlined } from '@ant-design/icons'; +import { DeployableItemConfig } from '../types/deployable-item.types'; +import { Query } from '../types/query.types'; +import { connectManagedQuery, unconnectManagedQuery } from '../services/enterprise.service'; +import { getMergedWorkspaceQueries, deployQuery } from '../services/query.service'; +import { Environment } from '../types/environment.types'; + +import { + createNameColumn, + createCreatorColumn, + createDateColumn, + createQueryTypeColumn, + createManagedColumn, + createDeployColumn, + createAuditColumn +} from '../utils/columnFactories'; + +// Define QueryStats interface +export interface QueryStats { + total: number; + managed: number; + unmanaged: number; +} + +export const queryConfig: DeployableItemConfig = { + // Basic info + type: 'queries', + singularLabel: 'Query', + pluralLabel: 'Queries', + icon: , + idField: 'id', + + // Navigation - queries don't have detail pages in this implementation + buildDetailRoute: () => '#', + + // Configuration + requiredEnvProps: ['environmentApikey', 'environmentApiServiceUrl'], + + // Stats rendering + renderStats: (stats) => ( + + + } /> + + + } /> + + + } /> + + + ), + + // Stats calculation + calculateStats: (queries) => { + const total = queries.length; + const managed = queries.filter(q => q.managed).length; + + return { + total, + managed, + unmanaged: total - managed + }; + }, + columns: [ + { + title: 'Name', + dataIndex: 'name', + key: 'name', + }, + { + title: 'Type', + dataIndex: 'type', + key: 'type', + render: (type: string) => ( + {type || 'Unknown'} + ), + }, + { + title: 'Database', + key: 'database', + render: (_, record: Query) => ( + {record.datasourceConfig?.database || 'N/A'} + ), + }, + { + title: 'Status', + dataIndex: 'datasourceStatus', + key: 'status', + render: (status: string) => ( + + {status} + + ), + }, + ], + getColumns: ({ environment, refreshing, onToggleManaged, openDeployModal, additionalParams }) => { + const columns = [ + createNameColumn(), + createCreatorColumn(), + createDateColumn('createTime', 'Creation Date'), + createQueryTypeColumn(), + ]; + + // Add managed column if enabled + if (queryConfig.enableManaged && onToggleManaged) { + columns.push(createManagedColumn(onToggleManaged, refreshing)); + } + + // Add deploy column if enabled + if (queryConfig.deploy?.enabled && openDeployModal) { + columns.push(createDeployColumn(queryConfig, environment, openDeployModal)); + } + + // Add audit column if enabled + if (queryConfig.audit?.enabled) { + columns.push(createAuditColumn(queryConfig, environment, additionalParams)); + } + + return columns; + }, + + // Deployment options + enableManaged: true, + + // Service functions + fetchItems: async ({ environment, workspaceId }) => { + if (!workspaceId) { + throw new Error("Workspace ID is required to fetch queries"); + } + + const result = await getMergedWorkspaceQueries( + workspaceId, + environment.environmentId, + environment.environmentApikey, + environment.environmentApiServiceUrl! + ); + + return result.queries; + }, + + toggleManaged: async ({ item, checked, environment }) => { + try { + if (checked) { + await connectManagedQuery(environment.environmentId, item.name, item.gid); + } else { + await unconnectManagedQuery(item.gid); + } + return true; + } catch (error) { + console.error('Error toggling managed status:', error); + return false; + } + }, + deploy: { + enabled: true, + 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/config/usergroups.config.tsx b/client/packages/lowcoder/src/pages/setting/environments/config/usergroups.config.tsx new file mode 100644 index 000000000..8ae041320 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/config/usergroups.config.tsx @@ -0,0 +1,169 @@ +// config/usergroups.config.tsx +import React from 'react'; +import { Row, Col, Statistic, Tag, Badge } from 'antd'; +import { TeamOutlined, UserOutlined } from '@ant-design/icons'; +import { getEnvironmentUserGroups } from '../services/environments.service'; +import { UserGroup, UserGroupStats } from '../types/userGroup.types'; +import { DeployableItemConfig } from '../types/deployable-item.types'; +import { + createUserGroupNameColumn, + createGroupIdColumn, + createUserCountColumn, + createDateColumn, + createGroupTypeColumn, + createAuditColumn +} from '../utils/columnFactories'; + +const formatDate = (timestamp: number): string => { + if (!timestamp) return 'N/A'; + const date = new Date(timestamp); + return `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`; +}; + + +export const userGroupsConfig: DeployableItemConfig = { + // Basic info + type: 'userGroups', + singularLabel: 'User Group', + pluralLabel: 'User Groups', + icon: , + idField: 'id', + + // Navigation - No navigation for user groups, provide a dummy function + buildDetailRoute: () => '#', + + // Configuration + requiredEnvProps: ['environmentApikey', 'environmentApiServiceUrl'], + + // Stats rendering - Custom for user groups + renderStats: (stats) => ( + + + } /> + + + } /> + + + } /> + + + ), + + // Stats calculation - Custom for user groups + calculateStats: (userGroups) => { + const total = userGroups.length; + const totalUsers = userGroups.reduce( + (sum, group) => sum + (group.stats?.userCount ?? 0), + 0 + ); + const adminUsers = userGroups.reduce( + (sum, group) => sum + (group.stats?.adminUserCount ?? 0), + 0 + ); + + return { + total, + managed: 0, // User groups don't have managed/unmanaged state + unmanaged: 0, // User groups don't have managed/unmanaged state + totalUsers, + adminUsers + }; + }, + + // Table configuration + columns: [ + { + title: 'Name', + dataIndex: 'groupName', + key: 'groupName', + render: (name: string, record: UserGroup) => ( +
+ {record.groupName} + {record.allUsersGroup && ( + All Users + )} + {record.devGroup && ( + Dev + )} +
+ ), + }, + { + title: 'ID', + dataIndex: 'groupId', + key: 'groupId', + ellipsis: true, + }, + { + title: 'Users', + key: 'userCount', + render: (_, record: UserGroup) => ( +
+ + + ({record.stats.adminUserCount} admin{record.stats.adminUserCount !== 1 ? 's' : ''}) + +
+ ), + }, + { + title: 'Created', + key: 'createTime', + render: (_, record: UserGroup) => formatDate(record.createTime), + }, + { + title: 'Type', + key: 'type', + render: (_, record: UserGroup) => { + if (record.allUsersGroup) return Global; + if (record.devGroup) return Dev; + if (record.syncGroup) return Sync; + return Standard; + }, + } + ], + + // No managed status for user groups + enableManaged: false, + + getColumns: ({ environment, additionalParams }) => { + const columns = [ + createGroupIdColumn(), + createUserGroupNameColumn(), + + createUserCountColumn(), + createDateColumn('createTime', 'Created'), + createGroupTypeColumn(), + ]; + + // User groups aren't managed, so we don't add the managed column + + // Add audit column if enabled + if (userGroupsConfig.audit?.enabled) { + columns.push(createAuditColumn(userGroupsConfig, environment, additionalParams)); + } + + return columns; + }, + // Service functions + fetchItems: async ({ environment }) => { + const userGroups = await getEnvironmentUserGroups( + environment.environmentId, + environment.environmentApikey, + environment.environmentApiServiceUrl! + ); + + // Map the required properties to satisfy DeployableItem interface + return userGroups.map(group => ({ + ...group, + id: group.groupId, // Map groupId to id + name: group.groupName // Map groupName to name + })); + }, + + // Dummy function for toggleManaged (will never be called since enableManaged is false) + toggleManaged: async () => { + return false; + } +}; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/config/workspace.config.tsx b/client/packages/lowcoder/src/pages/setting/environments/config/workspace.config.tsx new file mode 100644 index 000000000..87d15ae3b --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/config/workspace.config.tsx @@ -0,0 +1,157 @@ +// config/workspace.config.tsx +import React from 'react'; +import { Row, Col, Statistic, Tag } from 'antd'; +import { ClusterOutlined, AuditOutlined } from '@ant-design/icons'; +import { Workspace, WorkspaceStats, DeployableItemConfig } from '../types/deployable-item.types'; +import { 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 + type: 'workspaces', + singularLabel: 'Workspace', + pluralLabel: 'Workspaces', + icon: , + idField: 'id', + + // Navigation + buildDetailRoute: (params) => buildEnvironmentWorkspaceId(params.environmentId, params.itemId), + + // Configuration + requiredEnvProps: ['environmentApikey', 'environmentApiServiceUrl'], + + // Stats rendering + renderStats: (stats) => ( + +
+ } /> + + + } /> + + + } /> + + + ), + + // Stats calculation + calculateStats: (workspaces) => { + const total = workspaces.length; + const managed = workspaces.filter(w => w.managed).length; + return { + total, + managed, + unmanaged: total - managed + }; + }, + + // Original columns for backward compatibility + columns: [ + { + title: 'Name', + dataIndex: 'name', + key: 'name', + }, + { + title: 'ID', + dataIndex: 'id', + key: 'id', + ellipsis: true, + }, + { + title: 'Role', + dataIndex: 'role', + key: 'role', + render: (role: string) => {role}, + }, + { + title: 'Creation Date', + key: 'creationDate', + render: (_, record: Workspace) => { + if (!record.creationDate) return 'N/A'; + const date = new Date(record.creationDate); + return `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`; + }, + }, + { + title: 'Status', + dataIndex: 'status', + key: 'status', + render: (status: string) => ( + + {status} + + ), + } + ], + + // New getColumns method + getColumns: ({ environment, refreshing, onToggleManaged, additionalParams }) => { + const columns = [ + createIdColumn(), + createNameColumn(), + createRoleColumn(), + createManagedColumn(), + createDateColumn('creationDate', 'Creation Date'), + createStatusColumn() + + ]; + + + // Add audit column if enabled + if (workspaceConfig.audit?.enabled) { + columns.push(createAuditColumn(workspaceConfig, environment, additionalParams)); + } + + return columns; + }, + + // Enable managed functionality + enableManaged: true, + + // Fetch function + fetchItems: async ({ environment }) => { + const result = await getMergedEnvironmentWorkspaces( + environment.environmentId, + environment.environmentApikey, + environment.environmentApiServiceUrl! + ); + return result.workspaces; + }, + + // Toggle managed status + toggleManaged: async ({ item, checked, environment }) => { + try { + if (checked) { + await connectManagedWorkspace(environment.environmentId, item.name, item.gid!); + } else { + await unconnectManagedWorkspace(item.gid!); + } + return true; + } catch (error) { + console.error('Error toggling managed status:', error); + return false; + } + }, + + // Audit configuration + audit: { + enabled: true, + icon: , + label: 'Audit', + tooltip: 'View audit logs for this workspace', + getAuditUrl: (item, environment) => + `/setting/audit?environmentId=${environment.environmentId}&orgId=${item.id}&pageSize=100&pageNum=1` + } +}; \ 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/context/EnvironmentContext.tsx b/client/packages/lowcoder/src/pages/setting/environments/context/EnvironmentContext.tsx new file mode 100644 index 000000000..f8120ff71 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/context/EnvironmentContext.tsx @@ -0,0 +1,156 @@ +import React, { + createContext, + useContext, + useEffect, + useState, + useCallback, + ReactNode, +} from "react"; +import { message } from "antd"; +import { + getEnvironmentById, + getEnvironments, + updateEnvironment, +} from "../services/environments.service"; +import { Environment } from "../types/environment.types"; + +interface EnvironmentContextState { + // Environment data + environment: Environment | null; + environments: Environment[]; + + // Loading states + isLoadingEnvironment: boolean; + isLoadingEnvironments: boolean; + + // Error state + error: string | null; + + // Functions + refreshEnvironment: (envId?: string) => Promise; + refreshEnvironments: () => Promise; + updateEnvironmentData: (envId: string, data: Partial) => Promise; +} + +const EnvironmentContext = createContext(undefined); + +export const useEnvironmentContext = () => { + const context = useContext(EnvironmentContext); + if (!context) { + throw new Error( + "useEnvironmentContext must be used within an EnvironmentProvider" + ); + } + return context; +}; + +interface ProviderProps { + children: ReactNode; +} + +export const EnvironmentProvider: React.FC = ({ + children, +}) => { + // State for environment data + const [environment, setEnvironment] = useState(null); + const [environments, setEnvironments] = useState([]); + + // Loading states + const [isLoadingEnvironment, setIsLoadingEnvironment] = useState(false); + const [isLoadingEnvironments, setIsLoadingEnvironments] = useState(true); + + // Error state + const [error, setError] = useState(null); + + // Function to fetch a specific environment by ID + const fetchEnvironment = useCallback(async (environmentId?: string) => { + // Only fetch if we have an environment ID + if (!environmentId) { + setEnvironment(null); + return; + } + + setIsLoadingEnvironment(true); + setError(null); + + try { + const data = await getEnvironmentById(environmentId); + console.log("Environment data:", data); + setEnvironment(data); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : "Environment not found or failed to load"; + setError(errorMessage); + } finally { + setIsLoadingEnvironment(false); + } + }, []); + + // Function to fetch all environments + const fetchEnvironments = useCallback(async () => { + setIsLoadingEnvironments(true); + setError(null); + + try { + const data = await getEnvironments(); + console.log("Environments data:", data); + setEnvironments(data); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : "Failed to load environments list"; + setError(errorMessage); + } finally { + setIsLoadingEnvironments(false); + } + }, []); + + // Function to update an environment +// Function to update an environment +const updateEnvironmentData = useCallback(async ( + environmentId: string, + data: Partial +): Promise => { + try { + const updatedEnv = await updateEnvironment(environmentId, data); + + // Show success message + message.success("Environment updated successfully"); + + // Refresh the environments list + fetchEnvironments(); + + // If we're viewing a single environment and it's the one we updated, + // refresh that environment data as well + if (environment && environment.environmentId === environmentId) { + fetchEnvironment(environmentId); + } + + return updatedEnv; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : "Failed to update environment"; + message.error(errorMessage); + throw err; + } +}, [environment, fetchEnvironment, fetchEnvironments]); + + // Initial data loading - just fetch environments list + useEffect(() => { + fetchEnvironments(); + }, [fetchEnvironments]); + + // Create the context value + const value: EnvironmentContextState = { + environment, + environments, + isLoadingEnvironment, + isLoadingEnvironments, + error, + refreshEnvironment: fetchEnvironment, + refreshEnvironments: fetchEnvironments, + updateEnvironmentData, + }; + + return ( + + {children} + + ); +}; \ 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/services/apps.service.ts b/client/packages/lowcoder/src/pages/setting/environments/services/apps.service.ts new file mode 100644 index 000000000..8c4c8785b --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/services/apps.service.ts @@ -0,0 +1,132 @@ +// services/appService.ts +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; + published: number; + managed: number; + unmanaged: number; +} + +export interface MergedAppsResult { + apps: App[]; + 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) => ({ + ...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; + } +} + + + +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 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..b1fe06745 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/services/datasources.service.ts @@ -0,0 +1,161 @@ +// services/dataSources.service.ts +import axios from 'axios'; +import { message } from "antd"; +import { DataSource, DataSourceWithMeta } 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; +} + +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, + 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<{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.data; + } catch (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: 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 +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 regularDataSourcesWithMeta = await getWorkspaceDataSources( + workspaceId, + apiKey, + apiServiceUrl + ); + + // If no data sources, return early with empty result + if (!regularDataSourcesWithMeta.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(regularDataSourcesWithMeta, 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 async function deployDataSource(params: DeployDataSourceParams): Promise { + try { + 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 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..fe2433034 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/services/enterprise.service.ts @@ -0,0 +1,279 @@ +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 + * @param apiServiceUrl - API service URL for the environment + * @param environmentId - ID of the environment + * + * + */ + +export async function getManagedWorkspaces( + environmentId: string, + +): Promise { + if (!environmentId) { + throw new Error("Missing environmentId"); + } + + try { + const res = await axios.get(`/api/plugins/enterprise/org/list`); + 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, + orgName: string, + org_gid: string, // ✅ not optional + orgTags: string[] = [], +) { + if (!environmentId || !orgName || !org_gid) { + throw new Error("Missing required params to connect org"); + } + + try { + const payload = { + environment_id: environmentId, + org_name: orgName, + org_tags: orgTags, + org_gid, + }; + + 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"; + 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(orgGid: string) { + if (!orgGid) { + throw new Error("Missing orgGid to unconnect workspace"); + } + + try { + 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"; + message.error(errorMsg); + 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_gid: string, + app_tags: string[] = [] +) { + try { + const payload = { + environment_id: environmentId, + app_name, + 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; + } +} + +// 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 { + const payload = { + environment_id: environmentId, + name, + datasource_gid: datasourceGid, + }; + + + await axios.post(`/api/plugins/enterprise/datasource`, payload); + } 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; + } +}; + + + + +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 new file mode 100644 index 000000000..9fe5c9667 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts @@ -0,0 +1,461 @@ +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"; +import {App} from "../types/app.types"; +import { DataSourceWithMeta } from '../types/datasource.types'; +import { Query, QueryResponse } from "../types/query.types"; + + + + +export async function updateEnvironment( + environmentId: string, + environmentData: Partial +): Promise { + if (!environmentId) { + throw new Error("Missing environmentId"); + } + + try { + // Convert frontend model to API model + const payload = { + environment_description: environmentData.environmentDescription || "", + environment_icon: environmentData.environmentIcon || "", + environment_name: environmentData.environmentName || "", + environment_apikey: environmentData.environmentApikey || "", + environment_type: environmentData.environmentType || "", + environment_api_service_url: environmentData.environmentApiServiceUrl || "", + environment_frontend_url: environmentData.environmentFrontendUrl || "", + environment_node_service_url: environmentData.environmentNodeServiceUrl || "", + isMaster: environmentData.isMaster || false + }; + + const res = await axios.put(`/api/plugins/enterprise/environments`, payload, { + params: { environmentId } + }); + + return res.data; + } catch (err) { + const errorMsg = err instanceof Error ? err.message : "Failed to update environment"; + message.error(errorMsg); + throw err; + } +} + + + +/** + * 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?environmentId=${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; + } +} + +/* ================================================================================ + +=============================== 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 = { + Authorization: `Bearer ${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; + } +} + + + +/* ================================================================================ + +=============================== 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; + } +} + + + + +/* ================================================================================ + +=============================== 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; + } +} + +/* ================================================================================ + +=============================== WorkSpace Apps ============================ */ + + + +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 []; + } + + 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'; + message.error(errorMessage); + 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<{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.data ; + } catch (error) { + // Handle and transform error + const errorMessage = error instanceof Error ? error.message : 'Failed to fetch data sources'; + 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 + }); + debugger + // Check if response is valid + 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, + // 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..39eda0235 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/services/query.service.ts @@ -0,0 +1,87 @@ +/** + * 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'; +export interface MergedQueriesResult { + queries: Query[]; + stats: { + total: number; + managed: number; + unmanaged: number; + }; + } + + export interface DeployQueryParams { + envId: string; + targetEnvId: string; + queryId: string; + updateDependenciesIfNeeded?: boolean; + } + + + export async function getMergedWorkspaceQueries( + workspaceId: string, + environmentId: string, + apiKey: string, + apiServiceUrl: string + ): Promise { + try { + // 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) => { + 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, + stats: { + total, + managed, + unmanaged: total - managed + } + }; + + } catch (error) { + 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 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 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..b3af252b5 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/types/app.types.ts @@ -0,0 +1,33 @@ +import { DeployableItem, BaseStats } from "./deployable-item.types"; + +export interface App extends DeployableItem { + 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; + managed?: boolean; + id: string + } + + export interface AppStats extends BaseStats { + published: number + } \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/types/datasource.types.ts b/client/packages/lowcoder/src/pages/setting/environments/types/datasource.types.ts new file mode 100644 index 000000000..f4f03072d --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/types/datasource.types.ts @@ -0,0 +1,47 @@ +/** + * Represents a DataSource configuration + */ + +import { DeployableItem, BaseStats } from "./deployable-item.types"; +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 extends DeployableItem { + id: string; + createdBy: string; + gid: string; + name: string; + type: string; + organizationId: string; + creationSource: number; + datasourceStatus: string; + pluginDefinition: any | null; + createTime: number; + datasourceConfig: DataSourceConfig; + managed?: boolean; + } + + /** + * Represents a DataSource with additional metadata + */ + export interface DataSourceWithMeta { + datasource: DataSource; + edit: boolean; + creatorName: string; + } + + export interface DataSourceStats extends BaseStats { + byType: Record; // Count by each type + } \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/types/deployable-item.types.ts b/client/packages/lowcoder/src/pages/setting/environments/types/deployable-item.types.ts new file mode 100644 index 000000000..ac223c63d --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/types/deployable-item.types.ts @@ -0,0 +1,105 @@ +// types/deployable-item.types.ts +import { ReactNode } from 'react'; +import { Environment } from './environment.types'; +import { ColumnType } from 'antd/lib/table'; + + +// Base interface for all deployable items +export interface AuditConfig { + enabled: boolean; + icon?: React.ReactNode; + label?: string; + tooltip?: string; + getAuditUrl: (item: any, environment: Environment, additionalParams?: Record) => string; +} +export interface DeployableItem { + id: string; + name: string; + managed?: boolean; + [key: string]: any; // Allow for item-specific properties +} + +// Workspace specific implementation +export interface Workspace extends DeployableItem { + id: string; + name: string; + role?: string; + creationDate?: number; + status?: string; + managed?: boolean; + gid?: string; +} + +// Stats interface that can be extended for specific item types +// Base interface for stats +export interface BaseStats { + total: number; + managed: number; + unmanaged: number; + [key: string]: any; +} +export interface WorkspaceStats extends BaseStats {} + + +export interface DeployField { + name: string; + 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 + type: string; // e.g., 'workspaces' + singularLabel: string; // e.g., 'Workspace' + pluralLabel: string; // e.g., 'Workspaces' + + // UI elements + icon: ReactNode; // Icon to use in stats + + // Navigation + buildDetailRoute: (params: Record) => string; + + // Configuration + requiredEnvProps: string[]; // Required environment properties + + // Customization + idField: string; // Field to use as the ID (e.g., 'id') + + // Stats + renderStats: (stats: S) => ReactNode; + calculateStats: (items: T[]) => S; + + // Original columns (will be deprecated) + columns: ColumnType[]; + + // New method to generate columns + getColumns: (params: { + environment: Environment; + refreshing: boolean; + onToggleManaged?: (item: T, checked: boolean) => Promise; + openDeployModal?: (item: T, config: DeployableItemConfig, environment: Environment) => void; + additionalParams?: Record; + }) => ColumnType[]; + + // Add audit configuration + audit?: AuditConfig; + + + + // Deployable configuration + enableManaged: boolean; + + // Service functions + fetchItems: (params: { environment: Environment, [key: string]: any }) => Promise; + toggleManaged: (params: { item: T; checked: boolean; environment: Environment }) => Promise; + + deploy?: { + enabled: boolean; + fields: DeployField[]; + prepareParams: (item: T, values: any, sourceEnv: Environment, targetEnv: Environment) => any; + execute: (params: any) => Promise; + }; +} \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/types/enterprise.types.ts b/client/packages/lowcoder/src/pages/setting/environments/types/enterprise.types.ts new file mode 100644 index 000000000..e51a78740 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/types/enterprise.types.ts @@ -0,0 +1,12 @@ +import { Workspace } from "../types/workspace.types"; +export interface ManagedOrg { + orgGid: string; + environmentId: string; + orgName: string; + orgTags: string[]; + createdAt: string; + updatedAt: string; + } + + + export type MergedWorkspace = Workspace & { managed: boolean }; 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/environments/types/query.types.ts b/client/packages/lowcoder/src/pages/setting/environments/types/query.types.ts new file mode 100644 index 000000000..5d38385b0 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/types/query.types.ts @@ -0,0 +1,63 @@ +// types/query.types.ts +import { DeployableItem, BaseStats } 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; + managed?: boolean; + }; +} + +export interface Query extends DeployableItem { + id: string; + gid: string; + organizationId: string; + name: string; + libraryQueryDSL: LibraryQueryDSL; + createTime: number; + creatorName: string; +} + +export interface QueryStats extends BaseStats { + total: number; + managed: number; + unmanaged: number; +} + +export interface QueryResponse { + code: number; + message: string; + data: Query[]; + pageNum: number; + pageSize: number; + total: number; + success: boolean; +} \ 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..6a1938bcc --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/types/userGroup.types.ts @@ -0,0 +1,34 @@ +/** + * Represents a User Group entity in an environment +*/ + +import { DeployableItem, BaseStats } from "./deployable-item.types"; + +export interface UserGroup extends DeployableItem { + groupId: string; + groupGid: string; + groupName: string; + allUsersGroup: boolean; + visitorRole: string; + createTime: number; + dynamicRule: any; + stats: { + users: string[]; + userCount: number; + adminUserCount: number; + }; + syncDelete: boolean; + devGroup: boolean; + syncGroup: boolean; + id: string; + name: string; + } + + + /** + * Statistics for User Groups + */ +export interface UserGroupStats extends BaseStats { + totalUsers: number; + adminUsers: number; +} \ 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..15f1e7dfb --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/types/workspace.types.ts @@ -0,0 +1,16 @@ +/** + * 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; + managed?: boolean; + } \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/utils/columnFactories.tsx b/client/packages/lowcoder/src/pages/setting/environments/utils/columnFactories.tsx new file mode 100644 index 000000000..b33685ab7 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/utils/columnFactories.tsx @@ -0,0 +1,309 @@ +// utils/columnFactories.tsx +import React from 'react'; +import { Tag, Space, Switch, Button, Tooltip, Badge} from 'antd'; +import { CloudUploadOutlined, AuditOutlined } from '@ant-design/icons'; +import { ColumnType } from 'antd/lib/table'; +import { DeployableItem, DeployableItemConfig, BaseStats } from '../types/deployable-item.types'; +import { Environment } from '../types/environment.types'; + +// Base columns for workspace +export function createNameColumn(): ColumnType { + return { + title: 'Name', + dataIndex: 'name', + key: 'name', + }; +} + +export function createIdColumn(): ColumnType { + return { + title: 'ID', + dataIndex: 'id', + key: 'id', + ellipsis: true, + }; +} + +export function createRoleColumn(): ColumnType { + return { + title: 'Role', + dataIndex: 'role', + key: 'role', + render: (role: string) => {role}, + }; +} + +export function createDateColumn( + dateField: string, + title: string +): ColumnType { + return { + title: title, + key: dateField, + render: (_, record: any) => { + if (!record[dateField]) return 'N/A'; + const date = new Date(record[dateField]); + return `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`; + }, + }; +} + +export function createStatusColumn(): ColumnType { + return { + title: 'Status', + dataIndex: 'status', + key: 'status', + render: (status: string) => ( + + {status} + + ), + }; +} + +// Feature columns +export function createManagedColumn( + onToggleManaged?: (item: T, checked: boolean) => Promise, + refreshing: boolean = false +): ColumnType { + return { + title: 'Managed', + key: 'managed', + render: (_, record: T) => ( + + + {record.managed ? 'Managed' : 'Unmanaged'} + + {onToggleManaged && ( + { + e.stopPropagation(); // Stop row click event + onToggleManaged(record, checked); + }} + onChange={() => {}} + /> + )} + + ), + }; +} + +export function createAuditColumn( + config: DeployableItemConfig, + environment: Environment, + additionalParams: Record = {} +): ColumnType { + return { + title: 'Audit', + key: 'audit', + render: (_, record: T) => { + const openAuditPage = (e: React.MouseEvent) => { + e.stopPropagation(); + if (config.audit?.getAuditUrl) { + const auditUrl = config.audit.getAuditUrl(record, environment, additionalParams); + window.open(auditUrl, '_blank'); + } + }; + + return ( + + + + ); + }, + }; +} + + +export function createDescriptionColumn(): ColumnType { + return { + title: 'Description', + dataIndex: 'description', + key: 'description', + ellipsis: true, + }; +} + + +export function createDeployColumn( + config: DeployableItemConfig, + environment: Environment, + openDeployModal: (item: T, config: DeployableItemConfig, environment: Environment) => void +): ColumnType { + return { + title: 'Actions', + key: 'actions', + render: (_, record: T) => { + // Check if the item is managed + const isManaged = record.managed === true; + + return ( + + + + + + ); + }, + }; +} + +// App-specific columns +export function createPublishedColumn(): ColumnType { + return { + title: 'Status', + dataIndex: 'published', + key: 'published', + render: (published: boolean) => ( + + {published ? 'Published' : 'Unpublished'} + + ), + }; +} + +// Data Source specific columns +export function createTypeColumn(): ColumnType { + return { + title: 'Type', + dataIndex: 'type', + key: 'type', + render: (type: string) => ( + {type || 'Unknown'} + ), + }; +} + +export function createDatabaseColumn(): ColumnType { + return { + title: 'Database', + key: 'database', + render: (_, record: T) => ( + {record.datasourceConfig?.database || 'N/A'} + ), + }; +} + +export function createDatasourceStatusColumn(): ColumnType { + return { + title: 'Status', + dataIndex: 'datasourceStatus', + key: 'status', + render: (status: string) => ( + + {status} + + ), + }; +} + + +// Query-specific column factories to add to columnFactories.tsx +export function createCreatorColumn(): ColumnType { + return { + title: 'Creator', + dataIndex: 'creatorName', + key: 'creatorName', + }; +} + +export function createQueryTypeColumn(): ColumnType { + return { + title: 'Query Type', + key: 'queryType', + render: (_, record: T) => { + const queryType = record.libraryQueryDSL?.query?.compType || 'Unknown'; + return {queryType}; + }, + }; +} + +export function createUserGroupNameColumn(): ColumnType { + return { + title: 'Name', + dataIndex: 'groupName', + key: 'groupName', + render: (name: string, record: T) => ( +
+ {record.groupName} + {record.allUsersGroup && ( + All Users + )} + {record.devGroup && ( + Dev + )} +
+ ), + }; +} + +export function createGroupIdColumn(): ColumnType { + return { + title: 'ID', + dataIndex: 'groupId', + key: 'groupId', + ellipsis: true, + }; +} + +export function createUserCountColumn(): ColumnType { + return { + title: 'Users', + key: 'userCount', + render: (_, record: T) => ( +
+ + + ({record.stats?.adminUserCount || 0} admin{(record.stats?.adminUserCount || 0) !== 1 ? 's' : ''}) + +
+ ), + }; +} + +export function createGroupTypeColumn(): ColumnType { + return { + title: 'Type', + key: 'type', + render: (_, record: T) => { + if (record.allUsersGroup) return Global; + if (record.devGroup) return Dev; + if (record.syncGroup) return Sync; + return Standard; + }, + }; +} \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/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";