Skip to content
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

Issue/5386 embedded entity composer #5387

Closed
wants to merge 19 commits into from

Conversation

matborowczyk
Copy link
Collaborator

@matborowczyk matborowczyk commented Dec 13, 2023

Description

When preparing for QA session i noticed two bugs which I fixed

PR also contain E2E which were written to double check if there was no more bugs in core use-cases, which were, so also fixes for those issues
Also fix for delayed "Add in composer" button

closes #5386 #5276

Self Check:

Strike through any lines that are not applicable (~~line~~) then check the box

  • Attached issue to pull request
  • Changelog entry
  • Code is clear and sufficiently documented
  • Sufficient test cases (reproduces the bug/tests the requested feature)
  • Correct, in line with design
  • End user documentation is included or an issue is created for end-user documentation (add ref to issue here: )

@matborowczyk matborowczyk self-assigned this Dec 13, 2023
@matborowczyk matborowczyk marked this pull request as ready for review December 19, 2023 10:14
src/UI/Components/Diagram/Canvas.tsx Show resolved Hide resolved
src/UI/Components/Diagram/helpers.ts Outdated Show resolved Hide resolved
src/UI/Components/Diagram/helpers.ts Outdated Show resolved Hide resolved
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm losing myself in the code in this file. There seems to be lots of repetitive pieces of code, and a whole chain of nested if cases. Either it needs a rewrite to be more readable, or add more comments to the new parts you added. Because if we come back to this later on, it will be a major headache to figure everything out again. It's about the entire for loop starting on 286.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

valid point, I'll extract it into separate function

src/UI/Components/Diagram/interfaces.ts Outdated Show resolved Hide resolved
targetCell.addRelation(sourceCell.id as string, sourceName);
targetCell.addRelation(
sourceCell.id as string,
doesTargetHaveRule.attrName as string,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesTargetHaveRule is a boolean value for me when I read the variable name. Why is it an object?

sourceCell.addRelation(targetCell.id as string, targetName);
sourceCell.addRelation(
targetCell.id as string,
doesSourceHaveRule.attrName as string,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here, this is a boolean name, not inferring it might be an object.

}
}

