You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by dp...@apache.org on 2018/10/26 12:16:12 UTC

[ignite-teamcity-bot] branch master updated: IGNITE-9849 Updated tests along with statistics, changed switch button - Fixes #38.

This is an automated email from the ASF dual-hosted git repository.

dpavlov pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite-teamcity-bot.git


The following commit(s) were added to refs/heads/master by this push:
     new 15d88a6  IGNITE-9849 Updated tests along with statistics, changed switch button - Fixes #38.
15d88a6 is described below

commit 15d88a63989b2d39c7e5f3a08cf0ab2193fe5b51
Author: zzzadruga <zz...@gmail.com>
AuthorDate: Fri Oct 26 15:08:55 2018 +0300

    IGNITE-9849 Updated tests along with statistics, changed switch button - Fixes #38.
    
    Signed-off-by: Dmitriy Pavlov <dp...@apache.org>
---
 .../ignite/ci/tcbot/condition/BuildCondition.java  |  91 +++
 .../tcbot/condition/BuildConditionCompacted.java   |  71 ++
 .../ci/tcbot/condition/BuildConditionDao.java      |  78 ++
 .../ci/teamcity/ignited/ITeamcityIgnited.java      |  17 +-
 .../ci/teamcity/ignited/TeamcityIgnitedImpl.java   |  19 +-
 .../ci/teamcity/ignited/TeamcityIgnitedModule.java |   2 +
 .../web/model/current/BuildStatisticsSummary.java  |  22 +-
 .../ignite/ci/web/model/hist/BuildsHistory.java    |  35 +-
 .../ci/web/rest/build/GetBuildTestFailures.java    |  51 +-
 .../src/main/webapp/comparison.html                | 903 ++++++++++++++-------
 .../src/main/webapp/css/style-1.5.css              | 119 +--
 11 files changed, 1024 insertions(+), 384 deletions(-)

diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/condition/BuildCondition.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/condition/BuildCondition.java
new file mode 100644
index 0000000..6a1e0fa
--- /dev/null
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/condition/BuildCondition.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.ci.tcbot.condition;
+
+import java.util.Date;
+import java.util.Objects;
+
+/**
+ * Mark build as valid or invalid.
+ */
+public class BuildCondition {
+    /** Build id. */
+    public int buildId;
+
+    /** Username. */
+    public String username;
+
+    /** Is valid. */
+    public boolean isValid;
+
+    /** Date. */
+    public Date date;
+
+    /** Field, where build was marked. */
+    public String field;
+
+    /**
+     * Default constructor.
+     */
+    public BuildCondition(){}
+
+    /**
+     * @param buildId Build id.
+     * @param username Username.
+     * @param isValid Is valid.
+     * @param field Field.
+     */
+    public BuildCondition(int buildId, String username, boolean isValid, String field) {
+        this.buildId = buildId;
+        this.username = username;
+        this.isValid = isValid;
+        this.date = new Date();
+        this.field = field;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return "BuildCondition{" +
+            "buildId=" + buildId +
+            ", username='" + username + '\'' +
+            ", isValid=" + isValid +
+            ", date=" + date +
+            ", field='" + field + '\'' +
+            '}';
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+
+        if (!(o instanceof BuildCondition))
+            return false;
+
+        BuildCondition that = (BuildCondition)o;
+
+        return buildId == that.buildId &&
+            isValid == that.isValid &&
+            Objects.equals(username, that.username);
+    }
+
+    /** {@inheritDoc} */
+    @Override public int hashCode() {
+        return Objects.hash(buildId, username, isValid);
+    }
+}
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/condition/BuildConditionCompacted.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/condition/BuildConditionCompacted.java
new file mode 100644
index 0000000..83beee8
--- /dev/null
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/condition/BuildConditionCompacted.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.ci.tcbot.condition;
+
+import java.util.Date;
+import org.apache.ignite.ci.teamcity.ignited.IStringCompactor;
+
+public class BuildConditionCompacted {
+    /** Build id. */
+    int buildId = -1;
+
+    /** Username. */
+    int username = -1;
+
+    /** Is valid. */
+    int isValid = -1;
+
+    /** Date. */
+    long date = -1;
+
+    /** Field. */
+    int field = -1;
+
+    /**
+     * Default constructor.
+     */
+    public BuildConditionCompacted() {
+    }
+
+    /**
+     * @param compactor Compacter.
+     * @param cond Build condition.
+     */
+    public BuildConditionCompacted(IStringCompactor compactor, BuildCondition cond) {
+        buildId = cond.buildId;
+        username = compactor.getStringId(cond.username);
+        isValid = cond.isValid ? 1 : 0;
+        date = cond.date.getTime();
+        field = compactor.getStringId(cond.field);
+    }
+
+    /**
+     * @param compactor Compactor.
+     */
+    public BuildCondition toBuildCondition(IStringCompactor compactor) {
+        BuildCondition cond = new BuildCondition();
+
+        cond.buildId = buildId;
+        cond.isValid = isValid == 1;
+        cond.date = new Date(date);
+        cond.username = compactor.getStringFromId(username);
+        cond.field = compactor.getStringFromId(field);
+
+        return cond;
+    }
+}
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/condition/BuildConditionDao.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/condition/BuildConditionDao.java
new file mode 100644
index 0000000..633eddc
--- /dev/null
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/condition/BuildConditionDao.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.ci.tcbot.condition;
+
+import javax.inject.Inject;
+import javax.inject.Provider;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.ci.db.TcHelperDb;
+import org.apache.ignite.ci.teamcity.ignited.IStringCompactor;
+
+public class BuildConditionDao {
+    /** Cache name*/
+    public static final String BUILD_CONDITIONS_CACHE_NAME = "buildConditions";
+
+    /** Ignite provider. */
+    @Inject private Provider<Ignite> igniteProvider;
+
+    /** Builds cache. */
+    private IgniteCache<Long, BuildConditionCompacted> buildsCache;
+
+    /** Compactor. */
+    @Inject private IStringCompactor compactor;
+
+    /**
+     * Initialize
+     */
+    public void init () {
+        Ignite ignite = igniteProvider.get();
+        buildsCache = ignite.getOrCreateCache(TcHelperDb.getCacheV2Config(BUILD_CONDITIONS_CACHE_NAME));
+    }
+
+    /**
+     * @param srvIdMaskHigh Server id mask high.
+     * @param buildId Build id.
+     */
+    private long buildIdToCacheKey(long srvIdMaskHigh, int buildId) {
+
+        return (long)buildId | srvIdMaskHigh << 32;
+    }
+
+    public BuildCondition getBuildCondition(long srvIdMaskHigh, int buildId) {
+        long key = buildIdToCacheKey(srvIdMaskHigh, buildId);
+
+        return buildsCache.containsKey(key) ? buildsCache.get(key).toBuildCondition(compactor) : null;
+    }
+
+    public boolean setBuildCondition(long srvIdMaskHigh, BuildCondition cond) {
+        long key = buildIdToCacheKey(srvIdMaskHigh, cond.buildId);
+
+        if (cond.isValid)
+            return buildsCache.remove(key);
+        else {
+            if (!buildsCache.containsKey(key)) {
+                buildsCache.put(key, new BuildConditionCompacted(compactor, cond));
+
+                return true;
+            }
+        }
+
+        return false;
+    }
+}
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/teamcity/ignited/ITeamcityIgnited.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/teamcity/ignited/ITeamcityIgnited.java
index fadf0fb..bd85911 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/teamcity/ignited/ITeamcityIgnited.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/teamcity/ignited/ITeamcityIgnited.java
@@ -18,6 +18,7 @@ package org.apache.ignite.ci.teamcity.ignited;
 
 import java.util.List;
 import javax.annotation.Nullable;
+import org.apache.ignite.ci.tcbot.condition.BuildCondition;
 import org.apache.ignite.ci.tcmodel.hist.BuildRef;
 import org.apache.ignite.ci.tcmodel.result.Build;
 
@@ -40,7 +41,7 @@ public interface ITeamcityIgnited {
         @Nullable String branchName);
 
     /**
-     * Trigger build. Enforces TC Bot to load all buidls related to this triggered one.
+     * Trigger build. Enforces TC Bot to load all builds related to this triggered one.
      *
      * @param buildTypeId Build type identifier.
      * @param branchName Branch name.
@@ -56,4 +57,18 @@ public interface ITeamcityIgnited {
     public static int serverIdToInt(String srvId) {
         return Math.abs(srvId.hashCode());
     }
+
+    /**
+     * Check build condition.
+     *
+     * @param buildId Build id.
+     */
+    public boolean buildIsValid(int buildId);
+
+    /**
+     * Set build condition.
+     *
+     * @param cond Condition.
+     */
+    public boolean setBuildCondition(BuildCondition cond);
 }
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/teamcity/ignited/TeamcityIgnitedImpl.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/teamcity/ignited/TeamcityIgnitedImpl.java
index 0667976..b5bab97 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/teamcity/ignited/TeamcityIgnitedImpl.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/teamcity/ignited/TeamcityIgnitedImpl.java
@@ -30,6 +30,8 @@ import org.apache.ignite.ci.ITeamcity;
 import org.apache.ignite.ci.di.AutoProfiling;
 import org.apache.ignite.ci.di.MonitoredTask;
 import org.apache.ignite.ci.di.scheduler.IScheduler;
+import org.apache.ignite.ci.tcbot.condition.BuildCondition;
+import org.apache.ignite.ci.tcbot.condition.BuildConditionDao;
 import org.apache.ignite.ci.tcmodel.hist.BuildRef;
 import org.apache.ignite.ci.tcmodel.result.Build;
 import org.apache.ignite.ci.teamcity.pure.ITeamcityConn;
