Skip to content

Add Environments in the Redux State #1716

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,11 @@ export const ReduxActionTypes = {
/* Enterprise Edition */
FETCH_ENTERPRISE_LICENSE : "FETCH_ENTERPRISE_LICENSE",
SET_ENTERPRISE_LICENSE : "SET_ENTERPRISE_LICENSE",

/* Environments */
FETCH_ENVIRONMENTS : "FETCH_ENVIRONMENTS",
FETCH_ENVIRONMENTS_SUCCESS: "FETCH_ENVIRONMENTS_SUCCESS",
FETCH_ENVIRONMENTS_FAILURE: "FETCH_ENVIRONMENTS_FAILURE",

/* Branding Setting */
FETCH_BRANDING_SETTING : "FETCH_BRANDING_SETTING",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,28 @@
// client/packages/lowcoder/src/pages/setting/environments/Environments.tsx
import React from "react";
import { Switch, Route, useRouteMatch } from "react-router-dom";
import { EnvironmentProvider } from "./context/EnvironmentContext";
import EnvironmentRoutes from "./routes/EnvironmentRoutes";
import EnvironmentsList from "./EnvironmentsList";

/**
* Top-level Environments component
* Provides the EnvironmentProvider at the top level
* No longer needs the EnvironmentProvider since we use Redux
*/
const Environments: React.FC = () => {
const { path } = useRouteMatch();

return (
<EnvironmentProvider>
<Switch>
{/* Environment list route */}
<Route exact path={path}>
<EnvironmentsList />
</Route>

{/* All routes that need a specific environment */}
<Route path={`${path}/:envId`}>
<EnvironmentRoutes />
</Route>
</Switch>
</EnvironmentProvider>
<Switch>
{/* Environment list route */}
<Route exact path={path}>
<EnvironmentsList />
</Route>

{/* All routes that need a specific environment */}
<Route path={`${path}/:envId`}>
<EnvironmentRoutes />
</Route>
</Switch>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import React, { useState } from "react";
import React, { useState, useEffect, useRef } from "react";
import { Typography, Alert, Input, Button, Space, Empty, Card, Spin, Row, Col, Tooltip, Badge } from "antd";
import { SearchOutlined, CloudServerOutlined, SyncOutlined, PlusOutlined} from "@ant-design/icons";
import { useHistory } from "react-router-dom";
import { useEnvironmentContext } from "./context/EnvironmentContext";
import { useSelector, useDispatch } from "react-redux";
import { selectEnvironments, selectEnvironmentsLoading, selectEnvironmentsError } from "redux/selectors/enterpriseSelectors";
import { fetchEnvironments } from "redux/reduxActions/enterpriseActions";
import { Environment } from "./types/environment.types";
import EnvironmentsTable from "./components/EnvironmentsTable";
import CreateEnvironmentModal from "./components/CreateEnvironmentModal";
Expand All @@ -17,23 +19,23 @@ const { Title, Text } = Typography;
* Displays a table of environments
*/
const EnvironmentsList: React.FC = () => {
// Use the shared context instead of a local hook
const {
environments,
isLoading,
error,
refreshEnvironments
} = useEnvironmentContext();
// Use Redux state instead of context
const dispatch = useDispatch();
const environments = useSelector(selectEnvironments);
const isLoading = useSelector(selectEnvironmentsLoading);
const error = useSelector(selectEnvironmentsError);

// State for search input
const [searchText, setSearchText] = useState("");
const [isRefreshing, setIsRefreshing] = useState(false);
const [isCreateModalVisible, setIsCreateModalVisible] = useState(false);
const [isCreating, setIsCreating] = useState(false);



// Hook for navigation
const history = useHistory();


// Filter environments based on search text
const filteredEnvironments = environments.filter((env) => {
const searchLower = searchText.toLowerCase();
Expand Down Expand Up @@ -62,18 +64,16 @@ const EnvironmentsList: React.FC = () => {
};

// Handle refresh
const handleRefresh = async () => {
setIsRefreshing(true);
await refreshEnvironments();
setIsRefreshing(false);
const handleRefresh = () => {
dispatch(fetchEnvironments());
};

// Handle create environment
const handleCreateEnvironment = async (environmentData: Partial<Environment>) => {
setIsCreating(true);
try {
await createEnvironment(environmentData);
await refreshEnvironments(); // Refresh the list after creation
dispatch(fetchEnvironments()); // Refresh the list after creation
} catch (error) {
console.error("Failed to create environment:", error);
throw error; // Re-throw to let the modal handle the error display
Expand Down Expand Up @@ -153,7 +153,7 @@ const EnvironmentsList: React.FC = () => {
Create Environment
</Button>
<Button
icon={<SyncOutlined spin={isRefreshing} />}
icon={<SyncOutlined spin={isLoading} />}
onClick={handleRefresh}
loading={isLoading}
type="default"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
import React, { useState, useEffect } from 'react';
import { Modal, Form, Select, Checkbox, Button, Spin, Input, Tag, Space, Alert } from 'antd';
import { messageInstance } from 'lowcoder-design/src/components/GlobalInstances';
import { useSelector } from 'react-redux';
import { selectLicensedEnvironments, selectEnvironmentsLoading } from 'redux/selectors/enterpriseSelectors';
import { Environment } from '../types/environment.types';
import { DeployableItemConfig } from '../types/deployable-item.types';
import { useEnvironmentContext } from '../context/EnvironmentContext';
import { getEnvironmentTagColor, formatEnvironmentType } from '../utils/environmentUtils';
import { ExclamationCircleOutlined } from '@ant-design/icons';
import { showFirstCredentialOverwriteConfirm, showSecondCredentialOverwriteConfirm } from './credentialConfirmations';
Expand All @@ -27,7 +28,8 @@ function DeployItemModal({
onSuccess
}: DeployItemModalProps) {
const [form] = Form.useForm();
const { environments, isLoading } = useEnvironmentContext();
const licensedEnvironments = useSelector(selectLicensedEnvironments);
const isLoading = useSelector(selectEnvironmentsLoading);
const [deploying, setDeploying] = useState(false);
const [credentialConfirmationStep, setCredentialConfirmationStep] = useState(0); // 0: not started, 1: first confirmation, 2: confirmed

Expand All @@ -39,8 +41,8 @@ function DeployItemModal({
}, [visible, form]);

// Filter out source environment from target list
const targetEnvironments = environments.filter(
(env: Environment) => env.environmentId !== sourceEnvironment.environmentId && env.isLicensed !== false
const targetEnvironments = licensedEnvironments.filter(
(env: Environment) => env.environmentId !== sourceEnvironment.environmentId
);

// Handle credential checkbox change with double confirmation
Expand Down Expand Up @@ -82,7 +84,7 @@ function DeployItemModal({

try {
const values = await form.validateFields();
const targetEnv = environments.find(env => env.environmentId === values.targetEnvId);
const targetEnv = licensedEnvironments.find(env => env.environmentId === values.targetEnvId);

if (!targetEnv) {
messageInstance.error('Target environment not found');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import React, {
} from "react";
import { messageInstance } from "lowcoder-design/src/components/GlobalInstances";
import { useParams } from "react-router-dom";
import { useDispatch } from "react-redux";
import { fetchEnvironments } from "redux/reduxActions/enterpriseActions";
import { getEnvironmentById, updateEnvironment } from "../services/environments.service";
import { Environment } from "../types/environment.types";
import { useEnvironmentContext } from './EnvironmentContext';


interface SingleEnvironmentContextState {
// Environment data
environment: Environment | null;
Expand Down Expand Up @@ -53,8 +54,8 @@ import React, {
const { envId } = useParams<{ envId: string }>();
const environmentId = propEnvironmentId || envId;

// Access the environments context to refresh the list
const { refreshEnvironments } = useEnvironmentContext();
// Use Redux dispatch to refresh environments instead of context
const dispatch = useDispatch();

// State for environment data
const [environment, setEnvironment] = useState<Environment | null>(null);
Expand Down Expand Up @@ -103,18 +104,16 @@ import React, {
messageInstance.success("Environment updated successfully");

// Refresh both the single environment and environments list
await Promise.all([
fetchEnvironment(), // Refresh the current environment
refreshEnvironments() // Refresh the environments list
]);
await fetchEnvironment(); // Refresh the current environment
dispatch(fetchEnvironments()); // Refresh the environments list using Redux

return updatedEnv;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "Failed to update environment";
messageInstance.error(errorMessage);
throw err;
}
}, [environment, environmentId, fetchEnvironment, refreshEnvironments]);
}, [environment, environmentId, fetchEnvironment, dispatch]);

// Load environment data when the component mounts or environmentId changes
useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import { BrandingConfig, BrandingSettingResponse, EnterpriseLicenseResponse } from "@lowcoder-ee/api/enterpriseApi";
import { createReducer } from "@lowcoder-ee/util/reducerUtils";
import { ReduxAction, ReduxActionTypes } from "constants/reduxActionConstants";

import { Environment } from "pages/setting/environments/types/environment.types";
export interface EnterpriseReduxState {
enterprise: EnterpriseLicenseResponse,
globalBranding?: BrandingConfig,
workspaceBranding?: BrandingConfig,
environments: Environment[],
environmentsLoading: boolean,
environmentsError: string | null,
}

const initialState: EnterpriseReduxState = {
enterprise: {
eeActive: false,
remainingAPICalls: 0,
eeLicenses: [],
}
},
environments: [],
environmentsLoading: false,
environmentsError: null,
};

const enterpriseReducer = createReducer(initialState, {
Expand All @@ -38,6 +44,29 @@ const enterpriseReducer = createReducer(initialState, {
...state,
workspaceBranding: action.payload,
}),

[ReduxActionTypes.FETCH_ENVIRONMENTS]: (
state: EnterpriseReduxState
) => ({
...state,
environmentsLoading: true,
}),
[ReduxActionTypes.FETCH_ENVIRONMENTS_SUCCESS]: (
state: EnterpriseReduxState,
action: ReduxAction<Environment[]>
) => ({
...state,
environments: action.payload,
environmentsLoading: false,
}),
[ReduxActionTypes.FETCH_ENVIRONMENTS_FAILURE]: (
state: EnterpriseReduxState,
action: ReduxAction<string>
) => ({
...state,
environmentsLoading: false,
environmentsError: action.payload,
}),
});

export default enterpriseReducer;
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { EnterpriseLicenseResponse, FetchBrandingSettingPayload } from "@lowcoder-ee/api/enterpriseApi";
import { ReduxActionTypes } from "constants/reduxActionConstants";
import { Environment } from "pages/setting/environments/types/environment.types";

export const fetchEnterpriseLicense = () => ({
type: ReduxActionTypes.FETCH_ENTERPRISE_LICENSE,
Expand All @@ -16,3 +17,18 @@ export const fetchBrandingSetting = (payload: FetchBrandingSettingPayload) => {
payload,
};
};

export const fetchEnvironments = () => ({
type: ReduxActionTypes.FETCH_ENVIRONMENTS,
});

export const fetchEnvironmentsSuccess = (environments: Environment[]) => ({
type: ReduxActionTypes.FETCH_ENVIRONMENTS_SUCCESS,
payload: environments,
});

export const fetchEnvironmentsFailure = (error: string) => ({
type: ReduxActionTypes.FETCH_ENVIRONMENTS_FAILURE,
payload: error,
});

16 changes: 15 additions & 1 deletion client/packages/lowcoder/src/redux/sagas/enterpriseSagas.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { call, put, takeLatest } from 'redux-saga/effects';
import { ReduxAction, ReduxActionTypes } from "constants/reduxActionConstants";
import { setEnterpriseLicense } from "redux/reduxActions/enterpriseActions";
import { setEnterpriseLicense, fetchEnvironmentsSuccess, fetchEnvironmentsFailure } from "redux/reduxActions/enterpriseActions";
import { BrandingSettingResponse, EnterpriseLicenseResponse, FetchBrandingSettingPayload, getBranding, getEnterpriseLicense } from "api/enterpriseApi";
import { getEnvironmentsWithLicenseStatus } from "pages/setting/environments/services/environments.service";
import { Environment } from "pages/setting/environments/types/environment.types";

import { AxiosResponse } from 'axios';

function* fetchEnterpriseLicenseSaga(): Generator<any, void, EnterpriseLicenseResponse> {
Expand All @@ -14,6 +17,16 @@ function* fetchEnterpriseLicenseSaga(): Generator<any, void, EnterpriseLicenseRe
}
}

function* fetchEnvironmentsSaga(): Generator<any, void, Environment[]> {
try {
const environments: Environment[] = yield call(getEnvironmentsWithLicenseStatus);
yield put(fetchEnvironmentsSuccess(environments));
} catch (error) {
console.error('Failed to fetch environments:', error);
yield put(fetchEnvironmentsFailure(error as string));
}
}

function* fetchBrandingSettingSaga(action: ReduxAction<FetchBrandingSettingPayload>) {
try {
const response: BrandingSettingResponse = yield getBranding(action.payload.orgId);
Expand Down Expand Up @@ -45,4 +58,5 @@ function* fetchBrandingSettingSaga(action: ReduxAction<FetchBrandingSettingPaylo
export default function* enterpriseSagas() {
yield takeLatest(ReduxActionTypes.FETCH_ENTERPRISE_LICENSE, fetchEnterpriseLicenseSaga);
yield takeLatest(ReduxActionTypes.FETCH_BRANDING_SETTING, fetchBrandingSettingSaga);
yield takeLatest(ReduxActionTypes.FETCH_ENVIRONMENTS, fetchEnvironmentsSaga);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AppState } from "../reducers";


export const selectEnterpriseEditionStatus = (state: AppState) =>
state.ui.enterprise?.enterprise?.eeActive ?? false;

Expand All @@ -25,3 +26,23 @@ export const getGlobalBrandingSetting = (state: AppState) => {
export const getWorkspaceBrandingSetting = (state: AppState) => {
return state.ui.enterprise?.workspaceBranding;
}
// Environment selectors
export const selectEnvironments = (state: AppState) =>
state.ui.enterprise?.environments ?? [];

export const selectEnvironmentsLoading = (state: AppState) =>
state.ui.enterprise?.environmentsLoading ?? false;

export const selectEnvironmentsError = (state: AppState) =>
state.ui.enterprise?.environmentsError ?? null;

export const selectUnlicensedEnvironments = (state: AppState) => {
const environments = state.ui.enterprise?.environments ?? [];
return environments.filter(env => env.isLicensed === false);
};

export const selectLicensedEnvironments = (state: AppState) => {
const environments = state.ui.enterprise?.environments ?? [];
return environments.filter(env => env.isLicensed !== false);
};

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import { fetchEnterpriseLicense } from 'redux/reduxActions/enterpriseActions';
import { fetchEnterpriseLicense, fetchEnvironments } from 'redux/reduxActions/enterpriseActions';
import { selectEnterpriseEditionStatus } from '@lowcoder-ee/redux/selectors/enterpriseSelectors';
import { useDispatch, useSelector } from 'react-redux';
import { isEEEnvironment } from "util/envUtils";
Expand All @@ -23,6 +23,7 @@ export const EnterpriseProvider: React.FC<ProviderProps> = ({ children }) => {
if (isEEEnvironment()) {
// Fetch the enterprise license only if we're in an EE environment
dispatch(fetchEnterpriseLicense());
dispatch(fetchEnvironments());
} else {
// Set the state to false for non-EE environments
// setEEActiveState(false);
Expand Down
Loading