Skip to content

NU-2160-show-loader-during-dynamic-parameters-loading #8059

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 14 commits into from
May 16, 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
47 changes: 42 additions & 5 deletions designer/client/src/actions/nk/nodeDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,21 @@ import type { ThunkAction } from "../reduxTypes";
type NodeValidationUpdated = { type: "NODE_VALIDATION_UPDATED"; validationData: ValidationData; nodeId: string };
type NodeDetailsOpened = { type: "NODE_DETAILS_OPENED"; nodeId: string; windowId: string };
type NodeDetailsClosed = { type: "NODE_DETAILS_CLOSED"; nodeId: string; windowId: string };

export type NodeDetailsActions = NodeValidationUpdated | NodeDetailsOpened | NodeDetailsClosed;
type NodeValidationDynamicParametersLoading = {
type: "NODE_VALIDATION_DYNAMIC_PARAMETERS_LOADING";
nodeId: string;
dynamicParametersChanged: string[];
};
type NodeValidationDynamicParametersLoaded = {
type: "NODE_VALIDATION_DYNAMIC_PARAMETERS_LOADED";
nodeId: string;
};
export type NodeDetailsActions =
| NodeValidationUpdated
| NodeDetailsOpened
| NodeValidationDynamicParametersLoading
| NodeValidationDynamicParametersLoaded
| NodeDetailsClosed;

export interface ValidationData {
parameters?: UIParameter[];
Expand All @@ -35,6 +48,24 @@ export function nodeValidationDataUpdated(nodeId: string, validationData: Valida
};
}

export function nodeValidationDynamicParametersLoading(
nodeId: string,
dynamicParametersChanged: string[],
): NodeValidationDynamicParametersLoading {
return {
type: "NODE_VALIDATION_DYNAMIC_PARAMETERS_LOADING",
nodeId,
dynamicParametersChanged,
};
}

export function nodeValidationDynamicParametersLoaded(nodeId: string): NodeValidationDynamicParametersLoaded {
return {
type: "NODE_VALIDATION_DYNAMIC_PARAMETERS_LOADED",
nodeId,
};
}