@@ -47,16 +49,19 @@ public class TeamcityIgnitedImpl implements ITeamcityIgnited {
     /** Build reference DAO. */
     @Inject private BuildRefDao buildRefDao;
 
+    /** Build condition DAO. */
+    @Inject private BuildConditionDao buildConditionDao;
+
     /** Server ID mask for cache Entries. */
     private long srvIdMaskHigh;
 
-
     public void init(String srvId, ITeamcityConn conn) {
         this.srvId = srvId;
         this.conn = conn;
 
         srvIdMaskHigh = ITeamcityIgnited.serverIdToInt(srvId);
         buildRefDao.init(); //todo init somehow in auto
+        buildConditionDao.init();
     }
 
     /** {@inheritDoc} */
@@ -91,6 +96,18 @@ public class TeamcityIgnitedImpl implements ITeamcityIgnited {
         return build;
     }
 
+    /** {@inheritDoc} */
+    @Override public boolean buildIsValid(int buildId) {
+        BuildCondition cond = buildConditionDao.getBuildCondition(srvIdMaskHigh, buildId);
+
+        return cond == null || cond.isValid;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean setBuildCondition(BuildCondition cond) {
+        return buildConditionDao.setBuildCondition(srvIdMaskHigh, cond);
+    }
+
     /**
      *
      */
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/teamcity/ignited/TeamcityIgnitedModule.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/teamcity/ignited/TeamcityIgnitedModule.java
index 3909e8d..16ad2cc 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/teamcity/ignited/TeamcityIgnitedModule.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/teamcity/ignited/TeamcityIgnitedModule.java
@@ -18,6 +18,7 @@ package org.apache.ignite.ci.teamcity.ignited;
 
 import com.google.inject.AbstractModule;
 import com.google.inject.internal.SingletonScope;
+import org.apache.ignite.ci.tcbot.condition.BuildConditionDao;
 import org.apache.ignite.ci.teamcity.pure.ITeamcityHttpConnection;
 import org.apache.ignite.ci.teamcity.restcached.TcRestCachedModule;
 import org.jetbrains.annotations.Nullable;
@@ -33,6 +34,7 @@ public class TeamcityIgnitedModule extends AbstractModule {
     @Override protected void configure() {
         bind(ITeamcityIgnitedProvider.class).to(TcIgnitedCachingProvider.class).in(new SingletonScope());
         bind(BuildRefDao.class).in(new SingletonScope());
+        bind(BuildConditionDao.class).in(new SingletonScope());
 
         bind(IStringCompactor.class).to(IgniteStringCompactor.class).in(new SingletonScope());
 
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/current/BuildStatisticsSummary.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/current/BuildStatisticsSummary.java
index d3cd968..0c604cd 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/current/BuildStatisticsSummary.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/current/BuildStatisticsSummary.java
@@ -17,6 +17,8 @@
 
 package org.apache.ignite.ci.web.model.current;
 
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
@@ -32,18 +34,20 @@ import org.apache.ignite.ci.tcmodel.hist.BuildRef;
 import org.apache.ignite.ci.tcmodel.result.Build;
 import org.apache.ignite.ci.tcmodel.result.TestOccurrencesRef;
 import org.apache.ignite.ci.tcmodel.result.problems.ProblemOccurrence;
-import org.apache.ignite.ci.util.TimeUtil;
-import org.apache.ignite.ci.web.IBackgroundUpdatable;
 import org.apache.ignite.ci.web.rest.parms.FullQueryParams;
 
 /**
  * Summary of build statistics.
  */
-public class BuildStatisticsSummary extends UpdateInfo implements IBackgroundUpdatable {
+public class BuildStatisticsSummary {
     /** Short problem names. */
     public static final String TOTAL = "TOTAL";
 
-    private static Map<String, String> shortProblemNames = new HashMap<>();
+    /** Short problem names map. Full name - key, short name - value. */
+    public static BiMap<String, String> shortProblemNames = HashBiMap.create();
+
+    /** Full problem names map. Short name - key, full name - value. */
+    public static BiMap<String, String> fullProblemNames;
 
     static {
         shortProblemNames.put(TOTAL, "TT");
@@ -51,6 +55,8 @@ public class BuildStatisticsSummary extends UpdateInfo implements IBackgroundUpd
         shortProblemNames.put(ProblemOccurrence.TC_JVM_CRASH, "JC");
         shortProblemNames.put(ProblemOccurrence.TC_OOME, "OO");
         shortProblemNames.put(ProblemOccurrence.TC_EXIT_CODE, "EC");
+
+        fullProblemNames = shortProblemNames.inverse();
     }
 
     /** Build with test and problems references. */
@@ -74,6 +80,9 @@ public class BuildStatisticsSummary extends UpdateInfo implements IBackgroundUpd
     /** Is fake stub. */
     public boolean isFakeStub;
 
+    /** Is valid. */
+    public boolean isValid = true;
+
     /**
      * @param buildId Build id.
      */
@@ -204,11 +213,6 @@ public class BuildStatisticsSummary extends UpdateInfo implements IBackgroundUpd
     }
 
     /** {@inheritDoc} */
-    @Override public void setUpdateRequired(boolean update) {
-        updateRequired = update;
-    }
-
-    /** {@inheritDoc} */
     @Override public boolean equals(Object o) {
         if (this == o)
             return true;
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/hist/BuildsHistory.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/hist/BuildsHistory.java
index 90b78f0..7f88a98 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/hist/BuildsHistory.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/hist/BuildsHistory.java
@@ -19,14 +19,18 @@ package org.apache.ignite.ci.web.model.hist;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.Lists;
 import java.io.UncheckedIOException;
+import java.lang.reflect.Array;
 import java.text.DateFormat;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -35,6 +39,8 @@ import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
 import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
 import javax.servlet.ServletContext;
 import org.apache.ignite.ci.IAnalyticsEnabledTeamcity;
 import org.apache.ignite.ci.ITcHelper;
@@ -43,6 +49,8 @@ import org.apache.ignite.ci.tcbot.chain.BuildChainProcessor;
 import org.apache.ignite.ci.tcmodel.result.Build;
 import org.apache.ignite.ci.tcmodel.result.tests.TestOccurrence;
 import org.apache.ignite.ci.tcmodel.result.tests.TestOccurrences;
+import org.apache.ignite.ci.teamcity.ignited.ITeamcityIgnited;
+import org.apache.ignite.ci.teamcity.ignited.ITeamcityIgnitedProvider;
 import org.apache.ignite.ci.user.ICredentialsProv;
 import org.apache.ignite.ci.web.CtxListener;
 import org.apache.ignite.ci.web.model.current.BuildStatisticsSummary;
@@ -99,14 +107,26 @@ public class BuildsHistory {
 
         IAnalyticsEnabledTeamcity teamcity = tcHelper.server(srvId, prov);
 
+        ITeamcityIgnitedProvider tcIgnitedProv = CtxListener.getInjector(context)
+            .getInstance(ITeamcityIgnitedProvider.class);
+
+        ITeamcityIgnited ignited = tcIgnitedProv.server(srvId, prov);
+
         int[] finishedBuildsIds = teamcity.getBuildNumbersFromHistory(buildTypeId, branchName,
             sinceDateFilter, untilDateFilter);
 
-        initStatistics(teamcity, finishedBuildsIds);
+        Map<Integer, Boolean> buildIdsWithConditions = IntStream.of(finishedBuildsIds)
+            .boxed().collect(Collectors.toMap(v -> v, ignited::buildIsValid,  (e1, e2) -> e1, LinkedHashMap::new));
 
-        if (!skipTests) {
-            initFailedTests(teamcity, finishedBuildsIds);
-        }
+        initStatistics(teamcity, buildIdsWithConditions);
+
+        List<Integer> validBuilds = buildIdsWithConditions.keySet()
+            .stream()
+            .filter(buildIdsWithConditions::get)
+            .collect(Collectors.toList());
+
+        if (!skipTests)
+            initFailedTests(teamcity, validBuilds);
 
         ObjectMapper objectMapper = new ObjectMapper();
 
@@ -118,12 +138,13 @@ public class BuildsHistory {
     }
 
     /** */
-    private void initStatistics(IAnalyticsEnabledTeamcity teamcity, int[] buildIds) {
+    private void initStatistics(IAnalyticsEnabledTeamcity teamcity, Map<Integer, Boolean> buildIdsWithConditions) {
         List<Future<BuildStatisticsSummary>> buildStatiscsFutures = new ArrayList<>();
 
-        for (int buildId : buildIds) {
+        for (int buildId : buildIdsWithConditions.keySet()) {
             Future<BuildStatisticsSummary> buildFuture = CompletableFuture.supplyAsync(() -> {
                 BuildStatisticsSummary buildsStatistic = new BuildStatisticsSummary(buildId);
+                buildsStatistic.isValid = buildIdsWithConditions.get(buildId);
 
                 buildsStatistic.initialize(teamcity);
 
@@ -178,7 +199,7 @@ public class BuildsHistory {
     }
 
     /** */
-    private void initFailedTests(IAnalyticsEnabledTeamcity teamcity, int[] buildIds) {
+    private void initFailedTests(IAnalyticsEnabledTeamcity teamcity, List<Integer> buildIds) {
         List<Future<Void>> buildProcessorFutures = new ArrayList<>();
 
         for (int buildId : buildIds) {
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/build/GetBuildTestFailures.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/build/GetBuildTestFailures.java
index fdfe218..dffbb69 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/build/GetBuildTestFailures.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/build/GetBuildTestFailures.java
@@ -17,10 +17,12 @@
 
 package org.apache.ignite.ci.web.rest.build;
 
+import com.google.common.collect.BiMap;
 import java.text.ParseException;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
 import com.google.inject.Injector;
+import org.apache.ignite.ci.tcbot.condition.BuildCondition;
 import org.apache.ignite.ci.tcbot.chain.BuildChainProcessor;
 import org.apache.ignite.ci.IAnalyticsEnabledTeamcity;
 import org.apache.ignite.ci.ITcHelper;
@@ -29,8 +31,11 @@ import org.apache.ignite.ci.analysis.FullChainRunCtx;
 import org.apache.ignite.ci.analysis.mode.LatestRebuildMode;
 import org.apache.ignite.ci.analysis.mode.ProcessLogsMode;
 import org.apache.ignite.ci.tcmodel.hist.BuildRef;
+import org.apache.ignite.ci.teamcity.ignited.ITeamcityIgnited;
+import org.apache.ignite.ci.teamcity.ignited.ITeamcityIgnitedProvider;
 import org.apache.ignite.ci.tcmodel.result.tests.TestRef;
 import org.apache.ignite.ci.user.ICredentialsProv;
+import org.apache.ignite.ci.web.model.current.BuildStatisticsSummary;
 import org.apache.ignite.ci.web.model.hist.BuildsHistory;
 import org.apache.ignite.ci.web.BackgroundUpdater;
 import org.apache.ignite.ci.web.CtxListener;
@@ -53,6 +58,8 @@ import javax.ws.rs.core.MediaType;
 import java.util.Collections;
 import java.util.concurrent.atomic.AtomicInteger;
 
+import static com.google.common.base.Strings.isNullOrEmpty;
+
 @Path(GetBuildTestFailures.BUILD)
 @Produces(MediaType.APPLICATION_JSON)
 public class GetBuildTestFailures {
@@ -191,6 +198,44 @@ public class GetBuildTestFailures {
             + "&tab=testDetails" : null;
     }
 
+    /**
+     * Mark builds as "valid" or "invalid".
+     *
+     * @param buildId Build id.
+     * @param isValid Is valid.
+     * @param field Field.
+     * @param serverId Server.
+     */
+    @GET
+    @Path("condition")
+    public Boolean setBuildCondition(
+        @QueryParam("buildId") Integer buildId,
+        @QueryParam("isValid") Boolean isValid,
+        @QueryParam("field") String field,
+        @QueryParam("serverId") String serverId) {
+        String srvId = isNullOrEmpty(serverId) ? "apache" : serverId;
+
+        if (buildId == null || isValid == null)
+            return null;
+
+        final ICredentialsProv prov = ICredentialsProv.get(req);
+
+        if (!prov.hasAccess(srvId))
+            throw ServiceUnauthorizedException.noCreds(srvId);
+
+        ITeamcityIgnitedProvider tcIgnitedProv = CtxListener.getInjector(ctx)
+            .getInstance(ITeamcityIgnitedProvider.class);
+
+        ITeamcityIgnited ignited = tcIgnitedProv.server(srvId, prov);
+
+        BiMap<String, String> problemNames = BuildStatisticsSummary.fullProblemNames;
+
+        BuildCondition buildCond =
+            new BuildCondition(buildId, prov.getPrincipalId(), isValid, problemNames.getOrDefault(field, field));
+
+        return ignited.setBuildCondition(buildCond);
+    }
+
     @GET
     @Path("history")
     public BuildsHistory getBuildsHistory(
@@ -210,10 +255,10 @@ public class GetBuildTestFailures {
         if (Boolean.valueOf(skipTests))
             builder.skipTests();
 
-        BuildsHistory buildsHistory = builder.build();
+        BuildsHistory buildsHist = builder.build();
 
-        buildsHistory.initialize(ICredentialsProv.get(req), ctx);
+        buildsHist.initialize(ICredentialsProv.get(req), ctx);
 
-        return buildsHistory;
+        return buildsHist;
     }
 }
diff --git a/ignite-tc-helper-web/src/main/webapp/comparison.html b/ignite-tc-helper-web/src/main/webapp/comparison.html
index 65348c9..1d4c13c 100644
--- a/ignite-tc-helper-web/src/main/webapp/comparison.html
+++ b/ignite-tc-helper-web/src/main/webapp/comparison.html
@@ -11,155 +11,154 @@
     <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.css" />
     <link rel="stylesheet" href="css/style-1.5.css">
     <link rel="stylesheet" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
+    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.3.1/css/all.css"
+          integrity="sha384-mzrmE5qonljUremFsqc01SB46JvROS7bZs3IO2EmfFsd15uHvIt+Y8vEf7N7fWAU" crossorigin="anonymous">
     <script src="js/common-1.6.js"></script>
     <script src="https://d3js.org/d3.v4.min.js"></script>
 </head>
 <body>
 <br>
 <br>
+
 <table style="table-layout: fixed" class="compare">
     <tr>
-        <th class="section"  width="15%">DATE INTERVAL</th>
-        <th width="5%"></th>
-        <th style="text-align: center;" width="40%"><input type='text' name='daterange1'/></th>
-        <th style="text-align: center;" width="40%"><input type='text' name='daterange2'/></th>
-    </tr><tr><td></td><td></td><td></td></tr>
-    <tr><td class="field">DURATION</td>
-        <td><img id="clickGraphDuration" src='/img/browser.png'></td>
-        <td class="mmm1 data1" id="Duration1" data-allow-highlight="true"></td>
-        <td class="mmm2 data2" id="Duration2" data-allow-highlight="true"></td>
-    </tr>
-    <tr id="showGraphDuration" style="display: none;"><td></td>
-        <td></td>
-        <td style="text-align: center;"><svg id="graphDuration1" width="500" height="200"></svg></td>
-        <td style="text-align: center;"><svg id="graphDuration2" width="500" height="200"></svg></td>
+        <th class="section"  width="13%">DATE INTERVAL</th>
+        <th width="3%"></th>
+        <th style="text-align: center;" width="42%"><input type='text' name='daterange1'/></th>
+        <th style="text-align: center;" width="42%"><input type='text' name='daterange2'/></th>
     </tr>
-    <tr id="showInfo" style="display: none;">
-        <td class="section">FEATURES</td><td></td>
+    <tr id="showInfo">
+        <td class="section">SHOW INVALID BUILD</td>
+        <td class="icon"><div class="switch-btn builds"></div></td>
         <td style="text-align: center;" id="info1"></td>
         <td style="text-align: center;" id="info2"></td>
     </tr>
-    <tr><td class="section">TESTS</td><td></td><td class="title mmm1"></td><td class="title mmm2"></td></tr>
+    <tr><td class="field">DURATION</td>
+        <td class="icon"><img id="clickGraphDuration" src='/img/browser.png'></td>
+        <td class="mmm data 1" id="Duration1" data-allow-highlight="true"></td>
+        <td class="mmm data 2" id="Duration2" data-allow-highlight="true"></td>
+    </tr>
+    <tr id="showGraphDuration" style="display: none;"><td></td>
+        <td></td>
+        <td style="text-align: center;"><svg class="graph 1" id="graphDuration1" width="500" height="200"></svg></td>
+        <td style="text-align: center;"><svg class="graph 2" id="graphDuration2" width="500" height="200"></svg></td>
+    </tr><tr></tr>
+    <tr><td class="section">TESTS</td><td></td><td class="mmm title 1"></td><td class="mmm title 2"></td></tr>
     <tr><td class="field">COUNT</td>
-        <td><img id="clickGraphCount" src='/img/browser.png'></td>
-        <td class="mmm1 data1" id="Count1" data-allow-highlight="false"></td>
-        <td class="mmm2 data2" id="Count2" data-allow-highlight="false"></td>
+        <td class="icon"><img id="clickGraphCount" src='/img/browser.png'></td>
+        <td class="mmm data 1" id="Count1" data-allow-highlight="false"></td>
+        <td class="mmm data 2" id="Count2" data-allow-highlight="false"></td>
     </tr>
     <tr id="showGraphCount" style="display: none;"><td></td>
         <td></td>
-        <td style="text-align: center;"><svg id="graphCount1" width="500" height="200"></svg></td>
-        <td style="text-align: center;"><svg id="graphCount2" width="500" height="200"></svg></td>
+        <td style="text-align: center;"><svg class="graph 1" id="graphCount1" width="500" height="200"></svg></td>
+        <td style="text-align: center;"><svg class="graph 2" id="graphCount2" width="500" height="200"></svg></td>
     </tr>
     <tr><td class="field">PASSED</td>
-        <td><img id="clickGraphPassed" src='/img/browser.png'></td>
-        <td class="mmm1 data1" id="Passed1" data-allow-highlight="false"></td>
-        <td class="mmm2 data2" id="Passed2" data-allow-highlight="false"></td>
+        <td class="icon"><img id="clickGraphPassed" src='/img/browser.png'></td>
+        <td class="mmm data 1" id="Passed1" data-allow-highlight="false"></td>
+        <td class="mmm data 2" id="Passed2" data-allow-highlight="false"></td>
     </tr>
     <tr id="showGraphPassed" style="display: none;"><td></td>
         <td></td>
-        <td style="text-align: center;"><svg id="graphPassed1" width="500" height="200"></svg></td>
-        <td style="text-align: center;"><svg id="graphPassed2" width="500" height="200"></svg></td>
+        <td style="text-align: center;"><svg class="graph 1" id="graphPassed1" width="500" height="200"></svg></td>
+        <td style="text-align: center;"><svg class="graph 2" id="graphPassed2" width="500" height="200"></svg></td>
     </tr>
     <tr><td class="field">FAILED</td>
-        <td><img id="clickGraphFailed" src='/img/browser.png'></td>
-        <td class="mmm1 data1" id="Failed1" data-allow-highlight="true"></td>
-        <td class="mmm2 data2" id="Failed2" data-allow-highlight="true"></td>
+        <td class="icon"><img id="clickGraphFailed" src='/img/browser.png'></td>
+        <td class="mmm data 1" id="Failed1" data-allow-highlight="true"></td>
+        <td class="mmm data 2" id="Failed2" data-allow-highlight="true"></td>
     </tr>
     <tr id="showGraphFailed" style="display: none;"><td></td>
         <td></td>
-        <td style="text-align: center;"><svg id="graphFailed1" width="500" height="200"></svg></td>
-        <td style="text-align: center;"><svg id="graphFailed2" width="500" height="200"></svg></td>
+        <td style="text-align: center;"><svg class="graph 1" id="graphFailed1" width="500" height="200"></svg></td>
+        <td style="text-align: center;"><svg class="graph 2" id="graphFailed2" width="500" height="200"></svg></td>
     </tr>
     <tr><td class="field">IGNORED</td>
-        <td><img id="clickGraphIgnored" src='/img/browser.png'></td>
-        <td class="mmm1 data1" id="Ignored1" data-allow-highlight="false"></td>
-        <td class="mmm2 data2" id="Ignored2" data-allow-highlight="false"></td>
+        <td class="icon"><img id="clickGraphIgnored" src='/img/browser.png'></td>
+        <td class="mmm data 1" id="Ignored1" data-allow-highlight="false"></td>
+        <td class="mmm data 2" id="Ignored2" data-allow-highlight="false"></td>
     </tr>
     <tr id="showGraphIgnored" style="display: none;"><td></td>
         <td></td>
-        <td style="text-align: center;"><svg id="graphIgnored1" width="500" height="200"></svg></td>
-        <td style="text-align: center;"><svg id="graphIgnored2" width="500" height="200"></svg></td>
+        <td style="text-align: center;"><svg class="graph 1" id="graphIgnored1" width="500" height="200"></svg></td>
+        <td style="text-align: center;"><svg class="graph 2" id="graphIgnored2" width="500" height="200"></svg></td>
     </tr>
     <tr><td class="field">MUTED</td>
-        <td><img id="clickGraphMuted" src='/img/browser.png'></td>
-        <td class="mmm1 data1" id="Muted1" data-allow-highlight="false"></td>
-        <td class="mmm2 data2" id="Muted2" data-allow-highlight="false"></td></tr>
+        <td class="icon"><img id="clickGraphMuted" src='/img/browser.png'></td>
+        <td class="mmm data 1" id="Muted1" data-allow-highlight="false"></td>
+        <td class="mmm data 2" id="Muted2" data-allow-highlight="false"></td></tr>
     <tr id="showGraphMuted" style="display: none;"><td></td>
         <td></td>
-        <td style="text-align: center;"><svg id="graphMuted1" width="500" height="200"></svg></td>
-        <td style="text-align: center;"><svg id="graphMuted2" width="500" height="200"></svg></td>
+        <td style="text-align: center;"><svg class="graph 1" id="graphMuted1" width="500" height="200"></svg></td>
+        <td style="text-align: center;"><svg class="graph 2" id="graphMuted2" width="500" height="200"></svg></td>
     </tr>
-    <tr><td class="section">PROBLEMS</td><td></td><td class="title mmm1"></td><td class="title mmm2"></td></tr>
+    <tr><td class="section">PROBLEMS</td><td></td><td class="mmm title 1"></td><td class="mmm title 2"></td></tr>
     <tr style="display: none;"><td></td><td></td><td></td></tr>
     <tr><td class="field">TOTAL</td>
-        <td><img id="clickGraphTT" src='/img/browser.png'></td>
-        <td class="mmm1 data1" id="TT1" data-allow-highlight="true"></td>
-        <td class="mmm2 data2" id="TT2" data-allow-highlight="true"></td></tr>
+        <td class="icon"><img id="clickGraphTT" src='/img/browser.png'></td>
+        <td class="mmm data 1" id="TT1" data-allow-highlight="true"></td>
+        <td class="mmm data 2" id="TT2" data-allow-highlight="true"></td></tr>
     <tr id="showGraphTT" style="display: none;"><td></td>
         <td></td>
-        <td style="text-align: center;"><svg id="graphTT1" width="500" height="200"></svg></td>
-        <td style="text-align: center;"><svg id="graphTT2" width="500" height="200"></svg></td>
+        <td style="text-align: center;"><svg class="graph 1" id="graphTT1" width="500" height="200"></svg></td>
+        <td style="text-align: center;"><svg class="graph 2" id="graphTT2" width="500" height="200"></svg></td>
     </tr>
     <tr><td class="field">EXECUTION TIMEOUT</td>
-        <td><img id="clickGraphET" src='/img/browser.png'></td>
-        <td class="mmm1 data1" id="ET1" data-allow-highlight="true"></td>
-        <td class="mmm2 data2" id="ET2" data-allow-highlight="true"></td>
+        <td class="icon"><img id="clickGraphET" src='/img/browser.png'></td>
+        <td class="mmm data 1" id="ET1" data-allow-highlight="true"></td>
+        <td class="mmm data 2" id="ET2" data-allow-highlight="true"></td>
     </tr>
     <tr id="showGraphET" style="display: none;"><td></td>
         <td></td>
-        <td style="text-align: center;"><svg id="graphET1" width="500" height="200"></svg></td>
-        <td style="text-align: center;"><svg id="graphET2" width="500" height="200"></svg></td>
+        <td style="text-align: center;"><svg class="graph 1" id="graphET1" width="500" height="200"></svg></td>
+        <td style="text-align: center;"><svg class="graph 2" id="graphET2" width="500" height="200"></svg></td>
     </tr>
     <tr><td class="field">JVM CRASH</td>
-        <td><img id="clickGraphJC" src='/img/browser.png'></td>
-        <td class="mmm1 data1" id="JC1" data-allow-highlight="true"></td>
-        <td class="mmm2 data2" id="JC2" data-allow-highlight="true"></td>
+        <td class="icon"><img id="clickGraphJC" src='/img/browser.png'></td>
+        <td class="mmm data 1" id="JC1" data-allow-highlight="true"></td>
+        <td class="mmm data 2" id="JC2" data-allow-highlight="true"></td>
     </tr>
     <tr id="showGraphJC" style="display: none;"><td></td>
         <td></td>
-        <td style="text-align: center;"><svg id="graphJC1" width="500" height="200"></svg></td>
-        <td style="text-align: center;"><svg id="graphJC2" width="500" height="200"></svg></td>
+        <td style="text-align: center;"><svg class="graph 1" id="graphJC1" width="500" height="200"></svg></td>
+        <td style="text-align: center;"><svg class="graph 2" id="graphJC2" width="500" height="200"></svg></td>
     </tr>
     <tr><td class="field">OOME</td>
-        <td><img id="clickGraphOO" src='/img/browser.png'></td>
-        <td class="mmm1 data1" id="OO1" data-allow-highlight="true"></td>
-        <td class="mmm2 data2" id="OO2" data-allow-highlight="true"></td>
+        <td class="icon"><img id="clickGraphOO" src='/img/browser.png'></td>
+        <td class="mmm data 1" id="OO1" data-allow-highlight="true"></td>
+        <td class="mmm data 2" id="OO2" data-allow-highlight="true"></td>
     </tr>
     <tr id="showGraphOO" style="display: none;"><td></td>
         <td></td>
-        <td style="text-align: center;"><svg id="graphOO1" width="500" height="200"></svg></td>
-        <td style="text-align: center;"><svg id="graphOO2" width="500" height="200"></svg></td>
+        <td style="text-align: center;"><svg class="graph 1" id="graphOO1" width="500" height="200"></svg></td>
+        <td style="text-align: center;"><svg class="graph 2" id="graphOO2" width="500" height="200"></svg></td>
     </tr>
     <tr><td class="field">EXIT CODE</td>
-        <td><img id="clickGraphEC" src='/img/browser.png'></td>
-        <td class="mmm1 data1" id="EC1" data-allow-highlight="true"></td>
-        <td class="mmm2 data2" id="EC2" data-allow-highlight="true"></td>
+        <td class="icon"><img id="clickGraphEC" src='/img/browser.png'></td>
+        <td class="mmm data 1" id="EC1" data-allow-highlight="true"></td>
+        <td class="mmm data 2" id="EC2" data-allow-highlight="true"></td>
     </tr>
     <tr id="showGraphEC" style="display: none;"><td></td>
         <td></td>
-        <td style="text-align: center;"><svg id="graphEC1" width="500" height="200"></svg></td>
-        <td style="text-align: center;"><svg id="graphEC2" width="500" height="200"></svg></td>
+        <td style="text-align: center;"><svg class="graph 1" id="graphEC1" width="500" height="200"></svg></td>
+        <td style="text-align: center;"><svg class="graph 2" id="graphEC2" width="500" height="200"></svg></td>
     </tr>
-    <tr><td class="section">OTHER METRICS</td><td></td><td class="title t1"></td><td class="title t2"></td></tr>
+    <tr><td class="section">OTHER METRICS</td><td></td><td class="total title 1"></td><td class="total title 2"></td></tr>
     <tr style="display: none;"><td></td><td></td><td></td></tr>
     <tr><td class="field">RUNS COUNT</td>
         <td></td>
-        <td class="t1 data1" id="RunsCount1"></td>
-        <td class="t2 data2" id="RunsCount2"></td>
+        <td class="total data 1" id="RunsCount1"></td>
+        <td class="total data 2" id="RunsCount2"></td>
     </tr>
 </table><br>
 <table style="table-layout: fixed" id="testsTable" class="testsTable">
     <tbody>
     <tr>
-        <th class="failedTestsHeader" width="15%">FAILED TESTS</th>
-        <th width="5%">
-            <label class="switch">
-                <input type="checkbox" onclick="toggleTests()">
-                <span class="slider"></span>
-            </label>
-        </th>
-        <th width="40%"></th>
-        <th width="40%"></th>
+        <th class="failedTestsHeader" width="13%">FAILED TESTS</th>
+        <th width="3%"><div class="switch-btn tests"></div></th>
+        <th width="42%"></th>
+        <th width="42%"></th>
         </tr>
     </tbody>
 </table>
@@ -176,20 +175,27 @@
 
 <div id="version"></div>
 <script>
-    const TESTS_TABLE = '#testsTable';
-    const SKIP_TESTS = 'skipTests=true';
-    let oneWeekAgo = new Date();
-    let twoWeekAgo = new Date();
-    let g_updTimer = null;
-    let testsTrigger = false;
-
-    /** Structure for storing tests by suites parsed response for every date interval. */
-    let mergedTestsResults = {1 : {}, 2 : {} };
-
+    let oneWeekAgo = new Date(),
+        twoWeekAgo = new Date(),
+        invalidInclude = false,
+        markBuildId = 0,
+        testsTrigger = false;
 
     oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
     twoWeekAgo.setDate(twoWeekAgo.getDate() - 14);
 
+    function getDateFewWeeksAgo(numberOfWeeksAgo) {
+        let date = new Date();
+
+        if (isDefinedAndFilled(numberOfWeeksAgo) && Number.isInteger(numberOfWeeksAgo))
+            date.setDate(date.getDate() - numberOfWeeksAgo * 7);
+
+        return date;
+    }
+
+    /** Structure for storing tests by suites parsed response for every date interval. */
+    let mergedTestsResults = {1 : {}, 2 : {} };
+
     let dateIntervals = {1: {start: moment(oneWeekAgo), end: moment()},
         2: {start: moment(twoWeekAgo), end: moment(oneWeekAgo)}};
 
@@ -218,59 +224,130 @@
         formatYear = d3.timeFormat("%Y"),
         parseDuration = d3.utcParse("%s");
 
-    const tOcc = ["Count", "Passed", "Failed", "Ignored", "Muted"],
-        prOcc = ["TT", "ET", "JC", "OO", "EC"],
+    const fieldNames = new Map([['Duration', 'Duration'], ['Count', 'Count'], ['Passed', 'Passed'], ['Failed','Failed'],
+            ['Ignored','Ignored'], ['Muted','Muted'], ['OO' ,'OOME'], ['TT', 'Total'], ['JC', 'JVM crash'],
+            ['EC', 'Exit code'], ['ET', 'Execution timeout']]),
         mmmTitle = "min - median - max",
         tTitle = "total",
-        duration = "Duration";
+        duration = "Duration",
+        TESTS_TABLE = '#testsTable',
+        SKIP_TESTS = 'skipTests=true',
+        data = [];
+
+    class Data {
+        constructor(num, array, sinceDate, untilDate) {
+            this.num = num;
+            this.map = new Map(array.map(item => [item.buildId, new BuildStatistics(item)]));
+            this.sinceDate = sinceDate;
+            this.untilDate = untilDate;
+        }
 
-    function multiFormat(date) {
-        return (d3.timeSecond(date) < date ? formatMillisecond
-            : d3.timeMinute(date) < date ? formatSecond
-                : d3.timeHour(date) < date ? formatMinute
-                    : d3.timeDay(date) < date ? formatHour
-                        : d3.timeMonth(date) < date ? (d3.timeWeek(date) < date ? formatDay : formatWeek)
-                            : d3.timeYear(date) < date ? formatMonth
-                                : formatYear)(date);
-    }
+        get hasInvalidBuilds() {
+            return this.invalidBuildsCount !== 0;
+        }
 
-    function dateRangePickerParam(data1, data2) {
-        return {
-            "opens" : 'left',
-            "maxSpan": { "days": 7 },
-            "locale": {
-                "format": "DD/MM/YYYY", "separator": " - ", "applyLabel": "Apply", "cancelLabel": "Cancel",
-                "fromLabel": "From", "toLabel": "To", "customRangeLabel": "Custom", "weekLabel": "W",
-                "daysOfWeek": [ "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa" ],
-                "monthNames": [ "January", "February", "March", "April", "May", "June", "July",
-                    "August", "September", "October", "November", "December" ],
-                "firstDay": 1
-            },
-            "startDate": moment(data1).format("DD-MM-YYYY"), "endDate": moment(data2).format("DD-MM-YYYY")
+        get invalidBuildsCount() {
+            let count = 0;
+            for(let item of this.map.values()) {
+                if (!item.isValid)
+                    count++
+            }
+
+            return count;
         }
-    }
 
-    function getMinMaxMedian(arr) {
-        let newArr = arr.slice();
-        newArr = newArr.sort(function(a, b){ return a - b; });
+        get isEmpty() {
+            return this.builds.length === 0;
+        }
 
-        let i = newArr.length / 2;
+        get builds() {
+            let builds = [];
+            for (let item of this.map.values()) {
+                if (invalidInclude || item.isValid) {
+                    builds.push(item);
+                }
+            }
+            return builds;
+        }
 
-        let result = {};
-        result.median = i % 1 === 0 ? (newArr[i - 1] + newArr[i]) / 2 : newArr[Math.floor(i)];
-        result.min = newArr[0];
-        result.max = newArr[newArr.length - 1];
+        get realSinceDate() {
+            return this.builds[0].date;
+        }
 
-        return result;
+        get realUntilDate() {
+            return this.builds[this.builds.length - 1].date;
+        }
+
+        get fieldsStatistics() {
+            let fieldsStatistics = new FieldsStatistics(this.num);
+            for(let build of this.builds) {
+                for (let fieldName of fieldNames.keys())
+                    fieldsStatistics[fieldName].push(build.stat[fieldName]);
+
+                fieldsStatistics.dates.push(build.date);
+                fieldsStatistics.buildIds.push(build.date);
+            }
+
+            return fieldsStatistics;
+        }
     }
 
-    function parseMedian(string) {
-        let stringMedian = string.substring(string.indexOf("-") + 2, string.lastIndexOf("-") - 1);
+    class FieldsStatistics {
+         constructor(num){
+             this.dates = [];
+             this.buildIds = [];
+             this.num = num;
+             this['Duration'] = [];
+             this['DurationF'] = [];
+             this['Count'] = [];
+             this['Passed'] = [];
+             this['Failed'] = [];
+             this['Ignored'] = [];
+             this['Muted'] = [];
+             this['OO'] = [];
+             this['TT'] = [];
+             this['JC'] = [];
+             this['EC'] = [];
+             this['ET'] = [];
+         }
+    }
 
-        if (stringMedian.indexOf(":") !== -1)
-            return (parseInt(stringMedian.split(":")[0]) * 60 + parseInt(stringMedian.split(":")[1])) * 60;
-        else
-            return parseFloat(stringMedian);
+    class BuildStatistics {
+        constructor(item) {
+            this.buildId = item.buildId;
+            this.date = parseTime(item.startDate);
+            this.stat = {
+                'Duration' : item.duration,
+                'DurationF' : parseDuration(parseInt(item.duration)),
+                'Count' : item.testOccurrences.count,
+                'Passed' : item.testOccurrences.passed,
+                'Failed' : item.testOccurrences.failed,
+                'Ignored' : item.testOccurrences.ignored,
+                'Muted' : item.testOccurrences.muted,
+                'OO' : item.totalProblems.OO,
+                'TT' : item.totalProblems.TT,
+                'JC' : item.totalProblems.JC,
+                'EC' : item.totalProblems.EC,
+                'ET' : item.totalProblems.ET
+            };
+            this.isValid = item.isValid;
+            this.conditionLocal = false;
+        }
+    }
+
+    class Statistic {
+        constructor(array, name, num) {
+            let newArr = array.slice();
+            newArr = newArr.sort(function(a, b){ return a - b; });
+
+            let i = newArr.length / 2;
+
+            this.median = i % 1 === 0 ? (newArr[i - 1] + newArr[i]) / 2 : newArr[Math.floor(i)];
+            this.min = newArr[0];
+            this.max = newArr[newArr.length - 1];
+            this.name = name;
+            this.num = num;
+        }
     }
 
     var changeDisplay = function(id) {
@@ -397,91 +474,129 @@
         window.open(res);
     }
 
-    function printStatistics(num, map, sinceDate, untilDate) {
+    function printStatistics(data) {
         clearBackgroundFromAllDataCells();
-        clearGraphs(num);
-
-        const anotherNum = (num === 1) ? 2 : 1;
+        clearGraphs(data.num);
+        fillAllDataCells(data.num, "");
 
-        let statistics = {},
-            dates = [],
-            buildIds = [];
+        if (data.isEmpty) {
+            printImportantMessage(data.num, "#ff0000", "No data for the selected period");
+            fillTitleForData(data.num, 'mmm', "");
 
-        statistics[duration] = [];
-
-        for (let i = 0; i < prOcc.length; i++) {
-            statistics[prOcc[i]] = [];
-            statistics[tOcc[i]] = [];
-        }
-
-        for (let j = 0; j < map.length; j++) {
-            dates[j] = parseTime(map[j].startDate);
-            buildIds[j] = map[j].buildId;
-            statistics[duration][j] = map[j].duration;
-
-            for (let i = 0; i < prOcc.length; i++) {
-                statistics[prOcc[i]][j] = map[j].totalProblems[prOcc[i]];
-                statistics[tOcc[i]][j] = map[j].testOccurrences[tOcc[i].toLowerCase()];
-            }
-        }
-
-        if (dates.length === 0) {
-            printImportantMessage(num, "#ff0000", "No data for the selected period");
-            fillAllDataCells(num, "");
-            $('.title' + num).html("");
+            if (data.hasInvalidBuilds && !invalidInclude)
+                printRunsCount(data);
+            else
+                fillTitleForData(data.num, 'total', "");
 
             return;
         } else {
-            let firstDate = moment(parseTime(map[0].startDate)).format("DD-MM-YYYY");
-            let lastDate = moment(parseTime(map[map.length - 1].startDate)).format("DD-MM-YYYY");
+            let firstDate = moment(data.realSinceDate).format("DD-MM-YYYY");
+            let lastDate = moment(data.realUntilDate).format("DD-MM-YYYY");
 
-            if ((sinceDate.format("DD-MM-YYYY") !== firstDate) || (untilDate.format("DD-MM-YYYY") !== lastDate)) {
-                printImportantMessage(num, "#ffb856", "Data for " +
+            if ((data.sinceDate.format("DD-MM-YYYY") !== firstDate) || (data.untilDate.format("DD-MM-YYYY") !== lastDate)) {
+                printImportantMessage(data.num, "#ffb856", "Data for " +
                     (firstDate === lastDate ? firstDate : ("the period from " + firstDate + " to " + lastDate)) + "");
-            } else {
-                $("#info" + num).html("");
-                if ($('#info' + anotherNum).text() === ''){
-                    $('#showInfo').css('display', 'none')
-                }
-            }
+            } else
+                $("#info" + data.num).html("");
         }
 
-        $('.title.mmm' + num).html(mmmTitle);
-        $('.mmm' + num).prop('title', mmmTitle);
-        $('.title.t' + num).html(tTitle);
-        $('.t' + num).prop('title', tTitle);
+        fillNameForMinMedianMaxColumn(mmmTitle);
+        fillNameForTotalColumn(tTitle);
 
-        fillCellWithStatistics(duration, num, statistics, dates, buildIds);
+        fillTitleForData(data.num, 'mmm', mmmTitle);
+        fillTitleForData(data.num, 'total', tTitle);
 
-        for (let i = 0; i < prOcc.length; i++) {
-            fillCellWithStatistics(prOcc[i], num, statistics, dates, buildIds);
-            fillCellWithStatistics(tOcc[i], num, statistics, dates, buildIds);
+        let fieldsStatistics = data.fieldsStatistics;
+
+        for (let fieldName of fieldNames.keys()) {
+            let stat = new Statistic(fieldsStatistics[fieldName], fieldName, data.num);
+
+            fillCellWithStatistics(stat);
+
+            drawGraph(data.builds, fieldName, stat.median, data.num);
         }
 
-        $('#RunsCount' + num).html(map.length);
+        printRunsCount(data);
     }
 
-    function fillCellWithStatistics(prefix, num, statistics, dates, buildIds) {
-        let result = getMinMaxMedian(statistics[prefix]);
+    function printRunsCount(data){
+        $('#RunsCount' + data.num).html(data.builds.length + "<br>" + (invalidInclude || (!data.hasInvalidBuilds) ? "" :
+            "<span class='compare title'>(" + data.invalidBuildsCount +
+            " invalid builds hide)</span>"));
+    }
 
-        if (prefix === duration)
-            $('#' + prefix + num).html(time(result.min) + " - " + time(result.median) + " - " + time(result.max));
+    function fillCellWithStatistics(stat) {
+        if (stat.name === duration)
+            $('#' + stat.name + stat.num).html(time(stat.min) + " - " + time(stat.median) + " - " + time(stat.max));
         else
-            $('#' + prefix + num).html(result.min + " - " + result.median +  " - " + result.max);
-
-        compareAndHighlight(prefix, num, result.median);
+            $('#' + stat.name + stat.num).html(stat.min + " - " + stat.median +  " - " + stat.max);
 
-        drawGraph(prefix, num, dates, statistics[prefix], buildIds);
+        compareAndHighlight(stat);
 
         function time(ms){
             return moment(ms * 1000).utcOffset(0).format("H:mm");
         }
     }
 
-    function compareAndHighlight(prefix, thisNum, thisMedian){
-        let anotherNum = (thisNum === 1) ? 2 : 1,
-            thisElement = $('#' + prefix + thisNum),
-            anotherElement =  $('#' + prefix + anotherNum);
+    function fillNameForTotalColumn(num, title) {
+        $('.title.total.' + num).html(title);
+    }
+
+    function fillNameForMinMedianMaxColumn(num, title) {
+        $('.title.mmm .' + num).html(title);
+    }
+
+    function fillTitleForData(num, cssClass, title) {
+        $('.' + cssClass + '.data.' + num).prop('title', title);
+    }
+
+    function loadData(num, sinceDate, untilDate, testsTrigger) {
+        loadGif(num);
+
+        mergedTestsResults[num] = {};
+
+        let url = 'rest/build/history?sinceDate=' + sinceDate.format("DDMMYYYY") +
+            '000001&untilDate=' + untilDate.format("DDMMYYYY") + '235959';
+
+        if (!testsTrigger)
+            url = url + '&' + SKIP_TESTS;
+
+        $.ajax({
+                url: url,
+                success: function (result) {
+                    data[num] = new Data(num, result.buildsStatistics, sinceDate, untilDate);
+
+                    printStatistics(data[num]);
+
+                    try {
+                        mergedTestsResults[num] = JSON.parse(result.mergedTestsJson);
+                    } catch (e) {
+                        printImportantMessage(num, "#ff0000", "Invalid server response. Unable to parse JSON");
+                    }
+
+                    printTests(generateCompareTestsResults(mergedTestsResults));
+                },
+                error: showErrInLoadStatus,
+                timeout: 1800000
+            }
+        );
+    }
+
+    function parseMedian(string) {
+        let stringMedian = string.substring(string.indexOf("-") + 2, string.lastIndexOf("-") - 1);
+
+        if (stringMedian.indexOf(":") !== -1)
+            return (parseInt(stringMedian.split(":")[0]) * 60 + parseInt(stringMedian.split(":")[1])) * 60;
+        else
+            return parseFloat(stringMedian);
+    }
+
+    function compareAndHighlight(stat){
+
+        let anotherNum = (stat.num === 1) ? 2 : 1,
+            thisElement = $('#' + stat.name + stat.num),
+            anotherElement =  $('#' + stat.name + anotherNum),
+            thisMedian = stat.median;
 
         if (thisElement.data('allowHighlight').toString() === "true") {
             let anotherMedian = parseMedian(anotherElement.text());
@@ -502,44 +617,26 @@
         loadData(2, moment(twoWeekAgo), moment(oneWeekAgo), testsTrigger);
 
         $.ajax({ url: "rest/branches/version",  success: showVersionInfo, error: showErrInLoadStatus });
-
-        if(g_updTimer == null) {
-            g_updTimer = setTimeout(tstTimeout, 3200);
-        }
-        setInterval(tstTimeout, 10000);
     });
 
-    function tstTimeout() {
-        if (g_updTimer != null) {
-            clearTimeout(g_updTimer);
-            g_updTimer = null;
-        }
-
-        if (g_updTimer == null) {
-            g_updTimer = setTimeout(tstTimeout, 3200);
-        }
-    }
-    
     function clearBackgroundFromAllDataCells(num){
-        $('.data' + (num == null ? '1, .data2' : num)).css('background', '');
+        $('.data.' + (num == null ? '1, .data.2' : num)).css('background', '');
     }
-    
+
     function fillAllDataCells(num, message) {
-        $('.data' + num).html(message);
+        $(".data." + num).html(message);
         $('.testsCntCell').html(message);
     }
 
     function clearGraphs(num) {
-        $("#graph" + duration + num).empty();
-        for (let i = 0; i < prOcc.length; i++) {
-            $("#graph" + prOcc[i] + num).empty();
-            $("#graph" + tOcc[i] + num).empty();
-        }
+        $(".graph." + num).empty();
     }
 
     function loadGif(num) {
         clearBackgroundFromAllDataCells();
+
         clearGraphs(num);
+
         fillAllDataCells(num, "<img src='/img/loading.gif' width=15px height=15px>");
     }
 
@@ -549,102 +646,51 @@
         $('#showInfo').css('display', '');
     }
 
-    function loadData(num, sinceDate, untilDate, testsTrigger) {
-        loadGif(num);
-
-        mergedTestsResults[num] = {};
-
-        let url = 'rest/build/history?sinceDate=' + sinceDate.format("DDMMYYYY") +
-            '000001&untilDate=' + untilDate.format("DDMMYYYY") + '235959';
-
-        if (!testsTrigger)
-            url = url + '&' + SKIP_TESTS;
-
-        $.ajax({
-                url: url,
-                success: function (result) {
-                    printStatistics(num, result.buildsStatistics, sinceDate, untilDate);
-
-                    try {
-                        mergedTestsResults[num] = JSON.parse(result.mergedTestsJson);
-                    } catch (e) {
-                        printImportantMessage(num, "#ff0000", "Invalid server response. Unable to parse JSON");
-                    }
-
-                    printTests(generateCompareTestsResults(mergedTestsResults));
-                },
-                error: showErrInLoadStatus,
-                timeout: 1800000
-            }
-        );
-    }
-
     $(function() {
-        $('input[name="daterange1"]').daterangepicker(
-            dateRangePickerParam(oneWeekAgo, new Date()), function (start, end, label) {
-                dateIntervals[1].start = start;
-
-                dateIntervals[1].end = end;
+        for (let i = 1; i <= 2; i++) {
+            $('input[name="daterange' + i + '"]').daterangepicker(
+                dateRangePickerParam(getDateFewWeeksAgo(i), getDateFewWeeksAgo(i - 1), i), function (start, end) {
+                    dateIntervals[i].start = start;
 
-                loadData(1, start, end, testsTrigger);
-            });
-    });
-
-    $(function() {
-        $('input[name="daterange2"]').daterangepicker(
-            dateRangePickerParam(twoWeekAgo, oneWeekAgo), function (start, end, label) {
-                dateIntervals[2].start = start;
+                    dateIntervals[i].end = end;
 
-                dateIntervals[2].end = end;
-
-                loadData(2, start, end, testsTrigger);
-            });
+                    loadData(i, start, end, testsTrigger);
+                });
+        }
     });
 
-    graphSpoiler(duration);
+    for (let fieldName of fieldNames.keys())
+        graphSpoiler(fieldName);
 
-    for (let i = 0; i < prOcc.length; i++) {
-        graphSpoiler(prOcc[i]);
-        graphSpoiler(tOcc[i]);
-    }
-    
-    function graphSpoiler(prefix) {
-        $("#clickGraph" + prefix).click(function() {
-            let element = $('#showGraph' + prefix);
+    function graphSpoiler(fieldName) {
+        $("#clickGraph" + fieldName).click(function() {
+            let element = $('#showGraph' + fieldName);
             element.css('display') === 'none' ? element.css('display', '') : element.css('display', 'none');
         });
     }
 
-    function drawGraph(prefix, num, dates, counts, buildIds) {
-        let data = [],
-            isDuration = prefix === duration;
-
-        for (let i = 0; i < dates.length; i++) {
-            data[i] = {};
-            data[i].date = dates[i];
-            data[i].value = (isDuration ? parseDuration(parseInt(counts[i])) : counts[i]);
-            data[i].buildId = buildIds[i];
-        }
-
-        let svg = d3.select("#graph" + prefix + num).append("svg:svg"),
+    function drawGraph(builds, fieldName, median, num) {
+        let isDuration = (fieldName === duration),
+            F = isDuration ? "F" : "",
+            svg = d3.select("#graph" + fieldName + num).append("svg:svg"),
             margin = {top: 20, right: 20, bottom: 30, left: 50},
             width = 500 - margin.left - margin.right,
             height = 200 - margin.top - margin.bottom,
             g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
 
-        let div = d3.select("body").append("div")
-            .attr("class", "tooltip")
-            .style("opacity", 0);
+        let buildDate = d3.select("body").append("div").attr("class", "tooltip buildDate").style("opacity", 0),
+            condition = d3.select("body").append("div").attr("class", "tooltip condition").style("opacity", 0),
+            mark = d3.select("body").append("div").attr("class", "tooltip mark").style("opacity", 0);
 
         let y = (isDuration ? d3.scaleTime() : d3.scaleLinear()).range([height, 0]),
             x = d3.scaleTime().rangeRound([0, width]);
 
         let line = d3.line()
             .x(function(d) { return x(d.date); })
-            .y(function(d) { return y(d.value); });
+            .y(function(d) { return y(d.stat[fieldName + F]); });
 
-        x.domain(d3.extent(data, function(d) { return d.date; }));
-        y.domain(d3.extent(data, function(d) { return d.value; }));
+        x.domain(d3.extent(builds, function(d) { return d.date; }));
+        y.domain(d3.extent(builds, function(d) { return d.stat[fieldName + F]; }));
 
         g.append("svg:g").attr("transform", "translate(0," + (height) + ")")
             .call(d3.axisBottom(x).tickFormat(multiFormat));
@@ -657,10 +703,22 @@
             .attr("x2",400)
             .attr("y1",200)
             .attr("y2",200)
-            .style("display", "None");
+            .attr("stroke", "#4682B4")
+            .attr("stroke-width", "1")
+            .style("display", "none");
+
+        let median_line = g.append("line")
+            .attr("class", "line")
+            .attr("x1",0)
+            .attr("x2",width)
+            .attr("y1",y(isDuration ? parseDuration(parseInt(median)) : median))
+            .attr("y2",y(isDuration ? parseDuration(parseInt(median)) : median))
+            .attr("stroke", "red")
+            .attr("stroke-width", "0.2")
+            .style("display", "block");
 
         g.append("path")
-            .datum(data)
+            .datum(builds)
             .attr("fill", "none")
             .attr("stroke", "steelblue")
             .attr("stroke-linejoin", "round")
@@ -668,46 +726,186 @@
             .attr("stroke-width", 1.5)
             .attr("d", line);
 
-        g.selectAll("dot").data(data)
+        g.selectAll("dot").data(builds)
             .enter()
             .append("circle")
             .attr("r", 3)
             .attr("cx", function(d){return x(d.date); })
-            .attr("cy", function(d){return y(d.value); })
-            .attr("class", "dot")
+            .attr("cy", function(d){return y(d.stat[fieldName + F]); })
+            .attr("class", function(d) {
+                return getDotClass(d);
+            })
             .on("mouseover", function(d) {
                 d3.select(this).transition().duration(100)
-                    .style("fill", "green")
+                    .style("fill", "#12AD5E")
                     .attr("r", 5)
                     .attr("onclick", "checkAvailable(" + d.buildId + "," +  d.date.getTime() + ");" +
-                    "return false;");
-                div.transition()
-                    .duration(200)
-                    .style("opacity", .8);
+                        "return false;");
 
-                let graphTd = document.getElementById("graph" + prefix + num).getBoundingClientRect(),
-                    scrollTop = window.pageYOffset || document.documentElement.scrollTop;
-
-                div	.html(formatTime(d.date))
-                    .style("left", graphTd.left + x(d.date) + (x(d.date) > (500 - 150) ? -150 : 0) + "px")
-                    .style("top", scrollTop + graphTd.top - 10 + "px" );
                 svg_aline.transition().duration(10)
                     .style("display", "block")
                     .attr("x1", x(d.date))
-                    .attr("y1", y(d.value))
+                    .attr("y1", y(d.stat[fieldName + F]))
                     .attr("x2", x(d.date))
-                    .attr("y2", height)
+                    .attr("y2", height);
+
+                let graphTd = document.getElementById("graph" + fieldName + num).getBoundingClientRect(),
+                    scrollTop = window.pageYOffset || document.documentElement.scrollTop,
+                    top = scrollTop + graphTd.top + y(d.stat[fieldName + F]) - 10,
+                    partLeft = graphTd.left + x(d.date),
+                    buildDateNearTheBorder = x(d.date) > (440 - 140);
+
+                buildDate.html(formatTime(d.date))
+                    .style("left", partLeft + (buildDateNearTheBorder ? -99 : 49) + "px")
+                    .style("top", top + "px" ).transition()
+                    .duration(200).style("opacity", .8).style("display", "block");
+
+                mark.html('<i class="fas fa-' + (markBuildId === d.buildId ? 'eraser' : 'highlighter') + '"></i>')
+                    .style("left", partLeft + (buildDateNearTheBorder ? 49 : 23) + "px")
+                    .style("top", top + "px" )
+                    .attr("onclick", "markBuild(" + d.buildId + "); return false;").transition()
+                    .duration(200).style("opacity", .8).style("display", "block");
+
+                condition.html('<i class="' + (!d.isValid ? 'fas fa-undo' : 'far fa-trash')
+                    + '-alt"></i>')
+                    .style("left", partLeft + (buildDateNearTheBorder ? 75 : -3) + "px")
+                    .style("top", top + "px" )
+                    .attr("onclick", "setCondition(" + num + ", " + d.buildId + ", " +
+                        (isDuration ? d.stat[fieldName + F].getTime() / 1000 : d.stat[fieldName + F]) +
+                        ", " + median + ", '" + (fieldName + F) + "'); return false;").transition()
+                    .duration(200).style("opacity", .8).style("display", "block");
             })
 
-            .on("mouseout", function(d) {
+            .on("mouseout", function() {
                 d3.select(this).transition().duration(100)
-                    .style("fill", "grey")
+                    .style("fill", null)
                     .attr("r", 3);
-                div.transition()
-                    .duration(500)
-                    .style("opacity", 0);
-                svg_aline.style("display","None")
+
+                buildDate.transition().duration(2000)
+                    .style("opacity", 0).on("end", function() { buildDate.style("display", "none"); });
+
+                condition.transition().duration(2000)
+                    .style("opacity", 0).on("end", function() { condition.style("display", "none"); });
+
+                mark.transition().duration(2000)
+                    .style("opacity", 0).on("end", function() { mark.style("display", "none"); });
+
+                svg_aline.style("display","none")
             });
+
+        function getDotClass(build) {
+            if (markBuildId === build.buildId)
+                return "mark-dot";
+            else
+                return ((invalidInclude && !build.isValid) ? "hidden-dot" : "dot");
+        }
+    }
+
+    function setCondition(num, buildId, value, median, prefix) {
+        if (!isDefinedAndFilled(buildId) || buildId === "") {
+            showDialog("<i class='fas fa-exclamation-circle'></i><br><br>BuildId must be defined and filled!");
+
+            return;
+        }
+
+        let isValid = !data[num].map.get(buildId).isValid;
+        let conditionLocal = data[num].map.get(buildId).conditionLocal;
+        let modalDialog = $("#modalDialog");
+        let diff = ((value / median - 1) * 100 | 0);
+        let message = "<p style='text-align:center;opacity: 0.7;'><img src='img/tc.svg' width='100' height='100'></p>" +
+            "<br><p style='text-align:justify'><b>Build [" + buildId +  "]</b> will be marked as &laquo;" +
+            (isValid ? "" : "in") + "valid&raquo;. Value differs by <b>" + diff + "%</b> from the median.</p>" +
+            "<br><b>Field:</b>&nbsp;<select id='selectField' style='width: 200px'>";
+
+        for (let fieldName of fieldNames.keys())
+            message += getHtmlOption(fieldName, false, fieldName === prefix, fieldNames.get(fieldName));
+
+        message += "</select><br><br><b>Mark: </b><select id='selectExclusionArea' style='width: 200px'>" +
+            getHtmlOption(false, isValid && !conditionLocal, isValid && conditionLocal, "For me") +
+            getHtmlOption(true, isValid && conditionLocal, isValid && !conditionLocal, "For all") + "</select>";
+
+        message += "<br><br><span style='color:grey;font-size:smaller;text-align:justify'><hr>If you select &laquo;" +
+            "Mark for me&raquo;, build will be hidden from the results only for this dates interval until the page " +
+            "is updated.<br><hr>If you select &laquo;Mark for all&raquo;, your request will be hidden " +
+            "from the results for all users, and will not be taken into account when analyzing tests.</span>";
+
+        modalDialog.html(message);
+        modalDialog.dialog({
+            modal: true,
+            buttons: {
+                "Confirm": function () {
+                    $(this).dialog("close");
+
+                    let forAll = $('#selectExclusionArea').val() === 'true';
+                    let text = "";
+
+                    if (forAll) {
+                        $.ajax({
+                            url: 'rest/build/condition',
+                            data: {
+                                "buildId": buildId,
+                                "isValid": isValid,
+                                "field": $('#selectField').val()
+                            },
+                            success: function (res) {
+                                if (isDefinedAndFilled(res)) {
+                                    text = "<i class='fas fa-" + (res ? "check" : "exclamation") + "-circle'></i>" +
+                                        "<br><br>";
+
+                                    if (isValid) {
+                                        text += res ? "Build <b>remove</b> from invalid list!" :
+                                            "Invalid list <b>doesn't contain</b> build!";
+                                    }
+                                    else {
+                                        text += "Build " + (res ? "<b>add</b> in" :
+                                            "<b>is already</b> on the") + " invalid list!";
+                                    }
+                                } else {
+                                    text += "<i class='fas fa-exclamation-circle'><br><br></i>" +
+                                        "BuildId or condition is null!";
+                                }
+
+                                setBuildConditionOnFrontAndShowDialog(buildId, isValid, text, false);
+                            },
+                            error: showErrInLoadStatus
+                        });
+                    } else {
+                        text = "<i class='fas fa-check-circle'></i><br><br>Build <b>"  + (isValid ? "remove</b> from" :
+                            "add</b> in") +" <i>temporary</i> invalid list!";
+
+                        setBuildConditionOnFrontAndShowDialog(buildId, isValid, text, !isValid);
+                    }
+                },
+                "Cancel": function () {
+                    $(this).dialog("close");
+                }
+            }
+        });
+
+        function getHtmlOption(value, disabled, selected, text){
+            return "<option value='" + value + "' " + (disabled ? "disabled" : "") + (selected ? "selected" : "") +
+                ">" + text + "</option>";
+        }
+    }
+
+    function setBuildConditionOnFrontAndShowDialog(buildId, isValid, text, conditionLocal) {
+        setBuildCondition(buildId, isValid, conditionLocal);
+
+        showDialog(text);
+    }
+
+    function showDialog(text) {
+        let resultDialog = $("#resultDialog");
+        resultDialog.html("<p style='text-align:center'><br>" + text + "</p>");
+
+        resultDialog.dialog({
+            modal: true,
+            buttons: {
+                "Ok": function () {
+                    $(this).dialog("close");
+                }
+            }
+        });
     }
 
     function getBuildLink(buildId) { return "https://ci.ignite.apache.org/viewLog.html?buildId=" + buildId
@@ -755,7 +953,84 @@
         modal.hide();
     })
 
+
+    $('.switch-btn.builds').click(function(){
+        $(this).toggleClass('switch-on');
+
+        invalidInclude = $(this).hasClass('switch-on');
+
+        updateAll();
+    });
+
+    $('.switch-btn.tests').click(function(){
+        $(this).toggleClass('switch-on');
+
+        toggleTests();
+    });
+
+    function setBuildCondition(buildId, isValid, conditionLocal) {
+        setBuildConditionForColumn(buildId, isValid, 1, conditionLocal);
+        setBuildConditionForColumn(buildId, isValid, 2, conditionLocal);
+    }
+
+    function setBuildConditionForColumn(buildId, isValid, num, conditionLocal) {
+        if (isDefinedAndFilled(data[num])) {
+            if (data[num].map.has(buildId)) {
+                data[num].map.get(buildId).isValid = isValid;
+                data[num].map.get(buildId).conditionLocal = conditionLocal;
+            }
+
+            updateColumn(num);
+        }
+    }
+
+    function markBuild(buildId) {
+        markBuildId = (markBuildId === buildId) ? 0 : buildId;
+
+        updateAll();
+    }
+
+    function updateColumn(num) {
+        if (isDefinedAndFilled(data[num]))
+            printStatistics(data[num]);
+    }
+
+    function updateAll() {
+        updateColumn(1);
+        updateColumn(2);
+
+        if (isDefinedAndFilled(mergedTestsResults))
+            printTests(generateCompareTestsResults(mergedTestsResults));
+    }
+
+    function multiFormat(date) {
+        return (d3.timeSecond(date) < date ? formatMillisecond
+            : d3.timeMinute(date) < date ? formatSecond
+                : d3.timeHour(date) < date ? formatMinute
+                    : d3.timeDay(date) < date ? formatHour
+                        : d3.timeMonth(date) < date ? (d3.timeWeek(date) < date ? formatDay : formatWeek)
+                            : d3.timeYear(date) < date ? formatMonth
+                                : formatYear)(date);
+    }
+
+    function dateRangePickerParam(startDate, endDate, num) {
+        return {
+            "opens" : (num === 1 ? 'right' : 'left'),
+            "maxSpan": { "days": 7 },
+            "locale": {
+                "format": "DD/MM/YYYY", "separator": " - ", "applyLabel": "Apply", "cancelLabel": "Cancel",
+                "fromLabel": "From", "toLabel": "To", "customRangeLabel": "Custom", "weekLabel": "W",
+                "daysOfWeek": [ "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa" ],
+                "monthNames": [ "January", "February", "March", "April", "May", "June", "July",
+                    "August", "September", "October", "November", "December" ],
+                "firstDay": 1
+            },
+            "startDate": moment(startDate).format("DD-MM-YYYY"), "endDate": moment(endDate).format("DD-MM-YYYY")
+        }
+    }
 </script>
-<div style="visibility:hidden"><div id="modalDialog" title="Information"></div></div>
+<div style="visibility:hidden">
+    <div id="modalDialog" title="Information"></div><div id="resultDialog" title="Result"></div>
+</div>
 </body>
 </html>
diff --git a/ignite-tc-helper-web/src/main/webapp/css/style-1.5.css b/ignite-tc-helper-web/src/main/webapp/css/style-1.5.css
index c5aa999..309328c 100644
--- a/ignite-tc-helper-web/src/main/webapp/css/style-1.5.css
+++ b/ignite-tc-helper-web/src/main/webapp/css/style-1.5.css
@@ -219,16 +219,16 @@ form li:after
 	padding: 10px 5px 10px 5px;
 }
 
-.data1, .data2{
+.data {
 	text-align: center;
 	vertical-align: middle;
 }
 
-.section{
+.section {
 	font-weight: bold;
 }
 
-.compare .field{
+.compare .field {
 	padding-left: 15px;
 }
 
@@ -306,6 +306,7 @@ tr.shown td.details-control {
 
 	//url('../resources/details_close.png') no-repeat center center;
 }
+
 rect {
 	fill: red;
 	stroke: black;
@@ -314,18 +315,26 @@ rect {
 
 .dot {
 	opacity: 0.5;
-	fill: grey;
+	fill: #4682B4;
+}
+
+.hidden-dot {
+	opacity: 0.5;
+	fill: #E60000;
+}
+
+.mark-dot {
+	opacity: 0.5;
+	fill: #FF9800;
 }
 
 line {
 	fill: none;
-	stroke: green;
-	stroke-width: 1;
 	shape-rendering: crispEdges;
+	pointer-events: none;
 }
 
-.axis path,
-.axis line {
+.axis path {
 	fill: none;
 	stroke: grey;
 	stroke-width: 1;
@@ -335,68 +344,71 @@ line {
 div.tooltip {
 	position: absolute;
 	text-align: center;
-	width: 150px;
-	height: 15px;
 	padding: 2px;
-	margin-left: 50px;
-	margin-top: 5px;
+	border-radius: 8px;
 	font: 14px sans-serif;
-	background: #12AD5E;
 	border: 1px;
-	border-radius: 8px;
-	pointer-events: none;
 	color: white;
+	height: 15px;
 }
 
-.ui-dialog button{
-	height: auto;
+.buildDate {
+	width: 140px;
+	background: #12AD5E;
 }
 
-.switch {
-	position: relative;
-	display: inline-block;
-	width: 60px;
-	height: 34px;
+.condition {
+	width: 18px;
+	background: #E60000;
 }
 
-.switch input {display:none;}
+.mark {
+	width: 18px;
+	background: #FF9800;
+}
 
-.slider {
-	position: absolute;
+.switch-btn {
+	display: inline-block;
+	width: 23px;
+	height: 14px;
+	border-radius: 9px;
+	background: #bfbfbf;
+	z-index: 0;
+	margin: 0;
+	padding: 0;
+	border: none;
 	cursor: pointer;
-	top: 0;
-	left: 0;
-	right: 0;
-	bottom: 0;
-	background-color: #ccc;
-	-webkit-transition: .4s;
-	transition: .4s;
+	position: relative;
+	transition-duration: 300ms;
 }
 
-.slider:before {
-	position: absolute;
+.switch-btn::after {
 	content: "";
-	height: 26px;
-	width: 26px;
-	left: 4px;
-	bottom: 4px;
-	background-color: white;
-	-webkit-transition: .4s;
-	transition: .4s;
+	height: 12px;
+	width: 12px;
+	border-radius: 7px;
+	background: #fff;
+	top: 1px;
+	left: 1px;
+	transition-duration: 300ms;
+	position: absolute;
+	z-index: 1;
 }
 
-input:checked + .slider {
-	background-color: #12AD5E;
+.switch-on {
+	background: #12AD5E;
 }
 
-input:focus + .slider {
-	box-shadow: 0 0 1px #12AD5E;
+.switch-on::after {
+	left: 10px;
 }
 
-input:checked + .slider:before {
-	-webkit-transform: translateX(26px);
-	-ms-transform: translateX(26px);
-	transform: translateX(26px);
+.icon {
+	text-align: center;
+}
+
+.ui-dialog button{
+	height: auto;
 }
 
 .modal {
@@ -434,3 +446,12 @@ input:checked + .slider:before {
     cursor: pointer;
 }
 
+.fas.fa-check-circle {
+	font-size: 48px;
+	color: #12AD5E;
+}
+
+.fas.fa-exclamation-circle {
+	font-size: 48px;
+	color: #E60000;
+}