Skip to content

Commit 114fb38

Browse files
feat: added stream query for real time data updates
1 parent 1f233a3 commit 114fb38

File tree

9 files changed

+315
-2
lines changed

9 files changed

+315
-2
lines changed

client/packages/lowcoder/src/components/ResCreatePanel.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,13 @@ const ResButton = (props: {
161161
dataSourceId: QUICK_REST_API_ID,
162162
},
163163
},
164+
streamApi: {
165+
label: trans("query.quickStreamAPI"),
166+
type: BottomResTypeEnum.Query,
167+
extra: {
168+
compType: "streamApi",
169+
},
170+
},
164171
graphql: {
165172
label: trans("query.quickGraphql"),
166173
type: BottomResTypeEnum.Query,
@@ -318,6 +325,7 @@ export function ResCreatePanel(props: ResCreateModalProps) {
318325
<div className="section">
319326
<DataSourceListWrapper placement={placement}>
320327
<ResButton size={buttonSize} identifier={"restApi"} onSelect={onSelect} />
328+
<ResButton size={buttonSize} identifier={"streamApi"} onSelect={onSelect} />
321329
<ResButton size={buttonSize} identifier={"graphql"} onSelect={onSelect} />
322330
{placement === "editor" && (
323331
<ResButton size={buttonSize} identifier={"lowcoderApi"} onSelect={onSelect} />

client/packages/lowcoder/src/comps/queries/httpQuery/httpQueryConstants.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { HttpQuery } from "./httpQuery";
55
import styled from "styled-components";
66
import { QueryConfigItemWrapper, QueryConfigLabel, QueryConfigWrapper } from "components/query";
77
import { GraphqlQuery } from "./graphqlQuery";
8+
import { StreamQuery } from "./streamQuery";
89

910
const UrlInput = styled.div<{ hasAddonBefore: boolean }>`
1011
display: flex;
@@ -33,7 +34,7 @@ const UrlInputAddonBefore = styled.div`
3334
`;
3435

3536
export const HttpPathPropertyView = (props: {
36-
comp: InstanceType<typeof HttpQuery | typeof GraphqlQuery>;
37+
comp: InstanceType<typeof HttpQuery | typeof GraphqlQuery | typeof StreamQuery>;
3738
datasourceId: string;
3839
urlPlaceholder?: string;
3940
}) => {
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
import { ControlPropertyViewWrapper } from "components/control";
2+
import { Input } from "components/Input";
3+
import { KeyValueList } from "components/keyValueList";
4+
import { QueryConfigItemWrapper, QueryConfigLabel, QueryConfigWrapper } from "components/query";
5+
import { simpleMultiComp } from "comps/generators/multi";
6+
import { ReactNode } from "react";
7+
import { JSONValue } from "../../../util/jsonTypes";
8+
import { keyValueListControl } from "../../controls/keyValueControl";
9+
import { ParamsJsonControl, ParamsStringControl } from "../../controls/paramsControl";
10+
import { list } from "../../generators/list";
11+
import { valueComp, withDefault } from "../../generators/simpleGenerators";
12+
import { FunctionProperty, toQueryView } from "../queryCompUtils";
13+
import {
14+
HttpHeaderPropertyView,
15+
HttpParametersPropertyView,
16+
HttpPathPropertyView,
17+
} from "./httpQueryConstants";
18+
import { QueryResult } from "../queryComp";
19+
import { QUERY_EXECUTION_ERROR, QUERY_EXECUTION_OK } from "constants/queryConstants";
20+
import { FunctionControl } from "comps/controls/codeControl";
21+
22+
const connect = async (socket: WebSocket, timeout = 10000) => {
23+
const isOpened = () => (socket.readyState === WebSocket.OPEN)
24+
25+
if (socket.readyState !== WebSocket.CONNECTING) {
26+
return isOpened()
27+
}
28+
else {
29+
const intrasleep = 100
30+
const ttl = timeout / intrasleep // time to loop
31+
let loop = 0
32+
while (socket.readyState === WebSocket.CONNECTING && loop < ttl) {
33+
await new Promise(resolve => setTimeout(resolve, intrasleep))
34+
loop++
35+
}
36+
return isOpened()
37+
}
38+
}
39+
40+
const childrenMap = {
41+
path: ParamsStringControl,
42+
destroySocketConnection: FunctionControl,
43+
};
44+
45+
const StreamTmpQuery = simpleMultiComp(childrenMap);
46+
47+
export class StreamQuery extends StreamTmpQuery {
48+
private socket: WebSocket | undefined;
49+
50+
override getView() {
51+
return async (
52+
p: {
53+
args?: Record<string, unknown>,
54+
callback?: (result: QueryResult) => void
55+
}
56+
): Promise<QueryResult> => {
57+
const children = this.children;
58+
59+
try {
60+
const timer = performance.now();
61+
this.socket = new WebSocket(children.path.children.text.getView());
62+
63+
this.socket.onopen = function(e) {
64+
console.log("[open] Connection established");
65+
};
66+
67+
this.socket.onmessage = function(event) {
68+
console.log(`[message] Data received from server: ${event.data}`);
69+
if(typeof JSON.parse(event.data) === 'object') {
70+
const result = {
71+
data: JSON.parse(event.data),
72+
code: QUERY_EXECUTION_OK,
73+
success: true,
74+
runTime: Number((performance.now() - timer).toFixed()),
75+
}
76+
p?.callback?.(result);
77+
}
78+
};
79+
80+
this.socket.onclose = function(event) {
81+
if (event.wasClean) {
82+
console.log(`[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`);
83+
} else {
84+
console.log('[close] Connection died');
85+
}
86+
};
87+
88+
this.socket.onerror = function(error) {
89+
throw new Error(error as any)
90+
};
91+
92+
const isConnectionOpen = await connect(this.socket);
93+
if(!isConnectionOpen) {
94+
return {
95+
success: false,
96+
data: "",
97+
code: QUERY_EXECUTION_ERROR,
98+
message: "Socket connection failed",
99+
};
100+
}
101+
102+
return {
103+
data: "",
104+
code: QUERY_EXECUTION_OK,
105+
success: true,
106+
runTime: Number((performance.now() - timer).toFixed()),
107+
};
108+
} catch (e) {
109+
return {
110+
success: false,
111+
data: "",
112+
code: QUERY_EXECUTION_ERROR,
113+
message: (e as any).message || "",
114+
};
115+
}
116+
};
117+
}
118+
119+
propertyView(props: { datasourceId: string }) {
120+
return <PropertyView {...props} comp={this} />;
121+
}
122+
123+
destroy() {
124+
this.socket?.close();
125+
}
126+
}
127+
128+
const PropertyView = (props: { comp: InstanceType<typeof StreamQuery>; datasourceId: string }) => {
129+
const { comp } = props;
130+
131+
return (
132+
<>
133+
<HttpPathPropertyView
134+
{...props}
135+
comp={comp}
136+
urlPlaceholder="wss://www.example.com/socketserver"
137+
/>
138+
</>
139+
);
140+
};
141+
142+
143+
144+
// import { ParamsStringControl } from "comps/controls/paramsControl";
145+
// import { FunctionControl, StringControl, codeControl } from "comps/controls/codeControl";
146+
// import { MultiCompBuilder } from "comps/generators";
147+
// import { QueryResult } from "../queryComp";
148+
// import { QueryTutorials } from "util/tutorialUtils";
149+
// import { DocLink } from "lowcoder-design";
150+
// import { getGlobalSettings } from "comps/utils/globalSettings";
151+
// import { trans } from "i18n";
152+
// import { QUERY_EXECUTION_ERROR, QUERY_EXECUTION_OK } from "constants/queryConstants";
153+
154+
// const connect = async (socket: WebSocket, timeout = 10000) => {
155+
// const isOpened = () => (socket.readyState === WebSocket.OPEN)
156+
157+
// if (socket.readyState !== WebSocket.CONNECTING) {
158+
// return isOpened()
159+
// }
160+
// else {
161+
// const intrasleep = 100
162+
// const ttl = timeout / intrasleep // time to loop
163+
// let loop = 0
164+
// while (socket.readyState === WebSocket.CONNECTING && loop < ttl) {
165+
// await new Promise(resolve => setTimeout(resolve, intrasleep))
166+
// loop++
167+
// }
168+
// return isOpened()
169+
// }
170+
// }
171+
172+
// export const StreamQuery = (function () {
173+
// const childrenMap = {
174+
// path: StringControl,
175+
// destroySocketConnection: FunctionControl,
176+
// };
177+
// return new MultiCompBuilder(childrenMap, (props) => {
178+
// const { orgCommonSettings } = getGlobalSettings();
179+
// const runInHost = !!orgCommonSettings?.runJavaScriptInHost;
180+
181+
// console.log(props.path);
182+
// return async (
183+
// p: {
184+
// args?: Record<string, unknown>,
185+
// callback?: (result: QueryResult) => void
186+
// }
187+
// ): Promise<QueryResult> => {
188+
// console.log('Stream Query', props)
189+
190+
// try {
191+
// const timer = performance.now();
192+
// // const url = 'wss://free.blr2.piesocket.com/v3/1?api_key=yWUvGQggacrrTdXYjvTpRD5qhm4RIsglS7YJlKzp&notify_self=1'
193+
// const socket = new WebSocket(props.path);
194+
195+
// props.destroySocketConnection = () => {
196+
// socket.close();
197+
// };
198+
199+
// socket.onopen = function(e) {
200+
// console.log("[open] Connection established");
201+
// };
202+
203+
// socket.onmessage = function(event) {
204+
// console.log(`[message] Data received from server: ${event.data}`);
205+
// console.log(JSON.parse(event.data))
206+
// if(typeof JSON.parse(event.data) === 'object') {
207+
// const result = {
208+
// data: JSON.parse(event.data),
209+
// code: QUERY_EXECUTION_OK,
210+
// success: true,
211+
// runTime: Number((performance.now() - timer).toFixed()),
212+
// }
213+
// p?.callback?.(result);
214+
// }
215+
// };
216+
217+
// socket.onclose = function(event) {
218+
// if (event.wasClean) {
219+
// console.log(`[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`);
220+
// } else {
221+
// // e.g. server process killed or network down
222+
// // event.code is usually 1006 in this case
223+
// console.log('[close] Connection died');
224+
// }
225+
// };
226+
227+
// socket.onerror = function(error) {
228+
// throw new Error(error as any)
229+
// };
230+
// const isConnectionOpen = await connect(socket);
231+
// if(!isConnectionOpen) {
232+
// return {
233+
// success: false,
234+
// data: "",
235+
// code: QUERY_EXECUTION_ERROR,
236+
// message: "Socket connection failed",
237+
// };
238+
// }
239+
240+
// // const data = await props.script(p.args, runInHost);
241+
// return {
242+
// data: "",
243+
// code: QUERY_EXECUTION_OK,
244+
// success: true,
245+
// runTime: Number((performance.now() - timer).toFixed()),
246+
// };
247+
// } catch (e) {
248+
// return {
249+
// success: false,
250+
// data: "",
251+
// code: QUERY_EXECUTION_ERROR,
252+
// message: (e as any).message || "",
253+
// };
254+
// }
255+
// };
256+
// })
257+
// .setPropertyViewFn((children) => {
258+
// return (
259+
// <>
260+
// {
261+
// children.path.propertyView({
262+
// label: "URL",
263+
// placement: "bottom",
264+
// placeholder:"wss://www.example.com/socketserver",
265+
// })
266+
// }
267+
268+
// {/* TODO: Add docs for Stream Query
269+
// {QueryTutorials.js && (
270+
// <DocLink href={QueryTutorials.js}>{trans("query.jsQueryDocLink")}</DocLink>
271+
// )} */}
272+
// </>
273+
// );
274+
// })
275+
// .build();
276+
// })();

client/packages/lowcoder/src/comps/queries/queryComp.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import { millisecondsControl } from "../controls/millisecondControl";
6969
import { paramsMillisecondsControl } from "../controls/paramsControl";
7070
import { NameConfig, withExposingConfigs } from "../generators/withExposing";
7171
import { HttpQuery } from "./httpQuery/httpQuery";
72+
import { StreamQuery } from "./httpQuery/streamQuery";
7273
import { QueryConfirmationModal } from "./queryComp/queryConfirmationModal";
7374
import { QueryNotificationControl } from "./queryComp/queryNotificationControl";
7475
import { QueryPropertyView } from "./queryComp/queryPropertyView";
@@ -419,6 +420,7 @@ QueryCompTmp = class extends QueryCompTmp {
419420
applicationPath: parentApplicationPath,
420421
args: action.args,
421422
timeout: this.children.timeout,
423+
callback: (result) => this.processResult(result, action, startTime)
422424
});
423425
}, getTriggerType(this) === "manual")
424426
.then(
@@ -517,6 +519,7 @@ QueryCompTmp = class extends QueryCompTmp implements BottomResComp {
517519
switch (type) {
518520
case "js":
519521
case "restApi":
522+
case "streamApi":
520523
case "mongodb":
521524
case "redis":
522525
case "es":
@@ -708,6 +711,9 @@ class QueryListComp extends QueryListTmpComp implements BottomResListComp {
708711
],
709712
})
710713
);
714+
if(toDelQuery.children.compType.getView() === 'streamApi') {
715+
(toDelQuery.children.comp as StreamQuery)?.destroy();
716+
}
711717
messageInstance.success(trans("query.deleteSuccessMessage", { undoKey }));
712718
}
713719

client/packages/lowcoder/src/comps/queries/queryComp/queryPropertyView.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ export const QueryGeneralPropertyView = (props: {
307307
[
308308
{
309309
label:
310-
children.compType.getView() === "js"
310+
(children.compType.getView() === "js" || children.compType.getView() === "streamApi")
311311
? trans("query.triggerTypePageLoad")
312312
: trans("query.triggerTypeAuto"),
313313
value: "automatic",
@@ -363,6 +363,7 @@ function useDatasourceStatus(datasourceId: string, datasourceType: ResourceType)
363363
return useMemo(() => {
364364
if (
365365
datasourceType === "js" ||
366+
datasourceType === "streamApi" ||
366367
datasourceType === "libraryQuery" ||
367368
datasourceId === QUICK_REST_API_ID ||
368369
datasourceId === QUICK_GRAPHQL_ID ||

client/packages/lowcoder/src/comps/queries/resourceDropdown.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ const QuickRestAPIValue: ResourceOptionValue = {
9191
type: "restApi",
9292
};
9393

94+
const QuickStreamAPIValue: ResourceOptionValue = {
95+
id: "",
96+
type: "streamApi",
97+
};
98+
9499
const QuickGraphqlValue: ResourceOptionValue = {
95100
id: QUICK_GRAPHQL_ID,
96101
type: "graphql",
@@ -254,6 +259,17 @@ export const ResourceDropdown = (props: ResourceDropdownProps) => {
254259
</SelectOptionContains>
255260
</SelectOption>
256261

262+
<SelectOption
263+
key={JSON.stringify(QuickStreamAPIValue)}
264+
label={trans("query.quickStreamAPI")}
265+
value={JSON.stringify(QuickStreamAPIValue)}
266+
>
267+
<SelectOptionContains>
268+
{getBottomResIcon("restApi")}
269+
<SelectOptionLabel>{trans("query.quickStreamAPI")} </SelectOptionLabel>
270+
</SelectOptionContains>
271+
</SelectOption>
272+
257273
<SelectOption
258274
key={JSON.stringify(QuickGraphqlValue)}
259275
label={trans("query.quickGraphql")}

0 commit comments

Comments
 (0)