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'
},