diff --git a/web/api/js/codechecker-api-node/dist/codechecker-api-6.42.0.tgz b/web/api/js/codechecker-api-node/dist/codechecker-api-6.42.0.tgz deleted file mode 100644 index 2e8422d6f6..0000000000 Binary files a/web/api/js/codechecker-api-node/dist/codechecker-api-6.42.0.tgz and /dev/null differ diff --git a/web/api/js/codechecker-api-node/dist/codechecker-api-6.43.0.tgz b/web/api/js/codechecker-api-node/dist/codechecker-api-6.43.0.tgz new file mode 100644 index 0000000000..a7d04ace42 Binary files /dev/null and b/web/api/js/codechecker-api-node/dist/codechecker-api-6.43.0.tgz differ diff --git a/web/api/js/codechecker-api-node/package.json b/web/api/js/codechecker-api-node/package.json index 125e16f872..f2cd12486c 100644 --- a/web/api/js/codechecker-api-node/package.json +++ b/web/api/js/codechecker-api-node/package.json @@ -1,6 +1,6 @@ { "name": "codechecker-api", - "version": "6.42.0", + "version": "6.43.0", "description": "Generated node.js compatible API stubs for CodeChecker server.", "main": "lib", "homepage": "https://github.com/Ericsson/codechecker", diff --git a/web/api/py/codechecker_api/dist/codechecker_api.tar.gz b/web/api/py/codechecker_api/dist/codechecker_api.tar.gz index ea5713df64..d2fc49811d 100644 Binary files a/web/api/py/codechecker_api/dist/codechecker_api.tar.gz and b/web/api/py/codechecker_api/dist/codechecker_api.tar.gz differ diff --git a/web/api/py/codechecker_api/setup.py b/web/api/py/codechecker_api/setup.py index 0944da0799..e31fef3eb2 100644 --- a/web/api/py/codechecker_api/setup.py +++ b/web/api/py/codechecker_api/setup.py @@ -8,7 +8,7 @@ with open('README.md', encoding='utf-8', errors="ignore") as f: long_description = f.read() -api_version = '6.42.0' +api_version = '6.43.0' setup( name='codechecker_api', diff --git a/web/api/py/codechecker_api_shared/dist/codechecker_api_shared.tar.gz b/web/api/py/codechecker_api_shared/dist/codechecker_api_shared.tar.gz index adc9366d0d..461c57ba28 100644 Binary files a/web/api/py/codechecker_api_shared/dist/codechecker_api_shared.tar.gz and b/web/api/py/codechecker_api_shared/dist/codechecker_api_shared.tar.gz differ diff --git a/web/api/py/codechecker_api_shared/setup.py b/web/api/py/codechecker_api_shared/setup.py index afcf9be488..2cd6dedf94 100644 --- a/web/api/py/codechecker_api_shared/setup.py +++ b/web/api/py/codechecker_api_shared/setup.py @@ -8,7 +8,7 @@ with open('README.md', encoding='utf-8', errors="ignore") as f: long_description = f.read() -api_version = '6.42.0' +api_version = '6.43.0' setup( name='codechecker_api_shared', diff --git a/web/api/report_server.thrift b/web/api/report_server.thrift index deaa6a1f73..5d2df80d3d 100644 --- a/web/api/report_server.thrift +++ b/web/api/report_server.thrift @@ -288,6 +288,7 @@ struct ReportFilter { 16: optional ReportDate date, // Dates of the report. 17: optional list analyzerNames, // Names of the code analyzers. 18: optional i64 openReportsDate, // Open reports date in unix time format. + 19: optional list cleanupPlanNames, // Cleanup plan names. } struct RunReportCount { @@ -405,6 +406,22 @@ struct BlameInfo { 2: list blame, } +struct CleanupPlan { + 1: i64 id, + 2: string name, + 3: i64 dueDate, // Unix (epoch) time. + 4: string description, + 5: i64 closedAt, // Unix (epoch) time. + 6: list reportHashes, +} +typedef list CleanupPlans + +struct CleanupPlanFilter { + 1: list ids, + 2: list names, + 3: bool isOpen, +} + service codeCheckerDBAccess { // Gives back all analyzed runs. @@ -790,5 +807,60 @@ service codeCheckerDBAccess { // Import data from the server. // PERMISSION: PRODUCT_ADMIN bool importData(1: ExportData exportData) - throws (1: codechecker_api_shared.RequestFailed requestError), + throws (1: codechecker_api_shared.RequestFailed requestError), + + // Add a new cleanup plan. + // Returns the cleanup plan id if the cleanup plan was successfully created. + // PERMISSION: PRODUCT_ADMIN + i64 addCleanupPlan(1: string name, + 2: string description, + 3: i64 dueDate) + throws (1: codechecker_api_shared.RequestFailed requestError), + + // Update a cleanup plan. + // Returns 'true' if cleanup plan was successfully updated. + // PERMISSION: PRODUCT_ADMIN + bool updateCleanupPlan(1: i64 id, + 2: string name, + 3: string description, + 4: i64 dueDate) + throws (1: codechecker_api_shared.RequestFailed requestError), + + // Get cleanup plans. + // Returns a list of cleanup plans. + // PERMISSION: PRODUCT_VIEW + CleanupPlans getCleanupPlans(1: CleanupPlanFilter filter) + throws (1: codechecker_api_shared.RequestFailed requestError), + + // Remove a cleanup plan. + // Returns 'true' if cleanup plan was successfully removed. + // PERMISSION: PRODUCT_ADMIN + bool removeCleanupPlan(1: i64 cleanupPlanId) + throws (1: codechecker_api_shared.RequestFailed requestError), + + // Close a cleanup plan. + // Returns 'true' if cleanup plan was successfully closed. + // PERMISSION: PRODUCT_ADMIN + bool closeCleanupPlan(1: i64 cleanupPlanId) + throws (1: codechecker_api_shared.RequestFailed requestError), + + // Reopen a cleanup plan. + // Returns 'true' if cleanup plan was successfully reopened. + // PERMISSION: PRODUCT_ADMIN + bool reopenCleanupPlan(1: i64 cleanupPlanId) + throws (1: codechecker_api_shared.RequestFailed requestError), + + // Add report hashes to the given cleanup plan. + // Returns 'true' if report hashes are set for the given cleanup plan. + // PERMISSION: PRODUCT_ADMIN + bool setCleanupPlan(1: i64 cleanupPlanId, + 2: list reportHashes) + throws (1: codechecker_api_shared.RequestFailed requestError), + + // Remove report hashes from the given cleanup plan. + // Returns 'true' if report hashes are removed from the given cleanup plan. + // PERMISSION: PRODUCT_ADMIN + bool unsetCleanupPlan(1: i64 cleanupPlanId, + 2: list reportHashes) + throws (1: codechecker_api_shared.RequestFailed requestError), } diff --git a/web/codechecker_web/shared/version.py b/web/codechecker_web/shared/version.py index 86f4bd4287..cd0025da34 100644 --- a/web/codechecker_web/shared/version.py +++ b/web/codechecker_web/shared/version.py @@ -18,7 +18,7 @@ # The newest supported minor version (value) for each supported major version # (key) in this particular build. SUPPORTED_VERSIONS = { - 6: 42 + 6: 43 } # Used by the client to automatically identify the latest major and minor diff --git a/web/server/codechecker_server/api/report_server.py b/web/server/codechecker_server/api/report_server.py index b1829f7bd3..2dbd57aa49 100644 --- a/web/server/codechecker_server/api/report_server.py +++ b/web/server/codechecker_server/api/report_server.py @@ -14,11 +14,12 @@ import os import re import shlex +import time import zlib from collections import defaultdict from datetime import datetime, timedelta -from typing import List, Optional +from typing import Dict, List, Optional import sqlalchemy from sqlalchemy.sql.expression import or_, and_, not_, func, \ @@ -46,10 +47,10 @@ from ..database.config_db_model import Product from ..database.database import conv, DBSession, escape_like from ..database.run_db_model import \ - AnalysisInfo, AnalyzerStatistic, BugPathEvent, BugReportPoint, Comment, \ - ExtendedReportData, File, FileContent, Report, ReportAnalysisInfo, \ - ReviewStatus, Run, RunHistory, RunHistoryAnalysisInfo, RunLock, \ - SourceComponent + AnalysisInfo, AnalyzerStatistic, BugPathEvent, BugReportPoint, \ + CleanupPlan, CleanupPlanReportHash, Comment, ExtendedReportData, File, \ + FileContent, Report, ReportAnalysisInfo, ReviewStatus, Run, RunHistory, \ + RunHistoryAnalysisInfo, RunLock, SourceComponent from .thrift_enum_helper import detection_status_enum, \ detection_status_str, review_status_enum, review_status_str, \ @@ -241,6 +242,22 @@ def process_report_filter(session, run_ids, report_filter, cmp_data=None): AND.append(or_(*OR)) + if report_filter.cleanupPlanNames: + OR = [] + for cleanup_plan_name in report_filter.cleanupPlanNames: + q = select([CleanupPlanReportHash.bug_hash]) \ + .where( + CleanupPlanReportHash.cleanup_plan_id.in_( + select([CleanupPlan.id]) + .where(CleanupPlan.name == cleanup_plan_name) + .distinct() + )) \ + .distinct() + + OR.append(Report.bug_id.in_(q)) + + AND.append(or_(*OR)) + if report_filter.severity: AND.append(Report.severity.in_(report_filter.severity)) @@ -966,6 +983,41 @@ def get_commit_url( return url +def get_cleanup_plan(session, cleanup_plan_id: int) -> CleanupPlan: + """ + Check if the given cleanup id exists in the database and returns + the cleanup. Otherwise it will raise an exception. + """ + cleanup_plan = session.query(CleanupPlan).get(cleanup_plan_id) + + if not cleanup_plan: + raise codechecker_api_shared.ttypes.RequestFailed( + codechecker_api_shared.ttypes.ErrorCode.DATABASE, + f"Cleanup plan '{cleanup_plan_id}' was not found in the database.") + + return cleanup_plan + + +def get_cleanup_plan_report_hashes( + session, + cleanup_plan_ids: List[int] +) -> Dict[int, List[str]]: + """ Get report hashes for the given cleanup plan ids. """ + cleanup_plan_hashes = defaultdict(list) + + q = session \ + .query( + CleanupPlanReportHash.cleanup_plan_id, + CleanupPlanReportHash.bug_hash) \ + .filter(CleanupPlanReportHash.cleanup_plan_id.in_( + cleanup_plan_ids)) + + for cleanup_plan_id, report_hash in q: + cleanup_plan_hashes[cleanup_plan_id].append(report_hash) + + return cleanup_plan_hashes + + class ThriftRequestHandler: """ Connect to database and handle thrift client requests. @@ -2977,3 +3029,181 @@ def importData(self, exportData): session.commit() return True + + @exc_to_thrift_reqfail + @timeit + def addCleanupPlan(self, name, description, dueDate): + self.__require_admin() + + with DBSession(self._Session) as session: + cleanup_plan = session.query(CleanupPlan) \ + .filter(CleanupPlan.name == name) \ + .one_or_none() + + if cleanup_plan: + raise codechecker_api_shared.ttypes.RequestFailed( + codechecker_api_shared.ttypes.ErrorCode.DATABASE, + f"Cleanup plan '{name}' already exists.") + + cleanup_plan = CleanupPlan(name) + cleanup_plan.description = description + cleanup_plan.due_date = \ + datetime.fromtimestamp(dueDate) if dueDate else None + + session.add(cleanup_plan) + session.commit() + + LOG.info("New cleanup plan '%s' has been created by '%s'", + name, self._get_username()) + + return cleanup_plan.id + + @exc_to_thrift_reqfail + @timeit + def updateCleanupPlan(self, cleanup_plan_id, name, description, dueDate): + self.__require_admin() + + with DBSession(self._Session) as session: + cleanup_plan = get_cleanup_plan(session, cleanup_plan_id) + cleanup_plan.name = name + cleanup_plan.description = description + cleanup_plan.due_date = \ + datetime.fromtimestamp(dueDate) if dueDate else None + + session.add(cleanup_plan) + session.commit() + + LOG.info("Cleanup plan '%d' has been updated by '%s'", + cleanup_plan_id, self._get_username()) + + return True + + @exc_to_thrift_reqfail + @timeit + def getCleanupPlans(self, cleanup_plan_filter): + self.__require_view() + with DBSession(self._Session) as session: + q = session \ + .query(CleanupPlan) \ + .order_by(CleanupPlan.name) + + if cleanup_plan_filter: + if cleanup_plan_filter.ids: + q = q.filter(CleanupPlan.id.in_( + cleanup_plan_filter.ids)) + + if cleanup_plan_filter.names: + q = q.filter(CleanupPlan.name.in_( + cleanup_plan_filter.names)) + + if cleanup_plan_filter.isOpen is not None: + if cleanup_plan_filter.isOpen: + q = q.filter(CleanupPlan.closed_at.is_(None)) + else: + q = q.filter(CleanupPlan.closed_at.isnot(None)) + + cleanup_plans = q.all() + + cleanup_plan_hashes = get_cleanup_plan_report_hashes( + session, [c.id for c in cleanup_plans]) + + return [ttypes.CleanupPlan( + id=cp.id, + name=cp.name, + description=cp.description, + dueDate=int(time.mktime( + cp.due_date.timetuple())) if cp.due_date else None, + closedAt=int(time.mktime( + cp.closed_at.timetuple())) if cp.closed_at else None, + reportHashes=cleanup_plan_hashes[cp.id]) for cp in q] + + @exc_to_thrift_reqfail + @timeit + def removeCleanupPlan(self, cleanup_plan_id): + self.__require_admin() + with DBSession(self._Session) as session: + cleanup_plan = get_cleanup_plan(session, cleanup_plan_id) + name = cleanup_plan.name + + session.delete(cleanup_plan) + session.commit() + + LOG.info("Cleanup plan '%s' has been removed by '%s'", + name, self._get_username()) + + return True + + @exc_to_thrift_reqfail + @timeit + def closeCleanupPlan(self, cleanup_plan_id): + self.__require_admin() + + with DBSession(self._Session) as session: + cleanup_plan = get_cleanup_plan(session, cleanup_plan_id) + + cleanup_plan.closed_at = datetime.now() + session.add(cleanup_plan) + session.commit() + + LOG.info("Cleanup plan '%s' has been closed by '%s'", + cleanup_plan.name, self._get_username()) + + return True + + @exc_to_thrift_reqfail + @timeit + def reopenCleanupPlan(self, cleanup_plan_id): + self.__require_admin() + + with DBSession(self._Session) as session: + cleanup_plan = get_cleanup_plan(session, cleanup_plan_id) + + cleanup_plan.closed_at = None + session.add(cleanup_plan) + session.commit() + LOG.info("Cleanup plan '%s' has been reopened by '%s'", + cleanup_plan.name, self._get_username()) + return True + + @exc_to_thrift_reqfail + @timeit + def setCleanupPlan(self, cleanup_plan_id, reportHashes): + self.__require_admin() + + with DBSession(self._Session) as session: + cleanup_plan = get_cleanup_plan(session, cleanup_plan_id) + + q = session \ + .query(CleanupPlanReportHash.bug_hash) \ + .filter( + CleanupPlanReportHash.cleanup_plan_id == cleanup_plan.id) \ + .filter(CleanupPlanReportHash.bug_hash.in_(reportHashes)) + new_report_hashes = set(reportHashes) - set(b[0] for b in q) + + for report_hash in new_report_hashes: + session.add(CleanupPlanReportHash( + cleanup_plan_id=cleanup_plan.id, bug_hash=report_hash)) + + session.commit() + + return True + + @exc_to_thrift_reqfail + @timeit + def unsetCleanupPlan(self, cleanup_plan_id, reportHashes): + self.__require_admin() + + with DBSession(self._Session) as session: + cleanup_plan = get_cleanup_plan(session, cleanup_plan_id) + + session \ + .query(CleanupPlanReportHash) \ + .filter( + CleanupPlanReportHash.cleanup_plan_id == cleanup_plan.id) \ + .filter(CleanupPlanReportHash.bug_hash.in_(reportHashes)) \ + .delete(synchronize_session=False) + + session.commit() + session.close() + + return True diff --git a/web/server/codechecker_server/database/run_db_model.py b/web/server/codechecker_server/database/run_db_model.py index 1791f28051..5fe04642d4 100644 --- a/web/server/codechecker_server/database/run_db_model.py +++ b/web/server/codechecker_server/database/run_db_model.py @@ -456,6 +456,40 @@ def __init__(self, name, value, description=None, user_name=None): self.username = user_name +class CleanupPlan(Base): + __tablename__ = 'cleanup_plans' + + __table_args__ = ( + UniqueConstraint('name'), + ) + + id = Column(Integer, autoincrement=True, primary_key=True) + name = Column(String, nullable=False) + due_date = Column(DateTime, nullable=True) + description = Column(String, nullable=True) + closed_at = Column(DateTime, nullable=True) + + def __init__(self, name, due_date=None, description=None, closed_at=None): + self.name = name + self.due_date = due_date + self.description = description + self.closed_at = closed_at + + +class CleanupPlanReportHash(Base): + __tablename__ = 'cleanup_plan_report_hashes' + + cleanup_plan_id = Column( + Integer, + ForeignKey('cleanup_plans.id', + deferrable=True, + initially="DEFERRED", + ondelete="CASCADE"), + index=True) + + bug_hash = Column(String, primary_key=True) + + IDENTIFIER = { 'identifier': "RunDatabase", 'orm_meta': CC_META diff --git a/web/server/codechecker_server/migrations/report/versions/fb356f0eefed_cleanup_plan.py b/web/server/codechecker_server/migrations/report/versions/fb356f0eefed_cleanup_plan.py new file mode 100644 index 0000000000..e54834c55e --- /dev/null +++ b/web/server/codechecker_server/migrations/report/versions/fb356f0eefed_cleanup_plan.py @@ -0,0 +1,58 @@ +"""Cleanup plan + +Revision ID: fb356f0eefed +Revises: ad2a567e513a +Create Date: 2021-09-06 10:55:43.093729 + +""" + +# revision identifiers, used by Alembic. +revision = 'fb356f0eefed' +down_revision = 'ad2a567e513a' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.create_table('cleanup_plans', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('due_date', sa.DateTime(), nullable=True), + sa.Column('description', sa.String(), nullable=True), + sa.Column('closed_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('pk_cleanup_plans')), + sa.UniqueConstraint('name', name=op.f('uq_cleanup_plans_name')) + ) + + op.create_table('cleanup_plan_report_hashes', + sa.Column('cleanup_plan_id', sa.Integer(), nullable=True), + sa.Column('bug_hash', sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ['cleanup_plan_id'], + ['cleanup_plans.id'], + name=op.f('fk_cleanup_plan_report_hashes_cleanup_plan_id_cleanup_plans'), + ondelete='CASCADE', + initially='DEFERRED', + deferrable=True), + sa.PrimaryKeyConstraint( + 'bug_hash', + name=op.f('pk_cleanup_plan_report_hashes')) + ) + + op.create_index( + op.f('ix_cleanup_plan_report_hashes_cleanup_plan_id'), + 'cleanup_plan_report_hashes', + ['cleanup_plan_id'], + unique=False) + + +def downgrade(): + op.drop_index( + op.f('ix_cleanup_plan_report_hashes_cleanup_plan_id'), + table_name='cleanup_plan_report_hashes') + + op.drop_table('cleanup_plan_report_hashes') + op.drop_table('cleanup_plans') diff --git a/web/server/vue-cli/e2e/pages/report.js b/web/server/vue-cli/e2e/pages/report.js index 0938c71c92..2b136326f4 100644 --- a/web/server/vue-cli/e2e/pages/report.js +++ b/web/server/vue-cli/e2e/pages/report.js @@ -122,7 +122,10 @@ module.exports = { progressBar: ".v-data-table__progress", clearAllFilterBtn: "#clear-all-filter-btn", uniqueReports: ".unique-filter .v-input--checkbox", + setCleanupPlanBtn: ".set-cleanup-plan-btn", expandBtn: "button.v-data-table__expand-icon", + selectReportCheckbox: "tbody .v-simple-checkbox", + selectAllReportCheckbox: "thead .v-simple-checkbox" }, sections: { baselineRunFilter: createRunFilterSection("#run"), @@ -204,6 +207,56 @@ module.exports = { cancelBtn: ".cancel-btn", } }, + cleanupPlanFilter: { + selector: "#cleanup-plan", + elements: { + expansionBtn: ".expansion-btn", + manageBtn: ".manage-cleanup-plan-btn", + settings: ".settings-btn", + clearBtn: ".clear-btn", + selectedItems: ".selected-item" + }, + commands: [ filterCommands ] + }, + cleanupPlanDialog: { + selector: ".manage-cleanup-plan-dialog.v-dialog--active", + elements: { + newCleanupPlanBtn: ".new-cleanup-plan-btn", + tableRows: ".v-data-table tbody tr", + emptyTable: ".v-data-table tbody .v-data-table__empty-wrapper", + closeBtn: ".v-card__title .close-btn", + editCleanupPlanBtn: ".v-data-table .edit-btn", + removeCleanupPlanBtn: ".v-data-table .remove-btn", + closeCleanupPlanBtn: ".v-data-table .close-btn", + reopenCleanupPlanBtn: ".v-data-table .reopen-btn", + openCleanupPlansTab: ".v-tabs-bar__content .v-tab:nth-child(2)", + closedCleanupPlansTab: ".v-tabs-bar__content .v-tab:nth-child(3)", + } + }, + newCleanupPlanDialog: { + selector: ".edit-cleanup-plan-dialog.v-dialog--active", + elements: { + name: ".cleanup-plan-name input[type='text']", + description: ".cleanup-plan-description textarea", + saveBtn: ".save-btn", + cancelBtn: ".cancel-btn", + } + }, + removeCleanupPlanDialog: { + selector: ".remove-cleanup-plan-dialog.v-dialog--active", + elements: { + confirmBtn: ".confirm-btn", + cancelBtn: ".cancel-btn", + } + }, + setCleanupPlanDialog: { + selector: ".set-cleanup-plan-dialog.menuable__content__active", + elements: { + item: ".v-list-item", + activeItem: ".mdi-check", + notAllSelectedItem: ".mdi-minus" + } + }, dateFilters: { selector: "#date-filters", elements: { diff --git a/web/server/vue-cli/e2e/specs/reports.js b/web/server/vue-cli/e2e/specs/reports.js index 11890dd6ae..6939d92b43 100644 --- a/web/server/vue-cli/e2e/specs/reports.js +++ b/web/server/vue-cli/e2e/specs/reports.js @@ -32,6 +32,7 @@ module.exports = { reportPage.section.reviewStatusFilter, reportPage.section.detectionStatusFilter, reportPage.section.sourceComponentFilter, + reportPage.section.cleanupPlanFilter, reportPage.section.checkerMessageFilter, reportPage.section.checkerMessageFilter, reportPage.section.reportHashFilter, @@ -76,7 +77,7 @@ module.exports = { "sort reports by bug path length" (browser) { const reportPage = browser.page.report(); - const colIdx = 8; + const colIdx = 9; // Sort reports in ascending order by bug path length. reportPage.sortReports(colIdx, (data) => { @@ -504,6 +505,149 @@ module.exports = { .click("@closeBtn"); }, + "manage cleanup plans" (browser) { + const reportPage = browser.page.report(); + const section = reportPage.section.cleanupPlanFilter; + const dialogSection = reportPage.section.cleanupPlanDialog; + const newCleanupPlanDialog = reportPage.section.newCleanupPlanDialog; + const removeCleanupPlanDialog = reportPage.section.removeCleanupPlanDialog; + const setCleanupPlanDialog = reportPage.section.setCleanupPlanDialog; + + // Clear all filters so we will be able to select multiple reports. + reportPage.click("@clearAllFilterBtn"); + reportPage + .pause(500) + .waitForElementNotPresent("@progressBar"); + + section.click("@manageBtn"); + + reportPage.expect.section(dialogSection).to.be.visible.before(5000); + + dialogSection.pause(500); + + // Add a new cleanup plan. + dialogSection.waitForElementVisible("@newCleanupPlanBtn") + dialogSection.click("@newCleanupPlanBtn"); + reportPage.expect.section(newCleanupPlanDialog).to.be.visible.before(5000); + + let [ name, description ] = [ "e2e", "Test" ]; + newCleanupPlanDialog + .clearAndSetValue("@name", name, newCleanupPlanDialog) + .clearAndSetValue("@description", description, newCleanupPlanDialog) + .click("@saveBtn"); + + dialogSection.api.elements("@tableRows", (elements) => { + browser.assert.ok(elements.result.value.length === 1); + }); + + // Edit cleanup plan. + dialogSection.click({ selector: "@editCleanupPlanBtn", index: 0 }); + reportPage.expect.section(newCleanupPlanDialog).to.be.visible.before(5000); + + [ description ] = [ "Renamed" ]; + newCleanupPlanDialog + .clearAndSetValue("@description", description, newCleanupPlanDialog) + .click("@saveBtn"); + + dialogSection.api.elements("@tableRows", (elements) => { + browser.assert.ok(elements.result.value.length === 1); + }); + + dialogSection.click("@closeBtn"); + + reportPage.assert.cssClassPresent( + "@setCleanupPlanBtn", "v-btn--disabled"); + + // Assign report to cleanup plan. + reportPage.click("@selectReportCheckbox"); + + reportPage.assert.not.cssClassPresent( + "@setCleanupPlanBtn", "v-btn--disabled"); + + reportPage.click("@setCleanupPlanBtn"); + + reportPage.expect.section(setCleanupPlanDialog) + .to.be.visible.before(5000); + + setCleanupPlanDialog + .click({ selector: "@item", index: 0 }) + .waitForElementVisible("@activeItem"); + + // Select filter item. + section.openFilterSettings(); + + reportPage.section.settingsMenu + .toggleMenuItem(0) + .applyFilter(); + + reportPage.expect.section("@settingsMenu").to.not.be.present.before(5000); + + section.api.elements("@selectedItems", ({result}) => { + browser.assert.ok(result.value.length === 1); + }); + + reportPage.getTableRows("@tableRows", (data) => { + browser.assert.ok( + [...new Set(data.map(r => r[2]))].filter(d => d).length === 1); + }); + + // Clear the filter. + section.click("@clearBtn"); + + section.api.elements("@selectedItems", ({result}) => { + browser.assert.ok(result.value.length === 0); + }); + + // Unset cleanup. + reportPage + .click("@selectAllReportCheckbox") + .click("@setCleanupPlanBtn"); + + reportPage.expect.section(setCleanupPlanDialog) + .to.be.visible.before(5000); + + setCleanupPlanDialog.waitForElementVisible("@notAllSelectedItem"); + + setCleanupPlanDialog + .click({ selector: "@item", index: 0 }) + .waitForElementVisible("@activeItem") + .click({ selector: "@item", index: 0 }) + .waitForElementNotPresent("@activeItem"); + + reportPage.click("@setCleanupPlanBtn"); + + // Close cleanup plan. + section.click("@manageBtn"); + reportPage.expect.section(dialogSection).to.be.visible.before(5000); + dialogSection.pause(500); + + dialogSection.api.elements("@tableRows", (elements) => { + browser.assert.ok(elements.result.value.length === 1); + }); + + dialogSection + .click("@closeCleanupPlanBtn") + .waitForElementNotPresent("@closeCleanupPlanBtn") + .click("@closedCleanupPlansTab") + .waitForElementVisible("@reopenCleanupPlanBtn") + .click("@reopenCleanupPlanBtn") + .waitForElementNotPresent("@reopenCleanupPlanBtn") + .click("@openCleanupPlansTab") + .waitForElementVisible("@closeCleanupPlanBtn") + + // Remove the cleanup plan. + dialogSection.waitForElementVisible("@removeCleanupPlanBtn"); + dialogSection.click({ selector: "@removeCleanupPlanBtn", index: 0 }); + reportPage.expect.section(removeCleanupPlanDialog) + .to.be.visible.before(5000); + + removeCleanupPlanDialog.click("@confirmBtn"); + + dialogSection + .waitForElementVisible("@emptyTable") + .click("@closeBtn"); + }, + "set checker message filter" (browser) { const reportPage = browser.page.report(); const section = reportPage.section.checkerMessageFilter; diff --git a/web/server/vue-cli/package-lock.json b/web/server/vue-cli/package-lock.json index 84b3ed3529..924093eecc 100644 --- a/web/server/vue-cli/package-lock.json +++ b/web/server/vue-cli/package-lock.json @@ -6332,8 +6332,8 @@ "dev": true }, "codechecker-api": { - "version": "file:../../api/js/codechecker-api-node/dist/codechecker-api-6.42.0.tgz", - "integrity": "sha512-ub7L43hb/QClA7YDB9YNzENOt1eBoIolJ8RC8fVmQ7YXwPFcKS31ZE2nQBtkRlixRAkOVZG0veXDMSm+vtFBbg==", + "version": "file:../../api/js/codechecker-api-node/dist/codechecker-api-6.43.0.tgz", + "integrity": "sha512-kxLmJ9R3+tNrj4Zt8hL+uRRdtbni2Gmi1yyUBB7jzmcwbxxJYKDIV1Dagcrzx9l2Ln0tOo72PEbNERvw2d0BSw==", "requires": { "thrift": "0.13.0-hotfix.1" } diff --git a/web/server/vue-cli/package.json b/web/server/vue-cli/package.json index c3cd140541..39903b5564 100644 --- a/web/server/vue-cli/package.json +++ b/web/server/vue-cli/package.json @@ -27,7 +27,7 @@ }, "dependencies": { "@mdi/font": "^5.9.55", - "codechecker-api": "file:../../api/js/codechecker-api-node/dist/codechecker-api-6.42.0.tgz", + "codechecker-api": "file:../../api/js/codechecker-api-node/dist/codechecker-api-6.43.0.tgz", "chart.js": "^2.9.4", "chartjs-plugin-datalabels": "^0.7.0", "codemirror": "^5.60.0", diff --git a/web/server/vue-cli/src/assets/userguide/images/reports/assign_report_to_cleanup_plan.png b/web/server/vue-cli/src/assets/userguide/images/reports/assign_report_to_cleanup_plan.png new file mode 100644 index 0000000000..232303cbb5 Binary files /dev/null and b/web/server/vue-cli/src/assets/userguide/images/reports/assign_report_to_cleanup_plan.png differ diff --git a/web/server/vue-cli/src/assets/userguide/images/reports/assign_reports_to_cleanup_plan.png b/web/server/vue-cli/src/assets/userguide/images/reports/assign_reports_to_cleanup_plan.png new file mode 100644 index 0000000000..b135670386 Binary files /dev/null and b/web/server/vue-cli/src/assets/userguide/images/reports/assign_reports_to_cleanup_plan.png differ diff --git a/web/server/vue-cli/src/assets/userguide/images/reports/cleanup_plan_filter.png b/web/server/vue-cli/src/assets/userguide/images/reports/cleanup_plan_filter.png new file mode 100644 index 0000000000..1f17c0c0b6 Binary files /dev/null and b/web/server/vue-cli/src/assets/userguide/images/reports/cleanup_plan_filter.png differ diff --git a/web/server/vue-cli/src/assets/userguide/images/reports/list_of_cleanup_plans.png b/web/server/vue-cli/src/assets/userguide/images/reports/list_of_cleanup_plans.png new file mode 100644 index 0000000000..8a0dbb78ec Binary files /dev/null and b/web/server/vue-cli/src/assets/userguide/images/reports/list_of_cleanup_plans.png differ diff --git a/web/server/vue-cli/src/assets/userguide/userguide.md b/web/server/vue-cli/src/assets/userguide/userguide.md index dc67fa2810..47606f3170 100644 --- a/web/server/vue-cli/src/assets/userguide/userguide.md +++ b/web/server/vue-cli/src/assets/userguide/userguide.md @@ -622,6 +622,45 @@ Each line should begin with a `+` or a `-`, followed by a path glob pattern: For more information [see](https://github.com/Ericsson/codechecker/blob/master/docs/web/user_guide.md#format-of-component-file). +## Manage cleanup plans +You can use **cleanup plans** to track progress of reports in your product. +Clenup plans can be managed only by *administrators* after clicking on the +*pencil icon* at the *Cleanup plans* filter. + +![Manage cleanup plans](images/reports/cleanup_plan_filter.png) + +A pop-up window will be opened where you can add, edit, close or remove +existing cleanup plans. + +![List of cleanup plans](images/reports/list_of_cleanup_plans.png) + +## Assign reports to cleanup plans +You can assign multiple reports to a cleanup plan on the **Reports view**. For +this you have to select at least one report by using check boxes on the first +column of the report list table. + +After reports are selected the **Set cleanup plan** above the reports table +will be active and you can select a cleanup plan here by clicking on the +cleanup plan name. + +You can remove reports from a cleanup plan the same way by clicking on the +cleanup plan name. + +Notes: +- You can select all reports on the current page by using the checkbox in +the first column of the header bar. +- If multiple reports are selected and at least one report can be found in a +cleanup plan but not all of them, after clicking on the **Set cleanup plan** +button beside the cleanup plan a minus sign (**-**) will be shown. Clicking +on the cleanup plan name in this case will assign every selected report to +the cleanup. + +![Assign reports to cleanup plan](images/reports/assign_reports_to_cleanup_plan.png) + +You can also assign current report to a cleanup plan on the **Report detail** +view: +![Assign reports to cleanup plan](images/reports/assign_report_to_cleanup_plan.png) + # Report details ## Report Navigation Tree Report Navigation Tree shows the found reports at the currently opened file. diff --git a/web/server/vue-cli/src/components/Report/CleanupPlan/CleanupPlanList.vue b/web/server/vue-cli/src/components/Report/CleanupPlan/CleanupPlanList.vue new file mode 100644 index 0000000000..2f06912569 --- /dev/null +++ b/web/server/vue-cli/src/components/Report/CleanupPlan/CleanupPlanList.vue @@ -0,0 +1,74 @@ + + + diff --git a/web/server/vue-cli/src/components/Report/CleanupPlan/CleanupPlanTab.mixin.js b/web/server/vue-cli/src/components/Report/CleanupPlan/CleanupPlanTab.mixin.js new file mode 100644 index 0000000000..3082b2f651 --- /dev/null +++ b/web/server/vue-cli/src/components/Report/CleanupPlan/CleanupPlanTab.mixin.js @@ -0,0 +1,57 @@ +import { ccService, handleThriftError } from "@cc-api"; +import { CleanupPlanFilter } from "@cc/report-server-types"; + +export default { + name: "CleanupPlanTabMixin", + data() { + return { + tab: null, + loading: false, + openCleanupPlans: null, + closedCleanupPlans: null, + }; + }, + + watch: { + tab() { + this.fetchCleanupPlans(false); + } + }, + + methods: { + onFetchFinished(/* cleanupPlans */) { + + }, + + fetchCleanupPlans(force=true) { + if (!this.tab && (!this.openCleanupPlans || force)) { + this.fetchOpenCleanupPlans(); + } else if (!this.closedCleanupPlans || force) { + this.fetchClosedCleanupPlans(); + } + }, + + getCleanupPlans(filter) { + this.loading = true; + return new Promise(resolve => { + ccService.getClient().getCleanupPlans(filter, + handleThriftError(cleanupPlans => { + this.loading = false; + resolve(cleanupPlans); + })); + }); + }, + + async fetchOpenCleanupPlans() { + const filter = new CleanupPlanFilter({ isOpen: true }); + this.openCleanupPlans = await this.getCleanupPlans(filter); + this.onFetchFinished(this.openCleanupPlans); + }, + + async fetchClosedCleanupPlans() { + const filter = new CleanupPlanFilter({ isOpen: false }); + this.closedCleanupPlans = await this.getCleanupPlans(filter); + this.onFetchFinished(this.closedCleanupPlans); + }, + } +}; diff --git a/web/server/vue-cli/src/components/Report/CleanupPlan/CleanupPlanTab.vue b/web/server/vue-cli/src/components/Report/CleanupPlan/CleanupPlanTab.vue new file mode 100644 index 0000000000..27aef607af --- /dev/null +++ b/web/server/vue-cli/src/components/Report/CleanupPlan/CleanupPlanTab.vue @@ -0,0 +1,42 @@ + + + diff --git a/web/server/vue-cli/src/components/Report/CleanupPlan/DueDate.vue b/web/server/vue-cli/src/components/Report/CleanupPlan/DueDate.vue new file mode 100644 index 0000000000..60dfd7f418 --- /dev/null +++ b/web/server/vue-cli/src/components/Report/CleanupPlan/DueDate.vue @@ -0,0 +1,40 @@ + + + diff --git a/web/server/vue-cli/src/components/Report/CleanupPlan/DueDateMenu.vue b/web/server/vue-cli/src/components/Report/CleanupPlan/DueDateMenu.vue new file mode 100644 index 0000000000..1a9ca79831 --- /dev/null +++ b/web/server/vue-cli/src/components/Report/CleanupPlan/DueDateMenu.vue @@ -0,0 +1,56 @@ + + + diff --git a/web/server/vue-cli/src/components/Report/CleanupPlan/EditCleanupPlanDialog.vue b/web/server/vue-cli/src/components/Report/CleanupPlan/EditCleanupPlanDialog.vue new file mode 100644 index 0000000000..dd6a6014fc --- /dev/null +++ b/web/server/vue-cli/src/components/Report/CleanupPlan/EditCleanupPlanDialog.vue @@ -0,0 +1,181 @@ + + + + + diff --git a/web/server/vue-cli/src/components/Report/CleanupPlan/ListCleanupPlans.vue b/web/server/vue-cli/src/components/Report/CleanupPlan/ListCleanupPlans.vue new file mode 100644 index 0000000000..425110843f --- /dev/null +++ b/web/server/vue-cli/src/components/Report/CleanupPlan/ListCleanupPlans.vue @@ -0,0 +1,137 @@ + + + diff --git a/web/server/vue-cli/src/components/Report/CleanupPlan/ListCleanupPlansTable.vue b/web/server/vue-cli/src/components/Report/CleanupPlan/ListCleanupPlansTable.vue new file mode 100644 index 0000000000..751fcf4467 --- /dev/null +++ b/web/server/vue-cli/src/components/Report/CleanupPlan/ListCleanupPlansTable.vue @@ -0,0 +1,124 @@ + + + diff --git a/web/server/vue-cli/src/components/Report/CleanupPlan/ManageCleanupPlanDialog.vue b/web/server/vue-cli/src/components/Report/CleanupPlan/ManageCleanupPlanDialog.vue new file mode 100644 index 0000000000..f0143ebcba --- /dev/null +++ b/web/server/vue-cli/src/components/Report/CleanupPlan/ManageCleanupPlanDialog.vue @@ -0,0 +1,82 @@ + + + \ No newline at end of file diff --git a/web/server/vue-cli/src/components/Report/CleanupPlan/RemoveCleanupPlanDialog.vue b/web/server/vue-cli/src/components/Report/CleanupPlan/RemoveCleanupPlanDialog.vue new file mode 100644 index 0000000000..b3718296d9 --- /dev/null +++ b/web/server/vue-cli/src/components/Report/CleanupPlan/RemoveCleanupPlanDialog.vue @@ -0,0 +1,60 @@ + + + diff --git a/web/server/vue-cli/src/components/Report/CleanupPlan/SetCleanupPlanBtn.vue b/web/server/vue-cli/src/components/Report/CleanupPlan/SetCleanupPlanBtn.vue new file mode 100644 index 0000000000..8bd125ee67 --- /dev/null +++ b/web/server/vue-cli/src/components/Report/CleanupPlan/SetCleanupPlanBtn.vue @@ -0,0 +1,203 @@ + + + diff --git a/web/server/vue-cli/src/components/Report/CleanupPlan/index.js b/web/server/vue-cli/src/components/Report/CleanupPlan/index.js new file mode 100644 index 0000000000..72a66b993a --- /dev/null +++ b/web/server/vue-cli/src/components/Report/CleanupPlan/index.js @@ -0,0 +1,13 @@ +import EditCleanupPlanDialog from "./EditCleanupPlanDialog"; +import ListCleanupPlans from "./ListCleanupPlans"; +import ManageCleanupPlanDialog from "./ManageCleanupPlanDialog"; +import RemoveCleanupPlanDialog from "./RemoveCleanupPlanDialog"; +import SetCleanupPlanBtn from "./SetCleanupPlanBtn"; + +export { + EditCleanupPlanDialog, + ListCleanupPlans, + ManageCleanupPlanDialog, + RemoveCleanupPlanDialog, + SetCleanupPlanBtn +}; diff --git a/web/server/vue-cli/src/components/Report/Report.vue b/web/server/vue-cli/src/components/Report/Report.vue index ee69ab18fd..6ce69ca375 100644 --- a/web/server/vue-cli/src/components/Report/Report.vue +++ b/web/server/vue-cli/src/components/Report/Report.vue @@ -38,6 +38,16 @@ /> + + + + + + + + + + + + + + diff --git a/web/server/vue-cli/src/components/Report/ReportFilter/Filters/index.js b/web/server/vue-cli/src/components/Report/ReportFilter/Filters/index.js index 23b6e8ac73..1174d5642c 100644 --- a/web/server/vue-cli/src/components/Report/ReportFilter/Filters/index.js +++ b/web/server/vue-cli/src/components/Report/ReportFilter/Filters/index.js @@ -3,6 +3,7 @@ import UniqueFilter from "./UniqueFilter"; import ReportHashFilter from "./ReportHashFilter"; import BaselineOpenReportsDateFilter from "./BaselineOpenReportsDateFilter"; import BaselineRunFilter from "./BaselineRunFilter"; +import CleanupPlanFilter from "./CleanupPlanFilter"; import ComparedToDiffTypeFilter from "./ComparedToDiffTypeFilter"; import ComparedToOpenReportsDateFilter from "./ComparedToOpenReportsDateFilter"; @@ -25,6 +26,7 @@ export { ReportHashFilter, BaselineOpenReportsDateFilter, BaselineRunFilter, + CleanupPlanFilter, ComparedToDiffTypeFilter, ComparedToOpenReportsDateFilter, ComparedToRunFilter, diff --git a/web/server/vue-cli/src/components/Report/ReportFilter/ReportFilter.vue b/web/server/vue-cli/src/components/Report/ReportFilter/ReportFilter.vue index 4b006b89a8..4c59802b20 100644 --- a/web/server/vue-cli/src/components/Report/ReportFilter/ReportFilter.vue +++ b/web/server/vue-cli/src/components/Report/ReportFilter/ReportFilter.vue @@ -204,6 +204,18 @@ + + + + + + + + + +