Skip to content

Commit

Permalink
[NU-1805] Scenario labels support (#6766)
Browse files Browse the repository at this point in the history
* Scenario labels support

* style fixes

* labels validation

* Scenario labels validation

* QS

* Labels validation via endpoint, multiple validation rules

* QS

* Fixes

* Fixes

* Fixes

* Fixes

* Review fixes

* make input value uncontrolled

* Migration API with labels, permission handling, e2e tests

* Updated snapshots (#6831)

Co-authored-by: mateuszkp96 <[email protected]>

* Cypress test fix

* Review fixes

* fix truncate popover opening scenario on close

* Review fixes

* Fixes

* Fix

* Cypress fix

* Updated snapshots (#6881)

Co-authored-by: mateuszkp96 <[email protected]>

* Fixes

* Review fixes

* Review fixes

* Updated snapshots (#6903)

Co-authored-by: mateuszkp96 <[email protected]>

---------

Co-authored-by: Dawid Poliszak <[email protected]>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mateuszkp96 <[email protected]>
  • Loading branch information
4 people authored Sep 18, 2024
1 parent adc8db1 commit 3f1e660
Show file tree
Hide file tree
Showing 111 changed files with 3,203 additions and 474 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,10 @@ object ProcessCompilationError {
extends ProcessCompilationError
with ScenarioPropertiesError

final case class ScenarioLabelValidationError(label: String, description: String)
extends ProcessCompilationError
with ScenarioPropertiesError

final case class SpecificDataValidationError(paramName: ParameterName, message: String)
extends ProcessCompilationError
with ScenarioPropertiesError
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
92 changes: 92 additions & 0 deletions designer/client/cypress/e2e/labels.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
describe("Scenario labels", () => {
const seed = "process";

before(() => {
cy.deleteAllTestProcesses({ filter: seed, force: true });
});

beforeEach(() => {
cy.mockWindowDate();
});

afterEach(() => {
cy.deleteAllTestProcesses({ filter: seed, force: true });
});

describe("designer", () => {
it("should allow to set labels for new process", () => {
cy.visitNewProcess(seed).as("processName");

cy.intercept("PUT", "/api/processes/*").as("save");

cy.intercept("POST", "/api/scenarioLabels/validation").as("labelvalidation");

cy.get("[data-testid=AddLabel]").should("be.visible").click();

cy.get("[data-testid=LabelInput]").as("labelInput");

cy.get("@labelInput").should("be.visible").click().type("tagX");

cy.wait("@labelvalidation");

cy.get('.MuiAutocomplete-popper li[data-option-index="0"]').contains('Add label "tagX"').click();

cy.get("[data-testid=scenario-label-0]").should("be.visible").contains("tagX");

cy.get("@labelInput").should("be.visible").click().type("tag2");

cy.wait("@labelvalidation");

cy.get('.MuiAutocomplete-popper li[data-option-index="0"]').contains('Add label "tag2"').click();

cy.get("@labelInput").type("{enter}");

cy.get("[data-testid=scenario-label-1]").should("be.visible").contains("tag2");

cy.contains(/^save/i).should("be.enabled").click();
cy.contains(/^ok$/i).should("be.enabled").click();
cy.wait("@save").its("response.statusCode").should("eq", 200);

cy.viewport(1500, 800);

cy.get("[data-testid=scenario-label-0]").should("be.visible").contains("tag2");
cy.get("[data-testid=scenario-label-1]").should("be.visible").contains("tagX");

cy.get("@labelInput").should("be.visible").click().type("very long tag");

cy.wait("@labelvalidation").then((_) => cy.wait(100));

cy.get("@labelInput").should("be.visible").contains("Incorrect value 'very long tag'");

cy.contains(/^save/i).should("be.disabled");
});

it("should show labels for scenario", () => {
cy.visitNewProcess(seed).then((processName) => cy.addLabelsToNewProcess(processName, ["tag1", "tag3"]));

cy.viewport(1500, 800);

cy.get("[data-testid=scenario-label-0]").should("be.visible").contains("tag1");
cy.get("[data-testid=scenario-label-1]").should("be.visible").contains("tag3");
});
});

describe("scenario list", () => {
it("should allow to filter scenarios by label", () => {
cy.visitNewProcess(seed).then((processName) => cy.addLabelsToNewProcess(processName, ["tag1", "tag3"]));
cy.visitNewProcess(seed).then((processName) => cy.addLabelsToNewProcess(processName, ["tag2", "tag3"]));
cy.visitNewProcess(seed).then((processName) => cy.addLabelsToNewProcess(processName, ["tag4"]));
cy.visitNewProcess(seed);

cy.visit("/");

cy.contains("button", /label/i).click();

cy.get("ul[role='menu']").within(() => {
cy.contains(/tag2/i).click();
});

cy.contains(/1 of the 4 rows match the filters/i).should("be.visible");
});
});
});
23 changes: 23 additions & 0 deletions designer/client/cypress/support/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ declare global {
importTestProcess: typeof importTestProcess;
visitNewProcess: typeof visitNewProcess;
visitNewFragment: typeof visitNewFragment;
addLabelsToNewProcess: typeof addLabelsToNewProcess;
postFormData: typeof postFormData;
visitProcess: typeof visitProcess;
getNode: typeof getNode;
Expand Down Expand Up @@ -93,6 +94,26 @@ function visitNewFragment(name?: string, fixture?: string, category?: string) {
});
}

function addLabelsToNewProcess(name?: string, labels?: string[]) {
return cy.visitProcess(name).then((processName) => {
cy.intercept("PUT", "/api/processes/*").as("save");
cy.intercept("POST", "/api/scenarioLabels/validation").as("labelValidation");
cy.get("[data-testid=AddLabel]").should("be.visible").click();
cy.get("[data-testid=LabelInput]").should("be.visible").click().as("labelInput");

labels.forEach((label) => {
cy.get("@labelInput").type(label);
cy.wait("@labelValidation");
cy.get('.MuiAutocomplete-popper li[data-option-index="0"]').contains(label).click();
});

cy.contains(/^save/i).should("be.enabled").click();
cy.contains(/^ok$/i).should("be.enabled").click();
cy.wait("@save").its("response.statusCode").should("eq", 200);
return cy.wrap(processName);
});
}

function deleteTestProcess(processName: string, force?: boolean) {
const url = `/api/processes/${processName}`;

Expand Down Expand Up @@ -168,6 +189,7 @@ function importTestProcess(name: string, fixture = "testProcess") {
cy.request("PUT", `/api/processes/${name}`, {
comment: "import test data",
scenarioGraph: response.scenarioGraph,
scenarioLabels: [],
});
return cy.wrap(name);
});
Expand Down Expand Up @@ -292,6 +314,7 @@ Cypress.Commands.add("createTestFragment", createTestFragment);
Cypress.Commands.add("importTestProcess", importTestProcess);
Cypress.Commands.add("visitNewProcess", visitNewProcess);
Cypress.Commands.add("visitNewFragment", visitNewFragment);
Cypress.Commands.add("addLabelsToNewProcess", addLabelsToNewProcess);
Cypress.Commands.add("postFormData", postFormData);
Cypress.Commands.add("visitProcess", visitProcess);
Cypress.Commands.add("getNode", getNode);
Expand Down
1 change: 1 addition & 0 deletions designer/client/src/actions/actionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type ActionTypes =
| "RESET_SELECTION"
| "EDIT_NODE"
| "PROCESS_RENAME"
| "EDIT_LABELS"
| "SHOW_METRICS"
| "UPDATE_TEST_CAPABILITIES"
| "UPDATE_TEST_FORM_PARAMETERS"
Expand Down
11 changes: 11 additions & 0 deletions designer/client/src/actions/nk/editNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@ export type RenameProcessAction = {
name: string;
};

export type EditScenarioLabels = {
type: "EDIT_LABELS";
labels: string[];
};

export function editScenarioLabels(scenarioLabels: string[]) {
return (dispatch) => {
dispatch({ type: "EDIT_LABELS", labels: scenarioLabels });
};
}

export function editNode(scenarioBefore: Scenario, before: NodeType, after: NodeType, outputEdges?: Edge[]): ThunkAction {
return async (dispatch) => {
const { processName, scenarioGraph } = await dispatch(calculateProcessAfterChange(scenarioBefore, before, after, outputEdges));
Expand Down
5 changes: 3 additions & 2 deletions designer/client/src/actions/nk/node.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Edge, EdgeType, NodeId, NodeType, ProcessDefinitionData, ValidationResult } from "../../types";
import { ThunkAction } from "../reduxTypes";
import { layoutChanged, Position } from "./ui/layout";
import { EditNodeAction, RenameProcessAction } from "./editNode";
import { EditNodeAction, EditScenarioLabels, RenameProcessAction } from "./editNode";
import { getProcessDefinitionData } from "../../reducers/selectors/settings";
import { batchGroupBy } from "../../reducers/graph/batchGroupBy";
import NodeUtils from "../../components/graph/NodeUtils";
Expand Down Expand Up @@ -154,4 +154,5 @@ export type NodeActions =
| NodesWithEdgesAddedAction
| ValidationResultAction
| EditNodeAction
| RenameProcessAction;
| RenameProcessAction
| EditScenarioLabels;
17 changes: 15 additions & 2 deletions designer/client/src/common/ProcessUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ import {
import { RootState } from "../reducers";
import { isProcessRenamed } from "../reducers/selectors/graph";
import { Scenario } from "src/components/Process/types";
import { ScenarioLabelValidationError } from "../components/Labels/types";

class ProcessUtils {
nothingToSave = (state: RootState): boolean => {
const scenario = state.graphReducer.scenario;
const savedProcessState = state.graphReducer.history.past[0]?.scenario || state.graphReducer.history.present.scenario;

const omitValidation = (details: ScenarioGraph) => omit(details, ["validationResult"]);
const processRenamed = isProcessRenamed(state);

Expand All @@ -33,7 +33,14 @@ class ProcessUtils {
return true;
}

return !savedProcessState || isEqual(omitValidation(scenario.scenarioGraph), omitValidation(savedProcessState.scenarioGraph));
const labelsFor = (scenario: Scenario): string[] => {
return scenario.labels ? scenario.labels.slice().sort((a, b) => a.localeCompare(b)) : [];
};

const isGraphUpdated = isEqual(omitValidation(scenario.scenarioGraph), omitValidation(savedProcessState.scenarioGraph));
const areScenarioLabelsUpdated = isEqual(labelsFor(scenario), labelsFor(savedProcessState));

return !savedProcessState || (isGraphUpdated && areScenarioLabelsUpdated);
};

canExport = (state: RootState): boolean => {
Expand Down Expand Up @@ -87,6 +94,12 @@ class ProcessUtils {
return isEmpty(this.getValidationErrors(scenario)?.processPropertiesErrors);
};

getLabelsErrors = (scenario: Scenario): ScenarioLabelValidationError[] => {
return this.getValidationResult(scenario)
.errors.globalErrors.filter((e) => e.error.typ == "ScenarioLabelValidationError")
.map((e) => <ScenarioLabelValidationError>{ label: e.error.fieldName, messages: [e.error.description] });
};

getValidationErrors(scenario: Scenario): ValidationErrors {
return this.getValidationResult(scenario).errors;
}
Expand Down
12 changes: 12 additions & 0 deletions designer/client/src/components/Labels/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export type AvailableScenarioLabels = {
labels: string[];
};

export type ScenarioLabelValidationError = {
label: string;
messages: string[];
};

export type ScenarioLabelsValidationResponse = {
validationErrors: ScenarioLabelValidationError[];
};
1 change: 1 addition & 0 deletions designer/client/src/components/Process/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export interface Scenario {
createdAt: Instant;
modifiedAt: Instant;
createdBy: string;
labels: string[];
lastAction?: ProcessActionType;
lastDeployedAction?: ProcessActionType;
state: ProcessStateType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ function MoreScenarioDetailsDialog(props: WindowContentProps<WindowKind, Props>)
}, [scenario.processingMode]);

const displayStatus = !scenario.isArchived && !scenario.isFragment;
const displayLabels = scenario.labels.length !== 0;

return (
<WindowContent
Expand Down Expand Up @@ -94,6 +95,12 @@ function MoreScenarioDetailsDialog(props: WindowContentProps<WindowKind, Props>)
<ItemLabelStyled>{i18next.t("scenarioDetails.label.engine", "Engine")}</ItemLabelStyled>
<Typography variant={"caption"}>{scenario.engineSetupName}</Typography>
</ItemWrapperStyled>
{displayLabels && (
<ItemWrapperStyled>
<ItemLabelStyled>{i18next.t("scenarioDetails.label.labels", "Labels")}</ItemLabelStyled>
<Typography variant={"caption"}>{scenario.labels.join(", ")}</Typography>
</ItemWrapperStyled>
)}
<ItemWrapperStyled>
<ItemLabelStyled>{i18next.t("scenarioDetails.label.created", "Created")}</ItemLabelStyled>
<Typography variant={"caption"}>{moment(scenario.createdAt).format(DATE_FORMAT)}</Typography>
Expand Down
5 changes: 3 additions & 2 deletions designer/client/src/components/modals/SaveProcessDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { displayCurrentProcessVersion, displayProcessActivity, loadProcessToolba
import { PromptContent } from "../../windowManager";
import { CommentInput } from "../comment/CommentInput";
import { ThunkAction } from "../../actions/reduxTypes";
import { getScenarioGraph, getProcessName, getProcessUnsavedNewName, isProcessRenamed } from "../../reducers/selectors/graph";
import { getScenarioGraph, getProcessName, getProcessUnsavedNewName, isProcessRenamed, getScenarioLabels } from "../../reducers/selectors/graph";
import HttpService from "../../http/HttpService";
import { ActionCreators as UndoActionCreators } from "redux-undo";
import { visualizationUrl } from "../../common/VisualizationUrl";
Expand All @@ -24,9 +24,10 @@ export function SaveProcessDialog(props: WindowContentProps): JSX.Element {
const state = getState();
const scenarioGraph = getScenarioGraph(state);
const currentProcessName = getProcessName(state);
const labels = getScenarioLabels(state);

// save changes before rename and force same processName everywhere
await HttpService.saveProcess(currentProcessName, scenarioGraph, comment);
await HttpService.saveProcess(currentProcessName, scenarioGraph, comment, labels);

const unsavedNewName = getProcessUnsavedNewName(state);
const isRenamed = isProcessRenamed(state) && (await HttpService.changeProcessName(currentProcessName, unsavedNewName));
Expand Down
4 changes: 2 additions & 2 deletions designer/client/src/components/tips/error/ErrorTips.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ export const ErrorTips = ({ errors, showDetails, scenario }: Props) => {
() =>
globalErrors.map((error, index) =>
isEmpty(error.nodeIds) ? (
<span key={index} title={error.error.description}>
<div key={index} title={error.error.description}>
{error.error.message}
</span>
</div>
) : (
<NodeErrorsLinkSection
key={index}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,15 @@ import {
} from "./ScenarioDetailsComponents";
import { MoreScenarioDetailsButton } from "./buttons/MoreScenarioDetailsButton";
import { CategoryDetails } from "./CategoryDetails";
import { ScenarioLabels } from "./ScenarioLabels";
import { getLoggedUser } from "../../../reducers/selectors/settings";

const ScenarioDetails = memo((props: ToolbarPanelProps) => {
const scenario = useSelector((state: RootState) => getScenario(state));
const isRenamePending = useSelector((state: RootState) => isProcessRenamed(state));
const unsavedNewName = useSelector((state: RootState) => getProcessUnsavedNewName(state));
const processState = useSelector((state: RootState) => getProcessState(state));
const loggedUser = useSelector((state: RootState) => getLoggedUser(state));

const transitionKey = ProcessStateUtils.getTransitionKey(scenario, processState);

Expand Down Expand Up @@ -56,6 +59,7 @@ const ScenarioDetails = memo((props: ToolbarPanelProps) => {
<ProcessName variant={"subtitle2"}>{scenario.name}</ProcessName>
)}
</ScenarioDetailsItemWrapper>
<ScenarioLabels readOnly={!loggedUser.isWriter()} />
<MoreScenarioDetailsButton scenario={scenario} processState={processState} />
</PanelScenarioDetails>
</CssFade>
Expand Down
Loading

0 comments on commit 3f1e660

Please sign in to comment.