Skip to content

Echarts extension #372

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 5 commits into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
feat: added stream query for real time data updates
  • Loading branch information
raheeliftikhar5 committed Sep 13, 2023
commit 114fb380cc506a25f20bf97681e71fe8915e8d96
8 changes: 8 additions & 0 deletions client/packages/lowcoder/src/components/ResCreatePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,13 @@ const ResButton = (props: {
dataSourceId: QUICK_REST_API_ID,
},
},
streamApi: {
label: trans("query.quickStreamAPI"),
type: BottomResTypeEnum.Query,
extra: {
compType: "streamApi",
},
},
graphql: {
label: trans("query.quickGraphql"),
type: BottomResTypeEnum.Query,
Expand Down Expand Up @@ -318,6 +325,7 @@ export function ResCreatePanel(props: ResCreateModalProps) {
<div className="section">
<DataSourceListWrapper placement={placement}>
<ResButton size={buttonSize} identifier={"restApi"} onSelect={onSelect} />
<ResButton size={buttonSize} identifier={"streamApi"} onSelect={onSelect} />
<ResButton size={buttonSize} identifier={"graphql"} onSelect={onSelect} />
{placement === "editor" && (
<ResButton size={buttonSize} identifier={"lowcoderApi"} onSelect={onSelect} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { HttpQuery } from "./httpQuery";
import styled from "styled-components";
import { QueryConfigItemWrapper, QueryConfigLabel, QueryConfigWrapper } from "components/query";
import { GraphqlQuery } from "./graphqlQuery";
import { StreamQuery } from "./streamQuery";

const UrlInput = styled.div<{ hasAddonBefore: boolean }>`
display: flex;
Expand Down Expand Up @@ -33,7 +34,7 @@ const UrlInputAddonBefore = styled.div`
`;

export const HttpPathPropertyView = (props: {
comp: InstanceType<typeof HttpQuery | typeof GraphqlQuery>;
comp: InstanceType<typeof HttpQuery | typeof GraphqlQuery | typeof StreamQuery>;
datasourceId: string;
urlPlaceholder?: string;
}) => {
Expand Down
276 changes: 276 additions & 0 deletions client/packages/lowcoder/src/comps/queries/httpQuery/streamQuery.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
import { ControlPropertyViewWrapper } from "components/control";
import { Input } from "components/Input";
import { KeyValueList } from "components/keyValueList";
import { QueryConfigItemWrapper, QueryConfigLabel, QueryConfigWrapper } from "components/query";
import { simpleMultiComp } from "comps/generators/multi";
import { ReactNode } from "react";
import { JSONValue } from "../../../util/jsonTypes";
import { keyValueListControl } from "../../controls/keyValueControl";
import { ParamsJsonControl, ParamsStringControl } from "../../controls/paramsControl";
import { list } from "../../generators/list";
import { valueComp, withDefault } from "../../generators/simpleGenerators";
import { FunctionProperty, toQueryView } from "../queryCompUtils";
import {
HttpHeaderPropertyView,
HttpParametersPropertyView,
HttpPathPropertyView,
} from "./httpQueryConstants";
import { QueryResult } from "../queryComp";
import { QUERY_EXECUTION_ERROR, QUERY_EXECUTION_OK } from "constants/queryConstants";
import { FunctionControl } from "comps/controls/codeControl";

const connect = async (socket: WebSocket, timeout = 10000) => {
const isOpened = () => (socket.readyState === WebSocket.OPEN)

if (socket.readyState !== WebSocket.CONNECTING) {
return isOpened()
}
else {
const intrasleep = 100
const ttl = timeout / intrasleep // time to loop
let loop = 0
while (socket.readyState === WebSocket.CONNECTING && loop < ttl) {
await new Promise(resolve => setTimeout(resolve, intrasleep))
loop++
}
return isOpened()
}
}

const childrenMap = {
path: ParamsStringControl,
destroySocketConnection: FunctionControl,
};

const StreamTmpQuery = simpleMultiComp(childrenMap);

export class StreamQuery extends StreamTmpQuery {
private socket: WebSocket | undefined;

override getView() {
return async (
p: {
args?: Record<string, unknown>,
callback?: (result: QueryResult) => void
}
): Promise<QueryResult> => {
const children = this.children;

try {
const timer = performance.now();
this.socket = new WebSocket(children.path.children.text.getView());

this.socket.onopen = function(e) {
console.log("[open] Connection established");
};

this.socket.onmessage = function(event) {
console.log(`[message] Data received from server: ${event.data}`);
if(typeof JSON.parse(event.data) === 'object') {
const result = {
data: JSON.parse(event.data),
code: QUERY_EXECUTION_OK,
success: true,
runTime: Number((performance.now() - timer).toFixed()),
}
p?.callback?.(result);
}
};

this.socket.onclose = function(event) {
if (event.wasClean) {
console.log(`[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`);
} else {
console.log('[close] Connection died');
}
};

this.socket.onerror = function(error) {
throw new Error(error as any)
};

const isConnectionOpen = await connect(this.socket);
if(!isConnectionOpen) {
return {
success: false,
data: "",
code: QUERY_EXECUTION_ERROR,
message: "Socket connection failed",
};
}

return {
data: "",
code: QUERY_EXECUTION_OK,
success: true,
runTime: Number((performance.now() - timer).toFixed()),
};
} catch (e) {
return {
success: false,
data: "",
code: QUERY_EXECUTION_ERROR,
message: (e as any).message || "",
};
}
};
}

propertyView(props: { datasourceId: string }) {
return <PropertyView {...props} comp={this} />;
}

destroy() {
this.socket?.close();
}
}

const PropertyView = (props: { comp: InstanceType<typeof StreamQuery>; datasourceId: string }) => {
const { comp } = props;

return (
<>
<HttpPathPropertyView
{...props}
comp={comp}
urlPlaceholder="wss://www.example.com/socketserver"
/>
</>
);
};



// import { ParamsStringControl } from "comps/controls/paramsControl";
// import { FunctionControl, StringControl, codeControl } from "comps/controls/codeControl";
// import { MultiCompBuilder } from "comps/generators";
// import { QueryResult } from "../queryComp";
// import { QueryTutorials } from "util/tutorialUtils";
// import { DocLink } from "lowcoder-design";
// import { getGlobalSettings } from "comps/utils/globalSettings";
// import { trans } from "i18n";
// import { QUERY_EXECUTION_ERROR, QUERY_EXECUTION_OK } from "constants/queryConstants";

// const connect = async (socket: WebSocket, timeout = 10000) => {
// const isOpened = () => (socket.readyState === WebSocket.OPEN)

// if (socket.readyState !== WebSocket.CONNECTING) {
// return isOpened()
// }
// else {
// const intrasleep = 100
// const ttl = timeout / intrasleep // time to loop
// let loop = 0
// while (socket.readyState === WebSocket.CONNECTING && loop < ttl) {
// await new Promise(resolve => setTimeout(resolve, intrasleep))
// loop++
// }
// return isOpened()
// }
// }

// export const StreamQuery = (function () {
// const childrenMap = {
// path: StringControl,
// destroySocketConnection: FunctionControl,
// };
// return new MultiCompBuilder(childrenMap, (props) => {
// const { orgCommonSettings } = getGlobalSettings();
// const runInHost = !!orgCommonSettings?.runJavaScriptInHost;

// console.log(props.path);
// return async (
// p: {
// args?: Record<string, unknown>,
// callback?: (result: QueryResult) => void
// }
// ): Promise<QueryResult> => {
// console.log('Stream Query', props)

// try {
// const timer = performance.now();
// // const url = 'wss://free.blr2.piesocket.com/v3/1?api_key=yWUvGQggacrrTdXYjvTpRD5qhm4RIsglS7YJlKzp&notify_self=1'
// const socket = new WebSocket(props.path);

// props.destroySocketConnection = () => {
// socket.close();
// };

// socket.onopen = function(e) {
// console.log("[open] Connection established");
// };

// socket.onmessage = function(event) {
// console.log(`[message] Data received from server: ${event.data}`);
// console.log(JSON.parse(event.data))
// if(typeof JSON.parse(event.data) === 'object') {
// const result = {
// data: JSON.parse(event.data),
// code: QUERY_EXECUTION_OK,
// success: true,
// runTime: Number((performance.now() - timer).toFixed()),
// }
// p?.callback?.(result);
// }
// };

// socket.onclose = function(event) {
// if (event.wasClean) {
// console.log(`[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`);
// } else {
// // e.g. server process killed or network down
// // event.code is usually 1006 in this case
// console.log('[close] Connection died');
// }
// };

// socket.onerror = function(error) {
// throw new Error(error as any)
// };
// const isConnectionOpen = await connect(socket);
// if(!isConnectionOpen) {
// return {
// success: false,
// data: "",
// code: QUERY_EXECUTION_ERROR,
// message: "Socket connection failed",
// };
// }

// // const data = await props.script(p.args, runInHost);
// return {
// data: "",
// code: QUERY_EXECUTION_OK,
// success: true,
// runTime: Number((performance.now() - timer).toFixed()),
// };
// } catch (e) {
// return {
// success: false,
// data: "",
// code: QUERY_EXECUTION_ERROR,
// message: (e as any).message || "",
// };
// }
// };
// })
// .setPropertyViewFn((children) => {
// return (
// <>
// {
// children.path.propertyView({
// label: "URL",
// placement: "bottom",
// placeholder:"wss://www.example.com/socketserver",
// })
// }

// {/* TODO: Add docs for Stream Query
// {QueryTutorials.js && (
// <DocLink href={QueryTutorials.js}>{trans("query.jsQueryDocLink")}</DocLink>
// )} */}
// </>
// );
// })
// .build();
// })();
6 changes: 6 additions & 0 deletions client/packages/lowcoder/src/comps/queries/queryComp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import { millisecondsControl } from "../controls/millisecondControl";
import { paramsMillisecondsControl } from "../controls/paramsControl";
import { NameConfig, withExposingConfigs } from "../generators/withExposing";
import { HttpQuery } from "./httpQuery/httpQuery";
import { StreamQuery } from "./httpQuery/streamQuery";
import { QueryConfirmationModal } from "./queryComp/queryConfirmationModal";
import { QueryNotificationControl } from "./queryComp/queryNotificationControl";
import { QueryPropertyView } from "./queryComp/queryPropertyView";
Expand Down Expand Up @@ -419,6 +420,7 @@ QueryCompTmp = class extends QueryCompTmp {
applicationPath: parentApplicationPath,
args: action.args,
timeout: this.children.timeout,
callback: (result) => this.processResult(result, action, startTime)
});
}, getTriggerType(this) === "manual")
.then(
Expand Down Expand Up @@ -517,6 +519,7 @@ QueryCompTmp = class extends QueryCompTmp implements BottomResComp {
switch (type) {
case "js":
case "restApi":
case "streamApi":
case "mongodb":
case "redis":
case "es":
Expand Down Expand Up @@ -708,6 +711,9 @@ class QueryListComp extends QueryListTmpComp implements BottomResListComp {
],
})
);
if(toDelQuery.children.compType.getView() === 'streamApi') {
(toDelQuery.children.comp as StreamQuery)?.destroy();
}
messageInstance.success(trans("query.deleteSuccessMessage", { undoKey }));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ export const QueryGeneralPropertyView = (props: {
[
{
label:
children.compType.getView() === "js"
(children.compType.getView() === "js" || children.compType.getView() === "streamApi")
? trans("query.triggerTypePageLoad")
: trans("query.triggerTypeAuto"),
value: "automatic",
Expand Down Expand Up @@ -363,6 +363,7 @@ function useDatasourceStatus(datasourceId: string, datasourceType: ResourceType)
return useMemo(() => {
if (
datasourceType === "js" ||
datasourceType === "streamApi" ||
datasourceType === "libraryQuery" ||
datasourceId === QUICK_REST_API_ID ||
datasourceId === QUICK_GRAPHQL_ID ||
Expand Down
16 changes: 16 additions & 0 deletions client/packages/lowcoder/src/comps/queries/resourceDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ const QuickRestAPIValue: ResourceOptionValue = {
type: "restApi",
};

const QuickStreamAPIValue: ResourceOptionValue = {
id: "",
type: "streamApi",
};

const QuickGraphqlValue: ResourceOptionValue = {
id: QUICK_GRAPHQL_ID,
type: "graphql",
Expand Down Expand Up @@ -254,6 +259,17 @@ export const ResourceDropdown = (props: ResourceDropdownProps) => {
</SelectOptionContains>
</SelectOption>

<SelectOption
key={JSON.stringify(QuickStreamAPIValue)}
label={trans("query.quickStreamAPI")}
value={JSON.stringify(QuickStreamAPIValue)}
>
<SelectOptionContains>
{getBottomResIcon("restApi")}
<SelectOptionLabel>{trans("query.quickStreamAPI")} </SelectOptionLabel>
</SelectOptionContains>
</SelectOption>

<SelectOption
key={JSON.stringify(QuickGraphqlValue)}
label={trans("query.quickGraphql")}
Expand Down
Loading