From f4ab03150b62f12005c13f7c9676d037343d8e5d Mon Sep 17 00:00:00 2001 From: Ralf King Date: Sat, 2 Dec 2023 01:12:00 +0100 Subject: [PATCH 01/15] Support new collection projects. Display collection logic used in project list and mark collection projects visually. Signed-off-by: Ralf King --- src/assets/scss/_custom.scss | 6 +++ src/i18n/locales/en.json | 8 ++++ src/views/portfolio/projects/Project.vue | 4 ++ .../projects/ProjectCreateProjectModal.vue | 42 ++++++++++++++++++- .../projects/ProjectDetailsModal.vue | 40 ++++++++++++++++++ src/views/portfolio/projects/ProjectList.vue | 19 ++++++++- 6 files changed, 117 insertions(+), 2 deletions(-) diff --git a/src/assets/scss/_custom.scss b/src/assets/scss/_custom.scss index b6803f96a..7da6502bb 100644 --- a/src/assets/scss/_custom.scss +++ b/src/assets/scss/_custom.scss @@ -508,3 +508,9 @@ td a.detail-icon { display: inline; white-space: nowrap; } + + +.icon-cellend { + float: right; + padding-top: 0.3rem; +} diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 76dc3b0bf..a9dba28db 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -362,6 +362,8 @@ "close": "Close", "code_not_present": "Code not present", "code_not_reachable": "Code not reachable", + "collectionLogic": "Project Collection Logic", + "collection_indicator_tooltip": "Collection project with values calculated based on children.", "comment": "Comment", "comments": "Comments", "component": "Component", @@ -710,6 +712,12 @@ "profile_update": "Update Profile", "profile_updated": "Profile updated", "project_cloning_in_progress": "The project is being created with the cloning options specified", + "project_add_collection_tag": "Aggregate data of children with this tag", + "project_collection_logic_desc": "Specifies if this project is a collection project and which metrics calculation logic to apply for a collection project. Collection projects do not have own components but display data of their children using one of the available logics.", + "project_collection_logic_none": "None", + "project_collection_logic_aggregate_direct_children": "Aggregate direct children", + "project_collection_logic_aggregate_direct_children_with_tag": "Aggregate direct children with tag", + "project_collection_logic_highest_semver_child": "Show highest SemVer child", "project_created": "Project created", "project_delete_message": "Deleting this project will also permanently delete all associated child projects. This action cannot be undone. Are you sure you want to proceed?", "project_delete_title": "Confirm Project Deletion", diff --git a/src/views/portfolio/projects/Project.vue b/src/views/portfolio/projects/Project.vue index 24cbad614..d68916b4f 100644 --- a/src/views/portfolio/projects/Project.vue +++ b/src/views/portfolio/projects/Project.vue @@ -482,6 +482,10 @@ export default { }) .then((response) => { this.project = response.data; + // metrics are not always returned by API, fix error sometimes raised in following lines + if(!Object.hasOwn(this.project, 'metrics')) { + this.project.metrics = {} + } this.currentCritical = common.valueWithDefault( this.project.metrics.critical, 0, diff --git a/src/views/portfolio/projects/ProjectCreateProjectModal.vue b/src/views/portfolio/projects/ProjectCreateProjectModal.vue index 17322022d..6a694885d 100644 --- a/src/views/portfolio/projects/ProjectCreateProjectModal.vue +++ b/src/views/portfolio/projects/ProjectCreateProjectModal.vue @@ -67,6 +67,24 @@ :tooltip="$t('message.component_team_desc')" :disabled="isDisabled" /> + + +
0 ) ? {name: this.collectionTags[0].text} : null, purl: this.project.purl, cpe: this.project.cpe, swidTagId: this.project.swidTagId, @@ -414,12 +451,15 @@ export default { }, resetValues: function () { this.project = { + collectionLogic: 'NONE', // set default to regular project team: [], }; this.tag = ''; this.tags = []; this.selectedParent = null; this.availableParents = []; + this.collectionTags = []; + this.showCollectionTags = false; }, createProjectLabel: function (project) { if (project.version) { diff --git a/src/views/portfolio/projects/ProjectDetailsModal.vue b/src/views/portfolio/projects/ProjectDetailsModal.vue index 01a757d51..12f857cd7 100644 --- a/src/views/portfolio/projects/ProjectDetailsModal.vue +++ b/src/views/portfolio/projects/ProjectDetailsModal.vue @@ -69,6 +69,28 @@ :tooltip="$t('message.component_classifier_desc')" :readonly="this.isNotPermitted(PERMISSIONS.PORTFOLIO_MANAGEMENT)" /> + +
({ text: tag.name })); + this.collectionTags = this.project.collectionTag ? [{ text: this.project.collectionTag.name }] : []; + this.syncCollectionTagsVisibility(this.project.collectionLogic); }, syncReadOnlyNameField: function (value) { this.readOnlyProjectName = value; @@ -675,6 +708,9 @@ export default { syncReadOnlyVersionField: function (value) { this.readOnlyProjectVersion = value; }, + syncCollectionTagsVisibility: function(value) { + this.showCollectionTags = value === 'AGGREGATE_DIRECT_CHILDREN_WITH_TAG'; + }, updateProject: function () { let url = `${this.$api.BASE_URL}/${this.$api.URL_PROJECT}`; let tagsNode = []; @@ -694,6 +730,10 @@ export default { version: this.project.version, description: this.project.description, classifier: this.project.classifier, + collectionLogic: this.project.collectionLogic, + collectionTag: ( this.project.collectionLogic === 'AGGREGATE_DIRECT_CHILDREN_WITH_TAG' && + this.collectionTags && + this.collectionTags.length > 0 ) ? {name: this.collectionTags[0].text} : null, parent: parent, cpe: this.project.cpe, purl: this.project.purl, diff --git a/src/views/portfolio/projects/ProjectList.vue b/src/views/portfolio/projects/ProjectList.vue index 60fa04135..1b5d7690c 100644 --- a/src/views/portfolio/projects/ProjectList.vue +++ b/src/views/portfolio/projects/ProjectList.vue @@ -243,7 +243,24 @@ export default { sortable: true, formatter(value, row, index) { let url = xssFilters.uriInUnQuotedAttr('../projects/' + row.uuid); - return `${xssFilters.inHTMLData(value)}`; + let collectionIcon = ''; + if(row.collectionLogic !== 'NONE') { + let title = 'Metrics of collection project are calculated ' + switch (row.collectionLogic) { + case 'AGGREGATE_DIRECT_CHILDREN': + title += 'by aggregating numbers of all direct children.'; + break; + case 'AGGREGATE_DIRECT_CHILDREN_WITH_TAG': + const tag = !row.collectionTag ? '' : xssFilters.inDoubleQuotedAttr(row.collectionTag.name); + title += `by aggregating numbers of direct children with tag '${tag}'.`; + break; + case 'HIGHEST_SEMVER_CHILD': + title += 'by using the child with highest SemVer version.' + break; + } + collectionIcon = ` `; + } + return `${xssFilters.inHTMLData(value)}${collectionIcon}`; }, }, { From 88c306b24d8f677b1cb75a6f8196394599d276b0 Mon Sep 17 00:00:00 2001 From: Ralf King Date: Sat, 2 Dec 2023 17:04:48 +0100 Subject: [PATCH 02/15] * Fixed some routing bugs * Made project list reusable independent from the project list view * Reused project list to show children of collection projects in project view * Hide tabs without function in collection projects, show child projects instead * visually mark collection projects in project header and explain logic used in tooltip Signed-off-by: Ralf King --- src/i18n/locales/en.json | 1 + src/plugins/table.js | 1 + src/router/index.js | 5 +- src/shared/common.js | 6 +- src/views/portfolio/projects/Project.vue | 67 +++++++++++++++++-- .../projects/ProjectCollectionProjects.vue | 17 +++++ src/views/portfolio/projects/ProjectList.vue | 22 ++++-- .../portfolio/projects/ProjectListView.vue | 16 +++++ 8 files changed, 121 insertions(+), 14 deletions(-) create mode 100644 src/views/portfolio/projects/ProjectCollectionProjects.vue create mode 100644 src/views/portfolio/projects/ProjectListView.vue diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index a9dba28db..e9d06b5b0 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -362,6 +362,7 @@ "close": "Close", "code_not_present": "Code not present", "code_not_reachable": "Code not reachable", + "collection_projects": "Collection Projects", "collectionLogic": "Project Collection Logic", "collection_indicator_tooltip": "Collection project with values calculated based on children.", "comment": "Comment", diff --git a/src/plugins/table.js b/src/plugins/table.js index bd3b95274..d7476ad30 100644 --- a/src/plugins/table.js +++ b/src/plugins/table.js @@ -6,6 +6,7 @@ import 'bootstrap'; import 'jquery-treegrid/js/jquery.treegrid.min.js'; import 'bootstrap-table/dist/bootstrap-table.js'; import 'bootstrap-table/dist/extensions/treegrid/bootstrap-table-treegrid.min.js'; +import 'bootstrap-table/dist/extensions/defer-url/bootstrap-table-defer-url.js'; import BootstrapTable from 'bootstrap-table/dist/bootstrap-table-vue.esm.js'; Vue.component('BootstrapTable', BootstrapTable); diff --git a/src/router/index.js b/src/router/index.js index 68b299c55..99d3c2806 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -10,7 +10,7 @@ const DefaultContainer = () => import('@/containers/DefaultContainer'); // Views const Dashboard = () => import('@/views/Dashboard'); -const ProjectList = () => import('@/views/portfolio/projects/ProjectList'); +const ProjectListView = () => import('@/views/portfolio/projects/ProjectListView'); const TagList = () => import('@/views/portfolio/tags/TagList.vue'); const ComponentSearch = () => import('@/views/portfolio/components/ComponentSearch'); @@ -132,7 +132,7 @@ function configRoutes() { { path: 'projects', name: 'Projects', - component: ProjectList, + component: ProjectListView, meta: { title: i18n.t('message.projects'), i18n: 'message.projects', @@ -146,6 +146,7 @@ function configRoutes() { alias: [ 'projects/:uuid/overview', 'projects/:uuid/components', + 'projects/:uuid/collectionprojects', 'projects/:uuid/services', 'projects/:uuid/dependencyGraph', 'projects/:uuid/findings', diff --git a/src/shared/common.js b/src/shared/common.js index fb294b253..03dc8d517 100644 --- a/src/shared/common.js +++ b/src/shared/common.js @@ -407,7 +407,11 @@ $common.componentClassifierLabelFormatter = (i18n) => { */ $common.componentClassifierLabelProjectUrlFormatter = (i18n) => { return function (value) { - let url = '../projects/?classifier=' + value; + // if column defines a routerFunc returning the router we use a more robust solution + let url = !this.routerFunc ? + '../projects/?classifier=' + value : + this.routerFunc().resolve({name: 'Projects', query: {'classifier': value}}).href; + switch (value) { case 'APPLICATION': case 'FRAMEWORK': diff --git a/src/views/portfolio/projects/Project.vue b/src/views/portfolio/projects/Project.vue index d68916b4f..aa416c6aa 100644 --- a/src/views/portfolio/projects/Project.vue +++ b/src/views/portfolio/projects/Project.vue @@ -71,6 +71,7 @@ {{ project.version }} + {{ $t('message.inactive').toUpperCase() }} @@ -223,7 +224,7 @@ style="border-left: 0; border-right: 0; border-top: 0" /> - + - + + + + + - +