//convert relatedTo property into valid attribute
if (instance.relatedTo) {
instance.relatedTo.forEach((attrName, id) => {
instance.relatedTo.forEach((test, testOne) => {
console.log(test, testOne);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

console.log

@@ -388,8 +395,15 @@ export const bundleInstances = (
const embeddedInstances = deepCopiedMapToArray.filter(
(instance) => !topServicesNames.includes(instance.service_entity),
);
return topInstances.map((instance) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

topInstances, are those the root instances?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

topInstance are non-embedded entities, as they at the top and receive eventual embedded ones

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add that in comment please?

return shapesDataTransform(
embeddedInstances,
instance,
serviceModel as ServiceModel,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you type serviceModel correctly in line 399, you're not supposed to cast it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the reason why it is casted as I need to find the serviceModel from the array to pass down into the function, there it becomes serviceModel | undefined, I can add if statement and then filter out possible undefined values from array, but theoretically we should have all serviceModels in the first place, that why I chose this solution

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really like assuming we can trust something that would theoretically never be undefined, casting doesn't remove the eventuality your value could be undefined, it's only a type annotation telling your TS linter to ignore the warning. It's not a safe way of programming. Casting should only be done in very specific situations, and in my opinion, this ain't one.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I already find that there is way too much casting in the composer code, and I know it's mostly due to using JointJS, and maybe we should invest a bit of time to find a solution for that. Because we are discarding the purpose of having Typescript in the first place

);

areEmbeddedEdited = instance.action === null && data.action !== null;
shouldEmbeddedBeSingular =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know performance is not much a concern, but allocating a variable like this isn't ideal. It would be more efficient to have a method to which you pass as argument the model and return whether it's singular or not. When I read the flow of code here, it makes me wonder why it has to be done between the two if cases too.

const isSingularRelation: boolean = (model: ModelType ) => { 
     !!model.upper_limit && model.upper_limit === 1;
}

I don't know by heart which name the Type is supposed to have for the model. But you get my drift. You can then simply pass the method in line 323. And also reuse the method wherever needed. If I remember well, it's a logic that appears now and then, to check if it's one or more relations.

@@ -388,8 +395,15 @@ export const bundleInstances = (
const embeddedInstances = deepCopiedMapToArray.filter(
(instance) => !topServicesNames.includes(instance.service_entity),
);
return topInstances.map((instance) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add that in comment please?

return shapesDataTransform(
embeddedInstances,
instance,
serviceModel as ServiceModel,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really like assuming we can trust something that would theoretically never be undefined, casting doesn't remove the eventuality your value could be undefined, it's only a type annotation telling your TS linter to ignore the warning. It's not a safe way of programming. Casting should only be done in very specific situations, and in my opinion, this ain't one.

return shapesDataTransform(
embeddedInstances,
instance,
serviceModel as ServiceModel,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I already find that there is way too much casting in the composer code, and I know it's mostly due to using JointJS, and maybe we should invest a bit of time to find a solution for that. Because we are discarding the purpose of having Typescript in the first place

@@ -185,6 +185,7 @@ export default function diagramInit(
const target = linkView.model.target();
let didSourceChanged = false;
let didTargetChanged = false;
let didConnectionWasSet = false;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wasConnectionSet

}
}

if (!didConnectionWasSet) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have a lot of repetition in here, which makes it hard to read. Why not extract the logic into a function? I quickly drafted it, you might need to make adjustments and add typings

const checkUpdated = (element) => {
    const cell = graph.getCell(
        element.id as dia.Cell.ID,
    ) as ServiceEntityBlock;

    const relations = cell.getRelations();
    const name = cell.getName();

    if (relations) {
        const elementConnectionRule = connectionRules[name].find((rule) => rule.name === name);
        if (elementConnectionRule) {
            cell.addRelation(
                sourceCell.id as string,
                targetConnectionRule.attributeName as string,
              );
            updateInstancesToSend(cell, ActionEnum.UPDATE);
            return; // you don't need further information, so you can early return the function
        }
    }

    if (cell.get("isEmbedded") &&
    cell.get("embeddedTo") !== null) {
        cell.set("embeddedTo", cell.id);
        updateInstancesToSend(cell, ActionEnum.UPDATE);
        return;
    }
}

const source = linkView.model.source();
const target = linkView.model.target();
checkUpdated(source);
checkUpdated(target);

@@ -83,33 +83,42 @@ export function GetInstanceWithRelationsQueryManager(

if (Either.isRight(instanceWithReferences)) {
if (instanceWithReferences.value.data.referenced_by !== null) {
await Promise.all(
const resolvedNestedInstances = await Promise.all(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a few downsides to use promise.all, but I'm not sure it's something we need to care about.

  1. It doesn't timeout, so if one of the operations hangs, it won't abort on it's own.
  2. It's all or nothing, if one element fails, the entire promise will fail. It makes it hard to have individual error-handling.
  3. It isn't sequential, but executes all the promises concurrently.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this is almost the same as in the init file, I'll make the same comment, extract the repetitive code, to avoid having those boolean variables keeping track of the changes.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we check on the same things but the code in those if statement is different, so I don't think extracting here would be just for the sake of extracting

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not wanting to extract just for the sake of it. Functions have arguments, for minor differences like you have in this file. The element you pass is either a target or a source, but inherently, they are the same thing, an instance.

Having so many if cases like this makes the code not readable enough. Extracting methods with decent function names can improve readability, and get rid of some complexity. That's why you would extract, not just for the sake of it.

I'm really trying to follow along what's going on in this file (and the init one), but there's so much look-alike code, that I'm drowning in it. I don't even how I'd try to make changes to this code without breaking my head as to why some if cases are used. Splitting up your code in smaller pieces, that make more sense, is not a bad practice.

Anyway, let's discuss the PR tomorrow in the meeting.

(relation) => relation.name === attributeName,
);
if (model) {
if (model.upper_limit !== 1) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a switch case maybe, to make it more readable?

if (doesSourceHaveRule) {
sourceCell.addRelation(targetCell.id as string, targetName);
didSourceChanged = true;
const checkUpdated = (sourceElement, targetElement) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see it's a bit better, but still confusing, why do we need to execute in both directions in lines 230 and 231? Can you also add some docs strings to the method?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we can make the connections from the both sides, so we don't know which instance can get the data about being embedded/relatedTo, and previous version did that both way but there was some repetition, so avoiding repetition we need to check now from both perspectives, otherwise we have identical code but wrapped into extra function body.

I'll add docstring

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean, we don't know which instance gets the data, don't you have that information 🤔 You know the two elements that are being concerned by the event, you have the target, and the source, two instances the should get an update. Now, the way you are using this, you are mixing up source and target...

I'm lost on the reasoning to be fair, maybe the Kind field in the typing would already resolve some of the complexity of all those if cases throughout the file(s).

Extracting logic doesn't mean you have to plainly copy some content into a separate method. I made the example previously, as a direction to try to explain what I meant, I didn't have all the pieces of the reasoning. Sometimes it helps to write out in bullet points what you are trying to achieve.

For me, the two instances get an event, either it's adding something, or removing something. Then I see what kind of thing I need to add or remove, and update accordingly.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part of code checks which functions should get information about connection, as it's only stored in the children, I don't think that kind would help as theoretically both of them can be the same type and only one should get the data, so that's whats was done here.

We were checking both source and target, as depending from which instance we started it can be swapped on every attempt, that's why there was code repetition, we were checking the same thing on both instances, so as we now removed that into non-repetitive function we need to check it twice, for source and target

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But aren't you already doing the check on both directions in lines 213 and further?
Honestly, I'm drowning in the amount of nested if cases all over, I can't keep track of what's happening.
Right now, the only thing that got removed were those boolean values, but the repetition and complexity is still there.

Lets discuss this tomorrow, have a call about this PR, it will be more efficient than discussing here.

@@ -21,6 +21,7 @@ interface DictDialogData {
interface Rule {
name: string;
type: TypeEnum;
attributeName?: string; //only used for inter-service relations
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is required for inter-service relations, isn't it worth having a type extending the default rule set? I'm no expert in TS, so maybe I'm wrong :D But it seems like this is what has been done so far in the codebase

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but the function that use that interface accept both embedded and inter-service relation, so there is no other place that use that interface so that would be dubious in that case

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I understand that, but it means that in the case of an inter-service relation, you have an optional where it would be a required field... Unless it's not required in the inter-service relation, then it's fine. But if it is required, then it biases your code, and enforces you to eliminate undefined cases, or cast values to bypass the linter..

Why not go for the kind:"InterserviceRelation" technique that is being used all over the rest of the app? It's just a suggestion

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that more like if there is that attributeName then it's inter-service_relation, if not then it's embedded, and it's simple enough.

Again I'm not strongly behind or against one of those solutions so I can adjust if you feel like current solution is unclear, I will take step back to look on the code, to check whether you proposition will improve readability, which could be the case, as I have biased POV at the moment as I stare at it for quite some time :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

defining what kind of instance your dealing with, based on a field being present or not, is flakey and is not a clear solution that is maintainable. If someone new comes up in the team, and reads the code, they need explicit explanation to understand what's going on. That's something we don't want.

@@ -258,7 +261,7 @@ export function appendInstance(
if (neighborRelations) {
const connectedInstance = Array.from(
neighborRelations,
([id, attrName]) => ({ id, attrName }),
([id, attributeName]) => ({ id, attributeName }),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this the same as in line 238? if yes, maybe worth having it as a variable/function

Copy link
Collaborator

@LukasStordeur LukasStordeur left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll approve the PR as is now, Link it into the ticket about refactoring so we can go back to some of the comments later.

@matborowczyk matborowczyk added the merge-tool-ready This ticket is ready to be merged in label Jan 12, 2024
@inmantaci
Copy link
Contributor

Processing this pull request

inmantaci pushed a commit that referenced this pull request Jan 12, 2024
… array of embedded entities (Issue #5386, PR #5387)

# Description

When preparing for QA session i noticed two bugs which I fixed

PR also contain E2E which were written to double check if there was no more bugs in core use-cases, which were, so also fixes for those issues
Also fix for delayed "Add in composer" button

closes #5386 #5276

# Self Check:

Strike through any lines that are not applicable (`~~line~~`) then check the box

- [x] Attached issue to pull request
- [x] Changelog entry
- [x] Code is clear and sufficiently documented
- [x] Sufficient test cases (reproduces the bug/tests the requested feature)
- [x] Correct, in line with design
- [ ] End user documentation is included or an issue is created for end-user documentation (add ref to issue here: )
@inmantaci
Copy link
Contributor

Merged into branches master, iso7 in 0c0005a

@inmantaci inmantaci closed this Jan 12, 2024
@inmantaci inmantaci deleted the issue/5386-embedded-entity-composer branch January 12, 2024 09:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
merge-tool-ready This ticket is ready to be merged in
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Fix embedded entities is instanceComposer
3 participants