-
Notifications
You must be signed in to change notification settings - Fork 1
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
Conversation
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
…86-embedded-entity-composer
src/UI/Components/Diagram/init.ts
Outdated
targetCell.addRelation(sourceCell.id as string, sourceName); | ||
targetCell.addRelation( | ||
sourceCell.id as string, | ||
doesTargetHaveRule.attrName as string, |
There was a problem hiding this comment.
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?
src/UI/Components/Diagram/init.ts
Outdated
sourceCell.addRelation(targetCell.id as string, targetName); | ||
sourceCell.addRelation( | ||
targetCell.id as string, | ||
doesSourceHaveRule.attrName as string, |
There was a problem hiding this comment.
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.
src/UI/Components/Diagram/helpers.ts
Outdated
} | ||
} | ||
|
||
//convert relatedTo property into valid attribute | ||
if (instance.relatedTo) { | ||
instance.relatedTo.forEach((attrName, id) => { | ||
instance.relatedTo.forEach((test, testOne) => { | ||
console.log(test, testOne); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
console.log
src/UI/Components/Diagram/helpers.ts
Outdated
@@ -388,8 +395,15 @@ export const bundleInstances = ( | |||
const embeddedInstances = deepCopiedMapToArray.filter( | |||
(instance) => !topServicesNames.includes(instance.service_entity), | |||
); | |||
return topInstances.map((instance) => { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
src/UI/Components/Diagram/helpers.ts
Outdated
return shapesDataTransform( | ||
embeddedInstances, | ||
instance, | ||
serviceModel as ServiceModel, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
src/UI/Components/Diagram/helpers.ts
Outdated
); | ||
|
||
areEmbeddedEdited = instance.action === null && data.action !== null; | ||
shouldEmbeddedBeSingular = |
There was a problem hiding this comment.
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.
…86-embedded-entity-composer
…86-embedded-entity-composer
…86-embedded-entity-composer
src/UI/Components/Diagram/helpers.ts
Outdated
@@ -388,8 +395,15 @@ export const bundleInstances = ( | |||
const embeddedInstances = deepCopiedMapToArray.filter( | |||
(instance) => !topServicesNames.includes(instance.service_entity), | |||
); | |||
return topInstances.map((instance) => { |
There was a problem hiding this comment.
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?
src/UI/Components/Diagram/helpers.ts
Outdated
return shapesDataTransform( | ||
embeddedInstances, | ||
instance, | ||
serviceModel as ServiceModel, |
There was a problem hiding this comment.
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.
src/UI/Components/Diagram/helpers.ts
Outdated
return shapesDataTransform( | ||
embeddedInstances, | ||
instance, | ||
serviceModel as ServiceModel, |
There was a problem hiding this comment.
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
src/UI/Components/Diagram/init.ts
Outdated
@@ -185,6 +185,7 @@ export default function diagramInit( | |||
const target = linkView.model.target(); | |||
let didSourceChanged = false; | |||
let didTargetChanged = false; | |||
let didConnectionWasSet = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wasConnectionSet
src/UI/Components/Diagram/init.ts
Outdated
} | ||
} | ||
|
||
if (!didConnectionWasSet) { |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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.
- It doesn't timeout, so if one of the operations hangs, it won't abort on it's own.
- It's all or nothing, if one element fails, the entire promise will fail. It makes it hard to have individual error-handling.
- It isn't sequential, but executes all the promises concurrently.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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?
src/UI/Components/Diagram/init.ts
Outdated
if (doesSourceHaveRule) { | ||
sourceCell.addRelation(targetCell.id as string, targetName); | ||
didSourceChanged = true; | ||
const checkUpdated = (sourceElement, targetElement) => { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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.
src/UI/Components/Diagram/actions.ts
Outdated
@@ -258,7 +261,7 @@ export function appendInstance( | |||
if (neighborRelations) { | |||
const connectedInstance = Array.from( | |||
neighborRelations, | |||
([id, attrName]) => ({ id, attrName }), | |||
([id, attributeName]) => ({ id, attributeName }), |
There was a problem hiding this comment.
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
There was a problem hiding this 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.
Processing this pull request |
… 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: )
Merged into branches master, iso7 in 0c0005a |
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