Skip to content

Commit b99f183

Browse files
authored
Merge branch 'dev' into deployment_updates
2 parents 200f7d4 + 8f5368b commit b99f183

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1975
-435
lines changed

.github/workflows/sonarcloud.yml

+1
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,4 @@ jobs:
3030
env:
3131
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
3232
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
33+
SONAR_SCANNER_OPTS: "-Dsonar.javascript.node.maxspace=8192 -Xmx8192m"

client/packages/lowcoder/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"@fortawesome/free-regular-svg-icons": "^6.5.1",
2525
"@fortawesome/free-solid-svg-icons": "^6.5.1",
2626
"@fortawesome/react-fontawesome": "latest",
27+
"@lottiefiles/dotlottie-react": "^0.13.0",
2728
"@manaflair/redux-batch": "^1.0.0",
2829
"@rjsf/antd": "^5.21.2",
2930
"@rjsf/core": "^5.21.2",

client/packages/lowcoder/src/api/apiUtils.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,14 @@ export const apiFailureResponseInterceptor = (error: any) => {
122122
if (!notAuthRequiredPath(error.config?.url)) {
123123
if (error.response.status === API_STATUS_CODES.REQUEST_NOT_AUTHORISED) {
124124
// get x-org-id from failed request
125-
const organizationId = error.response.headers['x-org-id'] || undefined;
125+
let organizationId;
126+
if (error.response.headers['x-org-id']) {
127+
organizationId = error.response.headers['x-org-id'];
128+
}
129+
if (localStorage.getItem('lowcoder_login_orgId')) {
130+
organizationId = localStorage.getItem('lowcoder_login_orgId');
131+
localStorage.removeItem('lowcoder_login_orgId');
132+
}
126133
// Redirect to login and set a redirect url.
127134
StoreRegistry.getStore().dispatch(
128135
logoutAction({
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import Api from "api/api";
2+
import axios, { AxiosInstance, AxiosPromise, AxiosRequestConfig } from "axios";
3+
import { calculateFlowCode } from "./apiUtils";
4+
5+
export interface SearchParams {
6+
query: string;
7+
asset: string;
8+
per_page: number;
9+
page: 1;
10+
sort: string;
11+
formats?: string;
12+
price?: string;
13+
}
14+
15+
export type ResponseType = {
16+
response: any;
17+
};
18+
19+
const lcHeaders = {
20+
"Lowcoder-Token": calculateFlowCode(),
21+
"Content-Type": "application/json"
22+
};
23+
24+
let axiosIns: AxiosInstance | null = null;
25+
26+
const getAxiosInstance = (clientSecret?: string) => {
27+
if (axiosIns && !clientSecret) {
28+
return axiosIns;
29+
}
30+
31+
const headers: Record<string, string> = {
32+
"Content-Type": "application/json",
33+
};
34+
35+
const apiRequestConfig: AxiosRequestConfig = {
36+
baseURL: "https://api-service.lowcoder.cloud/api/flow",
37+
headers,
38+
};
39+
40+
axiosIns = axios.create(apiRequestConfig);
41+
return axiosIns;
42+
}
43+
44+
class IconFlowApi extends Api {
45+
46+
static async secureRequest(body: any, timeout: number = 6000): Promise<any> {
47+
let response;
48+
const axiosInstance = getAxiosInstance();
49+
50+
// Create a cancel token and set timeout for cancellation
51+
const source = axios.CancelToken.source();
52+
const timeoutId = setTimeout(() => {
53+
source.cancel("Request timed out.");
54+
}, timeout);
55+
56+
// Request configuration with cancel token
57+
const requestConfig: AxiosRequestConfig = {
58+
method: "POST",
59+
withCredentials: true,
60+
data: body,
61+
cancelToken: source.token, // Add cancel token
62+
};
63+
64+
try {
65+
response = await axiosInstance.request(requestConfig);
66+
} catch (error) {
67+
if (axios.isCancel(error)) {
68+
// Retry once after timeout cancellation
69+
try {
70+
// Reset the cancel token and retry
71+
const retrySource = axios.CancelToken.source();
72+
const retryTimeoutId = setTimeout(() => {
73+
retrySource.cancel("Retry request timed out.");
74+
}, 20000);
75+
76+
response = await axiosInstance.request({
77+
...requestConfig,
78+
cancelToken: retrySource.token,
79+
});
80+
81+
clearTimeout(retryTimeoutId);
82+
} catch (retryError) {
83+
console.warn("Error at Secure Flow Request. Retry failed:", retryError);
84+
throw retryError;
85+
}
86+
} else {
87+
console.warn("Error at Secure Flow Request:", error);
88+
throw error;
89+
}
90+
} finally {
91+
clearTimeout(timeoutId); // Clear the initial timeout
92+
}
93+
94+
return response;
95+
}
96+
97+
}
98+
99+
export const searchAssets = async (searchParameters : SearchParams) => {
100+
const apiBody = {
101+
path: "webhook/scout/search-asset",
102+
data: searchParameters,
103+
method: "post",
104+
headers: lcHeaders
105+
};
106+
try {
107+
const result = await IconFlowApi.secureRequest(apiBody);
108+
return result?.data?.response?.items?.total > 0 ? result.data.response.items as any : null;
109+
} catch (error) {
110+
console.error("Error searching Design Assets:", error);
111+
throw error;
112+
}
113+
};
114+
115+
export const getAssetLinks = async (uuid: string, params: Record<string, string>) => {
116+
const apiBody = {
117+
path: "webhook/scout/get-asset-links",
118+
data: {"uuid" : uuid, "params" : params},
119+
method: "post",
120+
headers: lcHeaders
121+
};
122+
try {
123+
const result = await IconFlowApi.secureRequest(apiBody);
124+
125+
return result?.data?.response?.download?.url.length > 0 ? result.data.response.download as any : null;
126+
} catch (error) {
127+
console.error("Error searching Design Assets:", error);
128+
throw error;
129+
}
130+
};
131+
132+
133+
/*
134+
135+
static async search(params: SearchParams): Promise<any> {
136+
let response;
137+
try {
138+
response = await getAxiosInstance().request({
139+
url: '/v3/search',
140+
method: "GET",
141+
withCredentials: false,
142+
params: {
143+
...params,
144+
},
145+
});
146+
} catch (error) {
147+
console.error(error);
148+
}
149+
return response?.data.response.items;
150+
}
151+
152+
static async download(uuid: string, params: Record<string, string>): Promise<any> {
153+
const response = await getAxiosInstance(clientSecret).request({
154+
url: `/v3/items/${uuid}/api-download?format=${params.format}`,
155+
method: "POST",
156+
withCredentials: false,
157+
});
158+
return response?.data.response.download;
159+
}
160+
161+
*/
162+
163+
export default IconFlowApi;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import Api from "api/api";
2+
import axios from "axios";
3+
4+
export type ResponseType = {
5+
response: any;
6+
};
7+
8+
class IconScoutApi extends Api {
9+
static async downloadAsset(url: string): Promise<any> {
10+
const response = await axios.get(url, {responseType: 'blob'})
11+
return response?.data;
12+
}
13+
}
14+
15+
export default IconScoutApi;

client/packages/lowcoder/src/api/inviteApi.ts

+6
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export type InviteInfo = {
2020
class InviteApi extends Api {
2121
static getInviteURL = "/invitation";
2222
static acceptInviteURL = (invitationId: string) => `/invitation/${invitationId}/invite`;
23+
static sendInvitationURL = `${this.getInviteURL}/email/invite`;
2324

2425
// generate invitation
2526
static getInvite(request: GetInviteRequest): AxiosPromise<GenericApiResponse<InviteInfo>> {
@@ -36,6 +37,11 @@ class InviteApi extends Api {
3637
// the same api as getInviteInfo, method is by post
3738
return Api.get(InviteApi.acceptInviteURL(request.invitationId));
3839
}
40+
41+
// send invitations
42+
static sendInvitations(request: {emails: string[], orgId: string}): AxiosPromise<GenericApiResponse<any>> {
43+
return Api.post(InviteApi.sendInvitationURL, request);
44+
}
3945
}
4046

4147
export default InviteApi;

client/packages/lowcoder/src/api/subscriptionApi.ts

-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
import Api from "api/api";
22
import axios, { AxiosInstance, AxiosRequestConfig, CancelToken } from "axios";
3-
import { useDispatch, useSelector } from "react-redux";
4-
import { useEffect, useState} from "react";
53
import { calculateFlowCode } from "./apiUtils";
6-
import { fetchGroupsAction, fetchOrgUsersAction } from "redux/reduxActions/orgActions";
7-
import { getOrgUsers } from "redux/selectors/orgSelectors";
8-
import { AppState } from "@lowcoder-ee/redux/reducers";
94
import type {
105
LowcoderNewCustomer,
116
LowcoderSearchCustomer,

client/packages/lowcoder/src/app.tsx

+56-28
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import React from "react";
3535
import { createRoot } from "react-dom/client";
3636
import { Helmet } from "react-helmet";
3737
import { connect, Provider } from "react-redux";
38-
import { Redirect, Router, Switch } from "react-router-dom";
38+
import { Redirect, Route, Router, Switch } from "react-router-dom";
3939
import type { AppState } from "redux/reducers";
4040
import { fetchConfigAction } from "redux/reduxActions/configActions";
4141
import { fetchUserAction } from "redux/reduxActions/userActions";
@@ -60,6 +60,7 @@ import GlobalInstances from 'components/GlobalInstances';
6060
import { fetchHomeData, fetchServerSettingsAction } from "./redux/reduxActions/applicationActions";
6161
import { getNpmPackageMeta } from "./comps/utils/remote";
6262
import { packageMetaReadyAction, setLowcoderCompsLoading } from "./redux/reduxActions/npmPluginActions";
63+
import { SimpleSubscriptionContextProvider } from "./util/context/SimpleSubscriptionContext";
6364

6465
const LazyUserAuthComp = React.lazy(() => import("pages/userAuth"));
6566
const LazyInviteLanding = React.lazy(() => import("pages/common/inviteLanding"));
@@ -310,33 +311,60 @@ class AppIndex extends React.Component<AppIndexProps, any> {
310311
component={LazyPublicAppEditor}
311312
/>
312313

313-
<LazyRoute
314-
fallback="layout"
315-
path={APP_EDITOR_URL}
316-
component={LazyAppEditor}
317-
/>
318-
<LazyRoute
319-
fallback="layout"
320-
path={[
321-
USER_PROFILE_URL,
322-
NEWS_URL,
323-
ORG_HOME_URL,
324-
ALL_APPLICATIONS_URL,
325-
DATASOURCE_CREATE_URL,
326-
DATASOURCE_EDIT_URL,
327-
DATASOURCE_URL,
328-
SUPPORT_URL,
329-
QUERY_LIBRARY_URL,
330-
FOLDERS_URL,
331-
FOLDER_URL,
332-
TRASH_URL,
333-
SETTING_URL,
334-
MARKETPLACE_URL,
335-
ADMIN_APP_URL
336-
]}
337-
// component={ApplicationListPage}
338-
component={LazyApplicationHome}
339-
/>
314+
<Route
315+
path={
316+
[
317+
APP_EDITOR_URL,
318+
USER_PROFILE_URL,
319+
NEWS_URL,
320+
ORG_HOME_URL,
321+
ALL_APPLICATIONS_URL,
322+
DATASOURCE_CREATE_URL,
323+
DATASOURCE_EDIT_URL,
324+
DATASOURCE_URL,
325+
SUPPORT_URL,
326+
QUERY_LIBRARY_URL,
327+
FOLDERS_URL,
328+
FOLDER_URL,
329+
TRASH_URL,
330+
SETTING_URL,
331+
MARKETPLACE_URL,
332+
ADMIN_APP_URL
333+
]
334+
}
335+
>
336+
<SimpleSubscriptionContextProvider>
337+
<Switch>
338+
<LazyRoute
339+
fallback="layout"
340+
path={APP_EDITOR_URL}
341+
component={LazyAppEditor}
342+
/>
343+
<LazyRoute
344+
fallback="layout"
345+
path={[
346+
USER_PROFILE_URL,
347+
NEWS_URL,
348+
ORG_HOME_URL,
349+
ALL_APPLICATIONS_URL,
350+
DATASOURCE_CREATE_URL,
351+
DATASOURCE_EDIT_URL,
352+
DATASOURCE_URL,
353+
SUPPORT_URL,
354+
QUERY_LIBRARY_URL,
355+
FOLDERS_URL,
356+
FOLDER_URL,
357+
TRASH_URL,
358+
SETTING_URL,
359+
MARKETPLACE_URL,
360+
ADMIN_APP_URL
361+
]}
362+
// component={ApplicationListPage}
363+
component={LazyApplicationHome}
364+
/>
365+
</Switch>
366+
</SimpleSubscriptionContextProvider>
367+
</Route>
340368
<LazyRoute exact path={ADMIN_AUTH_URL} component={LazyUserAuthComp} />
341369
<LazyRoute path={USER_AUTH_URL} component={LazyUserAuthComp} />
342370
<LazyRoute

client/packages/lowcoder/src/components/layout/Layout.tsx

+4-2
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,10 @@ export function Layout(props: LayoutProps) {
111111
placement="right"
112112
closable={true}
113113
onClose={toggleDrawer}
114-
visible={drawerVisible}
115-
bodyStyle={{ padding: "0px" }}
114+
open={drawerVisible}
115+
styles={{
116+
body: { padding: "0px" }
117+
}}
116118
destroyOnClose // Ensure drawer content is removed when closed
117119
>
118120
<DrawerContentWrapper>

0 commit comments

Comments
 (0)