From 25a96672eb9a879b241cc91a84bb3ccc29150f13 Mon Sep 17 00:00:00 2001 From: Ben Willenbring Date: Fri, 11 Sep 2020 11:10:11 +0200 Subject: [PATCH] Feature/epic fixed folders (#1239) * Feature/fixed folders (#1199) * Add example json representing folder structure in docs * Update model with optional fixed and margin attributes * Adds first steps to build the map, missing transformation of coordinates * Refactor improve performance by counting nodes and blacklisted nodes at the same time * Refactor improve performance by removing one loop * Fix typo in y properties * Add coordinate transformation, map is finally visible, but not scalable yet * Add edges to example json * Refactor use values in line * Add size increases based on margin * Refactor abstract methods and improved readability * Fix broken tests after changing the method header * Remove margin since it's not required anymore * Add snapshot test for fixed folders * Fix incoming and outgoing edge points are now set correctly * Fix root folder name should be set based on the root-name of the map * Presentation add example cc.json * Presentation rename example cc.json * Update rename fixed attributes * Update snapshot since ids are no longer pre-decorated * Refactor move static functions to helper file Update jest.config to mock localStorage to test in IntelliJ * Refactor convert class with static functions to exported objects with functions * Refactor convert class with static functions to exported objects with functions * Add no-else-return rule and fix errors * Refactor use null propagation operator * Update snapshots and remove unused snapshots * Update visualization/app/codeCharta/util/treeMapGenerator.spec.ts Co-authored-by: Ruben Bridgewater * Update visualization/app/codeCharta/util/fileHelper.ts Co-authored-by: Ruben Bridgewater * Refactor rename isNodeToBeFlat to shouldNodeBeFlat * Refactor use null coalescing to shorten code * Refactor move shortcut return to the top to improve performance * Update snapshot after renaming test * Refactor rearrange function and add return statements to prevent obsolete calculations * Refactor remove obsolete check in condition if attribute types are empty and added some more tests * Refactor rename function Co-authored-by: Ruben Bridgewater * Docs/fixed-folders (#1163) * Add documentation on how to generate custom cc.jsons with the fixed attribute * Fix list not formated correctly * Update image with x and y coordinates and bounds * Update remove margin from docs, since it's not required anymore It is still recommended to apply a margin between folders, but the coordinates of the folders can be set to anything as long as these respect the rules. * Update gh-pages/_posts/how-to/2020-08-17-fixate-folders-with-a-custom-cc-json.md Co-authored-by: Ruben Bridgewater * Update gh-pages/_posts/how-to/2020-08-17-fixate-folders-with-a-custom-cc-json.md * Update gh-pages/_posts/how-to/2020-08-17-fixate-folders-with-a-custom-cc-json.md Co-authored-by: Ruben Bridgewater * Update renamed properties and rephrased explanation on how to set the coordinates * Update gh-pages/_posts/how-to/2020-08-17-fixate-folders-with-a-custom-cc-json.md Co-authored-by: Ruben Bridgewater * Update gh-pages/_posts/how-to/2020-08-17-fixate-folders-with-a-custom-cc-json.md Co-authored-by: Ruben Bridgewater * Update docs * Update gh-pages/_posts/how-to/2020-08-17-fixate-folders-with-a-custom-cc-json.md Co-authored-by: Ruben Bridgewater * Update example image and it's corresponding cc.json Co-authored-by: Ruben Bridgewater * Feature/fixed folders validation (#1213) * Add example json representing folder structure in docs * Update model with optional fixed and margin attributes * Adds first steps to build the map, missing transformation of coordinates * Refactor improve performance by counting nodes and blacklisted nodes at the same time * Refactor improve performance by removing one loop * Fix typo in y properties * Add coordinate transformation, map is finally visible, but not scalable yet * Add edges to example json * Refactor use values in line * Add size increases based on margin * Refactor abstract methods and improved readability * Fix broken tests after changing the method header * Remove margin since it's not required anymore * Add snapshot test for fixed folders * Fix incoming and outgoing edge points are now set correctly * Fix root folder name should be set based on the root-name of the map * Add API.md to document the changes for the cc.json-api * Update api-versions for tests and files * Presentation add example cc.json * Presentation rename example cc.json * Add validation for fixed folders, not finalized * Add finalized validation Fix nodes empty error * Update collect errors again instead of throwing them instantly * Update e2e test to use the error messages enum instead of plain strings * Fix missing whitespace in test of dialog message * Update rename fixed attributes * Update snapshot since ids are no longer pre-decorated * Update tests with renamed fixed attributes * Refactor move static functions to helper file Update jest.config to mock localStorage to test in IntelliJ * Refactor convert class with static functions to exported objects with functions * Refactor convert class with static functions to exported objects with functions * Add no-else-return rule and fix errors * Refactor use null propagation operator * Update snapshots and remove unused snapshots * Add eslint rule no-lonely-if and autofix errors * Update visualization/app/codeCharta/util/treeMapGenerator.spec.ts Co-authored-by: Ruben Bridgewater * Update visualization/app/codeCharta/util/fileHelper.ts Co-authored-by: Ruben Bridgewater * Refactor rename isNodeToBeFlat to shouldNodeBeFlat * Refactor use null coalescing to shorten code * Refactor move shortcut return to the top to improve performance * Update snapshot after renaming test * Refactor rearrange function and add return statements to prevent obsolete calculations * Refactor remove obsolete check in condition if attribute types are empty and added some more tests * Add e2e test to ensure lower api versions are working without errors or warnings * Refactor replace custom type KeyValuePair with Record * Update jsonschema * Refactor replace _.cloneDeep with the rfdc library * Update snapshot with new api version * Add fixedPosition attribute of folders resulting in errors during validation after their name for easier debugging * Fix outOfBounds-Check not checking for negative width and height * Add another test to ensure the new out of bounds function is working * Add print found api version when a warning is thrown * Update visualization/app/codeCharta/util/fileValidator.ts Co-authored-by: Ruben Bridgewater * Update remove individual titles and use a generic title for errors and warnings * Refactor check if both nodes have the fixedPosition attribute before starting calculating overlaps * Refactor remove obsolete .values() call * Add found duplicate node to error message Refactor use a more intuitive implementation to find duplicate nodes Fix duplicate file in example * Update snapshot with new file names * Fix broken e2e test with wrong warning message * Refactor use root instead of whole file as parameter to validate fixed folders * Refactor reorder functions * Revert "Refactor replace custom type KeyValuePair with Record" This reverts commit 061ec816beaacf4329f0b6d9a4af788b575b2b3f. * Update json schema with KeyValuePair instead of Record again * Move API.md from visualization to root of project * Add throw an error if we encountered a fixed folder and the api version is smaller than 1.2 Co-authored-by: Ruben Bridgewater * Refactor rename file by replacing - with _ in the name * Update CHANGELOG * Update CHANGELOG, api version incremented * Docs move api change to Changed section * Update outdated property names Co-authored-by: Ruben Bridgewater --- API.md | 32 + CHANGELOG.md | 9 +- .../ValidationTool/src/main/resources/cc.json | 25 +- ...17-fixate_folders_with_a_custom_cc_json.md | 127 ++++ .../fixate-folders/fixated-folder-example.jpg | Bin 0 -> 184208 bytes visualization/.eslintrc.js | 25 +- .../app/codeCharta/assets/sample1.cc.json | 2 +- .../app/codeCharta/assets/sample2.cc.json | 2 +- .../app/codeCharta/assets/sample3.cc.json | 2 +- .../app/codeCharta/codeCharta.api.model.ts | 7 +- .../app/codeCharta/codeCharta.model.ts | 8 + .../app/codeCharta/codeCharta.service.spec.ts | 11 +- .../app/codeCharta/codeCharta.service.ts | 49 +- .../ressources/fixed-folders/example.json | 386 ++++++++++ .../fixed-folders/fixed-folders-example.ts | 216 ++++++ .../sample1_with_different_edges.cc.json | 2 +- .../sample1_with_lower_minor_api.cc.json | 96 +++ .../codeCharta/state/nodeSearch.service.ts | 9 +- .../focusedNodePath.splitter.ts | 3 +- .../state/store/files/files.reducer.ts | 9 +- .../ui/codeMap/codeMap.preRender.service.ts | 7 +- .../ui/codeMap/rendering/geometryGenerator.ts | 3 +- .../codeMap/threeViewer/threeSceneService.ts | 3 +- .../codeCharta/ui/dialog/dialog.service.ts | 4 +- .../ui/fileChooser/fileChooser.e2e.ts | 16 +- .../ui/filePanel/filePanel.component.ts | 3 +- .../metricValueHovered.component.ts | 3 +- .../nodeContextMenu.component.ts | 3 +- .../viewCube/viewCube.mouseEvents.service.ts | 6 +- .../aggregationGenerator.spec.ts.snap | 6 +- .../__snapshots__/fileHelper.spec.ts.snap | 74 ++ .../treeMapGenerator.spec.ts.snap | 671 ++++++++++++++++++ .../__snapshots__/treeMapHelper.spec.ts.snap | 8 +- .../util/aggregationGenerator.spec.ts | 5 +- .../app/codeCharta/util/codeMapHelper.spec.ts | 25 +- .../app/codeCharta/util/codeMapHelper.ts | 138 ++-- .../app/codeCharta/util/dataMocks.ts | 13 +- .../app/codeCharta/util/fileDownloader.ts | 3 +- .../util/fileExtensionCalculator.ts | 3 +- .../app/codeCharta/util/fileHelper.spec.ts | 56 ++ .../app/codeCharta/util/fileHelper.ts | 44 ++ .../app/codeCharta/util/fileValidator.spec.ts | 239 ++++++- .../app/codeCharta/util/fileValidator.ts | 186 +++-- .../app/codeCharta/util/generatedSchema.json | 25 +- .../app/codeCharta/util/scenarioHelper.ts | 5 +- .../codeCharta/util/treeMapGenerator.spec.ts | 10 + .../app/codeCharta/util/treeMapGenerator.ts | 109 ++- .../app/codeCharta/util/treeMapHelper.spec.ts | 330 ++++----- .../app/codeCharta/util/treeMapHelper.ts | 306 ++++---- visualization/jest.config.json | 3 +- visualization/package.json | 2 +- 51 files changed, 2679 insertions(+), 650 deletions(-) create mode 100644 API.md create mode 100644 gh-pages/_posts/how-to/2020-08-17-fixate_folders_with_a_custom_cc_json.md create mode 100644 gh-pages/assets/images/posts/how-to/fixate-folders/fixated-folder-example.jpg create mode 100644 visualization/app/codeCharta/ressources/fixed-folders/example.json create mode 100644 visualization/app/codeCharta/ressources/fixed-folders/fixed-folders-example.ts create mode 100644 visualization/app/codeCharta/ressources/sample1_with_lower_minor_api.cc.json create mode 100644 visualization/app/codeCharta/util/__snapshots__/fileHelper.spec.ts.snap create mode 100644 visualization/app/codeCharta/util/fileHelper.spec.ts create mode 100644 visualization/app/codeCharta/util/fileHelper.ts diff --git a/API.md b/API.md new file mode 100644 index 0000000000..0ebf72e43f --- /dev/null +++ b/API.md @@ -0,0 +1,32 @@ +# API-Changelog + +## 1.1 + +- An additional optional property `edges` has been added to the `cc.json` +- Defines an array of edges between buildings +- Use SCMLogParser to generate edges + +```ts +export interface Edge { + fromNodeName: string + toNodeName: string + attributes: KeyValuePair +} +``` + +## 1.2 + +- An additional optional property `fixedPosition` has been added to the `cc.json` +- Property can be set to direct children of the root-folder +- Define `left` and `top` as the top-left corner of the folder +- Define `width` and `height` for the length in x and y-direction +- Folders can't overlap and must be defined in range of `[0-100]` + +```ts +export interface Fixed { + left: number + top: number + width: number + height: number +} +``` diff --git a/CHANGELOG.md b/CHANGELOG.md index 2851e0d01b..347c3f476b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,15 +9,22 @@ and this project adheres to [Semantic Versioning](http://semver.org/) ### Added 🚀 +- `fixedPosition` as a new property in the `cc.json` that allows to fixate folders in the map + ### Changed +- `cc.json` version updated to `1.2` + ### Removed 🗑 ### Fixed 🐞 - Compressed `cc.jsons (.gz) not marked as accepted when selecting a file in the file chooser -### Chore 👨‍💻 👩‍💻 +### Docs 🔎 + +- [How-To: Fixate Folders in the `cc.json`](https://maibornwolff.github.io//codecharta/how-to/fixate_folders_with_a_custom_cc_json/) +- CC-Json-API changes ## [1.56.0] - 2020-09-04 diff --git a/analysis/tools/ValidationTool/src/main/resources/cc.json b/analysis/tools/ValidationTool/src/main/resources/cc.json index 7d573c0cb3..c6f07f5a22 100644 --- a/analysis/tools/ValidationTool/src/main/resources/cc.json +++ b/analysis/tools/ValidationTool/src/main/resources/cc.json @@ -44,6 +44,9 @@ }, "type": "object" }, + "fixedPosition": { + "$ref": "#/definitions/FixedPosition" + }, "id": { "type": "number" }, @@ -165,6 +168,24 @@ "required": ["apiVersion", "nodes", "projectName"], "type": "object" }, + "FixedPosition": { + "properties": { + "height": { + "type": "number" + }, + "left": { + "type": "number" + }, + "top": { + "type": "number" + }, + "width": { + "type": "number" + } + }, + "required": ["height", "left", "top", "width"], + "type": "object" + }, "KeyValuePair": { "additionalProperties": { "type": "number" @@ -210,7 +231,7 @@ "type": "object" } ], - "minItems": 1, + "minItems": 0, "type": "array" }, "nodes": { @@ -234,7 +255,7 @@ "type": "object" } ], - "minItems": 1, + "minItems": 0, "type": "array" } }, diff --git a/gh-pages/_posts/how-to/2020-08-17-fixate_folders_with_a_custom_cc_json.md b/gh-pages/_posts/how-to/2020-08-17-fixate_folders_with_a_custom_cc_json.md new file mode 100644 index 0000000000..0478bc488b --- /dev/null +++ b/gh-pages/_posts/how-to/2020-08-17-fixate_folders_with_a_custom_cc_json.md @@ -0,0 +1,127 @@ +--- +categories: + - How-to +title: Fixate folders using a custom cc.json +--- + +# Introduction + +The [Squarified-Tree-Map-Algorithm](https://www.win.tue.nl/~vanwijk/stm.pdf) generates a layout for the visualized map. +The layout varies depending on the area of each node. Adding a new file to a folder might increase the area of the parent folder, +so that the algorithm decides that the parent-folder needs to be relocated. Thus, folders may not be located at the same position anymore as before. +Another way to follow this problem is by changing the area-metric. + +# Fixate folders + +In order to prevent folders from changing locations, we introduced a new attribute to the `.cc.json` called `fixedPosition`. +This attribute is not auto-generated during the analysis and has to be defined manually by editing the `.cc.json`. + +Setting this property is restricted to the top level folders and won't have any effect on sub-folders (folders in folders). + +# API + +```json + "fixedPosition": { + "left": 60, + "top:": 40, + "width": 35, + "height": 55 + } +``` + +The property values must be numbers in the range between 0 and 100. They represent the size of the value in percent. + +## Example + +![Example]({{site.baseurl}}/assets/images/posts/how-to/fixate-folders/fixated-folder-example.jpg) + +```json +{ + "projectName": "example-project", + "apiVersion": "1.2", + "nodes": [ + { + "name": "root", + "type": "Folder", + "attributes": {}, + "children": [ + { + "name": "folder_1_red", + "type": "Folder", + "attributes": {}, + "children": [], + "fixedPosition": { + "left": 10, + "top": 10, + "width": 30, + "height": 20 + } + }, + { + "name": "folder_2_orange", + "type": "Folder", + "attributes": {}, + "children": [], + "fixedPosition": { + "left": 50, + "top": 10, + "width": 40, + "height": 20 + } + }, + { + "name": "folder_3_blue", + "type": "Folder", + "attributes": {}, + "children": [], + "fixedPosition": { + "left": 10, + "top": 40, + "width": 20, + "height": 50 + } + }, + { + "name": "folder_4_green", + "type": "Folder", + "attributes": {}, + "children": [], + "fixedPosition": { + "left": 40, + "top": 40, + "width": 50, + "height": 20 + } + }, + { + "name": "folder_5_magenta", + "type": "Folder", + "attributes": {}, + "children": [], + "fixedPosition": { + "left": 40, + "top": 70, + "width": 50, + "height": 20 + } + } + ] + } + ] +} +``` + +A margin between folders is recommended. To apply one, set the coordinates for the folder in the top-left corner +to e.g., `left: 10` and `top: 10`. The margin between the border of the map and `folder_1` is therefore `10`. It may be chosen at will. +In order to define the coordinates of adjacent folders, apply any margin between `folder_1` and `folder_2`. +The example uses a margin of `10`, so that the coordinates of `folder_2` must be `left: 50` and `top: 10`. + +## Restrictions + +The following rules apply in order to build a valid custom `.cc.json`: + +- The values of `left`, `top`, `width` and `height` must be in range of `[0, 100] +- The value `left + width` or `top + height` must be in a range of `[0, 100]`. +- Folders may not overlap. +- Leaving space between folders (for visibility reasons) is recommended. +- All children of the root folder require the `fixedPosition` attribute. diff --git a/gh-pages/assets/images/posts/how-to/fixate-folders/fixated-folder-example.jpg b/gh-pages/assets/images/posts/how-to/fixate-folders/fixated-folder-example.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7581df4e14b1a6b7426f6c1c1496ebaa23e4c5a4 GIT binary patch literal 184208 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 {_t3EB6f*o%1%;6H|0$h5PLthZL3) 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 literal 0 HcmV?d00001 diff --git a/visualization/.eslintrc.js b/visualization/.eslintrc.js index e7340225ef..5fb8d858b5 100644 --- a/visualization/.eslintrc.js +++ b/visualization/.eslintrc.js @@ -33,22 +33,31 @@ module.exports = { "error", { "selector": ["variable", "function"], - "format": ["camelCase", "UPPER_CASE"], + "format": ["camelCase", "UPPER_CASE", "PascalCase"], "leadingUnderscore": "allow" } ], "@typescript-eslint/no-unused-vars": ["error"], "object-shorthand": ["error", "always"], + "no-else-return": ["error"], + "no-lonely-if": ["error"], // Do not apply inappropriate rules below - "@typescript-eslint/no-use-before-define": "off", - "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/no-use-before-define": + "off", + "@typescript-eslint/ban-ts-comment": + "off", // TODO fix and remove rules below - "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/explicit-module-boundary-types": "off", - "@typescript-eslint/ban-types": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/consistent-type-assertions": "off", + "@typescript-eslint/explicit-function-return-type": + "off", + "@typescript-eslint/explicit-module-boundary-types": + "off", + "@typescript-eslint/ban-types": + "off", + "@typescript-eslint/no-explicit-any": + "off", + "@typescript-eslint/consistent-type-assertions": + "off", } } diff --git a/visualization/app/codeCharta/assets/sample1.cc.json b/visualization/app/codeCharta/assets/sample1.cc.json index f44aef4e0e..f6f067b993 100755 --- a/visualization/app/codeCharta/assets/sample1.cc.json +++ b/visualization/app/codeCharta/assets/sample1.cc.json @@ -1,6 +1,6 @@ { "projectName": "Sample Project with Edges", - "apiVersion": "1.1", + "apiVersion": "1.2", "nodes": [ { "name": "root", diff --git a/visualization/app/codeCharta/assets/sample2.cc.json b/visualization/app/codeCharta/assets/sample2.cc.json index 789710e5e3..6c2ba19ac0 100755 --- a/visualization/app/codeCharta/assets/sample2.cc.json +++ b/visualization/app/codeCharta/assets/sample2.cc.json @@ -1,6 +1,6 @@ { "projectName": "Sample Project", - "apiVersion": "1.1", + "apiVersion": "1.2", "nodes": [ { "name": "root", diff --git a/visualization/app/codeCharta/assets/sample3.cc.json b/visualization/app/codeCharta/assets/sample3.cc.json index 2e038bb7c1..b0bfe1c218 100755 --- a/visualization/app/codeCharta/assets/sample3.cc.json +++ b/visualization/app/codeCharta/assets/sample3.cc.json @@ -1,6 +1,6 @@ { "projectName": "Sample Project", - "apiVersion": "1.1", + "apiVersion": "1.2", "nodes": [ { "name": "root", diff --git a/visualization/app/codeCharta/codeCharta.api.model.ts b/visualization/app/codeCharta/codeCharta.api.model.ts index 06b371959e..ccd17b6940 100644 --- a/visualization/app/codeCharta/codeCharta.api.model.ts +++ b/visualization/app/codeCharta/codeCharta.api.model.ts @@ -23,7 +23,8 @@ export enum ExportBlacklistType { export enum APIVersions { ZERO_POINT_ONE = "0.1", ONE_POINT_ZERO = "1.0", - ONE_POINT_ONE = "1.1" + ONE_POINT_ONE = "1.1", + ONE_POINT_TWO = "1.2" } export interface ExportScenario { @@ -32,6 +33,6 @@ export interface ExportScenario { } export interface OldAttributeTypes { - nodes?: [{ [key: string]: AttributeTypeValue }] - edges?: [{ [key: string]: AttributeTypeValue }] + nodes?: [{ [key: string]: AttributeTypeValue }?] + edges?: [{ [key: string]: AttributeTypeValue }?] } diff --git a/visualization/app/codeCharta/codeCharta.model.ts b/visualization/app/codeCharta/codeCharta.model.ts index 6e1b9cdbd0..856742cb31 100644 --- a/visualization/app/codeCharta/codeCharta.model.ts +++ b/visualization/app/codeCharta/codeCharta.model.ts @@ -40,6 +40,14 @@ export interface CodeMapNode { deltas?: { [key: string]: number } + fixedPosition?: FixedPosition +} + +export interface FixedPosition { + left: number + top: number + width: number + height: number } export enum NodeType { diff --git a/visualization/app/codeCharta/codeCharta.service.spec.ts b/visualization/app/codeCharta/codeCharta.service.spec.ts index 56c16d6413..c49d00add4 100755 --- a/visualization/app/codeCharta/codeCharta.service.spec.ts +++ b/visualization/app/codeCharta/codeCharta.service.spec.ts @@ -10,6 +10,7 @@ import { getCCFiles, isSingleState } from "./model/files/files.helper" import { DialogService } from "./ui/dialog/dialog.service" import { CCValidationResult, ERROR_MESSAGES } from "./util/fileValidator" import { setNodeMetricData } from "./state/store/metricData/nodeMetricData/nodeMetricData.actions" +import packageJson from "../../package.json" import { clone } from "./util/clone" describe("codeChartaService", () => { @@ -52,7 +53,7 @@ describe("codeChartaService", () => { describe("loadFiles", () => { const expected: CCFile = { - fileMeta: { apiVersion: "1.1", fileName, projectName: "Sample Map" }, + fileMeta: { apiVersion: packageJson.codecharta.apiVersion, fileName, projectName: "Sample Map" }, map: { attributes: {}, isExcluded: false, @@ -169,8 +170,7 @@ describe("codeChartaService", () => { it("should show error on invalid file", () => { const expectedError: CCValidationResult = { - title: ERROR_MESSAGES.fileIsInvalid.title, - error: [ERROR_MESSAGES.fileIsInvalid.message], + error: [ERROR_MESSAGES.fileIsInvalid], warning: [] } @@ -182,8 +182,7 @@ describe("codeChartaService", () => { it("should show error on a random string", () => { const expectedError: CCValidationResult = { - title: ERROR_MESSAGES.apiVersionIsInvalid.title, - error: [ERROR_MESSAGES.apiVersionIsInvalid.message], + error: [ERROR_MESSAGES.apiVersionIsInvalid], warning: [] } @@ -195,7 +194,6 @@ describe("codeChartaService", () => { it("should show error if a file is missing a required property", () => { const expectedError: CCValidationResult = { - title: ERROR_MESSAGES.validationError.title, error: ["Required error: should have required property 'projectName'"], warning: [] } @@ -224,7 +222,6 @@ describe("codeChartaService", () => { it("should break the loop after the first invalid file was validated", () => { const expectedError: CCValidationResult = { - title: ERROR_MESSAGES.validationError.title, error: ["Required error: should have required property 'projectName'"], warning: [] } diff --git a/visualization/app/codeCharta/codeCharta.service.ts b/visualization/app/codeCharta/codeCharta.service.ts index 14ccb9843b..85ff50f9e4 100755 --- a/visualization/app/codeCharta/codeCharta.service.ts +++ b/visualization/app/codeCharta/codeCharta.service.ts @@ -1,8 +1,8 @@ import { validate } from "./util/fileValidator" -import { AttributeTypes, BlacklistItem, BlacklistType, CCFile, NameDataPair } from "./codeCharta.model" +import { CCFile, NameDataPair } from "./codeCharta.model" import _ from "lodash" import { NodeDecorator } from "./util/nodeDecorator" -import { ExportBlacklistType, ExportCCFile, OldAttributeTypes } from "./codeCharta.api.model" +import { ExportCCFile } from "./codeCharta.api.model" import { StoreService } from "./state/store.service" import { resetFiles, setFiles, setSingle } from "./state/store/files/files.actions" import { getCCFiles } from "./model/files/files.helper" @@ -11,6 +11,7 @@ import { setState } from "./state/store/state.actions" import { ScenarioHelper } from "./util/scenarioHelper" import { setIsLoadingFile } from "./state/store/appSettings/isLoadingFile/isLoadingFile.actions" import { FileSelectionState, FileState } from "./model/files/files" +import { getCCFile } from "./util/fileHelper" export class CodeChartaService { public static ROOT_NAME = "root" @@ -50,53 +51,11 @@ export class CodeChartaService { } private addFile(fileName: string, migratedFile: ExportCCFile) { - const ccFile: CCFile = this.getCCFile(fileName, migratedFile) + const ccFile: CCFile = getCCFile(fileName, migratedFile) NodeDecorator.decorateMapWithPathAttribute(ccFile) this.fileStates.push({ file: ccFile, selectedAs: FileSelectionState.None }) } - private getCCFile(fileName: string, fileContent: ExportCCFile): CCFile { - return { - fileMeta: { - fileName, - projectName: fileContent.projectName, - apiVersion: fileContent.apiVersion - }, - settings: { - fileSettings: { - edges: fileContent.edges || [], - attributeTypes: this.getAttributeTypes(fileContent.attributeTypes), - blacklist: this.potentiallyUpdateBlacklistTypes(fileContent.blacklist || []), - markedPackages: fileContent.markedPackages || [] - } - }, - map: fileContent.nodes[0] - } - } - - private getAttributeTypes(attributeTypes: AttributeTypes | OldAttributeTypes): AttributeTypes { - if (_.isEmpty(attributeTypes) || !attributeTypes || Array.isArray(attributeTypes.nodes) || Array.isArray(attributeTypes.edges)) { - return { - nodes: {}, - edges: {} - } - } - - return { - nodes: !attributeTypes.nodes ? {} : attributeTypes.nodes, - edges: !attributeTypes.edges ? {} : attributeTypes.edges - } - } - - private potentiallyUpdateBlacklistTypes(blacklist): BlacklistItem[] { - blacklist.forEach(x => { - if (x.type === ExportBlacklistType.hide) { - x.type = BlacklistType.flatten - } - }) - return blacklist - } - private setDefaultScenario() { const { areaMetric, heightMetric, colorMetric } = ScenarioHelper.getDefaultScenarioSetting().dynamicSettings const names = [areaMetric, heightMetric, colorMetric] diff --git a/visualization/app/codeCharta/ressources/fixed-folders/example.json b/visualization/app/codeCharta/ressources/fixed-folders/example.json new file mode 100644 index 0000000000..65c33ac4d4 --- /dev/null +++ b/visualization/app/codeCharta/ressources/fixed-folders/example.json @@ -0,0 +1,386 @@ +{ + "projectName": "example-project", + "apiVersion": "1.1", + "nodes": [ + { + "name": "root", + "type": "Folder", + "attributes": {}, + "children": [ + { + "name": "folder_1", + "type": "Folder", + "attributes": {}, + "children": [ + { + "name": "children_1", + "type": "File", + "attributes": { + "rloc": 200, + "mcc": 45 + }, + "children": [] + }, + { + "name": "children_2", + "type": "File", + "attributes": { + "rloc": 400, + "mcc": 65 + }, + "children": [] + }, + { + "name": "children_3", + "type": "File", + "attributes": { + "rloc": 600, + "mcc": 25 + }, + "children": [] + }, + { + "name": "children_13", + "type": "File", + "attributes": { + "rloc": 95, + "mcc": 20 + }, + "children": [] + }, + { + "name": "children_14", + "type": "File", + "attributes": { + "rloc": 66, + "mcc": 72 + }, + "children": [] + } + ], + "fixedPosition": { + "left": 5, + "top": 5, + "width": 55, + "height": 15 + } + }, + { + "name": "folder_2", + "type": "Folder", + "attributes": {}, + "children": [ + { + "name": "children_4", + "type": "File", + "attributes": { + "rloc": 1000, + "mcc": 37 + }, + "children": [] + }, + { + "name": "children_5", + "type": "File", + "attributes": { + "rloc": 800, + "mcc": 10 + }, + "children": [] + }, + { + "name": "folder_7", + "type": "Folder", + "attributes": {}, + "children": [ + { + "name": "children_15", + "type": "File", + "attributes": { + "rloc": 500, + "mcc": 5 + }, + "children": [] + }, + { + "name": "children_16", + "type": "File", + "attributes": { + "rloc": 142, + "mcc": 36 + }, + "children": [] + }, + { + "name": "children_17", + "type": "File", + "attributes": { + "rloc": 1531, + "mcc": 156 + }, + "children": [] + } + ] + } + ], + "fixedPosition": { + "left": 5, + "top": 25, + "width": 25, + "height": 50 + } + }, + { + "name": "folder_3", + "type": "Folder", + "attributes": {}, + "children": [ + { + "name": "children_5", + "type": "File", + "attributes": { + "rloc": 450, + "mcc": 3 + }, + "children": [] + }, + { + "name": "children_6", + "type": "File", + "attributes": { + "rloc": 132, + "mcc": 10 + }, + "children": [] + }, + { + "name": "children_18", + "type": "File", + "attributes": { + "rloc": 2165, + "mcc": 168 + }, + "children": [] + }, + { + "name": "children_19", + "type": "File", + "attributes": { + "rloc": 789, + "mcc": 71 + }, + "children": [] + } + ], + "fixedPosition": { + "left": 35, + "top": 25, + "width": 25, + "height": 50 + } + }, + { + "name": "folder_4", + "type": "Folder", + "attributes": {}, + "children": [ + { + "name": "children_7", + "type": "File", + "attributes": { + "rloc": 200, + "mcc": 12 + }, + "children": [] + }, + { + "name": "children_8", + "type": "File", + "attributes": { + "rloc": 500, + "mcc": 35 + }, + "children": [] + }, + { + "name": "children_20", + "type": "File", + "attributes": { + "rloc": 516, + "mcc": 45 + }, + "children": [] + }, + { + "name": "children_21", + "type": "File", + "attributes": { + "rloc": 948, + "mcc": 23 + }, + "children": [] + }, + { + "name": "children_22", + "type": "File", + "attributes": { + "rloc": 1876, + "mcc": 65 + }, + "children": [] + }, + { + "name": "children_23", + "type": "File", + "attributes": { + "rloc": 654, + "mcc": 58 + }, + "children": [] + }, + { + "name": "children_24", + "type": "File", + "attributes": { + "rloc": 561, + "mcc": 66 + }, + "children": [] + }, + { + "name": "children_25", + "type": "File", + "attributes": { + "rloc": 987, + "mcc": 145 + }, + "children": [] + } + ], + "fixedPosition": { + "left": 65, + "top": 5, + "width": 10, + "height": 90 + } + }, + { + "name": "folder_5", + "type": "Folder", + "attributes": {}, + "children": [ + { + "name": "children_9", + "type": "File", + "attributes": { + "rloc": 100, + "mcc": 8 + }, + "children": [] + }, + { + "name": "children_10", + "type": "File", + "attributes": { + "rloc": 265, + "mcc": 10 + }, + "children": [] + }, + { + "name": "children_26", + "type": "File", + "attributes": { + "rloc": 123, + "mcc": 8 + }, + "children": [] + }, + { + "name": "children_27", + "type": "File", + "attributes": { + "rloc": 113, + "mcc": 32 + }, + "children": [] + } + ], + "fixedPosition": { + "left": 80, + "top": 5, + "width": 15, + "height": 90 + } + }, + { + "name": "folder_6", + "type": "Folder", + "attributes": {}, + "children": [ + { + "name": "children_11", + "type": "File", + "attributes": { + "rloc": 1200, + "mcc": 120 + }, + "children": [] + }, + { + "name": "children_12", + "type": "File", + "attributes": { + "rloc": 100, + "mcc": 24 + }, + "children": [] + }, + { + "name": "children_28", + "type": "File", + "attributes": { + "rloc": 612, + "mcc": 12 + }, + "children": [] + }, + { + "name": "children_29", + "type": "File", + "attributes": { + "rloc": 3652, + "mcc": 15 + }, + "children": [] + } + ], + "fixedPosition": { + "left": 5, + "top": 80, + "width": 55, + "height": 15 + } + } + ] + } + ], + "edges": [ + { + "fromNodeName": "/root/folder_1/children_1", + "toNodeName": "/root/folder_2/children_4", + "attributes": { + "pairingRate": 89, + "avgCommits": 34 + } + }, + { + "fromNodeName": "/root/folder_2/children_4", + "toNodeName": "/root/folder_3/children_5", + "attributes": { + "pairingRate": 32, + "avgCommits": 17 + } + } + ] +} diff --git a/visualization/app/codeCharta/ressources/fixed-folders/fixed-folders-example.ts b/visualization/app/codeCharta/ressources/fixed-folders/fixed-folders-example.ts new file mode 100644 index 0000000000..b44b37e296 --- /dev/null +++ b/visualization/app/codeCharta/ressources/fixed-folders/fixed-folders-example.ts @@ -0,0 +1,216 @@ +import { NodeType } from "../../codeCharta.model" +import { ExportCCFile } from "../../codeCharta.api.model" + +export const fileWithFixedFolders: ExportCCFile = { + projectName: "example-project", + apiVersion: "1.2", + nodes: [ + { + name: "root", + type: NodeType.FOLDER, + attributes: {}, + children: [ + { + name: "folder_1", + type: NodeType.FOLDER, + attributes: {}, + children: [ + { + name: "children_1", + type: NodeType.FILE, + attributes: { + custom_metric: 2 + }, + children: [] + }, + { + name: "children_2", + type: NodeType.FILE, + attributes: { + custom_metric: 4 + }, + children: [] + }, + { + name: "children_3", + type: NodeType.FILE, + attributes: { + custom_metric: 6 + }, + children: [] + } + ], + fixedPosition: { + left: 5, + top: 5, + width: 55, + height: 15 + } + }, + { + name: "folder_2", + type: NodeType.FOLDER, + attributes: {}, + children: [ + { + name: "children_4", + type: NodeType.FILE, + attributes: { + custom_metric: 10 + }, + children: [] + }, + { + name: "children_5", + type: NodeType.FILE, + attributes: { + custom_metric: 0 + }, + children: [] + } + ], + fixedPosition: { + left: 5, + top: 25, + width: 25, + height: 50 + } + }, + { + name: "folder_3", + type: NodeType.FOLDER, + attributes: {}, + children: [ + { + name: "children_6", + type: NodeType.FILE, + attributes: { + custom_metric: 4 + }, + children: [] + }, + { + name: "children_7", + type: NodeType.FILE, + attributes: { + custom_metric: 0 + }, + children: [] + } + ], + fixedPosition: { + left: 35, + top: 25, + width: 25, + height: 50 + } + }, + { + name: "folder_4", + type: NodeType.FOLDER, + attributes: {}, + children: [ + { + name: "children_8", + type: NodeType.FILE, + attributes: { + custom_metric: 20 + }, + children: [] + }, + { + name: "children_9", + type: NodeType.FILE, + attributes: { + custom_metric: 5 + }, + children: [] + } + ], + fixedPosition: { + left: 65, + top: 5, + width: 10, + height: 90 + } + }, + { + name: "folder_5", + type: NodeType.FOLDER, + attributes: {}, + children: [ + { + name: "children_10", + type: NodeType.FILE, + attributes: { + custom_metric: 10 + }, + children: [] + }, + { + name: "children_11", + type: NodeType.FILE, + attributes: { + custom_metric: 0 + }, + children: [] + } + ], + fixedPosition: { + left: 80, + top: 5, + width: 15, + height: 90 + } + }, + { + name: "folder_6", + type: NodeType.FOLDER, + attributes: {}, + children: [ + { + name: "children_12", + type: NodeType.FILE, + attributes: { + custom_metric: 1 + }, + children: [] + }, + { + name: "children_13", + type: NodeType.FILE, + attributes: { + custom_metric: 1 + }, + children: [] + } + ], + fixedPosition: { + left: 5, + top: 80, + width: 55, + height: 15 + } + } + ] + } + ], + edges: [ + { + fromNodeName: "/root/folder_1/children_1", + toNodeName: "/root/folder_2/children_4", + attributes: { + pairingRate: 89, + avgCommits: 34 + } + }, + { + fromNodeName: "/root/folder_2/children_4", + toNodeName: "/root/folder_3/children_5", + attributes: { + pairingRate: 32, + avgCommits: 17 + } + } + ] +} diff --git a/visualization/app/codeCharta/ressources/sample1_with_different_edges.cc.json b/visualization/app/codeCharta/ressources/sample1_with_different_edges.cc.json index ee52f187b2..807cd76a7b 100755 --- a/visualization/app/codeCharta/ressources/sample1_with_different_edges.cc.json +++ b/visualization/app/codeCharta/ressources/sample1_with_different_edges.cc.json @@ -1,6 +1,6 @@ { "projectName": "Sample Project with different edges", - "apiVersion": "1.1", + "apiVersion": "1.2", "nodes": [ { "name": "root", diff --git a/visualization/app/codeCharta/ressources/sample1_with_lower_minor_api.cc.json b/visualization/app/codeCharta/ressources/sample1_with_lower_minor_api.cc.json new file mode 100644 index 0000000000..f44aef4e0e --- /dev/null +++ b/visualization/app/codeCharta/ressources/sample1_with_lower_minor_api.cc.json @@ -0,0 +1,96 @@ +{ + "projectName": "Sample Project with Edges", + "apiVersion": "1.1", + "nodes": [ + { + "name": "root", + "type": "Folder", + "attributes": {}, + "children": [ + { + "name": "sample1OnlyLeaf.scss", + "type": "File", + "attributes": { + "rloc": 400, + "functions": 10, + "mcc": 100, + "pairingRate": 32, + "avgCommits": 17 + }, + "link": "http://www.google.de" + }, + { + "name": "bigLeaf.ts", + "type": "File", + "attributes": { + "rloc": 100, + "functions": 10, + "mcc": 1, + "pairingRate": 77, + "avgCommits": 56 + }, + "link": "http://www.google.de" + }, + { + "name": "ParentLeaf", + "type": "Folder", + "attributes": {}, + "children": [ + { + "name": "smallLeaf.html", + "type": "File", + "attributes": { + "rloc": 30, + "functions": 100, + "mcc": 100, + "pairingRate": 60, + "avgCommits": 51 + } + }, + { + "name": "otherSmallLeaf.ts", + "type": "File", + "attributes": { + "rloc": 70, + "functions": 1000, + "mcc": 10, + "pairingRate": 65, + "avgCommits": 22 + } + } + ] + } + ] + } + ], + "edges": [ + { + "fromNodeName": "/root/bigLeaf.ts", + "toNodeName": "/root/ParentLeaf/smallLeaf.html", + "attributes": { + "pairingRate": 89, + "avgCommits": 34 + } + }, + { + "fromNodeName": "/root/sample1OnlyLeaf.scss", + "toNodeName": "/root/ParentLeaf/smallLeaf.html", + "attributes": { + "pairingRate": 32, + "avgCommits": 17 + } + }, + { + "fromNodeName": "/root/ParentLeaf/otherSmallLeaf.ts", + "toNodeName": "/root/bigLeaf.ts", + "attributes": { + "pairingRate": 65, + "avgCommits": 22 + } + } + ], + "attributeTypes": { + "nodes": { "rloc": "absolute", "functions": "absolute", "mcc": "absolute", "pairingRate": "relative" }, + "edges": { "pairingRate": "relative", "avgCommits": "absolute" } + } +} diff --git a/visualization/app/codeCharta/state/nodeSearch.service.ts b/visualization/app/codeCharta/state/nodeSearch.service.ts index e59d96bb98..cffca7e130 100644 --- a/visualization/app/codeCharta/state/nodeSearch.service.ts +++ b/visualization/app/codeCharta/state/nodeSearch.service.ts @@ -34,12 +34,11 @@ export class NodeSearchService implements SearchPatternSubscriber { private findSearchedNodes(searchPattern: string): CodeMapNode[] { if (searchPattern.length == 0) { return [] - } else { - const nodes = hierarchy(this.codeMapPreRenderService.getRenderMap()) - .descendants() - .map(d => d.data) - return CodeMapHelper.getNodesByGitignorePath(nodes, searchPattern) } + const nodes = hierarchy(this.codeMapPreRenderService.getRenderMap()) + .descendants() + .map(d => d.data) + return CodeMapHelper.getNodesByGitignorePath(nodes, searchPattern) } private applySettingsSearchedNodePaths() { diff --git a/visualization/app/codeCharta/state/store/dynamicSettings/focusedNodePath/focusedNodePath.splitter.ts b/visualization/app/codeCharta/state/store/dynamicSettings/focusedNodePath/focusedNodePath.splitter.ts index 0c2550b633..caf3ce3be8 100644 --- a/visualization/app/codeCharta/state/store/dynamicSettings/focusedNodePath/focusedNodePath.splitter.ts +++ b/visualization/app/codeCharta/state/store/dynamicSettings/focusedNodePath/focusedNodePath.splitter.ts @@ -3,7 +3,6 @@ import { FocusedNodePathAction, focusNode, unfocusNode } from "./focusedNodePath export function splitFocusedNodePathAction(payload: string): FocusedNodePathAction { if (payload === "") { return unfocusNode() - } else { - return focusNode(payload) } + return focusNode(payload) } diff --git a/visualization/app/codeCharta/state/store/files/files.reducer.ts b/visualization/app/codeCharta/state/store/files/files.reducer.ts index e76ce64a11..0de7e1961f 100644 --- a/visualization/app/codeCharta/state/store/files/files.reducer.ts +++ b/visualization/app/codeCharta/state/store/files/files.reducer.ts @@ -48,9 +48,8 @@ function setSingleByName(state: FileState[], fileName: string): FileState[] { return state.map(x => { if (x.file.fileMeta.fileName === fileName) { return { ...x, selectedAs: FileSelectionState.Single } - } else { - return { ...x, selectedAs: FileSelectionState.None } } + return { ...x, selectedAs: FileSelectionState.None } }) } @@ -64,9 +63,8 @@ function setDeltaByNames(state: FileState[], referenceFileName: string, comparis return { ...x, selectedAs: FileSelectionState.Reference } } else if (x.file.fileMeta.fileName === comparisonFileName) { return { ...x, selectedAs: FileSelectionState.Comparison } - } else { - return { ...x, selectedAs: FileSelectionState.None } } + return { ...x, selectedAs: FileSelectionState.None } }) } @@ -78,9 +76,8 @@ function setMultipleByNames(state: FileState[], partialFileNames: string[]): Fil return state.map(x => { if (partialFileNames.includes(x.file.fileMeta.fileName)) { return { ...x, selectedAs: FileSelectionState.Partial } - } else { - return { ...x, selectedAs: FileSelectionState.None } } + return { ...x, selectedAs: FileSelectionState.None } }) } diff --git a/visualization/app/codeCharta/ui/codeMap/codeMap.preRender.service.ts b/visualization/app/codeCharta/ui/codeMap/codeMap.preRender.service.ts index 1874d98481..4fffe9e171 100755 --- a/visualization/app/codeCharta/ui/codeMap/codeMap.preRender.service.ts +++ b/visualization/app/codeCharta/ui/codeMap/codeMap.preRender.service.ts @@ -149,11 +149,10 @@ export class CodeMapPreRenderService implements StoreSubscriber, MetricDataSubsc const referenceFile = visibleFileStates.find(x => x.selectedAs == FileSelectionState.Reference).file const comparisonFile = visibleFileStates.find(x => x.selectedAs == FileSelectionState.Comparison).file return DeltaGenerator.getDeltaFile(referenceFile, comparisonFile) - } else { - const referenceFile = visibleFileStates[0].file - const comparisonFile = visibleFileStates[0].file - return DeltaGenerator.getDeltaFile(referenceFile, comparisonFile) } + const referenceFile = visibleFileStates[0].file + const comparisonFile = visibleFileStates[0].file + return DeltaGenerator.getDeltaFile(referenceFile, comparisonFile) } private renderAndNotify() { diff --git a/visualization/app/codeCharta/ui/codeMap/rendering/geometryGenerator.ts b/visualization/app/codeCharta/ui/codeMap/rendering/geometryGenerator.ts index a8e508a64a..bba07e4248 100644 --- a/visualization/app/codeCharta/ui/codeMap/rendering/geometryGenerator.ts +++ b/visualization/app/codeCharta/ui/codeMap/rendering/geometryGenerator.ts @@ -94,9 +94,8 @@ export class GeometryGenerator { const markingColorAsNumber = ColorConverter.getNumber(n.markingColor) const markingColorWithGradient = markingColorAsNumber & (n.depth % 2 === 0 ? 0xdddddd : 0xffffff) return ColorConverter.convertNumberToHex(markingColorWithGradient) - } else { - return this.floorGradient[n.depth] } + return this.floorGradient[n.depth] } private addBuilding( diff --git a/visualization/app/codeCharta/ui/codeMap/threeViewer/threeSceneService.ts b/visualization/app/codeCharta/ui/codeMap/threeViewer/threeSceneService.ts index ecd04e2aa8..18d8ca9b5f 100755 --- a/visualization/app/codeCharta/ui/codeMap/threeViewer/threeSceneService.ts +++ b/visualization/app/codeCharta/ui/codeMap/threeViewer/threeSceneService.ts @@ -149,9 +149,8 @@ export class ThreeSceneService implements CodeMapPreRenderServiceSubscriber { public getHighlightedNode(): Node { if (this.getHighlightedBuilding()) { return this.getHighlightedBuilding().node - } else { - return null } + return null } private reselectBuilding() { diff --git a/visualization/app/codeCharta/ui/dialog/dialog.service.ts b/visualization/app/codeCharta/ui/dialog/dialog.service.ts index af204a7cc7..9791bbbd4a 100755 --- a/visualization/app/codeCharta/ui/dialog/dialog.service.ts +++ b/visualization/app/codeCharta/ui/dialog/dialog.service.ts @@ -38,7 +38,7 @@ export class DialogService { const htmlMessage = this.buildHtmlMessage(warningSymbol, validationResult.warning) - this.showErrorDialog(htmlMessage, validationResult.title) + this.showErrorDialog(htmlMessage, "Validation Warning") } public showValidationErrorDialog(validationResult: CCValidationResult) { @@ -46,7 +46,7 @@ export class DialogService { const htmlMessage = this.buildHtmlMessage(errorSymbol, validationResult.error) - this.showErrorDialogAndOpenFileChooser(htmlMessage, validationResult.title) + this.showErrorDialogAndOpenFileChooser(htmlMessage, "Validation Error") } private buildHtmlMessage(symbol: string, validationResult: string[]): string { diff --git a/visualization/app/codeCharta/ui/fileChooser/fileChooser.e2e.ts b/visualization/app/codeCharta/ui/fileChooser/fileChooser.e2e.ts index 0d246554b6..a399153584 100644 --- a/visualization/app/codeCharta/ui/fileChooser/fileChooser.e2e.ts +++ b/visualization/app/codeCharta/ui/fileChooser/fileChooser.e2e.ts @@ -2,6 +2,7 @@ import { goto } from "../../../puppeteer.helper" import { FileChooserPageObject } from "./fileChooser.po" import { FilePanelPageObject } from "../filePanel/filePanel.po" import { DialogErrorPageObject } from "../dialog/dialog.error.po" +import { ERROR_MESSAGES } from "../../util/fileValidator" describe("FileChooser", () => { let fileChooser: FileChooserPageObject @@ -21,6 +22,7 @@ describe("FileChooser", () => { expect(await filePanel.getSelectedName()).toEqual("sample3.cc.json") }) + it("should load multiple cc.json files", async () => { await fileChooser.openFiles(["./app/codeCharta/assets/sample3.cc.json", "./app/codeCharta/assets/sample2.cc.json"]) @@ -40,7 +42,7 @@ describe("FileChooser", () => { it("should open an invalid file, close the dialog and open a valid file", async () => { await fileChooser.openFiles(["./app/codeCharta/assets/logo.png"]) - expect(await dialogError.getMessage()).toEqual(" file is empty or invalid") + expect(await dialogError.getMessage()).toEqual(` ${ERROR_MESSAGES.fileIsInvalid}`) await dialogError.clickOk() @@ -51,7 +53,7 @@ describe("FileChooser", () => { it("should open an valid and an invalid file, close the dialog and open a valid file", async () => { await fileChooser.openFiles(["./app/codeCharta/assets/logo.png", "./app/codeCharta/assets/sample3.cc.json"]) - expect(await dialogError.getMessage()).toEqual(" file is empty or invalid") + expect(await dialogError.getMessage()).toEqual(` ${ERROR_MESSAGES.fileIsInvalid}`) await dialogError.clickOk() @@ -63,14 +65,20 @@ describe("FileChooser", () => { it("should not load a map and show error, when loading a map with warning and a map with error", async () => { await fileChooser.openFiles(["./app/codeCharta/ressources/sample1_with_api_warning.cc.json", "./app/codeCharta/assets/logo.png"]) - expect(await dialogError.getMessage()).toEqual(" Minor API Version Outdated") + expect(await dialogError.getMessage()).toEqual(` ${ERROR_MESSAGES.minorApiVersionOutdated} Found: 1.5`) await dialogError.waitUntilDialogIsClosed() - expect(await dialogError.getMessage()).toEqual(" file is empty or invalid") + expect(await dialogError.getMessage()).toEqual(` ${ERROR_MESSAGES.fileIsInvalid}`) await dialogError.clickOk() await fileChooser.openFiles(["./app/codeCharta/assets/sample3.cc.json"], false) expect(await filePanel.getSelectedName()).toEqual("sample3.cc.json") }) + + it("should be able to open a cc.json with a lower minor api version without a warning", async () => { + await fileChooser.openFiles(["./app/codeCharta/ressources/sample1_with_lower_minor_api.cc.json"]) + + expect(await filePanel.getSelectedName()).toEqual("sample1_with_lower_minor_api.cc.json") + }) }) diff --git a/visualization/app/codeCharta/ui/filePanel/filePanel.component.ts b/visualization/app/codeCharta/ui/filePanel/filePanel.component.ts index 41b1e578bb..c0766ffb23 100644 --- a/visualization/app/codeCharta/ui/filePanel/filePanel.component.ts +++ b/visualization/app/codeCharta/ui/filePanel/filePanel.component.ts @@ -154,9 +154,8 @@ export class FilePanelController implements FilesSelectionSubscriber { const visibleFileStates = getVisibleFileStates(this._viewModel.files) if (fileStatesAvailable(this._viewModel.files)) { return visibleFileStates[0].file.fileMeta.fileName - } else { - return this._viewModel.files[0].file.fileMeta.fileName } + return this._viewModel.files[0].file.fileMeta.fileName } else if (this.lastRenderState === FileSelectionState.Comparison) { return this._viewModel.selectedFileNames.delta.reference } diff --git a/visualization/app/codeCharta/ui/metricValueHovered/metricValueHovered.component.ts b/visualization/app/codeCharta/ui/metricValueHovered/metricValueHovered.component.ts index a3ec37ed96..fd09bd7573 100644 --- a/visualization/app/codeCharta/ui/metricValueHovered/metricValueHovered.component.ts +++ b/visualization/app/codeCharta/ui/metricValueHovered/metricValueHovered.component.ts @@ -71,9 +71,8 @@ export class MetricValueHoveredController return this.POSITIVE_COLOR } else if (heightDelta < 0) { return this.NEGATIVE_COLOR - } else { - return this.NEUTRAL_COLOR } + return this.NEUTRAL_COLOR } //TODO: Check if this is required after finishing redux diff --git a/visualization/app/codeCharta/ui/nodeContextMenu/nodeContextMenu.component.ts b/visualization/app/codeCharta/ui/nodeContextMenu/nodeContextMenu.component.ts index 550e7612ed..4080abcf7a 100644 --- a/visualization/app/codeCharta/ui/nodeContextMenu/nodeContextMenu.component.ts +++ b/visualization/app/codeCharta/ui/nodeContextMenu/nodeContextMenu.component.ts @@ -153,9 +153,8 @@ export class NodeContextMenuController if (this.isNodeMarked()) { return this.packageMatchesColor(color) - } else { - return this.packageMatchesColorOfParentMP(color) } + return this.packageMatchesColorOfParentMP(color) } private isClickInsideNodeContextMenu(mousePosition: Vector2) { diff --git a/visualization/app/codeCharta/ui/viewCube/viewCube.mouseEvents.service.ts b/visualization/app/codeCharta/ui/viewCube/viewCube.mouseEvents.service.ts index 89a78fc23b..32f84001f4 100644 --- a/visualization/app/codeCharta/ui/viewCube/viewCube.mouseEvents.service.ts +++ b/visualization/app/codeCharta/ui/viewCube/viewCube.mouseEvents.service.ts @@ -71,10 +71,8 @@ export class ViewCubeMouseEventsService { if (cube) { if (this.currentlyHovered && cube.uuid !== this.currentlyHovered.uuid) { this.triggerViewCubeUnhoverEvent() - } else { - if (!this.currentlyHovered) { - this.triggerViewCubeHoverEvent(cube) - } + } else if (!this.currentlyHovered) { + this.triggerViewCubeHoverEvent(cube) } } else { if (this.currentlyHovered) { diff --git a/visualization/app/codeCharta/util/__snapshots__/aggregationGenerator.spec.ts.snap b/visualization/app/codeCharta/util/__snapshots__/aggregationGenerator.spec.ts.snap index 77186fbbab..e787981a2d 100644 --- a/visualization/app/codeCharta/util/__snapshots__/aggregationGenerator.spec.ts.snap +++ b/visualization/app/codeCharta/util/__snapshots__/aggregationGenerator.spec.ts.snap @@ -3,7 +3,7 @@ exports[`AggregationGenerator multipleService aggregation of four maps 1`] = ` Object { "fileMeta": Object { - "apiVersion": "1.1", + "apiVersion": "1.2", "fileName": "file_aggregation_of_file1_and_file2_and_file1_and_file2", "projectName": "project_aggregation_of_Sample_Project_and_Sample_Project_and_Sample_Project_and_Sample_Project", }, @@ -219,7 +219,7 @@ Object { exports[`AggregationGenerator multipleService aggregation of two maps 1`] = ` Object { "fileMeta": Object { - "apiVersion": "1.1", + "apiVersion": "1.2", "fileName": "file_aggregation_of_file1_and_file2", "projectName": "project_aggregation_of_Sample_Project_and_Sample_Project", }, @@ -344,7 +344,7 @@ Object { exports[`AggregationGenerator multipleService aggregation one map 1`] = ` Object { "fileMeta": Object { - "apiVersion": "1.1", + "apiVersion": "1.2", "fileName": "file1", "projectName": "Sample Project", }, diff --git a/visualization/app/codeCharta/util/__snapshots__/fileHelper.spec.ts.snap b/visualization/app/codeCharta/util/__snapshots__/fileHelper.spec.ts.snap new file mode 100644 index 0000000000..a9c42d49ae --- /dev/null +++ b/visualization/app/codeCharta/util/__snapshots__/fileHelper.spec.ts.snap @@ -0,0 +1,74 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FileHelper getCCFile should build a CCFile 1`] = ` +Object { + "fileMeta": Object { + "apiVersion": "1.2", + "fileName": "fileName", + "projectName": "Sample Map", + }, + "map": Object { + "attributes": Object {}, + "children": Array [ + Object { + "attributes": Object { + "functions": 10, + "mcc": 1, + "rloc": 100, + }, + "isExcluded": false, + "isFlattened": false, + "link": "http://www.google.de", + "name": "big leaf", + "type": "File", + }, + Object { + "attributes": Object {}, + "children": Array [ + Object { + "attributes": Object { + "functions": 100, + "mcc": 100, + "rloc": 30, + }, + "isExcluded": false, + "isFlattened": false, + "name": "small leaf", + "type": "File", + }, + Object { + "attributes": Object { + "functions": 1000, + "mcc": 10, + "rloc": 70, + }, + "isExcluded": false, + "isFlattened": false, + "name": "other small leaf", + "type": "File", + }, + ], + "isExcluded": false, + "isFlattened": false, + "name": "Parent Leaf", + "type": "Folder", + }, + ], + "isExcluded": false, + "isFlattened": false, + "name": "root", + "type": "Folder", + }, + "settings": Object { + "fileSettings": Object { + "attributeTypes": Object { + "edges": Object {}, + "nodes": Object {}, + }, + "blacklist": Array [], + "edges": Array [], + "markedPackages": Array [], + }, + }, +} +`; diff --git a/visualization/app/codeCharta/util/__snapshots__/treeMapGenerator.spec.ts.snap b/visualization/app/codeCharta/util/__snapshots__/treeMapGenerator.spec.ts.snap index 007bf18ebc..a013416a13 100644 --- a/visualization/app/codeCharta/util/__snapshots__/treeMapGenerator.spec.ts.snap +++ b/visualization/app/codeCharta/util/__snapshots__/treeMapGenerator.spec.ts.snap @@ -350,3 +350,674 @@ Array [ }, ] `; + +exports[`treeMapGenerator create Treemap nodes should build the tree map with valid coordinates using the fixed folder structure 1`] = ` +Array [ + Object { + "attributes": Object {}, + "color": "#666666", + "deltas": undefined, + "depth": 0, + "edgeAttributes": undefined, + "flat": false, + "height": 2, + "heightDelta": 0, + "id": 0, + "incomingEdgePoint": Vector3 { + "x": -200, + "y": 2, + "z": -225, + }, + "isLeaf": false, + "length": 929.3250516799595, + "link": undefined, + "markingColor": null, + "name": "root", + "outgoingEdgePoint": Vector3 { + "x": -200, + "y": 2, + "z": -175, + }, + "path": "/root", + "visible": true, + "width": 929.3250516799595, + "x0": 0, + "y0": 0, + "z0": 0, + }, + Object { + "attributes": Object {}, + "color": "#666666", + "deltas": undefined, + "depth": 1, + "edgeAttributes": undefined, + "flat": false, + "height": 2, + "heightDelta": 0, + "id": undefined, + "incomingEdgePoint": Vector3 { + "x": -75.75155281000758, + "y": 4, + "z": -133.83436854000507, + }, + "isLeaf": false, + "length": 139.39875775199394, + "link": undefined, + "markingColor": null, + "name": "folder_1", + "outgoingEdgePoint": Vector3 { + "x": 179.81283640198131, + "y": 4, + "z": -133.83436854000507, + }, + "path": "/root/folder_1", + "visible": true, + "width": 511.1287784239778, + "x0": 46.46625258399798, + "y0": 46.46625258399798, + "z0": 2, + }, + Object { + "attributes": Object { + "custom_metric": 2, + }, + "color": "#666666", + "deltas": undefined, + "depth": 2, + "edgeAttributes": undefined, + "flat": false, + "height": 500, + "heightDelta": 0, + "id": undefined, + "incomingEdgePoint": Vector3 { + "x": -55.88581891174769, + "y": 504, + "z": -197.06887749127185, + }, + "isLeaf": true, + "length": 0, + "link": undefined, + "markingColor": null, + "name": "children_1", + "outgoingEdgePoint": Vector3 { + "x": 159.94710250372148, + "y": 504, + "z": -197.06887749127185, + }, + "path": "/root/folder_1/children_1", + "visible": true, + "width": 431.66584283093835, + "x0": 86.19772038051772, + "y0": 52.93112250872814, + "z0": 4, + }, + Object { + "attributes": Object { + "custom_metric": 4, + }, + "color": "#666666", + "deltas": undefined, + "depth": 2, + "edgeAttributes": undefined, + "flat": false, + "height": 500, + "heightDelta": 0, + "id": undefined, + "incomingEdgePoint": Vector3 { + "x": -55.88581891174769, + "y": 504, + "z": -197.06887749127185, + }, + "isLeaf": true, + "length": 0, + "link": undefined, + "markingColor": null, + "name": "children_2", + "outgoingEdgePoint": Vector3 { + "x": 159.94710250372148, + "y": 504, + "z": -197.06887749127185, + }, + "path": "/root/folder_1/children_2", + "visible": true, + "width": 431.66584283093835, + "x0": 86.19772038051772, + "y0": 52.93112250872814, + "z0": 4, + }, + Object { + "attributes": Object { + "custom_metric": 6, + }, + "color": "#666666", + "deltas": undefined, + "depth": 2, + "edgeAttributes": undefined, + "flat": false, + "height": 500, + "heightDelta": 0, + "id": undefined, + "incomingEdgePoint": Vector3 { + "x": -55.88581891174769, + "y": 504, + "z": -197.06887749127185, + }, + "isLeaf": true, + "length": 0, + "link": undefined, + "markingColor": null, + "name": "children_3", + "outgoingEdgePoint": Vector3 { + "x": 159.94710250372148, + "y": 504, + "z": -197.06887749127185, + }, + "path": "/root/folder_1/children_3", + "visible": true, + "width": 431.66584283093835, + "x0": 86.19772038051772, + "y0": 52.93112250872814, + "z0": 4, + }, + Object { + "attributes": Object {}, + "color": "#666666", + "deltas": undefined, + "depth": 1, + "edgeAttributes": undefined, + "flat": false, + "height": 2, + "heightDelta": 0, + "id": undefined, + "incomingEdgePoint": Vector3 { + "x": -87.36811595600707, + "y": 4, + "z": 98.49689437998484, + }, + "isLeaf": false, + "length": 464.6625258399798, + "link": undefined, + "markingColor": null, + "name": "folder_2", + "outgoingEdgePoint": Vector3 { + "x": -87.36811595600707, + "y": 4, + "z": 330.8281572999747, + }, + "path": "/root/folder_2", + "visible": true, + "width": 232.3312629199899, + "x0": 46.46625258399798, + "y0": 232.33126291998988, + "z0": 2, + }, + Object { + "attributes": Object { + "custom_metric": 10, + }, + "color": "#666666", + "deltas": undefined, + "depth": 2, + "edgeAttributes": undefined, + "flat": false, + "height": 500, + "heightDelta": 0, + "id": undefined, + "incomingEdgePoint": Vector3 { + "x": -191.8732678385811, + "y": 504, + "z": 119.12212394256221, + }, + "isLeaf": true, + "length": 382.16160758967044, + "link": undefined, + "markingColor": null, + "name": "children_4", + "outgoingEdgePoint": Vector3 { + "x": -191.8732678385811, + "y": 504, + "z": 310.20292773739743, + }, + "path": "/root/folder_2/children_4", + "visible": true, + "width": 0, + "x0": 58.12673216141889, + "y0": 273.5817220451446, + "z0": 4, + }, + Object { + "attributes": Object { + "custom_metric": 0, + }, + "color": "#666666", + "deltas": undefined, + "depth": 2, + "edgeAttributes": undefined, + "flat": false, + "height": 500, + "heightDelta": 0, + "id": undefined, + "incomingEdgePoint": Vector3 { + "x": -191.8732678385811, + "y": 504, + "z": 119.12212394256221, + }, + "isLeaf": true, + "length": 382.16160758967044, + "link": undefined, + "markingColor": null, + "name": "children_5", + "outgoingEdgePoint": Vector3 { + "x": -191.8732678385811, + "y": 504, + "z": 310.20292773739743, + }, + "path": "/root/folder_2/children_5", + "visible": true, + "width": 0, + "x0": 58.12673216141889, + "y0": 273.5817220451446, + "z0": 4, + }, + Object { + "attributes": Object {}, + "color": "#666666", + "deltas": undefined, + "depth": 1, + "edgeAttributes": undefined, + "flat": false, + "height": 2, + "heightDelta": 0, + "id": undefined, + "incomingEdgePoint": Vector3 { + "x": 191.4293995479808, + "y": 4, + "z": 98.49689437998484, + }, + "isLeaf": false, + "length": 464.6625258399798, + "link": undefined, + "markingColor": null, + "name": "folder_3", + "outgoingEdgePoint": Vector3 { + "x": 191.4293995479808, + "y": 4, + "z": 330.8281572999747, + }, + "path": "/root/folder_3", + "visible": true, + "width": 232.3312629199899, + "x0": 325.26376808798585, + "y0": 232.33126291998988, + "z0": 2, + }, + Object { + "attributes": Object { + "custom_metric": 4, + }, + "color": "#666666", + "deltas": undefined, + "depth": 2, + "edgeAttributes": undefined, + "flat": false, + "height": 500, + "heightDelta": 0, + "id": undefined, + "incomingEdgePoint": Vector3 { + "x": 86.92424766540677, + "y": 504, + "z": 119.12212394256221, + }, + "isLeaf": true, + "length": 382.16160758967044, + "link": undefined, + "markingColor": null, + "name": "children_6", + "outgoingEdgePoint": Vector3 { + "x": 86.92424766540677, + "y": 504, + "z": 310.20292773739743, + }, + "path": "/root/folder_3/children_6", + "visible": true, + "width": 0, + "x0": 336.92424766540677, + "y0": 273.5817220451446, + "z0": 4, + }, + Object { + "attributes": Object { + "custom_metric": 0, + }, + "color": "#666666", + "deltas": undefined, + "depth": 2, + "edgeAttributes": undefined, + "flat": false, + "height": 500, + "heightDelta": 0, + "id": undefined, + "incomingEdgePoint": Vector3 { + "x": 86.92424766540677, + "y": 504, + "z": 119.12212394256221, + }, + "isLeaf": true, + "length": 382.16160758967044, + "link": undefined, + "markingColor": null, + "name": "children_7", + "outgoingEdgePoint": Vector3 { + "x": 86.92424766540677, + "y": 504, + "z": 310.20292773739743, + }, + "path": "/root/folder_3/children_7", + "visible": true, + "width": 0, + "x0": 336.92424766540677, + "y0": 273.5817220451446, + "z0": 4, + }, + Object { + "attributes": Object {}, + "color": "#666666", + "deltas": undefined, + "depth": 1, + "edgeAttributes": undefined, + "flat": false, + "height": 2, + "heightDelta": 0, + "id": undefined, + "incomingEdgePoint": Vector3 { + "x": 400.5275361759717, + "y": 4, + "z": 5.564389211988868, + }, + "isLeaf": false, + "length": 836.3925465119636, + "link": undefined, + "markingColor": null, + "name": "folder_4", + "outgoingEdgePoint": Vector3 { + "x": 400.5275361759717, + "y": 4, + "z": 423.76066246797063, + }, + "path": "/root/folder_4", + "visible": true, + "width": 92.93250516799594, + "x0": 604.0612835919737, + "y0": 46.46625258399798, + "z0": 2, + }, + Object { + "attributes": Object { + "custom_metric": 20, + }, + "color": "#666666", + "deltas": undefined, + "depth": 2, + "edgeAttributes": undefined, + "flat": false, + "height": 500, + "heightDelta": 0, + "id": undefined, + "incomingEdgePoint": Vector3 { + "x": 359.12236739451646, + "y": 504, + "z": 36.8952237531623, + }, + "isLeaf": true, + "length": 711.06920834727, + "link": undefined, + "markingColor": null, + "name": "children_8", + "outgoingEdgePoint": Vector3 { + "x": 359.12236739451646, + "y": 504, + "z": 392.4298279267973, + }, + "path": "/root/folder_4/children_8", + "visible": true, + "width": 0, + "x0": 609.1223673945165, + "y0": 109.1279216663448, + "z0": 4, + }, + Object { + "attributes": Object { + "custom_metric": 5, + }, + "color": "#666666", + "deltas": undefined, + "depth": 2, + "edgeAttributes": undefined, + "flat": false, + "height": 500, + "heightDelta": 0, + "id": undefined, + "incomingEdgePoint": Vector3 { + "x": 359.12236739451646, + "y": 504, + "z": 36.8952237531623, + }, + "isLeaf": true, + "length": 711.06920834727, + "link": undefined, + "markingColor": null, + "name": "children_9", + "outgoingEdgePoint": Vector3 { + "x": 359.12236739451646, + "y": 504, + "z": 392.4298279267973, + }, + "path": "/root/folder_4/children_9", + "visible": true, + "width": 0, + "x0": 609.1223673945165, + "y0": 109.1279216663448, + "z0": 4, + }, + Object { + "attributes": Object {}, + "color": "#666666", + "deltas": undefined, + "depth": 1, + "edgeAttributes": undefined, + "flat": false, + "height": 2, + "heightDelta": 0, + "id": undefined, + "incomingEdgePoint": Vector3 { + "x": 563.1594202199647, + "y": 4, + "z": 5.564389211988868, + }, + "isLeaf": false, + "length": 836.3925465119636, + "link": undefined, + "markingColor": null, + "name": "folder_5", + "outgoingEdgePoint": Vector3 { + "x": 563.1594202199647, + "y": 4, + "z": 423.76066246797063, + }, + "path": "/root/folder_5", + "visible": true, + "width": 139.3987577519939, + "x0": 743.4600413439676, + "y0": 46.46625258399798, + "z0": 2, + }, + Object { + "attributes": Object { + "custom_metric": 10, + }, + "color": "#666666", + "deltas": undefined, + "depth": 2, + "edgeAttributes": undefined, + "flat": false, + "height": 500, + "heightDelta": 0, + "id": undefined, + "incomingEdgePoint": Vector3 { + "x": 500.84227394129766, + "y": 504, + "z": 36.8952237531623, + }, + "isLeaf": true, + "length": 711.06920834727, + "link": undefined, + "markingColor": null, + "name": "children_10", + "outgoingEdgePoint": Vector3 { + "x": 500.84227394129766, + "y": 504, + "z": 392.4298279267973, + }, + "path": "/root/folder_5/children_10", + "visible": true, + "width": 0, + "x0": 750.8422739412977, + "y0": 109.1279216663448, + "z0": 4, + }, + Object { + "attributes": Object { + "custom_metric": 0, + }, + "color": "#666666", + "deltas": undefined, + "depth": 2, + "edgeAttributes": undefined, + "flat": false, + "height": 500, + "heightDelta": 0, + "id": undefined, + "incomingEdgePoint": Vector3 { + "x": 500.84227394129766, + "y": 504, + "z": 36.8952237531623, + }, + "isLeaf": true, + "length": 711.06920834727, + "link": undefined, + "markingColor": null, + "name": "children_11", + "outgoingEdgePoint": Vector3 { + "x": 500.84227394129766, + "y": 504, + "z": 392.4298279267973, + }, + "path": "/root/folder_5/children_11", + "visible": true, + "width": 0, + "x0": 750.8422739412977, + "y0": 109.1279216663448, + "z0": 4, + }, + Object { + "attributes": Object {}, + "color": "#666666", + "deltas": undefined, + "depth": 1, + "edgeAttributes": undefined, + "flat": false, + "height": 2, + "heightDelta": 0, + "id": undefined, + "incomingEdgePoint": Vector3 { + "x": -75.75155281000758, + "y": 4, + "z": 563.1594202199647, + }, + "isLeaf": false, + "length": 139.3987577519939, + "link": undefined, + "markingColor": null, + "name": "folder_6", + "outgoingEdgePoint": Vector3 { + "x": 179.81283640198131, + "y": 4, + "z": 563.1594202199647, + }, + "path": "/root/folder_6", + "visible": true, + "width": 511.1287784239778, + "x0": 46.46625258399798, + "y0": 743.4600413439676, + "z0": 2, + }, + Object { + "attributes": Object { + "custom_metric": 1, + }, + "color": "#666666", + "deltas": undefined, + "depth": 2, + "edgeAttributes": undefined, + "flat": false, + "height": 500, + "heightDelta": 0, + "id": undefined, + "incomingEdgePoint": Vector3 { + "x": -53.576455584677035, + "y": 504, + "z": 500.84227394129766, + }, + "isLeaf": true, + "length": 0, + "link": undefined, + "markingColor": null, + "name": "children_12", + "outgoingEdgePoint": Vector3 { + "x": 157.63773917665077, + "y": 504, + "z": 500.84227394129766, + }, + "path": "/root/folder_6/children_12", + "visible": true, + "width": 422.4283895226556, + "x0": 90.81644703465908, + "y0": 750.8422739412977, + "z0": 4, + }, + Object { + "attributes": Object { + "custom_metric": 1, + }, + "color": "#666666", + "deltas": undefined, + "depth": 2, + "edgeAttributes": undefined, + "flat": false, + "height": 500, + "heightDelta": 0, + "id": undefined, + "incomingEdgePoint": Vector3 { + "x": -53.576455584677035, + "y": 504, + "z": 500.84227394129766, + }, + "isLeaf": true, + "length": 0, + "link": undefined, + "markingColor": null, + "name": "children_13", + "outgoingEdgePoint": Vector3 { + "x": 157.63773917665077, + "y": 504, + "z": 500.84227394129766, + }, + "path": "/root/folder_6/children_13", + "visible": true, + "width": 422.4283895226556, + "x0": 90.81644703465908, + "y0": 750.8422739412977, + "z0": 4, + }, +] +`; diff --git a/visualization/app/codeCharta/util/__snapshots__/treeMapHelper.spec.ts.snap b/visualization/app/codeCharta/util/__snapshots__/treeMapHelper.spec.ts.snap index 3bf2036cc6..ef7ca70009 100644 --- a/visualization/app/codeCharta/util/__snapshots__/treeMapHelper.spec.ts.snap +++ b/visualization/app/codeCharta/util/__snapshots__/treeMapHelper.spec.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`treeMapHelper build node deltas 1`] = ` +exports[`TreeMapHelper build node deltas 1`] = ` Object { "attributes": Object { "theHeight": 100, @@ -39,7 +39,7 @@ Object { } `; -exports[`treeMapHelper build node invertHeight 1`] = ` +exports[`TreeMapHelper build node invertHeight 1`] = ` Object { "attributes": Object { "theHeight": 100, @@ -76,7 +76,7 @@ Object { } `; -exports[`treeMapHelper build node minimal 1`] = ` +exports[`TreeMapHelper build node minimal 1`] = ` Object { "attributes": Object { "theHeight": 100, @@ -113,7 +113,7 @@ Object { } `; -exports[`treeMapHelper build node should set lowest possible height caused by other visible edge pairs 1`] = ` +exports[`TreeMapHelper build node should set lowest possible height caused by other visible edge pairs 1`] = ` Object { "attributes": Object { "theHeight": 100, diff --git a/visualization/app/codeCharta/util/aggregationGenerator.spec.ts b/visualization/app/codeCharta/util/aggregationGenerator.spec.ts index ff6e1e48c5..b99c033057 100644 --- a/visualization/app/codeCharta/util/aggregationGenerator.spec.ts +++ b/visualization/app/codeCharta/util/aggregationGenerator.spec.ts @@ -1,12 +1,13 @@ import { CCFile, NodeType, Settings } from "../codeCharta.model" import { AggregationGenerator } from "./aggregationGenerator" +import packageJson from "../../../package.json" describe("AggregationGenerator", () => { const file1: CCFile = { fileMeta: { fileName: "file1", projectName: "Sample Project", - apiVersion: "1.1" + apiVersion: packageJson.codecharta.apiVersion }, map: { name: "root", @@ -44,7 +45,7 @@ describe("AggregationGenerator", () => { fileMeta: { fileName: "file2", projectName: "Sample Project", - apiVersion: "1.1" + apiVersion: packageJson.codecharta.apiVersion }, map: { name: "root", diff --git a/visualization/app/codeCharta/util/codeMapHelper.spec.ts b/visualization/app/codeCharta/util/codeMapHelper.spec.ts index f393ef3b41..1018f010fa 100644 --- a/visualization/app/codeCharta/util/codeMapHelper.spec.ts +++ b/visualization/app/codeCharta/util/codeMapHelper.spec.ts @@ -60,29 +60,10 @@ describe("codeMapHelper", () => { }) describe("getAnyCodeMapNodeFromPath", () => { - it("should call getCodeMapNodeFromPath with type File at the beginning of call", () => { - CodeMapHelper.getCodeMapNodeFromPath = jest.fn() + it("should return the node that matches the path exactly", () => { + const result = CodeMapHelper.getAnyCodeMapNodeFromPath("/root/big leaf", testRoot) - CodeMapHelper.getAnyCodeMapNodeFromPath("/root", testRoot) - - expect(CodeMapHelper.getCodeMapNodeFromPath).toHaveBeenCalledWith("/root", NodeType.FILE, testRoot) - }) - - it("should call getCodeMapNodeFromPath with type Folder when no file was found and return null", () => { - CodeMapHelper.getCodeMapNodeFromPath = jest.fn().mockReturnValue(null) - - const result = CodeMapHelper.getAnyCodeMapNodeFromPath("/root", testRoot) - - expect(CodeMapHelper.getCodeMapNodeFromPath).toHaveBeenCalledWith("/root", NodeType.FOLDER, testRoot) - expect(result).toBeNull() - }) - - it("should return the first file found by getCodeMapNodeFromPath", () => { - CodeMapHelper.getCodeMapNodeFromPath = jest.fn().mockReturnValue(testRoot) - - const result = CodeMapHelper.getAnyCodeMapNodeFromPath("/root", testRoot) - - expect(result).toEqual(testRoot) + expect(result).toEqual(testRoot.children[0]) }) }) diff --git a/visualization/app/codeCharta/util/codeMapHelper.ts b/visualization/app/codeCharta/util/codeMapHelper.ts index ecb6172019..f65305f14a 100644 --- a/visualization/app/codeCharta/util/codeMapHelper.ts +++ b/visualization/app/codeCharta/util/codeMapHelper.ts @@ -1,84 +1,94 @@ import { hierarchy } from "d3-hierarchy" -import { BlacklistItem, BlacklistType, CodeMapNode, MarkedPackage, NodeType } from "../codeCharta.model" +import { BlacklistItem, BlacklistType, CodeMapNode, MarkedPackage } from "../codeCharta.model" import ignore from "ignore" -export class CodeMapHelper { - public static getAnyCodeMapNodeFromPath(path: string, root: CodeMapNode): CodeMapNode { - const firstTryNode = this.getCodeMapNodeFromPath(path, NodeType.FILE, root) - if (!firstTryNode) { - return this.getCodeMapNodeFromPath(path, NodeType.FOLDER, root) - } - return firstTryNode - } - - public static getCodeMapNodeFromPath(path: string, nodeType: string, root: CodeMapNode): CodeMapNode { - const matchingNode = hierarchy(root) - .descendants() - .find(node => node.data.path === path && node.data.type === nodeType) - return matchingNode ? matchingNode.data : null - } +function getAnyCodeMapNodeFromPath(path: string, root: CodeMapNode): CodeMapNode { + const matchingNode = hierarchy(root) + .descendants() + .find(node => node.data.path === path) + return matchingNode ? matchingNode.data : null +} - public static getAllPaths(node: CodeMapNode): Array { - return hierarchy(node) - .descendants() - .map(node => node.data.path) - } +function getCodeMapNodeFromPath(path: string, nodeType: string, root: CodeMapNode): CodeMapNode { + const matchingNode = hierarchy(root) + .descendants() + .find(node => node.data.path === path && node.data.type === nodeType) + return matchingNode ? matchingNode.data : null +} - public static transformPath(toTransform: string): string { - let removeNumberOfCharactersFromStart = 0 +function getAllPaths(node: CodeMapNode): Array { + return hierarchy(node) + .descendants() + .map(node => node.data.path) +} - if (toTransform.startsWith("./")) { - removeNumberOfCharactersFromStart = 2 - } else if (toTransform.startsWith("/")) { - removeNumberOfCharactersFromStart = 1 - } - return toTransform.substring(removeNumberOfCharactersFromStart) - } +function transformPath(toTransform: string): string { + let removeNumberOfCharactersFromStart = 0 - public static getNodesByGitignorePath(nodes: Array, gitignorePath: string): CodeMapNode[] { - const ignoredNodePaths = ignore() - .add(CodeMapHelper.transformPath(gitignorePath)) - .filter(nodes.map(n => CodeMapHelper.transformPath(n.path))) - //TODO: Review again once we use a isBlacklisted attribute in our CodeMapNodes - const set = new Set(ignoredNodePaths) - return nodes.filter(n => !set.has(CodeMapHelper.transformPath(n.path))) + if (toTransform.startsWith("./")) { + removeNumberOfCharactersFromStart = 2 + } else if (toTransform.startsWith("/")) { + removeNumberOfCharactersFromStart = 1 } + return toTransform.substring(removeNumberOfCharactersFromStart) +} - public static numberOfBlacklistedNodes(nodes: Array): number { - return nodes.filter(node => node.isExcluded || node.isFlattened).length - } +function getNodesByGitignorePath(nodes: Array, gitignorePath: string): CodeMapNode[] { + const ignoredNodePaths = ignore() + .add(transformPath(gitignorePath)) + .filter(nodes.map(n => transformPath(n.path))) + //TODO: Review again once we use a isBlacklisted attribute in our CodeMapNodes + const set = new Set(ignoredNodePaths) + return nodes.filter(n => !set.has(transformPath(n.path))) +} - public static isPathHiddenOrExcluded(path: string, blacklist: Array): boolean { - return ( - CodeMapHelper.isPathBlacklisted(path, blacklist, BlacklistType.exclude) || - CodeMapHelper.isPathBlacklisted(path, blacklist, BlacklistType.flatten) - ) - } +function numberOfBlacklistedNodes(nodes: Array): number { + return nodes.filter(node => isBlacklisted(node)).length +} - public static isPathBlacklisted(path: string, blacklist: Array, type: BlacklistType): boolean { - if (blacklist.length === 0) { - return false - } +function isPathHiddenOrExcluded(path: string, blacklist: Array): boolean { + return isPathBlacklisted(path, blacklist, BlacklistType.exclude) || isPathBlacklisted(path, blacklist, BlacklistType.flatten) +} - const ig = ignore().add(blacklist.filter(b => b.type === type).map(ex => CodeMapHelper.transformPath(ex.path))) - return ig.ignores(CodeMapHelper.transformPath(path)) +function isPathBlacklisted(path: string, blacklist: Array, type: BlacklistType): boolean { + if (blacklist.length === 0) { + return false } - public static getMarkingColor(node: CodeMapNode, markedPackages: MarkedPackage[]): string { - let markingColor: string = null + const ig = ignore().add(blacklist.filter(b => b.type === type).map(ex => transformPath(ex.path))) + return ig.ignores(transformPath(path)) +} + +function getMarkingColor(node: CodeMapNode, markedPackages: MarkedPackage[]): string { + let markingColor: string = null - if (markedPackages) { - const markedParentPackages = markedPackages.filter(mp => node.path.includes(mp.path)) + if (markedPackages) { + const markedParentPackages = markedPackages.filter(mp => node.path.includes(mp.path)) - if (markedParentPackages.length > 0) { - markedParentPackages.sort((a, b) => this.sortByPathLength(a, b)) - markingColor = markedParentPackages[0].color - } + if (markedParentPackages.length > 0) { + markedParentPackages.sort((a, b) => sortByPathLength(a, b)) + markingColor = markedParentPackages[0].color } - return markingColor } + return markingColor +} - private static sortByPathLength(a: MarkedPackage, b: MarkedPackage) { - return b.path.length - a.path.length - } +function sortByPathLength(a: MarkedPackage, b: MarkedPackage) { + return b.path.length - a.path.length +} + +function isBlacklisted(node: CodeMapNode): boolean { + return node.isExcluded || node.isFlattened +} + +export const CodeMapHelper = { + getAnyCodeMapNodeFromPath, + getNodesByGitignorePath, + getAllPaths, + transformPath, + getCodeMapNodeFromPath, + numberOfBlacklistedNodes, + isPathHiddenOrExcluded, + isPathBlacklisted, + getMarkingColor } diff --git a/visualization/app/codeCharta/util/dataMocks.ts b/visualization/app/codeCharta/util/dataMocks.ts index 21d4a4a0e6..512fa70e8c 100644 --- a/visualization/app/codeCharta/util/dataMocks.ts +++ b/visualization/app/codeCharta/util/dataMocks.ts @@ -30,6 +30,7 @@ import { ScenarioItem } from "../ui/scenarioDropDown/scenarioDropDown.component" import { FileSelectionState, FileState } from "../model/files/files" import { APIVersions, ExportCCFile } from "../codeCharta.api.model" import { NodeMetricDataService } from "../state/store/metricData/nodeMetricData/nodeMetricData.service" +import packageJson from "../../../package.json" export const VALID_NODE: CodeMapNode = { name: "root", @@ -523,7 +524,7 @@ export const VALID_EDGE: Edge = { export const TEST_FILE_CONTENT: ExportCCFile = { projectName: "Sample Map", - apiVersion: APIVersions.ONE_POINT_ONE, + apiVersion: APIVersions.ONE_POINT_TWO, nodes: [VALID_NODE] } @@ -537,7 +538,7 @@ export const TEST_FILE_CONTENT_INVALID_MAJOR_API = { export const TEST_FILE_CONTENT_INVALID_MINOR_API = { fileName: "noFileName", projectName: "Valid Sample Map Minor API High", - apiVersion: "1.2", + apiVersion: "1.3", nodes: [VALID_NODE] } @@ -557,7 +558,7 @@ export const TEST_FILE_CONTENT_NO_API = { export const FILE_META: FileMeta = { fileName: "fileA", projectName: "Sample Project", - apiVersion: "1.1" + apiVersion: packageJson.codecharta.apiVersion } export const TEST_FILE_DATA: CCFile = { @@ -878,7 +879,7 @@ export const TEST_DELTA_MAP_A: CCFile = { fileMeta: { fileName: "fileA", projectName: "Sample Project", - apiVersion: "1.1" + apiVersion: packageJson.codecharta.apiVersion }, map: { name: "root", @@ -934,7 +935,7 @@ export const TEST_DELTA_MAP_B: CCFile = { fileMeta: { fileName: "fileB", projectName: "Sample Project", - apiVersion: "1.1" + apiVersion: packageJson.codecharta.apiVersion }, map: { name: "root", @@ -1003,7 +1004,7 @@ export const TEST_DELTA_MAP_B: CCFile = { } export const TEST_FILE_DATA_DOWNLOADED = { - apiVersion: "1.1", + apiVersion: packageJson.codecharta.apiVersion, attributeTypes: {}, blacklist: [ { path: "/root/bigLeaf.ts", type: "hide" }, diff --git a/visualization/app/codeCharta/util/fileDownloader.ts b/visualization/app/codeCharta/util/fileDownloader.ts index 62f3d93a7b..d15cb3bcf9 100644 --- a/visualization/app/codeCharta/util/fileDownloader.ts +++ b/visualization/app/codeCharta/util/fileDownloader.ts @@ -57,9 +57,8 @@ export class FileDownloader { private static getAttributeTypesForJSON(attributeTypes: AttributeTypes): AttributeTypes | {} { if (Object.keys(attributeTypes.edges).length === 0 && Object.keys(attributeTypes.nodes).length === 0) { return {} - } else { - return attributeTypes } + return attributeTypes } private static getFilteredBlacklist(blacklist: BlacklistItem[], type: BlacklistType): BlacklistItem[] { diff --git a/visualization/app/codeCharta/util/fileExtensionCalculator.ts b/visualization/app/codeCharta/util/fileExtensionCalculator.ts index 1760368a41..2defb3d84b 100644 --- a/visualization/app/codeCharta/util/fileExtensionCalculator.ts +++ b/visualization/app/codeCharta/util/fileExtensionCalculator.ts @@ -102,9 +102,8 @@ export class FileExtensionCalculator { public static estimateFileExtension(fileName: string): string { if (fileName.includes(".")) { return fileName.split(".").reverse()[0].toLowerCase() - } else { - return FileExtensionCalculator.NO_EXTENSION } + return FileExtensionCalculator.NO_EXTENSION } public static hashCode(fileExtension: string): number { diff --git a/visualization/app/codeCharta/util/fileHelper.spec.ts b/visualization/app/codeCharta/util/fileHelper.spec.ts new file mode 100644 index 0000000000..1163b39d63 --- /dev/null +++ b/visualization/app/codeCharta/util/fileHelper.spec.ts @@ -0,0 +1,56 @@ +import { ExportBlacklistType, ExportCCFile } from "../codeCharta.api.model" +import { AttributeTypeValue, BlacklistType } from "../codeCharta.model" +import { getCCFile } from "./fileHelper" +import { TEST_FILE_CONTENT } from "./dataMocks" +import { clone } from "./clone" + +describe("FileHelper", () => { + let fileContent: ExportCCFile + + beforeEach(() => { + fileContent = clone(TEST_FILE_CONTENT) + }) + + describe("getCCFile", () => { + it("should build a CCFile", () => { + const result = getCCFile("fileName", fileContent) + + expect(result).toMatchSnapshot() + }) + + it("should convert old blacklist type", () => { + fileContent.blacklist = [{ path: "foo", type: ExportBlacklistType.hide }] + + const result = getCCFile("fileName", fileContent) + + expect(result.settings.fileSettings.blacklist).toEqual([{ path: "foo", type: BlacklistType.flatten }]) + }) + + it("should ignore old attribute types", () => { + fileContent.attributeTypes = { + nodes: [{ mcc: AttributeTypeValue.absolute }], + edges: [{ pairingRate: AttributeTypeValue.relative }] + } + + const result = getCCFile("fileName", fileContent) + + expect(result.settings.fileSettings.attributeTypes).toEqual({ nodes: {}, edges: {} }) + }) + + it("should return empty attributeTypes", () => { + fileContent.attributeTypes = {} + + const result = getCCFile("fileName", fileContent) + + expect(result.settings.fileSettings.attributeTypes).toEqual({ nodes: {}, edges: {} }) + }) + + it("should return empty attributeTypes if the property doesn't exist", () => { + fileContent.attributeTypes = undefined + + const result = getCCFile("fileName", fileContent) + + expect(result.settings.fileSettings.attributeTypes).toEqual({ nodes: {}, edges: {} }) + }) + }) +}) diff --git a/visualization/app/codeCharta/util/fileHelper.ts b/visualization/app/codeCharta/util/fileHelper.ts new file mode 100644 index 0000000000..23139306fb --- /dev/null +++ b/visualization/app/codeCharta/util/fileHelper.ts @@ -0,0 +1,44 @@ +import { ExportBlacklistType, ExportCCFile, OldAttributeTypes } from "../codeCharta.api.model" +import { AttributeTypes, BlacklistItem, BlacklistType, CCFile } from "../codeCharta.model" + +export function getCCFile(fileName: string, fileContent: ExportCCFile): CCFile { + return { + fileMeta: { + fileName, + projectName: fileContent.projectName, + apiVersion: fileContent.apiVersion + }, + settings: { + fileSettings: { + edges: fileContent.edges || [], + attributeTypes: getAttributeTypes(fileContent.attributeTypes), + blacklist: potentiallyUpdateBlacklistTypes(fileContent.blacklist || []), + markedPackages: fileContent.markedPackages || [] + } + }, + map: fileContent.nodes[0] + } +} + +function getAttributeTypes(attributeTypes: AttributeTypes | OldAttributeTypes): AttributeTypes { + if (!attributeTypes || Array.isArray(attributeTypes.nodes) || Array.isArray(attributeTypes.edges)) { + return { + nodes: {}, + edges: {} + } + } + + return { + nodes: attributeTypes.nodes ?? {}, + edges: attributeTypes.edges ?? {} + } +} + +function potentiallyUpdateBlacklistTypes(blacklist): BlacklistItem[] { + blacklist.forEach(x => { + if (x.type === ExportBlacklistType.hide) { + x.type = BlacklistType.flatten + } + }) + return blacklist +} diff --git a/visualization/app/codeCharta/util/fileValidator.spec.ts b/visualization/app/codeCharta/util/fileValidator.spec.ts index 1f739d4e95..2b4932e3e4 100755 --- a/visualization/app/codeCharta/util/fileValidator.spec.ts +++ b/visualization/app/codeCharta/util/fileValidator.spec.ts @@ -5,13 +5,16 @@ import { TEST_FILE_CONTENT_INVALID_MINOR_API, TEST_FILE_CONTENT_NO_API } from "./dataMocks" -import { NodeType } from "../codeCharta.model" +import { CodeMapNode, NodeType } from "../codeCharta.model" import packageJson from "../../../package.json" import { CCValidationResult, ERROR_MESSAGES, validate } from "./fileValidator" import assert from "assert" +import { fileWithFixedFolders } from "../ressources/fixed-folders/fixed-folders-example" +import { APIVersions, ExportCCFile } from "../codeCharta.api.model" +import { clone } from "./clone" describe("FileValidator", () => { - let file + let file: ExportCCFile let invalidFile beforeEach(() => { @@ -19,13 +22,12 @@ describe("FileValidator", () => { }) it("API version exists in package.json", () => { - expect(packageJson.codecharta.apiVersion).toEqual("1.1") + expect(packageJson.codecharta.apiVersion).toEqual("1.2") }) it("should throw on null", () => { const expectedError: CCValidationResult = { - title: ERROR_MESSAGES.fileIsInvalid.title, - error: [ERROR_MESSAGES.fileIsInvalid.message], + error: [ERROR_MESSAGES.fileIsInvalid], warning: [] } @@ -38,8 +40,7 @@ describe("FileValidator", () => { invalidFile = TEST_FILE_CONTENT_INVALID_MAJOR_API const expectedError: CCValidationResult = { - title: ERROR_MESSAGES.majorApiVersionIsOutdated.title, - error: [ERROR_MESSAGES.majorApiVersionIsOutdated.message], + error: [ERROR_MESSAGES.majorApiVersionIsOutdated], warning: [] } @@ -52,9 +53,8 @@ describe("FileValidator", () => { file = TEST_FILE_CONTENT_INVALID_MINOR_API const expectedError: CCValidationResult = { - title: ERROR_MESSAGES.minorApiVersionOutdated.title, error: [], - warning: [ERROR_MESSAGES.minorApiVersionOutdated.message] + warning: [`${ERROR_MESSAGES.minorApiVersionOutdated} Found: ${file.apiVersion}`] } assert.throws(() => { @@ -66,8 +66,7 @@ describe("FileValidator", () => { invalidFile = TEST_FILE_CONTENT_NO_API const expectedError: CCValidationResult = { - title: ERROR_MESSAGES.apiVersionIsInvalid.title, - error: [ERROR_MESSAGES.apiVersionIsInvalid.message], + error: [ERROR_MESSAGES.apiVersionIsInvalid], warning: [] } @@ -80,8 +79,7 @@ describe("FileValidator", () => { invalidFile = TEST_FILE_CONTENT_INVALID_API const expectedError: CCValidationResult = { - title: ERROR_MESSAGES.apiVersionIsInvalid.title, - error: [ERROR_MESSAGES.apiVersionIsInvalid.message], + error: [ERROR_MESSAGES.apiVersionIsInvalid], warning: [] } @@ -92,8 +90,7 @@ describe("FileValidator", () => { it("should throw on string", () => { const expectedError: CCValidationResult = { - title: ERROR_MESSAGES.fileIsInvalid.title, - error: [ERROR_MESSAGES.fileIsInvalid.message], + error: [ERROR_MESSAGES.fileIsInvalid], warning: [] } @@ -136,8 +133,7 @@ describe("FileValidator", () => { file.nodes[0].children[1].type = NodeType.FILE const expectedError: CCValidationResult = { - title: ERROR_MESSAGES.nodesNotUnique.title, - error: [ERROR_MESSAGES.nodesNotUnique.message], + error: [`${ERROR_MESSAGES.nodesNotUnique} Found duplicate of File with name: same`], warning: [] } @@ -147,11 +143,10 @@ describe("FileValidator", () => { }) it("should throw when nodes are empty", () => { - file.nodes[0] = [] + file.nodes = [] const expectedError: CCValidationResult = { - title: ERROR_MESSAGES.validationError.title, - error: ["Type error: nodes[0] should be object"], + error: [ERROR_MESSAGES.nodesEmpty], warning: [] } @@ -163,10 +158,9 @@ describe("FileValidator", () => { it("should throw if nodes is not a node and therefore has no name or id", () => { file.nodes[0] = { something: "something" - } + } as any const expectedError: CCValidationResult = { - title: ERROR_MESSAGES.validationError.title, error: [ "Required error: nodes[0] should have required property 'name'", "Required error: nodes[0] should have required property 'type'" @@ -185,7 +179,6 @@ describe("FileValidator", () => { } const expectedError: CCValidationResult = { - title: ERROR_MESSAGES.validationError.title, error: [ "Required error: nodes[0] should have required property 'name'", "Required error: nodes[0] should have required property 'type'" @@ -204,7 +197,6 @@ describe("FileValidator", () => { } const expectedError: CCValidationResult = { - title: ERROR_MESSAGES.validationError.title, error: [ "Required error: nodes[0] should have required property 'name'", "Required error: nodes[0] should have required property 'type'" @@ -216,4 +208,203 @@ describe("FileValidator", () => { validate(file) }, expectedError) }) + + describe("fixed folders validation", () => { + let folder1: CodeMapNode + let folder2: CodeMapNode + + beforeEach(() => { + file = clone(fileWithFixedFolders) + folder1 = file.nodes[0].children[0] + folder2 = file.nodes[0].children[1] + }) + + it("should throw an error, if there are fixed folders, but not every folder on root is fixed", () => { + folder1.fixedPosition = undefined + + const expectedError: CCValidationResult = { + error: [ERROR_MESSAGES.notAllFoldersAreFixed + " Found: folder_1"], + warning: [] + } + + assert.throws(() => { + validate(file) + }, expectedError) + }) + + it("should throw an error, if at least one fixed folder has a padding that is out of bounds", () => { + folder1.fixedPosition.left = -5 + folder1.fixedPosition.width = 7 + + const expectedError: CCValidationResult = { + error: [`${ERROR_MESSAGES.fixedFoldersOutOfBounds} Found: folder_1 ${JSON.stringify(folder1.fixedPosition)}`], + warning: [] + } + + assert.throws(() => { + validate(file) + }, expectedError) + }) + + it("should throw an error, if at least one fixed folder has a width or height that is out of bounds", () => { + folder1.fixedPosition.left = 10 + folder1.fixedPosition.width = -50 + + const expectedError: CCValidationResult = { + error: [`${ERROR_MESSAGES.fixedFoldersOutOfBounds} Found: folder_1 ${JSON.stringify(folder1.fixedPosition)}`], + warning: [] + } + + assert.throws(() => { + validate(file) + }, expectedError) + }) + + it("should throw an error, if at least one fixed folder exceeds the maximum coordinate of 100", () => { + folder1.fixedPosition.left = 99 + folder1.fixedPosition.width = 2 + + const expectedError: CCValidationResult = { + error: [`${ERROR_MESSAGES.fixedFoldersOutOfBounds} Found: folder_1 ${JSON.stringify(folder1.fixedPosition)}`], + warning: [] + } + + assert.throws(() => { + validate(file) + }, expectedError) + }) + + it("should throw an error, if two folders horizontally overlap", () => { + folder1.fixedPosition = { + left: 0, + top: 0, + width: 10, + height: 10 + } + folder2.fixedPosition = { + left: 5, + top: 1, + width: 10, + height: 10 + } + + const expectedError: CCValidationResult = { + error: [ + `${ERROR_MESSAGES.fixedFoldersOverlapped} Found: folder_1 ${JSON.stringify( + folder1.fixedPosition + )} and folder_2 ${JSON.stringify(folder2.fixedPosition)}` + ], + warning: [] + } + + assert.throws(() => { + validate(file) + }, expectedError) + }) + + it("should throw an error, if two folders vertically overlap", () => { + folder1.fixedPosition = { + left: 0, + top: 0, + width: 10, + height: 10 + } + folder2.fixedPosition = { + left: 0, + top: 5, + width: 10, + height: 10 + } + + const expectedError: CCValidationResult = { + error: [ + `${ERROR_MESSAGES.fixedFoldersOverlapped} Found: folder_1 ${JSON.stringify( + folder1.fixedPosition + )} and folder_2 ${JSON.stringify(folder2.fixedPosition)}` + ], + warning: [] + } + + assert.throws(() => { + validate(file) + }, expectedError) + }) + + it("should throw an error, if a folder is placed inside another", () => { + folder1.fixedPosition = { + left: 0, + top: 0, + width: 10, + height: 10 + } + folder2.fixedPosition = { + left: 1, + top: 1, + width: 1, + height: 1 + } + + const expectedError: CCValidationResult = { + error: [ + `${ERROR_MESSAGES.fixedFoldersOverlapped} Found: folder_2 ${JSON.stringify( + folder2.fixedPosition + )} and folder_1 ${JSON.stringify(folder1.fixedPosition)}` + ], + warning: [] + } + + assert.throws(() => { + validate(file) + }, expectedError) + }) + + it("should throw an error, if a folder has the same boundaries as another", () => { + folder1.fixedPosition = { + left: 0, + top: 0, + width: 10, + height: 10 + } + folder2.fixedPosition = folder1.fixedPosition + + const expectedError: CCValidationResult = { + error: [ + `${ERROR_MESSAGES.fixedFoldersOverlapped} Found: folder_1 ${JSON.stringify( + folder1.fixedPosition + )} and folder_2 ${JSON.stringify(folder2.fixedPosition)}` + ], + warning: [] + } + + assert.throws(() => { + validate(file) + }, expectedError) + }) + + it("should throw an error, if the major api version is smaller and fixed folders were defined", () => { + file.apiVersion = APIVersions.ZERO_POINT_ONE + + const expectedError: CCValidationResult = { + error: [`${ERROR_MESSAGES.fixedFoldersNotAllowed} Found: 0.1`], + warning: [] + } + + assert.throws(() => { + validate(file) + }, expectedError) + }) + + it("should throw an error, if the minor api version is smaller and fixed folders were defined", () => { + file.apiVersion = APIVersions.ONE_POINT_ONE + + const expectedError: CCValidationResult = { + error: [`${ERROR_MESSAGES.fixedFoldersNotAllowed} Found: 1.1`], + warning: [] + } + + assert.throws(() => { + validate(file) + }, expectedError) + }) + }) }) diff --git a/visualization/app/codeCharta/util/fileValidator.ts b/visualization/app/codeCharta/util/fileValidator.ts index 006d5d538e..74414f1d55 100644 --- a/visualization/app/codeCharta/util/fileValidator.ts +++ b/visualization/app/codeCharta/util/fileValidator.ts @@ -1,8 +1,7 @@ -import { CodeMapNode } from "../codeCharta.model" +import { CodeMapNode, FixedPosition } from "../codeCharta.model" import Ajv from "ajv" import packageJson from "../../../package.json" import { ExportCCFile } from "../codeCharta.api.model" -import _ from "lodash" import jsonSchema from "./generatedSchema.json" const latestApiVersion = packageJson.codecharta.apiVersion @@ -15,55 +14,36 @@ interface ApiVersion { export interface CCValidationResult { error: string[] warning: string[] - title: string } export const ERROR_MESSAGES = { - fileIsInvalid: { - title: "Error Loading File", - message: "file is empty or invalid" - }, - apiVersionIsInvalid: { - title: "Error API Version", - message: "API Version is empty or invalid" - }, - majorApiVersionIsOutdated: { - title: "Error Major API Version", - message: "API Version Outdated: Update CodeCharta API Version to match cc.json" - }, - minorApiVersionOutdated: { - title: "Warning Minor API Version", - message: "Minor API Version Outdated" - }, - nodesNotUnique: { - title: "Error Node Uniques", - message: "node names in combination with node types are not unique" - }, - validationError: { - title: "Error Validation" - } + fileIsInvalid: "File is empty or invalid.", + apiVersionIsInvalid: "API Version is empty or invalid.", + majorApiVersionIsOutdated: "API Version Outdated: Update CodeCharta API Version to match cc.json.", + minorApiVersionOutdated: "Minor API Version Outdated.", + nodesNotUnique: "Node names in combination with node types are not unique.", + nodesEmpty: "The nodes array is empty. At least one node is required.", + notAllFoldersAreFixed: "If at least one direct sub-folder of root is marked as fixed, all direct sub-folders of root must be fixed.", + fixedFoldersOutOfBounds: "Coordinates of fixed folders must be within a range of 0 and 100.", + fixedFoldersOverlapped: "Folders may not overlap.", + fixedFoldersNotAllowed: "Fixated folders may not be defined in API-Version < 1.2." } export function validate(file: ExportCCFile) { - const result: CCValidationResult = { error: [], warning: [], title: "" } + const result: CCValidationResult = { error: [], warning: [] } switch (true) { case !file: - result.error.push(ERROR_MESSAGES.fileIsInvalid.message) - result.title = ERROR_MESSAGES.fileIsInvalid.title + result.error.push(ERROR_MESSAGES.fileIsInvalid) break case !isValidApiVersion(file): - result.error.push(ERROR_MESSAGES.apiVersionIsInvalid.message) - result.title = ERROR_MESSAGES.apiVersionIsInvalid.title + result.error.push(ERROR_MESSAGES.apiVersionIsInvalid) break case fileHasHigherMajorVersion(file): - result.error.push(ERROR_MESSAGES.majorApiVersionIsOutdated.message) - result.title = ERROR_MESSAGES.majorApiVersionIsOutdated.title + result.error.push(ERROR_MESSAGES.majorApiVersionIsOutdated) break case fileHasHigherMinorVersion(file): - result.warning.push(ERROR_MESSAGES.minorApiVersionOutdated.message) - result.title = ERROR_MESSAGES.minorApiVersionOutdated.title - break + result.warning.push(`${ERROR_MESSAGES.minorApiVersionOutdated} Found: ${file.apiVersion}`) } if (result.error.length === 0) { @@ -73,44 +53,19 @@ export function validate(file: ExportCCFile) { if (!valid) { result.error = validate.errors.map((error: Ajv.ErrorObject) => getValidationMessage(error)) - result.title = ERROR_MESSAGES.validationError.title - } else if (!hasUniqueChildren(file.nodes[0])) { - result.error.push(ERROR_MESSAGES.nodesNotUnique.message) - result.title = ERROR_MESSAGES.nodesNotUnique.title + } else if (file.nodes.length === 0) { + result.error.push(ERROR_MESSAGES.nodesEmpty) + } else { + validateAllNodesAreUnique(file.nodes[0], result) + validateFixedFolders(file, result) } } - if (!_.isEmpty(result.error) || !_.isEmpty(result.warning)) { + if (result.error.length > 0 || result.warning.length > 0) { throw result } } -function getValidationMessage(error: Ajv.ErrorObject) { - const errorType = error.keyword.charAt(0).toUpperCase() + error.keyword.slice(1) - const errorParameter = error.dataPath.slice(1) - return errorType + " error: " + errorParameter + " " + error.message -} - -function hasUniqueChildren(node: CodeMapNode): boolean { - if (!node.children || node.children.length === 0) { - return true - } - - const names = {} - node.children.forEach(child => (names[child.name + child.type] = true)) - - if (Object.keys(names).length !== node.children.length) { - return false - } - - for (const child of node.children) { - if (!hasUniqueChildren(child)) { - return false - } - } - return true -} - function isValidApiVersion(file: ExportCCFile): boolean { const apiVersion = file.apiVersion const hasApiVersion = apiVersion !== undefined @@ -119,12 +74,12 @@ function isValidApiVersion(file: ExportCCFile): boolean { return hasApiVersion && isValidVersion } -function fileHasHigherMajorVersion(file: { apiVersion: string; nodes: CodeMapNode[] }): boolean { +function fileHasHigherMajorVersion(file: ExportCCFile): boolean { const apiVersion = getAsApiVersion(file.apiVersion) return apiVersion.major > getAsApiVersion(latestApiVersion).major } -function fileHasHigherMinorVersion(file: { apiVersion: string; nodes: CodeMapNode[] }): boolean { +function fileHasHigherMinorVersion(file: ExportCCFile): boolean { const apiVersion = getAsApiVersion(file.apiVersion) return apiVersion.minor > getAsApiVersion(latestApiVersion).minor } @@ -135,3 +90,96 @@ function getAsApiVersion(version: string): ApiVersion { minor: Number(version.split(".")[1]) } } + +function getValidationMessage(error: Ajv.ErrorObject) { + const errorType = error.keyword.charAt(0).toUpperCase() + error.keyword.slice(1) + const errorParameter = error.dataPath.slice(1) + return `${errorType} error: ${errorParameter} ${error.message}` +} + +function validateAllNodesAreUnique(node: CodeMapNode, result: CCValidationResult) { + const names = new Set() + names.add(`${node.name}|${node.type}`) + validateChildrenAreUniqueRecursive(node, result, names) +} + +function validateChildrenAreUniqueRecursive(node: CodeMapNode, result: CCValidationResult, names: Set) { + if (!node.children || node.children.length === 0) { + return + } + + for (const child of node.children) { + if (names.has(`${child.name}|${child.type}`)) { + result.error.push(`${ERROR_MESSAGES.nodesNotUnique} Found duplicate of ${child.type} with name: ${child.name}`) + } else { + names.add(`${child.name}|${child.type}`) + validateChildrenAreUniqueRecursive(child, result, names) + } + } +} + +function validateFixedFolders(file: ExportCCFile, result: CCValidationResult) { + const notFixed: string[] = [] + const outOfBounds: string[] = [] + const intersections: Set = new Set() + + for (const node of file.nodes[0].children) { + if (node.fixedPosition === undefined) { + notFixed.push(`${node.name}`) + } else { + const apiVersion = getAsApiVersion(file.apiVersion) + if (apiVersion.major < 1 || (apiVersion.major === 1 && apiVersion.minor < 2)) { + result.error.push(`${ERROR_MESSAGES.fixedFoldersNotAllowed} Found: ${file.apiVersion}`) + return + } + + if (isOutOfBounds(node)) { + outOfBounds.push(getFoundFolderMessage(node)) + } + + for (const node2 of file.nodes[0].children) { + if ( + node2.fixedPosition !== undefined && + node !== node2 && + rectanglesIntersect(node.fixedPosition, node2.fixedPosition) && + !intersections.has(`${getFoundFolderMessage(node2)} and ${getFoundFolderMessage(node)}`) + ) { + intersections.add(`${getFoundFolderMessage(node)} and ${getFoundFolderMessage(node2)}`) + } + } + } + } + + if (notFixed.length > 0 && notFixed.length !== file.nodes[0].children.length) { + result.error.push(`${ERROR_MESSAGES.notAllFoldersAreFixed} Found: ${notFixed.join(", ")}`) + } + + if (outOfBounds.length > 0) { + result.error.push(`${ERROR_MESSAGES.fixedFoldersOutOfBounds} Found: ${outOfBounds.join(", ")}`) + } + + if (intersections.size > 0) { + result.error.push(`${ERROR_MESSAGES.fixedFoldersOverlapped} Found: ${[...intersections].join(", ")}`) + } +} + +function getFoundFolderMessage(node: CodeMapNode): string { + return `${node.name} ${JSON.stringify(node.fixedPosition)}` +} + +function rectanglesIntersect(rect1: FixedPosition, rect2: FixedPosition): boolean { + return ( + isInRectangle(rect1.left, rect1.top, rect2) || + isInRectangle(rect1.left, rect1.top + rect1.height, rect2) || + isInRectangle(rect1.left + rect1.width, rect1.top, rect2) || + isInRectangle(rect1.left + rect1.width, rect1.top + rect1.height, rect2) + ) +} + +function isInRectangle(x: number, y: number, rect: FixedPosition): boolean { + return x >= rect.left && x <= rect.left + rect.width && y >= rect.top && y <= rect.top + rect.height +} + +function isOutOfBounds({ fixedPosition: { left, top, width, height } }: CodeMapNode): boolean { + return left < 0 || top < 0 || left + width > 100 || top + height > 100 || width < 0 || height < 0 +} diff --git a/visualization/app/codeCharta/util/generatedSchema.json b/visualization/app/codeCharta/util/generatedSchema.json index 7d573c0cb3..c6f07f5a22 100644 --- a/visualization/app/codeCharta/util/generatedSchema.json +++ b/visualization/app/codeCharta/util/generatedSchema.json @@ -44,6 +44,9 @@ }, "type": "object" }, + "fixedPosition": { + "$ref": "#/definitions/FixedPosition" + }, "id": { "type": "number" }, @@ -165,6 +168,24 @@ "required": ["apiVersion", "nodes", "projectName"], "type": "object" }, + "FixedPosition": { + "properties": { + "height": { + "type": "number" + }, + "left": { + "type": "number" + }, + "top": { + "type": "number" + }, + "width": { + "type": "number" + } + }, + "required": ["height", "left", "top", "width"], + "type": "object" + }, "KeyValuePair": { "additionalProperties": { "type": "number" @@ -210,7 +231,7 @@ "type": "object" } ], - "minItems": 1, + "minItems": 0, "type": "array" }, "nodes": { @@ -234,7 +255,7 @@ "type": "object" } ], - "minItems": 1, + "minItems": 0, "type": "array" } }, diff --git a/visualization/app/codeCharta/util/scenarioHelper.ts b/visualization/app/codeCharta/util/scenarioHelper.ts index f1c0f53181..33f3042ad8 100755 --- a/visualization/app/codeCharta/util/scenarioHelper.ts +++ b/visualization/app/codeCharta/util/scenarioHelper.ts @@ -138,10 +138,9 @@ export class ScenarioHelper { const ccLocalStorage: CCLocalStorage = JSON.parse(localStorage.getItem("scenarios")) if (ccLocalStorage) { return new Map(ccLocalStorage.scenarios) - } else { - this.setScenariosToLocalStorage(this.getPreLoadScenarios()) - return this.getPreLoadScenarios() } + this.setScenariosToLocalStorage(this.getPreLoadScenarios()) + return this.getPreLoadScenarios() } public static addScenario(newScenario: RecursivePartial) { diff --git a/visualization/app/codeCharta/util/treeMapGenerator.spec.ts b/visualization/app/codeCharta/util/treeMapGenerator.spec.ts index f2a940de2a..8d290dbed6 100755 --- a/visualization/app/codeCharta/util/treeMapGenerator.spec.ts +++ b/visualization/app/codeCharta/util/treeMapGenerator.spec.ts @@ -4,6 +4,9 @@ import { TreeMapGenerator } from "./treeMapGenerator" import { METRIC_DATA, TEST_FILE_WITH_PATHS, VALID_NODE_WITH_PATH, VALID_EDGES, STATE } from "./dataMocks" import { clone } from "./clone" import _ from "lodash" +import { NodeDecorator } from "./nodeDecorator" +import { fileWithFixedFolders } from "../ressources/fixed-folders/fixed-folders-example" +import { getCCFile } from "./fileHelper" describe("treeMapGenerator", () => { let map: CodeMapNode @@ -18,6 +21,7 @@ describe("treeMapGenerator", () => { function restartSystem() { map = clone(TEST_FILE_WITH_PATHS.map) + NodeDecorator.decorateMapWithPathAttribute(getCCFile("someFile", fileWithFixedFolders)) state = _.cloneDeep(STATE) codeMapNode = clone(VALID_NODE_WITH_PATH) metricData = clone(METRIC_DATA) @@ -47,6 +51,12 @@ describe("treeMapGenerator", () => { expect(nodes).toMatchSnapshot() }) + + it("should build the tree map with valid coordinates using the fixed folder structure", () => { + const nodes = TreeMapGenerator.createTreemapNodes(fileWithFixedFolders.nodes[0], state, metricData, isDeltaState) + + expect(nodes).toMatchSnapshot() + }) }) describe("CodeMap value calculation", () => { diff --git a/visualization/app/codeCharta/util/treeMapGenerator.ts b/visualization/app/codeCharta/util/treeMapGenerator.ts index 36b16ef7df..a5baf796bc 100755 --- a/visualization/app/codeCharta/util/treeMapGenerator.ts +++ b/visualization/app/codeCharta/util/treeMapGenerator.ts @@ -1,46 +1,109 @@ import { hierarchy, HierarchyNode, HierarchyRectangularNode, treemap, TreemapLayout } from "d3" import { TreeMapHelper } from "./treeMapHelper" -import { CodeMapHelper } from "./codeMapHelper" import { CodeMapNode, Node, NodeMetricData, State } from "../codeCharta.model" +export type SquarifiedTreeMap = { treeMap: HierarchyRectangularNode; height: number; width: number } + export class TreeMapGenerator { private static PADDING_SCALING_FACTOR = 0.4 public static createTreemapNodes(map: CodeMapNode, s: State, metricData: NodeMetricData[], isDeltaState: boolean): Node[] { - const squarifiedTreeMap: HierarchyRectangularNode = this.getSquarifiedTreeMap(map, s) const maxHeight = metricData.find(x => x.name == s.dynamicSettings.heightMetric).maxValue const heightScale = (s.treeMap.mapSize * 2) / maxHeight - const nodesAsArray: HierarchyRectangularNode[] = this.getNodesAsArray(squarifiedTreeMap) - return nodesAsArray.map(squarifiedNode => { - return TreeMapHelper.buildNodeFrom(squarifiedNode, heightScale, maxHeight, s, isDeltaState) - }) + + if (this.hasFixedFolders(map)) { + return this.buildSquarifiedTreeMapsForFixedFolders(map, s, heightScale, maxHeight, isDeltaState) + } + const squarifiedTreeMap = this.getSquarifiedTreeMap(map, s) + return squarifiedTreeMap.treeMap + .descendants() + .map(squarifiedNode => TreeMapHelper.buildNodeFrom(squarifiedNode, heightScale, maxHeight, s, isDeltaState)) } - private static getSquarifiedTreeMap(map: CodeMapNode, s: State): HierarchyRectangularNode { + private static hasFixedFolders(map: CodeMapNode): boolean { + return !!map.children[0]?.fixedPosition + } + + private static buildSquarifiedTreeMapsForFixedFolders( + map: CodeMapNode, + state: State, + heightScale: number, + maxHeight: number, + isDeltaState: boolean + ): Node[] { const hierarchyNode: HierarchyNode = hierarchy(map) - const nodeLeafs: CodeMapNode[] = hierarchyNode.descendants().map(d => d.data) - const blacklisted: number = CodeMapHelper.numberOfBlacklistedNodes(nodeLeafs) - const nodesPerSide: number = 2 * Math.sqrt(hierarchyNode.descendants().length - blacklisted) - const mapLength: number = s.treeMap.mapSize * 2 + nodesPerSide * s.dynamicSettings.margin - const padding: number = s.dynamicSettings.margin * TreeMapGenerator.PADDING_SCALING_FACTOR - const treeMap: TreemapLayout = treemap() - .size([mapLength, mapLength]) - .paddingOuter(padding) - .paddingInner(padding) - return treeMap(hierarchyNode.sum(node => this.calculateAreaValue(node, s))) - } + const nodes: Node[] = [TreeMapHelper.buildRootFolderForFixedFolders(map, heightScale, state, isDeltaState)] + const scale = + (state.treeMap.mapSize * 2 + this.getEstimatedNodesPerSide(hierarchyNode) * state.dynamicSettings.margin) / nodes[0].length + this.scaleRoot(nodes[0], scale) - private static getNodesAsArray(node: HierarchyRectangularNode): HierarchyRectangularNode[] { - const nodes = [node] - if (node.children) { - node.children.forEach(child => nodes.push(...this.getNodesAsArray(child))) + for (const fixedFolder of map.children) { + const squarified = this.getSquarifiedTreeMap(fixedFolder, state) + squarified.treeMap.descendants().forEach(squarifiedNode => { + this.scaleAndTranslateSquarifiedNode(squarifiedNode, fixedFolder, squarified, scale) + const node = TreeMapHelper.buildNodeFrom(squarifiedNode, heightScale, maxHeight, state, isDeltaState) + nodes.push(node) + }) } return nodes } + private static scaleAndTranslateSquarifiedNode( + squarifiedNode: HierarchyRectangularNode, + fixedFolder: CodeMapNode, + squarified: SquarifiedTreeMap, + scale: number + ) { + // Transform coordinates from local folder space to world space (between 0 and 100). + const scaleX = fixedFolder.fixedPosition.width / squarified.width + const scaleY = fixedFolder.fixedPosition.height / squarified.height + + // Scales to usual map-size of 500 matching the three-scene-size + squarifiedNode.x0 = (squarifiedNode.x0 * scaleX + fixedFolder.fixedPosition.left) * scale + squarifiedNode.x1 = (squarifiedNode.x1 * scaleX + fixedFolder.fixedPosition.left) * scale + squarifiedNode.y0 = (squarifiedNode.y0 * scaleY + fixedFolder.fixedPosition.top) * scale + squarifiedNode.y1 = (squarifiedNode.y1 * scaleY + fixedFolder.fixedPosition.top) * scale + } + + private static scaleRoot(root: Node, scale: number) { + root.x0 *= scale + root.y0 *= scale + root.width *= scale + root.length *= scale + } + + private static getSquarifiedTreeMap(map: CodeMapNode, s: State): SquarifiedTreeMap { + const hierarchyNode: HierarchyNode = hierarchy(map) + const nodesPerSide = this.getEstimatedNodesPerSide(hierarchyNode) + const padding: number = s.dynamicSettings.margin * TreeMapGenerator.PADDING_SCALING_FACTOR + let mapWidth + let mapHeight + + if (map.fixedPosition !== undefined) { + mapWidth = map.fixedPosition.width + mapHeight = map.fixedPosition.height + } else { + mapWidth = s.treeMap.mapSize * 2 + mapHeight = s.treeMap.mapSize * 2 + } + + const width = mapWidth + nodesPerSide * s.dynamicSettings.margin + const height = mapHeight + nodesPerSide * s.dynamicSettings.margin + + const treeMap: TreemapLayout = treemap().size([width, height]).paddingOuter(padding).paddingInner(padding) + + return { treeMap: treeMap(hierarchyNode.sum(node => this.calculateAreaValue(node, s))), height, width } + } + + private static getEstimatedNodesPerSide(hierarchyNode: HierarchyNode): number { + const descendants = hierarchyNode.descendants() + const blacklisted = descendants.filter(node => node.data.isExcluded || node.data.isFlattened).length + return 2 * Math.sqrt(descendants.length - blacklisted) + } + private static isOnlyVisibleInComparisonMap(node: CodeMapNode, s: State): boolean { - return node && node.deltas && node.deltas[s.dynamicSettings.heightMetric] < 0 && node.attributes[s.dynamicSettings.areaMetric] === 0 + return node?.deltas?.[s.dynamicSettings.heightMetric] < 0 && node.attributes[s.dynamicSettings.areaMetric] === 0 } private static calculateAreaValue(node: CodeMapNode, s: State): number { diff --git a/visualization/app/codeCharta/util/treeMapHelper.spec.ts b/visualization/app/codeCharta/util/treeMapHelper.spec.ts index 2b75149dc6..2052005040 100644 --- a/visualization/app/codeCharta/util/treeMapHelper.spec.ts +++ b/visualization/app/codeCharta/util/treeMapHelper.spec.ts @@ -3,7 +3,7 @@ import { BlacklistType, CodeMapNode, EdgeVisibility, NodeType, State } from "../ import { CODE_MAP_BUILDING, STATE } from "./dataMocks" import { HierarchyRectangularNode } from "d3" -describe("treeMapHelper", () => { +describe("TreeMapHelper", () => { describe("build node", () => { let codeMapNode: CodeMapNode let squaredNode: HierarchyRectangularNode @@ -118,210 +118,194 @@ describe("treeMapHelper", () => { it("should be visible if it's not excluded and no focused node path is given", () => { expect(buildNode().visible).toBeTruthy() }) - }) - - describe("count nodes", () => { - it("root only should be 1", () => { - const root = {} - expect(TreeMapHelper.countNodes(root)).toBe(1) - }) - - it("root plus child should be 2", () => { - const root = { children: [{}] } - expect(TreeMapHelper.countNodes(root)).toBe(2) - }) - - it("root plus child in child should be 3", () => { - const root = { children: [{ children: [{}] }] } - expect(TreeMapHelper.countNodes(root)).toBe(3) - }) - - it("root plus two children should be 3", () => { - const root = { children: [{}, {}] } - expect(TreeMapHelper.countNodes(root)).toBe(3) - }) - }) - - describe("isNodeToBeFlat", () => { - let codeMapNode: CodeMapNode - let squaredNode: HierarchyRectangularNode - let state: State - beforeEach(() => { - codeMapNode = { - name: "Anode", - path: "/root/Anode", - type: NodeType.FILE, - attributes: {}, - edgeAttributes: { pairingRate: { incoming: 42, outgoing: 23 } }, - isExcluded: false, - isFlattened: false - } - - squaredNode = { - data: codeMapNode, - value: 42, - x0: 0, - y0: 0, - x1: 400, - y1: 400 - } as HierarchyRectangularNode - - state = STATE - state.treeMap.mapSize = 1 - state.dynamicSettings.margin = 15 - }) - - it("should not be a flat node when no visibleEdges", () => { - state.fileSettings.edges = [] - expect(TreeMapHelper["isNodeToBeFlat"](squaredNode, state)).toBeFalsy() - }) - - it("should be a flat node when other edges are visible", () => { - state.appSettings.showOnlyBuildingsWithEdges = true - state.fileSettings.edges = [ - { - fromNodeName: "/root/anotherNode", - toNodeName: "/root/anotherNode2", + describe("isNodeToBeFlat", () => { + beforeEach(() => { + codeMapNode = { + name: "Anode", + path: "/root/Anode", + type: NodeType.FILE, attributes: {}, - visible: EdgeVisibility.both + edgeAttributes: { pairingRate: { incoming: 42, outgoing: 23 } }, + isExcluded: false, + isFlattened: false } - ] - expect(TreeMapHelper["isNodeToBeFlat"](squaredNode, state)).toBeTruthy() - }) - it("should not be a flat node when it contains edges", () => { - state.fileSettings.edges = [ - { - fromNodeName: "/root/Anode", - toNodeName: "/root/anotherNode", + squaredNode = { + data: codeMapNode, + value: 42, + x0: 0, + y0: 0, + x1: 400, + y1: 400 + } as HierarchyRectangularNode + + state = STATE + state.treeMap.mapSize = 1 + state.dynamicSettings.margin = 15 + }) + + it("should not be a flat node when no visibleEdges", () => { + state.fileSettings.edges = [] + expect(buildNode().flat).toBeFalsy() + }) + + it("should be a flat node when other edges are visible", () => { + state.appSettings.showOnlyBuildingsWithEdges = true + state.fileSettings.edges = [ + { + fromNodeName: "/root/anotherNode", + toNodeName: "/root/anotherNode2", + attributes: {}, + visible: EdgeVisibility.both + } + ] + expect(buildNode().flat).toBeTruthy() + }) + + it("should not be a flat node when it contains edges", () => { + state.fileSettings.edges = [ + { + fromNodeName: "/root/Anode", + toNodeName: "/root/anotherNode", + attributes: {} + } + ] + expect(buildNode().flat).toBeFalsy() + }) + + it("should not be a flat node, because its searched for", () => { + state.dynamicSettings.searchedNodePaths = new Set(["/root/Anode"]) + state.dynamicSettings.searchPattern = "Anode" + expect(buildNode().flat).toBeFalsy() + }) + + it("should be a flat node, because other nodes are searched for", () => { + state.dynamicSettings.searchedNodePaths = new Set(["/root/anotherNode", "/root/anotherNode2"]) + state.dynamicSettings.searchPattern = "Anode" + expect(buildNode().flat).toBeTruthy() + }) + + it("should not be a flat node when searchPattern is empty", () => { + state.dynamicSettings.searchedNodePaths = new Set(["/root/anotherNode", "/root/anotherNode2"]) + state.dynamicSettings.searchPattern = "" + expect(buildNode().flat).toBeFalsy() + }) + + it("should be flat if node is flattened in blacklist", () => { + state.fileSettings.blacklist = [{ path: "*Anode", type: BlacklistType.flatten }] + squaredNode.data.isFlattened = true + + expect(buildNode().flat).toBeTruthy() + }) + + it("should not be flat if node is not blacklisted", () => { + state.fileSettings.blacklist = [] + + expect(buildNode().flat).toBeFalsy() + }) + }) + + describe("getBuildingColor", () => { + let node: CodeMapNode + let state: State + + beforeEach(() => { + node = { + name: "Anode", + path: "/root/Anode", + type: NodeType.FILE, attributes: {} - } - ] - expect(TreeMapHelper["isNodeToBeFlat"](squaredNode, state)).toBeFalsy() - }) - - it("should not be a flat node, because its searched for", () => { - state.dynamicSettings.searchedNodePaths = new Set(["/root/Anode"]) - state.dynamicSettings.searchPattern = "Anode" - expect(TreeMapHelper["isNodeToBeFlat"](squaredNode, state)).toBeFalsy() - }) + } as CodeMapNode - it("should be a flat node, because other nodes are searched for", () => { - state.dynamicSettings.searchedNodePaths = new Set(["/root/anotherNode", "/root/anotherNode2"]) - state.dynamicSettings.searchPattern = "Anode" - expect(TreeMapHelper["isNodeToBeFlat"](squaredNode, state)).toBeTruthy() - }) - - it("should not be a flat node when searchPattern is empty", () => { - state.dynamicSettings.searchedNodePaths = new Set(["/root/anotherNode", "/root/anotherNode2"]) - state.dynamicSettings.searchPattern = "" - expect(TreeMapHelper["isNodeToBeFlat"](squaredNode, state)).toBeFalsy() - }) + squaredNode.data = node - it("should be flat if node is flattened in blacklist", () => { - state.fileSettings.blacklist = [{ path: "*Anode", type: BlacklistType.flatten }] - squaredNode.data.isFlattened = true + node.attributes = { validMetricName: 0 } - expect(TreeMapHelper["isNodeToBeFlat"](squaredNode, state)).toBeTruthy() - }) + state = STATE + state.appSettings.invertColorRange = false + state.appSettings.whiteColorBuildings = false + state.dynamicSettings.colorRange.from = 5 + state.dynamicSettings.colorRange.to = 10 + state.dynamicSettings.colorMetric = "validMetricName" + }) - it("should not be flat if node is not blacklisted", () => { - state.fileSettings.blacklist = [] - - expect(TreeMapHelper["isNodeToBeFlat"](squaredNode, state)).toBeFalsy() - }) - }) + it("creates grey building for undefined colorMetric", () => { + state.dynamicSettings.colorMetric = "invalid" + expect(buildNode().color).toBe(state.appSettings.mapColors.base) + }) - describe("getBuildingColor", () => { - let node: CodeMapNode - let state: State - - beforeEach(() => { - node = { - name: "Anode", - path: "/root/Anode", - type: NodeType.FILE, - attributes: {} - } as CodeMapNode + it("creates flat colored building", () => { + state.fileSettings.blacklist = [{ path: "*Anode", type: BlacklistType.flatten }] + squaredNode.data.isFlattened = true - node.attributes = { validMetircName: 0 } + expect(buildNode().color).toBe(state.appSettings.mapColors.flat) + }) - state = STATE - state.appSettings.invertColorRange = false - state.appSettings.whiteColorBuildings = false - state.dynamicSettings.colorRange.from = 5 - state.dynamicSettings.colorRange.to = 10 - state.dynamicSettings.colorMetric = "validMetircName" - }) + it("creates green colored building colorMetricValue < colorRangeFrom", () => { + expect(buildNode().color).toBe(state.appSettings.mapColors.positive) + }) - it("creates grey building for undefined colorMetric", () => { - state.dynamicSettings.colorMetric = "invalid" - const buildingColor = TreeMapHelper["getBuildingColor"](node, state, false, false) - expect(buildingColor).toBe(state.appSettings.mapColors.base) - }) + it("creates white colored building colorMetricValue < colorRangeFrom", () => { + state.appSettings.whiteColorBuildings = true - it("creates flat colored building", () => { - const flattened = true + expect(buildNode().color).toBe(state.appSettings.mapColors.lightGrey) + }) - const buildingColor = TreeMapHelper["getBuildingColor"](node, state, false, flattened) + it("creates red colored building colorMetricValue < colorRangeFrom with inverted range", () => { + state.appSettings.invertColorRange = true - expect(buildingColor).toBe(state.appSettings.mapColors.flat) - }) + expect(buildNode().color).toBe(state.appSettings.mapColors.negative) + }) - it("creates green colored building colorMetricValue < colorRangeFrom", () => { - const buildingColor = TreeMapHelper["getBuildingColor"](node, state, false, false) + it("creates red colored building colorMetricValue > colorRangeFrom", () => { + node.attributes = { validMetricName: 12 } - expect(buildingColor).toBe(state.appSettings.mapColors.positive) - }) + expect(buildNode().color).toBe(state.appSettings.mapColors.negative) + }) - it("creates white colored building colorMetricValue < colorRangeFrom", () => { - state.appSettings.whiteColorBuildings = true + it("creates green colored building colorMetricValue > colorRangeFrom with inverted range", () => { + state.appSettings.invertColorRange = true + node.attributes = { validMetricName: 12 } - const buildingColor = TreeMapHelper["getBuildingColor"](node, state, false, false) + expect(buildNode().color).toBe(state.appSettings.mapColors.positive) + }) - expect(buildingColor).toBe(state.appSettings.mapColors.lightGrey) - }) + it("creates white colored building colorMetricValue > colorRangeFrom with inverted range", () => { + state.appSettings.invertColorRange = true + state.appSettings.whiteColorBuildings = true + node.attributes = { validMetricName: 12 } - it("creates red colored building colorMetricValue < colorRangeFrom with inverted range", () => { - state.appSettings.invertColorRange = true + expect(buildNode().color).toBe(state.appSettings.mapColors.lightGrey) + }) - const buildingColor = TreeMapHelper["getBuildingColor"](node, state, false, false) + it("creates yellow colored building", () => { + node.attributes = { validMetricName: 7 } - expect(buildingColor).toBe(state.appSettings.mapColors.negative) + expect(buildNode().color).toBe(state.appSettings.mapColors.neutral) + }) }) + }) - it("creates red colored building colorMetricValue > colorRangeFrom", () => { - node.attributes = { validMetircName: 12 } - - const buildingColor = TreeMapHelper["getBuildingColor"](node, state, false, false) - - expect(buildingColor).toBe(state.appSettings.mapColors.negative) + describe("count nodes", () => { + it("root only should be 1", () => { + const root = {} + expect(TreeMapHelper.countNodes(root)).toBe(1) }) - it("creates green colored building colorMetricValue > colorRangeFrom with inverted range", () => { - state.appSettings.invertColorRange = true - node.attributes = { validMetircName: 12 } - - const buildingColor = TreeMapHelper["getBuildingColor"](node, state, false, false) - - expect(buildingColor).toBe(state.appSettings.mapColors.positive) + it("root plus child should be 2", () => { + const root = { children: [{}] } + expect(TreeMapHelper.countNodes(root)).toBe(2) }) - it("creates white colored building colorMetricValue > colorRangeFrom with inverted range", () => { - state.appSettings.invertColorRange = true - state.appSettings.whiteColorBuildings = true - node.attributes = { validMetircName: 12 } - - const buildingColor = TreeMapHelper["getBuildingColor"](node, state, false, false) - - expect(buildingColor).toBe(state.appSettings.mapColors.lightGrey) + it("root plus child in child should be 3", () => { + const root = { children: [{ children: [{}] }] } + expect(TreeMapHelper.countNodes(root)).toBe(3) }) - it("creates yellow colored building", () => { - node.attributes = { validMetircName: 7 } - const buildingColor = TreeMapHelper["getBuildingColor"](node, state, false, false) - expect(buildingColor).toBe(state.appSettings.mapColors.neutral) + it("root plus two children should be 3", () => { + const root = { children: [{}, {}] } + expect(TreeMapHelper.countNodes(root)).toBe(3) }) }) diff --git a/visualization/app/codeCharta/util/treeMapHelper.ts b/visualization/app/codeCharta/util/treeMapHelper.ts index fd358ad895..60c88cd5d8 100644 --- a/visualization/app/codeCharta/util/treeMapHelper.ts +++ b/visualization/app/codeCharta/util/treeMapHelper.ts @@ -4,171 +4,193 @@ import { Vector3 } from "three" import { CodeMapBuilding } from "../ui/codeMap/rendering/codeMapBuilding" import { HierarchyRectangularNode } from "d3" -export class TreeMapHelper { - private static FOLDER_HEIGHT = 2 - private static MIN_BUILDING_HEIGHT = 2 - private static HEIGHT_VALUE_WHEN_METRIC_NOT_FOUND = 0 - - public static countNodes(node: { children?: any }): number { - let count = 1 - if (node.children && node.children.length > 0) { - for (let i = 0; i < node.children.length; i++) { - count += this.countNodes(node.children[i]) - } +const FOLDER_HEIGHT = 2 +const MIN_BUILDING_HEIGHT = 2 +const HEIGHT_VALUE_WHEN_METRIC_NOT_FOUND = 0 + +function countNodes(node: { children?: any }): number { + let count = 1 + if (node.children && node.children.length > 0) { + for (let i = 0; i < node.children.length; i++) { + count += countNodes(node.children[i]) } - return count } + return count +} + +function buildingArrayToMap(highlighted: CodeMapBuilding[]): Map { + const geomMap = new Map() + highlighted.forEach(building => { + geomMap.set(building.id, building) + }) - public static buildingArrayToMap(highlighted: CodeMapBuilding[]): Map { - const geomMap = new Map() - highlighted.forEach(building => { - geomMap.set(building.id, building) - }) + return geomMap +} - return geomMap +function buildRootFolderForFixedFolders(map: CodeMapNode, heightScale: number, state: State, isDeltaState: boolean): Node { + const flattened: boolean = isNodeFlat(map, state) + const height = FOLDER_HEIGHT + const width = 100 + const length = 100 + + return { + name: map.name, + id: 0, + width, + height, + length, + depth: 0, + x0: 0, + z0: 0, + y0: 0, + isLeaf: false, + attributes: map.attributes, + edgeAttributes: map.edgeAttributes, + deltas: map.deltas, + heightDelta: (map.deltas?.[state.dynamicSettings.heightMetric] ?? 0) * heightScale, + visible: isVisible(map, false, state, flattened), + path: map.path, + link: map.link, + markingColor: CodeMapHelper.getMarkingColor(map, state.fileSettings.markedPackages), + flat: false, + color: getBuildingColor(map, state, isDeltaState, flattened), + incomingEdgePoint: getIncomingEdgePoint(width, height, length, new Vector3(0, 0, 0), state.treeMap.mapSize), + outgoingEdgePoint: getOutgoingEdgePoint(width, height, length, new Vector3(0, 0, 0), state.treeMap.mapSize) } +} - private static getHeightValue( - s: State, - squaredNode: HierarchyRectangularNode, - maxHeight: number, - flattened: boolean - ): number { - const heightValue = squaredNode.data.attributes[s.dynamicSettings.heightMetric] || TreeMapHelper.HEIGHT_VALUE_WHEN_METRIC_NOT_FOUND - - if (flattened) { - return TreeMapHelper.MIN_BUILDING_HEIGHT - } else if (s.appSettings.invertHeight) { - return maxHeight - heightValue - } else { - return heightValue - } +function buildNodeFrom( + squaredNode: HierarchyRectangularNode, + heightScale: number, + maxHeight: number, + s: State, + isDeltaState: boolean +): Node { + const isNodeLeaf = !(squaredNode.children && squaredNode.children.length > 0) + const flattened: boolean = isNodeFlat(squaredNode.data, s) + const heightValue: number = getHeightValue(s, squaredNode, maxHeight, flattened) + const depth: number = squaredNode.data.path.split("/").length - 2 + const width = squaredNode.x1 - squaredNode.x0 + const height = Math.abs(isNodeLeaf ? Math.max(heightScale * heightValue, MIN_BUILDING_HEIGHT) : FOLDER_HEIGHT) + const length = squaredNode.y1 - squaredNode.y0 + const x0 = squaredNode.x0 + const y0 = squaredNode.y0 + const z0 = depth * FOLDER_HEIGHT + + return { + name: squaredNode.data.name, + id: squaredNode.data.id, + width, + height, + length, + depth, + x0, + z0, + y0, + isLeaf: isNodeLeaf, + attributes: squaredNode.data.attributes, + edgeAttributes: squaredNode.data.edgeAttributes, + deltas: squaredNode.data.deltas, + heightDelta: (squaredNode.data.deltas?.[s.dynamicSettings.heightMetric] ?? 0) * heightScale, + visible: isVisible(squaredNode.data, isNodeLeaf, s, flattened), + path: squaredNode.data.path, + link: squaredNode.data.link, + markingColor: CodeMapHelper.getMarkingColor(squaredNode.data, s.fileSettings.markedPackages), + flat: flattened, + color: getBuildingColor(squaredNode.data, s, isDeltaState, flattened), + incomingEdgePoint: getIncomingEdgePoint(width, height, length, new Vector3(x0, z0, y0), s.treeMap.mapSize), + outgoingEdgePoint: getOutgoingEdgePoint(width, height, length, new Vector3(x0, z0, y0), s.treeMap.mapSize) } +} - public static buildNodeFrom( - squaredNode: HierarchyRectangularNode, - heightScale: number, - maxHeight: number, - s: State, - isDeltaState: boolean - ): Node { - const isNodeLeaf = !(squaredNode.children && squaredNode.children.length > 0) - const flattened: boolean = this.isNodeToBeFlat(squaredNode, s) - const heightValue: number = this.getHeightValue(s, squaredNode, maxHeight, flattened) - const depth: number = squaredNode.data.path.split("/").length - 2 - const width = squaredNode.x1 - squaredNode.x0 - const height = Math.abs( - isNodeLeaf ? Math.max(heightScale * heightValue, TreeMapHelper.MIN_BUILDING_HEIGHT) : TreeMapHelper.FOLDER_HEIGHT - ) - const length = squaredNode.y1 - squaredNode.y0 - const x0 = squaredNode.x0 - const y0 = squaredNode.y0 - const z0 = depth * TreeMapHelper.FOLDER_HEIGHT - - return { - name: squaredNode.data.name, - id: squaredNode.data.id, - width, - height, - length, - depth, - x0, - z0, - y0, - isLeaf: isNodeLeaf, - attributes: squaredNode.data.attributes, - edgeAttributes: squaredNode.data.edgeAttributes, - deltas: squaredNode.data.deltas, - heightDelta: - squaredNode.data.deltas && squaredNode.data.deltas[s.dynamicSettings.heightMetric] - ? heightScale * squaredNode.data.deltas[s.dynamicSettings.heightMetric] - : 0, - visible: this.isVisible(squaredNode.data, isNodeLeaf, s, flattened), - path: squaredNode.data.path, - link: squaredNode.data.link, - markingColor: CodeMapHelper.getMarkingColor(squaredNode.data, s.fileSettings.markedPackages), - flat: flattened, - color: this.getBuildingColor(squaredNode.data, s, isDeltaState, flattened), - incomingEdgePoint: this.getIncomingEdgePoint(width, height, length, new Vector3(x0, z0, y0), s.treeMap.mapSize), - outgoingEdgePoint: this.getOutgoingEdgePoint(width, height, length, new Vector3(x0, z0, y0), s.treeMap.mapSize) - } +function getHeightValue(s: State, squaredNode: HierarchyRectangularNode, maxHeight: number, flattened: boolean): number { + const heightValue = squaredNode.data.attributes[s.dynamicSettings.heightMetric] || HEIGHT_VALUE_WHEN_METRIC_NOT_FOUND + + if (flattened) { + return MIN_BUILDING_HEIGHT } - private static isVisible(squaredNode: CodeMapNode, isNodeLeaf: boolean, s: State, flattened: boolean): boolean { - let isVisible = true - if (s.dynamicSettings.focusedNodePath.length > 0) { - isVisible = squaredNode.path.includes(s.dynamicSettings.focusedNodePath) - } - if (squaredNode.isExcluded || (isNodeLeaf && s.appSettings.hideFlatBuildings && flattened)) { - isVisible = false - } - return isVisible + if (s.appSettings.invertHeight) { + return maxHeight - heightValue } + return heightValue +} - private static getIncomingEdgePoint(width: number, height: number, length: number, vector: Vector3, mapSize: number) { - if (width > length) { - return new Vector3(vector.x - mapSize + width / 4, vector.y + height, vector.z - mapSize + length / 2) - } else { - return new Vector3(vector.x - mapSize + width / 2, vector.y + height, vector.z - mapSize + length / 4) - } +function isVisible(squaredNode: CodeMapNode, isNodeLeaf: boolean, s: State, flattened: boolean): boolean { + if (squaredNode.isExcluded || (isNodeLeaf && s.appSettings.hideFlatBuildings && flattened)) { + return false } - private static getOutgoingEdgePoint(width: number, height: number, length: number, vector: Vector3, mapSize: number) { - if (width > length) { - return new Vector3(vector.x - mapSize + 0.75 * width, vector.y + height, vector.z - mapSize + length / 2) - } else { - return new Vector3(vector.x - mapSize + width / 2, vector.y + height, vector.z - mapSize + 0.75 * length) - } + if (s.dynamicSettings.focusedNodePath.length > 0) { + return squaredNode.path.includes(s.dynamicSettings.focusedNodePath) } - private static isNodeToBeFlat(squaredNode: HierarchyRectangularNode, s: State): boolean { - let flattened = false - if ( - s.appSettings.showOnlyBuildingsWithEdges && - s.fileSettings.edges && - s.fileSettings.edges.filter(edge => edge.visible).length > 0 - ) { - flattened = this.nodeHasNoVisibleEdges(squaredNode, s) - } + return true +} - if (s.dynamicSettings.searchedNodePaths && s.dynamicSettings.searchPattern && s.dynamicSettings.searchPattern.length > 0) { - flattened = s.dynamicSettings.searchedNodePaths.size == 0 ? true : this.isNodeNonSearched(squaredNode, s) - } +function getIncomingEdgePoint(width: number, height: number, length: number, vector: Vector3, mapSize: number) { + if (width > length) { + return new Vector3(vector.x - mapSize + width / 4, vector.y + height, vector.z - mapSize + length / 2) + } + return new Vector3(vector.x - mapSize + width / 2, vector.y + height, vector.z - mapSize + length / 4) +} - flattened = squaredNode.data.isFlattened || flattened - return flattened +function getOutgoingEdgePoint(width: number, height: number, length: number, vector: Vector3, mapSize: number) { + if (width > length) { + return new Vector3(vector.x - mapSize + 0.75 * width, vector.y + height, vector.z - mapSize + length / 2) } + return new Vector3(vector.x - mapSize + width / 2, vector.y + height, vector.z - mapSize + 0.75 * length) +} - private static nodeHasNoVisibleEdges(squaredNode: HierarchyRectangularNode, s: State): boolean { - return ( - squaredNode.data.edgeAttributes[s.dynamicSettings.edgeMetric] === undefined || - s.fileSettings.edges.filter(edge => squaredNode.data.path === edge.fromNodeName || squaredNode.data.path === edge.toNodeName) - .length == 0 - ) +function isNodeFlat(codeMapNode: CodeMapNode, s: State): boolean { + if (codeMapNode.isFlattened) { + return true } - private static isNodeNonSearched(squaredNode: HierarchyRectangularNode, s: State): boolean { - return !s.dynamicSettings.searchedNodePaths.has(squaredNode.data.path) + if (s.dynamicSettings.searchedNodePaths && s.dynamicSettings.searchPattern && s.dynamicSettings.searchPattern.length > 0) { + return s.dynamicSettings.searchedNodePaths.size === 0 || isNodeNonSearched(codeMapNode, s) } - private static getBuildingColor(node: CodeMapNode, s: State, isDeltaState: boolean, flattened: boolean): string { - const mapColorPositive = s.appSettings.whiteColorBuildings ? s.appSettings.mapColors.lightGrey : s.appSettings.mapColors.positive - if (isDeltaState) { - return s.appSettings.mapColors.base - } else { - const metricValue: number = node.attributes[s.dynamicSettings.colorMetric] - - if (metricValue == null) { - return s.appSettings.mapColors.base - } else if (flattened) { - return s.appSettings.mapColors.flat - } else if (metricValue < s.dynamicSettings.colorRange.from) { - return s.appSettings.invertColorRange ? s.appSettings.mapColors.negative : mapColorPositive - } else if (metricValue > s.dynamicSettings.colorRange.to) { - return s.appSettings.invertColorRange ? mapColorPositive : s.appSettings.mapColors.negative - } else { - return s.appSettings.mapColors.neutral - } - } + if (s.appSettings.showOnlyBuildingsWithEdges && s.fileSettings.edges?.filter(edge => edge.visible).length > 0) { + return nodeHasNoVisibleEdges(codeMapNode, s) } + + return false +} + +function nodeHasNoVisibleEdges(codeMapNode: CodeMapNode, s: State): boolean { + return ( + codeMapNode.edgeAttributes[s.dynamicSettings.edgeMetric] === undefined || + s.fileSettings.edges.filter(edge => codeMapNode.path === edge.fromNodeName || codeMapNode.path === edge.toNodeName).length == 0 + ) +} + +function isNodeNonSearched(squaredNode: CodeMapNode, s: State): boolean { + return !s.dynamicSettings.searchedNodePaths.has(squaredNode.path) +} + +function getBuildingColor(node: CodeMapNode, s: State, isDeltaState: boolean, flattened: boolean): string { + const mapColorPositive = s.appSettings.whiteColorBuildings ? s.appSettings.mapColors.lightGrey : s.appSettings.mapColors.positive + if (isDeltaState) { + return s.appSettings.mapColors.base + } + const metricValue: number = node.attributes[s.dynamicSettings.colorMetric] + + if (metricValue == null) { + return s.appSettings.mapColors.base + } else if (flattened) { + return s.appSettings.mapColors.flat + } else if (metricValue < s.dynamicSettings.colorRange.from) { + return s.appSettings.invertColorRange ? s.appSettings.mapColors.negative : mapColorPositive + } else if (metricValue > s.dynamicSettings.colorRange.to) { + return s.appSettings.invertColorRange ? mapColorPositive : s.appSettings.mapColors.negative + } + return s.appSettings.mapColors.neutral +} + +export const TreeMapHelper = { + countNodes, + buildingArrayToMap, + buildRootFolderForFixedFolders, + buildNodeFrom } diff --git a/visualization/jest.config.json b/visualization/jest.config.json index 9ea7d3a1b6..17f13bf087 100644 --- a/visualization/jest.config.json +++ b/visualization/jest.config.json @@ -25,5 +25,6 @@ }, "testURL": "http://localhost:3000", "preset": "./jest-preset.js", - "testTimeout": 60000 + "testTimeout": 60000, + "setupFiles": ["/mocks/localStorageMock.js"] } diff --git a/visualization/package.json b/visualization/package.json index 8fb42d748f..e9a46eaf98 100644 --- a/visualization/package.json +++ b/visualization/package.json @@ -25,7 +25,7 @@ "cli.js" ], "codecharta": { - "apiVersion": "1.1" + "apiVersion": "1.2" }, "window": { "icon": "app/icon.png",