export function nodeDetailsOpened(nodeId: string, windowId: string): NodeDetailsOpened {
return {
type: "NODE_DETAILS_OPENED",
Expand Down Expand Up @@ -69,14 +100,20 @@ const validate = debounce(
500,
);

export function validateNodeData(processName: string, validationRequestData: ValidationRequest, callback?: () => void): ThunkAction {
export function validateNodeData(
processName: string,
validationRequestData: ValidationRequest,
callback?: ({ status }: { status: "allowDataUpdate" | "unknown" }) => void,
): ThunkAction {
return (dispatch, getState) => {
validate(processName, validationRequestData, (nodeId, data) => {
const allowDataUpdate = data && getNodeDetails(getState())(nodeId);
// node details view creates this on open and removes after close
if (data && getNodeDetails(getState())(nodeId)) {
if (allowDataUpdate) {
dispatch(nodeValidationDataUpdated(nodeId, data));
callback?.();
}

callback?.({ status: allowDataUpdate ? "allowDataUpdate" : "unknown" });
});
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const getCurrentErrors = createSelector(
(originalNodeId: NodeId, nodeErrors: NodeValidationError[] = []) =>
validationPerformed(originalNodeId) ? validationErrors(originalNodeId) : nodeErrors,
);
export const getDynamicParameterDefinitions = createSelector(
export const getDynamicParameterDefinitions = createDeepEqualSelector(
getValidationPerformed,
getDetailsParameters,
getResultParameters,
Expand All @@ -78,3 +78,11 @@ export const getVariableTypes = createSelector(
getNodeResults,
(nodeResults) => (originalNodeId) => ProcessUtils.getVariablesFromValidation(nodeResults, originalNodeId) || {},
);

export const getDynamicParametersChanged =
(state: RootState) =>
(nodeId: string): string[] | undefined => {
const nodeDetails = getNodeDetails(state);
const nodeDetail = nodeDetails(nodeId);
return nodeDetail?.changingDynamicParameters;
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { IdField } from "./IdField";
import { ParametersListAdvanced } from "./parametersListAdvanced";
import type { SetProperty } from "./useNodeTypeDetailsContentLogic";

const getListFieldPath = (index: number) => `ref.parameters[${index}]`;

interface SourceSinkCommonProps {
errors: NodeValidationError[];
findAvailableVariables?: ReturnType<typeof ProcessUtils.findAvailableVariables>;
Expand Down Expand Up @@ -54,7 +56,7 @@ export const SourceSinkCommon = ({
errors={errors}
renderFieldLabel={renderFieldLabel}
setProperty={setProperty}
getListFieldPath={(index: number) => `ref.parameters[${index}]`}
getListFieldPath={getListFieldPath}
>
{children}
<DescriptionField
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,27 @@ export const GenerateNewEndpoint = ({ node, handleNewEndpointGenerated }: Props)
const handleSendHttpRequest = useCallback(async () => {
try {
const { result } = await HttpService.nodeActions(scenarioName, "generate-endpoint", node);
const newTopic = result?.actionName === "GenerateEndpointResult" ? result.topic : "";
dispatch(
validateNodeData(
scenarioName,
{
outgoingEdges: scenarioGraph.edges,
nodeData: node,
processProperties: scenarioGraph.properties,
branchVariableTypes: getBranchVariableTypes(node.id),
variableTypes,
},
() => handleNewEndpointGenerated(newTopic),
),
);
const newTopic = result?.actionName === "GENERATE_ENDPOINT" ? result?.topic?.expression : "";
await new Promise<void>((resolve) => {
dispatch(
validateNodeData(
scenarioName,
{
outgoingEdges: scenarioGraph.edges,
nodeData: node,
processProperties: scenarioGraph.properties,
branchVariableTypes: getBranchVariableTypes(node.id),
variableTypes,
},
({ status }) => {
if (status === "allowDataUpdate") {
handleNewEndpointGenerated(newTopic);
}
resolve();
},
),
);
});
} catch (error) {
console.error("Error sending request:", error);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { type SetStateAction, useCallback, useEffect, useMemo, useState } from "
import { useDispatch, useSelector } from "react-redux";
import { useDebounce } from "rooks";

import { editNode } from "../../../../actions/nk";
import { editNode, nodeValidationDynamicParametersLoaded } from "../../../../actions/nk";
import { PendingPromise } from "../../../../common/PendingPromise";
import { useUserSettings } from "../../../../common/userSettings";
import { parseWindowsQueryParams, replaceSearchQuery } from "../../../../containers/hooks/useSearchQuery";
Expand Down Expand Up @@ -93,6 +93,10 @@ export function useNodeState(data: NodeDetailsMeta): NodeState {
} catch (e) {
console.error(e);
setStatus("error");
} finally {
if (autoApply) {
dispatch(nodeValidationDynamicParametersLoaded(node.id));
}
}
},
[setStatus, dispatch, scenario, node, autoApply],
Expand Down
90 changes: 16 additions & 74 deletions designer/client/src/components/graph/node-modal/parametersList.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
import { Box } from "@mui/material";
import React from "react";
import { useTranslation } from "react-i18next";
import { Box, Skeleton } from "@mui/material";
import { isEqual } from "lodash";
import React, { Fragment } from "react";
import { useSelector } from "react-redux";

import { CopyIconButton, useCopyClipboard } from "../../../common/copyToClipboard";
import { useUserSettings } from "../../../common/userSettings";
import { getProcessState } from "../../../reducers/selectors/scenarioState";
import type { Parameter } from "../../../types";
import { getValidationErrorsForField } from "./editors/Validators";
import { GenerateNewEndpoint } from "./node-action-buttons/GenerateNewEndpoint";
import { SendRequestButton } from "./node-action-buttons/SendRequestButton";
import { getDynamicParametersChanged } from "./NodeDetailsContent/selectors";
import type { ParameterExpressionFieldProps } from "./ParameterExpressionField";
import { ParameterExpressionField } from "./ParameterExpressionField";
import { ParametersListField } from "./parametersListField";

type ParametersListItemProps = Omit<ParameterExpressionFieldProps, "listFieldPath" | "parameter">;

Expand All @@ -25,76 +20,23 @@ export type ParametersListProps = ParametersListItemProps & {
getListFieldPath: (index: number) => string;
};

export const ParametersList = ({ parameters = [], getListFieldPath, ...props }: ParametersListProps) => {
const { node } = props;
const scenarioState = useSelector(getProcessState);
const { t } = useTranslation();
const [isCopied, copy] = useCopyClipboard();
const [settings] = useUserSettings();
export const ParametersList = (props: ParametersListProps) => {
const { parameters = [], node } = props;
const dynamicParametersChanged = useSelector(getDynamicParametersChanged, isEqual)(node.id);

return (
<>
{parameters.map((paramWithIndex) => (
<React.Fragment key={node.id + paramWithIndex.param.name + paramWithIndex.index}>
<ParameterExpressionField
listFieldPath={getListFieldPath(paramWithIndex.index)}
parameter={paramWithIndex.param}
endAdornment={
paramWithIndex.param.name === "Endpoint" && (
<CopyIconButton
onClick={() => {
const possibleValues = props.parameterDefinitions.find(
(parameterDefinition) => parameterDefinition.name === "Endpoint",
).editors[0].possibleValues;

const selectedValue = possibleValues.find(
(possibleValue) => possibleValue.expression === paramWithIndex.param.expression.expression,
);
copy(selectedValue.label);
}}
isCopied={isCopied}
/>
)
}
{...props}
/>
{/*
* TODO: Remove it when the backend is ready and action buttons will be send by default
*/}
{paramWithIndex.param.name === "Endpoint" && settings["node.showGenerateEndpointButton"] && (
<Box display={"flex"} justifyContent={"flex-end"}>
<GenerateNewEndpoint
node={node}
handleNewEndpointGenerated={(topic: string) => {
const expressionProperty = "expression.expression";
const expressionPath = `${getListFieldPath(paramWithIndex.index)}${expressionProperty}`;

props.setProperty(expressionPath, topic);
}}
/>
</Box>
)}

{/*
* TODO: Remove it when the backend is ready and action buttons will be send by default
*/}
{paramWithIndex.param.name === "Data sample" && settings["node.showSendRequestButton"] && (
<Box display={"flex"} justifyContent={"flex-end"}>
<SendRequestButton
disabled={
getValidationErrorsForField(props.errors, paramWithIndex.param.name).length > 0 ||
scenarioState.status.name !== "RUNNING"
}
infoTooltip={
scenarioState.status.name !== "RUNNING" &&
t("node.actions.sendRequest.tooltip.deployScenarioFirst", "Deploy your scenario first")
}
expression={paramWithIndex.param.expression.expression}
node={node}
/>
<Fragment key={node.id + paramWithIndex.param.name + paramWithIndex.index}>
{dynamicParametersChanged?.length > 0 && !dynamicParametersChanged.includes(paramWithIndex.param.name) ? (
<Box display={"flex"} justifyContent={"space-between"} mt={2}>
<Skeleton variant="rectangular" height={15} width={"100%"} sx={{ flexBasis: "10%", mt: "9px" }} />
<Skeleton variant="rectangular" height={35} width={"100%"} sx={{ flexBasis: "80%" }} />
</Box>
) : (
<ParametersListField {...props} paramWithIndex={paramWithIndex} />
)}
</React.Fragment>
</Fragment>
))}
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { Box } from "@mui/material";
import React, { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";

import { CopyIconButton, useCopyClipboard } from "../../../common/copyToClipboard";
import { useUserSettings } from "../../../common/userSettings";
import { getProcessState } from "../../../reducers/selectors/scenarioState";
import { getValidationErrorsForField } from "./editors/Validators";
import { GenerateNewEndpoint } from "./node-action-buttons/GenerateNewEndpoint";
import { SendRequestButton } from "./node-action-buttons/SendRequestButton";
import { ParameterExpressionField } from "./ParameterExpressionField";
import type { ParametersListProps, ParameterWithIndex } from "./parametersList";

interface Props extends ParametersListProps {
paramWithIndex: ParameterWithIndex;
}

export const ParametersListField = (props: Props) => {
const { node, getListFieldPath, paramWithIndex, parameterDefinitions, setProperty } = props;
const handleGetListFieldPath = useCallback(
(index: number) => {
return getListFieldPath(index);
},
[getListFieldPath],
);

const scenarioState = useSelector(getProcessState);
const { t } = useTranslation();
const [isCopied, copy] = useCopyClipboard();
const [settings] = useUserSettings();

if (paramWithIndex.param.name === "Endpoint") {
return (
<>
<ParameterExpressionField
listFieldPath={handleGetListFieldPath(paramWithIndex.index)}
parameter={paramWithIndex.param}
endAdornment={
paramWithIndex.param.name === "Endpoint" && (
<CopyIconButton
onClick={() => {
const possibleValues = parameterDefinitions.find(
(parameterDefinition) => parameterDefinition.name === "Endpoint",
).editors[0].possibleValues;

const selectedValue = possibleValues.find(
(possibleValue) => possibleValue.expression === paramWithIndex.param.expression.expression,
);
copy(selectedValue.label);
}}
isCopied={isCopied}
/>
)
}
{...props}
/>
{/*
* TODO: Remove it when the backend is ready and action buttons will be send by default
*/}
{settings["node.showGenerateEndpointButton"] && (
<Box display={"flex"} justifyContent={"flex-end"}>
<GenerateNewEndpoint
node={node}
handleNewEndpointGenerated={(topic: string) => {
const expressionProperty = "expression.expression";
const expressionPath = `${getListFieldPath(paramWithIndex.index)}.${expressionProperty}`;

setProperty(expressionPath, topic);
}}
/>
</Box>
)}
</>
);
}

return (
<>
<ParameterExpressionField
listFieldPath={handleGetListFieldPath(paramWithIndex.index)}
parameter={paramWithIndex.param}
{...props}
/>
{paramWithIndex.param.name === "Data sample" && settings["node.showSendRequestButton"] && (
<Box display={"flex"} justifyContent={"flex-end"}>
<SendRequestButton
disabled={
getValidationErrorsForField(props.errors, paramWithIndex.param.name).length > 0 ||
scenarioState.status.name !== "RUNNING"
}
infoTooltip={
scenarioState.status.name !== "RUNNING" &&
t("node.actions.sendRequest.tooltip.deployScenarioFirst", "Deploy your scenario first")
}
expression={paramWithIndex.param.expression.expression}
node={node}
/>
</Box>
)}
</>
);
};
Loading