diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx
index 041997779..245e12971 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx
+++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentDetail.tsx
@@ -10,6 +10,10 @@ import {
Button,
Tag,
Result,
+ Row,
+ Col,
+ Statistic,
+ Progress,
} from "antd";
import {
LinkOutlined,
@@ -21,6 +25,11 @@ import {
CloseCircleOutlined,
ExclamationCircleOutlined,
SyncOutlined,
+ CloudServerOutlined,
+ UserOutlined,
+ SafetyOutlined,
+ CrownOutlined,
+ ApiOutlined,
} from "@ant-design/icons";
import { useSingleEnvironmentContext } from "./context/SingleEnvironmentContext";
@@ -31,10 +40,12 @@ import history from "@lowcoder-ee/util/history";
import WorkspacesTab from "./components/WorkspacesTab";
import UserGroupsTab from "./components/UserGroupsTab";
import EnvironmentHeader from "./components/EnvironmentHeader";
+import StatsCard from "./components/StatsCard";
import ModernBreadcrumbs from "./components/ModernBreadcrumbs";
import { getEnvironmentTagColor } from "./utils/environmentUtils";
+import { formatAPICalls, getAPICallsStatusColor } from "./services/license.service";
import ErrorComponent from './components/ErrorComponent';
-const { TabPane } = Tabs;
+import { Level1SettingPageContent } from "../styled";
/**
* Environment Detail Page Component
@@ -124,33 +135,80 @@ const EnvironmentDetail: React.FC = () => {
);
}
- const breadcrumbItems = [
+ // Stats data for the cards
+ const statsData = [
{
- key: 'environments',
- title: (
+ title: "Type",
+ value: environment.environmentType || "Unknown",
+ icon: ,
+ color: getEnvironmentTagColor(environment.environmentType)
+ },
+ {
+ title: "Status",
+ value: environment.isLicensed ? "Licensed" : "Unlicensed",
+ icon: environment.isLicensed ? : ,
+ color: environment.isLicensed ? "#52c41a" : "#ff4d4f"
+ },
+ {
+ title: "API Key",
+ value: environment.environmentApikey ? "Configured" : "Not Set",
+ icon: ,
+ color: environment.environmentApikey ? "#1890ff" : "#faad14"
+ },
+ {
+ title: "Master Env",
+ value: environment.isMaster ? "Yes" : "No",
+ icon: ,
+ color: environment.isMaster ? "#722ed1" : "#8c8c8c"
+ }
+ ];
+
+ const tabItems = [
+ {
+ key: 'workspaces',
+ label: (
- Environments
+ Workspaces
),
- onClick: () => history.push("/setting/environments")
+ children:
},
{
- key: 'currentEnvironment',
- title: environment.environmentName
+ key: 'userGroups',
+ label: (
+
+ User Groups
+
+ ),
+ children:
}
];
return (
-
+
+ {/* Breadcrumbs */}
+
+
{/* Environment Header Component */}
+ {/* Stats Cards Row */}
+
+ {statsData.map((stat, index) => (
+
+
+
+ ))}
+
+
{/* Basic Environment Information Card */}
{
"No domain set"
)}
-
-
- {environment.environmentType}
-
+
+
+ {environment.environmentId}
+
{(() => {
@@ -196,29 +251,178 @@ const EnvironmentDetail: React.FC = () => {
case 'licensed':
return } color="green" style={{ borderRadius: '4px' }}>Licensed;
case 'unlicensed':
- return } color="red" style={{ borderRadius: '4px' }}>Not Licensed;
+ return } color="orange" style={{ borderRadius: '4px' }}>License Needed;
case 'error':
- return } color="orange" style={{ borderRadius: '4px' }}>License Error;
+ return } color="orange" style={{ borderRadius: '4px' }}>Setup Required;
default:
return Unknown ;
}
})()}
-
- {environment.environmentApikey ? (
- Configured
- ) : (
- Not Configured
- )}
-
-
- {environment.isMaster ? "Yes" : "No"}
+
+ {environment.createdAt ? new Date(environment.createdAt).toLocaleDateString() : "Unknown"}
- {/* Modern Breadcrumbs navigation */}
-
+ history.push('/setting/environments')
+ },
+ {
+ key: 'current',
+ title: environment.environmentName || "Environment Detail"
+ }
+ ]}
+ />
+ {/* Detailed License Information Card - only show for licensed environments with details */}
+ {environment.isLicensed && environment.licenseDetails && (
+
+
+ License Details
+
+ }
+ style={{
+ marginBottom: "24px",
+ borderRadius: '4px',
+ border: '1px solid #f0f0f0'
+ }}
+ className="license-details-card"
+ >
+
+ {/* API Calls Status */}
+
+
+ (
+
+ {value?.toLocaleString()}
+
+ )}
+ prefix={ }
+ />
+
+
+
+ {environment.licenseDetails.apiCallsUsage || 0}% used
+
+
+
+
+
+ {/* Total License Limit */}
+
+
+ value?.toLocaleString()}
+ prefix={ }
+ />
+
+ {environment.licenseDetails.eeLicenses.length} License{environment.licenseDetails.eeLicenses.length !== 1 ? 's' : ''}
+
+
+
+
+ {/* Enterprise Edition Status */}
+
+
+ (
+ : }
+ >
+ {value}
+
+ )}
+ />
+
+
+
+
+ {/* License Details */}
+
+
+
+ License Information
+
+
+
+ {environment.licenseDetails.eeLicenses.map((license, index) => (
+
+
+
+
+ {license.customerName}
+
+
+
+ ID: {license.customerId}
+
+
+ UUID: {license.uuid.substring(0, 8)}...
+
+
+ {license.apiCallsLimit.toLocaleString()} calls
+
+
+
+ ))}
+
+
+
+ )}
+
{/* Tabs for Workspaces and User Groups */}
{
onChange={setActiveTab}
className="modern-tabs"
type="line"
- >
-
- Workspaces
-
- }
- key="workspaces"
- >
-
-
-
-
- User Groups
-
- }
- key="userGroups"
- >
-
-
-
+ items={tabItems}
+ />
{/* Edit Environment Modal */}
{environment && (
@@ -261,7 +444,7 @@ const EnvironmentDetail: React.FC = () => {
loading={isUpdating}
/>
)}
-
+
);
};
diff --git a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx
index 7b041b81f..36be33166 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx
+++ b/client/packages/lowcoder/src/pages/setting/environments/EnvironmentsList.tsx
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react";
-import { Alert, Empty, Spin, Card } from "antd";
+import { Alert, Empty, Spin, Card, Row, Col } from "antd";
import { SyncOutlined, CloudServerOutlined } from "@ant-design/icons";
import { AddIcon, Search, TacoButton } from "lowcoder-design";
import { useHistory } from "react-router-dom";
@@ -9,6 +9,7 @@ import { fetchEnvironments } from "redux/reduxActions/enterpriseActions";
import { Environment } from "./types/environment.types";
import EnvironmentsTable from "./components/EnvironmentsTable";
import CreateEnvironmentModal from "./components/CreateEnvironmentModal";
+import StatsCard from "./components/StatsCard";
import { buildEnvironmentId } from "@lowcoder-ee/constants/routesURL";
import { createEnvironment } from "./services/environments.service";
import { getEnvironmentTagColor } from "./utils/environmentUtils";
@@ -107,37 +108,6 @@ const EnvironmentsList: React.FC = () => {
return ;
};
- // Stat card component
- const StatCard = ({ title, value, color }: { title: string; value: number; color: string }) => (
-
-
-
-
- {getEnvironmentIcon(title)}
-
-
-
- );
-
// Filter environments based on search text
const filteredEnvironments = environments.filter((env) => {
const searchLower = searchText.toLowerCase();
@@ -201,75 +171,65 @@ const EnvironmentsList: React.FC = () => {
>
Refresh
- setIsCreateModalVisible(true)}>
- New Environment
+ }
+ onClick={() => setIsCreateModalVisible(true)}
+ >
+ Add Environment
- {/* Environment Type Statistics */}
- {!isLoading && environments.length > 0 && (
-
-
- {environmentStats.map(([type, count]) => (
-
-
-
- ))}
-
-
- )}
+ {/* Environment Statistics Cards */}
+
+
+ {environmentStats.map(([type, count]) => (
+
+
+
+ ))}
+
+
- {/* Error handling */}
{error && (
)}
- {/* Loading, empty state or table */}
- {isLoading ? (
-
-
-
- ) : environments.length === 0 && !error ? (
-
- ) : filteredEnvironments.length === 0 ? (
-
- ) : (
- /* Table component */
+ )}
+
+ {(filteredEnvironments.length > 0 || isLoading) && (
)}
-
- {/* Results counter when searching */}
- {searchText && filteredEnvironments.length !== environments.length && (
-
- Showing {filteredEnvironments.length} of {environments.length} environments
-
- )}
- {/* Create Environment Modal */}
setIsCreateModalVisible(false)}
diff --git a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx
index 6ed3a1427..8ca8d3294 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx
+++ b/client/packages/lowcoder/src/pages/setting/environments/WorkspaceDetail.tsx
@@ -4,6 +4,8 @@ import {
Spin,
Typography,
Tabs,
+ Row,
+ Col,
} from "antd";
import { messageInstance } from "lowcoder-design/src/components/GlobalInstances";
import {
@@ -12,6 +14,8 @@ import {
CodeOutlined,
HomeOutlined,
TeamOutlined,
+ CloudServerOutlined,
+ CheckCircleOutlined,
} from "@ant-design/icons";
// Use the context hooks
@@ -25,9 +29,9 @@ import DataSourcesTab from "./components/DataSourcesTab";
import QueriesTab from "./components/QueriesTab";
import ModernBreadcrumbs from "./components/ModernBreadcrumbs";
import WorkspaceHeader from "./components/WorkspaceHeader";
+import StatsCard from "./components/StatsCard";
import ErrorComponent from "./components/ErrorComponent";
-
-const { TabPane } = Tabs;
+import { Level1SettingPageContent } from "../styled";
const WorkspaceDetail: React.FC = () => {
// Use the context hooks
@@ -35,7 +39,6 @@ const WorkspaceDetail: React.FC = () => {
const { workspace, isLoading, error, toggleManagedStatus } = useWorkspaceContext();
const { openDeployModal } = useDeployModal();
-
const [isToggling, setIsToggling] = useState(false);
// Handle toggle managed status
@@ -58,7 +61,17 @@ const WorkspaceDetail: React.FC = () => {
if (isLoading) {
return (
-
+
+
+ Loading workspace details...
+
+
);
}
@@ -98,13 +111,53 @@ const WorkspaceDetail: React.FC = () => {
}
];
+ const tabItems = [
+ {
+ key: 'apps',
+ label: (
+
+ Apps
+
+ ),
+ children: (
+
+ )
+ },
+ {
+ key: 'dataSources',
+ label: (
+
+ Data Sources
+
+ ),
+ children: (
+
+ )
+ },
+ {
+ key: 'queries',
+ label: (
+
+ Queries
+
+ ),
+ children: (
+
+ )
+ }
+ ];
+
return (
-
+
{/* New Workspace Header */}
{
onDeploy={() => openDeployModal(workspace, workspaceConfig, environment)}
/>
+ {/* Stats Cards Row */}
+
+
+ : }
+ color={workspace.managed ? "#52c41a" : "#faad14"}
+ />
+
+
+ }
+ color="#1890ff"
+ />
+
+
+ }
+ color="#722ed1"
+ />
+
+
+ }
+ color="#52c41a"
+ />
+
+
+
{/* Modern Breadcrumbs navigation */}
@@ -122,29 +211,9 @@ const WorkspaceDetail: React.FC = () => {
defaultActiveKey="apps"
className="modern-tabs"
type="line"
- >
- Apps} key="apps">
-
-
-
- Data Sources} key="dataSources">
-
-
- Queries} key="queries">
-
-
-
-
-
+ items={tabItems}
+ />
+
);
};
diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/ContactLowcoderModal.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/ContactLowcoderModal.tsx
index 145b08e98..34ab0526f 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/components/ContactLowcoderModal.tsx
+++ b/client/packages/lowcoder/src/pages/setting/environments/components/ContactLowcoderModal.tsx
@@ -100,7 +100,7 @@ const ContactLowcoderModal: React.FC = ({
width={800}
centered
style={{ top: 20 }}
- bodyStyle={{ padding: '24px' }}
+ styles={{ body: { padding: '24px' } }}
>
{/* Environment Context Section */}
= ({
background: '#fafafa',
border: '1px solid #f0f0f0'
}}
- bodyStyle={{ padding: '16px' }}
+ styles={{ body: { padding: '16px' } }}
>
diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/CreateEnvironmentModal.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/CreateEnvironmentModal.tsx
index 421a001e4..8a6132c0a 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/components/CreateEnvironmentModal.tsx
+++ b/client/packages/lowcoder/src/pages/setting/environments/components/CreateEnvironmentModal.tsx
@@ -192,9 +192,9 @@ const CreateEnvironmentModal: React.FC = ({
diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/EditEnvironmentModal.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/EditEnvironmentModal.tsx
index 593e4be2e..3b90bc826 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/components/EditEnvironmentModal.tsx
+++ b/client/packages/lowcoder/src/pages/setting/environments/components/EditEnvironmentModal.tsx
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
-import { Modal, Form, Input, Select, Switch, Button, Tooltip } from 'antd';
+import { Modal, Form, Input, Select, Switch, Button, Alert, Tooltip } from 'antd';
import { useSelector } from 'react-redux';
import { selectMasterEnvironment, selectHasMasterEnvironment } from 'redux/selectors/enterpriseSelectors';
import { Environment } from '../types/environment.types';
@@ -191,7 +191,13 @@ const EditEnvironmentModal: React.FC = ({
-
+
);
diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentHeader.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentHeader.tsx
index 0a999129e..43ecb5fc0 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentHeader.tsx
+++ b/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentHeader.tsx
@@ -2,7 +2,7 @@ import React from 'react';
import { Button, Tag, Typography, Row, Col } from 'antd';
import { EditOutlined, CloudServerOutlined } from '@ant-design/icons';
import { Environment } from '../types/environment.types';
-import { getEnvironmentTagColor, getEnvironmentHeaderGradient } from '../utils/environmentUtils';
+import { getEnvironmentTagColor } from '../utils/environmentUtils';
const { Title, Text } = Typography;
@@ -24,11 +24,11 @@ const EnvironmentHeader: React.FC = ({
className="environment-header"
style={{
marginBottom: "24px",
- background: getEnvironmentHeaderGradient(environment.environmentType),
+ background: '#fff',
padding: '20px 24px',
borderRadius: '8px',
- color: 'white',
- boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
+ border: '1px solid #f0f0f0',
+ boxShadow: '0 2px 8px rgba(0,0,0,0.06)'
}}
>
@@ -36,35 +36,50 @@ const EnvironmentHeader: React.FC = ({
-
+
{environment.environmentName || "Unnamed Environment"}
-
+
ID: {environment.environmentId}
{environment.environmentType}
{environment.isMaster && (
-
+
Master
)}
+ {environment.isLicensed === false && (
+
+ Unlicensed
+
+ )}
@@ -73,14 +88,10 @@ const EnvironmentHeader: React.FC = ({
}
onClick={onEditClick}
- type="default"
- size="large"
+ type="primary"
style={{
- background: 'white',
- color: '#1890ff',
- borderColor: 'white',
- fontWeight: 500,
- boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
+ fontWeight: '500',
+ borderRadius: '4px'
}}
>
Edit Environment
diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentsTable.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentsTable.tsx
index 509cb509a..2336c8d5d 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentsTable.tsx
+++ b/client/packages/lowcoder/src/pages/setting/environments/components/EnvironmentsTable.tsx
@@ -1,8 +1,9 @@
import React from 'react';
-import { Table, Tag, Button, Tooltip, Space, Card, Row, Col, Typography, Avatar, Spin, Alert } from 'antd';
-import { EditOutlined, AuditOutlined, LinkOutlined, EnvironmentOutlined, StarFilled, CloudServerOutlined, CheckCircleOutlined, CloseCircleOutlined, ExclamationCircleOutlined, SyncOutlined } from '@ant-design/icons';
+import { Table, Tag, Button, Tooltip, Space, Card, Row, Col, Typography, Avatar, Spin, Alert, Progress } from 'antd';
+import { EditOutlined, AuditOutlined, LinkOutlined, EnvironmentOutlined, StarFilled, CloudServerOutlined, CheckCircleOutlined, CloseCircleOutlined, ExclamationCircleOutlined, SyncOutlined, ApiOutlined } from '@ant-design/icons';
import { Environment } from '../types/environment.types';
import { getEnvironmentTagColor, formatEnvironmentType } from '../utils/environmentUtils';
+import { getAPICallsStatusColor } from '../services/license.service';
const { Text, Title } = Typography;
@@ -54,29 +55,29 @@ const EnvironmentsTable: React.FC = ({
case 'checking':
return {
icon: ,
- color: '#1890ff',
+ color: '#40a9ff',
text: 'Checking...',
status: 'processing' as const
};
case 'licensed':
return {
icon: ,
- color: '#52c41a',
+ color: '#73d13d',
text: 'Licensed',
status: 'success' as const
};
case 'unlicensed':
return {
icon: ,
- color: '#ff4d4f',
- text: 'Not Licensed',
- status: 'error' as const
+ color: '#ff7875',
+ text: 'License Required',
+ status: 'warning' as const
};
case 'error':
return {
icon: ,
- color: '#faad14',
- text: 'License Error',
+ color: '#ffc53d',
+ text: 'Setup Required',
status: 'warning' as const
};
default:
@@ -112,7 +113,7 @@ const EnvironmentsTable: React.FC = ({
border: '1px solid #f0f0f0',
position: 'relative'
}}
- bodyStyle={{ padding: '16px' }}
+ styles={{ body: { padding: '16px' } }}
onClick={() => handleRowClick(env)}
>
{/* Subtle overlay for unlicensed environments */}
@@ -179,8 +180,8 @@ const EnvironmentsTable: React.FC = ({
{licenseDisplay.text}
@@ -267,6 +268,37 @@ const EnvironmentsTable: React.FC = ({
+
+ {/* API Calls Information - show if license details are available */}
+ {env.licenseDetails && (
+
+
+
+
+ {env.licenseDetails.apiCallsUsage || 0}% used
+
+
+ )}
diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/ModernBreadcrumbs.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/ModernBreadcrumbs.tsx
index 1a3d35524..0403be24b 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/components/ModernBreadcrumbs.tsx
+++ b/client/packages/lowcoder/src/pages/setting/environments/components/ModernBreadcrumbs.tsx
@@ -2,7 +2,7 @@ import React, { ReactNode } from 'react';
import { Breadcrumb } from 'antd';
import { BreadcrumbProps } from 'antd/lib/breadcrumb';
-interface ModernBreadcrumbsProps extends BreadcrumbProps {
+interface ModernBreadcrumbsProps extends Omit {
/**
* Items to display in the breadcrumb
*/
@@ -17,36 +17,52 @@ interface ModernBreadcrumbsProps extends BreadcrumbProps {
* Modern styled breadcrumb component with consistent styling
*/
const ModernBreadcrumbs: React.FC = ({ items = [], ...props }) => {
+ // Convert custom items format to Antd's expected format
+ const breadcrumbItems = items.map(item => ({
+ key: item.key,
+ title: item.onClick ? (
+ {
+ e.currentTarget.style.color = '#096dd9';
+ e.currentTarget.style.textDecoration = 'underline';
+ }}
+ onMouseLeave={(e) => {
+ e.currentTarget.style.color = '#1890ff';
+ e.currentTarget.style.textDecoration = 'none';
+ }}
+ >
+ {item.title}
+
+ ) : (
+
+ {item.title}
+
+ )
+ }));
+
return (
-
- {items.map(item => (
-
- {item.onClick ? (
- e.currentTarget.style.textDecoration = 'underline'}
- onMouseLeave={(e) => e.currentTarget.style.textDecoration = 'none'}
- >
- {item.title}
-
- ) : (
-
- {item.title}
-
- )}
-
- ))}
-
+ /}
+ items={breadcrumbItems}
+ />
);
};
diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/StatsCard.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/StatsCard.tsx
new file mode 100644
index 000000000..214a60106
--- /dev/null
+++ b/client/packages/lowcoder/src/pages/setting/environments/components/StatsCard.tsx
@@ -0,0 +1,74 @@
+import React from 'react';
+import { Card } from 'antd';
+
+interface StatsCardProps {
+ title: string;
+ value: number | string;
+ icon?: React.ReactNode;
+ color?: string;
+ className?: string;
+ style?: React.CSSProperties;
+}
+
+/**
+ * Reusable StatsCard component for displaying environment statistics
+ * Used across all Environment pages for consistency
+ */
+const StatsCard: React.FC = ({
+ title,
+ value,
+ icon,
+ color = '#1890ff',
+ className = '',
+ style = {}
+}) => {
+ return (
+
+
+
+
+ {title}
+
+
+ {value}
+
+
+ {icon && (
+
+ {icon}
+
+ )}
+
+
+ );
+};
+
+export default StatsCard;
\ No newline at end of file
diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/UnlicensedEnvironmentView.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/UnlicensedEnvironmentView.tsx
index 6b61379dd..7388ae810 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/components/UnlicensedEnvironmentView.tsx
+++ b/client/packages/lowcoder/src/pages/setting/environments/components/UnlicensedEnvironmentView.tsx
@@ -6,10 +6,15 @@ import {
ArrowLeftOutlined,
CloseCircleOutlined,
ExclamationCircleOutlined,
- WarningOutlined
+ WarningOutlined,
+ CloudServerOutlined
} from '@ant-design/icons';
import { Environment } from '../types/environment.types';
import ContactLowcoderModal from './ContactLowcoderModal';
+import ModernBreadcrumbs from './ModernBreadcrumbs';
+import EnvironmentHeader from './EnvironmentHeader';
+import StatsCard from './StatsCard';
+import { Level1SettingPageContent } from "../../styled";
import history from "@lowcoder-ee/util/history";
const { Title, Text } = Typography;
@@ -20,7 +25,7 @@ interface UnlicensedEnvironmentViewProps {
}
/**
- * Modern UI for unlicensed environments
+ * Consistent UI for unlicensed environments matching other environment pages
*/
const UnlicensedEnvironmentView: React.FC = ({
environment,
@@ -31,118 +36,136 @@ const UnlicensedEnvironmentView: React.FC = ({
const getLicenseIcon = () => {
switch (environment.licenseStatus) {
case 'unlicensed':
- return ;
+ return ;
case 'error':
- return ;
+ return ;
default:
- return ;
+ return ;
}
};
const getLicenseTitle = () => {
- switch (environment.licenseStatus) {
- case 'unlicensed':
- return 'Environment Not Licensed';
- case 'error':
- return 'License Configuration Error';
- default:
- return 'License Issue';
- }
- };
+ return environment.licenseError;
+ }
+
+
const getLicenseDescription = () => {
- if (environment.licenseError) {
- return environment.licenseError;
- }
switch (environment.licenseStatus) {
case 'unlicensed':
- return 'This environment requires a valid license to access its features and functionality.';
+ return 'This environment needs a valid license to unlock its full capabilities and features. Please make sure your API Service URL is correctly configured and Plugin is installed.';
case 'error':
- return 'There was an error validating the license for this environment. Please check the configuration.';
+ return 'We encountered an issue while checking the license. Please review the configuration settings.';
default:
- return 'This environment has license-related issues that need to be resolved.';
+ return 'This environment requires license configuration to proceed.';
}
};
+ // Stats data consistent with other environment pages
+ const statsData = [
+ {
+ title: "Type",
+ value: environment.environmentType || "Unknown",
+ icon: ,
+ color: "#1890ff"
+ },
+ {
+ title: "Status",
+ value: "Unlicensed",
+ icon: ,
+ color: "#ff4d4f"
+ },
+ {
+ title: "Master Env",
+ value: environment.isMaster ? "Yes" : "No",
+ icon: ,
+ color: environment.isMaster ? "#722ed1" : "#8c8c8c"
+ },
+ {
+ title: "License Issue",
+ value: environment.licenseStatus === 'error' ? "Error" : "Missing",
+ icon: environment.licenseStatus === 'error' ? : ,
+ color: environment.licenseStatus === 'error' ? "#faad14" : "#ff4d4f"
+ }
+ ];
+
return (
-
-
-
-
- {/* Main Status Card */}
-
+
+ {/* Environment Header Component */}
+
+
+ {/* Stats Cards Row */}
+
+ {statsData.map((stat, index) => (
+
+
+
+ ))}
+
+
+ {/* Breadcrumbs */}
+ history.push('/setting/environments')
+ },
+ {
+ key: 'current',
+ title: environment.environmentName || "Environment Detail"
+ }
+ ]}
+ />
+
+ {/* License Issue Card */}
+
+
+
+
{/* Status Icon */}
{getLicenseIcon()}
- {/* Environment Info */}
-
-
- {getLicenseTitle()}
-
-
- {getLicenseDescription()}
-
-
- {/* Environment Details */}
-
- Environment:
-
- {environment.environmentName || 'Unnamed Environment'}
-
-
- ID: {environment.environmentId}
-
-
-
+ {/* License Issue Information */}
+
+ {getLicenseTitle()}
+
+
+ {getLicenseDescription()}
+
{/* Action Buttons */}
-
+
= ({
style={{
width: '100%',
height: '48px',
- borderRadius: '8px',
+ borderRadius: '4px',
fontSize: '16px',
- fontWeight: 500,
- background: 'linear-gradient(135deg, #1890ff 0%, #0050b3 100%)',
- border: 'none',
- boxShadow: '0 4px 12px rgba(24, 144, 255, 0.3)'
+ fontWeight: 500
}}
>
Contact Lowcoder Team
@@ -169,11 +189,9 @@ const UnlicensedEnvironmentView: React.FC = ({
style={{
width: '100%',
height: '48px',
- borderRadius: '8px',
+ borderRadius: '4px',
fontSize: '16px',
- fontWeight: 500,
- borderColor: '#d9d9d9',
- color: '#595959'
+ fontWeight: 500
}}
>
Edit Environment
@@ -186,31 +204,38 @@ const UnlicensedEnvironmentView: React.FC = ({
style={{
width: '100%',
height: '48px',
- borderRadius: '8px',
+ borderRadius: '4px',
fontSize: '16px',
- fontWeight: 500,
- borderColor: '#d9d9d9',
- color: '#8c8c8c'
+ fontWeight: 500
}}
>
Back to Environments
-
-
- {/* Footer Help Text */}
-
- Need assistance? Contact our team for licensing support or edit the environment configuration to resolve this issue.
-
-
-
-
+
+
+
+
+
+ {/* Help Text */}
+
+
+ Need assistance? Contact our team for licensing support or edit the environment configuration to resolve this issue.
+
+
{/* Contact Lowcoder Modal */}
= ({
onClose={() => setIsContactModalVisible(false)}
environment={environment}
/>
-
+
);
};
diff --git a/client/packages/lowcoder/src/pages/setting/environments/components/WorkspaceHeader.tsx b/client/packages/lowcoder/src/pages/setting/environments/components/WorkspaceHeader.tsx
index f4c00c22e..462e6bf35 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/components/WorkspaceHeader.tsx
+++ b/client/packages/lowcoder/src/pages/setting/environments/components/WorkspaceHeader.tsx
@@ -7,144 +7,20 @@ import {
Tooltip,
Row,
Col,
- Statistic,
Avatar,
Space,
- Divider,
- Card,
- Dropdown,
- Menu
} from "antd";
import {
CloudUploadOutlined,
- SettingOutlined,
TeamOutlined,
- AppstoreOutlined,
- DatabaseOutlined,
- CodeOutlined,
CloudServerOutlined,
ClockCircleOutlined,
- MoreOutlined,
- StarOutlined,
- StarFilled
} from "@ant-design/icons";
import { Environment } from "../types/environment.types";
import { Workspace } from "../types/workspace.types";
-import styled from "styled-components";
const { Title, Text } = Typography;
-// Styled components for custom design
-const HeaderWrapper = styled.div`
- border-radius: 12px;
- overflow: hidden;
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
- position: relative;
- margin-bottom: 24px;
-`;
-
-const GradientBanner = styled.div<{ avatarColor: string }>`
- background: linear-gradient(135deg, ${props => props.avatarColor} 0%, rgba(24, 144, 255, 0.8) 100%);
- height: 140px;
- position: relative;
- overflow: hidden;
- transition: background 1s ease-in-out;
-
- &::before {
- content: '';
- position: absolute;
- top: -50%;
- left: -50%;
- width: 200%;
- height: 200%;
- background: repeating-linear-gradient(
- 45deg,
- rgba(255,255,255,0.1),
- rgba(255,255,255,0.1) 1px,
- transparent 1px,
- transparent 10px
- );
- animation: moveBackground 30s linear infinite;
- }
-
- @keyframes moveBackground {
- 0% {
- transform: translate(0, 0);
- }
- 100% {
- transform: translate(100px, 100px);
- }
- }
-
- &:hover {
- background: linear-gradient(135deg, rgba(24, 144, 255, 0.8) 0%, ${props => props.avatarColor} 100%);
- transition: background 1s ease-in-out;
- }
-`;
-
-const ContentContainer = styled.div`
- background-color: white;
- padding: 24px;
- position: relative;
- transition: transform 0.3s ease-in-out;
-
- &:hover {
- transform: translateY(-2px);
- }
-`;
-
-const AvatarContainer = styled.div`
- position: absolute;
- top: -50px;
- left: 24px;
- background: white;
- padding: 4px;
- border-radius: 8px;
- border: 1px solid #f0f0f0;
-`;
-
-const StatusBadge = styled(Tag)<{ $active?: boolean }>`
- position: absolute;
- top: 12px;
- right: 12px;
- font-weight: 500;
- font-size: 12px;
- padding: 4px 12px;
- border-radius: 4px;
- border: none;
- background: ${props => props.$active ? '#52c41a' : '#f5f5f5'};
- color: ${props => props.$active ? 'white' : '#8c8c8c'};
-`;
-
-const StatCard = styled(Card)`
- border-radius: 4px;
- border: 1px solid #f0f0f0;
- transition: all 0.3s;
-
- &:hover {
- transform: translateY(-2px);
- border-color: #d9d9d9;
- }
-`;
-
-const ActionButton = styled(Button)`
- border-radius: 4px;
- display: flex;
- align-items: center;
- justify-content: center;
- height: 32px;
-`;
-
-const FavoriteButton = styled(Button)`
- position: absolute;
- top: 12px;
- right: 80px;
- border: none;
- border-radius: 4px;
- background: rgba(255, 255, 255, 0.9);
- color: #722ed1;
-`;
-
interface WorkspaceHeaderProps {
workspace: Workspace;
environment: Environment;
@@ -172,7 +48,7 @@ const WorkspaceHeader: React.FC = ({
};
// Format date for last updated
- const formatDate = (date: number | undefined) => {
+ const formatDate = (date: number | undefined) => {
if (!date) return "N/A";
return new Date(date).toLocaleDateString("en-US", {
month: "short",
@@ -181,81 +57,97 @@ const WorkspaceHeader: React.FC = ({
});
};
-
-
-
-
return (
-
-
-
- {workspace.managed ? "Managed" : "Unmanaged"}
-
-
-
-
-
-
-
- {workspace.name.charAt(0).toUpperCase()}
-
-
-
-
-
-
- {workspace.name}
-
-
- ID: {workspace.id}
-
-
- created on {formatDate(workspace.creationDate)}
-
-
- {environment.environmentName}
-
-
-
-
-
-
-
- Managed:
-
-
-
- }
- onClick={onDeploy}
- disabled={!workspace.managed}
+
+
+
+
+
+ {workspace.name.charAt(0).toUpperCase()}
+
+
+
+ {workspace.name}
+
+
+
+ ID: {workspace.id}
+
+
+
+ {formatDate(workspace.creationDate)}
+
+
+
+ {environment.environmentName}
+
+
- Deploy
-
-
+ {workspace.managed ? 'Managed' : 'Unmanaged'}
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+ Managed:
+
+
+
+ }
+ onClick={onDeploy}
+ disabled={!workspace.managed}
+ style={{
+ fontWeight: '500',
+ borderRadius: '4px'
+ }}
+ >
+ Deploy
+
+
+
+
+
+
);
};
diff --git a/client/packages/lowcoder/src/pages/setting/environments/config/data-sources.config.tsx b/client/packages/lowcoder/src/pages/setting/environments/config/data-sources.config.tsx
index 591621862..ce802e8de 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/config/data-sources.config.tsx
+++ b/client/packages/lowcoder/src/pages/setting/environments/config/data-sources.config.tsx
@@ -5,18 +5,10 @@ import { DataSource} from '../types/datasource.types';
import { Environment } from '../types/environment.types';
import { deployDataSource, DataSourceStats } from '../services/datasources.service';
-
-
export const dataSourcesConfig: DeployableItemConfig = {
deploy: {
singularLabel: 'Data Source',
fields: [
- {
- name: 'updateDependenciesIfNeeded',
- label: 'Update Dependencies If Needed',
- type: 'checkbox',
- defaultValue: false
- },
{
name: 'deployCredential',
label: 'Overwrite Credentials',
@@ -29,7 +21,6 @@ export const dataSourcesConfig: DeployableItemConfig = {
envId: sourceEnv.environmentId,
targetEnvId: targetEnv.environmentId,
datasourceId: item.id,
- updateDependenciesIfNeeded: values.updateDependenciesIfNeeded,
datasourceGid: item.gid,
deployCredential: values.deployCredential ?? false
};
diff --git a/client/packages/lowcoder/src/pages/setting/environments/config/query.config.tsx b/client/packages/lowcoder/src/pages/setting/environments/config/query.config.tsx
index 35189c8c9..7495e5371 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/config/query.config.tsx
+++ b/client/packages/lowcoder/src/pages/setting/environments/config/query.config.tsx
@@ -4,17 +4,14 @@ import { Query } from '../types/query.types';
import { deployQuery } from '../services/query.service';
import { Environment } from '../types/environment.types';
-
-
-
export const queryConfig: DeployableItemConfig = {
deploy: {
singularLabel: 'Query',
fields: [
{
- name: 'updateDependenciesIfNeeded',
- label: 'Update Dependencies If Needed',
+ name: 'deployCredential',
+ label: 'Overwrite Credentials',
type: 'checkbox',
defaultValue: false
}
@@ -24,8 +21,8 @@ export const queryConfig: DeployableItemConfig = {
envId: sourceEnv.environmentId,
targetEnvId: targetEnv.environmentId,
queryId: item.id,
- updateDependenciesIfNeeded: values.updateDependenciesIfNeeded,
queryGid: item.gid,
+ deployCredential: values.deployCredential ?? false
};
},
execute: (params: any) => deployQuery(params)
diff --git a/client/packages/lowcoder/src/pages/setting/environments/config/workspace.config.tsx b/client/packages/lowcoder/src/pages/setting/environments/config/workspace.config.tsx
index 6689931f9..e35423f2f 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/config/workspace.config.tsx
+++ b/client/packages/lowcoder/src/pages/setting/environments/config/workspace.config.tsx
@@ -5,32 +5,23 @@ import { Environment } from '../types/environment.types';
import { deployWorkspace } from '../services/workspace.service';
import { Workspace } from '../types/workspace.types';
-
-
export const workspaceConfig: DeployableItemConfig = {
// Deploy configuration
deploy: {
singularLabel: 'Workspace',
fields: [
- {
- name: 'deployCredential',
- label: 'Overwrite Credentials',
- type: 'checkbox',
- defaultValue: false
- }
+ // Removed deployCredential field as workspaces don't need credential overwrite
],
prepareParams: (item: Workspace, values: any, sourceEnv: Environment, targetEnv: Environment) => {
if (!item.gid) {
console.error('Missing workspace.gid when deploying workspace:', item);
}
- console.log('item.gid', item.gid);
return {
envId: sourceEnv.environmentId,
targetEnvId: targetEnv.environmentId,
workspaceId: item.gid,
- deployCredential: values.deployCredential ?? false
};
},
execute: (params: any) => deployWorkspace(params)
diff --git a/client/packages/lowcoder/src/pages/setting/environments/services/datasources.service.ts b/client/packages/lowcoder/src/pages/setting/environments/services/datasources.service.ts
index e743179b0..2fc492028 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/services/datasources.service.ts
+++ b/client/packages/lowcoder/src/pages/setting/environments/services/datasources.service.ts
@@ -21,7 +21,6 @@ export interface DeployDataSourceParams {
targetEnvId: string;
datasourceId: string;
datasourceGid: string;
- updateDependenciesIfNeeded?: boolean;
deployCredential: boolean;
}
// Get data sources for a workspace - using your correct implementation
@@ -158,7 +157,6 @@ export async function deployDataSource(params: DeployDataSourceParams): Promise<
envId: params.envId,
targetEnvId: params.targetEnvId,
datasourceId: params.datasourceId,
- updateDependenciesIfNeeded: params.updateDependenciesIfNeeded ?? false,
deployCredential: params.deployCredential
}
});
diff --git a/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts b/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts
index eb11609f5..b3ccb5314 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts
+++ b/client/packages/lowcoder/src/pages/setting/environments/services/environments.service.ts
@@ -136,6 +136,7 @@ export async function getEnvironmentById(id: string): Promise {
envWithLicense.isLicensed = licenseInfo.isValid;
envWithLicense.licenseStatus = licenseInfo.isValid ? 'licensed' : 'unlicensed';
envWithLicense.licenseError = licenseInfo.error;
+ envWithLicense.licenseDetails = licenseInfo.details;
} else {
envWithLicense.isLicensed = false;
envWithLicense.licenseStatus = 'error';
@@ -556,6 +557,7 @@ export async function getEnvironmentsWithLicenseStatus(): Promise
envWithLicense.isLicensed = licenseInfo.isValid;
envWithLicense.licenseStatus = licenseInfo.isValid ? 'licensed' : 'unlicensed';
envWithLicense.licenseError = licenseInfo.error;
+ envWithLicense.licenseDetails = licenseInfo.details;
} else {
envWithLicense.isLicensed = false;
envWithLicense.licenseStatus = 'error';
diff --git a/client/packages/lowcoder/src/pages/setting/environments/services/license.service.ts b/client/packages/lowcoder/src/pages/setting/environments/services/license.service.ts
index ff0be0ce6..ebc0ae46a 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/services/license.service.ts
+++ b/client/packages/lowcoder/src/pages/setting/environments/services/license.service.ts
@@ -1,11 +1,11 @@
import axios from 'axios';
-import { EnvironmentLicense } from '../types/environment.types';
+import { EnvironmentLicense, DetailedLicenseInfo } from '../types/environment.types';
/**
- * Check if license endpoint exists for an environment
+ * Check license and fetch detailed license information for an environment
* @param apiServiceUrl - API service URL for the environment
* @param apiKey - API key for the environment
- * @returns Promise with license information
+ * @returns Promise with license information including detailed data
*/
export async function checkEnvironmentLicense(
apiServiceUrl: string,
@@ -25,8 +25,8 @@ export async function checkEnvironmentLicense(
headers.Authorization = `Bearer ${apiKey}`;
}
- // Use GET request to check endpoint existence
- await axios.get(
+ // Fetch detailed license information
+ const response = await axios.get(
`${apiServiceUrl}/api/plugins/enterprise/license`,
{
headers,
@@ -34,16 +34,84 @@ export async function checkEnvironmentLicense(
}
);
- // If we get a successful response, the endpoint exists
+ // Parse the license response
+ const licenseData = response.data;
+
+ // Calculate total API calls limit and usage percentage
+ const totalAPICallsLimit = licenseData.eeLicenses?.reduce(
+ (sum: number, license: any) => sum + (license.apiCallsLimit || 0),
+ 0
+ ) || 0;
+
+ const apiCallsUsage = totalAPICallsLimit > 0
+ ? Math.round(((totalAPICallsLimit - licenseData.remainingAPICalls) / totalAPICallsLimit) * 100)
+ : 0;
+
+ const licenseDetails: DetailedLicenseInfo = {
+ eeActive: licenseData.eeActive || false,
+ remainingAPICalls: licenseData.remainingAPICalls || 0,
+ eeLicenses: licenseData.eeLicenses || [],
+ totalAPICallsLimit,
+ apiCallsUsage
+ };
+
+ // Determine if license is valid based on enterprise edition status and remaining calls
+ const isValid = licenseDetails.eeActive && licenseDetails.remainingAPICalls > 0;
+
return {
- isValid: true
+ isValid,
+ details: licenseDetails
};
} catch (error) {
- // Any error means the endpoint doesn't exist or isn't accessible
+ // Determine the specific error type
+ let errorMessage = 'License information unavailable';
+
+ if (axios.isAxiosError(error)) {
+ if (error.code === 'ECONNABORTED') {
+ errorMessage = 'License check took too long';
+ } else if (error.response?.status === 404) {
+ errorMessage = 'License service not available';
+ } else if (error.response?.status === 401) {
+ errorMessage = 'Authentication required - please check API key';
+ } else if (error.response && error.response.status >= 500) {
+ errorMessage = 'License service temporarily unavailable';
+ }
+ }
+
return {
isValid: false,
- error: 'License not available'
+ error: errorMessage
};
}
+}
+
+/**
+ * Format API calls for display
+ * @param remaining - Remaining API calls
+ * @param total - Total API calls limit
+ * @returns Formatted string
+ */
+export function formatAPICalls(remaining: number, total: number): string {
+ const used = total - remaining;
+ const percentage = total > 0 ? Math.round((used / total) * 100) : 0;
+
+ return `${remaining.toLocaleString()} remaining (${used.toLocaleString()}/${total.toLocaleString()} used, ${percentage}%)`;
+}
+
+/**
+ * Get API calls status color based on usage percentage - using softer, less aggressive colors
+ * @param remainingCalls - Remaining API calls
+ * @param totalCalls - Total API calls limit
+ * @returns Color string for UI components
+ */
+export function getAPICallsStatusColor(remainingCalls: number, totalCalls: number): string {
+ if (totalCalls === 0) return '#d9d9d9'; // Unknown
+
+ const usagePercentage = ((totalCalls - remainingCalls) / totalCalls) * 100;
+
+ if (usagePercentage >= 90) return '#ff7875'; // Soft red - High usage
+ if (usagePercentage >= 75) return '#ffc53d'; // Soft orange - Moderate usage
+ if (usagePercentage >= 50) return '#40a9ff'; // Soft blue - Normal usage
+ return '#73d13d'; // Soft green - Low usage
}
\ No newline at end of file
diff --git a/client/packages/lowcoder/src/pages/setting/environments/services/query.service.ts b/client/packages/lowcoder/src/pages/setting/environments/services/query.service.ts
index 4cf7a29be..20b79f4ee 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/services/query.service.ts
+++ b/client/packages/lowcoder/src/pages/setting/environments/services/query.service.ts
@@ -14,8 +14,8 @@ export interface MergedQueriesResult {
envId: string;
targetEnvId: string;
queryId: string;
- updateDependenciesIfNeeded?: boolean;
queryGid: string;
+ deployCredential: boolean;
}
@@ -71,7 +71,7 @@ export interface MergedQueriesResult {
envId: params.envId,
targetEnvId: params.targetEnvId,
queryId: params.queryId,
- updateDependenciesIfNeeded: params.updateDependenciesIfNeeded ?? false
+ deployCredential: params.deployCredential
}
});
if (response.status === 200) {
diff --git a/client/packages/lowcoder/src/pages/setting/environments/services/workspace.service.ts b/client/packages/lowcoder/src/pages/setting/environments/services/workspace.service.ts
index dec7c7cf7..b7cf4a37c 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/services/workspace.service.ts
+++ b/client/packages/lowcoder/src/pages/setting/environments/services/workspace.service.ts
@@ -84,7 +84,6 @@ export async function deployWorkspace(params: {
envId: string;
targetEnvId: string;
workspaceId: string;
- deployCredential: boolean; // Mandatory parameter
}): Promise {
try {
// Use the new endpoint format with only essential parameters
@@ -93,7 +92,6 @@ export async function deployWorkspace(params: {
orgGid: params.workspaceId, // Using workspaceId as orgGid
envId: params.envId,
targetEnvId: params.targetEnvId,
- deployCredential: params.deployCredential
}
});
@@ -107,7 +105,6 @@ export async function deployWorkspace(params: {
);
}
-
return response.status === 200;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to deploy workspace';
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
index 4660ea38b..1cca58f7e 100644
--- a/client/packages/lowcoder/src/pages/setting/environments/types/environment.types.ts
+++ b/client/packages/lowcoder/src/pages/setting/environments/types/environment.types.ts
@@ -18,6 +18,8 @@ export interface Environment {
isLicensed?: boolean;
licenseStatus?: 'checking' | 'licensed' | 'unlicensed' | 'error';
licenseError?: string;
+ // Enhanced license details
+ licenseDetails?: DetailedLicenseInfo;
}
/**
@@ -26,4 +28,28 @@ export interface Environment {
export interface EnvironmentLicense {
isValid: boolean;
error?: string;
+ // Enhanced license details
+ details?: DetailedLicenseInfo;
+}
+
+/**
+ * Interface representing detailed license information from the license endpoint
+ */
+export interface DetailedLicenseInfo {
+ eeActive: boolean;
+ remainingAPICalls: number;
+ eeLicenses: LicenseEntry[];
+ // Calculated fields
+ totalAPICallsLimit?: number;
+ apiCallsUsage?: number; // percentage used
+}
+
+/**
+ * Interface representing a single license entry
+ */
+export interface LicenseEntry {
+ uuid: string;
+ customerId: string;
+ customerName: string;
+ apiCallsLimit: number;
}
\ No newline at end of file
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