diff --git a/api/v6/report_server.thrift b/api/v6/report_server.thrift index 14520efdf2..f7aae1c325 100644 --- a/api/v6/report_server.thrift +++ b/api/v6/report_server.thrift @@ -33,6 +33,22 @@ struct RunFilter { 2: string name } +struct RunHistoryData { + 1: i64 runId, // Unique id of the run. + 2: string runName, // Name of the run. + 3: string versionTag, // Version tag of the report. + 4: string user, // User name who analysed the run. + 5: string time // Date time when the run was analysed. +} +typedef list RunHistoryDataList + +struct RunTagCount { + 1: string time, // Date time of the last run. + 2: string name, // Name of the tag. + 3: i64 count // Count of the reports. +} +typedef list RunTagCounts + struct RunReportCount { 1: i64 runId, // unique id of the run 2: string name, // human readable name of the run @@ -81,7 +97,10 @@ struct ReportFilter { 4: list reportHash, 5: list severity, 6: list reviewStatus, - 7: list detectionStatus + 7: list detectionStatus, + 8: list runHistoryTag, + 9: optional string firstDetectionDate, + 10: optional string fixDate } struct ReportDetails{ @@ -174,6 +193,15 @@ service codeCheckerDBAccess { RunDataList getRunData(1: RunFilter runFilter) throws (1: shared.RequestFailed requestError), + // Get run history for runs. + // If an empty run id list is provided the history + // will be returned for all the available runs ordered by run history date. + // PERMISSION: PRODUCT_ACCESS + RunHistoryDataList getRunHistory(1: list runIds, + 2: i64 limit, + 3: i64 offset) + throws (1: shared.RequestFailed requestError), + // PERMISSION: PRODUCT_ACCESS ReportData getReport( 1: i64 reportId) @@ -336,6 +364,14 @@ service codeCheckerDBAccess { 3: CompareData cmpData) throws (1: shared.RequestFailed requestError), + // If the run id list is empty the metrics will be counted + // for all of the runs and in compare mode all of the runs + // will be used as a baseline excluding the runs in compare data. + // PERMISSION: PRODUCT_ACCESS + RunTagCounts getRunHistoryTagCounts(1: list runIds, + 2: ReportFilter reportFilter, + 3: CompareData cmpData) + throws (1: shared.RequestFailed requestError), //============================================ // Analysis result storage related API calls. @@ -364,9 +400,10 @@ service codeCheckerDBAccess { // PERMISSION: PRODUCT_STORE i64 massStoreRun( 1: string runName, - 2: string version, - 3: string zipfile, - 4: bool force) + 2: string tag, + 3: string version, + 4: string zipfile, + 5: bool force) throws (1: shared.RequestFailed requestError), } diff --git a/docs/user_guide.md b/docs/user_guide.md index 36e46be4f9..3718f574a9 100644 --- a/docs/user_guide.md +++ b/docs/user_guide.md @@ -671,7 +671,7 @@ a database. to the database. ~~~~~~~~~~~~~~~~~~~~~ -usage: CodeChecker store [-h] [-t {plist}] [-n NAME] [-f] +usage: CodeChecker store [-h] [-t {plist}] [-n NAME] [--tag TAG] [-f] [--url PRODUCT_URL] [--verbose {info,debug,debug_analyzer}] [file/folder [file/folder ...]] @@ -693,6 +693,8 @@ optional arguments: reports to the database. If not specified, the '-- name' parameter given to 'codechecker-analyze' will be used, if exists. + --tag TAG A unique identifier for this individual store of results + in the run's history. -f, --force Delete analysis results stored in the database for the current analysis run's name and store only the results reported in the 'input' files. (By default, diff --git a/libcodechecker/analyze/store_handler.py b/libcodechecker/analyze/store_handler.py index b8889512af..95acb9dd8b 100644 --- a/libcodechecker/analyze/store_handler.py +++ b/libcodechecker/analyze/store_handler.py @@ -200,7 +200,8 @@ def is_same_event_path(report_id, events, session): str(ex)) -def addCheckerRun(session, storage_session, command, name, version, force): +def addCheckerRun(session, storage_session, command, name, tag, username, + run_history_time, version, force): """ Store checker run related data to the database. By default updates the results if name already exists. @@ -246,6 +247,22 @@ def addCheckerRun(session, storage_session, command, name, version, force): session.flush() run_id = checker_run.id + # Add run to the history. + LOG.debug("adding run to the history") + + if tag is not None: + version_tag = session.query(RunHistory) \ + .filter(RunHistory.run_id == run_id, + RunHistory.version_tag == tag) \ + .one_or_none() + + if version_tag: + raise Exception('Tag ' + tag + ' is already used in this run') + + run_history = RunHistory(run_id, tag, username, run_history_time) + session.add(run_history) + session.flush() + storage_session.start_run_session(run_id, session) return run_id except Exception as ex: @@ -255,7 +272,7 @@ def addCheckerRun(session, storage_session, command, name, version, force): str(ex)) -def finishCheckerRun(storage_session, run_id): +def finishCheckerRun(storage_session, run_id, run_history_time): """ """ try: @@ -269,7 +286,7 @@ def finishCheckerRun(storage_session, run_id): run.mark_finished() - storage_session.end_run_session(run_id) + storage_session.end_run_session(run_id, run_history_time) return True @@ -303,7 +320,8 @@ def addReport(storage_session, bugpath, events, checker_id, - severity): + severity, + detection_time): """ """ try: @@ -346,6 +364,7 @@ def addReport(storage_session, elif report.detection_status == 'resolved': new_status = 'reopened' report.file_id = file_id + report.fixed_at = None change_path_and_events(session, report.id, bugpath, @@ -369,7 +388,8 @@ def addReport(storage_session, line_num, column, severity, - "new") + "new", + detection_time) session.add(report) session.flush() diff --git a/libcodechecker/libclient/thrift_helper.py b/libcodechecker/libclient/thrift_helper.py index b1f3df18fc..6696b15316 100644 --- a/libcodechecker/libclient/thrift_helper.py +++ b/libcodechecker/libclient/thrift_helper.py @@ -192,5 +192,5 @@ def getMissingContentHashes(self, file_hashes): pass @ThriftClientCall - def massStoreRun(self, name, version, zipdir, force): + def massStoreRun(self, name, tag, version, zipdir, force): pass diff --git a/libcodechecker/libhandlers/store.py b/libcodechecker/libhandlers/store.py index 858361ee8c..ed094b448f 100644 --- a/libcodechecker/libhandlers/store.py +++ b/libcodechecker/libhandlers/store.py @@ -107,6 +107,14 @@ def add_arguments_to_parser(parser): "the '--name' parameter given to 'codechecker-" "analyze' will be used, if exists.") + parser.add_argument('--tag', + type=str, + dest="tag", + required=False, + default=argparse.SUPPRESS, + help="A uniques identifier for this individual store " + "of results in the run's history.") + parser.add_argument('-f', '--force', dest="force", default=argparse.SUPPRESS, @@ -334,6 +342,7 @@ def main(args): context = generic_package_context.get_context() client.massStoreRun(args.name, + args.tag if 'tag' in args else None, context.version, b64zip, 'force' in args) diff --git a/libcodechecker/server/client_db_access_handler.py b/libcodechecker/server/client_db_access_handler.py index a797ee4130..9c5c0cb6ef 100644 --- a/libcodechecker/server/client_db_access_handler.py +++ b/libcodechecker/server/client_db_access_handler.py @@ -8,9 +8,10 @@ """ import base64 +import calendar import codecs from collections import defaultdict -import datetime +from datetime import datetime, timedelta import json import os import shutil @@ -49,6 +50,7 @@ class CountFilter: SEVERITY = 3 REVIEW_STATUS = 4 DETECTION_STATUS = 5 + RUN_HISTORY_TAG = 6 def conv(text): @@ -115,6 +117,40 @@ def process_report_filter_v2(report_filter, count_filter=None): AND.append(or_(*OR)) + detection_status = report_filter.detectionStatus + if report_filter.firstDetectionDate is not None: + date = datetime.strptime(report_filter.firstDetectionDate, + '%Y-%m-%d %H:%M:%S') + + OR = [] + if detection_status is not None and len(detection_status) == 1 and \ + shared.ttypes.DetectionStatus.RESOLVED in detection_status: + OR.append(Report.fixed_at >= date) + else: + OR.append(Report.detected_at >= date) + AND.append(or_(*OR)) + + if report_filter.fixDate is not None: + date = datetime.strptime(report_filter.fixDate, + '%Y-%m-%d %H:%M:%S') + + OR = [] + if detection_status is not None and len(detection_status) == 1 and \ + shared.ttypes.DetectionStatus.RESOLVED in detection_status: + OR.append(Report.fixed_at < date) + else: + OR.append(Report.detected_at < date) + AND.append(or_(*OR)) + + if report_filter.runHistoryTag is not None and \ + count_filter != CountFilter.RUN_HISTORY_TAG: + OR = [] + for history_date in report_filter.runHistoryTag: + date = datetime.strptime(history_date, + '%Y-%m-%d %H:%M:%S.%f') + OR.append(Report.detected_at <= date) + AND.append(or_(*OR)) + filter_expr = and_(*AND) return filter_expr @@ -256,7 +292,7 @@ def start_run_session(self, run_id, transaction): 'transaction': transaction, 'timer': time.time()} - def end_run_session(self, run_id): + def end_run_session(self, run_id, run_history_time): this_session = self.__sessions[run_id] transaction = this_session['transaction'] @@ -265,7 +301,8 @@ def end_run_session(self, run_id): transaction.query(Report) \ .filter(Report.run_id == run_id, Report.id.notin_(this_session['touched_reports'])) \ - .update({Report.detection_status: 'resolved'}, + .update({Report.detection_status: 'resolved', + Report.fixed_at: run_history_time}, synchronize_session='fetch') transaction.commit() @@ -485,6 +522,38 @@ def getRunData(self, run_filter): finally: session.close() + @timeit + def getRunHistory(self, run_ids, limit, offset): + self.__require_access() + try: + session = self.__Session() + + if not run_ids: + run_ids = self.__get_run_ids_to_query(session, None) + + res = session.query(RunHistory) \ + .filter(RunHistory.run_id.in_(run_ids)) \ + .order_by(RunHistory.time.desc()) \ + .limit(limit) \ + .offset(offset) + + results = [] + for history in res: + results.append(RunHistoryData(runId=history.run.id, + runName=history.run.name, + versionTag=history.version_tag, + user=history.user, + time=str(history.time))) + + return results + except sqlalchemy.exc.SQLAlchemyError as alchemy_ex: + msg = str(alchemy_ex) + LOG.error(msg) + raise shared.ttypes.RequestFailed(shared.ttypes.ErrorCode.DATABASE, + msg) + finally: + session.close() + @timeit def getReport(self, reportId): self.__require_access() @@ -1394,6 +1463,69 @@ def getFileCounts(self, run_ids, report_filter, cmp_data): session.close() return results + @timeit + def getRunHistoryTagCounts(self, run_ids, report_filter, cmp_data): + """ + If the run id list is empty the metrics will be counted + for all of the runs and in compare mode all of the runs + will be used as a baseline excluding the runs in compare data. + """ + self.__require_access() + results = [] + session = self.__Session() + try: + + if not run_ids: + run_ids = self.__get_run_ids_to_query(session, cmp_data) + + if cmp_data: + diff_hashes, run_ids = self._cmp_helper(session, + run_ids, + cmp_data) + if not diff_hashes: + # There is no difference. + return results + + filter_expression = process_report_filter_v2( + report_filter, CountFilter.RUN_HISTORY_TAG) + + count_expr = func.count(literal_column('*')) + + q = session.query(func.max(Report.id), + Run.name, + RunHistory.time, + RunHistory.version_tag, + count_expr) \ + .filter(Report.run_id.in_(run_ids)) \ + .outerjoin(Run, + Run.id == Report.run_id) \ + .outerjoin(File, + Report.file_id == File.id) \ + .outerjoin(RunHistory, + RunHistory.run_id == Report.run_id) \ + .outerjoin(ReviewStatus, + ReviewStatus.bug_hash == Report.bug_id) \ + .filter(filter_expression) \ + + if cmp_data: + q = q.filter(Report.bug_id.in_(diff_hashes)) + + history_tags = q.group_by(Run.name, + RunHistory.time, + RunHistory.version_tag).all() + + for _, run_name, time, tag, count in history_tags: + if tag: + results.append(RunTagCount(time=str(time), + name=run_name + ':' + tag, + count=count)) + + except Exception as ex: + LOG.error(ex) + finally: + session.close() + return results + @timeit def getDetectionStatusCounts(self, run_ids, report_filter, cmp_data): """ @@ -1525,8 +1657,9 @@ def getMissingContentHashes(self, file_hashes): session.close() @timeit - def massStoreRun(self, name, version, b64zip, force): + def massStoreRun(self, name, tag, version, b64zip, force): self.__require_store() + # Unzip sent data. zip_dir = unzip(b64zip) @@ -1605,10 +1738,17 @@ def massStoreRun(self, name, version, b64zip, force): file_content, None) + user = self.__auth_session.user \ + if self.__auth_session else 'Anonymous' + + run_history_time = datetime.now() run_id = store_handler.addCheckerRun(self.__Session(), self.__storage_session, command, name, + tag, + user, + run_history_time, version, force) @@ -1660,7 +1800,8 @@ def massStoreRun(self, name, version, b64zip, force): bug_paths, bug_events, checker_name, - severity) + severity, + run_history_time) last_report_event = report.bug_path[-1] sp_handler = suppress_handler.SourceSuppressHandler( @@ -1683,7 +1824,8 @@ def massStoreRun(self, name, version, b64zip, force): # Round the duration to seconds. int(sum(check_durations))) - store_handler.finishCheckerRun(self.__storage_session, run_id) + store_handler.finishCheckerRun(self.__storage_session, run_id, + run_history_time) # TODO: This directory should be removed even if an exception is thrown # above. diff --git a/libcodechecker/server/run_db_model.py b/libcodechecker/server/run_db_model.py index 982227f888..76efe6caf6 100644 --- a/libcodechecker/server/run_db_model.py +++ b/libcodechecker/server/run_db_model.py @@ -66,6 +66,29 @@ def mark_finished(self): self.duration = ceil((datetime.now() - self.date).total_seconds()) +class RunHistory(Base): + __tablename__ = 'run_histories' + + id = Column(Integer, autoincrement=True, primary_key=True) + run_id = Column(Integer, + ForeignKey('runs.id', deferrable=True, + initially="DEFERRED", ondelete='CASCADE'), + index=True) + version_tag = Column(String) + user = Column(String, nullable=False) + time = Column(DateTime, nullable=False) + + run = relationship(Run, uselist=False) + + __table_args__ = (UniqueConstraint('run_id', 'version_tag'),) + + def __init__(self, run_id, version_tag, user, time): + self.run_id = run_id + self.version_tag = version_tag + self.user = user + self.time = time + + class FileContent(Base): __tablename__ = 'file_contents' @@ -178,6 +201,9 @@ class Report(Base): 'reopened', name='detection_status')) + detected_at = Column(DateTime, nullable=False) + fixed_at = Column(DateTime) + # Cascade delete might remove rows SQLAlchemy warns about this # to remove warnings about already deleted items set this to False. __mapper_args__ = { @@ -187,7 +213,7 @@ class Report(Base): # Priority/severity etc... def __init__(self, run_id, bug_id, file_id, checker_message, checker_id, checker_cat, bug_type, line, column, severity, - detection_status): + detection_status, detection_date): self.run_id = run_id self.file_id = file_id self.bug_id = bug_id @@ -199,6 +225,7 @@ def __init__(self, run_id, bug_id, file_id, checker_message, checker_id, self.detection_status = detection_status self.line = line self.column = column + self.detected_at = detection_date class Comment(Base): diff --git a/tests/functional/report_viewer_api/__init__.py b/tests/functional/report_viewer_api/__init__.py index ddc4c09167..168201eed3 100644 --- a/tests/functional/report_viewer_api/__init__.py +++ b/tests/functional/report_viewer_api/__init__.py @@ -45,6 +45,8 @@ def setup_package(): skip_list_file = None + tag = 'v1.0' + test_env = env.test_env(TEST_WORKSPACE) codechecker_cfg = { @@ -52,7 +54,8 @@ def setup_package(): 'skip_list_file': skip_list_file, 'check_env': test_env, 'workspace': TEST_WORKSPACE, - 'checkers': [] + 'checkers': [], + 'tag': tag } ret = project.clean(test_project) @@ -86,6 +89,7 @@ def setup_package(): '-d', 'core.StackAddressEscape', '-d', 'unix.Malloc' ] + codechecker_cfg['tag'] = None ret = codechecker.check(codechecker_cfg, test_project_name_new, project.path(test_project)) diff --git a/tests/functional/report_viewer_api/test_report_counting.py b/tests/functional/report_viewer_api/test_report_counting.py index f0afea09c8..2b1c69f2a6 100644 --- a/tests/functional/report_viewer_api/test_report_counting.py +++ b/tests/functional/report_viewer_api/test_report_counting.py @@ -53,8 +53,8 @@ def setUp(self): runs = self._cc_client.getRunData(None) - test_runs = [run for run in runs if run.name in run_names] - self._runids = [r.runId for r in test_runs] + self._test_runs = [run for run in runs if run.name in run_names] + self._runids = [r.runId for r in self._test_runs] self.run1_checkers = \ {'core.CallAndMessage': 5, @@ -550,3 +550,15 @@ def test_all_run_report_counts(self): all_report_counts += rc.reportCount self.assertEqual(separate_report_counts, all_report_counts) + + def test_run_history_tag_counts(self): + """ + Count reports for all the runs. + There is only one tag + """ + run = self._test_runs[0] + tag_reports = self._cc_client.getRunHistoryTagCounts(None, + None, + None) + self.assertEqual(len(tag_reports), 1) + self.assertEqual(tag_reports[0].name, run.name + ':v1.0') diff --git a/tests/libtest/codechecker.py b/tests/libtest/codechecker.py index c8f72ff378..ab3a110802 100644 --- a/tests/libtest/codechecker.py +++ b/tests/libtest/codechecker.py @@ -174,6 +174,10 @@ def check(codechecker_cfg, test_project_name, test_project_path): '--url', env.parts_to_url(codechecker_cfg), '--verbose', 'debug'] + tag = codechecker_cfg.get('tag') + if tag: + store_cmd.extend(['--tag', tag]) + try: print("RUNNING STORE") print(' '.join(store_cmd)) diff --git a/www/fonts/codechecker.eot b/www/fonts/codechecker.eot index e7dbdcbb7f..1e4f872edd 100644 Binary files a/www/fonts/codechecker.eot and b/www/fonts/codechecker.eot differ diff --git a/www/fonts/codechecker.svg b/www/fonts/codechecker.svg index 2e151693bc..058ec906e4 100644 --- a/www/fonts/codechecker.svg +++ b/www/fonts/codechecker.svg @@ -38,4 +38,9 @@ + + + + + \ No newline at end of file diff --git a/www/fonts/codechecker.ttf b/www/fonts/codechecker.ttf index cc9de9279f..fc56c14451 100644 Binary files a/www/fonts/codechecker.ttf and b/www/fonts/codechecker.ttf differ diff --git a/www/fonts/codechecker.woff b/www/fonts/codechecker.woff index 2a02c8bd95..373f9c56b2 100644 Binary files a/www/fonts/codechecker.woff and b/www/fonts/codechecker.woff differ diff --git a/www/scripts/codecheckerviewer/BugFilterView.js b/www/scripts/codecheckerviewer/BugFilterView.js index 14d7240488..c3042a086a 100644 --- a/www/scripts/codecheckerviewer/BugFilterView.js +++ b/www/scripts/codecheckerviewer/BugFilterView.js @@ -13,14 +13,17 @@ define([ 'dojo/topic', 'dojox/widget/Standby', 'dijit/form/Button', + 'dijit/form/DateTextBox', 'dijit/form/TextBox', + 'dijit/form/TimeTextBox', 'dijit/popup', 'dijit/TooltipDialog', 'dijit/layout/ContentPane', 'codechecker/hashHelper', 'codechecker/util'], function (declare, Deferred, dom, domClass, all, topic, Standby, Button, - TextBox, popup, TooltipDialog, ContentPane, hashHelper, util) { + DateTextBox, TextBox, TimeTextBox, popup, TooltipDialog, ContentPane, + hashHelper, util) { function alphabetical(a, b) { if (a < b) return -1; @@ -43,7 +46,8 @@ function (declare, Deferred, dom, domClass, all, topic, Standby, Button, if (this.iconClass) var label = dom.create('span', { - class : this.iconClass + class : this.iconClass, + style : this.iconStyle }, this.domNode); this._labelWrapper = dom.create('span', { @@ -89,7 +93,7 @@ function (declare, Deferred, dom, domClass, all, topic, Standby, Button, class : 'select-menu-list ' + that.class }); - if (this.search.enable) + if (this.search && this.search.enable) this._searchBox = new TextBox({ placeholder : this.search.placeHolder, class : 'select-menu-filter', @@ -106,7 +110,7 @@ function (declare, Deferred, dom, domClass, all, topic, Standby, Button, }, postCreate : function () { - if (this.search.enable) + if (this.search && this.search.enable) this.addChild(this._searchBox); dom.place(this._selectMenuList, this.domNode); @@ -164,12 +168,16 @@ function (declare, Deferred, dom, domClass, all, topic, Standby, Button, var content = '' + (item.iconClass - ? '' + ? '' : '') - + '' + item.label + '' - + '' + item.count + ''; + + '' + item.label + ''; - var selected = item.value in that.reportFilter._selectedFilterItems; + if (item.count !== undefined) + content += '' + item.count + ''; + + var selected = that.reportFilter._selectedFilterItems && + item.value in that.reportFilter._selectedFilterItems; var disabled = skippedList.indexOf(item.value.toString()) !== -1; @@ -182,16 +190,17 @@ function (declare, Deferred, dom, domClass, all, topic, Standby, Button, onclick : function () { if (that.disableMultipleOption) { that.reportFilter.clearAll(); - that.reportFilter.selectItem(item.value); + that.reportFilter.selectItem(that.class, item.value); that._render(); return; } - if (item.value in that.reportFilter._selectedFilterItems) { + if (that.reportFilter._selectedFilterItems + && item.value in that.reportFilter._selectedFilterItems) { that.reportFilter.deselectItem(item.value); domClass.remove(this, 'selected'); } else { - that.reportFilter.selectItem(item.value); + that.reportFilter.selectItem(that.class, item.value); domClass.add(this, 'selected'); } @@ -209,6 +218,108 @@ function (declare, Deferred, dom, domClass, all, topic, Standby, Button, } }); + var FilterBase = declare(ContentPane, { + postCreate : function () { + var that = this; + + //--- Filter header ---// + + this._header = dom.create('div', { class : 'header' }, this.domNode); + + this._title = dom.create('span', { + class : 'title', + innerHTML : this.title + }, this._header); + + this._options = dom.create('span', { class : 'options' }, this._header); + + if (!that.disableMultipleOption) + this._clean = dom.create('span', { + class : 'customIcon clean', + onclick : function () { + that.clearAll(); + + topic.publish('filterchange', { + parent : that.parent, + changed : that.getUrlState() + }); + } + }, this._options); + + //--- Loading widget ---// + + this._standBy = new Standby({ + target : this.domNode, + color : '#ffffff' + }); + this.addChild(this._standBy); + }, + + /** + * Converts an item value to human readable format. + * Let's ReviewStatus = {UNREVIEWED = 0;}. If the value parameter is 0 then + * it returns with the `unreviewed` string. + */ + stateConverter : function (value) { return value; }, + + /** + * Get value of the human readable string value of the item. + * Let's ReviewStatus = {UNREVIEWED = 0;}. If the value parameter is + * `unreviewed` it returns with the 0. + */ + stateDecoder : function (value) { return value; }, + + /** + * Returns human readable url state object by calling the state converter + * function. + */ + getUrlState : function () { + var that = this; + + var state = this.getState(); + if (state[this.class]) + state[this.class] = state[this.class].map(function (value) { + return that.stateConverter(value); + }); + + return state; + }, + + /** + * Returns the current state of the filter as key-value pair object. The key + * will be the filter class name and the value will be the selected item + * values. If no filter item is selected the value will be null. + */ + getState : function () { return { [this.class] : null }; }, + + /** + * Set filter state from the parameter state object. + * Returns true if there is any item which is newly selected. + */ + setState : function (state) { return false; }, + + loading : function () { + this._standBy.show(); + }, + + loaded : function () { + this._standBy.hide(); + }, + + /** + * Clears all selected items. + */ + clearAll : function () {}, + + destroy : function () { + this.inherited(arguments); + + this._standBy.destroyRecursive(); + }, + + getSkippedValues : function () { return []; } + }); + /** * Base class of filters. * @property title {string} - Title of the filter. @@ -224,10 +335,11 @@ function (declare, Deferred, dom, domClass, all, topic, Standby, Button, * @property Object.iconClass {string} - Class names for an icon which will * be shown in the gui beside the label. */ - var FilterBase = declare(ContentPane, { + var SelectFilter = declare(FilterBase, { constructor : function (args) { dojo.safeMixin(this, args); + this.inherited(arguments); var that = this; @@ -249,35 +361,13 @@ function (declare, Deferred, dom, domClass, all, topic, Standby, Button, }, disableMultipleOption : this.disableMultipleOption, reportFilter : this - }) + }); }, postCreate : function () { - var that = this; - - //--- Filter header ---// - - this._header = dom.create('div', { class : 'header' }, this.domNode); - - this._title = dom.create('span', { - class : 'title', - innerHTML : this.title - }, this._header); - - this._options = dom.create('span', { class : 'options' }, this._header); - - if (!that.disableMultipleOption) - this._clean = dom.create('span', { - class : 'customIcon clean', - onclick : function () { - that.clearAll(); + this.inherited(arguments); - topic.publish('filterchange', { - parent : that.parent, - changed : {[that.class] : that.getState()} - }); - } - }, this._options); + var that = this; this._edit = dom.create('span', { class : 'customIcon edit', @@ -295,26 +385,42 @@ function (declare, Deferred, dom, domClass, all, topic, Standby, Button, //--- No filter enabled ---// this._selectedFilters.addChild(this._noFilterItem); + }, - //--- Loading widget ---// + setState : function (state) { + var that = this; - this._standBy = new Standby({ - target : this.domNode, - color : '#ffffff' + var changed = false; + var filterState = state[this.class] + ? state[this.class] + : []; + + if (!(filterState instanceof Array)) + filterState = [filterState]; + + filterState.forEach(function (value) { + var value = that.stateDecoder(value); + + if (value === null) { + changed = true; + return; + } + + if (!that._selectedFilterItems[value]) { + that._selectedFilterItems[value] = null; + changed = true; + } }); - this.addChild(this._standBy); + + return changed; }, - /** - * Return the current state of the filter as an array. If no filter item is - * selected it returns null. - */ getState : function () { var state = Object.keys(this._selectedFilterItems).map(function (key) { - return isNaN(key) ? key : parseInt(key); + return isNaN(key) ? key : parseFloat(key); }); - return state.length ? state : null; + return { [this.class] : state.length ? state : null }; }, /** @@ -338,10 +444,11 @@ function (declare, Deferred, dom, domClass, all, topic, Standby, Button, /** * Select a filter item by value. + * @param key {string} - The name of the selected filter item. * @param value {integer|string} - Value of the selected item. * @param preventFilterChange - If true it prevents the filter change event. */ - selectItem : function (value, preventFilterChange) { + selectItem : function (key, value, preventFilterChange) { var that = this; var item = this.getItem(value); @@ -363,7 +470,6 @@ function (declare, Deferred, dom, domClass, all, topic, Standby, Button, } } - //--- Remove the No Filter item ---// if (!this._selectedValuesCount) { @@ -377,6 +483,7 @@ function (declare, Deferred, dom, domClass, all, topic, Standby, Button, class : 'select-menu-item ' + (disableRemove ? 'disabled' : ''), label : item.label, iconClass : item.iconClass, + iconStyle : item.iconStyle, value : value, count : item.count, item : item, @@ -396,7 +503,7 @@ function (declare, Deferred, dom, domClass, all, topic, Standby, Button, if (!preventFilterChange) topic.publish('filterchange', { parent : this.parent, - changed : {[this.class] : this.getState()} + changed : this.getUrlState() }); }, @@ -426,38 +533,264 @@ function (declare, Deferred, dom, domClass, all, topic, Standby, Button, if (!preventFilterChange) { topic.publish('filterchange', { parent : this.parent, - changed : {[this.class] : this.getState()} + changed : this.getUrlState() }); } }, - /** - * Clears all selected items. - */ clearAll : function () { for (key in this._selectedFilterItems) { var item = this._selectedFilterItems[key]; if (item) this.deselectItem(item.value, true); + else + delete this._selectedFilterItems[key]; } + } + }); + + var DateFilter = declare(FilterBase, { + constructor : function () { + var that = this; + + this._fromDate = new DateTextBox({ + class : 'first-detection-date', + placeholder : 'Detection date...', + constraints : { datePattern : 'yyyy-MM-dd' }, + promptMessage : 'yyyy-MM-dd', + invalidMessage: 'Invalid date format. Use yyyy-MM-dd', + onChange : function (state) { + that.onTimeChange(); + } + }); + + this._fromTime = new TimeTextBox({ + class : 'first-detection-time', + constraints: { + timePattern: 'HH:mm:ss' + }, + onChange : function () { + if (that._fromDate.get('value')) + that.onTimeChange(); + } + }); + + this._toDate = new DateTextBox({ + class : 'fix-date', + placeholder : 'Fixed date...', + constraints : { datePattern : 'yyyy-MM-dd' }, + promptMessage : 'yyyy-MM-dd', + invalidMessage: 'Invalid date format. Use yyyy-MM-dd', + onChange : function (state) { + that.onTimeChange(); + } + }); + + this._toTime = new TimeTextBox({ + class : 'fix-time', + constraints: { + timePattern: 'HH:mm:ss' + }, + onChange : function () { + if (that._toDate.get('value')) + that.onTimeChange(); + } + }); + + this._filterTooltip = new FilterTooltip({ + class : this.class, + reportFilter : this + }); }, - loading : function () { - this._standBy.show(); + postCreate : function () { + this.inherited(arguments); + + var that = this; + + this._edit = dom.create('span', { + class : 'customIcon edit', + onclick : function () { + that._filterTooltip.show(); + } + }, this._options); + + this._filterTooltip.set('around', this._edit); + + var fromDateWrapper = + dom.create('div', { class : 'date-wrapper' }, this.domNode); + + dom.place(this._fromDate.domNode, fromDateWrapper); + dom.place(this._fromTime.domNode, fromDateWrapper); + + var toDateWrapper = + dom.create('div', { class : 'date-wrapper' }, this.domNode); + + dom.place(this._toDate.domNode, toDateWrapper); + dom.place(this._toTime.domNode, toDateWrapper); }, - loaded : function () { - this._standBy.hide(); + _updateConstrains : function () { + this._toDate.constraints.min = this._fromDate.get('value'); + this._fromDate.constraints.max = new Date(); }, - destroy : function () { - this.inherited(arguments); + onTimeChange : function () { + topic.publish('filterchange', { + parent : this.parent, + changed : this.getUrlState() + }); + }, - this._standBy.destroyRecursive(); + setToday : function () { + this._fromDate.set('value', new Date(), false); + var zero = new Date(); + zero.setHours(0,0,0,0); + this._fromTime.set('value', zero); + + this._toDate.set('value', new Date(), false); + var midnight = new Date(); + zero.setHours(23,59,59,0); + this._toTime.set('value', zero); + + this.onTimeChange(); }, - getSkippedValues : function () { return []; } + setYesterday : function () { + var yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + yesterday.setHours(0,0,0,0); + + this._fromDate.set('value', yesterday, false); + this._fromTime.set('value', yesterday); + + var yesterdayMidnight = yesterday; + yesterdayMidnight.setHours(23,59,59,0); + + this._toDate.set('value', yesterdayMidnight, false); + this._toTime.set('value', yesterdayMidnight); + + this.onTimeChange(); + }, + + getPrevDateTime : function (date, time) { + var prevDate = date.get('value'); + var prevTime = time.get('value'); + + if (!prevDate) + return null; + + if (prevTime) { + prevDate.setHours(prevTime.getHours()); + prevDate.setMinutes(prevTime.getMinutes()); + prevDate.setSeconds(prevTime.getSeconds()); + } + + return prevDate; + }, + + selectItem : function (key, value) { + var changed = false; + + if (value === 'today') { + this.setToday(); + changed = true; + } else if (value === 'yesterday') { + this.setYesterday(); + changed = true; + } else if (key === 'firstDetectionDate') { + if (!this._fromTime.get('displayedValue').length) { + var zero = new Date(); + zero.setHours(0,0,0,0); + this._fromTime.set('value', zero); + } + + var prevDate = this.getPrevDateTime(this._fromDate, this._fromTime); + var date = new Date(value); + date.setMilliseconds(0); + + if (!prevDate || prevDate.getTime() !== date.getTime()) { + this._fromDate.set('value', date , false); + this._fromTime.set('value', date, false); + + changed = true; + } + } else if (key === 'fixDate') { + if (!this._toTime.get('displayedValue').length) { + var zero = new Date(); + zero.setHours(23,59,59,0); + this._toTime.set('value', zero); + } + + var prevDate = this.getPrevDateTime(this._toDate, this._toTime); + var date = new Date(value); + date.setMilliseconds(0); + + if (!prevDate || prevDate.getTime() !== date.getTime()) { + this._toDate.set('value', date, false); + this._toTime.set('value', date, false); + + changed = true; + } + } + + this._updateConstrains(); + return changed; + }, + + setState : function (state) { + var changed = false; + var from = state[this._fromDate.class]; + var to = state[this._toDate.class]; + + if (from) + changed = this.selectItem('firstDetectionDate', from) || changed; + + if (to) + changed = this.selectItem('fixDate', to) || changed; + + return changed; + }, + + stateDecoder : function (str) { + return new Date(str); + }, + + getUrlState : function () { + var state = {}; + + var from = this._fromDate.get('displayedValue'); + var fromTime = this._fromTime.get('displayedValue'); + + var to = this._toDate.get('displayedValue'); + var toTime = this._toTime.get('displayedValue') + + if (from) + state[this._fromDate.class] = from + ' ' + fromTime; + + if (to) + state[this._toDate.class] = to + ' ' + toTime; + + return state; + }, + + getState : function () { + var state = this.getUrlState(); + + return { + [this._fromDate.class] : state[this._fromDate.class], + [this._toDate.class] : state[this._toDate.class] + }; + }, + + clearAll : function () { + this._fromDate.set('value', null, false); + this._fromTime.set('value', null, false); + + this._toDate.set('value', null, false); + this._toTime.set('value', null, false); + } }); return declare(ContentPane, { @@ -468,7 +801,7 @@ function (declare, Deferred, dom, domClass, all, topic, Standby, Button, this._filters = []; - //--- Cleare all filter button ---// + //--- Clear all filter button ---// this._clearAllButton = new Button({ class : 'clear-all-btn', @@ -476,15 +809,13 @@ function (declare, Deferred, dom, domClass, all, topic, Standby, Button, onClick : function () { //--- Clear selected items ---// - this.filters.forEach(function (filter) { - filter.clearAll(); - }); + that.clearAll(); //--- Remove states from the url ---// topic.publish('filterchange', { parent : that, - changed : that.getState() + changed : that.getUrlState() }); } }); @@ -505,15 +836,14 @@ function (declare, Deferred, dom, domClass, all, topic, Standby, Button, //--- Normal or Diff view filters ---// if (this.baseline || this.newcheck || this.difftype) { - this._runNameBaseFilter = new FilterBase({ + this._runNameBaseFilter = new SelectFilter({ class : 'baseline', - reportFilterName : 'baseline', title : 'Baseline', parent : this, getSkippedValues : function () { return Object.keys(this.runNameNewCheckFilter._selectedFilterItems); }, - getItems : function (query) { + getItems : function () { var deferred = new Deferred(); var reportFilter = that.getReportFilters(); @@ -535,15 +865,14 @@ function (declare, Deferred, dom, domClass, all, topic, Standby, Button, }); this._filters.push(this._runNameBaseFilter); - this._runNameNewCheckFilter = new FilterBase({ + this._runNameNewCheckFilter = new SelectFilter({ class : 'newcheck', - reportFilterName : 'newcheck', title : 'Newcheck', parent : this, getSkippedValues : function () { return Object.keys(that._runNameBaseFilter._selectedFilterItems); }, - getItems : function (query) { + getItems : function () { var deferred = new Deferred(); var reportFilter = that.getReportFilters(); @@ -568,14 +897,14 @@ function (declare, Deferred, dom, domClass, all, topic, Standby, Button, this._runNameBaseFilter.set('runNameNewCheckFilter', this._runNameNewCheckFilter); - this._diffTypeFilter = new FilterBase({ + this._diffTypeFilter = new SelectFilter({ class : 'difftype', - reportFilterName : 'difftype', title : 'Diff type', parent : this, defaultValues : [CC_OBJECTS.DiffType.NEW], disableMultipleOption : true, - getItems : function (query) { + + getItems : function () { var deferred = new Deferred(); var reportFilter = that.getReportFilters(); @@ -616,12 +945,11 @@ function (declare, Deferred, dom, domClass, all, topic, Standby, Button, }); this._filters.push(this._diffTypeFilter); } else { - this._runNameFilter = new FilterBase({ + this._runNameFilter = new SelectFilter({ class : 'run', - reportFilterName : 'run', title : 'Run name', parent : this, - getItems : function (query) { + getItems : function () { var deferred = new Deferred(); var reportFilter = that.getReportFilters(); @@ -646,11 +974,16 @@ function (declare, Deferred, dom, domClass, all, topic, Standby, Button, //--- Review status filter ---// - this._reviewStatusFilter = new FilterBase({ + this._reviewStatusFilter = new SelectFilter({ class : 'review-status', - reportFilterName : 'reviewStatus', title : 'Review status', parent : this, + stateConverter : function (value) { + return util.enumValueToKey(ReviewStatus, value).toLowerCase() + }, + stateDecoder : function (key) { + return ReviewStatus[key.toUpperCase()]; + }, getItems : function () { var deferred = new Deferred(); @@ -677,11 +1010,16 @@ function (declare, Deferred, dom, domClass, all, topic, Standby, Button, //--- Detection status filter ---// - this._detectionStatusFilter = new FilterBase({ + this._detectionStatusFilter = new SelectFilter({ class : 'detection-status', - reportFilterName : 'detectionStatus', title : 'Detection status', parent : this, + stateConverter : function (value) { + return util.enumValueToKey(DetectionStatus, value).toLowerCase() + }, + stateDecoder : function (key) { + return DetectionStatus[key.toUpperCase()]; + }, getItems : function () { var deferred = new Deferred(); @@ -708,11 +1046,16 @@ function (declare, Deferred, dom, domClass, all, topic, Standby, Button, //--- Severity filter ---// - this._severityFilter = new FilterBase({ + this._severityFilter = new SelectFilter({ class : 'severity', - reportFilterName : 'severity', title : 'Severity', parent : this, + stateConverter : function (value) { + return util.enumValueToKey(Severity, value).toLowerCase() + }, + stateDecoder : function (key) { + return Severity[key.toUpperCase()]; + }, getItems : function () { var deferred = new Deferred(); @@ -739,16 +1082,95 @@ function (declare, Deferred, dom, domClass, all, topic, Standby, Button, }); this._filters.push(this._severityFilter); + //--- Run history tags filter ---// + + this._runHistoryTagFilter = new SelectFilter({ + class : 'run-history-tag', + title : 'Run tag', + parent : this, + enableSearch : true, + filterLabel : 'Search for run tags...', + createItem : function (runHistoryTagCount) { + return runHistoryTagCount.map(function (tag) { + return { + label : tag.name, + value : tag.time, + count : tag.count, + iconClass : 'customIcon tag', + iconStyle : 'color: ' + util.strToColor(tag.name) + }; + }); + }, + stateConverter : function (value) { + var item = this._items.filter(function (item) { + return item.value === value; + }); + + return item.length ? item[0].label : null; + }, + stateDecoder : function (key) { + // If no item is available, get items from the server to decode URL + // value. + if (!this._items) { + var runs = that.getRunIds(); + var reportFilter = that.getReportFilters(); + + var res = CC_SERVICE.getRunHistoryTagCounts( + runs.baseline, reportFilter, runs.newcheck); + this._items = this.createItem(res); + } + + var item = this._items.filter(function (item) { + return item.label === key; + }); + + return item.length ? item[0].value : null; + }, + getItems : function () { + var self = this; + var deferred = new Deferred(); + + var runs = that.getRunIds(); + var reportFilter = that.getReportFilters(); + + CC_SERVICE.getRunHistoryTagCounts(runs.baseline, reportFilter, + runs.newcheck, function (res) { + deferred.resolve(self.createItem(res)); + }); + + return deferred; + } + }); + this._filters.push(this._runHistoryTagFilter); + + //--- Detection date filter ---// + + this._detectionDateFilter = new DateFilter({ + class : 'detection-date', + title : 'Detection date', + parent : this, + getItems : function () { + var deferred = new Deferred(); + + deferred.resolve([ + { label : 'Today', value : 'today', iconClass : 'customIcon text-icon today' }, + { label : 'Yesterday', value : 'yesterday', iconClass : 'customIcon text-icon yesterday' } + ]); + + return deferred; + } + }); + this._filters.push(this._detectionDateFilter); + //--- File path filter ---// - this._fileFilter = new FilterBase({ - class : 'file', - reportFilterName : 'filepath', + this._fileFilter = new SelectFilter({ + class : 'filepath', title : 'File', parent : this, enableSearch : true, filterLabel : 'Search for files...', - getItems : function (query) { + getItems : function () { var deferred = new Deferred(); var runs = that.getRunIds(); @@ -773,14 +1195,13 @@ function (declare, Deferred, dom, domClass, all, topic, Standby, Button, //--- Checker name filter ---// - this._checkerNameFilter = new FilterBase({ - class : 'checker', - reportFilterName : 'checkerName', + this._checkerNameFilter = new SelectFilter({ + class : 'checker-name', title : 'Checker name', parent : this, enableSearch : true, filterLabel : 'Search for checker names...', - getItems : function (query) { + getItems : function () { var deferred = new Deferred(); var runs = that.getRunIds(); @@ -809,14 +1230,13 @@ function (declare, Deferred, dom, domClass, all, topic, Standby, Button, //--- Checker message filter ---// - this._checkerMessageFilter = new FilterBase({ + this._checkerMessageFilter = new SelectFilter({ class : 'checker-msg', - reportFilterName : 'checkerMsg', title : 'Checker message', parent : this, enableSearch : true, filterLabel : 'Search for checker messages...', - getItems : function (query) { + getItems : function () { var deferred = new Deferred(); var runs = that.getRunIds(); @@ -869,6 +1289,19 @@ function (declare, Deferred, dom, domClass, all, topic, Standby, Button, this._subscribeTopics(); }, + clearAll : function () { + var state = this.getState(); + Object.keys(state).forEach(function (key) { + state[key] = null; + }); + + this._filters.forEach(function (filter) { + filter.clearAll(); + }); + + return state; + }, + /** * Returns run informations for normal and diff view. * @property baseline In normal view it will contain the run ids. Otherwise @@ -878,8 +1311,9 @@ function (declare, Deferred, dom, domClass, all, topic, Standby, Button, */ getRunIds : function () { if (this.diffView) { - var diffType = this._diffTypeFilter.getState(); - var newCheckIds = this._runNameNewCheckFilter.getState(); + var diffType = this._diffTypeFilter.getState()[this._diffTypeFilter.class]; + var newCheckIds = + this._runNameNewCheckFilter.getState()[this._runNameNewCheckFilter.class]; var cmpData = null; if (newCheckIds) { @@ -889,12 +1323,12 @@ function (declare, Deferred, dom, domClass, all, topic, Standby, Button, } return { - baseline : this._runNameBaseFilter.getState(), + baseline : this._runNameBaseFilter.getState()[this._runNameBaseFilter.class], newcheck : cmpData }; } else { return { - baseline : this._runNameFilter.getState(), + baseline : this._runNameFilter.getState()[this._runNameFilter.class], newcheck : null }; } @@ -918,7 +1352,20 @@ function (declare, Deferred, dom, domClass, all, topic, Standby, Button, var state = {}; this._filters.forEach(function (filter) { - state[filter.class] = filter.getState(); + Object.assign(state, filter.getState()); + }); + + return state; + }, + + /** + * Return the current URL state of the filters as an object. + */ + getUrlState : function () { + var state = {}; + + this._filters.forEach(function (filter) { + Object.assign(state, filter.getUrlState()); }); return state; @@ -932,8 +1379,18 @@ function (declare, Deferred, dom, domClass, all, topic, Standby, Button, this._filters.forEach(function (filter) { var state = filter.getState(); - if (state) - reportFilter[filter.reportFilterName] = state; + Object.keys(state).forEach(function (key) { + // The report filter name comes from the actual filter state. + // It converts the state key to camelCased. + var reportFilterName = key.split('-').map(function (str, ind) { + if (ind === 0) + return str; + + return str.charAt(0).toUpperCase() + str.slice(1); + }).join(''); + + reportFilter[reportFilterName] = state[key]; + }); }); return reportFilter; @@ -947,16 +1404,7 @@ function (declare, Deferred, dom, domClass, all, topic, Standby, Button, var changed = false || force; this._filters.forEach(function (filter) { - var filterState = state[filter.class] ? state[filter.class] : []; - if (!(filterState instanceof Array)) - filterState = [filterState]; - - filterState.forEach(function (value) { - if (!filter._selectedFilterItems[value]) { - filter._selectedFilterItems[value] = null; - changed = true; - } - }); + changed = filter.setState(state) || changed; }); if (!changed) @@ -965,22 +1413,29 @@ function (declare, Deferred, dom, domClass, all, topic, Standby, Button, var finished = 0; this._filters.forEach(function (filter) { filter.loading(); - var filterState = filter.getState(); filter.getItems().then(function (items) { filter._items = items; - if (filterState) - filterState.forEach(function (item) { - filter.selectItem(item, true); - }); + filter.setState(state); + var filterState = filter.getState(); + + Object.keys(filterState).forEach(function (key) { + if (filterState[key]) { + if (!Array.isArray(filterState[key])) + filterState[key] = [filterState[key]]; + + filterState[key].forEach(function (item) { + filter.selectItem(key, item, true); + }); + } + }); //--- Load default values for the first time ---// - if (!filter.initalized && filter.defaultValues - && !filter.getState()) { + if (!filter.initalized && filter.defaultValues) { filter.defaultValues.forEach(function (value) { - filter.selectItem(value, true); + filter.selectItem(filter.class, value, true); }); filter.initalized = true; } @@ -1025,7 +1480,7 @@ function (declare, Deferred, dom, domClass, all, topic, Standby, Button, function (state) { if (that.parent.selected) { that.hashChangeInProgress = true; - that.refreshFilters(that.getState(), true); + that.refreshFilters(state.changed, true); hashHelper.setStateValues(state.changed); } }); diff --git a/www/scripts/codecheckerviewer/ListOfBugs.js b/www/scripts/codecheckerviewer/ListOfBugs.js index 27c2db5962..b27cfb3009 100644 --- a/www/scripts/codecheckerviewer/ListOfBugs.js +++ b/www/scripts/codecheckerviewer/ListOfBugs.js @@ -18,11 +18,12 @@ define([ 'dojox/grid/DataGrid', 'codechecker/BugViewer', 'codechecker/BugFilterView', + 'codechecker/RunHistory', 'codechecker/hashHelper', 'codechecker/util'], function (declare, dom, Deferred, ObjectStore, Store, QueryResults, topic, BorderContainer, TabContainer, Tooltip, DataGrid, BugViewer, BugFilterView, - hashHelper, util) { + RunHistory, hashHelper, util) { var filterHook = function(filters, isDiff) { var length = 0; @@ -239,7 +240,7 @@ function (declare, dom, Deferred, ObjectStore, Store, QueryResults, topic, var item = this.getItem(evt.rowIndex); switch (evt.cell.field) { case 'reviewComment': - if (item.review.author) { + if (item.review && item.review.author) { var content = util.reviewStatusTooltipContent(item.review); Tooltip.show(content.outerHTML, evt.target, ['below']); @@ -301,6 +302,28 @@ function (declare, dom, Deferred, ObjectStore, Store, QueryResults, topic, this.addChild(content); + //--- Run history ---// + + var runHistory = new RunHistory({ + title : 'Run history', + runData : this.runData, + bugOverView : content, + bugFilterView : this._bugFilterView, + parent : this, + onShow : function () { + hashHelper.setStateValue('tab', 'runHistory'); + }, + onHide : function () { + hashHelper.setStateValue('tab', null); + } + }); + + this.addChild(runHistory); + + var urlState = hashHelper.getValues(); + if (urlState.tab && urlState.tab === 'runHistory') + this.selectChild(runHistory); + //--- Events ---// this._openFileTopic = topic.subscribe('openFile', @@ -330,6 +353,10 @@ function (declare, dom, Deferred, ObjectStore, Store, QueryResults, topic, runResultParam : runResultParam, onShow : function () { hashHelper.setStateValue('report', reportData.reportId); + }, + onClose : function () { + that.selectChild(content); + return true; } }); @@ -350,7 +377,7 @@ function (declare, dom, Deferred, ObjectStore, Store, QueryResults, topic, }, onShow : function () { - var state = this._bugFilterView.getState(); + var state = this._bugFilterView.getUrlState(); state.report = null; if (this.allReportView) diff --git a/www/scripts/codecheckerviewer/RunHistory.js b/www/scripts/codecheckerviewer/RunHistory.js new file mode 100644 index 0000000000..936df4029f --- /dev/null +++ b/www/scripts/codecheckerviewer/RunHistory.js @@ -0,0 +1,119 @@ +// ------------------------------------------------------------------------- +// The CodeChecker Infrastructure +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// ------------------------------------------------------------------------- + +define([ + 'dojo/_base/declare', + 'dojo/dom-construct', + 'dojo/topic', + 'dijit/layout/ContentPane', + 'codechecker/util'], +function (declare, dom, topic, ContentPane, util) { + + return declare(ContentPane, { + constructor : function (args) { + dojo.safeMixin(this, args); + + this.runId = this.runData ? this.runData.runId : null; + }, + + postCreate : function () { + this.inherited(arguments); + + var that = this; + + // Get histories for the actual run + var runIds = this.runId ? [this.runId] : null; + var historyData = CC_SERVICE.getRunHistory( + runIds, CC_OBJECTS.MAX_QUERY_SIZE, 0); + + var historyGroupByDate = {}; + historyData.forEach(function (data) { + var date = new Date(data.time); + var groupDate = date.getDate() + ' ' + + util.getMonthName(date.getMonth()) + ', ' + + date.getFullYear(); + + if (!historyGroupByDate[groupDate]) + historyGroupByDate[groupDate] = []; + + historyGroupByDate[groupDate].push(data); + }); + + var dateFilter = that.bugFilterView._detectionDateFilter; + var detectionStatusFilter = + that.bugFilterView._detectionStatusFilter; + + Object.keys(historyGroupByDate).forEach(function (key) { + var group = dom.create('div', { class : 'history-group' }, that.domNode); + dom.create('div', { class : 'header', innerHTML : key }, group); + var content = dom.create('div', { class : 'content' }, group); + + historyGroupByDate[key].forEach(function (data) { + var date = new Date(data.time); + var time = util.formatDateAMPM(date); + + var history = dom.create('div', { + class : 'history', + onclick : function () { + var state = that.bugFilterView.clearAll(); + state.run = [data.runId]; + + if (!data.versionTag) { + state[detectionStatusFilter.class] = [ + detectionStatusFilter.stateConverter(DetectionStatus.NEW), + detectionStatusFilter.stateConverter(DetectionStatus.UNRESOLVED), + detectionStatusFilter.stateConverter(DetectionStatus.REOPENED)]; + state[dateFilter._toDate.class] = that._formatDate(date); + } else { + state['run-history-tag'] = data.runName + ':' + data.versionTag; + } + + topic.publish('filterchange', { + parent : that.bugOverView, + changed : state + }); + that.parent.selectChild(that.bugOverView); + } + }, content); + + dom.create('span', { class : 'time', innerHTML : time }, history); + + var runNameWrapper = dom.create('span', { class : 'run-name-wrapper', title: 'Run name'}, history); + dom.create('span', { class : 'customIcon run-name' }, runNameWrapper); + dom.create('span', { class : 'run-name', innerHTML : data.runName }, runNameWrapper); + + if (data.versionTag) { + var tagWrapper = dom.create('span', { class : 'tag-wrapper', title: 'Version tag' }, history); + dom.create('span', { + class : 'customIcon tag', + style : 'color:' + util.strToColor(data.runName + ':' + data.versionTag) + }, tagWrapper); + dom.create('span', { class : 'tag', innerHTML : data.versionTag }, tagWrapper); + } + + var userWrapper = dom.create('span', {class : 'user-wrapper', title: 'User name' }, history); + dom.create('span', { class : 'customIcon user' }, userWrapper); + dom.create('span', { class : 'user', innerHTML : data.user }, userWrapper); + }) + }); + }, + + + _formatDate : function (date) { + var mm = date.getMonth() + 1; // getMonth() is zero-based + var dd = date.getDate(); + + return [date.getFullYear(), + (mm > 9 ? '' : '0') + mm, + (dd > 9 ? '' : '0') + dd + ].join('-') + ' ' + + [date.getHours(), + date.getMinutes(), + date.getSeconds() + (date.getMilliseconds() > 0 ? 1 : 0) + ].join(':'); + } + }); +}); \ No newline at end of file diff --git a/www/scripts/codecheckerviewer/util.js b/www/scripts/codecheckerviewer/util.js index cde5ee9099..cf9c1a64c7 100644 --- a/www/scripts/codecheckerviewer/util.js +++ b/www/scripts/codecheckerviewer/util.js @@ -10,6 +10,10 @@ define([ 'dojo/dom-style', 'dojo/json'], function (locale, dom, style, json) { + var MONTH_NAMES = ['January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December' + ]; + return { /** * This function returns the first element of the given array for which the @@ -298,6 +302,45 @@ function (locale, dom, style, json) { */ createPermissionParams : function (values) { return json.stringify(values); + }, + + /** + * Get string representation of the month. + * @param {number} month - The month (from 0-11). + */ + getMonthName : function (month) { + return MONTH_NAMES[month]; + }, + + /** + * Format a date to an AM/PM format. + * @param {Date} date - date which will be converted. + */ + formatDateAMPM : function (date) { + var hours = date.getHours(); + var minutes = date.getMinutes(); + var ampm = hours >= 12 ? 'PM' : 'AM'; + + hours = hours % 12; + hours = hours ? hours : 12; + minutes = minutes < 10 ? '0' + minutes : minutes; + + return hours + ':' + minutes + ' ' + ampm; + }, + + /** + * Converts a date to an UTC format timestamp. + * @param {Date} date - date which will be converted. + */ + dateToUTCTime : function (date) { + return Date.UTC( + date.getUTCFullYear(), + date.getUTCMonth(), + date.getUTCDate(), + date.getUTCHours(), + date.getUTCMinutes(), + date.getUTCSeconds(), + date.getUTCMilliseconds()); } }; }); diff --git a/www/style/codecheckerviewer.css b/www/style/codecheckerviewer.css index c80f9af55d..ec41680fdb 100644 --- a/www/style/codecheckerviewer.css +++ b/www/style/codecheckerviewer.css @@ -834,6 +834,7 @@ span[class*="severity-"] { .select-menu-item .customIcon { vertical-align: text-top; + float: left; } .select-menu-item .label{ @@ -866,14 +867,38 @@ span[class*="severity-"] { /* File filter */ -.file .select-menu-item .label { +.filepath .select-menu-item .label { direction: rtl; } -.file .select-menu-item.none .label { +.filepath .select-menu-item.none .label { direction: ltr; } +/* Date filter */ +.bug-filters .date-wrapper { + margin-top: 5px; + overflow: hidden; +} + +.bug-filters .date-wrapper > div { + width: 45%; +} + +.bug-filters .first-detection-date, +.bug-filters .fix-date { + float: left; +} + +.bug-filters .first-detection-time, +.bug-filters .fix-time { + float: right; +} + +.select-menu-item .tag { + color: #007ea7; +} + /*** Message box pane ***/ .mbox { @@ -938,3 +963,100 @@ span[class*="severity-"] { content: "\e01d"; } +/* Run history */ +.history-group { + margin-bottom: 10px; + border: 1px solid #e1e4e8; + border-radius: 5px; +} + +.history-group .header { + font-size: 1.5em; + font-weight: bold; + color: #000; + font-family: Arial, sans serif; + display: inline-block; + padding: 10px; +} + +.history-group .content { + padding: 10px; +} + +.history { + padding: 5px; + border-bottom: 1px solid #e1e4e8; +} + +.history:hover { + cursor: pointer; + background-color: #f5fdff; +} + +.history:last-child { + border-bottom: none; +} + +.history .time { + font-weight: bold; + margin-right: 10px; +} + +.history .run-name-wrapper, +.history .tag-wrapper, +.history .user-wrapper { + margin-left: 5px; +} + +.history .customIcon { + color: #73a7b9; +} + +.customIcon.run-name:before { + content: "\e024"; +} + +.customIcon.tag:before { + content: "\e020"; +} + +.customIcon.run:before { + content: "\e021"; +} + +.customIcon.user:before { + content: "\e022"; +} + +.customIcon.today:before { + content : "T" +} + +.text-icon { + line-height: normal; + text-transform: capitalize; + font-size: 1em; + font-weight: bold; +} + +.customIcon.today { + background-color: #ee8985; + color: white; + font-weight: bold; + +} + +.customIcon.yesterday:before { + content : "Y" +} + +.customIcon.yesterday { + background-color: #007ea7; + color: white; + font-weight: bold; +} + +.select-menu-item .today, +.select-menu-item .yesterday { + float: left; +}