Skip to content

Commit

Permalink
* Fixed some routing bugs
Browse files Browse the repository at this point in the history
* Fixed double data loading for all tables in Project tabs (data was loaded twice on each view opening for each table, which decreased performance)
* 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 <[email protected]>
  • Loading branch information
rkg-mm committed Dec 2, 2023
1 parent ac7c288 commit 9c6d94c
Show file tree
Hide file tree
Showing 13 changed files with 143 additions and 38 deletions.
1 change: 1 addition & 0 deletions src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"home": "Home",
"dashboard": "Dashboard",
"projects": "Projects",
"collection_projects": "Collection Projects",
"components": "Components",
"component_search": "Component Search",
"component": "Component",
Expand Down
1 change: 1 addition & 0 deletions src/plugins/table.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
6 changes: 3 additions & 3 deletions src/router/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ComponentSearch = () => import('@/views/portfolio/components/ComponentSearch');
const VulnerabilityList = () => import('@/views/portfolio/vulnerabilities/VulnerabilityList');
const LicenseList = () => import('@/views/portfolio/licenses/LicenseList');
Expand Down Expand Up @@ -95,7 +95,7 @@ function configRoutes() {
{
path: 'projects',
name: 'Projects',
component: ProjectList,
component: ProjectListView,
meta: {
title: i18n.t('message.projects'),
i18n: 'message.projects',
Expand All @@ -106,7 +106,7 @@ function configRoutes() {
{
path: 'projects/:uuid',
name: 'Project',
alias: ['projects/:uuid/overview', 'projects/:uuid/components', 'projects/:uuid/services', 'projects/:uuid/dependencyGraph', 'projects/:uuid/findings', 'projects/:uuid/epss', 'projects/:uuid/policyViolations'],
alias: ['projects/:uuid/overview', 'projects/:uuid/components', 'projects/:uuid/collectionprojects', 'projects/:uuid/services', 'projects/:uuid/dependencyGraph', 'projects/:uuid/findings', 'projects/:uuid/epss', 'projects/:uuid/policyViolations'],
props: (route) => ( { uuid: route.params.uuid } ),
component: Project,
meta: {
Expand Down
6 changes: 5 additions & 1 deletion src/shared/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,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':
Expand Down
61 changes: 54 additions & 7 deletions src/views/portfolio/projects/Project.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@
<a href="#" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><i class="fa fa-caret-down" aria-hidden="true" style="padding-left:10px; padding-right:10px; padding-top:3px; padding-bottom:3px;"></i></a>
<ul class="dropdown-menu">
<span v-for="projectVersion in project.versions">
<b-dropdown-item :to="projectVersion.uuid">{{ projectVersion.version }}</b-dropdown-item>
<b-dropdown-item :to="{name: 'Project', params: {'uuid': projectVersion.uuid}}">{{ projectVersion.version }}</b-dropdown-item>
</span>
</ul>
</li>
</ol>
{{ project.version }}
<i v-if="isCollectionProject()" class="fa fa-calculator fa-fw collectionlogic-icon" v-b-tooltip.hover="{title: getCollectionLogicText(project)}"></i>
</b-col>
<b-col class="d-none d-md-flex">
<span class="text-muted font-xs font-italic align-text-top text-truncate" style="max-width: 100ch;" v-b-tooltip.hover="{title: project.description}">{{ project.description }}</span>
Expand Down Expand Up @@ -118,27 +119,31 @@
<template v-slot:title><i class="fa fa-line-chart"></i> {{ $t('message.overview') }}</template>
<project-dashboard :key="this.uuid" :uuid="this.uuid" :project="this.project" style="border-left: 0; border-right:0; border-top:0 "/>
</b-tab>
<b-tab ref="components" @click="routeTo('components')">
<b-tab ref="components" @click="routeTo('components')" v-if="isShowComponents()">
<template v-slot:title><i class="fa fa-cubes"></i> {{ $t('message.components') }} <b-badge variant="tab-total">{{ totalComponents }}</b-badge></template>
<project-components :key="this.uuid" :uuid="this.uuid" :project="this.project" v-on:total="totalComponents = $event" />
</b-tab>
<b-tab ref="services" @click="routeTo('services')">
<b-tab ref="collectionprojects" @click="routeTo('collectionprojects')" v-if="isShowCollectionProjects()" lazy>
<template v-slot:title><i class="fa fa-sitemap"></i> {{ $t('message.collection_projects') }}</template>
<project-collection-projects :key="this.uuid" :uuid="this.uuid" :project="this.project"/>
</b-tab>
<b-tab ref="services" @click="routeTo('services')" v-if="isShowServices()">
<template v-slot:title><i class="fa fa-exchange"></i> {{ $t('message.services') }} <b-badge variant="tab-total">{{ totalServices }}</b-badge></template>
<project-services :key="this.uuid" :uuid="this.uuid" v-on:total="totalServices = $event" />
</b-tab>
<b-tab ref="dependencygraph" @click="routeTo('dependencyGraph')">
<b-tab ref="dependencygraph" @click="routeTo('dependencyGraph')" v-if="isShowDependencyGraph()">
<template v-slot:title><i class="fa fa-sitemap"></i> {{ $t('message.dependency_graph') }} <b-badge variant="tab-total">{{ totalDependencyGraphs }}</b-badge></template>
<project-dependency-graph :key="this.uuid" :uuid="this.uuid" :project="this.project" v-on:total="totalDependencyGraphs = $event" />
</b-tab>
<b-tab ref="findings" v-if="isPermitted(PERMISSIONS.VIEW_VULNERABILITY)" @click="routeTo('findings')">
<b-tab ref="findings" v-if="isShowFindings()" @click="routeTo('findings')">
<template v-slot:title><i class="fa fa-tasks"></i> {{ $t('message.audit_vulnerabilities') }} <b-badge variant="tab-total">{{ totalFindings }}</b-badge></template>
<project-findings :key="this.uuid" :uuid="this.uuid" v-on:total="totalFindings = $event" />
</b-tab>
<b-tab ref="epss" v-if="isPermitted(PERMISSIONS.VIEW_VULNERABILITY)" @click="routeTo('epss')">
<b-tab ref="epss" v-if="isShowFindings()" @click="routeTo('epss')">
<template v-slot:title><i class="fa fa-tasks"></i> {{ $t('message.exploit_predictions') }} <b-badge variant="tab-total">{{ totalEpss }}</b-badge></template>
<project-epss :key="this.uuid" :uuid="this.uuid" v-on:total="totalEpss = $event" />
</b-tab>
<b-tab ref="policyviolations" v-if="isPermitted(PERMISSIONS.VIEW_POLICY_VIOLATION)" @click="routeTo('policyViolations')">
<b-tab ref="policyviolations" v-if="isShowPolicyViolations()" @click="routeTo('policyViolations')">
<template v-slot:title><i class="fa fa-fire"></i> {{ $t('message.policy_violations') }} <b-badge variant="tab-total">{{ totalViolations }}</b-badge></template>
<project-policy-violations :key="this.uuid" :uuid="this.uuid" v-on:total="totalViolations = $event" />
</b-tab>
Expand All @@ -156,6 +161,7 @@
import { getStyle } from '@coreui/coreui/dist/js/coreui-utilities'
import VueEasyPieChart from 'vue-easy-pie-chart'
import ProjectComponents from "./ProjectComponents";
import ProjectCollectionProjects from "./ProjectCollectionProjects";
import ProjectDependencyGraph from "./ProjectDependencyGraph";
import ProjectServices from "./ProjectServices";
import PortfolioWidgetRow from "../../dashboard/PortfolioWidgetRow";
Expand All @@ -171,6 +177,7 @@
import ProjectPolicyViolations from "./ProjectPolicyViolations";
import ProjectEpss from "./ProjectEpss";
import ExternalReferencesDropdown from "../../components/ExternalReferencesDropdown.vue";
import xssFilters from "xss-filters";
export default {
mixins: [permissionsMixin],
Expand All @@ -182,6 +189,7 @@
ProjectPropertiesModal,
ProjectDetailsModal,
ProjectComponents,
ProjectCollectionProjects,
ProjectDependencyGraph,
ProjectServices,
SeverityBarChart,
Expand Down Expand Up @@ -277,6 +285,45 @@
let pattern = new RegExp("/projects\\/" + this.uuid + "\\/([^\\/]*)", "gi");
let tab = pattern.exec(this.$route.fullPath.toLowerCase());
return this.$refs[(tab && tab[1]) ? tab[1].toLowerCase() : 'overview'];
},
getCollectionLogicText: function(project) {
let title = 'Metrics of collection project are calculated '
switch (project.collectionLogic) {
case 'NONE':
return '';
case 'AGGREGATE_DIRECT_CHILDREN':
title += 'by aggregating numbers of all direct children.';
break;
case 'AGGREGATE_DIRECT_CHILDREN_WITH_TAG':
const tag = !project.collectionTag ? '' : xssFilters.inDoubleQuotedAttr(project.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;
}
return title;
},
isCollectionProject: function() {
return this.project.collectionLogic !== 'NONE';
},
isShowComponents: function() {
return !this.isCollectionProject();
},
isShowCollectionProjects: function() {
return this.isCollectionProject();
},
isShowServices: function() {
return !this.isCollectionProject();
},
isShowDependencyGraph: function() {
return !this.isCollectionProject();
},
isShowFindings: function() {
return !this.isCollectionProject() && this.isPermitted(this.PERMISSIONS.VIEW_VULNERABILITY)
},
isShowPolicyViolations: function() {
return !this.isCollectionProject() && this.isPermitted(this.PERMISSIONS.VIEW_POLICY_VIOLATION)
}
},
beforeMount() {
Expand Down
17 changes: 17 additions & 0 deletions src/views/portfolio/projects/ProjectCollectionProjects.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<template>
<project-list :parentProject="this.project" :uuid="this.uuid"></project-list>
</template>

<script>
import ProjectList from "@/views/portfolio/projects/ProjectList.vue";
export default {
components: {
ProjectList
},
props: {
uuid: String,
project: Object,
}
};
</script>
8 changes: 5 additions & 3 deletions src/views/portfolio/projects/ProjectComponents.vue
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,10 @@ import SeverityProgressBar from "../../components/SeverityProgressBar";
],
data: [],
options: {
onPostBody: this.initializeTooltips,
onPostBody: () => {
this.initializeTooltips();
loadUserPreferencesForBootstrapTable(this, "ProjectComponents", this.$refs.table.columns);
},
search: true,
showColumns: true,
showRefresh: true,
Expand All @@ -215,7 +218,7 @@ import SeverityProgressBar from "../../components/SeverityProgressBar";
res.total = xhr.getResponseHeader("X-Total-Count");
return res;
},
url: this.apiUrl(),
deferUrl: this.apiUrl(), // use deferUrl to prevent double loading of data after initializing columns
onPageChange: ((number, size) => {
if (localStorage) {
localStorage.setItem("ProjectComponentsPageSize", size.toString())
Expand Down Expand Up @@ -285,7 +288,6 @@ import SeverityProgressBar from "../../components/SeverityProgressBar";
});
},
tableLoaded: function(data) {
loadUserPreferencesForBootstrapTable(this, "ProjectComponents", this.$refs.table.columns);
if (data && Object.prototype.hasOwnProperty.call(data, "total")) {
this.$emit('total', data.total);
} else {
Expand Down
8 changes: 5 additions & 3 deletions src/views/portfolio/projects/ProjectEpss.vue
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,11 @@ export default {
res.total = xhr.getResponseHeader("X-Total-Count");
return res;
},
url: this.apiUrl(),
onPostBody: this.initializeTooltips,
deferUrl: this.apiUrl(), // use deferUrl to prevent double loading of data after initializing columns
onPostBody: () => {
this.initializeTooltips()
loadUserPreferencesForBootstrapTable(this, "ProjectEpss", this.$refs.table.columns);
},
onPageChange: ((number, size) => {
if (localStorage) {
localStorage.setItem("ProjectEpssPageSize", size.toString())
Expand Down Expand Up @@ -188,7 +191,6 @@ export default {
});
},
tableLoaded: function(data) {
loadUserPreferencesForBootstrapTable(this, "ProjectEpss", this.$refs.table.columns);
this.$emit('total', data.total);
this.$refs.chartEpssVsCvss.render(data);
},
Expand Down
8 changes: 5 additions & 3 deletions src/views/portfolio/projects/ProjectFindings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,10 @@ import common from "../../../shared/common";
],
data: [],
options: {
onPostBody: this.initializeTooltips,
onPostBody: () => {
this.initializeTooltips()
loadUserPreferencesForBootstrapTable(this, "ProjectFindings", this.$refs.table.columns);
},
search: true,
showColumns: true,
showRefresh: true,
Expand Down Expand Up @@ -454,7 +457,7 @@ import common from "../../../shared/common";
res.total = xhr.getResponseHeader("X-Total-Count");
return res;
},
url: this.apiUrl(),
deferUrl: this.apiUrl(), // use deferUrl to prevent double loading of data after initializing columns
onPageChange: ((number, size) => {
if (localStorage) {
localStorage.setItem("ProjectFindingsPageSize", size.toString())
Expand Down Expand Up @@ -556,7 +559,6 @@ import common from "../../../shared/common";
});
},
tableLoaded: function(data) {
loadUserPreferencesForBootstrapTable(this, "ProjectFindings", this.$refs.table.columns);
this.$emit('total', data.total);
},
initializeTooltips: function () {
Expand Down
33 changes: 21 additions & 12 deletions src/views/portfolio/projects/ProjectList.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<template>
<div class="animated fadeIn" v-permission="'VIEW_PORTFOLIO'">
<portfolio-widget-row :fetch="true" />
<div>
<div id="projectsToolbar" class="bs-table-custom-toolbar">
<b-button size="md" variant="outline-primary" @click="initializeProjectCreateProjectModal" v-permission="PERMISSIONS.PORTFOLIO_MANAGEMENT">
<span class="fa fa-plus"></span> {{ $t('message.create_project') }}
Expand All @@ -13,7 +12,6 @@
:columns="columns"
:data="data"
:options="options"
@on-load-success="onLoadSuccess"
@on-pre-body="onPreBody"
@on-post-body="onPostBody">
</bootstrap-table>
Expand All @@ -22,7 +20,7 @@
</template>

<script>
import { loadUserPreferencesForBootstrapTable } from "@/shared/utils";
import { loadUserPreferencesForBootstrapTable } from "@/shared/utils";
import { Switch as cSwitch } from '@coreui/vue';
import MurmurHash2 from "imurmurhash";
import Vue from 'vue';
Expand All @@ -42,6 +40,13 @@ import ProjectCreateProjectModal from "./ProjectCreateProjectModal";
ProjectCreateProjectModal,
PortfolioWidgetRow
},
props: {
/**
* If only children from a specific project shall be shown this must be set to the corresponding project
*/
parentProject: Object,
uuid: String
},
beforeCreate() {
this.showInactiveProjects = (localStorage && localStorage.getItem("ProjectListShowInactiveProjects") !== null) ? (localStorage.getItem("ProjectListShowInactiveProjects") === "true") : false;
this.showFlatView = (localStorage && localStorage.getItem("ProjectListShowFlatView") !== null) ? (localStorage.getItem("ProjectListShowFlatView") === "true") : false;
Expand All @@ -51,6 +56,11 @@ import ProjectCreateProjectModal from "./ProjectCreateProjectModal";
this.$root.$emit("initializeProjectCreateProjectModal");
},
apiUrl: function (uuid) {
// if we only want to show children of a specific parent we force the base call to fetch its children
if(this.uuid && !uuid) {
uuid = this.uuid;
}
let url = `${this.$api.BASE_URL}/${this.$api.URL_PROJECT}`;
if (uuid) {
url += `/${uuid}/children`;
Expand Down Expand Up @@ -86,9 +96,6 @@ import ProjectCreateProjectModal from "./ProjectCreateProjectModal";
pageNumber: 1
});
},
onLoadSuccess: function () {
loadUserPreferencesForBootstrapTable(this, "ProjectList", this.$refs.table.columns);
},
onPreBody: function () {
this.$refs.table.getData().forEach(project => {
project.id = MurmurHash2(project.uuid).result();
Expand Down Expand Up @@ -120,6 +127,7 @@ import ProjectCreateProjectModal from "./ProjectCreateProjectModal";
}
})
}
loadUserPreferencesForBootstrapTable(this, "ProjectList", this.$refs.table.columns);
this.$refs.table.hideLoading();
},
getChildren: async function (project) {
Expand All @@ -138,9 +146,6 @@ import ProjectCreateProjectModal from "./ProjectCreateProjectModal";
}
},
watch: {
$route(to, from) {
this.refreshTable();
},
showInactiveProjects() {
if (localStorage) {
localStorage.setItem("ProjectListShowInactiveProjects", this.showInactiveProjects.toString());
Expand Down Expand Up @@ -175,8 +180,11 @@ import ProjectCreateProjectModal from "./ProjectCreateProjectModal";
title: this.$t('message.project_name'),
field: "name",
sortable: true,
routerFunc: () => this.$router,
formatter(value, row, index) {
let url = xssFilters.uriInUnQuotedAttr("../projects/" + row.uuid);
let url = xssFilters.uriInUnQuotedAttr(
this.routerFunc().resolve({name: 'Project', params: {'uuid': row.uuid}}).href
);
let collectionIcon = '';
if(row.collectionLogic !== 'NONE') {
let title = 'Metrics of collection project are calculated '
Expand Down Expand Up @@ -230,6 +238,7 @@ import ProjectCreateProjectModal from "./ProjectCreateProjectModal";
title: this.$t('message.classifier'),
field: "classifier",
sortable: true,
routerFunc: () => this.$router, // needed by formatter
formatter: common.componentClassifierLabelProjectUrlFormatter(this),
},
{
Expand Down Expand Up @@ -331,7 +340,7 @@ import ProjectCreateProjectModal from "./ProjectCreateProjectModal";
res.total = xhr.getResponseHeader("X-Total-Count");
return res;
},
url: this.apiUrl(),
deferUrl: this.apiUrl(), // use deferUrl to prevent double loading of data after initializing columns
// onClickRow is used instead of a tree node's onExpand event, because onExpand does not pass any arguments and therefore makes it complicated to retrieve a row's data which is needed for fetching its children and appending the data
onClickRow: ((row, $element) => {
if (!this.showFlatView && !this.isSearching) {
Expand Down
Loading

0 comments on commit 9c6d94c

Please sign in to comment.