Skip to content

Commit 3bbd821

Browse files
committed
[Nu-7702] Warn user when remote scenario version is newer
1 parent 7272bb0 commit 3bbd821

File tree

14 files changed

+708
-33
lines changed

14 files changed

+708
-33
lines changed

designer/client/src/components/modals/GenericConfirmDialog.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { css, cx } from "@emotion/css";
2-
import { WindowButtonProps, WindowContentProps } from "@touk/window-manager";
3-
import React, { PropsWithChildren, useMemo } from "react";
4-
import { useTranslation } from "react-i18next";
5-
import { PromptContent, WindowKind } from "../../windowManager";
62
import { Typography } from "@mui/material";
3+
import type { WindowButtonProps, WindowContentProps } from "@touk/window-manager";
4+
import type { PropsWithChildren} from "react";
5+
import React, { useMemo } from "react";
6+
import { useTranslation } from "react-i18next";
7+
8+
import type { WindowKind } from "../../windowManager";
9+
import { PromptContent } from "../../windowManager";
710
import { LoadingButtonTypes } from "../../windowManager/LoadingButton";
811

912
export interface ConfirmDialogData {
@@ -12,6 +15,7 @@ export interface ConfirmDialogData {
1215
denyText?: string;
1316
//TODO: get rid of callbacks in store
1417
onConfirmCallback: (confirmed: boolean) => void;
18+
width?: number;
1519
}
1620

1721
export function GenericConfirmDialog({

designer/client/src/components/toolbars/process/buttons/SaveButton.tsx

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,69 @@
1+
import { defaults } from "lodash";
12
import React from "react";
23
import { useTranslation } from "react-i18next";
34
import { useSelector } from "react-redux";
5+
46
import Icon from "../../../../assets/img/toolbarButtons/save.svg";
5-
import { getProcessName, getProcessUnsavedNewName, isProcessRenamed, isSaveDisabled } from "../../../../reducers/selectors/graph";
7+
import HttpService from "../../../../http/HttpService";
8+
import {
9+
getProcessName,
10+
getProcessUnsavedNewName,
11+
getProcessVersionId,
12+
isProcessRenamed,
13+
isSaveDisabled,
14+
} from "../../../../reducers/selectors/graph";
615
import { getCapabilities } from "../../../../reducers/selectors/other";
716
import { useWindows, WindowKind } from "../../../../windowManager";
817
import { ToolbarButton } from "../../../toolbarComponents/toolbarButtons";
9-
import { ToolbarButtonProps } from "../../types";
18+
import type { ToolbarButtonProps } from "../../types";
1019
function SaveButton(props: ToolbarButtonProps): JSX.Element {
1120
const { t } = useTranslation();
1221
const { disabled, type } = props;
1322
const capabilities = useSelector(getCapabilities);
1423
const saveDisabled = useSelector(isSaveDisabled);
1524

1625
const processName = useSelector(getProcessName);
26+
const processVersionId = useSelector(getProcessVersionId);
1727
const unsavedNewName = useSelector(getProcessUnsavedNewName);
1828
const isRenamed = useSelector(isProcessRenamed);
1929
const title = isRenamed
2030
? t("saveProcess.renameTitle", "Save scenario as {{name}}", { name: unsavedNewName })
2131
: t("saveProcess.title", "Save scenario {{name}}", { name: processName });
2232

23-
const { open } = useWindows();
24-
const onClick = () =>
25-
open({
26-
title,
27-
isModal: true,
28-
shouldCloseOnEsc: true,
29-
kind: WindowKind.saveProcess,
33+
const { open, confirm } = useWindows();
34+
const onClick = async () => {
35+
await HttpService.validateProcessVersion(processName, processVersionId).then((res) => {
36+
if (!res.data.isLatest) {
37+
confirm({
38+
text: t(
39+
"panels.actions.confirm-unsafe-save.message",
40+
`Your local scenario version #${processVersionId} is outdated.
41+
There is newer version #${res.data.latestVersion} created by ${res.data.modifiedBy} available. Are you sure you want to override it?`,
42+
),
43+
confirmText: t("panels.actions.confirm-unsafe-save.confirmButton", "Confirm"),
44+
denyText: t("panels.actions.confirm-unsafe-save.cancelButton", "Cancel"),
45+
onConfirmCallback: (confirmed) => {
46+
if (confirmed) {
47+
open({
48+
title,
49+
isModal: true,
50+
shouldCloseOnEsc: true,
51+
kind: WindowKind.saveProcess,
52+
});
53+
}
54+
},
55+
width: window.innerWidth / 3,
56+
});
57+
} else {
58+
open({
59+
title,
60+
isModal: true,
61+
shouldCloseOnEsc: true,
62+
kind: WindowKind.saveProcess,
63+
});
64+
}
3065
});
66+
};
3167

3268
const available = !disabled && !saveDisabled && capabilities.write;
3369

designer/client/src/components/toolbars/scenarioActions/buttons/DeployButton.tsx

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,27 @@
11
import React from "react";
22
import { useTranslation } from "react-i18next";
33
import { useDispatch, useSelector } from "react-redux";
4+
45
import { disableToolTipsHighlight, enableToolTipsHighlight, loadProcessState } from "../../../../actions/nk";
56
import Icon from "../../../../assets/img/toolbarButtons/deploy.svg";
6-
import HttpService, { NodesDeploymentData } from "../../../../http/HttpService";
7+
import type { NodesDeploymentData } from "../../../../http/HttpService";
8+
import HttpService from "../../../../http/HttpService";
79
import {
810
getProcessName,
11+
getProcessVersionId,
912
hasError,
1013
isDeployPossible,
1114
isSaveDisabled,
1215
isValidationResultPresent,
1316
} from "../../../../reducers/selectors/graph";
1417
import { getCapabilities } from "../../../../reducers/selectors/other";
18+
import { ACTION_DIALOG_WIDTH } from "../../../../stylesheets/variables";
1519
import { useWindows } from "../../../../windowManager";
1620
import { WindowKind } from "../../../../windowManager";
17-
import { ToggleProcessActionModalData } from "../../../modals/DeployProcessDialog";
21+
import type { ToggleProcessActionModalData } from "../../../modals/DeployProcessDialog";
22+
import type { ProcessName, ProcessVersionId } from "../../../Process/types";
1823
import { ToolbarButton } from "../../../toolbarComponents/toolbarButtons";
19-
import { ToolbarButtonProps } from "../../types";
20-
import { ACTION_DIALOG_WIDTH } from "../../../../stylesheets/variables";
21-
import { ProcessName, ProcessVersionId } from "../../../Process/types";
24+
import type { ToolbarButtonProps } from "../../types";
2225

2326
export default function DeployButton(props: ToolbarButtonProps) {
2427
const dispatch = useDispatch();
@@ -27,6 +30,7 @@ export default function DeployButton(props: ToolbarButtonProps) {
2730
const hasErrors = useSelector(hasError);
2831
const validationResultPresent = useSelector(isValidationResultPresent);
2932
const processName = useSelector(getProcessName);
33+
const processVersionId = useSelector(getProcessVersionId);
3034
const capabilities = useSelector(getCapabilities);
3135
const { disabled, type } = props;
3236

@@ -42,26 +46,54 @@ export default function DeployButton(props: ToolbarButtonProps) {
4246
const deployMouseOver = hasErrors ? () => dispatch(enableToolTipsHighlight()) : null;
4347
const deployMouseOut = hasErrors ? () => dispatch(disableToolTipsHighlight()) : null;
4448

45-
const { open } = useWindows();
49+
const { open, confirm } = useWindows();
4650

4751
const message = t("panels.actions.deploy.dialog", "Deploy scenario {{name}}", { name: processName });
4852
const action = (name: ProcessName, versionId: ProcessVersionId, comment: string, nodesDeploymentData?: NodesDeploymentData) =>
4953
HttpService.deploy(name, comment, nodesDeploymentData).finally(() => dispatch(loadProcessState(name, versionId)));
5054

51-
return (
52-
<ToolbarButton
53-
name={t("panels.actions.deploy.button", "deploy")}
54-
disabled={!available}
55-
icon={<Icon />}
56-
title={deployToolTip}
57-
onClick={() =>
55+
const handleOnClick = async () => {
56+
await HttpService.validateProcessVersion(processName, processVersionId).then((res) => {
57+
if (!res.data.isLatest) {
58+
confirm({
59+
text: t(
60+
"panels.actions.confirm-unsafe-deployment.message",
61+
`There is newer version #${res.data.latestVersion} created by ${res.data.modifiedBy} available. Scenario will be deployed using the newest version.
62+
You're currently checked out on version #${res.data.localVersion}.
63+
Are you sure you want to perform this action?`,
64+
),
65+
confirmText: t("panels.actions.confirm-unsafe-deployment.confirmButton", "Confirm"),
66+
denyText: t("panels.actions.confirm-unsafe-deployment.cancelButton", "Cancel"),
67+
onConfirmCallback: (confirmed) => {
68+
if (confirmed) {
69+
open<ToggleProcessActionModalData>({
70+
title: message,
71+
kind: WindowKind.deployWithParameters,
72+
width: ACTION_DIALOG_WIDTH,
73+
meta: { action, displayWarnings: true },
74+
});
75+
}
76+
},
77+
width: window.innerWidth / 3,
78+
});
79+
} else {
5880
open<ToggleProcessActionModalData>({
5981
title: message,
6082
kind: WindowKind.deployWithParameters,
6183
width: ACTION_DIALOG_WIDTH,
6284
meta: { action, displayWarnings: true },
63-
})
85+
});
6486
}
87+
});
88+
};
89+
90+
return (
91+
<ToolbarButton
92+
name={t("panels.actions.deploy.button", "deploy")}
93+
disabled={!available}
94+
icon={<Icon />}
95+
title={deployToolTip}
96+
onClick={handleOnClick}
6597
onMouseOver={deployMouseOver}
6698
onMouseOut={deployMouseOut}
6799
type={type}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export type ProcessVersionValidationResponse = {
2+
processName: string;
3+
isLatest: boolean;
4+
localVersion: number;
5+
latestDeployedVersion: number | null;
6+
latestVersion: number;
7+
modifiedBy: string;
8+
};

designer/client/src/http/HttpService.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { handleAxiosError } from "../devHelpers";
3131
import { Dimensions, StickyNote } from "../common/StickyNote";
3232
import { STICKY_NOTE_DEFAULT_COLOR } from "../components/graph/EspNode/stickyNote";
3333
import { ScenarioActionResult, ScenarioActionResultType } from "../components/toolbars/scenarioActions/buttons/types";
34+
import { ProcessVersionValidationResponse } from "../components/versionControl/types";
3435

3536
type HealthCheckProcessDeploymentType = {
3637
status: string;
@@ -596,6 +597,17 @@ class HttpService {
596597
);
597598
}
598599

600+
validateProcessVersion(processName: string, localVersion: number): Promise<AxiosResponse<ProcessVersionValidationResponse>> {
601+
const data = { localVersion: localVersion };
602+
return api
603+
.post<ProcessVersionValidationResponse>(`/versionControl/${processName}/versionValidation`, data)
604+
.catch((error) =>
605+
Promise.reject(
606+
this.#addError(i18next.t("notification.error.cannotValidateProcessVersion", "Cannot validate process version"), error),
607+
),
608+
);
609+
}
610+
599611
getExpressionSuggestions(processingType: string, request: ExpressionSuggestionRequest): Promise<AxiosResponse<ExpressionSuggestion[]>> {
600612
const promise = api.post<ExpressionSuggestion[]>(`/parameters/${encodeURIComponent(processingType)}/suggestions`, request);
601613
promise.catch((error) =>

designer/client/src/windowManager/useWindows.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
import { useWindowManager, WindowId, WindowType } from "@touk/window-manager";
1+
import type { WindowId, WindowType } from "@touk/window-manager";
2+
import { useWindowManager } from "@touk/window-manager";
23
import { defaults } from "lodash";
34
import { useCallback, useEffect, useMemo } from "react";
5+
46
import { useUserSettings } from "../common/userSettings";
5-
import { ConfirmDialogData } from "../components/modals/GenericConfirmDialog";
6-
import { InfoDialogData } from "../components/modals/GenericInfoDialog";
7-
import { Scenario } from "../components/Process/types";
8-
import { NodeType } from "../types";
7+
import type { ConfirmDialogData } from "../components/modals/GenericConfirmDialog";
8+
import type { InfoDialogData } from "../components/modals/GenericInfoDialog";
9+
import type { Scenario } from "../components/Process/types";
10+
import type { NodeType } from "../types";
911
import { WindowKind } from "./WindowKind";
1012

1113
const useRemoveFocusOnEscKey = (isWindowOpen: boolean) => {
@@ -93,6 +95,7 @@ export function useWindows(parent?: WindowId) {
9395
title: data.text,
9496
kind: WindowKind.confirm,
9597
meta: defaults(data, { confirmText: "Yes", denyText: "No" }),
98+
...(data.width != null && { layoutData: { width: data.width } }),
9699
});
97100
},
98101
[open],
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package pl.touk.nussknacker.ui.api
2+
3+
import cats.data.EitherT
4+
import com.typesafe.scalalogging.LazyLogging
5+
import pl.touk.nussknacker.engine.api.process.{ProcessId, ProcessIdWithName, ProcessName}
6+
import pl.touk.nussknacker.ui.api.description.VersionControlApiEndpoints
7+
import pl.touk.nussknacker.ui.api.description.VersionControlApiEndpoints.Dtos._
8+
import pl.touk.nussknacker.ui.api.description.VersionControlApiEndpoints.VersionControlError
9+
import pl.touk.nussknacker.ui.api.description.VersionControlApiEndpoints.VersionControlError.{
10+
MissingProcessId,
11+
MissingProcessVersion
12+
}
13+
import pl.touk.nussknacker.ui.process.{ProcessService, ScenarioQuery, ScenarioVersionQuery}
14+
import pl.touk.nussknacker.ui.process.ProcessService.{GetScenarioWithDetailsOptions, SkipScenarioGraph}
15+
import pl.touk.nussknacker.ui.process.repository.ScenarioVersionMetadata
16+
import pl.touk.nussknacker.ui.security.api.AuthManager
17+
18+
import scala.concurrent.{ExecutionContext, Future}
19+
20+
class VersionControlApiHttpService(
21+
authManager: AuthManager,
22+
processService: ProcessService
23+
)(implicit executionContext: ExecutionContext)
24+
extends BaseHttpService(authManager)
25+
with LazyLogging {
26+
private val securityInput = authManager.authenticationEndpointInput()
27+
28+
private val endpoints = new VersionControlApiEndpoints(securityInput)
29+
30+
expose {
31+
endpoints.versionValidationEndpoint
32+
.serverSecurityLogic(authorizeKnownUser[VersionControlError])
33+
.serverLogicEitherT { implicit loggedUser =>
34+
{ case (scenarioName, processVersionValidationRequest) =>
35+
for {
36+
pid <- EitherT.fromOptionF(processService.getProcessId(scenarioName), MissingProcessId(scenarioName.value))
37+
38+
processWithDetails <- EitherT.right(
39+
processService.getLatestProcessWithDetails(
40+
ProcessIdWithName(pid, scenarioName),
41+
GetScenarioWithDetailsOptions(SkipScenarioGraph, false)
42+
)
43+
)
44+
45+
localVersion = processVersionValidationRequest.localVersion
46+
latestDeployedVersion = processWithDetails.lastDeployedAction.map(_.processVersionId).map(_.value)
47+
latestVersion = processWithDetails.processVersionId.value
48+
modifiedBy = processWithDetails.modifiedBy
49+
50+
response = ProcessVersionValidationResponseDto(
51+
processName = scenarioName.value,
52+
isLatest = localVersion == latestVersion,
53+
localVersion = localVersion,
54+
latestDeployedVersion = latestDeployedVersion,
55+
latestVersion = latestVersion,
56+
modifiedBy = modifiedBy
57+
)
58+
} yield response
59+
}
60+
}
61+
}
62+
63+
}

0 commit comments

Comments
 (0)