diff --git a/client/packages/lowcoder/src/constants/routesURL.ts b/client/packages/lowcoder/src/constants/routesURL.ts index 6bec5c190a..a675ec6495 100644 --- a/client/packages/lowcoder/src/constants/routesURL.ts +++ b/client/packages/lowcoder/src/constants/routesURL.ts @@ -24,6 +24,10 @@ export const AUDIT_LOG_DETAIL = "/setting/audit/:eventId/detail"; export const APP_USAGE_DASHBOARD = "/setting/app-usage"; export const APP_USAGE_DETAIL = "/setting/app-usage/:eventId/detail"; +export const ENVIRONMENT_SETTING = "/setting/environments"; +export const ENVIRONMENT_DETAIL = `${ENVIRONMENT_SETTING}/:environmentId`; +export const ENVIRONMENT_WORKSPACE_DETAIL = `${ENVIRONMENT_DETAIL}/workspaces/:workspaceId`; + export const OAUTH_PROVIDER_SETTING = "/setting/oauth-provider"; export const OAUTH_PROVIDER_DETAIL = "/setting/oauth-provider/detail"; @@ -120,3 +124,7 @@ export const buildSubscriptionSettingsLink = (subscriptionId: string, productId export const buildSubscriptionInfoLink = (productId: string) => `${SUBSCRIPTION_SETTING}/info/${productId}`; export const buildSupportTicketLink = (ticketId: string) => `${SUPPORT_URL}/details/${ticketId}`; + +export const buildEnvironmentId = (environmentId: string) => `${ENVIRONMENT_SETTING}/${environmentId}`; +export const buildEnvironmentWorkspaceId = (environmentId: string, workspaceId: string) => + `${ENVIRONMENT_SETTING}/${environmentId}/workspaces/${workspaceId}`; diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx new file mode 100644 index 0000000000..d16f52c242 --- /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 61a73fe24d..a1fb13e020 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 0000000000..09517016d8 --- /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 0000000000..2867171b0a --- /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 0000000000..3ff4f284de --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/components/DeployItemModal.tsx @@ -0,0 +1,165 @@ +// components/DeployItemModal.tsx +import React, { useState, useEffect } from 'react'; +import { Modal, Form, Select, Checkbox, Button, message, Spin, Input } from 'antd'; +import { Environment } from '../types/environment.types'; +import { DeployableItem, BaseStats, DeployableItemConfig } from '../types/deployable-item.types'; +import { useEnvironmentContext } from '../context/EnvironmentContext'; + +interface DeployItemModalProps { + visible: boolean; + item: T | null; + sourceEnvironment: Environment; + config: DeployableItemConfig; + onClose: () => void; + onSuccess?: () => void; +} + +function DeployItemModal({ + visible, + item, + sourceEnvironment, + config, + onClose, + onSuccess +}: DeployItemModalProps) { + const [form] = Form.useForm(); + const { environments, isLoadingEnvironments } = useEnvironmentContext(); + const [deploying, setDeploying] = useState(false); + + useEffect(() => { + if (visible) { + form.resetFields(); + } + }, [visible, form]); + + // Filter out source environment from target list + const targetEnvironments = environments.filter( + env => env.environmentId !== sourceEnvironment.environmentId + ); + + const handleDeploy = async () => { + if (!config.deploy?.enabled || !item) return; + + try { + const values = await form.validateFields(); + const targetEnv = environments.find(env => env.environmentId === values.targetEnvId); + + if (!targetEnv) { + message.error('Target environment not found'); + return; + } + + setDeploying(true); + + // Prepare parameters based on item type + const params = config.deploy.prepareParams(item, values, sourceEnvironment, targetEnv); + + // Execute deployment + await config.deploy.execute(params); + + message.success(`Successfully deployed ${item.name} to target environment`); + if (onSuccess) onSuccess(); + onClose(); + } catch (error) { + console.error('Deployment error:', error); + message.error(`Failed to deploy ${config.singularLabel.toLowerCase()}`); + } finally { + setDeploying(false); + } + }; + + return ( + + {isLoadingEnvironments ? ( +
+ +
+ ) : ( +
+ + + + + {/* Render dynamic fields based on config */} + {config.deploy?.fields.map(field => { + switch (field.type) { + case 'checkbox': + return ( + + {field.label} + + ); + case 'select': + return ( + + + + ); + case 'input': + return ( + + + + ); + default: + return null; + } + })} + + + + + +
+ )} +
+ ); +} + +export default DeployItemModal; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsList.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsList.tsx new file mode 100644 index 0000000000..63f8dda721 --- /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 0000000000..4e50a873c7 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/components/DeployableItemsTab.tsx @@ -0,0 +1,126 @@ +// components/DeployableItemsTab.tsx +import React from 'react'; +import { Card, Button, Divider, Alert, message } from 'antd'; +import { SyncOutlined } from '@ant-design/icons'; +import Title from 'antd/lib/typography/Title'; +import { Environment } from '../types/environment.types'; +import { DeployableItem, BaseStats, DeployableItemConfig } from '../types/deployable-item.types'; +import { useDeployableItems } from '../hooks/useDeployableItems'; +import DeployableItemsList from './DeployableItemsList'; + +interface DeployableItemsTabProps { + environment: Environment; + config: DeployableItemConfig; + additionalParams?: Record; + title?: string; +} + +function DeployableItemsTab({ + environment, + config, + additionalParams = {}, + title +}: DeployableItemsTabProps) { + // Use our generic hook with the provided config + const { + items, + stats, + loading, + error, + refreshing, + toggleManagedStatus, + refreshItems + } = useDeployableItems(config, environment, additionalParams); + + // Handle toggling managed status + const handleToggleManaged = async (item: T, checked: boolean) => { + const success = await toggleManagedStatus(item, checked); + + if (success) { + message.success(`${item.name} is now ${checked ? 'Managed' : 'Unmanaged'}`); + } else { + message.error(`Failed to toggle managed state for ${item.name}`); + } + + return success; + }; + + // Handle refresh button click + const handleRefresh = () => { + refreshItems(); + message.info(`Refreshing ${config.pluralLabel.toLowerCase()}...`); + }; + + // Check for missing required environment properties + const missingProps = config.requiredEnvProps.filter( + prop => !environment[prop as keyof Environment] + ); + + return ( + + {/* Header with refresh button */} +
+ + {title || `${config.pluralLabel} in this Environment`} + + +
+ + {/* Render stats using the config's renderStats function */} + {config.renderStats(stats)} + + + + {/* Show error if loading failed */} + {error && ( + + )} + + {/* Configuration warnings based on required props */} + {missingProps.length > 0 && !error && ( + + )} + + {/* Items List */} + +
+ ); +} + +export default DeployableItemsTab; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/EditEnvironmentModal.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/EditEnvironmentModal.tsx new file mode 100644 index 0000000000..5c09cc42b5 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/components/EditEnvironmentModal.tsx @@ -0,0 +1,159 @@ +import React, { useState, useEffect } from 'react'; +import { Modal, Form, Input, Select, Switch, Button, message } from 'antd'; +import { Environment } from '../types/environment.types'; + +const { Option } = Select; + +interface EditEnvironmentModalProps { + visible: boolean; + environment: Environment | null; + onClose: () => void; + onSave: (environmentId: string, data: Partial) => Promise; + loading?: boolean; +} + +const EditEnvironmentModal: React.FC = ({ + visible, + environment, + onClose, + onSave, + loading = false +}) => { + const [form] = Form.useForm(); + const [submitLoading, setSubmitLoading] = useState(false); + + // Initialize form with environment data when it changes + useEffect(() => { + if (environment) { + form.setFieldsValue({ + environmentName: environment.environmentName || '', + environmentDescription: environment.environmentDescription || '', + environmentType: environment.environmentType, + environmentApiServiceUrl: environment.environmentApiServiceUrl || '', + environmentFrontendUrl: environment.environmentFrontendUrl || '', + environmentNodeServiceUrl: environment.environmentNodeServiceUrl || '', + environmentApikey: environment.environmentApikey || '', + isMaster: environment.isMaster + }); + } + }, [environment, form]); + + const handleSubmit = async () => { + if (!environment) return; + + try { + const values = await form.validateFields(); + setSubmitLoading(true); + + await onSave(environment.environmentId, values); + onClose(); + } catch (error) { + if (error instanceof Error) { + console.error("Form validation or submission error:", error); + } + } finally { + setSubmitLoading(false); + } + }; + + return ( + + Cancel + , + + ]} + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ); +}; + +export default EditEnvironmentModal; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentScopedRoutes.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentScopedRoutes.tsx new file mode 100644 index 0000000000..e8a04d103b --- /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 0000000000..0208932d74 --- /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 0000000000..90b673f346 --- /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 0000000000..567e460a79 --- /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 0000000000..00721f0331 --- /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 0000000000..8ae0413202 --- /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 0000000000..87d15ae3b5 --- /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 0000000000..7084e94058 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/context/DeployModalContext.tsx @@ -0,0 +1,75 @@ +// context/DeployModalContext.tsx +import React, { createContext, useContext, useState } from 'react'; +import { DeployableItem, BaseStats, DeployableItemConfig } from '../types/deployable-item.types'; +import { Environment } from '../types/environment.types'; +import DeployItemModal from '../components/DeployItemModal'; + +interface DeployModalContextType { + openDeployModal: ( + item: T, + config: DeployableItemConfig, + sourceEnvironment: Environment, + onSuccess?: () => void + ) => void; +} + +const DeployModalContext = createContext(undefined); + +export const DeployModalProvider: React.FC<{children: React.ReactNode}> = ({ children }) => { + const [modalState, setModalState] = useState<{ + visible: boolean; + item: DeployableItem | null; + config: DeployableItemConfig | null; + sourceEnvironment: Environment | null; + onSuccess?: () => void; + }>({ + visible: false, + item: null, + config: null, + sourceEnvironment: null + }); + + const openDeployModal = ( + item: T, + config: DeployableItemConfig, + sourceEnvironment: Environment, + onSuccess?: () => void + ) => { + setModalState({ + visible: true, + item, + config, + sourceEnvironment, + onSuccess + }); + }; + + const closeDeployModal = () => { + setModalState(prev => ({ ...prev, visible: false })); + }; + + return ( + + {children} + + {modalState.config && modalState.sourceEnvironment && ( + + )} + + ); +}; + +export const useDeployModal = () => { + const context = useContext(DeployModalContext); + if (context === undefined) { + throw new Error('useDeployModal must be used within a DeployModalProvider'); + } + return context; +}; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/context/EnvironmentContext.tsx b/client/packages/lowcoder/src/pages/setting/environments/context/EnvironmentContext.tsx new file mode 100644 index 0000000000..f8120ff711 --- /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 0000000000..bb04cf54fd --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/hooks/useDeployableItems.ts @@ -0,0 +1,146 @@ +// hooks/useDeployableItems.ts +import { useState, useEffect, useCallback } from "react"; +import { DeployableItem, BaseStats, DeployableItemConfig } from "../types/deployable-item.types"; +import { Environment } from "../types/environment.types"; + +interface UseDeployableItemsState { + items: T[]; + stats: S; + loading: boolean; + error: string | null; + refreshing: boolean; +} + +export interface UseDeployableItemsResult { + items: T[]; + stats: S; + loading: boolean; + error: string | null; + refreshing: boolean; + toggleManagedStatus: (item: T, checked: boolean) => Promise; + refreshItems: () => Promise; +} + +export const useDeployableItems = ( + config: DeployableItemConfig, + environment: Environment | null, + additionalParams: Record = {} +): UseDeployableItemsResult => { + // Create a default empty stats object based on the config's calculateStats method + const createEmptyStats = (): S => { + return config.calculateStats([]) as S; + }; + + const [state, setState] = useState>({ + items: [], + stats: createEmptyStats(), + loading: false, + error: null, + refreshing: false + }); + + const fetchItems = useCallback(async () => { + if (!environment) return; + + // Check for required environment properties + const missingProps = config.requiredEnvProps.filter(prop => !environment[prop as keyof Environment]); + + if (missingProps.length > 0) { + setState(prev => ({ + ...prev, + loading: false, + error: `Missing required configuration: ${missingProps.join(', ')}` + })); + return; + } + + setState(prev => ({ ...prev, loading: true, error: null })); + + try { + // Call the fetchItems function from the config + const items = await config.fetchItems({ + environment, + ...additionalParams + }); + + // Calculate stats using the config's function + const stats = config.calculateStats(items); + + // Update state with items and stats + setState({ + items, + stats, + loading: false, + error: null, + refreshing: false + }); + } catch (err) { + setState(prev => ({ + ...prev, + loading: false, + refreshing: false, + error: err instanceof Error ? err.message : "Failed to fetch items" + })); + } + }, [environment, config]); + + useEffect(() => { + if (environment) { + fetchItems(); + } + }, [environment, fetchItems]); + + const toggleManagedStatus = async (item: T, checked: boolean): Promise => { + if (!config.enableManaged) return false; + if (!environment) return false; + + setState(prev => ({ ...prev, refreshing: true })); + + try { + // Call the toggleManaged function from the config + const success = await config.toggleManaged({ + item, + checked, + environment + }); + + if (success) { + // Optimistically update the state + setState(prev => { + // Update items with the new managed status + const updatedItems = prev.items.map(i => + i[config.idField] === item[config.idField] ? { ...i, managed: checked } : i + ); + + // Recalculate stats + const stats = config.calculateStats(updatedItems); + + return { + ...prev, + items: updatedItems, + stats, + refreshing: false + }; + }); + } else { + setState(prev => ({ ...prev, refreshing: false })); + } + + return success; + } catch (err) { + setState(prev => ({ ...prev, refreshing: false })); + return false; + } + }; + + const refreshItems = async (): Promise => { + setState(prev => ({ ...prev, refreshing: true })); + await fetchItems(); + }; + + return { + ...state, + toggleManagedStatus, + refreshItems + }; +}; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/services/apps.service.ts b/client/packages/lowcoder/src/pages/setting/environments/services/apps.service.ts new file mode 100644 index 0000000000..8c4c8785b9 --- /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 0000000000..b1fe06745f --- /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 0000000000..fe24330349 --- /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 0000000000..9fe5c96675 --- /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 0000000000..39eda02355 --- /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 0000000000..c56b978b55 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/services/workspace.service.ts @@ -0,0 +1,76 @@ +// services/workspacesService.ts (or wherever makes sense in your structure) +import { message } from "antd"; +import { getEnvironmentWorkspaces } from "./environments.service"; +import { getManagedWorkspaces } from "./enterprise.service"; +import { Workspace } from "../types/workspace.types"; +import { ManagedOrg } from "../types/enterprise.types"; + +export interface WorkspaceStats { + total: number; + managed: number; + unmanaged: number; +} + +export interface MergedWorkspacesResult { + workspaces: Workspace[]; + stats: WorkspaceStats; +} + +export async function getMergedEnvironmentWorkspaces( + environmentId: string, + apiKey: string, + apiServiceUrl: string +): Promise { + try { + // First, get regular workspaces + const regularWorkspaces = await getEnvironmentWorkspaces( + environmentId, + apiKey, + apiServiceUrl + ); + + // If no workspaces, return early with empty result + if (!regularWorkspaces.length) { + return { + workspaces: [], + stats: { + total: 0, + managed: 0, + unmanaged: 0 + } + }; + } + + // Only fetch managed workspaces if we have regular workspaces + let managedOrgs: ManagedOrg[] = []; + try { + managedOrgs = await getManagedWorkspaces(environmentId); + } catch (error) { + console.error("Failed to fetch managed workspaces:", error); + // Continue with empty managed list + } + + // Merge the workspaces + const mergedWorkspaces = regularWorkspaces.map(ws => ({ + ...ws, + managed: managedOrgs.some(org => org.orgGid === ws.gid) + })); + + // Calculate stats + const managedCount = mergedWorkspaces.filter(ws => ws.managed).length; + + return { + workspaces: mergedWorkspaces, + stats: { + total: mergedWorkspaces.length, + managed: managedCount, + unmanaged: mergedWorkspaces.length - managedCount + } + }; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Failed to fetch workspaces"; + message.error(errorMessage); + throw error; + } +} \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/types/app.types.ts b/client/packages/lowcoder/src/pages/setting/environments/types/app.types.ts new file mode 100644 index 0000000000..b3af252b58 --- /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 0000000000..f4f03072db --- /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 0000000000..ac223c63d9 --- /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 0000000000..e51a787403 --- /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 0000000000..39766c1eae --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/environments/types/environment.types.ts @@ -0,0 +1,17 @@ +/** + * Interface representing an Environment entity + */ +export interface Environment { + environmentId: string; + environmentName?: string; + environmentDescription?: string; + environmentIcon?: string; + environmentType: string; + environmentApiServiceUrl?: string; + environmentNodeServiceUrl?: string; + environmentFrontendUrl?: string; + environmentApikey: string; + isMaster: boolean; + createdAt: string; + updatedAt: string; +} \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/environments/types/query.types.ts b/client/packages/lowcoder/src/pages/setting/environments/types/query.types.ts new file mode 100644 index 0000000000..5d38385b0e --- /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 0000000000..6a1938bcc3 --- /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 0000000000..15f1e7dfbc --- /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 0000000000..b33685ab70 --- /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 c98d6540df..8eabd126aa 100644 --- a/client/packages/lowcoder/src/pages/setting/settingHome.tsx +++ b/client/packages/lowcoder/src/pages/setting/settingHome.tsx @@ -25,7 +25,7 @@ import { getUser } from "redux/selectors/usersSelectors"; import history from "util/history"; import { useParams } from "react-router-dom"; import { BrandingSetting } from "@lowcoder-ee/pages/setting/branding/BrandingSetting"; -import { Environments } from "@lowcoder-ee/pages/setting/environments/Environments"; +import Environments from "@lowcoder-ee/pages/setting/environments/Environments"; import { AppUsage } from "@lowcoder-ee/pages/setting/appUsage"; import { AuditLog } from "@lowcoder-ee/pages/setting/audit"; import { IdSourceHome } from "@lowcoder-ee/pages/setting/idSource"; pFad - Phonifier reborn

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

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


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy