diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 2ea4dea73..ac6df07e0 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -105,6 +105,7 @@ "property_deleted": "Property deleted", "create_project": "Create Project", "show_inactive_projects": "Show inactive projects", + "hierarchical_view": "Hierarchical view", "create_project_property": "Create Project Property", "group_name": "Group Name", "property_name": "Property Name", @@ -232,6 +233,7 @@ "snapshot_notification": "Snapshot Notification", "select_project": "Select Project", "select_tag": "Select Tag", + "parent": "Parent", "select": "Select", "identity": "Identity", "extended": "Extended", @@ -372,7 +374,8 @@ "component_device": "Device", "component_firmware": "Firmware", "component_file": "File", - "dates": "Dates" + "dates": "Dates", + "inactive_active_children": "The project cannot be set to inactive if it has active children" }, "admin": { "configuration": "Configuration", diff --git a/src/views/portfolio/projects/Project.vue b/src/views/portfolio/projects/Project.vue index b5e0c6b75..384fc0889 100644 --- a/src/views/portfolio/projects/Project.vue +++ b/src/views/portfolio/projects/Project.vue @@ -125,7 +125,7 @@ - + @@ -244,8 +244,6 @@ }, beforeMount() { this.uuid = this.$route.params.uuid; - }, - mounted() { this.initialize(); }, watch:{ diff --git a/src/views/portfolio/projects/ProjectCreateProjectModal.vue b/src/views/portfolio/projects/ProjectCreateProjectModal.vue index 5cafff126..0191397c2 100644 --- a/src/views/portfolio/projects/ProjectCreateProjectModal.vue +++ b/src/views/portfolio/projects/ProjectCreateProjectModal.vue @@ -14,6 +14,9 @@ + tagsNode.push({name: tag.text})); this.axios.put(url, { name: this.project.name, @@ -156,6 +168,7 @@ group: this.project.group, description: this.project.description, //license: this.selectedLicense, + parent: parent, classifier: this.project.classifier, purl: this.project.purl, cpe: this.project.cpe, @@ -186,10 +199,31 @@ this.$toastr.w(this.$t('condition.unsuccessful_action')); }); }, + retrieveParents: function() { + let url = `${this.$api.BASE_URL}/${this.$api.URL_PROJECT}`; + this.axios.get(url).then((response) => { + for (let i = 0; i < response.data.length; i++) { + let project = response.data[i]; + if (project.version) { + this.availableParents.push({value: project.uuid, text: project.name + ' : ' + project.version}); + } else { + this.availableParents.push({value: project.uuid, text: project.name}); + } + if (this.project.parent && this.project.parent.uuid === project.uuid ) { + this.selectedParent = project.uuid; + } + } + }).catch((error) => { + this.$toastr.w(this.$t('condition.unsuccessful_action')); + }); + }, resetValues: function () { this.project = {}; this.tag = ""; this.tags = []; + this.selectedParent = null; + this.availableParents = [{ value: null, text: ''}] + this.retrieveParents(); } } } diff --git a/src/views/portfolio/projects/ProjectDetailsModal.vue b/src/views/portfolio/projects/ProjectDetailsModal.vue index a8137812d..a4a7a4b20 100644 --- a/src/views/portfolio/projects/ProjectDetailsModal.vue +++ b/src/views/portfolio/projects/ProjectDetailsModal.vue @@ -17,6 +17,9 @@ v-model="project.classifier" :options="availableClassifiers" :label="$t('message.classifier')" :tooltip="$t('message.component_classifier_desc')" :readonly="this.isNotPermitted(PERMISSIONS.PORTFOLIO_MANAGEMENT)" /> + - {{$t('message.active')}} + {{$t('message.active')}}

{ this.$toastr.w(this.$t('condition.unsuccessful_action')); }); + }, + retrieveParents: function() { + let url = `${this.$api.BASE_URL}/${this.$api.URL_PROJECT}/withoutDescendantsOf/${this.$props.uuid}`; + this.axios.get(url).then((response) => { + for (let i = 0; i < response.data.length; i++) { + let project = response.data[i]; + if (project.uuid !== this.project.uuid){ + if (project.version){ + this.availableParents.push({value: project.uuid, text: project.name + ' : ' + project.version}); + } else { + this.availableParents.push({value: project.uuid, text: project.name}); + } + } + if (this.project.parent && this.project.parent.uuid === project.uuid ) { + this.selectedParent = project.uuid; + } + } + }).catch((error) => { + this.$toastr.w(this.$t('condition.unsuccessful_action')); + }); + }, + hasActiveChild: function (project) { + let bool = false; + if (project.children){ + for (const child of project.children){ + if (child.active || bool){ + return true; + } else { + bool = this.hasActiveChild(child); + } + } + } + return bool; } } } diff --git a/src/views/portfolio/projects/ProjectList.vue b/src/views/portfolio/projects/ProjectList.vue index a9b58f383..65a2a1b32 100644 --- a/src/views/portfolio/projects/ProjectList.vue +++ b/src/views/portfolio/projects/ProjectList.vue @@ -6,8 +6,9 @@ {{ $t('message.create_project') }} {{ $t('message.show_inactive_projects') }} + {{ $t('message.hierarchical_view') }} - + `, + mixins: [permissionsMixin, bootstrapTableMixin], + methods: { + apiUrl: function (uuid) { + let url = `${api.BASE_URL}/${api.URL_PROJECT}/${uuid}/children`; + let tag = route.query.tag; + if (tag) { + url += "/tag/" + encodeURIComponent(tag); + } + let classifier = route.query.classifier; + if (classifier) { + url += "/classifier/" + encodeURIComponent(classifier); + } + if (showInactiveProjects === undefined) { + url += "?excludeInactive=true"; + } else { + url += "?excludeInactive=" + !showInactiveProjects; + } + return url; + }, + }, + data(){ + return{ + columns: columns, + data: [], + options: { + url: this.apiUrl(row.uuid), + sidePagination: 'server', + queryParamsType: 'pageSize', + detailView: true, + detailFilter: detailFilter, + detailFormatter: (index, row) => { + return this.vueFormatter(vueFormatterObject(index, row)); + }, + onExpandRow: this.vueFormatterInit + } + } + } + + } + } + + function detailFilter(index, row){ + return (Object.prototype.hasOwnProperty.call(row, 'children') && row.children && row.children.some(child => child.active)); + } export default { - mixins: [permissionsMixin], + mixins: [permissionsMixin, bootstrapTableMixin], components: { cSwitch, ProjectCreateProjectModal, PortfolioWidgetRow }, + mounted() { + showInactiveProjects = this.showInactiveProjects; + api = this.$api; + route = this.$route; + columns = this.columns; + }, methods: { apiUrl: function () { - let url = `${this.$api.BASE_URL}/${this.$api.URL_PROJECT}`; + api = this.$api; + route = this.$route; + let url = `${api.BASE_URL}/${api.URL_PROJECT}`; let tag = this.$route.query.tag; if (tag) { url += "/tag/" + encodeURIComponent(tag); @@ -51,6 +116,11 @@ } else { url += "?excludeInactive=" + !this.showInactiveProjects; } + if (this.showHierarchy === undefined) { + url += "&onlyRoot=false" + } else { + url += "&onlyRoot=" + this.showHierarchy + } return url; }, refreshTable: function() { @@ -65,12 +135,18 @@ this.refreshTable(); }, showInactiveProjects() { + showInactiveProjects = !showInactiveProjects this.refreshTable(); + }, + showHierarchy(){ + this.options.detailView = !this.options.detailView; + this.options.url = this.apiUrl(); } }, data() { return { showInactiveProjects: false, + showHierarchy: false, labelIcon: { dataOn: '\u2713', dataOff: '\u2715' @@ -80,6 +156,8 @@ title: this.$t('message.project_name'), field: "name", sortable: true, + widthUnit: '%', + width: '15.23', formatter(value, row, index) { let url = xssFilters.uriInUnQuotedAttr("../projects/" + row.uuid); return `${xssFilters.inHTMLData(value)}`; @@ -89,6 +167,8 @@ title: this.$t('message.version'), field: "version", sortable: true, + widthUnit: '%', + width: '10,27', formatter(value, row, index) { return xssFilters.inHTMLData(common.valueWithDefault(value, "")); } @@ -97,12 +177,16 @@ title: this.$t('message.classifier'), field: "classifier", sortable: true, + widthUnit: '%', + width: '8.96', formatter: common.componentClassifierLabelProjectUrlFormatter(this), }, { title: this.$t('message.last_bom_import'), field: "lastBomImport", sortable: true, + widthUnit: '%', + width: '14.71', formatter(timestamp, row, index) { return typeof timestamp === "number" ? common.formatTimestamp(timestamp, true) @@ -112,12 +196,16 @@ { title: this.$t('message.bom_format'), field: "lastBomImportFormat", - sortable: true + sortable: true, + widthUnit: '%', + width: '11.14', }, { title: this.$t('message.risk_score'), field: "lastInheritedRiskScore", - sortable: true + sortable: true, + widthUnit: '%', + width: '9.75', }, { title: this.$t('message.active'), @@ -126,11 +214,15 @@ return value === true ? '' : ""; }, align: "center", - sortable: true + sortable: true, + widthUnit: '%', + width: '7.40', }, { title: this.$t('message.policy_violations'), field: "metrics", + widthUnit: '%', + width: '11.84', formatter: function (metrics) { if (typeof metrics === "undefined") { return "-"; // No vulnerability info available @@ -150,6 +242,8 @@ title: this.$t('message.vulnerabilities'), field: "metrics", sortable: false, + widthUnit: '%', + width: '10.70', formatter(metrics, row, index) { if (typeof metrics === "undefined") { return "-"; // No vulnerability info available @@ -173,6 +267,12 @@ ], data: [], options: { + detailView: false, + detailFilter: detailFilter, + detailFormatter: (index, row) => { + return this.vueFormatter(vueFormatterObject(index, row)) + }, + onExpandRow: this.vueFormatterInit, search: true, showColumns: true, showRefresh: true, @@ -182,6 +282,7 @@ queryParamsType: 'pageSize', pageList: '[10, 25, 50, 100]', pageSize: 10, + pageNumber: 1, icons: { refresh: 'fa-refresh' },