You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hbase.apache.org by ap...@apache.org on 2019/10/02 00:02:54 UTC

[hbase] branch branch-1 updated (1df9962 -> dd9eadb)

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

apurtell pushed a change to branch branch-1
in repository https://gitbox.apache.org/repos/asf/hbase.git.


    from 1df9962  HBASE-22902 At regionserver start there's a request to roll the WAL
     new 37e5e47  HBASE-21947 TestShell is broken after we remove the jackson dependencies
     new dd9eadb  HBASE-22988 Backport HBASE-11062 "hbtop" to branch-1

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 bin/hbase                                          |   7 +
 conf/log4j-hbtop.properties                        |  27 ++
 hbase-assembly/pom.xml                             |   5 +
 hbase-hbtop/pom.xml                                | 241 ++++++++++
 .../java/org/apache/hadoop/hbase/hbtop/HBTop.java  | 140 ++++++
 .../java/org/apache/hadoop/hbase/hbtop/Record.java | 185 ++++++++
 .../apache/hadoop/hbase/hbtop/RecordFilter.java    | 339 ++++++++++++++
 .../org/apache/hadoop/hbase/hbtop/field/Field.java |  98 ++++
 .../apache/hadoop/hbase/hbtop/field/FieldInfo.java |  55 +++
 .../hadoop/hbase/hbtop/field/FieldValue.java       | 283 ++++++++++++
 .../hadoop/hbase/hbtop/field/FieldValueType.java   |  28 ++
 .../org/apache/hadoop/hbase/hbtop/field/Size.java  | 157 +++++++
 .../hadoop/hbase/hbtop/mode/DrillDownInfo.java     |  50 +++
 .../org/apache/hadoop/hbase/hbtop/mode/Mode.java   |  74 +++
 .../hadoop/hbase/hbtop/mode/ModeStrategy.java      |  38 ++
 .../hbase/hbtop/mode/NamespaceModeStrategy.java    | 105 +++++
 .../hbase/hbtop/mode/RegionModeStrategy.java       | 182 ++++++++
 .../hbase/hbtop/mode/RegionServerModeStrategy.java | 124 +++++
 .../hbase/hbtop/mode/RequestCountPerSecond.java    |  63 +++
 .../hadoop/hbase/hbtop/mode/TableModeStrategy.java | 108 +++++
 .../hbase/hbtop/screen/AbstractScreenView.java     | 102 +++++
 .../apache/hadoop/hbase/hbtop/screen/Screen.java   | 132 ++++++
 .../hadoop/hbase/hbtop/screen/ScreenView.java      |  33 ++
 .../hbtop/screen/field/FieldScreenPresenter.java   | 184 ++++++++
 .../hbase/hbtop/screen/field/FieldScreenView.java  | 193 ++++++++
 .../hbtop/screen/help/CommandDescription.java      |  52 +++
 .../hbtop/screen/help/HelpScreenPresenter.java     |  72 +++
 .../hbase/hbtop/screen/help/HelpScreenView.java    |  89 ++++
 .../hbtop/screen/mode/ModeScreenPresenter.java     | 134 ++++++
 .../hbase/hbtop/screen/mode/ModeScreenView.java    | 136 ++++++
 .../top/FilterDisplayModeScreenPresenter.java      |  53 +++
 .../screen/top/FilterDisplayModeScreenView.java    |  77 ++++
 .../hadoop/hbase/hbtop/screen/top/Header.java      |  48 ++
 .../hbtop/screen/top/InputModeScreenPresenter.java | 168 +++++++
 .../hbtop/screen/top/InputModeScreenView.java      | 105 +++++
 .../screen/top/MessageModeScreenPresenter.java     |  51 +++
 .../hbtop/screen/top/MessageModeScreenView.java    |  65 +++
 .../hadoop/hbase/hbtop/screen/top/Paging.java      | 151 +++++++
 .../hadoop/hbase/hbtop/screen/top/Summary.java     |  93 ++++
 .../hbase/hbtop/screen/top/TopScreenModel.java     | 235 ++++++++++
 .../hbase/hbtop/screen/top/TopScreenPresenter.java | 356 +++++++++++++++
 .../hbase/hbtop/screen/top/TopScreenView.java      | 308 +++++++++++++
 .../hbtop/terminal/AbstractTerminalPrinter.java    |  69 +++
 .../hadoop/hbase/hbtop/terminal/Attributes.java    | 128 ++++++
 .../apache/hadoop/hbase/hbtop/terminal/Color.java  |  28 ++
 .../hbase/hbtop/terminal/CursorPosition.java       |  61 +++
 .../hadoop/hbase/hbtop/terminal/KeyPress.java      | 128 ++++++
 .../hadoop/hbase/hbtop/terminal/Terminal.java      |  39 ++
 .../hbase/hbtop/terminal/TerminalPrinter.java      |  54 +++
 .../hadoop/hbase/hbtop/terminal/TerminalSize.java  |  61 +++
 .../hadoop/hbase/hbtop/terminal/impl/Cell.java     | 122 +++++
 .../hbase/hbtop/terminal/impl/EscapeSequences.java | 140 ++++++
 .../hbtop/terminal/impl/KeyPressGenerator.java     | 500 +++++++++++++++++++++
 .../hbase/hbtop/terminal/impl/ScreenBuffer.java    | 170 +++++++
 .../hbase/hbtop/terminal/impl/TerminalImpl.java    | 229 ++++++++++
 .../hbtop/terminal/impl/TerminalPrinterImpl.java   |  83 ++++
 .../org/apache/hadoop/hbase/hbtop/TestRecord.java  |  87 ++++
 .../hadoop/hbase/hbtop/TestRecordFilter.java       | 209 +++++++++
 .../org/apache/hadoop/hbase/hbtop/TestUtils.java   | 373 +++++++++++++++
 .../hadoop/hbase/hbtop/field/TestFieldValue.java   | 290 ++++++++++++
 .../apache/hadoop/hbase/hbtop/field/TestSize.java  |  80 ++++
 .../hadoop/hbase/hbtop/mode/TestModeBase.java      |  46 ++
 .../hadoop/hbase/hbtop/mode/TestNamespaceMode.java |  63 +++
 .../hadoop/hbase/hbtop/mode/TestRegionMode.java    |  47 ++
 .../hbase/hbtop/mode/TestRegionServerMode.java     |  62 +++
 .../hbtop/mode/TestRequestCountPerSecond.java      |  49 ++
 .../hadoop/hbase/hbtop/mode/TestTableMode.java     |  73 +++
 .../screen/field/TestFieldScreenPresenter.java     | 151 +++++++
 .../hbtop/screen/help/TestHelpScreenPresenter.java |  75 ++++
 .../hbtop/screen/mode/TestModeScreenPresenter.java | 140 ++++++
 .../top/TestFilterDisplayModeScreenPresenter.java  |  91 ++++
 .../screen/top/TestInputModeScreenPresenter.java   | 198 ++++++++
 .../screen/top/TestMessageModeScreenPresenter.java |  65 +++
 .../hadoop/hbase/hbtop/screen/top/TestPaging.java  | 293 ++++++++++++
 .../hbase/hbtop/screen/top/TestTopScreenModel.java | 200 +++++++++
 .../hbtop/screen/top/TestTopScreenPresenter.java   | 291 ++++++++++++
 .../hbase/hbtop/terminal/impl/TestCursor.java      |  77 ++++
 .../hbase/hbtop/terminal/impl/TestKeyPress.java    |  52 +++
 .../hbtop/terminal/impl/TestTerminalPrinter.java   |  58 +++
 hbase-shell/src/main/ruby/hbase/taskmonitor.rb     |  47 +-
 pom.xml                                            |   7 +
 81 files changed, 10061 insertions(+), 21 deletions(-)
 create mode 100644 conf/log4j-hbtop.properties
 create mode 100644 hbase-hbtop/pom.xml
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/HBTop.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/Record.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/RecordFilter.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/Field.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/FieldInfo.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/FieldValue.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/FieldValueType.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/Size.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/DrillDownInfo.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/Mode.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/ModeStrategy.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/NamespaceModeStrategy.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/RegionModeStrategy.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/RegionServerModeStrategy.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/RequestCountPerSecond.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/TableModeStrategy.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/AbstractScreenView.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/Screen.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/ScreenView.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/field/FieldScreenPresenter.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/field/FieldScreenView.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/help/CommandDescription.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/help/HelpScreenPresenter.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/help/HelpScreenView.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/mode/ModeScreenPresenter.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/mode/ModeScreenView.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/FilterDisplayModeScreenPresenter.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/FilterDisplayModeScreenView.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/Header.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/InputModeScreenPresenter.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/InputModeScreenView.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/MessageModeScreenPresenter.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/MessageModeScreenView.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/Paging.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/Summary.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/TopScreenModel.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/TopScreenPresenter.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/TopScreenView.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/AbstractTerminalPrinter.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/Attributes.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/Color.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/CursorPosition.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/KeyPress.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/Terminal.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/TerminalPrinter.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/TerminalSize.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/Cell.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/EscapeSequences.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/KeyPressGenerator.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/ScreenBuffer.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/TerminalImpl.java
 create mode 100644 hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/TerminalPrinterImpl.java
 create mode 100644 hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/TestRecord.java
 create mode 100644 hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/TestRecordFilter.java
 create mode 100644 hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/TestUtils.java
 create mode 100644 hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/field/TestFieldValue.java
 create mode 100644 hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/field/TestSize.java
 create mode 100644 hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestModeBase.java
 create mode 100644 hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestNamespaceMode.java
 create mode 100644 hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestRegionMode.java
 create mode 100644 hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestRegionServerMode.java
 create mode 100644 hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestRequestCountPerSecond.java
 create mode 100644 hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestTableMode.java
 create mode 100644 hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/field/TestFieldScreenPresenter.java
 create mode 100644 hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/help/TestHelpScreenPresenter.java
 create mode 100644 hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/mode/TestModeScreenPresenter.java
 create mode 100644 hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/top/TestFilterDisplayModeScreenPresenter.java
 create mode 100644 hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/top/TestInputModeScreenPresenter.java
 create mode 100644 hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/top/TestMessageModeScreenPresenter.java
 create mode 100644 hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/top/TestPaging.java
 create mode 100644 hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/top/TestTopScreenModel.java
 create mode 100644 hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/top/TestTopScreenPresenter.java
 create mode 100644 hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/terminal/impl/TestCursor.java
 create mode 100644 hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/terminal/impl/TestKeyPress.java
 create mode 100644 hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/terminal/impl/TestTerminalPrinter.java


[hbase] 02/02: HBASE-22988 Backport HBASE-11062 "hbtop" to branch-1

Posted by ap...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

apurtell pushed a commit to branch branch-1
in repository https://gitbox.apache.org/repos/asf/hbase.git

commit dd9eadb00f9dcd071a246482a11dfc7d63845f00
Author: Toshihiro Suzuki <br...@gmail.com>
AuthorDate: Fri Sep 20 15:42:15 2019 +0900

    HBASE-22988 Backport HBASE-11062 "hbtop" to branch-1
    
    Fixes #647
    
    Signed-off-by: Andrew Purtell <ap...@apache.org>
---
 bin/hbase                                          |   7 +
 conf/log4j-hbtop.properties                        |  27 ++
 hbase-assembly/pom.xml                             |   5 +
 hbase-hbtop/pom.xml                                | 241 ++++++++++
 .../java/org/apache/hadoop/hbase/hbtop/HBTop.java  | 140 ++++++
 .../java/org/apache/hadoop/hbase/hbtop/Record.java | 185 ++++++++
 .../apache/hadoop/hbase/hbtop/RecordFilter.java    | 339 ++++++++++++++
 .../org/apache/hadoop/hbase/hbtop/field/Field.java |  98 ++++
 .../apache/hadoop/hbase/hbtop/field/FieldInfo.java |  55 +++
 .../hadoop/hbase/hbtop/field/FieldValue.java       | 283 ++++++++++++
 .../hadoop/hbase/hbtop/field/FieldValueType.java   |  28 ++
 .../org/apache/hadoop/hbase/hbtop/field/Size.java  | 157 +++++++
 .../hadoop/hbase/hbtop/mode/DrillDownInfo.java     |  50 +++
 .../org/apache/hadoop/hbase/hbtop/mode/Mode.java   |  74 +++
 .../hadoop/hbase/hbtop/mode/ModeStrategy.java      |  38 ++
 .../hbase/hbtop/mode/NamespaceModeStrategy.java    | 105 +++++
 .../hbase/hbtop/mode/RegionModeStrategy.java       | 182 ++++++++
 .../hbase/hbtop/mode/RegionServerModeStrategy.java | 124 +++++
 .../hbase/hbtop/mode/RequestCountPerSecond.java    |  63 +++
 .../hadoop/hbase/hbtop/mode/TableModeStrategy.java | 108 +++++
 .../hbase/hbtop/screen/AbstractScreenView.java     | 102 +++++
 .../apache/hadoop/hbase/hbtop/screen/Screen.java   | 132 ++++++
 .../hadoop/hbase/hbtop/screen/ScreenView.java      |  33 ++
 .../hbtop/screen/field/FieldScreenPresenter.java   | 184 ++++++++
 .../hbase/hbtop/screen/field/FieldScreenView.java  | 193 ++++++++
 .../hbtop/screen/help/CommandDescription.java      |  52 +++
 .../hbtop/screen/help/HelpScreenPresenter.java     |  72 +++
 .../hbase/hbtop/screen/help/HelpScreenView.java    |  89 ++++
 .../hbtop/screen/mode/ModeScreenPresenter.java     | 134 ++++++
 .../hbase/hbtop/screen/mode/ModeScreenView.java    | 136 ++++++
 .../top/FilterDisplayModeScreenPresenter.java      |  53 +++
 .../screen/top/FilterDisplayModeScreenView.java    |  77 ++++
 .../hadoop/hbase/hbtop/screen/top/Header.java      |  48 ++
 .../hbtop/screen/top/InputModeScreenPresenter.java | 168 +++++++
 .../hbtop/screen/top/InputModeScreenView.java      | 105 +++++
 .../screen/top/MessageModeScreenPresenter.java     |  51 +++
 .../hbtop/screen/top/MessageModeScreenView.java    |  65 +++
 .../hadoop/hbase/hbtop/screen/top/Paging.java      | 151 +++++++
 .../hadoop/hbase/hbtop/screen/top/Summary.java     |  93 ++++
 .../hbase/hbtop/screen/top/TopScreenModel.java     | 235 ++++++++++
 .../hbase/hbtop/screen/top/TopScreenPresenter.java | 356 +++++++++++++++
 .../hbase/hbtop/screen/top/TopScreenView.java      | 308 +++++++++++++
 .../hbtop/terminal/AbstractTerminalPrinter.java    |  69 +++
 .../hadoop/hbase/hbtop/terminal/Attributes.java    | 128 ++++++
 .../apache/hadoop/hbase/hbtop/terminal/Color.java  |  28 ++
 .../hbase/hbtop/terminal/CursorPosition.java       |  61 +++
 .../hadoop/hbase/hbtop/terminal/KeyPress.java      | 128 ++++++
 .../hadoop/hbase/hbtop/terminal/Terminal.java      |  39 ++
 .../hbase/hbtop/terminal/TerminalPrinter.java      |  54 +++
 .../hadoop/hbase/hbtop/terminal/TerminalSize.java  |  61 +++
 .../hadoop/hbase/hbtop/terminal/impl/Cell.java     | 122 +++++
 .../hbase/hbtop/terminal/impl/EscapeSequences.java | 140 ++++++
 .../hbtop/terminal/impl/KeyPressGenerator.java     | 500 +++++++++++++++++++++
 .../hbase/hbtop/terminal/impl/ScreenBuffer.java    | 170 +++++++
 .../hbase/hbtop/terminal/impl/TerminalImpl.java    | 229 ++++++++++
 .../hbtop/terminal/impl/TerminalPrinterImpl.java   |  83 ++++
 .../org/apache/hadoop/hbase/hbtop/TestRecord.java  |  87 ++++
 .../hadoop/hbase/hbtop/TestRecordFilter.java       | 209 +++++++++
 .../org/apache/hadoop/hbase/hbtop/TestUtils.java   | 373 +++++++++++++++
 .../hadoop/hbase/hbtop/field/TestFieldValue.java   | 290 ++++++++++++
 .../apache/hadoop/hbase/hbtop/field/TestSize.java  |  80 ++++
 .../hadoop/hbase/hbtop/mode/TestModeBase.java      |  46 ++
 .../hadoop/hbase/hbtop/mode/TestNamespaceMode.java |  63 +++
 .../hadoop/hbase/hbtop/mode/TestRegionMode.java    |  47 ++
 .../hbase/hbtop/mode/TestRegionServerMode.java     |  62 +++
 .../hbtop/mode/TestRequestCountPerSecond.java      |  49 ++
 .../hadoop/hbase/hbtop/mode/TestTableMode.java     |  73 +++
 .../screen/field/TestFieldScreenPresenter.java     | 151 +++++++
 .../hbtop/screen/help/TestHelpScreenPresenter.java |  75 ++++
 .../hbtop/screen/mode/TestModeScreenPresenter.java | 140 ++++++
 .../top/TestFilterDisplayModeScreenPresenter.java  |  91 ++++
 .../screen/top/TestInputModeScreenPresenter.java   | 198 ++++++++
 .../screen/top/TestMessageModeScreenPresenter.java |  65 +++
 .../hadoop/hbase/hbtop/screen/top/TestPaging.java  | 293 ++++++++++++
 .../hbase/hbtop/screen/top/TestTopScreenModel.java | 200 +++++++++
 .../hbtop/screen/top/TestTopScreenPresenter.java   | 291 ++++++++++++
 .../hbase/hbtop/terminal/impl/TestCursor.java      |  77 ++++
 .../hbase/hbtop/terminal/impl/TestKeyPress.java    |  52 +++
 .../hbtop/terminal/impl/TestTerminalPrinter.java   |  58 +++
 pom.xml                                            |   7 +
 80 files changed, 10035 insertions(+)

diff --git a/bin/hbase b/bin/hbase
index f82acb6..fa29abc 100755
--- a/bin/hbase
+++ b/bin/hbase
@@ -105,6 +105,7 @@ if [ $# = 0 ]; then
   echo "  pe               Run PerformanceEvaluation"
   echo "  ltt              Run LoadTestTool"
   echo "  canary           Run the Canary tool"
+  echo "  hbtop            Run the HBTop tool"
   echo "  version          Print the version"
   echo "  CLASSNAME        Run the class named CLASSNAME"
   exit 1
@@ -402,6 +403,12 @@ elif [ "$COMMAND" = "ltt" ] ; then
 elif [ "$COMMAND" = "canary" ] ; then
   CLASS='org.apache.hadoop.hbase.tool.Canary'
   HBASE_OPTS="$HBASE_OPTS $HBASE_CANARY_OPTS"
+elif [ "$COMMAND" = "hbtop" ] ; then
+  CLASS='org.apache.hadoop.hbase.hbtop.HBTop'
+  if [ -f "${HBASE_HOME}/conf/log4j-hbtop.properties" ] ; then
+    HBASE_HBTOP_OPTS="${HBASE_HBTOP_OPTS} -Dlog4j.configuration=file:${HBASE_HOME}/conf/log4j-hbtop.properties"
+  fi
+  HBASE_OPTS="${HBASE_OPTS} ${HBASE_HBTOP_OPTS}"
 elif [ "$COMMAND" = "version" ] ; then
   CLASS='org.apache.hadoop.hbase.util.VersionInfo'
 elif [ "$COMMAND" = "completebulkload" ] ; then
diff --git a/conf/log4j-hbtop.properties b/conf/log4j-hbtop.properties
new file mode 100644
index 0000000..4d68d79
--- /dev/null
+++ b/conf/log4j-hbtop.properties
@@ -0,0 +1,27 @@
+# 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.
+
+log4j.rootLogger=WARN,console
+log4j.threshold=WARN
+
+# console
+log4j.appender.console=org.apache.log4j.ConsoleAppender
+log4j.appender.console.target=System.err
+log4j.appender.console.layout=org.apache.log4j.PatternLayout
+log4j.appender.console.layout.ConversionPattern=%d{ISO8601} %-5p [%t] %c{2}: %m%n
+
+# ZooKeeper will still put stuff at WARN
+log4j.logger.org.apache.zookeeper=ERROR
diff --git a/hbase-assembly/pom.xml b/hbase-assembly/pom.xml
index e808c20..8a1b323 100644
--- a/hbase-assembly/pom.xml
+++ b/hbase-assembly/pom.xml
@@ -227,5 +227,10 @@
       <groupId>org.codehaus.jackson</groupId>
       <artifactId>jackson-mapper-asl</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.apache.hbase</groupId>
+      <artifactId>hbase-hbtop</artifactId>
+      <version>${project.version}</version>
+    </dependency>
   </dependencies>
 </project>
diff --git a/hbase-hbtop/pom.xml b/hbase-hbtop/pom.xml
new file mode 100644
index 0000000..00d382e
--- /dev/null
+++ b/hbase-hbtop/pom.xml
@@ -0,0 +1,241 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <!--
+  /**
+   * 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.
+   */
+  -->
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <artifactId>hbase</artifactId>
+    <groupId>org.apache.hbase</groupId>
+    <version>1.5.0-SNAPSHOT</version>
+    <relativePath>..</relativePath>
+  </parent>
+  <artifactId>hbase-hbtop</artifactId>
+  <name>Apache HBase - HBTop</name>
+  <description>A real-time monitoring tool for HBase like Unix's top command</description>
+
+  <build>
+    <plugins>
+      <!-- Make a jar and put the sources in the jar -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-source-plugin</artifactId>
+      </plugin>
+    </plugins>
+  </build>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-lang3</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.google.protobuf</groupId>
+      <artifactId>protobuf-java</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>commons-logging</groupId>
+      <artifactId>commons-logging</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.hadoop</groupId>
+      <artifactId>hadoop-common</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>commons-cli</groupId>
+      <artifactId>commons-cli</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.hbase</groupId>
+      <artifactId>hbase-annotations</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-all</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.hbase</groupId>
+      <artifactId>hbase-annotations</artifactId>
+      <type>test-jar</type>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.hbase</groupId>
+      <artifactId>hbase-common</artifactId>
+      <type>jar</type>
+      <exclusions>
+        <exclusion>
+          <groupId>com.fasterxml.jackson.jaxrs</groupId>
+          <artifactId>jackson-jaxrs-json-provider</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>com.fasterxml.jackson.core</groupId>
+          <artifactId>jackson-annotations</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>com.fasterxml.jackson.core</groupId>
+          <artifactId>jackson-core</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>com.fasterxml.jackson.core</groupId>
+          <artifactId>jackson-databind</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.hbase</groupId>
+      <artifactId>hbase-protocol</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.hbase</groupId>
+      <artifactId>hbase-client</artifactId>
+      <exclusions>
+        <exclusion>
+          <groupId>com.fasterxml.jackson.jaxrs</groupId>
+          <artifactId>jackson-jaxrs-json-provider</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>com.fasterxml.jackson.core</groupId>
+          <artifactId>jackson-annotations</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>com.fasterxml.jackson.core</groupId>
+          <artifactId>jackson-core</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>com.fasterxml.jackson.core</groupId>
+          <artifactId>jackson-databind</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+  </dependencies>
+
+  <profiles>
+    <!-- Needs to make the profile in apache parent pom -->
+    <profile>
+      <id>apache-release</id>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-resources-plugin</artifactId>
+            <executions>
+              <execution>
+                <id>license-javadocs</id>
+                <phase>prepare-package</phase>
+                <goals>
+                  <goal>copy-resources</goal>
+                </goals>
+                <configuration>
+                  <outputDirectory>${project.build.directory}/apidocs</outputDirectory>
+                  <resources>
+                    <resource>
+                      <directory>src/main/javadoc/META-INF/</directory>
+                      <targetPath>META-INF/</targetPath>
+                      <includes>
+                        <include>NOTICE</include>
+                      </includes>
+                      <filtering>true</filtering>
+                    </resource>
+                  </resources>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+    <!-- Skip the tests in this module -->
+    <profile>
+      <id>skipCommonTests</id>
+      <activation>
+        <property>
+          <name>skipCommonTests</name>
+        </property>
+      </activation>
+      <properties>
+        <surefire.skipFirstPart>true</surefire.skipFirstPart>
+      </properties>
+    </profile>
+
+    <profile>
+      <id>errorProne</id>
+      <activation>
+        <activeByDefault>false</activeByDefault>
+      </activation>
+      <build>
+        <plugins>
+          <!-- Turn on error-prone -->
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-compiler-plugin</artifactId>
+            <version>${maven.compiler.version}</version>
+            <configuration>
+              <compilerId>javac-with-errorprone</compilerId>
+              <forceJavacCompilerUse>true</forceJavacCompilerUse>
+              <showWarnings>true</showWarnings>
+              <compilerArgs>
+                <arg>-XepDisableWarningsInGeneratedCode</arg>
+                <arg>-Xep:FallThrough:OFF</arg> <!-- already in findbugs -->
+              </compilerArgs>
+              <annotationProcessorPaths>
+                <path>
+                  <groupId>org.apache.hbase</groupId>
+                  <artifactId>hbase-error-prone</artifactId>
+                  <version>${project.version}</version>
+                </path>
+              </annotationProcessorPaths>
+            </configuration>
+            <dependencies>
+              <dependency>
+                <groupId>org.apache.hbase</groupId>
+                <artifactId>hbase-error-prone</artifactId>
+                <version>${project.version}</version>
+              </dependency>
+              <dependency>
+                <groupId>org.codehaus.plexus</groupId>
+                <artifactId>plexus-compiler-javac-errorprone</artifactId>
+                <version>${plexus.errorprone.javac.version}</version>
+              </dependency>
+              <!-- override plexus-compiler-javac-errorprone's dependency on
+                Error Prone with the latest version -->
+              <dependency>
+                <groupId>com.google.errorprone</groupId>
+                <artifactId>error_prone_core</artifactId>
+                <version>${error-prone.version}</version>
+              </dependency>
+              <dependency>
+                <groupId>org.apache.hbase</groupId>
+                <artifactId>hbase-error-prone</artifactId>
+                <version>${project.version}</version>
+              </dependency>
+            </dependencies>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
+</project>
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/HBTop.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/HBTop.java
new file mode 100644
index 0000000..ac05bb2
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/HBTop.java
@@ -0,0 +1,140 @@
+/**
+ * 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.hadoop.hbase.hbtop;
+
+import java.util.Objects;
+
+import org.apache.commons.cli.BasicParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Options;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.conf.Configured;
+import org.apache.hadoop.hbase.HBaseConfiguration;
+import org.apache.hadoop.hbase.HBaseInterfaceAudience;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.mode.Mode;
+import org.apache.hadoop.hbase.hbtop.screen.Screen;
+import org.apache.hadoop.util.Tool;
+import org.apache.hadoop.util.ToolRunner;
+
+/**
+ * A real-time monitoring tool for HBase like Unix top command.
+ */
+@InterfaceAudience.LimitedPrivate(HBaseInterfaceAudience.TOOLS)
+public class HBTop extends Configured implements Tool {
+
+  private static final Log LOG = LogFactory.getLog(HBTop.class);
+
+  public HBTop() {
+    this(HBaseConfiguration.create());
+  }
+
+  public HBTop(Configuration conf) {
+    super(Objects.requireNonNull(conf));
+  }
+
+  @Override
+  public int run(String[] args) throws Exception {
+    long initialRefreshDelay = 3 * 1000;
+    Mode initialMode = Mode.REGION;
+    try {
+      // Command line options
+      Options opts = new Options();
+      opts.addOption("h", "help", false,
+        "Print usage; for help while the tool is running press 'h'");
+      opts.addOption("d", "delay", true,
+        "The refresh delay (in seconds); default is 3 seconds");
+      opts.addOption("m", "mode", true,
+        "The mode; n (Namespace)|t (Table)|r (Region)|s (RegionServer)"
+          + ", default is r (Region)");
+
+      CommandLine commandLine = new BasicParser().parse(opts, args);
+
+      if (commandLine.hasOption("help")) {
+        printUsage(opts);
+        return 0;
+      }
+
+      if (commandLine.hasOption("delay")) {
+        int delay = 0;
+        try {
+          delay = Integer.parseInt(commandLine.getOptionValue("delay"));
+        } catch (NumberFormatException ignored) {
+        }
+
+        if (delay < 1) {
+          LOG.warn("Delay set too low or invalid, using default");
+        } else {
+          initialRefreshDelay = delay * 1000L;
+        }
+      }
+
+      if (commandLine.hasOption("mode")) {
+        String mode = commandLine.getOptionValue("mode");
+        switch (mode) {
+          case "n":
+            initialMode = Mode.NAMESPACE;
+            break;
+
+          case "t":
+            initialMode = Mode.TABLE;
+            break;
+
+          case "r":
+            initialMode = Mode.REGION;
+            break;
+
+          case "s":
+            initialMode = Mode.REGION_SERVER;
+            break;
+
+          default:
+            LOG.warn("Mode set invalid, using default");
+            break;
+        }
+      }
+    } catch (Exception e) {
+      LOG.error("Unable to parse options", e);
+      return 1;
+    }
+
+    try (Screen screen = new Screen(getConf(), initialRefreshDelay, initialMode)) {
+      screen.run();
+    }
+
+    return 0;
+  }
+
+  private void printUsage(Options opts) {
+    new HelpFormatter().printHelp("hbase hbtop [opts] [-D<property=value>]*", opts);
+    System.out.println("");
+    System.out.println(" Note: -D properties will be applied to the conf used.");
+    System.out.println("  For example:");
+    System.out.println("   -Dhbase.client.zookeeper.quorum=<zookeeper quorum>");
+    System.out.println("   -Dzookeeper.znode.parent=<znode parent>");
+    System.out.println("");
+  }
+
+  public static void main(String[] args) throws Exception {
+    int res = ToolRunner.run(new HBTop(), args);
+    System.exit(res);
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/Record.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/Record.java
new file mode 100644
index 0000000..8409283
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/Record.java
@@ -0,0 +1,185 @@
+/**
+ * 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.hadoop.hbase.hbtop;
+
+import com.google.common.collect.ImmutableMap;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import java.util.AbstractMap;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.hbtop.field.FieldValue;
+import org.apache.hadoop.hbase.hbtop.field.FieldValueType;
+
+
+/**
+ * Represents a record of the metrics in the top screen.
+ */
+@InterfaceAudience.Private
+public final class Record implements Map<Field, FieldValue> {
+
+  private final ImmutableMap<Field, FieldValue> values;
+
+  public final static class Entry extends AbstractMap.SimpleImmutableEntry<Field, FieldValue> {
+    private Entry(Field key, FieldValue value) {
+      super(key, value);
+    }
+  }
+
+  public final static class Builder {
+
+    private final ImmutableMap.Builder<Field, FieldValue> builder;
+
+    private Builder() {
+      builder = ImmutableMap.builder();
+    }
+
+    public Builder put(Field key, Object value) {
+      builder.put(key, key.newValue(value));
+      return this;
+    }
+
+    public Builder put(Field key, FieldValue value) {
+      builder.put(key, value);
+      return this;
+    }
+
+    public Builder put(Entry entry) {
+      builder.put(entry);
+      return this;
+    }
+
+    public Builder putAll(Map<Field, FieldValue> map) {
+      builder.putAll(map);
+      return this;
+    }
+
+    public Record build() {
+      return new Record(builder.build());
+    }
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static Entry entry(Field field, Object value) {
+    return new Entry(field, field.newValue(value));
+  }
+
+  public static Entry entry(Field field, FieldValue value) {
+    return new Entry(field, value);
+  }
+
+  public static Record ofEntries(Entry... entries) {
+    Builder builder = builder();
+    for (Entry entry : entries) {
+      builder.put(entry.getKey(), entry.getValue());
+    }
+    return builder.build();
+  }
+
+  public static Record ofEntries(Iterable<Entry> entries) {
+    Builder builder = builder();
+    for (Entry entry : entries) {
+      builder.put(entry.getKey(), entry.getValue());
+    }
+    return builder.build();
+  }
+
+  private Record(ImmutableMap<Field, FieldValue> values) {
+    this.values = values;
+  }
+
+  @Override
+  public int size() {
+    return values.size();
+  }
+
+  @Override
+  public boolean isEmpty() {
+    return values.isEmpty();
+  }
+
+  @Override
+  public boolean containsKey(Object key) {
+    return values.containsKey(key);
+  }
+
+  @Override
+  public boolean containsValue(Object value) {
+    return values.containsValue(value);
+  }
+
+  @Override
+  public FieldValue get(Object key) {
+    return values.get(key);
+  }
+
+  @Override
+  public FieldValue put(Field key, FieldValue value) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public FieldValue remove(Object key) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void putAll(@NonNull Map<? extends Field, ? extends FieldValue> m) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void clear() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  @NonNull
+  public Set<Field> keySet() {
+    return values.keySet();
+  }
+
+  @Override
+  @NonNull
+  public Collection<FieldValue> values() {
+    return values.values();
+  }
+
+  @Override
+  @NonNull
+  public Set<Map.Entry<Field, FieldValue>> entrySet() {
+    return values.entrySet();
+  }
+
+  public Record combine(Record o) {
+    Builder builder = builder();
+    for (Field k : values.keySet()) {
+      if (k.getFieldValueType() == FieldValueType.STRING) {
+        builder.put(k, values.get(k));
+      } else {
+        builder.put(k, values.get(k).plus(o.values.get(k)));
+      }
+    }
+    return builder.build();
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/RecordFilter.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/RecordFilter.java
new file mode 100644
index 0000000..da9d391
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/RecordFilter.java
@@ -0,0 +1,339 @@
+/**
+ * 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.hadoop.hbase.hbtop;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.hbtop.field.FieldValue;
+
+/**
+ * Represents a filter that's filtering the metric {@link Record}s.
+ */
+@InterfaceAudience.Private
+public final class RecordFilter {
+
+  private enum Operator {
+    EQUAL("="),
+    DOUBLE_EQUALS("=="),
+    GREATER(">"),
+    GREATER_OR_EQUAL(">="),
+    LESS("<"),
+    LESS_OR_EQUAL("<=");
+
+    private final String operator;
+
+    Operator(String operator) {
+      this.operator = operator;
+    }
+
+    @Override
+    public String toString() {
+      return operator;
+    }
+  }
+
+  public static RecordFilter parse(String filterString, boolean ignoreCase) {
+    return parse(filterString, Arrays.asList(Field.values()), ignoreCase);
+  }
+
+  /*
+   * Parse a filter string and build a RecordFilter instance.
+   */
+  public static RecordFilter parse(String filterString, List<Field> fields, boolean ignoreCase) {
+    int index = 0;
+
+    boolean not = isNot(filterString);
+    if (not) {
+      index += 1;
+    }
+
+    StringBuilder fieldString = new StringBuilder();
+    while (filterString.length() > index && filterString.charAt(index) != '<'
+      && filterString.charAt(index) != '>' && filterString.charAt(index) != '=') {
+      fieldString.append(filterString.charAt(index++));
+    }
+
+    if (fieldString.length() == 0 || filterString.length() == index) {
+      return null;
+    }
+
+    Field field = getField(fields, fieldString.toString());
+    if (field == null) {
+      return null;
+    }
+
+    StringBuilder operatorString = new StringBuilder();
+    while (filterString.length() > index && (filterString.charAt(index) == '<' ||
+      filterString.charAt(index) == '>' || filterString.charAt(index) == '=')) {
+      operatorString.append(filterString.charAt(index++));
+    }
+
+    Operator operator = getOperator(operatorString.toString());
+    if (operator == null) {
+      return null;
+    }
+
+    String value = filterString.substring(index);
+    FieldValue fieldValue = getFieldValue(field, value);
+    if (fieldValue == null) {
+      return null;
+    }
+
+    return new RecordFilter(ignoreCase, not, field, operator, fieldValue);
+  }
+
+  private static FieldValue getFieldValue(Field field, String value) {
+    try {
+      return field.newValue(value);
+    } catch (Exception e) {
+      return null;
+    }
+  }
+
+  private static boolean isNot(String filterString) {
+    return filterString.startsWith("!");
+  }
+
+  private static Field getField(List<Field> fields, String fieldString) {
+    for (Field f : fields) {
+      if (f.getHeader().equals(fieldString)) {
+        return f;
+      }
+    }
+    return null;
+  }
+
+  private static Operator getOperator(String operatorString) {
+    for (Operator o : Operator.values()) {
+      if (operatorString.equals(o.toString())) {
+        return o;
+      }
+    }
+    return null;
+  }
+
+  private final boolean ignoreCase;
+  private final boolean not;
+  private final Field field;
+  private final Operator operator;
+  private final FieldValue value;
+
+  private RecordFilter(boolean ignoreCase, boolean not, Field field, Operator operator,
+    FieldValue value) {
+    this.ignoreCase = ignoreCase;
+    this.not = not;
+    this.field = Objects.requireNonNull(field);
+    this.operator = Objects.requireNonNull(operator);
+    this.value = Objects.requireNonNull(value);
+  }
+
+  public boolean execute(Record record) {
+    FieldValue fieldValue = record.get(field);
+    if (fieldValue == null) {
+      return false;
+    }
+
+    if (operator == Operator.EQUAL) {
+      boolean ret;
+      if (ignoreCase) {
+        ret = fieldValue.asString().toLowerCase().contains(value.asString().toLowerCase());
+      } else {
+        ret = fieldValue.asString().contains(value.asString());
+      }
+      return not != ret;
+    }
+
+    int compare = ignoreCase ?
+      fieldValue.compareToIgnoreCase(value) : fieldValue.compareTo(value);
+
+    boolean ret;
+    switch (operator) {
+      case DOUBLE_EQUALS:
+        ret = compare == 0;
+        break;
+
+      case GREATER:
+        ret = compare > 0;
+        break;
+
+      case GREATER_OR_EQUAL:
+        ret = compare >= 0;
+        break;
+
+      case LESS:
+        ret = compare < 0;
+        break;
+
+      case LESS_OR_EQUAL:
+        ret = compare <= 0;
+        break;
+
+      default:
+        throw new AssertionError();
+    }
+    return not != ret;
+  }
+
+  @Override
+  public String toString() {
+    return (not ? "!" : "") + field.getHeader() + operator + value.asString();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof RecordFilter)) {
+      return false;
+    }
+    RecordFilter filter = (RecordFilter) o;
+    return ignoreCase == filter.ignoreCase && not == filter.not && field == filter.field
+      && operator == filter.operator && value.equals(filter.value);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(ignoreCase, not, field, operator, value);
+  }
+
+  /*
+   * For FilterBuilder
+   */
+  public static FilterBuilder newBuilder(Field field) {
+    return new FilterBuilder(field, false);
+  }
+
+  public static FilterBuilder newBuilder(Field field, boolean ignoreCase) {
+    return new FilterBuilder(field, ignoreCase);
+  }
+
+  public static final class FilterBuilder {
+    private final Field field;
+    private final boolean ignoreCase;
+
+    private FilterBuilder(Field field, boolean ignoreCase) {
+      this.field = Objects.requireNonNull(field);
+      this.ignoreCase = ignoreCase;
+    }
+
+    public RecordFilter equal(FieldValue value) {
+      return newFilter(false, Operator.EQUAL, value);
+    }
+
+    public RecordFilter equal(Object value) {
+      return equal(field.newValue(value));
+    }
+
+    public RecordFilter notEqual(FieldValue value) {
+      return newFilter(true, Operator.EQUAL, value);
+    }
+
+    public RecordFilter notEqual(Object value) {
+      return notEqual(field.newValue(value));
+    }
+
+    public RecordFilter doubleEquals(FieldValue value) {
+      return newFilter(false, Operator.DOUBLE_EQUALS, value);
+    }
+
+    public RecordFilter doubleEquals(Object value) {
+      return doubleEquals(field.newValue(value));
+    }
+
+    public RecordFilter notDoubleEquals(FieldValue value) {
+      return newFilter(true, Operator.DOUBLE_EQUALS, value);
+    }
+
+    public RecordFilter notDoubleEquals(Object value) {
+      return notDoubleEquals(field.newValue(value));
+    }
+
+    public RecordFilter greater(FieldValue value) {
+      return newFilter(false, Operator.GREATER, value);
+    }
+
+    public RecordFilter greater(Object value) {
+      return greater(field.newValue(value));
+    }
+
+    public RecordFilter notGreater(FieldValue value) {
+      return newFilter(true, Operator.GREATER, value);
+    }
+
+    public RecordFilter notGreater(Object value) {
+      return notGreater(field.newValue(value));
+    }
+
+    public RecordFilter greaterOrEqual(FieldValue value) {
+      return newFilter(false, Operator.GREATER_OR_EQUAL, value);
+    }
+
+    public RecordFilter greaterOrEqual(Object value) {
+      return greaterOrEqual(field.newValue(value));
+    }
+
+    public RecordFilter notGreaterOrEqual(FieldValue value) {
+      return newFilter(true, Operator.GREATER_OR_EQUAL, value);
+    }
+
+    public RecordFilter notGreaterOrEqual(Object value) {
+      return notGreaterOrEqual(field.newValue(value));
+    }
+
+    public RecordFilter less(FieldValue value) {
+      return newFilter(false, Operator.LESS, value);
+    }
+
+    public RecordFilter less(Object value) {
+      return less(field.newValue(value));
+    }
+
+    public RecordFilter notLess(FieldValue value) {
+      return newFilter(true, Operator.LESS, value);
+    }
+
+    public RecordFilter notLess(Object value) {
+      return notLess(field.newValue(value));
+    }
+
+    public RecordFilter lessOrEqual(FieldValue value) {
+      return newFilter(false, Operator.LESS_OR_EQUAL, value);
+    }
+
+    public RecordFilter lessOrEqual(Object value) {
+      return lessOrEqual(field.newValue(value));
+    }
+
+    public RecordFilter notLessOrEqual(FieldValue value) {
+      return newFilter(true, Operator.LESS_OR_EQUAL, value);
+    }
+
+    public RecordFilter notLessOrEqual(Object value) {
+      return notLessOrEqual(field.newValue(value));
+    }
+
+    private RecordFilter newFilter(boolean not, Operator operator, FieldValue value) {
+      return new RecordFilter(ignoreCase, not, field, operator, value);
+    }
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/Field.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/Field.java
new file mode 100644
index 0000000..a6f1c48
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/Field.java
@@ -0,0 +1,98 @@
+/**
+ * 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.hadoop.hbase.hbtop.field;
+
+import java.util.Objects;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+
+/**
+ * Represents fields that are displayed in the top screen.
+ */
+@InterfaceAudience.Private
+public enum Field {
+  REGION_NAME("RNAME", "Region Name", true, true, FieldValueType.STRING),
+  NAMESPACE("NAMESPACE", "Namespace Name", true, true, FieldValueType.STRING),
+  TABLE("TABLE", "Table Name", true, true, FieldValueType.STRING),
+  START_CODE("SCODE", "Start Code", false, true, FieldValueType.STRING),
+  REPLICA_ID("REPID", "Replica ID", false, false, FieldValueType.STRING),
+  REGION("REGION", "Encoded Region Name", false, true, FieldValueType.STRING),
+  REGION_SERVER("RS", "Short Region Server Name", true, true, FieldValueType.STRING),
+  LONG_REGION_SERVER("LRS", "Long Region Server Name", true, true, FieldValueType.STRING),
+  REQUEST_COUNT_PER_SECOND("#REQ/S", "Request Count per second", false, false,
+    FieldValueType.LONG),
+  READ_REQUEST_COUNT_PER_SECOND("#READ/S", "Read Request Count per second", false, false,
+    FieldValueType.LONG),
+  WRITE_REQUEST_COUNT_PER_SECOND("#WRITE/S", "Write Request Count per second", false, false,
+    FieldValueType.LONG),
+  STORE_FILE_SIZE("SF", "StoreFile Size", false, false, FieldValueType.SIZE),
+  UNCOMPRESSED_STORE_FILE_SIZE("USF", "Uncompressed StoreFile Size", false, false,
+    FieldValueType.SIZE),
+  NUM_STORE_FILES("#SF", "Number of StoreFiles", false, false, FieldValueType.INTEGER),
+  MEM_STORE_SIZE("MEMSTORE", "MemStore Size", false, false, FieldValueType.SIZE),
+  LOCALITY("LOCALITY", "Block Locality", false, false, FieldValueType.FLOAT),
+  START_KEY("SKEY", "Start Key", true, true, FieldValueType.STRING),
+  COMPACTING_CELL_COUNT("#COMPingCELL", "Compacting Cell Count", false, false,
+    FieldValueType.LONG),
+  COMPACTED_CELL_COUNT("#COMPedCELL", "Compacted Cell Count", false, false, FieldValueType.LONG),
+  COMPACTION_PROGRESS("%COMP", "Compaction Progress", false, false, FieldValueType.PERCENT),
+  LAST_MAJOR_COMPACTION_TIME("LASTMCOMP", "Last Major Compaction Time", false, true,
+    FieldValueType.STRING),
+  REGION_COUNT("#REGION", "Region Count", false, false, FieldValueType.INTEGER),
+  USED_HEAP_SIZE("UHEAP", "Used Heap Size", false, false, FieldValueType.SIZE),
+  MAX_HEAP_SIZE("MHEAP", "Max Heap Size", false, false, FieldValueType.SIZE);
+
+  private final String header;
+  private final String description;
+  private final boolean autoAdjust;
+  private final boolean leftJustify;
+  private final FieldValueType fieldValueType;
+
+  Field(String header, String description, boolean autoAdjust, boolean leftJustify,
+    FieldValueType fieldValueType) {
+    this.header = Objects.requireNonNull(header);
+    this.description = Objects.requireNonNull(description);
+    this.autoAdjust = autoAdjust;
+    this.leftJustify = leftJustify;
+    this.fieldValueType = Objects.requireNonNull(fieldValueType);
+  }
+
+  public FieldValue newValue(Object value) {
+    return new FieldValue(value, fieldValueType);
+  }
+
+  public String getHeader() {
+    return header;
+  }
+
+  public String getDescription() {
+    return description;
+  }
+
+  public boolean isAutoAdjust() {
+    return autoAdjust;
+  }
+
+  public boolean isLeftJustify() {
+    return leftJustify;
+  }
+
+  public FieldValueType getFieldValueType() {
+    return fieldValueType;
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/FieldInfo.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/FieldInfo.java
new file mode 100644
index 0000000..b198e20
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/FieldInfo.java
@@ -0,0 +1,55 @@
+/**
+ * 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.hadoop.hbase.hbtop.field;
+
+import java.util.Objects;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+
+/**
+ * Information about a field.
+ *
+ * This has a {@link Field} itself and additional information (e.g. {@code defaultLength} and
+ * {@code displayByDefault}). This additional information is different between the
+ * {@link org.apache.hadoop.hbase.hbtop.mode.Mode}s even when the field is the same. That's why the
+ * additional information is separated from {@link Field}.
+ */
+@InterfaceAudience.Private
+public class FieldInfo {
+  private final Field field;
+  private final int defaultLength;
+  private final boolean displayByDefault;
+
+  public FieldInfo(Field field, int defaultLength, boolean displayByDefault) {
+    this.field = Objects.requireNonNull(field);
+    this.defaultLength = defaultLength;
+    this.displayByDefault = displayByDefault;
+  }
+
+  public Field getField() {
+    return field;
+  }
+
+  public int getDefaultLength() {
+    return defaultLength;
+  }
+
+  public boolean isDisplayByDefault() {
+    return displayByDefault;
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/FieldValue.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/FieldValue.java
new file mode 100644
index 0000000..db7d22f
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/FieldValue.java
@@ -0,0 +1,283 @@
+/**
+ * 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.hadoop.hbase.hbtop.field;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import java.util.Objects;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+
+/**
+ * Represents a value of a field.
+ *
+ * The type of a value is defined by {@link FieldValue}.
+ */
+@InterfaceAudience.Private
+public final class FieldValue implements Comparable<FieldValue> {
+
+  private final Object value;
+  private final FieldValueType type;
+
+  FieldValue(Object value, FieldValueType type) {
+    Objects.requireNonNull(value);
+    this.type = Objects.requireNonNull(type);
+
+    switch (type) {
+      case STRING:
+        if (value instanceof String) {
+          this.value = value;
+          break;
+        }
+        throw new IllegalArgumentException("invalid type");
+
+      case INTEGER:
+        if (value instanceof Integer) {
+          this.value = value;
+          break;
+        } else if (value instanceof String) {
+          this.value = Integer.valueOf((String) value);
+          break;
+        }
+        throw new IllegalArgumentException("invalid type");
+
+      case LONG:
+        if (value instanceof Long) {
+          this.value = value;
+          break;
+        } else if (value instanceof String) {
+          this.value = Long.valueOf((String) value);
+          break;
+        }
+        throw new IllegalArgumentException("invalid type");
+
+      case FLOAT:
+        if (value instanceof Float) {
+          this.value = value;
+          break;
+        } else if (value instanceof String) {
+          this.value = Float.valueOf((String) value);
+          break;
+        }
+        throw new IllegalArgumentException("invalid type");
+
+      case SIZE:
+        if (value instanceof Size) {
+          this.value = optimizeSize((Size) value);
+          break;
+        } else if (value instanceof String) {
+          this.value = optimizeSize(parseSizeString((String) value));
+          break;
+        }
+        throw new IllegalArgumentException("invalid type");
+
+      case PERCENT:
+        if (value instanceof Float) {
+          this.value = value;
+          break;
+        } else if (value instanceof String) {
+          this.value = parsePercentString((String) value);
+          break;
+        }
+        throw new IllegalArgumentException("invalid type");
+
+      default:
+        throw new AssertionError();
+    }
+  }
+
+  private Size optimizeSize(Size size) {
+    if (size.get(Size.Unit.BYTE) < 1024d) {
+      return size.getUnit() == Size.Unit.BYTE ?
+        size : new Size(size.get(Size.Unit.BYTE), Size.Unit.BYTE);
+    } else if (size.get(Size.Unit.KILOBYTE) < 1024d) {
+      return size.getUnit() == Size.Unit.KILOBYTE ?
+        size : new Size(size.get(Size.Unit.KILOBYTE), Size.Unit.KILOBYTE);
+    } else if (size.get(Size.Unit.MEGABYTE) < 1024d) {
+      return size.getUnit() == Size.Unit.MEGABYTE ?
+        size : new Size(size.get(Size.Unit.MEGABYTE), Size.Unit.MEGABYTE);
+    } else if (size.get(Size.Unit.GIGABYTE) < 1024d) {
+      return size.getUnit() == Size.Unit.GIGABYTE ?
+        size : new Size(size.get(Size.Unit.GIGABYTE), Size.Unit.GIGABYTE);
+    } else if (size.get(Size.Unit.TERABYTE) < 1024d) {
+      return size.getUnit() == Size.Unit.TERABYTE ?
+        size : new Size(size.get(Size.Unit.TERABYTE), Size.Unit.TERABYTE);
+    }
+    return size.getUnit() == Size.Unit.PETABYTE ?
+      size : new Size(size.get(Size.Unit.PETABYTE), Size.Unit.PETABYTE);
+  }
+
+  private Size parseSizeString(String sizeString) {
+    if (sizeString.length() < 3) {
+      throw new IllegalArgumentException("invalid size");
+    }
+
+    String valueString = sizeString.substring(0, sizeString.length() - 2);
+    String unitSimpleName = sizeString.substring(sizeString.length() - 2);
+    return new Size(Double.parseDouble(valueString), convertToUnit(unitSimpleName));
+  }
+
+  private Size.Unit convertToUnit(String unitSimpleName) {
+    for (Size.Unit unit: Size.Unit.values()) {
+      if (unitSimpleName.equals(unit.getSimpleName())) {
+        return unit;
+      }
+    }
+    throw new IllegalArgumentException("invalid size");
+  }
+
+  private Float parsePercentString(String percentString) {
+    if (percentString.endsWith("%")) {
+      percentString = percentString.substring(0, percentString.length() - 1);
+    }
+    return Float.valueOf(percentString);
+  }
+
+  public String asString() {
+    return toString();
+  }
+
+  public int asInt() {
+    return (Integer) value;
+  }
+
+  public long asLong() {
+    return (Long) value;
+  }
+
+  public float asFloat() {
+    return (Float) value;
+  }
+
+  public Size asSize() {
+    return (Size) value;
+  }
+
+  @Override
+  public String toString() {
+    switch (type) {
+      case STRING:
+      case INTEGER:
+      case LONG:
+      case FLOAT:
+      case SIZE:
+        return value.toString();
+
+      case PERCENT:
+        return String.format("%.2f", (Float) value) + "%";
+
+      default:
+        throw new AssertionError();
+    }
+  }
+
+  @Override
+  public int compareTo(@NonNull FieldValue o) {
+    if (type != o.type) {
+      throw new IllegalArgumentException("invalid type");
+    }
+
+    switch (type) {
+      case STRING:
+        return ((String) value).compareTo((String) o.value);
+
+      case INTEGER:
+        return ((Integer) value).compareTo((Integer) o.value);
+
+      case LONG:
+        return ((Long) value).compareTo((Long) o.value);
+
+      case FLOAT:
+      case PERCENT:
+        return ((Float) value).compareTo((Float) o.value);
+
+      case SIZE:
+        return ((Size) value).compareTo((Size) o.value);
+
+      default:
+        throw new AssertionError();
+    }
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof FieldValue)) {
+      return false;
+    }
+    FieldValue that = (FieldValue) o;
+    return value.equals(that.value) && type == that.type;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(value, type);
+  }
+
+  public FieldValue plus(FieldValue o) {
+    if (type != o.type) {
+      throw new IllegalArgumentException("invalid type");
+    }
+
+    switch (type) {
+      case STRING:
+        return new FieldValue(((String) value).concat((String) o.value), type);
+
+      case INTEGER:
+        return new FieldValue(((Integer) value) + ((Integer) o.value), type);
+
+      case LONG:
+        return new FieldValue(((Long) value) + ((Long) o.value), type);
+
+      case FLOAT:
+      case PERCENT:
+        return new FieldValue(((Float) value) + ((Float) o.value), type);
+
+      case SIZE:
+        Size size = (Size) value;
+        Size oSize = (Size) o.value;
+        Size.Unit unit = size.getUnit();
+        return new FieldValue(new Size(size.get(unit) + oSize.get(unit), unit), type);
+
+      default:
+        throw new AssertionError();
+    }
+  }
+
+  public int compareToIgnoreCase(FieldValue o) {
+    if (type != o.type) {
+      throw new IllegalArgumentException("invalid type");
+    }
+
+    switch (type) {
+      case STRING:
+        return ((String) value).compareToIgnoreCase((String) o.value);
+
+      case INTEGER:
+      case LONG:
+      case FLOAT:
+      case SIZE:
+      case PERCENT:
+        return compareTo(o);
+
+      default:
+        throw new AssertionError();
+    }
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/FieldValueType.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/FieldValueType.java
new file mode 100644
index 0000000..bb781d3
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/FieldValueType.java
@@ -0,0 +1,28 @@
+/**
+ * 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.hadoop.hbase.hbtop.field;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+
+/**
+ * Represents the type of a {@link FieldValue}.
+ */
+@InterfaceAudience.Private
+public enum FieldValueType {
+  STRING, INTEGER, LONG, FLOAT, SIZE, PERCENT
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/Size.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/Size.java
new file mode 100644
index 0000000..709d11d
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/Size.java
@@ -0,0 +1,157 @@
+/**
+ * Copyright The Apache Software Foundation
+ * 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.hadoop.hbase.hbtop.field;
+
+import java.math.BigDecimal;
+import java.util.Objects;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+
+/**
+ * It is used to represent the size with different units.
+ * This class doesn't serve for the precise computation.
+ */
+@InterfaceAudience.Private
+public final class Size implements Comparable<Size> {
+  public static final Size ZERO = new Size(0, Unit.KILOBYTE);
+  private static final BigDecimal SCALE_BASE = BigDecimal.valueOf(1024D);
+
+  public enum Unit {
+    // keep the room to add more units for HBase 10.x
+    PETABYTE(100, "PB"),
+    TERABYTE(99, "TB"),
+    GIGABYTE(98, "GB"),
+    MEGABYTE(97, "MB"),
+    KILOBYTE(96, "KB"),
+    BYTE(95, "B");
+    private final int orderOfSize;
+    private final String simpleName;
+
+    Unit(int orderOfSize, String simpleName) {
+      this.orderOfSize = orderOfSize;
+      this.simpleName = simpleName;
+    }
+
+    public int getOrderOfSize() {
+      return orderOfSize;
+    }
+
+    public String getSimpleName() {
+      return simpleName;
+    }
+  }
+
+  private final double value;
+  private final Unit unit;
+
+  public Size(double value, Unit unit) {
+    if (value < 0) {
+      throw new IllegalArgumentException("The value:" + value + " can't be negative");
+    }
+    this.value = value;
+    this.unit = unit;
+  }
+
+  /**
+   * @return size unit
+   */
+  public Unit getUnit() {
+    return unit;
+  }
+
+  /**
+   * get the value
+   */
+  public long getLongValue() {
+    return (long) value;
+  }
+
+  /**
+   * get the value
+   */
+  public double get() {
+    return value;
+  }
+
+  /**
+   * get the value which is converted to specified unit.
+   *
+   * @param unit size unit
+   * @return the converted value
+   */
+  public double get(Unit unit) {
+    if (value == 0) {
+      return value;
+    }
+    int diff = this.unit.getOrderOfSize() - unit.getOrderOfSize();
+    if (diff == 0) {
+      return value;
+    }
+
+    BigDecimal rval = BigDecimal.valueOf(value);
+    for (int i = 0; i != Math.abs(diff); ++i) {
+      rval = diff > 0 ? rval.multiply(SCALE_BASE) : rval.divide(SCALE_BASE);
+    }
+    return rval.doubleValue();
+  }
+
+  @Override
+  public int compareTo(Size other) {
+    int diff = unit.getOrderOfSize() - other.unit.getOrderOfSize();
+    if (diff == 0) {
+      return Double.compare(value, other.value);
+    }
+
+    BigDecimal thisValue = BigDecimal.valueOf(value);
+    BigDecimal otherValue = BigDecimal.valueOf(other.value);
+    if (diff > 0) {
+      for (int i = 0; i != Math.abs(diff); ++i) {
+        thisValue = thisValue.multiply(SCALE_BASE);
+      }
+    } else {
+      for (int i = 0; i != Math.abs(diff); ++i) {
+        otherValue = otherValue.multiply(SCALE_BASE);
+      }
+    }
+    return thisValue.compareTo(otherValue);
+  }
+
+  @Override
+  public String toString() {
+    return value + unit.getSimpleName();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj == null) {
+      return false;
+    }
+    if (obj == this) {
+      return true;
+    }
+    if (obj instanceof Size) {
+      return compareTo((Size)obj) == 0;
+    }
+    return false;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(value, unit);
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/DrillDownInfo.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/DrillDownInfo.java
new file mode 100644
index 0000000..5311299
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/DrillDownInfo.java
@@ -0,0 +1,50 @@
+/**
+ * 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.hadoop.hbase.hbtop.mode;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.RecordFilter;
+
+/**
+ * Information about drilling down.
+ *
+ * When drilling down, going to next {@link Mode} with initial {@link RecordFilter}s.
+ */
+@InterfaceAudience.Private
+public class DrillDownInfo {
+  private final Mode nextMode;
+  private final List<RecordFilter> initialFilters;
+
+  public DrillDownInfo(Mode nextMode, List<RecordFilter> initialFilters) {
+    this.nextMode = Objects.requireNonNull(nextMode);
+    this.initialFilters = Collections.unmodifiableList(new ArrayList<>(initialFilters));
+  }
+
+  public Mode getNextMode() {
+    return nextMode;
+  }
+
+  public List<RecordFilter> getInitialFilters() {
+    return initialFilters;
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/Mode.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/Mode.java
new file mode 100644
index 0000000..e5dd42e
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/Mode.java
@@ -0,0 +1,74 @@
+/**
+ * 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.hadoop.hbase.hbtop.mode;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.hadoop.hbase.ClusterStatus;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.Record;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.hbtop.field.FieldInfo;
+
+/**
+ * Represents a display mode in the top screen.
+ */
+@InterfaceAudience.Private
+public enum Mode {
+  NAMESPACE("Namespace", "Record per Namespace", new NamespaceModeStrategy()),
+  TABLE("Table", "Record per Table", new TableModeStrategy()),
+  REGION("Region", "Record per Region", new RegionModeStrategy()),
+  REGION_SERVER("RegionServer", "Record per RegionServer", new RegionServerModeStrategy());
+
+  private final String header;
+  private final String description;
+  private final ModeStrategy modeStrategy;
+
+  Mode(String header, String description, ModeStrategy modeStrategy) {
+    this.header  = Objects.requireNonNull(header);
+    this.description = Objects.requireNonNull(description);
+    this.modeStrategy = Objects.requireNonNull(modeStrategy);
+  }
+
+  public String getHeader() {
+    return header;
+  }
+
+  public String getDescription() {
+    return description;
+  }
+
+  public List<Record> getRecords(ClusterStatus clusterStatus) {
+    return modeStrategy.getRecords(clusterStatus);
+  }
+
+  public List<FieldInfo> getFieldInfos() {
+    return modeStrategy.getFieldInfos();
+  }
+
+  public Field getDefaultSortField() {
+    return modeStrategy.getDefaultSortField();
+  }
+
+  @Nullable
+  public DrillDownInfo drillDown(Record currentRecord) {
+    return modeStrategy.drillDown(currentRecord);
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/ModeStrategy.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/ModeStrategy.java
new file mode 100644
index 0000000..cbfae83
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/ModeStrategy.java
@@ -0,0 +1,38 @@
+/**
+ * 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.hadoop.hbase.hbtop.mode;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.util.List;
+
+import org.apache.hadoop.hbase.ClusterStatus;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.Record;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.hbtop.field.FieldInfo;
+
+/**
+ * An interface for strategy logic for {@link Mode}.
+ */
+@InterfaceAudience.Private
+interface ModeStrategy {
+  List<FieldInfo> getFieldInfos();
+  Field getDefaultSortField();
+  List<Record> getRecords(ClusterStatus clusterStatus);
+  @Nullable DrillDownInfo drillDown(Record selectedRecord);
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/NamespaceModeStrategy.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/NamespaceModeStrategy.java
new file mode 100644
index 0000000..b17c372
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/NamespaceModeStrategy.java
@@ -0,0 +1,105 @@
+/**
+ * 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.hadoop.hbase.hbtop.mode;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.hadoop.hbase.ClusterStatus;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.Record;
+import org.apache.hadoop.hbase.hbtop.RecordFilter;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.hbtop.field.FieldInfo;
+
+/**
+ * Implementation for {@link ModeStrategy} for Namespace Mode.
+ */
+@InterfaceAudience.Private
+public final class NamespaceModeStrategy implements ModeStrategy {
+
+  private final List<FieldInfo> fieldInfos = Arrays.asList(
+    new FieldInfo(Field.NAMESPACE, 0, true),
+    new FieldInfo(Field.REGION_COUNT, 7, true),
+    new FieldInfo(Field.REQUEST_COUNT_PER_SECOND, 10, true),
+    new FieldInfo(Field.READ_REQUEST_COUNT_PER_SECOND, 10, true),
+    new FieldInfo(Field.WRITE_REQUEST_COUNT_PER_SECOND, 10, true),
+    new FieldInfo(Field.STORE_FILE_SIZE, 13, true),
+    new FieldInfo(Field.UNCOMPRESSED_STORE_FILE_SIZE, 15, false),
+    new FieldInfo(Field.NUM_STORE_FILES, 7, true),
+    new FieldInfo(Field.MEM_STORE_SIZE, 11, true)
+  );
+
+  private final RegionModeStrategy regionModeStrategy = new RegionModeStrategy();
+
+  NamespaceModeStrategy(){
+  }
+
+  @Override
+  public List<FieldInfo> getFieldInfos() {
+    return fieldInfos;
+  }
+
+  @Override
+  public Field getDefaultSortField() {
+    return Field.REQUEST_COUNT_PER_SECOND;
+  }
+
+  @Override
+  public List<Record> getRecords(ClusterStatus clusterStatus) {
+    // Get records from RegionModeStrategy and add REGION_COUNT field
+    List<Record> records = new ArrayList<>();
+    for (Record record : regionModeStrategy.getRecords(clusterStatus)) {
+      List<Record.Entry> entries = new ArrayList<>();
+      for (FieldInfo fieldInfo : fieldInfos) {
+        if (record.containsKey(fieldInfo.getField())) {
+          entries.add(Record.entry(fieldInfo.getField(),
+            record.get(fieldInfo.getField())));
+        }
+      }
+
+      // Add REGION_COUNT field
+      records.add(Record.builder().putAll(Record.ofEntries(entries))
+        .put(Field.REGION_COUNT, 1).build());
+    }
+
+    // Aggregation by NAMESPACE field
+    Map<String, Record> retMap = new HashMap<>();
+    for (Record record : records) {
+      String namespace = record.get(Field.NAMESPACE).asString();
+      if (retMap.containsKey(namespace)) {
+        retMap.put(namespace, retMap.get(namespace).combine(record));
+      } else {
+        retMap.put(namespace, record);
+      }
+    }
+    return new ArrayList<>(retMap.values());
+  }
+
+  @Override
+  public DrillDownInfo drillDown(Record selectedRecord) {
+    List<RecordFilter> initialFilters =
+      Collections.singletonList(RecordFilter.newBuilder(Field.NAMESPACE)
+        .doubleEquals(selectedRecord.get(Field.NAMESPACE)));
+    return new DrillDownInfo(Mode.TABLE, initialFilters);
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/RegionModeStrategy.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/RegionModeStrategy.java
new file mode 100644
index 0000000..6ab7163
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/RegionModeStrategy.java
@@ -0,0 +1,182 @@
+/**
+ * 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.hadoop.hbase.hbtop.mode;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.lang3.time.FastDateFormat;
+import org.apache.hadoop.hbase.ClusterStatus;
+import org.apache.hadoop.hbase.HRegionInfo;
+import org.apache.hadoop.hbase.RegionLoad;
+import org.apache.hadoop.hbase.ServerLoad;
+import org.apache.hadoop.hbase.ServerName;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.Record;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.hbtop.field.FieldInfo;
+import org.apache.hadoop.hbase.hbtop.field.Size;
+import org.apache.hadoop.hbase.hbtop.field.Size.Unit;
+import org.apache.hadoop.hbase.util.Bytes;
+
+/**
+ * Implementation for {@link ModeStrategy} for Region Mode.
+ */
+@InterfaceAudience.Private
+public final class RegionModeStrategy implements ModeStrategy {
+
+  private final List<FieldInfo> fieldInfos = Arrays.asList(
+    new FieldInfo(Field.REGION_NAME, 0, false),
+    new FieldInfo(Field.NAMESPACE, 0, true),
+    new FieldInfo(Field.TABLE, 0,  true),
+    new FieldInfo(Field.START_CODE, 13, false),
+    new FieldInfo(Field.REPLICA_ID, 5, false),
+    new FieldInfo(Field.REGION, 32, true),
+    new FieldInfo(Field.REGION_SERVER, 0, true),
+    new FieldInfo(Field.LONG_REGION_SERVER, 0, false),
+    new FieldInfo(Field.REQUEST_COUNT_PER_SECOND, 8, true),
+    new FieldInfo(Field.READ_REQUEST_COUNT_PER_SECOND, 8, true),
+    new FieldInfo(Field.WRITE_REQUEST_COUNT_PER_SECOND, 8, true),
+    new FieldInfo(Field.STORE_FILE_SIZE, 10, true),
+    new FieldInfo(Field.UNCOMPRESSED_STORE_FILE_SIZE, 12, false),
+    new FieldInfo(Field.NUM_STORE_FILES,4, true),
+    new FieldInfo(Field.MEM_STORE_SIZE, 8, true),
+    new FieldInfo(Field.LOCALITY, 8, true),
+    new FieldInfo(Field.START_KEY, 0, false),
+    new FieldInfo(Field.COMPACTING_CELL_COUNT, 12, false),
+    new FieldInfo(Field.COMPACTED_CELL_COUNT, 12, false),
+    new FieldInfo(Field.COMPACTION_PROGRESS, 7, false),
+    new FieldInfo(Field.LAST_MAJOR_COMPACTION_TIME, 19, false)
+  );
+
+  private final Map<String, RequestCountPerSecond> requestCountPerSecondMap = new HashMap<>();
+
+  RegionModeStrategy() {
+  }
+
+  @Override
+  public List<FieldInfo> getFieldInfos() {
+    return fieldInfos;
+  }
+
+  @Override
+  public Field getDefaultSortField() {
+    return Field.REQUEST_COUNT_PER_SECOND;
+  }
+
+  @Override
+  public List<Record> getRecords(ClusterStatus clusterStatus) {
+    List<Record> ret = new ArrayList<>();
+    for (ServerName sn: clusterStatus.getServers()) {
+      ServerLoad sl = clusterStatus.getLoad(sn);
+      long lastReportTimestamp = sl.obtainServerLoadPB().getReportEndTime();
+      for (RegionLoad rl: sl.getRegionsLoad().values()) {
+        ret.add(createRecord(sn, rl, lastReportTimestamp));
+      }
+    }
+    return ret;
+  }
+
+  private Record createRecord(ServerName sn, RegionLoad regionLoad, long lastReportTimestamp) {
+    Record.Builder builder = Record.builder();
+
+    String regionName = regionLoad.getNameAsString();
+    builder.put(Field.REGION_NAME, regionName);
+
+    String namespaceName = "";
+    String tableName = "";
+    String region = "";
+    String startKey = "";
+    String startCode = "";
+    String replicaId = "";
+    try {
+      byte[][] elements = HRegionInfo.parseRegionName(regionLoad.getName());
+      TableName tn = TableName.valueOf(elements[0]);
+      namespaceName = tn.getNamespaceAsString();
+      tableName = tn.getQualifierAsString();
+      startKey = Bytes.toStringBinary(elements[1]);
+      startCode = Bytes.toString(elements[2]);
+      replicaId = elements.length == 4 ?
+        Integer.valueOf(Bytes.toString(elements[3])).toString() : "";
+      region = HRegionInfo.encodeRegionName(regionLoad.getName());
+    } catch (IOException ignored) {
+    }
+
+    builder.put(Field.NAMESPACE, namespaceName);
+    builder.put(Field.TABLE, tableName);
+    builder.put(Field.START_CODE, startCode);
+    builder.put(Field.REPLICA_ID, replicaId);
+    builder.put(Field.REGION, region);
+    builder.put(Field.START_KEY, startKey);
+    builder.put(Field.REGION_SERVER, sn.toShortString());
+    builder.put(Field.LONG_REGION_SERVER, sn.getServerName());
+
+    RequestCountPerSecond requestCountPerSecond = requestCountPerSecondMap.get(regionName);
+    if (requestCountPerSecond == null) {
+      requestCountPerSecond = new RequestCountPerSecond();
+      requestCountPerSecondMap.put(regionName, requestCountPerSecond);
+    }
+    requestCountPerSecond.refresh(lastReportTimestamp, regionLoad.getReadRequestsCount(),
+      regionLoad.getWriteRequestsCount());
+
+    builder.put(Field.READ_REQUEST_COUNT_PER_SECOND,
+      requestCountPerSecond.getReadRequestCountPerSecond());
+    builder.put(Field.WRITE_REQUEST_COUNT_PER_SECOND,
+      requestCountPerSecond.getWriteRequestCountPerSecond());
+    builder.put(Field.REQUEST_COUNT_PER_SECOND,
+      requestCountPerSecond.getRequestCountPerSecond());
+
+    builder.put(Field.STORE_FILE_SIZE, new Size(regionLoad.getStorefileSizeMB(), Unit.MEGABYTE));
+    builder.put(Field.UNCOMPRESSED_STORE_FILE_SIZE,
+      new Size(regionLoad.getStoreUncompressedSizeMB(), Unit.MEGABYTE));
+    builder.put(Field.NUM_STORE_FILES, regionLoad.getStorefiles());
+    builder.put(Field.MEM_STORE_SIZE, new Size(regionLoad.getMemStoreSizeMB(), Unit.MEGABYTE));
+    builder.put(Field.LOCALITY, regionLoad.getDataLocality());
+
+    long compactingCellCount = regionLoad.getTotalCompactingKVs();
+    long compactedCellCount = regionLoad.getCurrentCompactedKVs();
+    float compactionProgress = 0;
+    if  (compactedCellCount > 0) {
+      compactionProgress = 100 * ((float) compactedCellCount / compactingCellCount);
+    }
+
+    builder.put(Field.COMPACTING_CELL_COUNT, compactingCellCount);
+    builder.put(Field.COMPACTED_CELL_COUNT, compactedCellCount);
+    builder.put(Field.COMPACTION_PROGRESS, compactionProgress);
+
+    FastDateFormat df = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss");
+    long lastMajorCompactionTimestamp = regionLoad.getLastMajorCompactionTs();
+
+    builder.put(Field.LAST_MAJOR_COMPACTION_TIME,
+      lastMajorCompactionTimestamp == 0 ? "" : df.format(lastMajorCompactionTimestamp));
+
+    return builder.build();
+  }
+
+  @Nullable
+  @Override
+  public DrillDownInfo drillDown(Record selectedRecord) {
+    // do nothing
+    return null;
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/RegionServerModeStrategy.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/RegionServerModeStrategy.java
new file mode 100644
index 0000000..4d7169a
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/RegionServerModeStrategy.java
@@ -0,0 +1,124 @@
+/**
+ * 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.hadoop.hbase.hbtop.mode;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.hadoop.hbase.ClusterStatus;
+import org.apache.hadoop.hbase.ServerLoad;
+import org.apache.hadoop.hbase.ServerName;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.Record;
+import org.apache.hadoop.hbase.hbtop.RecordFilter;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.hbtop.field.FieldInfo;
+import org.apache.hadoop.hbase.hbtop.field.Size;
+import org.apache.hadoop.hbase.hbtop.field.Size.Unit;
+
+/**
+ * Implementation for {@link ModeStrategy} for RegionServer Mode.
+ */
+@InterfaceAudience.Private
+public final class RegionServerModeStrategy implements ModeStrategy {
+
+  private final List<FieldInfo> fieldInfos = Arrays.asList(
+    new FieldInfo(Field.REGION_SERVER, 0, true),
+    new FieldInfo(Field.LONG_REGION_SERVER, 0, false),
+    new FieldInfo(Field.REGION_COUNT, 7, true),
+    new FieldInfo(Field.REQUEST_COUNT_PER_SECOND, 10, true),
+    new FieldInfo(Field.READ_REQUEST_COUNT_PER_SECOND, 10, true),
+    new FieldInfo(Field.WRITE_REQUEST_COUNT_PER_SECOND, 10, true),
+    new FieldInfo(Field.STORE_FILE_SIZE, 13, true),
+    new FieldInfo(Field.UNCOMPRESSED_STORE_FILE_SIZE, 15, false),
+    new FieldInfo(Field.NUM_STORE_FILES, 7, true),
+    new FieldInfo(Field.MEM_STORE_SIZE, 11, true),
+    new FieldInfo(Field.USED_HEAP_SIZE, 11, true),
+    new FieldInfo(Field.MAX_HEAP_SIZE, 11, true)
+  );
+
+  private final RegionModeStrategy regionModeStrategy = new RegionModeStrategy();
+
+  RegionServerModeStrategy(){
+  }
+
+  @Override
+  public List<FieldInfo> getFieldInfos() {
+    return fieldInfos;
+  }
+
+  @Override
+  public Field getDefaultSortField() {
+    return Field.REQUEST_COUNT_PER_SECOND;
+  }
+
+  @Override
+  public List<Record> getRecords(ClusterStatus clusterStatus) {
+    // Get records from RegionModeStrategy and add REGION_COUNT field
+    List<Record> records = new ArrayList<>();
+    for (Record record : regionModeStrategy.getRecords(clusterStatus)) {
+      List<Record.Entry> entries = new ArrayList<>();
+      for (FieldInfo fieldInfo : fieldInfos) {
+        if (record.containsKey(fieldInfo.getField())) {
+          entries.add(Record.entry(fieldInfo.getField(),
+            record.get(fieldInfo.getField())));
+        }
+      }
+
+      // Add REGION_COUNT field
+      records.add(Record.builder().putAll(Record.ofEntries(entries))
+        .put(Field.REGION_COUNT, 1).build());
+    }
+
+    // Aggregation by NAMESPACE field
+    Map<String, Record> retMap = new HashMap<>();
+    for (Record record : records) {
+      String regionServer = record.get(Field.LONG_REGION_SERVER).asString();
+      if (retMap.containsKey(regionServer)) {
+        retMap.put(regionServer, retMap.get(regionServer).combine(record));
+      } else {
+        retMap.put(regionServer, record);
+      }
+    }
+
+    // Add USED_HEAP_SIZE field and MAX_HEAP_SIZE field
+    for (ServerName sn : clusterStatus.getServers()) {
+      Record record = retMap.get(sn.getServerName());
+      if (record == null) {
+        continue;
+      }
+      ServerLoad sl = clusterStatus.getLoad(sn);
+      Record newRecord = Record.builder().putAll(record)
+        .put(Field.USED_HEAP_SIZE, new Size(sl.getUsedHeapMB(), Unit.MEGABYTE))
+        .put(Field.MAX_HEAP_SIZE, new Size(sl.getMaxHeapMB(), Unit.MEGABYTE)).build();
+      retMap.put(sn.getServerName(), newRecord);
+    }
+    return new ArrayList<>(retMap.values());
+  }
+
+  @Override
+  public DrillDownInfo drillDown(Record selectedRecord) {
+    List<RecordFilter> initialFilters = Collections.singletonList(RecordFilter
+      .newBuilder(Field.REGION_SERVER)
+      .doubleEquals(selectedRecord.get(Field.REGION_SERVER)));
+    return new DrillDownInfo(Mode.REGION, initialFilters);
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/RequestCountPerSecond.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/RequestCountPerSecond.java
new file mode 100644
index 0000000..ee6bef5
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/RequestCountPerSecond.java
@@ -0,0 +1,63 @@
+/**
+ * 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.hadoop.hbase.hbtop.mode;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+
+/**
+ * Utility class for calculating request counts per second.
+ */
+@InterfaceAudience.Private
+public class RequestCountPerSecond {
+  private long previousLastReportTimestamp;
+  private long previousReadRequestCount;
+  private long previousWriteRequestCount;
+  private long readRequestCountPerSecond;
+  private long writeRequestCountPerSecond;
+
+  public void refresh(long lastReportTimestamp, long readRequestCount, long writeRequestCount) {
+    if (previousLastReportTimestamp == 0) {
+      previousLastReportTimestamp = lastReportTimestamp;
+      previousReadRequestCount = readRequestCount;
+      previousWriteRequestCount = writeRequestCount;
+    } else if (previousLastReportTimestamp != lastReportTimestamp) {
+      long delta = (lastReportTimestamp - previousLastReportTimestamp) / 1000;
+      if (delta < 1) {
+        delta = 1;
+      }
+      readRequestCountPerSecond = (readRequestCount - previousReadRequestCount) / delta;
+      writeRequestCountPerSecond = (writeRequestCount - previousWriteRequestCount) / delta;
+
+      previousLastReportTimestamp = lastReportTimestamp;
+      previousReadRequestCount = readRequestCount;
+      previousWriteRequestCount = writeRequestCount;
+    }
+  }
+
+  public long getReadRequestCountPerSecond() {
+    return readRequestCountPerSecond < 0 ? 0 : readRequestCountPerSecond;
+  }
+
+  public long getWriteRequestCountPerSecond() {
+    return writeRequestCountPerSecond < 0 ? 0 : writeRequestCountPerSecond;
+  }
+
+  public long getRequestCountPerSecond() {
+    return getReadRequestCountPerSecond() + getWriteRequestCountPerSecond();
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/TableModeStrategy.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/TableModeStrategy.java
new file mode 100644
index 0000000..eeefbf2
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/TableModeStrategy.java
@@ -0,0 +1,108 @@
+/**
+ * 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.hadoop.hbase.hbtop.mode;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.hadoop.hbase.ClusterStatus;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.Record;
+import org.apache.hadoop.hbase.hbtop.RecordFilter;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.hbtop.field.FieldInfo;
+
+/**
+ * Implementation for {@link ModeStrategy} for Table Mode.
+ */
+@InterfaceAudience.Private
+public final class TableModeStrategy implements ModeStrategy {
+
+  private final List<FieldInfo> fieldInfos = Arrays.asList(
+    new FieldInfo(Field.NAMESPACE, 0, true),
+    new FieldInfo(Field.TABLE, 0, true),
+    new FieldInfo(Field.REGION_COUNT, 7, true),
+    new FieldInfo(Field.REQUEST_COUNT_PER_SECOND, 10, true),
+    new FieldInfo(Field.READ_REQUEST_COUNT_PER_SECOND, 10, true),
+    new FieldInfo(Field.WRITE_REQUEST_COUNT_PER_SECOND, 10, true),
+    new FieldInfo(Field.STORE_FILE_SIZE, 13, true),
+    new FieldInfo(Field.UNCOMPRESSED_STORE_FILE_SIZE, 15, false),
+    new FieldInfo(Field.NUM_STORE_FILES, 7, true),
+    new FieldInfo(Field.MEM_STORE_SIZE, 11, true)
+  );
+
+  private final RegionModeStrategy regionModeStrategy = new RegionModeStrategy();
+
+  TableModeStrategy() {
+  }
+
+  @Override
+  public List<FieldInfo> getFieldInfos() {
+    return fieldInfos;
+  }
+
+  @Override
+  public Field getDefaultSortField() {
+    return Field.REQUEST_COUNT_PER_SECOND;
+  }
+
+  @Override
+  public List<Record> getRecords(ClusterStatus clusterStatus) {
+    // Get records from RegionModeStrategy and add REGION_COUNT field
+    List<Record> records = new ArrayList<>();
+    for (Record record : regionModeStrategy.getRecords(clusterStatus)) {
+      List<Record.Entry> entries = new ArrayList<>();
+      for (FieldInfo fieldInfo : fieldInfos) {
+        if (record.containsKey(fieldInfo.getField())) {
+          entries.add(Record.entry(fieldInfo.getField(),
+            record.get(fieldInfo.getField())));
+        }
+      }
+
+      // Add REGION_COUNT field
+      records.add(Record.builder().putAll(Record.ofEntries(entries))
+        .put(Field.REGION_COUNT, 1).build());
+    }
+
+    // Aggregation by NAMESPACE field
+    Map<TableName, Record> retMap = new HashMap<>();
+    for (Record record : records) {
+      String namespace = record.get(Field.NAMESPACE).asString();
+      String table = record.get(Field.TABLE).asString();
+      TableName tableName = TableName.valueOf(namespace, table);
+
+      if (retMap.containsKey(tableName)) {
+        retMap.put(tableName, retMap.get(tableName).combine(record));
+      } else {
+        retMap.put(tableName, record);
+      }
+    }
+    return new ArrayList<>(retMap.values());
+  }
+
+  @Override
+  public DrillDownInfo drillDown(Record selectedRecord) {
+    List<RecordFilter> initialFilters = Arrays.asList(
+      RecordFilter.newBuilder(Field.NAMESPACE).doubleEquals(selectedRecord.get(Field.NAMESPACE)),
+      RecordFilter.newBuilder(Field.TABLE).doubleEquals(selectedRecord.get(Field.TABLE)));
+    return new DrillDownInfo(Mode.REGION, initialFilters);
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/AbstractScreenView.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/AbstractScreenView.java
new file mode 100644
index 0000000..32dfb53
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/AbstractScreenView.java
@@ -0,0 +1,102 @@
+/**
+ * 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.hadoop.hbase.hbtop.screen;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.util.Objects;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.terminal.KeyPress;
+import org.apache.hadoop.hbase.hbtop.terminal.Terminal;
+import org.apache.hadoop.hbase.hbtop.terminal.TerminalPrinter;
+import org.apache.hadoop.hbase.hbtop.terminal.TerminalSize;
+
+/**
+ * An abstract class for {@link ScreenView} that has the common useful methods and the default
+ * implementations for the abstract methods.
+ */
+@InterfaceAudience.Private
+public abstract class AbstractScreenView implements ScreenView {
+
+  private final Screen screen;
+  private final Terminal terminal;
+
+  public AbstractScreenView(Screen screen, Terminal terminal) {
+    this.screen = Objects.requireNonNull(screen);
+    this.terminal = Objects.requireNonNull(terminal);
+  }
+
+  @Override
+  public void init() {
+  }
+
+  @Override
+  public ScreenView handleKeyPress(KeyPress keyPress) {
+    return this;
+  }
+
+  @Override
+  public ScreenView handleTimer() {
+    return this;
+  }
+
+  protected Screen getScreen() {
+    return screen;
+  }
+
+  protected Terminal getTerminal() {
+    return terminal;
+  }
+
+  protected void setTimer(long delay) {
+    screen.setTimer(delay);
+  }
+
+  protected void cancelTimer() {
+    screen.cancelTimer();
+  }
+
+  protected TerminalPrinter getTerminalPrinter(int startRow) {
+    return terminal.getTerminalPrinter(startRow);
+  }
+
+  protected TerminalSize getTerminalSize() {
+    return terminal.getSize();
+  }
+
+  @Nullable
+  protected TerminalSize doResizeIfNecessary() {
+    return terminal.doResizeIfNecessary();
+  }
+
+  public void clearTerminal() {
+    terminal.clear();
+  }
+
+  public void refreshTerminal() {
+    terminal.refresh();
+  }
+
+  public void hideCursor() {
+    terminal.hideCursor();
+  }
+
+  public void setCursorPosition(int column, int row) {
+    terminal.setCursorPosition(column, row);
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/Screen.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/Screen.java
new file mode 100644
index 0000000..bbcbba2
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/Screen.java
@@ -0,0 +1,132 @@
+/**
+ * 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.hadoop.hbase.hbtop.screen;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.client.Admin;
+import org.apache.hadoop.hbase.client.Connection;
+import org.apache.hadoop.hbase.client.ConnectionFactory;
+import org.apache.hadoop.hbase.hbtop.mode.Mode;
+import org.apache.hadoop.hbase.hbtop.screen.top.TopScreenView;
+import org.apache.hadoop.hbase.hbtop.terminal.KeyPress;
+import org.apache.hadoop.hbase.hbtop.terminal.Terminal;
+import org.apache.hadoop.hbase.hbtop.terminal.impl.TerminalImpl;
+
+/**
+ * This dispatches key presses and timers to the current {@link ScreenView}.
+ */
+@InterfaceAudience.Private
+public class Screen implements Closeable {
+
+  private static final Log LOG = LogFactory.getLog(Screen.class);
+
+  private static final long SLEEP_TIMEOUT_MILLISECONDS = 100;
+
+  private final Connection connection;
+  private final Admin admin;
+  private final Terminal terminal;
+
+  private ScreenView currentScreenView;
+  private Long timerTimestamp;
+
+  public Screen(Configuration conf, long initialRefreshDelay, Mode initialMode)
+    throws IOException {
+    connection = ConnectionFactory.createConnection(conf);
+    admin = connection.getAdmin();
+
+    // The first screen is the top screen
+    this.terminal = new TerminalImpl("hbtop");
+    currentScreenView = new TopScreenView(this, terminal, initialRefreshDelay, admin,
+      initialMode);
+  }
+
+  @Override
+  public void close() throws IOException {
+    try {
+      admin.close();
+    } finally {
+      try {
+        connection.close();
+      } finally {
+        terminal.close();
+      }
+    }
+  }
+
+  public void run() {
+    currentScreenView.init();
+    while (true) {
+      try {
+        KeyPress keyPress = terminal.pollKeyPress();
+
+        ScreenView nextScreenView;
+        if (keyPress != null) {
+          // Dispatch the key press to the current screen
+          nextScreenView = currentScreenView.handleKeyPress(keyPress);
+        } else {
+          if (timerTimestamp != null) {
+            long now = System.currentTimeMillis();
+            if (timerTimestamp <= now) {
+              // Dispatch the timer to the current screen
+              timerTimestamp = null;
+              nextScreenView = currentScreenView.handleTimer();
+            } else {
+              if (timerTimestamp - now < SLEEP_TIMEOUT_MILLISECONDS) {
+                TimeUnit.MILLISECONDS.sleep(timerTimestamp - now);
+              } else {
+                TimeUnit.MILLISECONDS.sleep(SLEEP_TIMEOUT_MILLISECONDS);
+              }
+              continue;
+            }
+          } else {
+            TimeUnit.MILLISECONDS.sleep(SLEEP_TIMEOUT_MILLISECONDS);
+            continue;
+          }
+        }
+
+        // If the next screen is null, then exit
+        if (nextScreenView == null) {
+          return;
+        }
+
+        // If the next screen is not the previous, then go to the next screen
+        if (nextScreenView != currentScreenView) {
+          currentScreenView = nextScreenView;
+          currentScreenView.init();
+        }
+      } catch (Exception e) {
+        LOG.error("Caught an exception", e);
+      }
+    }
+  }
+
+  public void setTimer(long delay) {
+    timerTimestamp = System.currentTimeMillis() + delay;
+  }
+
+  public void cancelTimer() {
+    timerTimestamp = null;
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/ScreenView.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/ScreenView.java
new file mode 100644
index 0000000..33f04b6
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/ScreenView.java
@@ -0,0 +1,33 @@
+/**
+ * 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.hadoop.hbase.hbtop.screen;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.terminal.KeyPress;
+
+/**
+ * An interface for a screen view that handles key presses and timers.
+ */
+@InterfaceAudience.Private
+public interface ScreenView {
+  void init();
+  @Nullable ScreenView handleKeyPress(KeyPress keyPress);
+  @Nullable ScreenView handleTimer();
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/field/FieldScreenPresenter.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/field/FieldScreenPresenter.java
new file mode 100644
index 0000000..c32fc1b
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/field/FieldScreenPresenter.java
@@ -0,0 +1,184 @@
+/**
+ * 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.hadoop.hbase.hbtop.screen.field;
+
+import java.util.ArrayList;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+
+/**
+ * The presentation logic for the field screen.
+ */
+@InterfaceAudience.Private
+public class FieldScreenPresenter {
+
+  public interface ResultListener {
+    void accept(Field sortField, List<Field> fields, EnumMap<Field, Boolean> fieldDisplayMap);
+  }
+
+  private final FieldScreenView fieldScreenView;
+  private Field sortField;
+  private final List<Field> fields;
+  private final EnumMap<Field, Boolean> fieldDisplayMap;
+  private final ResultListener resultListener;
+  private final ScreenView nextScreenView;
+
+  private final int headerMaxLength;
+  private final int descriptionMaxLength;
+
+  private int currentPosition;
+  private boolean moveMode;
+
+  public FieldScreenPresenter(FieldScreenView fieldScreenView, Field sortField, List<Field> fields,
+    EnumMap<Field, Boolean> fieldDisplayMap, ResultListener resultListener,
+    ScreenView nextScreenView) {
+    this.fieldScreenView = Objects.requireNonNull(fieldScreenView);
+    this.sortField = Objects.requireNonNull(sortField);
+    this.fields = new ArrayList<>(Objects.requireNonNull(fields));
+    this.fieldDisplayMap = new EnumMap<>(Objects.requireNonNull(fieldDisplayMap));
+    this.resultListener = Objects.requireNonNull(resultListener);
+    this.nextScreenView = Objects.requireNonNull(nextScreenView);
+
+    int headerLength = 0;
+    int descriptionLength = 0;
+    for (int i = 0; i < fields.size(); i ++) {
+      Field field = fields.get(i);
+
+      if (field == sortField) {
+        currentPosition = i;
+      }
+
+      if (headerLength < field.getHeader().length()) {
+        headerLength = field.getHeader().length();
+      }
+
+      if (descriptionLength < field.getDescription().length()) {
+        descriptionLength = field.getDescription().length();
+      }
+    }
+
+    headerMaxLength = headerLength;
+    descriptionMaxLength = descriptionLength;
+  }
+
+  public void init() {
+    fieldScreenView.hideCursor();
+    fieldScreenView.clearTerminal();
+    fieldScreenView.showFieldScreen(sortField.getHeader(), fields, fieldDisplayMap,
+      currentPosition, headerMaxLength, descriptionMaxLength, moveMode);
+    fieldScreenView.refreshTerminal();
+  }
+
+  public void arrowUp() {
+    if (currentPosition > 0) {
+      currentPosition -= 1;
+
+      if (moveMode) {
+        Field tmp = fields.remove(currentPosition);
+        fields.add(currentPosition + 1, tmp);
+      }
+
+      showField(currentPosition);
+      showField(currentPosition + 1);
+      fieldScreenView.refreshTerminal();
+    }
+  }
+
+  public void arrowDown() {
+    if (currentPosition < fields.size() - 1) {
+      currentPosition += 1;
+
+      if (moveMode) {
+        Field tmp = fields.remove(currentPosition - 1);
+        fields.add(currentPosition, tmp);
+      }
+
+      showField(currentPosition);
+      showField(currentPosition - 1);
+      fieldScreenView.refreshTerminal();
+    }
+  }
+
+  public void pageUp() {
+    if (currentPosition > 0 && !moveMode) {
+      int previousPosition = currentPosition;
+      currentPosition = 0;
+      showField(previousPosition);
+      showField(currentPosition);
+      fieldScreenView.refreshTerminal();
+    }
+  }
+
+  public void pageDown() {
+    if (currentPosition < fields.size() - 1  && !moveMode) {
+      int previousPosition = currentPosition;
+      currentPosition = fields.size() - 1;
+      showField(previousPosition);
+      showField(currentPosition);
+      fieldScreenView.refreshTerminal();
+    }
+  }
+
+  public void turnOnMoveMode() {
+    moveMode = true;
+    showField(currentPosition);
+    fieldScreenView.refreshTerminal();
+  }
+
+  public void turnOffMoveMode() {
+    moveMode = false;
+    showField(currentPosition);
+    fieldScreenView.refreshTerminal();
+  }
+
+  public void switchFieldDisplay() {
+    if (!moveMode) {
+      Field field = fields.get(currentPosition);
+      fieldDisplayMap.put(field, !fieldDisplayMap.get(field));
+      showField(currentPosition);
+      fieldScreenView.refreshTerminal();
+    }
+  }
+
+  private void showField(int pos) {
+    Field field = fields.get(pos);
+    fieldScreenView.showField(pos, field, fieldDisplayMap.get(field), pos == currentPosition,
+      headerMaxLength, descriptionMaxLength, moveMode);
+  }
+
+  public void setSortField() {
+    if (!moveMode) {
+      Field newSortField = fields.get(currentPosition);
+      if (newSortField != this.sortField) {
+        this.sortField = newSortField;
+        fieldScreenView.showScreenDescription(sortField.getHeader());
+        fieldScreenView.refreshTerminal();
+      }
+    }
+  }
+
+  public ScreenView transitionToNextScreen() {
+    resultListener.accept(sortField, fields, fieldDisplayMap);
+    return nextScreenView;
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/field/FieldScreenView.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/field/FieldScreenView.java
new file mode 100644
index 0000000..b9eec85
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/field/FieldScreenView.java
@@ -0,0 +1,193 @@
+/**
+ * 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.hadoop.hbase.hbtop.screen.field;
+
+import java.util.EnumMap;
+import java.util.List;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.hbtop.screen.AbstractScreenView;
+import org.apache.hadoop.hbase.hbtop.screen.Screen;
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+import org.apache.hadoop.hbase.hbtop.terminal.KeyPress;
+import org.apache.hadoop.hbase.hbtop.terminal.Terminal;
+import org.apache.hadoop.hbase.hbtop.terminal.TerminalPrinter;
+
+/**
+ * The screen where we can change the displayed fields, the sort key and the order of the fields.
+ */
+@InterfaceAudience.Private
+public class FieldScreenView extends AbstractScreenView {
+
+  private static final int SCREEN_DESCRIPTION_START_ROW = 0;
+  private static final int FIELD_START_ROW = 5;
+
+  private final FieldScreenPresenter fieldScreenPresenter;
+
+  public FieldScreenView(Screen screen, Terminal terminal, Field sortField, List<Field> fields,
+    EnumMap<Field, Boolean> fieldDisplayMap, FieldScreenPresenter.ResultListener resultListener,
+    ScreenView nextScreenView) {
+    super(screen, terminal);
+    this.fieldScreenPresenter = new FieldScreenPresenter(this, sortField, fields, fieldDisplayMap,
+      resultListener, nextScreenView);
+  }
+
+  @Override
+  public void init() {
+    fieldScreenPresenter.init();
+  }
+
+  @Override
+  public ScreenView handleKeyPress(KeyPress keyPress) {
+    switch (keyPress.getType()) {
+      case Escape:
+        return fieldScreenPresenter.transitionToNextScreen();
+
+      case ArrowUp:
+        fieldScreenPresenter.arrowUp();
+        return this;
+
+      case ArrowDown:
+        fieldScreenPresenter.arrowDown();
+        return this;
+
+      case PageUp:
+      case Home:
+        fieldScreenPresenter.pageUp();
+        return this;
+
+      case PageDown:
+      case End:
+        fieldScreenPresenter.pageDown();
+        return this;
+
+      case ArrowRight:
+        fieldScreenPresenter.turnOnMoveMode();
+        return this;
+
+      case ArrowLeft:
+      case Enter:
+        fieldScreenPresenter.turnOffMoveMode();
+        return this;
+
+      default:
+        // Do nothing
+        break;
+    }
+
+    if (keyPress.getType() != KeyPress.Type.Character) {
+      return this;
+    }
+
+    assert keyPress.getCharacter() != null;
+    switch (keyPress.getCharacter()) {
+      case 'd':
+      case ' ':
+        fieldScreenPresenter.switchFieldDisplay();
+        break;
+
+      case 's':
+        fieldScreenPresenter.setSortField();
+        break;
+
+      case 'q':
+        return fieldScreenPresenter.transitionToNextScreen();
+
+      default:
+        // Do nothing
+        break;
+    }
+
+    return this;
+  }
+
+  public void showFieldScreen(String sortFieldHeader, List<Field> fields,
+    EnumMap<Field, Boolean> fieldDisplayMap, int currentPosition, int headerMaxLength,
+    int descriptionMaxLength, boolean moveMode) {
+    showScreenDescription(sortFieldHeader);
+
+    for (int i = 0; i < fields.size(); i ++) {
+      Field field = fields.get(i);
+      showField(i, field, fieldDisplayMap.get(field), i == currentPosition, headerMaxLength,
+        descriptionMaxLength, moveMode);
+    }
+  }
+
+  public void showScreenDescription(String sortKeyHeader) {
+    TerminalPrinter printer = getTerminalPrinter(SCREEN_DESCRIPTION_START_ROW);
+    printer.startBold().print("Fields Management").stopBold().endOfLine();
+    printer.print("Current Sort Field: ").startBold().print(sortKeyHeader).stopBold().endOfLine();
+    printer.print("Navigate with up/down, Right selects for move then <Enter> or Left commits,")
+      .endOfLine();
+    printer.print("'d' or <Space> toggles display, 's' sets sort. Use 'q' or <Esc> to end!")
+      .endOfLine();
+  }
+
+  public void showField(int pos, Field field, boolean display, boolean selected,
+    int fieldHeaderMaxLength, int fieldDescriptionMaxLength, boolean moveMode) {
+
+    String fieldHeader = String.format("%-" + fieldHeaderMaxLength + "s", field.getHeader());
+    String fieldDescription = String.format("%-" + fieldDescriptionMaxLength + "s",
+      field.getDescription());
+
+    int row = FIELD_START_ROW + pos;
+    TerminalPrinter printer = getTerminalPrinter(row);
+    if (selected) {
+      String prefix = display ? "* " : "  ";
+      if (moveMode) {
+        printer.print(prefix);
+
+        if (display) {
+          printer.startBold();
+        }
+
+        printer.startHighlight()
+          .printFormat("%s = %s", fieldHeader, fieldDescription).stopHighlight();
+
+        if (display) {
+          printer.stopBold();
+        }
+
+        printer.endOfLine();
+      } else {
+        printer.print(prefix);
+
+        if (display) {
+          printer.startBold();
+        }
+
+        printer.startHighlight().print(fieldHeader).stopHighlight()
+          .printFormat(" = %s", fieldDescription);
+
+        if (display) {
+          printer.stopBold();
+        }
+
+        printer.endOfLine();
+      }
+    } else {
+      if (display) {
+        printer.print("* ").startBold().printFormat("%s = %s", fieldHeader, fieldDescription)
+          .stopBold().endOfLine();
+      } else {
+        printer.printFormat("  %s = %s", fieldHeader, fieldDescription).endOfLine();
+      }
+    }
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/help/CommandDescription.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/help/CommandDescription.java
new file mode 100644
index 0000000..c77848f
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/help/CommandDescription.java
@@ -0,0 +1,52 @@
+/**
+ * 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.hadoop.hbase.hbtop.screen.help;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+
+/**
+ * Represents a description of a command that we can execute in the top screen.
+ */
+@InterfaceAudience.Private
+public class CommandDescription {
+
+  private final List<String> keys;
+  private final String description;
+
+  public CommandDescription(String key, String description) {
+    this(Collections.singletonList(Objects.requireNonNull(key)), description);
+  }
+
+  public CommandDescription(List<String> keys, String description) {
+    this.keys = Collections.unmodifiableList(new ArrayList<>(Objects.requireNonNull(keys)));
+    this.description = Objects.requireNonNull(description);
+  }
+
+  public List<String> getKeys() {
+    return keys;
+  }
+
+  public String getDescription() {
+    return description;
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/help/HelpScreenPresenter.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/help/HelpScreenPresenter.java
new file mode 100644
index 0000000..eafb852
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/help/HelpScreenPresenter.java
@@ -0,0 +1,72 @@
+/**
+ * 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.hadoop.hbase.hbtop.screen.help;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+
+/**
+ * The presentation logic for the help screen.
+ */
+@InterfaceAudience.Private
+public class HelpScreenPresenter {
+
+  private static final CommandDescription[] COMMAND_DESCRIPTIONS = new CommandDescription[] {
+    new CommandDescription("f", "Add/Remove/Order/Sort the fields"),
+    new CommandDescription("R", "Toggle the sort order (ascending/descending)"),
+    new CommandDescription("m", "Select mode"),
+    new CommandDescription("o", "Add a filter with ignoring case"),
+    new CommandDescription("O", "Add a filter with case sensitive"),
+    new CommandDescription("^o", "Show the current filters"),
+    new CommandDescription("=", "Clear the current filters"),
+    new CommandDescription("i", "Drill down"),
+    new CommandDescription(
+      Arrays.asList("up", "down", "left", "right", "pageUp", "pageDown", "home", "end"),
+      "Scroll the metrics"),
+    new CommandDescription("d", "Change the refresh delay"),
+    new CommandDescription("X", "Adjust the field length"),
+    new CommandDescription("<Enter>", "Refresh the display"),
+    new CommandDescription("h", "Display this screen"),
+    new CommandDescription(Arrays.asList("q", "<Esc>"), "Quit")
+  };
+
+  private final HelpScreenView helpScreenView;
+  private final long refreshDelay;
+  private final ScreenView nextScreenView;
+
+  public HelpScreenPresenter(HelpScreenView helpScreenView, long refreshDelay,
+    ScreenView nextScreenView) {
+    this.helpScreenView = Objects.requireNonNull(helpScreenView);
+    this.refreshDelay = refreshDelay;
+    this.nextScreenView = Objects.requireNonNull(nextScreenView);
+  }
+
+  public void init() {
+    helpScreenView.hideCursor();
+    helpScreenView.clearTerminal();
+    helpScreenView.showHelpScreen(refreshDelay, COMMAND_DESCRIPTIONS);
+    helpScreenView.refreshTerminal();
+  }
+
+  public ScreenView transitionToNextScreen() {
+    return nextScreenView;
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/help/HelpScreenView.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/help/HelpScreenView.java
new file mode 100644
index 0000000..cf547ed
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/help/HelpScreenView.java
@@ -0,0 +1,89 @@
+/**
+ * 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.hadoop.hbase.hbtop.screen.help;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.screen.AbstractScreenView;
+import org.apache.hadoop.hbase.hbtop.screen.Screen;
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+import org.apache.hadoop.hbase.hbtop.terminal.KeyPress;
+import org.apache.hadoop.hbase.hbtop.terminal.Terminal;
+import org.apache.hadoop.hbase.hbtop.terminal.TerminalPrinter;
+
+/**
+ * The help screen.
+ */
+@InterfaceAudience.Private
+public class HelpScreenView extends AbstractScreenView {
+
+  private static final int SCREEN_DESCRIPTION_START_ROW = 0;
+  private static final int COMMAND_DESCRIPTION_START_ROW = 3;
+
+  private final HelpScreenPresenter helpScreenPresenter;
+
+  public HelpScreenView(Screen screen, Terminal terminal, long refreshDelay,
+    ScreenView nextScreenView) {
+    super(screen, terminal);
+    this.helpScreenPresenter = new HelpScreenPresenter(this, refreshDelay, nextScreenView);
+  }
+
+  @Override
+  public void init() {
+    helpScreenPresenter.init();
+  }
+
+  @Override
+  public ScreenView handleKeyPress(KeyPress keyPress) {
+    return helpScreenPresenter.transitionToNextScreen();
+  }
+
+  public void showHelpScreen(long refreshDelay, CommandDescription[] commandDescriptions) {
+    showScreenDescription(refreshDelay);
+
+    TerminalPrinter printer = getTerminalPrinter(COMMAND_DESCRIPTION_START_ROW);
+    for (CommandDescription commandDescription : commandDescriptions) {
+      showCommandDescription(printer, commandDescription);
+    }
+
+    printer.endOfLine();
+    printer.print("Press any key to continue").endOfLine();
+  }
+
+  private void showScreenDescription(long refreshDelay) {
+    TerminalPrinter printer = getTerminalPrinter(SCREEN_DESCRIPTION_START_ROW);
+    printer.startBold().print("Help for Interactive Commands").stopBold().endOfLine();
+    printer.print("Refresh delay: ").startBold()
+      .print((double) refreshDelay / 1000).stopBold().endOfLine();
+  }
+
+  private void showCommandDescription(TerminalPrinter terminalPrinter,
+    CommandDescription commandDescription) {
+    terminalPrinter.print("  ");
+    boolean first = true;
+    for (String key : commandDescription.getKeys()) {
+      if (first) {
+        first = false;
+      } else {
+        terminalPrinter.print(",");
+      }
+      terminalPrinter.startBold().print(key).stopBold();
+    }
+
+    terminalPrinter.printFormat(": %s", commandDescription.getDescription()).endOfLine();
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/mode/ModeScreenPresenter.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/mode/ModeScreenPresenter.java
new file mode 100644
index 0000000..672b556
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/mode/ModeScreenPresenter.java
@@ -0,0 +1,134 @@
+/**
+ * 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.hadoop.hbase.hbtop.screen.mode;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.mode.Mode;
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+
+/**
+ * The presentation logic for the mode screen.
+ */
+@InterfaceAudience.Private
+public class ModeScreenPresenter {
+
+  public interface ResultListener {
+    void accept(Mode mode);
+  }
+
+  private final ModeScreenView modeScreenView;
+  private final Mode currentMode;
+  private final ResultListener resultListener;
+  private final ScreenView nextScreenView;
+
+  private final int modeHeaderMaxLength;
+  private final int modeDescriptionMaxLength;
+  private final List<Mode> modes = Arrays.asList(Mode.values());
+
+  private int currentPosition;
+
+  public ModeScreenPresenter(ModeScreenView modeScreenView, Mode currentMode,
+    ResultListener resultListener, ScreenView nextScreenView) {
+    this.modeScreenView = Objects.requireNonNull(modeScreenView);
+    this.currentMode = Objects.requireNonNull(currentMode);
+    this.resultListener = Objects.requireNonNull(resultListener);
+    this.nextScreenView = Objects.requireNonNull(nextScreenView);
+
+    int modeHeaderLength = 0;
+    int modeDescriptionLength = 0;
+    for (int i = 0; i < modes.size(); i++) {
+      Mode mode = modes.get(i);
+      if (mode == currentMode) {
+        currentPosition = i;
+      }
+
+      if (modeHeaderLength < mode.getHeader().length()) {
+        modeHeaderLength = mode.getHeader().length();
+      }
+
+      if (modeDescriptionLength < mode.getDescription().length()) {
+        modeDescriptionLength = mode.getDescription().length();
+      }
+    }
+
+    modeHeaderMaxLength = modeHeaderLength;
+    modeDescriptionMaxLength = modeDescriptionLength;
+  }
+
+  public void init() {
+    modeScreenView.hideCursor();
+    modeScreenView.clearTerminal();
+    modeScreenView.showModeScreen(currentMode, modes, currentPosition, modeHeaderMaxLength,
+      modeDescriptionMaxLength);
+    modeScreenView.refreshTerminal();
+  }
+
+  public void arrowUp() {
+    if (currentPosition > 0) {
+      currentPosition -= 1;
+      showMode(currentPosition);
+      showMode(currentPosition + 1);
+      modeScreenView.refreshTerminal();
+    }
+  }
+
+  public void arrowDown() {
+    if (currentPosition < modes.size() - 1) {
+      currentPosition += 1;
+      showMode(currentPosition);
+      showMode(currentPosition - 1);
+      modeScreenView.refreshTerminal();
+    }
+  }
+
+  public void pageUp() {
+    if (currentPosition > 0) {
+      int previousPosition = currentPosition;
+      currentPosition = 0;
+      showMode(previousPosition);
+      showMode(currentPosition);
+      modeScreenView.refreshTerminal();
+    }
+  }
+
+  public void pageDown() {
+    if (currentPosition < modes.size() - 1) {
+      int previousPosition = currentPosition;
+      currentPosition = modes.size() - 1;
+      showMode(previousPosition);
+      showMode(currentPosition);
+      modeScreenView.refreshTerminal();
+    }
+  }
+
+  private void showMode(int pos) {
+    modeScreenView.showMode(pos, modes.get(pos), pos == currentPosition, modeHeaderMaxLength,
+      modeDescriptionMaxLength);
+  }
+
+  public ScreenView transitionToNextScreen(boolean changeMode) {
+    Mode selectedMode = modes.get(currentPosition);
+    if (changeMode && currentMode != selectedMode) {
+      resultListener.accept(selectedMode);
+    }
+    return nextScreenView;
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/mode/ModeScreenView.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/mode/ModeScreenView.java
new file mode 100644
index 0000000..57b3b3a
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/mode/ModeScreenView.java
@@ -0,0 +1,136 @@
+/**
+ * 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.hadoop.hbase.hbtop.screen.mode;
+
+import java.util.List;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.mode.Mode;
+import org.apache.hadoop.hbase.hbtop.screen.AbstractScreenView;
+import org.apache.hadoop.hbase.hbtop.screen.Screen;
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+import org.apache.hadoop.hbase.hbtop.terminal.KeyPress;
+import org.apache.hadoop.hbase.hbtop.terminal.Terminal;
+import org.apache.hadoop.hbase.hbtop.terminal.TerminalPrinter;
+
+/**
+ * The screen where we can choose the {@link Mode} in the top screen.
+ */
+@InterfaceAudience.Private
+public class ModeScreenView extends AbstractScreenView {
+
+  private static final int SCREEN_DESCRIPTION_START_ROW = 0;
+  private static final int MODE_START_ROW = 4;
+
+  private final ModeScreenPresenter modeScreenPresenter;
+
+  public ModeScreenView(Screen screen, Terminal terminal, Mode currentMode,
+    ModeScreenPresenter.ResultListener resultListener, ScreenView nextScreenView) {
+    super(screen, terminal);
+    this.modeScreenPresenter = new ModeScreenPresenter(this, currentMode, resultListener,
+      nextScreenView);
+  }
+
+  @Override
+  public void init() {
+    modeScreenPresenter.init();
+  }
+
+  @Override
+  public ScreenView handleKeyPress(KeyPress keyPress) {
+    switch (keyPress.getType()) {
+      case Escape:
+        return modeScreenPresenter.transitionToNextScreen(false);
+
+      case Enter:
+        return modeScreenPresenter.transitionToNextScreen(true);
+
+      case ArrowUp:
+        modeScreenPresenter.arrowUp();
+        return this;
+
+      case ArrowDown:
+        modeScreenPresenter.arrowDown();
+        return this;
+
+      case PageUp:
+      case Home:
+        modeScreenPresenter.pageUp();
+        return this;
+
+      case PageDown:
+      case End:
+        modeScreenPresenter.pageDown();
+        return this;
+
+      default:
+        // Do nothing
+        break;
+    }
+
+    if (keyPress.getType() != KeyPress.Type.Character) {
+      return this;
+    }
+
+    assert keyPress.getCharacter() != null;
+    switch (keyPress.getCharacter()) {
+      case 'q':
+        return modeScreenPresenter.transitionToNextScreen(false);
+
+      default:
+        // Do nothing
+        break;
+    }
+
+    return this;
+  }
+
+  public void showModeScreen(Mode currentMode, List<Mode> modes, int currentPosition,
+    int modeHeaderMaxLength, int modeDescriptionMaxLength) {
+    showScreenDescription(currentMode);
+
+    for (int i = 0; i < modes.size(); i++) {
+      showMode(i, modes.get(i), i == currentPosition,
+        modeHeaderMaxLength, modeDescriptionMaxLength);
+    }
+  }
+
+  private void showScreenDescription(Mode currentMode) {
+    TerminalPrinter printer = getTerminalPrinter(SCREEN_DESCRIPTION_START_ROW);
+    printer.startBold().print("Mode Management").stopBold().endOfLine();
+    printer.print("Current mode: ")
+      .startBold().print(currentMode.getHeader()).stopBold().endOfLine();
+    printer.print("Select mode followed by <Enter>").endOfLine();
+  }
+
+  public void showMode(int pos, Mode mode, boolean selected, int modeHeaderMaxLength,
+    int modeDescriptionMaxLength) {
+
+    String modeHeader = String.format("%-" + modeHeaderMaxLength + "s", mode.getHeader());
+    String modeDescription = String.format("%-" + modeDescriptionMaxLength + "s",
+      mode.getDescription());
+
+    int row = MODE_START_ROW + pos;
+    TerminalPrinter printer = getTerminalPrinter(row);
+    if (selected) {
+      printer.startHighlight().print(modeHeader).stopHighlight()
+        .printFormat(" = %s", modeDescription).endOfLine();
+    } else {
+      printer.printFormat("%s = %s", modeHeader, modeDescription).endOfLine();
+    }
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/FilterDisplayModeScreenPresenter.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/FilterDisplayModeScreenPresenter.java
new file mode 100644
index 0000000..25ba608
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/FilterDisplayModeScreenPresenter.java
@@ -0,0 +1,53 @@
+/**
+ * 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.hadoop.hbase.hbtop.screen.top;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.RecordFilter;
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+
+/**
+ * The presentation logic for the filter display mode.
+ */
+@InterfaceAudience.Private
+public class FilterDisplayModeScreenPresenter {
+
+  private final FilterDisplayModeScreenView filterDisplayModeScreenView;
+  private final List<RecordFilter> filters;
+  private final ScreenView nextScreenView;
+
+  public FilterDisplayModeScreenPresenter(FilterDisplayModeScreenView filterDisplayModeScreenView,
+    List<RecordFilter> filters, ScreenView nextScreenView) {
+    this.filterDisplayModeScreenView = Objects.requireNonNull(filterDisplayModeScreenView);
+    this.filters = Collections.unmodifiableList(new ArrayList<>(Objects.requireNonNull(filters)));
+    this.nextScreenView = Objects.requireNonNull(nextScreenView);
+  }
+
+  public void init() {
+    filterDisplayModeScreenView.showFilters(filters);
+    filterDisplayModeScreenView.refreshTerminal();
+  }
+
+  public ScreenView returnToNextScreen() {
+    return nextScreenView;
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/FilterDisplayModeScreenView.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/FilterDisplayModeScreenView.java
new file mode 100644
index 0000000..42eeb97
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/FilterDisplayModeScreenView.java
@@ -0,0 +1,77 @@
+/**
+ * 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.hadoop.hbase.hbtop.screen.top;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.RecordFilter;
+import org.apache.hadoop.hbase.hbtop.screen.AbstractScreenView;
+import org.apache.hadoop.hbase.hbtop.screen.Screen;
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+import org.apache.hadoop.hbase.hbtop.terminal.KeyPress;
+import org.apache.hadoop.hbase.hbtop.terminal.Terminal;
+
+/**
+ * The filter display mode in the top screen.
+ *
+ * Exit if Enter key is pressed.
+ */
+@InterfaceAudience.Private
+public class FilterDisplayModeScreenView extends AbstractScreenView {
+
+  private final int row;
+  private final FilterDisplayModeScreenPresenter filterDisplayModeScreenPresenter;
+
+  public FilterDisplayModeScreenView(Screen screen, Terminal terminal, int row,
+    List<RecordFilter> filters, ScreenView nextScreenView) {
+    super(screen, terminal);
+    this.row = row;
+    this.filterDisplayModeScreenPresenter =
+      new FilterDisplayModeScreenPresenter(this, filters, nextScreenView);
+  }
+
+  @Override
+  public void init() {
+    filterDisplayModeScreenPresenter.init();
+  }
+
+  @Override
+  public ScreenView handleKeyPress(KeyPress keyPress) {
+    if (keyPress.getType() == KeyPress.Type.Enter) {
+      return filterDisplayModeScreenPresenter.returnToNextScreen();
+    }
+    return this;
+  }
+
+  public void showFilters(List<RecordFilter> filters) {
+    String filtersString = "none";
+    if (!filters.isEmpty()) {
+      List<String> filterStrings = new ArrayList<>();
+      for (RecordFilter filter : filters) {
+        filterStrings.add(String.format("'%s'", filter));
+      }
+      filtersString = StringUtils.join(filterStrings, " + ");
+    }
+
+    getTerminalPrinter(row).startBold().print("<Enter> to resume, filters: " + filtersString)
+      .stopBold().endOfLine();
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/Header.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/Header.java
new file mode 100644
index 0000000..bb7230d
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/Header.java
@@ -0,0 +1,48 @@
+/**
+ * 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.hadoop.hbase.hbtop.screen.top;
+
+import java.util.Objects;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+
+/**
+ * Represents headers for the metrics in the top screen.
+ */
+@InterfaceAudience.Private
+public class Header {
+  private final Field field;
+  private final int length;
+
+  public Header(Field field, int length) {
+    this.field = Objects.requireNonNull(field);
+    this.length = length;
+  }
+
+  public String format() {
+    return "%" + (field.isLeftJustify() ? "-" : "")  + length + "s";
+  }
+
+  public Field getField() {
+    return field;
+  }
+
+  public int getLength() {
+    return length;
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/InputModeScreenPresenter.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/InputModeScreenPresenter.java
new file mode 100644
index 0000000..33ec96f
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/InputModeScreenPresenter.java
@@ -0,0 +1,168 @@
+/**
+ * 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.hadoop.hbase.hbtop.screen.top;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+
+/**
+ * The presentation logic for the input mode.
+ */
+@InterfaceAudience.Private
+public class InputModeScreenPresenter {
+
+  public interface ResultListener {
+    public ScreenView apply(String inputString);
+  }
+
+  private final InputModeScreenView inputModeScreenView;
+  private final String message;
+  private final List<String> histories;
+  private final ResultListener resultListener;
+
+  private StringBuilder inputString = new StringBuilder();
+  private int cursorPosition;
+  private int historyPosition = -1;
+
+  public InputModeScreenPresenter(InputModeScreenView inputModeScreenView, String message,
+    @Nullable List<String> histories, ResultListener resultListener) {
+    this.inputModeScreenView = Objects.requireNonNull(inputModeScreenView);
+    this.message = Objects.requireNonNull(message);
+
+    if (histories != null) {
+      this.histories = Collections.unmodifiableList(new ArrayList<>(histories));
+    } else {
+      this.histories = Collections.emptyList();
+    }
+
+    this.resultListener = Objects.requireNonNull(resultListener);
+  }
+
+  public void init() {
+    inputModeScreenView.showInput(message, inputString.toString(), cursorPosition);
+    inputModeScreenView.refreshTerminal();
+  }
+
+  public ScreenView returnToNextScreen() {
+    inputModeScreenView.hideCursor();
+    String result = inputString.toString();
+
+    return resultListener.apply(result);
+  }
+
+  public void character(Character character) {
+    inputString.insert(cursorPosition, character);
+    cursorPosition += 1;
+    inputModeScreenView.showInput(message, inputString.toString(), cursorPosition);
+    inputModeScreenView.refreshTerminal();
+  }
+
+  public void backspace() {
+    if (cursorPosition == 0) {
+      return;
+    }
+
+    inputString.deleteCharAt(cursorPosition - 1);
+    cursorPosition -= 1;
+    inputModeScreenView.showInput(message, inputString.toString(), cursorPosition);
+    inputModeScreenView.refreshTerminal();
+  }
+
+  public void delete() {
+    if (inputString.length() == 0 || cursorPosition > inputString.length() - 1) {
+      return;
+    }
+
+    inputString.deleteCharAt(cursorPosition);
+    inputModeScreenView.showInput(message, inputString.toString(), cursorPosition);
+    inputModeScreenView.refreshTerminal();
+  }
+
+  public void arrowLeft() {
+    if (cursorPosition == 0) {
+      return;
+    }
+
+    cursorPosition -= 1;
+    inputModeScreenView.showInput(message, inputString.toString(), cursorPosition);
+    inputModeScreenView.refreshTerminal();
+  }
+
+  public void arrowRight() {
+    if (cursorPosition > inputString.length() - 1) {
+      return;
+    }
+
+    cursorPosition += 1;
+    inputModeScreenView.showInput(message, inputString.toString(), cursorPosition);
+    inputModeScreenView.refreshTerminal();
+  }
+
+  public void home() {
+    cursorPosition = 0;
+    inputModeScreenView.showInput(message, inputString.toString(), cursorPosition);
+    inputModeScreenView.refreshTerminal();
+  }
+
+  public void end() {
+    cursorPosition = inputString.length();
+    inputModeScreenView.showInput(message, inputString.toString(), cursorPosition);
+    inputModeScreenView.refreshTerminal();
+  }
+
+  public void arrowUp() {
+    if (historyPosition == 0 || histories.isEmpty()) {
+      return;
+    }
+
+    if (historyPosition == -1) {
+      historyPosition = histories.size() - 1;
+    } else {
+      historyPosition -= 1;
+    }
+
+    inputString = new StringBuilder(histories.get(historyPosition));
+
+    cursorPosition = inputString.length();
+    inputModeScreenView.showInput(message, inputString.toString(), cursorPosition);
+    inputModeScreenView.refreshTerminal();
+  }
+
+  public void arrowDown() {
+    if (historyPosition == -1 || histories.isEmpty()) {
+      return;
+    }
+
+    if (historyPosition == histories.size() - 1) {
+      historyPosition = -1;
+      inputString = new StringBuilder();
+    } else {
+      historyPosition += 1;
+      inputString = new StringBuilder(histories.get(historyPosition));
+    }
+
+    cursorPosition = inputString.length();
+    inputModeScreenView.showInput(message, inputString.toString(), cursorPosition);
+    inputModeScreenView.refreshTerminal();
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/InputModeScreenView.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/InputModeScreenView.java
new file mode 100644
index 0000000..ad5847a
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/InputModeScreenView.java
@@ -0,0 +1,105 @@
+/**
+ * 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.hadoop.hbase.hbtop.screen.top;
+
+import java.util.List;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.screen.AbstractScreenView;
+import org.apache.hadoop.hbase.hbtop.screen.Screen;
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+import org.apache.hadoop.hbase.hbtop.terminal.KeyPress;
+import org.apache.hadoop.hbase.hbtop.terminal.Terminal;
+
+/**
+ * The input mode in the top screen.
+ */
+@InterfaceAudience.Private
+public class InputModeScreenView extends AbstractScreenView {
+
+  private final int row;
+  private final InputModeScreenPresenter inputModeScreenPresenter;
+
+  public InputModeScreenView(Screen screen, Terminal terminal, int row, String message,
+    List<String> histories, InputModeScreenPresenter.ResultListener resultListener) {
+    super(screen, terminal);
+    this.row = row;
+    this.inputModeScreenPresenter = new InputModeScreenPresenter(this, message, histories,
+      resultListener);
+  }
+
+  @Override
+  public void init() {
+    inputModeScreenPresenter.init();
+  }
+
+  @Override
+  public ScreenView handleKeyPress(KeyPress keyPress) {
+
+    switch (keyPress.getType()) {
+      case Enter:
+        return inputModeScreenPresenter.returnToNextScreen();
+
+      case Character:
+        inputModeScreenPresenter.character(keyPress.getCharacter());
+        break;
+
+      case Backspace:
+        inputModeScreenPresenter.backspace();
+        break;
+
+      case Delete:
+        inputModeScreenPresenter.delete();
+        break;
+
+      case ArrowLeft:
+        inputModeScreenPresenter.arrowLeft();
+        break;
+
+      case ArrowRight:
+        inputModeScreenPresenter.arrowRight();
+        break;
+
+      case Home:
+        inputModeScreenPresenter.home();
+        break;
+
+      case End:
+        inputModeScreenPresenter.end();
+        break;
+
+      case ArrowUp:
+        inputModeScreenPresenter.arrowUp();
+        break;
+
+      case ArrowDown:
+        inputModeScreenPresenter.arrowDown();
+        break;
+
+      default:
+        break;
+    }
+    return this;
+  }
+
+  public void showInput(String message, String inputString, int cursorPosition) {
+    getTerminalPrinter(row).startBold().print(message).stopBold().print(" ").print(inputString)
+      .endOfLine();
+    setCursorPosition(message.length() + 1 + cursorPosition, row);
+    refreshTerminal();
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/MessageModeScreenPresenter.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/MessageModeScreenPresenter.java
new file mode 100644
index 0000000..419afbc
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/MessageModeScreenPresenter.java
@@ -0,0 +1,51 @@
+/**
+ * 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.hadoop.hbase.hbtop.screen.top;
+
+import java.util.Objects;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+
+/**
+ * The presentation logic for the message mode.
+ *
+ * Exit after 2 seconds or if any key is pressed.
+ */
+@InterfaceAudience.Private
+public class MessageModeScreenPresenter {
+
+  private final MessageModeScreenView messageModeScreenView;
+  private final String message;
+  private final ScreenView nextScreenView;
+
+  public MessageModeScreenPresenter(MessageModeScreenView messageModeScreenView, String message,
+    ScreenView nextScreenView) {
+    this.messageModeScreenView = Objects.requireNonNull(messageModeScreenView);
+    this.message = Objects.requireNonNull(message);
+    this.nextScreenView = Objects.requireNonNull(nextScreenView);
+  }
+
+  public void init() {
+    messageModeScreenView.showMessage(message);
+    messageModeScreenView.refreshTerminal();
+  }
+
+  public ScreenView returnToNextScreen() {
+    return nextScreenView;
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/MessageModeScreenView.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/MessageModeScreenView.java
new file mode 100644
index 0000000..955b145
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/MessageModeScreenView.java
@@ -0,0 +1,65 @@
+/**
+ * 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.hadoop.hbase.hbtop.screen.top;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.screen.AbstractScreenView;
+import org.apache.hadoop.hbase.hbtop.screen.Screen;
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+import org.apache.hadoop.hbase.hbtop.terminal.KeyPress;
+import org.apache.hadoop.hbase.hbtop.terminal.Terminal;
+
+/**
+ * The message mode in the top screen.
+ */
+@InterfaceAudience.Private
+public class MessageModeScreenView extends AbstractScreenView {
+
+  private final int row;
+  private final MessageModeScreenPresenter messageModeScreenPresenter;
+
+  public MessageModeScreenView(Screen screen, Terminal terminal, int row, String message,
+    ScreenView nextScreenView) {
+    super(screen, terminal);
+    this.row = row;
+    this.messageModeScreenPresenter =
+      new MessageModeScreenPresenter(this, message, nextScreenView);
+  }
+
+  @Override
+  public void init() {
+    messageModeScreenPresenter.init();
+    setTimer(2000);
+  }
+
+  @Override
+  public ScreenView handleTimer() {
+    return messageModeScreenPresenter.returnToNextScreen();
+  }
+
+  @Override
+  public ScreenView handleKeyPress(KeyPress keyPress) {
+    cancelTimer();
+    return messageModeScreenPresenter.returnToNextScreen();
+  }
+
+  public void showMessage(String message) {
+    getTerminalPrinter(row).startHighlight().print(" ").print(message).print(" ").stopHighlight()
+      .endOfLine();
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/Paging.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/Paging.java
new file mode 100644
index 0000000..276d3e3
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/Paging.java
@@ -0,0 +1,151 @@
+/**
+ * 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.hadoop.hbase.hbtop.screen.top;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+
+/**
+ * Utility class for paging for the metrics.
+ */
+@InterfaceAudience.Private
+public class Paging {
+  private int currentPosition;
+  private int pageStartPosition;
+  private int pageEndPosition;
+
+  private int pageSize;
+  private int recordsSize;
+
+  public void init() {
+    currentPosition = 0;
+    pageStartPosition = 0;
+    pageEndPosition = Math.min(pageSize, recordsSize);
+  }
+
+  public void updatePageSize(int pageSize) {
+    this.pageSize = pageSize;
+
+    if (pageSize == 0) {
+      pageStartPosition = 0;
+      pageEndPosition = 0;
+    } else {
+      pageEndPosition = pageStartPosition + pageSize;
+      keepConsistent();
+    }
+  }
+
+  public void updateRecordsSize(int recordsSize) {
+    if (this.recordsSize == 0) {
+      currentPosition = 0;
+      pageStartPosition = 0;
+      pageEndPosition = Math.min(pageSize, recordsSize);
+      this.recordsSize = recordsSize;
+    } else if (recordsSize == 0) {
+      currentPosition = 0;
+      pageStartPosition = 0;
+      pageEndPosition = 0;
+      this.recordsSize = recordsSize;
+    } else {
+      this.recordsSize = recordsSize;
+      if (pageSize > 0) {
+        pageEndPosition = pageStartPosition + pageSize;
+        keepConsistent();
+      }
+    }
+  }
+
+  public void arrowUp() {
+    if (currentPosition > 0) {
+      currentPosition -= 1;
+      if (pageSize > 0) {
+        keepConsistent();
+      }
+    }
+  }
+
+  public void arrowDown() {
+    if (currentPosition < recordsSize - 1) {
+      currentPosition += 1;
+      if (pageSize > 0) {
+        keepConsistent();
+      }
+    }
+  }
+
+  public void pageUp() {
+    if (pageSize > 0 && currentPosition > 0) {
+      currentPosition -= pageSize;
+      if (currentPosition < 0) {
+        currentPosition = 0;
+      }
+      keepConsistent();
+    }
+  }
+
+  public void pageDown() {
+    if (pageSize > 0 && currentPosition < recordsSize - 1) {
+
+      currentPosition = currentPosition + pageSize;
+      if (currentPosition >= recordsSize) {
+        currentPosition = recordsSize - 1;
+      }
+
+      pageStartPosition = currentPosition;
+      pageEndPosition = pageStartPosition + pageSize;
+      keepConsistent();
+    }
+  }
+
+  private void keepConsistent() {
+    if (currentPosition < pageStartPosition) {
+      pageStartPosition = currentPosition;
+      pageEndPosition = pageStartPosition + pageSize;
+    } else if (currentPosition > recordsSize - 1) {
+      currentPosition = recordsSize - 1;
+      pageEndPosition = recordsSize;
+      pageStartPosition = pageEndPosition - pageSize;
+    } else if (currentPosition > pageEndPosition - 1) {
+      pageEndPosition = currentPosition + 1;
+      pageStartPosition = pageEndPosition - pageSize;
+    }
+
+    if (pageStartPosition < 0) {
+      pageStartPosition = 0;
+    }
+
+    if (pageEndPosition > recordsSize) {
+      pageEndPosition = recordsSize;
+      pageStartPosition = pageEndPosition - pageSize;
+      if (pageStartPosition < 0) {
+        pageStartPosition = 0;
+      }
+    }
+  }
+
+  public int getCurrentPosition() {
+    return currentPosition;
+  }
+
+  public int getPageStartPosition() {
+    return pageStartPosition;
+  }
+
+  public int getPageEndPosition() {
+    return pageEndPosition;
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/Summary.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/Summary.java
new file mode 100644
index 0000000..a3feff6
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/Summary.java
@@ -0,0 +1,93 @@
+/**
+ * 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.hadoop.hbase.hbtop.screen.top;
+
+import java.util.Objects;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+
+/**
+ * Represents the summary of the metrics.
+ */
+@InterfaceAudience.Private
+public class Summary {
+  private final String currentTime;
+  private final String version;
+  private final String clusterId;
+  private final int servers;
+  private final int liveServers;
+  private final int deadServers;
+  private final int regionCount;
+  private final int ritCount;
+  private final double averageLoad;
+  private final long aggregateRequestPerSecond;
+
+  public Summary(String currentTime, String version, String clusterId, int servers,
+    int liveServers, int deadServers, int regionCount, int ritCount, double averageLoad,
+    long aggregateRequestPerSecond) {
+    this.currentTime = Objects.requireNonNull(currentTime);
+    this.version = Objects.requireNonNull(version);
+    this.clusterId = Objects.requireNonNull(clusterId);
+    this.servers = servers;
+    this.liveServers = liveServers;
+    this.deadServers = deadServers;
+    this.regionCount = regionCount;
+    this.ritCount = ritCount;
+    this.averageLoad = averageLoad;
+    this.aggregateRequestPerSecond = aggregateRequestPerSecond;
+  }
+
+  public String getCurrentTime() {
+    return currentTime;
+  }
+
+  public String getVersion() {
+    return version;
+  }
+
+  public String getClusterId() {
+    return clusterId;
+  }
+
+  public int getServers() {
+    return servers;
+  }
+
+  public int getLiveServers() {
+    return liveServers;
+  }
+
+  public int getDeadServers() {
+    return deadServers;
+  }
+
+  public int getRegionCount() {
+    return regionCount;
+  }
+
+  public int getRitCount() {
+    return ritCount;
+  }
+
+  public double getAverageLoad() {
+    return averageLoad;
+  }
+
+  public long getAggregateRequestPerSecond() {
+    return aggregateRequestPerSecond;
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/TopScreenModel.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/TopScreenModel.java
new file mode 100644
index 0000000..f795c4a
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/TopScreenModel.java
@@ -0,0 +1,235 @@
+/**
+ * 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.hadoop.hbase.hbtop.screen.top;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import org.apache.commons.lang3.time.DateFormatUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.hadoop.hbase.ClusterStatus;
+import org.apache.hadoop.hbase.ServerLoad;
+import org.apache.hadoop.hbase.ServerName;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.client.Admin;
+import org.apache.hadoop.hbase.hbtop.Record;
+import org.apache.hadoop.hbase.hbtop.RecordFilter;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.hbtop.field.FieldInfo;
+import org.apache.hadoop.hbase.hbtop.field.FieldValue;
+import org.apache.hadoop.hbase.hbtop.mode.DrillDownInfo;
+import org.apache.hadoop.hbase.hbtop.mode.Mode;
+
+/**
+ * The data and business logic for the top screen.
+ */
+@InterfaceAudience.Private
+public class TopScreenModel {
+
+  private static final Log LOG = LogFactory.getLog(TopScreenModel.class);
+
+  private final Admin admin;
+
+  private Mode currentMode;
+  private Field currentSortField;
+  private List<FieldInfo> fieldInfos;
+  private List<Field> fields;
+
+  private Summary summary;
+  private List<Record> records;
+
+  private final List<RecordFilter> filters = new ArrayList<>();
+  private final List<String> filterHistories = new ArrayList<>();
+
+  private boolean ascendingSort;
+
+  public TopScreenModel(Admin admin, Mode initialMode) {
+    this.admin = Objects.requireNonNull(admin);
+    switchMode(Objects.requireNonNull(initialMode), null, false);
+  }
+
+  public void switchMode(Mode nextMode, List<RecordFilter> initialFilters,
+    boolean keepSortFieldAndSortOrderIfPossible) {
+
+    currentMode = nextMode;
+    fieldInfos = Collections.unmodifiableList(new ArrayList<>(currentMode.getFieldInfos()));
+
+    fields = new ArrayList<>();
+    for (FieldInfo fieldInfo : currentMode.getFieldInfos()) {
+      fields.add(fieldInfo.getField());
+    }
+    fields = Collections.unmodifiableList(fields);
+
+    if (keepSortFieldAndSortOrderIfPossible) {
+      boolean match = false;
+      for (Field field : fields) {
+        if (field == currentSortField) {
+          match = true;
+          break;
+        }
+      }
+
+      if (!match) {
+        currentSortField = nextMode.getDefaultSortField();
+        ascendingSort = false;
+      }
+
+    } else {
+      currentSortField = nextMode.getDefaultSortField();
+      ascendingSort = false;
+    }
+
+    clearFilters();
+    if (initialFilters != null) {
+      filters.addAll(initialFilters);
+    }
+  }
+
+  public void setSortFieldAndFields(Field sortField, List<Field> fields) {
+    this.currentSortField = sortField;
+    this.fields = Collections.unmodifiableList(new ArrayList<>(fields));
+  }
+
+  /*
+   * HBTop only calls this from a single thread, and if that ever changes, this needs
+   * synchronization
+   */
+  public void refreshMetricsData() {
+    ClusterStatus clusterStatus;
+    try {
+      clusterStatus = admin.getClusterStatus();
+    } catch (Exception e) {
+      LOG.error("Unable to get cluster status", e);
+      return;
+    }
+
+    refreshSummary(clusterStatus);
+    refreshRecords(clusterStatus);
+  }
+
+  private void refreshSummary(ClusterStatus clusterStatus) {
+    String currentTime = DateFormatUtils.ISO_8601_EXTENDED_TIME_FORMAT
+      .format(System.currentTimeMillis());
+    String version = clusterStatus.getHBaseVersion();
+    String clusterId = clusterStatus.getClusterId();
+    int liveServers = clusterStatus.getServersSize();
+    int deadServers = clusterStatus.getDeadServerNames().size();
+    int regionCount = clusterStatus.getRegionsCount();
+    int ritCount = clusterStatus.getRegionsInTransition().size();
+    double averageLoad = clusterStatus.getAverageLoad();
+    long aggregateRequestPerSecond = 0;
+    for (ServerName sn: clusterStatus.getServers()) {
+      ServerLoad sl = clusterStatus.getLoad(sn);
+      aggregateRequestPerSecond += sl.getNumberOfRequests();
+    }
+    summary = new Summary(currentTime, version, clusterId, liveServers + deadServers,
+      liveServers, deadServers, regionCount, ritCount, averageLoad, aggregateRequestPerSecond);
+  }
+
+  private void refreshRecords(ClusterStatus clusterStatus) {
+    // Filter
+    List<Record> records = new ArrayList<>();
+    for (Record record : currentMode.getRecords(clusterStatus)) {
+      boolean filter = false;
+      for (RecordFilter recordFilter : filters) {
+        if (!recordFilter.execute(record)) {
+          filter = true;
+          break;
+        }
+      }
+      if (!filter) {
+        records.add(record);
+      }
+    }
+
+    // Sort
+    Collections.sort(records, new Comparator<Record>() {
+      @Override
+      public int compare(Record recordLeft, Record recordRight) {
+        FieldValue left = recordLeft.get(currentSortField);
+        FieldValue right = recordRight.get(currentSortField);
+        return (ascendingSort ? 1 : -1) * left.compareTo(right);
+      }
+    });
+
+    this.records = Collections.unmodifiableList(records);
+  }
+
+  public void switchSortOrder() {
+    ascendingSort = !ascendingSort;
+  }
+
+  public boolean addFilter(String filterString, boolean ignoreCase) {
+    RecordFilter filter = RecordFilter.parse(filterString, fields, ignoreCase);
+    if (filter == null) {
+      return false;
+    }
+
+    filters.add(filter);
+    filterHistories.add(filterString);
+    return true;
+  }
+
+  public void clearFilters() {
+    filters.clear();
+  }
+
+  public boolean drillDown(Record selectedRecord) {
+    DrillDownInfo drillDownInfo = currentMode.drillDown(selectedRecord);
+    if (drillDownInfo == null) {
+      return false;
+    }
+    switchMode(drillDownInfo.getNextMode(), drillDownInfo.getInitialFilters(), true);
+    return true;
+  }
+
+  public Mode getCurrentMode() {
+    return currentMode;
+  }
+
+  public Field getCurrentSortField() {
+    return currentSortField;
+  }
+
+  public List<FieldInfo> getFieldInfos() {
+    return fieldInfos;
+  }
+
+  public List<Field> getFields() {
+    return fields;
+  }
+
+  public Summary getSummary() {
+    return summary;
+  }
+
+  public List<Record> getRecords() {
+    return records;
+  }
+
+  public List<RecordFilter> getFilters() {
+    return Collections.unmodifiableList(filters);
+  }
+
+  public List<String> getFilterHistories() {
+    return Collections.unmodifiableList(filterHistories);
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/TopScreenPresenter.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/TopScreenPresenter.java
new file mode 100644
index 0000000..02c35b8
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/TopScreenPresenter.java
@@ -0,0 +1,356 @@
+/**
+ * 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.hadoop.hbase.hbtop.screen.top;
+
+import java.util.ArrayList;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.Record;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.hbtop.field.FieldInfo;
+import org.apache.hadoop.hbase.hbtop.mode.Mode;
+import org.apache.hadoop.hbase.hbtop.screen.Screen;
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+import org.apache.hadoop.hbase.hbtop.screen.field.FieldScreenPresenter;
+import org.apache.hadoop.hbase.hbtop.screen.field.FieldScreenView;
+import org.apache.hadoop.hbase.hbtop.screen.help.HelpScreenView;
+import org.apache.hadoop.hbase.hbtop.screen.mode.ModeScreenPresenter;
+import org.apache.hadoop.hbase.hbtop.screen.mode.ModeScreenView;
+import org.apache.hadoop.hbase.hbtop.terminal.Terminal;
+import org.apache.hadoop.hbase.hbtop.terminal.TerminalSize;
+
+/**
+ * The presentation logic for the top screen.
+ */
+@InterfaceAudience.Private
+public class TopScreenPresenter {
+  private final TopScreenView topScreenView;
+  private final AtomicLong refreshDelay;
+  private long lastRefreshTimestamp;
+
+  private final AtomicBoolean adjustFieldLength = new AtomicBoolean(true);
+  private final TopScreenModel topScreenModel;
+  private int terminalLength;
+  private int horizontalScroll;
+  private final Paging paging = new Paging();
+
+  private final EnumMap<Field, Boolean> fieldDisplayMap = new EnumMap<>(Field.class);
+  private final EnumMap<Field, Integer> fieldLengthMap = new EnumMap<>(Field.class);
+
+  public TopScreenPresenter(TopScreenView topScreenView, long initialRefreshDelay,
+    TopScreenModel topScreenModel) {
+    this.topScreenView = Objects.requireNonNull(topScreenView);
+    this.refreshDelay = new AtomicLong(initialRefreshDelay);
+    this.topScreenModel = Objects.requireNonNull(topScreenModel);
+
+    initFieldDisplayMapAndFieldLengthMap();
+  }
+
+  public void init() {
+    terminalLength = topScreenView.getTerminalSize().getColumns();
+    paging.updatePageSize(topScreenView.getPageSize());
+    topScreenView.hideCursor();
+  }
+
+  public long refresh(boolean force) {
+    if (!force) {
+      long delay = System.currentTimeMillis() - lastRefreshTimestamp;
+      if (delay < refreshDelay.get()) {
+        return refreshDelay.get() - delay;
+      }
+    }
+
+    TerminalSize newTerminalSize = topScreenView.doResizeIfNecessary();
+    if (newTerminalSize != null) {
+      terminalLength = newTerminalSize.getColumns();
+      paging.updatePageSize(topScreenView.getPageSize());
+      topScreenView.clearTerminal();
+    }
+
+    topScreenModel.refreshMetricsData();
+    paging.updateRecordsSize(topScreenModel.getRecords().size());
+
+    adjustFieldLengthIfNeeded();
+
+    topScreenView.showTopScreen(topScreenModel.getSummary(), getDisplayedHeaders(),
+      getDisplayedRecords(), getSelectedRecord());
+
+    topScreenView.refreshTerminal();
+
+    lastRefreshTimestamp = System.currentTimeMillis();
+    return refreshDelay.get();
+  }
+
+  public void adjustFieldLength() {
+    adjustFieldLength.set(true);
+    refresh(true);
+  }
+
+  private void adjustFieldLengthIfNeeded() {
+    if (adjustFieldLength.get()) {
+      adjustFieldLength.set(false);
+
+      for (Field f : topScreenModel.getFields()) {
+        if (f.isAutoAdjust()) {
+          int maxLength = 0;
+          for (Record record : topScreenModel.getRecords()) {
+            int length = record.get(f).asString().length();
+            maxLength = Math.max(length, maxLength);
+          }
+          fieldLengthMap.put(f, Math.max(maxLength, f.getHeader().length()));
+        }
+      }
+    }
+  }
+
+  private List<Header> getDisplayedHeaders() {
+    List<Field> displayFields = new ArrayList<>();
+    for (Field field : topScreenModel.getFields()) {
+      if (fieldDisplayMap.get(field)) {
+        displayFields.add(field);
+      }
+    }
+
+    if (displayFields.isEmpty()) {
+      horizontalScroll = 0;
+    } else if (horizontalScroll > displayFields.size() - 1) {
+      horizontalScroll = displayFields.size() - 1;
+    }
+
+    List<Header> ret = new ArrayList<>();
+
+    int length = 0;
+    for (int i = horizontalScroll; i < displayFields.size(); i++) {
+      Field field = displayFields.get(i);
+      int fieldLength = fieldLengthMap.get(field);
+
+      length += fieldLength + 1;
+      if (length > terminalLength) {
+        break;
+      }
+      ret.add(new Header(field, fieldLength));
+    }
+
+    return ret;
+  }
+
+  private List<Record> getDisplayedRecords() {
+    List<Record> ret = new ArrayList<>();
+    for (int i = paging.getPageStartPosition(); i < paging.getPageEndPosition(); i++) {
+      ret.add(topScreenModel.getRecords().get(i));
+    }
+    return ret;
+  }
+
+  private Record getSelectedRecord() {
+    if (topScreenModel.getRecords().isEmpty()) {
+      return null;
+    }
+    return topScreenModel.getRecords().get(paging.getCurrentPosition());
+  }
+
+  public void arrowUp() {
+    paging.arrowUp();
+    refresh(true);
+  }
+
+  public void arrowDown() {
+    paging.arrowDown();
+    refresh(true);
+  }
+
+  public void pageUp() {
+    paging.pageUp();
+    refresh(true);
+  }
+
+  public void pageDown() {
+    paging.pageDown();
+    refresh(true);
+  }
+
+  public void arrowLeft() {
+    if (horizontalScroll > 0) {
+      horizontalScroll -= 1;
+    }
+    refresh(true);
+  }
+
+  public void arrowRight() {
+    if (horizontalScroll < getHeaderSize() - 1) {
+      horizontalScroll += 1;
+    }
+    refresh(true);
+  }
+
+  public void home() {
+    if (horizontalScroll > 0) {
+      horizontalScroll = 0;
+    }
+    refresh(true);
+  }
+
+  public void end() {
+    int headerSize = getHeaderSize();
+    horizontalScroll = headerSize == 0 ? 0 : headerSize - 1;
+    refresh(true);
+  }
+
+  private int getHeaderSize() {
+    int size = 0;
+    for (Field field : topScreenModel.getFields()) {
+      if (fieldDisplayMap.get(field)) {
+        size++;
+      }
+    }
+    return size;
+  }
+
+  public void switchSortOrder() {
+    topScreenModel.switchSortOrder();
+    refresh(true);
+  }
+
+  public ScreenView transitionToHelpScreen(Screen screen, Terminal terminal) {
+    return new HelpScreenView(screen, terminal, refreshDelay.get(), topScreenView);
+  }
+
+  public ScreenView transitionToModeScreen(Screen screen, Terminal terminal) {
+    return new ModeScreenView(screen, terminal, topScreenModel.getCurrentMode(),
+      new ModeScreenPresenter.ResultListener() {
+        @Override
+        public void accept(Mode mode) {
+          switchMode(mode);
+        }
+      }, topScreenView);
+  }
+
+  public ScreenView transitionToFieldScreen(Screen screen, Terminal terminal) {
+    return new FieldScreenView(screen, terminal,
+      topScreenModel.getCurrentSortField(), topScreenModel.getFields(),
+      fieldDisplayMap,
+      new FieldScreenPresenter.ResultListener() {
+        @Override
+        public void accept(Field sortField, List<Field> fields,
+          EnumMap<Field, Boolean> fieldDisplayMap) {
+          topScreenModel.setSortFieldAndFields(sortField, fields);
+          TopScreenPresenter.this.fieldDisplayMap.clear();
+          TopScreenPresenter.this.fieldDisplayMap.putAll(fieldDisplayMap);
+        }
+      }, topScreenView);
+  }
+
+  private void switchMode(Mode nextMode) {
+    topScreenModel.switchMode(nextMode, null, false);
+    reset();
+  }
+
+  public void drillDown() {
+    Record selectedRecord = getSelectedRecord();
+    if (selectedRecord == null) {
+      return;
+    }
+    if (topScreenModel.drillDown(selectedRecord)) {
+      reset();
+      refresh(true);
+    }
+  }
+
+  private void reset() {
+    initFieldDisplayMapAndFieldLengthMap();
+    adjustFieldLength.set(true);
+    paging.init();
+    horizontalScroll = 0;
+    topScreenView.clearTerminal();
+  }
+
+  private void initFieldDisplayMapAndFieldLengthMap() {
+    fieldDisplayMap.clear();
+    fieldLengthMap.clear();
+    for (FieldInfo fieldInfo : topScreenModel.getFieldInfos()) {
+      fieldDisplayMap.put(fieldInfo.getField(), fieldInfo.isDisplayByDefault());
+      fieldLengthMap.put(fieldInfo.getField(), fieldInfo.getDefaultLength());
+    }
+  }
+
+  public ScreenView goToMessageMode(Screen screen, Terminal terminal, int row, String message) {
+    return new MessageModeScreenView(screen, terminal, row, message, topScreenView);
+  }
+
+  public ScreenView goToInputModeForRefreshDelay(final Screen screen, final Terminal terminal,
+    final int row) {
+    return new InputModeScreenView(screen, terminal, row,
+      "Change refresh delay from " + (double) refreshDelay.get() / 1000 + " to", null,
+      new InputModeScreenPresenter.ResultListener() {
+        @Override
+        public ScreenView apply(String inputString) {
+          if (inputString.isEmpty()) {
+            return topScreenView;
+          }
+
+          double delay;
+          try {
+            delay = Double.parseDouble(inputString);
+          } catch (NumberFormatException e) {
+            return goToMessageMode(screen, terminal, row, "Unacceptable floating point");
+          }
+
+          refreshDelay.set((long) (delay * 1000));
+          return topScreenView;
+        }
+      });
+  }
+
+  public ScreenView goToInputModeForFilter(final Screen screen, final Terminal terminal,
+    final int row, final boolean ignoreCase) {
+    return new InputModeScreenView(screen, terminal, row,
+      "add filter #" + (topScreenModel.getFilters().size() + 1) +
+        " (" + (ignoreCase ? "ignoring case" : "case sensitive") + ") as: [!]FLD?VAL",
+      topScreenModel.getFilterHistories(),
+      new InputModeScreenPresenter.ResultListener() {
+        @Override
+        public ScreenView apply(String inputString) {
+          if (inputString.isEmpty()) {
+            return topScreenView;
+          }
+
+          if (!topScreenModel.addFilter(inputString, ignoreCase)) {
+            return goToMessageMode(screen, terminal, row, "Unacceptable filter expression");
+          }
+
+          paging.init();
+          return topScreenView;
+        }
+      });
+  }
+
+  public void clearFilters() {
+    topScreenModel.clearFilters();
+    paging.init();
+    refresh(true);
+  }
+
+  public ScreenView goToFilterDisplayMode(Screen screen, Terminal terminal, int row) {
+    return new FilterDisplayModeScreenView(screen, terminal, row, topScreenModel.getFilters(),
+      topScreenView);
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/TopScreenView.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/TopScreenView.java
new file mode 100644
index 0000000..6d9348b
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/TopScreenView.java
@@ -0,0 +1,308 @@
+/**
+ * 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.hadoop.hbase.hbtop.screen.top;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.client.Admin;
+import org.apache.hadoop.hbase.hbtop.Record;
+import org.apache.hadoop.hbase.hbtop.mode.Mode;
+import org.apache.hadoop.hbase.hbtop.screen.AbstractScreenView;
+import org.apache.hadoop.hbase.hbtop.screen.Screen;
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+import org.apache.hadoop.hbase.hbtop.terminal.KeyPress;
+import org.apache.hadoop.hbase.hbtop.terminal.Terminal;
+import org.apache.hadoop.hbase.hbtop.terminal.TerminalPrinter;
+import org.apache.hadoop.hbase.hbtop.terminal.TerminalSize;
+
+/**
+ * The screen that provides a dynamic real-time view for the HBase metrics.
+ *
+ * This shows the metric {@link Summary} and the metric {@link Record}s. The summary and the
+ * metrics are updated periodically (3 seconds by default).
+ */
+@InterfaceAudience.Private
+public class TopScreenView extends AbstractScreenView {
+
+  private static final int SUMMARY_START_ROW = 0;
+  private static final int SUMMARY_ROW_NUM = 7;
+  private static final int MESSAGE_ROW = 7;
+  private static final int RECORD_HEADER_ROW = 8;
+  private static final int RECORD_START_ROW = 9;
+
+  private final TopScreenPresenter topScreenPresenter;
+  private int pageSize;
+
+  public TopScreenView(Screen screen, Terminal terminal, long initialRefreshDelay, Admin admin,
+    Mode initialMode) {
+    super(screen, terminal);
+    this.topScreenPresenter = new TopScreenPresenter(this, initialRefreshDelay,
+      new TopScreenModel(admin, initialMode));
+  }
+
+  @Override
+  public void init() {
+    topScreenPresenter.init();
+    long delay = topScreenPresenter.refresh(true);
+    setTimer(delay);
+  }
+
+  @Override
+  public ScreenView handleTimer() {
+    long delay = topScreenPresenter.refresh(false);
+    setTimer(delay);
+    return this;
+  }
+
+  @Nullable
+  @Override
+  public ScreenView handleKeyPress(KeyPress keyPress) {
+    switch (keyPress.getType()) {
+      case Enter:
+        topScreenPresenter.refresh(true);
+        return this;
+
+      case ArrowUp:
+        topScreenPresenter.arrowUp();
+        return this;
+
+      case ArrowDown:
+        topScreenPresenter.arrowDown();
+        return this;
+
+      case ArrowLeft:
+        topScreenPresenter.arrowLeft();
+        return this;
+
+      case ArrowRight:
+        topScreenPresenter.arrowRight();
+        return this;
+
+      case PageUp:
+        topScreenPresenter.pageUp();
+        return this;
+
+      case PageDown:
+        topScreenPresenter.pageDown();
+        return this;
+
+      case Home:
+        topScreenPresenter.home();
+        return this;
+
+      case End:
+        topScreenPresenter.end();
+        return this;
+
+      case Escape:
+        return null;
+
+      default:
+        // Do nothing
+        break;
+    }
+
+    if (keyPress.getType() != KeyPress.Type.Character) {
+      return unknownCommandMessage();
+    }
+
+    assert keyPress.getCharacter() != null;
+    switch (keyPress.getCharacter()) {
+      case 'R':
+        topScreenPresenter.switchSortOrder();
+        break;
+
+      case 'f':
+        cancelTimer();
+        return topScreenPresenter.transitionToFieldScreen(getScreen(), getTerminal());
+
+      case 'm':
+        cancelTimer();
+        return topScreenPresenter.transitionToModeScreen(getScreen(), getTerminal());
+
+      case 'h':
+        cancelTimer();
+        return topScreenPresenter.transitionToHelpScreen(getScreen(), getTerminal());
+
+      case 'd':
+        cancelTimer();
+        return topScreenPresenter.goToInputModeForRefreshDelay(getScreen(), getTerminal(),
+          MESSAGE_ROW);
+
+      case 'o':
+        cancelTimer();
+        if (keyPress.isCtrl()) {
+          return topScreenPresenter.goToFilterDisplayMode(getScreen(), getTerminal(), MESSAGE_ROW);
+        }
+        return topScreenPresenter.goToInputModeForFilter(getScreen(), getTerminal(), MESSAGE_ROW,
+          true);
+
+      case 'O':
+        cancelTimer();
+        return topScreenPresenter.goToInputModeForFilter(getScreen(), getTerminal(), MESSAGE_ROW,
+          false);
+
+      case '=':
+        topScreenPresenter.clearFilters();
+        break;
+
+      case 'X':
+        topScreenPresenter.adjustFieldLength();
+        break;
+
+      case 'i':
+        topScreenPresenter.drillDown();
+        break;
+
+      case 'q':
+        return null;
+
+      default:
+        return unknownCommandMessage();
+    }
+    return this;
+  }
+
+  @Override
+  public TerminalSize getTerminalSize() {
+    TerminalSize terminalSize = super.getTerminalSize();
+    updatePageSize(terminalSize);
+    return terminalSize;
+  }
+
+  @Override
+  public TerminalSize doResizeIfNecessary() {
+    TerminalSize terminalSize = super.doResizeIfNecessary();
+    if (terminalSize == null) {
+      return null;
+    }
+    updatePageSize(terminalSize);
+    return terminalSize;
+  }
+
+  private void updatePageSize(TerminalSize terminalSize) {
+    pageSize = terminalSize.getRows() - SUMMARY_ROW_NUM - 2;
+    if (pageSize < 0) {
+      pageSize = 0;
+    }
+  }
+
+  public int getPageSize() {
+    return pageSize;
+  }
+
+  public void showTopScreen(Summary summary, List<Header> headers, List<Record> records,
+    Record selectedRecord) {
+    showSummary(summary);
+    clearMessage();
+    showHeaders(headers);
+    showRecords(headers, records, selectedRecord);
+  }
+
+  private void showSummary(Summary summary) {
+    TerminalPrinter printer = getTerminalPrinter(SUMMARY_START_ROW);
+    printer.print(String.format("HBase hbtop - %s", summary.getCurrentTime())).endOfLine();
+    printer.print(String.format("Version: %s", summary.getVersion())).endOfLine();
+    printer.print(String.format("Cluster ID: %s", summary.getClusterId())).endOfLine();
+    printer.print("RegionServer(s): ")
+      .startBold().print(Integer.toString(summary.getServers())).stopBold()
+      .print(" total, ")
+      .startBold().print(Integer.toString(summary.getLiveServers())).stopBold()
+      .print(" live, ")
+      .startBold().print(Integer.toString(summary.getDeadServers())).stopBold()
+      .print(" dead").endOfLine();
+    printer.print("RegionCount: ")
+      .startBold().print(Integer.toString(summary.getRegionCount())).stopBold()
+      .print(" total, ")
+      .startBold().print(Integer.toString(summary.getRitCount())).stopBold()
+      .print(" rit").endOfLine();
+    printer.print("Average Cluster Load: ")
+      .startBold().print(String.format("%.2f", summary.getAverageLoad())).stopBold().endOfLine();
+    printer.print("Aggregate Request/s: ")
+      .startBold().print(Long.toString(summary.getAggregateRequestPerSecond())).stopBold()
+      .endOfLine();
+  }
+
+  private void showRecords(List<Header> headers, List<Record> records, Record selectedRecord) {
+    TerminalPrinter printer = getTerminalPrinter(RECORD_START_ROW);
+    List<String> buf = new ArrayList<>(headers.size());
+    for (int i = 0; i < pageSize; i++) {
+      if(i < records.size()) {
+        Record record = records.get(i);
+        buf.clear();
+        for (Header header : headers) {
+          String value = "";
+          if (record.containsKey(header.getField())) {
+            value = record.get(header.getField()).asString();
+          }
+
+          buf.add(limitLineLength(String.format(header.format(), value), header.getLength()));
+        }
+
+        String recordString = StringUtils.join(buf, " ");
+        if (!recordString.isEmpty()) {
+          recordString += " ";
+        }
+
+        if (record == selectedRecord) {
+          printer.startHighlight().print(recordString).stopHighlight().endOfLine();
+        } else {
+          printer.print(recordString).endOfLine();
+        }
+      } else {
+        printer.endOfLine();
+      }
+    }
+  }
+
+  private void showHeaders(List<Header> headers) {
+    List<String> headerStrings = new ArrayList<>();
+    for (Header header : headers) {
+      headerStrings.add(String.format(header.format(), header.getField().getHeader()));
+    }
+    String header = StringUtils.join(headerStrings, " ");
+
+    if (!header.isEmpty()) {
+      header += " ";
+    }
+
+    getTerminalPrinter(RECORD_HEADER_ROW).startHighlight().print(header).stopHighlight()
+      .endOfLine();
+  }
+
+  private String limitLineLength(String line, int length) {
+    if (line.length() > length) {
+      return line.substring(0, length - 1) + "+";
+    }
+    return line;
+  }
+
+  private void clearMessage() {
+    getTerminalPrinter(MESSAGE_ROW).print("").endOfLine();
+  }
+
+  private ScreenView unknownCommandMessage() {
+    cancelTimer();
+    return topScreenPresenter.goToMessageMode(getScreen(), getTerminal(), MESSAGE_ROW,
+      "Unknown command - try 'h' for help");
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/AbstractTerminalPrinter.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/AbstractTerminalPrinter.java
new file mode 100644
index 0000000..237729c
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/AbstractTerminalPrinter.java
@@ -0,0 +1,69 @@
+/**
+ * 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.hadoop.hbase.hbtop.terminal;
+
+public abstract class AbstractTerminalPrinter implements TerminalPrinter {
+
+  @Override
+  public TerminalPrinter print(Object value) {
+    print(value.toString());
+    return this;
+  }
+
+  @Override
+  public TerminalPrinter print(char value) {
+    print(Character.toString(value));
+    return this;
+  }
+
+  @Override
+  public TerminalPrinter print(short value) {
+    print(Short.toString(value));
+    return this;
+  }
+
+  @Override
+  public TerminalPrinter print(int value) {
+    print(Integer.toString(value));
+    return this;
+  }
+
+  @Override
+  public TerminalPrinter print(long value) {
+    print(Long.toString(value));
+    return this;
+  }
+
+  @Override
+  public TerminalPrinter print(float value) {
+    print(Float.toString(value));
+    return this;
+  }
+
+  @Override
+  public TerminalPrinter print(double value) {
+    print(Double.toString(value));
+    return this;
+  }
+
+  @Override
+  public TerminalPrinter printFormat(String format, Object... args) {
+    print(String.format(format, args));
+    return this;
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/Attributes.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/Attributes.java
new file mode 100644
index 0000000..8fcbc78
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/Attributes.java
@@ -0,0 +1,128 @@
+/**
+ * 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.hadoop.hbase.hbtop.terminal;
+
+import java.util.Objects;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+
+/**
+ * The attributes of text in the terminal.
+ */
+@InterfaceAudience.Private
+public class Attributes {
+  private boolean bold;
+  private boolean blink;
+  private boolean reverse;
+  private boolean underline;
+  private Color foregroundColor;
+  private Color backgroundColor;
+
+  public Attributes() {
+    reset();
+  }
+
+  public Attributes(Attributes attributes) {
+    set(attributes);
+  }
+
+  public boolean isBold() {
+    return bold;
+  }
+
+  public void setBold(boolean bold) {
+    this.bold = bold;
+  }
+
+  public boolean isBlink() {
+    return blink;
+  }
+
+  public void setBlink(boolean blink) {
+    this.blink = blink;
+  }
+
+  public boolean isReverse() {
+    return reverse;
+  }
+
+  public void setReverse(boolean reverse) {
+    this.reverse = reverse;
+  }
+
+  public boolean isUnderline() {
+    return underline;
+  }
+
+  public void setUnderline(boolean underline) {
+    this.underline = underline;
+  }
+
+  public Color getForegroundColor() {
+    return foregroundColor;
+  }
+
+  public void setForegroundColor(Color foregroundColor) {
+    this.foregroundColor = foregroundColor;
+  }
+
+  public Color getBackgroundColor() {
+    return backgroundColor;
+  }
+
+  public void setBackgroundColor(Color backgroundColor) {
+    this.backgroundColor = backgroundColor;
+  }
+
+  public void reset() {
+    bold = false;
+    blink = false;
+    reverse = false;
+    underline = false;
+    foregroundColor = Color.WHITE;
+    backgroundColor = Color.BLACK;
+  }
+
+  public void set(Attributes attributes) {
+    bold = attributes.bold;
+    blink = attributes.blink;
+    reverse = attributes.reverse;
+    underline = attributes.underline;
+    foregroundColor = attributes.foregroundColor;
+    backgroundColor = attributes.backgroundColor;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof Attributes)) {
+      return false;
+    }
+    Attributes that = (Attributes) o;
+    return bold == that.bold && blink == that.blink && reverse == that.reverse
+      && underline == that.underline && foregroundColor == that.foregroundColor
+      && backgroundColor == that.backgroundColor;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(bold, blink, reverse, underline, foregroundColor, backgroundColor);
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/Color.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/Color.java
new file mode 100644
index 0000000..42baf5c
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/Color.java
@@ -0,0 +1,28 @@
+/**
+ * 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.hadoop.hbase.hbtop.terminal;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+
+/**
+ * Terminal color definitions.
+ */
+@InterfaceAudience.Private
+public enum Color {
+  BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/CursorPosition.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/CursorPosition.java
new file mode 100644
index 0000000..2b1d6de
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/CursorPosition.java
@@ -0,0 +1,61 @@
+/**
+ * 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.hadoop.hbase.hbtop.terminal;
+
+import java.util.Objects;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+
+/**
+ * A 2-d position in 'terminal space'.
+ */
+@InterfaceAudience.Private
+public class CursorPosition {
+  private final int column;
+  private final int row;
+
+  public CursorPosition(int column, int row) {
+    this.column = column;
+    this.row = row;
+  }
+
+  public int getColumn() {
+    return column;
+  }
+
+  public int getRow() {
+    return row;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof CursorPosition)) {
+      return false;
+    }
+    CursorPosition that = (CursorPosition) o;
+    return column == that.column && row == that.row;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(column, row);
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/KeyPress.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/KeyPress.java
new file mode 100644
index 0000000..5b334e3
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/KeyPress.java
@@ -0,0 +1,128 @@
+/**
+ * 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.hadoop.hbase.hbtop.terminal;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.util.Objects;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+
+/**
+ * Represents the user pressing a key on the keyboard.
+ */
+@InterfaceAudience.Private
+public class KeyPress {
+  public enum Type {
+    Character,
+    Escape,
+    Backspace,
+    ArrowLeft,
+    ArrowRight,
+    ArrowUp,
+    ArrowDown,
+    Insert,
+    Delete,
+    Home,
+    End,
+    PageUp,
+    PageDown,
+    ReverseTab,
+    Tab,
+    Enter,
+    F1,
+    F2,
+    F3,
+    F4,
+    F5,
+    F6,
+    F7,
+    F8,
+    F9,
+    F10,
+    F11,
+    F12,
+    Unknown
+  }
+
+  private final Type type;
+  private final Character character;
+  private final boolean alt;
+  private final boolean ctrl;
+  private final boolean shift;
+
+  public KeyPress(Type type, @Nullable Character character, boolean alt, boolean ctrl,
+    boolean shift) {
+    this.type = Objects.requireNonNull(type);
+    this.character = character;
+    this.alt = alt;
+    this.ctrl = ctrl;
+    this.shift = shift;
+  }
+
+  public Type getType() {
+    return type;
+  }
+
+  @Nullable
+  public Character getCharacter() {
+    return character;
+  }
+
+  public boolean isAlt() {
+    return alt;
+  }
+
+  public boolean isCtrl() {
+    return ctrl;
+  }
+
+  public boolean isShift() {
+    return shift;
+  }
+
+  @Override
+  public String toString() {
+    return "KeyPress{" +
+      "type=" + type +
+      ", character=" + escape(character) +
+      ", alt=" + alt +
+      ", ctrl=" + ctrl +
+      ", shift=" + shift +
+      '}';
+  }
+
+  private String escape(Character character) {
+    if (character == null) {
+      return "null";
+    }
+
+    switch (character) {
+      case '\n':
+        return "\\n";
+
+      case '\b':
+        return "\\b";
+
+      case '\t':
+        return "\\t";
+
+      default:
+        return character.toString();
+    }
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/Terminal.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/Terminal.java
new file mode 100644
index 0000000..8da71af
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/Terminal.java
@@ -0,0 +1,39 @@
+/**
+ * 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.hadoop.hbase.hbtop.terminal;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.io.Closeable;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+
+/**
+ * The terminal interface that is an abstraction of terminal screen.
+ */
+@InterfaceAudience.Private
+public interface Terminal extends Closeable {
+  void clear();
+  void refresh();
+  TerminalSize getSize();
+  @Nullable TerminalSize doResizeIfNecessary();
+  @Nullable KeyPress pollKeyPress();
+  CursorPosition getCursorPosition();
+  void setCursorPosition(int column, int row);
+  void hideCursor();
+  TerminalPrinter getTerminalPrinter(int startRow);
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/TerminalPrinter.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/TerminalPrinter.java
new file mode 100644
index 0000000..a28892a
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/TerminalPrinter.java
@@ -0,0 +1,54 @@
+/**
+ * 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.hadoop.hbase.hbtop.terminal;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+
+/**
+ * The interface responsible for printing to the terminal.
+ */
+@InterfaceAudience.Private
+public interface TerminalPrinter {
+  TerminalPrinter print(String value);
+
+  TerminalPrinter print(Object value);
+
+  TerminalPrinter print(char value);
+
+  TerminalPrinter print(short value);
+
+  TerminalPrinter print(int value);
+
+  TerminalPrinter print(long value);
+
+  TerminalPrinter print(float value);
+
+  TerminalPrinter print(double value);
+
+  TerminalPrinter printFormat(String format, Object... args);
+
+  TerminalPrinter startHighlight();
+
+  TerminalPrinter stopHighlight();
+
+  TerminalPrinter startBold();
+
+  TerminalPrinter stopBold();
+
+  void endOfLine();
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/TerminalSize.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/TerminalSize.java
new file mode 100644
index 0000000..44c5d82
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/TerminalSize.java
@@ -0,0 +1,61 @@
+/**
+ * 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.hadoop.hbase.hbtop.terminal;
+
+import java.util.Objects;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+
+/**
+ * Terminal dimensions in 2-d space, measured in number of rows and columns.
+ */
+@InterfaceAudience.Private
+public class TerminalSize {
+  private final int columns;
+  private final int rows;
+
+  public TerminalSize(int columns, int rows) {
+    this.columns = columns;
+    this.rows = rows;
+  }
+
+  public int getColumns() {
+    return columns;
+  }
+
+  public int getRows() {
+    return rows;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof TerminalSize)) {
+      return false;
+    }
+    TerminalSize that = (TerminalSize) o;
+    return columns == that.columns && rows == that.rows;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(columns, rows);
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/Cell.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/Cell.java
new file mode 100644
index 0000000..3100e09
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/Cell.java
@@ -0,0 +1,122 @@
+/**
+ * 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.hadoop.hbase.hbtop.terminal.impl;
+
+import java.util.Objects;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.terminal.Attributes;
+import org.apache.hadoop.hbase.hbtop.terminal.Color;
+
+/**
+ * Represents a single text cell of the terminal.
+ */
+@InterfaceAudience.Private
+public class Cell {
+  private static final char UNSET_VALUE = (char) 65535;
+  private static final char END_OF_LINE = '\0';
+
+  private final Attributes attributes;
+  private char ch;
+
+  public Cell() {
+    attributes = new Attributes();
+    ch = ' ';
+  }
+
+  public char getChar() {
+    return ch;
+  }
+
+  public void setChar(char ch) {
+    this.ch = ch;
+  }
+
+  public void reset() {
+    attributes.reset();
+    ch = ' ';
+  }
+
+  public void unset() {
+    attributes.reset();
+    ch = UNSET_VALUE;
+  }
+
+  public void endOfLine() {
+    attributes.reset();
+    ch = END_OF_LINE;
+  }
+
+  public boolean isEndOfLine() {
+    return ch == END_OF_LINE;
+  }
+
+  public void set(Cell cell) {
+    attributes.set(cell.attributes);
+    this.ch = cell.ch;
+  }
+
+  public Attributes getAttributes() {
+    return new Attributes(attributes);
+  }
+
+  public void setAttributes(Attributes attributes) {
+    this.attributes.set(attributes);
+  }
+
+  public boolean isBold() {
+    return attributes.isBold();
+  }
+
+  public boolean isBlink() {
+    return attributes.isBlink();
+  }
+
+  public boolean isReverse() {
+    return attributes.isReverse();
+  }
+
+  public boolean isUnderline() {
+    return attributes.isUnderline();
+  }
+
+  public Color getForegroundColor() {
+    return attributes.getForegroundColor();
+  }
+
+  public Color getBackgroundColor() {
+    return attributes.getBackgroundColor();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof Cell)) {
+      return false;
+    }
+    Cell cell = (Cell) o;
+    return ch == cell.ch && attributes.equals(cell.attributes);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(attributes, ch);
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/EscapeSequences.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/EscapeSequences.java
new file mode 100644
index 0000000..fe286e0
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/EscapeSequences.java
@@ -0,0 +1,140 @@
+/**
+ * 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.hadoop.hbase.hbtop.terminal.impl;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.terminal.Color;
+
+/**
+ * Utility class for escape sequences.
+ */
+@InterfaceAudience.Private
+public final class EscapeSequences {
+
+  private EscapeSequences() {
+  }
+
+  public static String clearAll() {
+    return "\033[0;37;40m\033[2J";
+  }
+
+  public static String setTitle(String title) {
+    return "\033]2;" + title + "\007";
+  }
+
+  public static String cursor(boolean on) {
+    if (on) {
+      return "\033[?25h";
+    }
+    return "\033[?25l";
+  }
+
+  public static String moveCursor(int column, int row) {
+    return String.format("\033[%d;%dH", row + 1, column + 1);
+  }
+
+  public static String clearRemainingLine() {
+    return "\033[0;37;40m\033[K";
+  }
+
+  public static String color(Color foregroundColor, Color backgroundColor, boolean bold,
+    boolean reverse, boolean blink, boolean underline) {
+
+    int foregroundColorValue = getColorValue(foregroundColor, true);
+    int backgroundColorValue = getColorValue(backgroundColor, false);
+
+    StringBuilder sb = new StringBuilder();
+    if (bold && reverse && blink && !underline) {
+      sb.append("\033[0;1;7;5;");
+    } else if (bold && reverse && !blink && !underline) {
+      sb.append("\033[0;1;7;");
+    } else if (!bold && reverse && blink && !underline) {
+      sb.append("\033[0;7;5;");
+    } else if (bold && !reverse && blink && !underline) {
+      sb.append("\033[0;1;5;");
+    } else if (bold && !reverse && !blink && !underline) {
+      sb.append("\033[0;1;");
+    } else if (!bold && reverse && !blink && !underline) {
+      sb.append("\033[0;7;");
+    } else if (!bold && !reverse && blink && !underline) {
+      sb.append("\033[0;5;");
+    } else if (bold && reverse && blink) {
+      sb.append("\033[0;1;7;5;4;");
+    } else if (bold && reverse) {
+      sb.append("\033[0;1;7;4;");
+    } else if (!bold && reverse && blink) {
+      sb.append("\033[0;7;5;4;");
+    } else if (bold && blink) {
+      sb.append("\033[0;1;5;4;");
+    } else if (bold) {
+      sb.append("\033[0;1;4;");
+    } else if (reverse) {
+      sb.append("\033[0;7;4;");
+    } else if (blink) {
+      sb.append("\033[0;5;4;");
+    } else if (underline) {
+      sb.append("\033[0;4;");
+    } else {
+      sb.append("\033[0;");
+    }
+    sb.append(String.format("%d;%dm", foregroundColorValue, backgroundColorValue));
+    return sb.toString();
+  }
+
+  private static int getColorValue(Color color, boolean foreground) {
+    int baseValue;
+    if (foreground) {
+      baseValue = 30;
+    } else { // background
+      baseValue = 40;
+    }
+
+    switch (color) {
+      case BLACK:
+        return baseValue;
+
+      case RED:
+        return baseValue + 1;
+
+      case GREEN:
+        return baseValue + 2;
+
+      case YELLOW:
+        return baseValue + 3;
+
+      case BLUE:
+        return baseValue + 4;
+
+      case MAGENTA:
+        return baseValue + 5;
+
+      case CYAN:
+        return baseValue + 6;
+
+      case WHITE:
+        return baseValue + 7;
+
+      default:
+        throw new AssertionError();
+    }
+  }
+
+  public static String normal() {
+    return "\033[0;37;40m";
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/KeyPressGenerator.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/KeyPressGenerator.java
new file mode 100644
index 0000000..13a5b28
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/KeyPressGenerator.java
@@ -0,0 +1,500 @@
+/**
+ * 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.hadoop.hbase.hbtop.terminal.impl;
+
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.util.Queue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.terminal.KeyPress;
+import org.apache.hadoop.hbase.util.Threads;
+
+/**
+ * This generates {@link KeyPress} objects from the given input stream and offers them to the
+ * given queue.
+ */
+@InterfaceAudience.Private
+public class KeyPressGenerator {
+
+  private static final Log LOG = LogFactory.getLog(KeyPressGenerator.class);
+
+  private enum ParseState {
+    START, ESCAPE, ESCAPE_SEQUENCE_PARAM1, ESCAPE_SEQUENCE_PARAM2
+  }
+
+  private final Queue<KeyPress> keyPressQueue;
+  private final BlockingQueue<Character> inputCharacterQueue = new LinkedBlockingQueue<>();
+  private final Reader input;
+  private final InputStream inputStream;
+  private final AtomicBoolean stopThreads = new AtomicBoolean();
+  private final ExecutorService executorService;
+
+  private ParseState parseState;
+  private int param1;
+  private int param2;
+
+  public KeyPressGenerator(InputStream inputStream, Queue<KeyPress> keyPressQueue) {
+    this.inputStream = inputStream;
+    input = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
+    this.keyPressQueue = keyPressQueue;
+
+    executorService = Executors.newFixedThreadPool(2, new ThreadFactoryBuilder()
+      .setNameFormat("KeyPressGenerator-%d").setDaemon(true)
+      .setUncaughtExceptionHandler(Threads.LOGGING_EXCEPTION_HANDLER).build());
+
+    initState();
+  }
+
+  public void start() {
+    executorService.execute(new Runnable() {
+      @Override
+      public void run() {
+        readerThread();
+      }
+    });
+    executorService.execute(new Runnable() {
+      @Override
+      public void run() {
+        generatorThread();
+      }
+    });
+  }
+
+  private void initState() {
+    parseState = ParseState.START;
+    param1 = 0;
+    param2 = 0;
+  }
+
+  private void readerThread() {
+    boolean done = false;
+    char[] readBuffer = new char[128];
+
+    while (!done && !stopThreads.get()) {
+      try {
+        int n = inputStream.available();
+        if (n > 0) {
+          if (readBuffer.length < n) {
+            readBuffer = new char[readBuffer.length * 2];
+          }
+
+          int rc = input.read(readBuffer, 0, readBuffer.length);
+          if (rc == -1) {
+            // EOF
+            done = true;
+          } else {
+            for (int i = 0; i < rc; i++) {
+              int ch = readBuffer[i];
+              inputCharacterQueue.offer((char) ch);
+            }
+          }
+        } else {
+          Thread.sleep(20);
+        }
+      } catch (InterruptedException ignored) {
+      } catch (IOException e) {
+        LOG.error("Caught an exception", e);
+        done = true;
+      }
+    }
+  }
+
+  private void generatorThread() {
+    while (!stopThreads.get()) {
+      Character ch;
+      try {
+        ch = inputCharacterQueue.poll(100, TimeUnit.MILLISECONDS);
+      } catch (InterruptedException ignored) {
+        continue;
+      }
+
+      if (ch == null) {
+        if (parseState == ParseState.ESCAPE) {
+          offer(new KeyPress(KeyPress.Type.Escape, null, false, false, false));
+          initState();
+        } else if (parseState != ParseState.START) {
+          offer(new KeyPress(KeyPress.Type.Unknown, null, false, false, false));
+          initState();
+        }
+        continue;
+      }
+
+      if (parseState == ParseState.START) {
+        if (ch == 0x1B) {
+          parseState = ParseState.ESCAPE;
+          continue;
+        }
+
+        switch (ch) {
+          case '\n':
+          case '\r':
+            offer(new KeyPress(KeyPress.Type.Enter, '\n', false, false, false));
+            continue;
+
+          case 0x08:
+          case 0x7F:
+            offer(new KeyPress(KeyPress.Type.Backspace, '\b', false, false, false));
+            continue;
+
+          case '\t':
+            offer(new KeyPress(KeyPress.Type.Tab, '\t', false, false, false));
+            continue;
+
+          default:
+            // Do nothing
+            break;
+        }
+
+        if (ch < 32) {
+          ctrlAndCharacter(ch);
+          continue;
+        }
+
+        if (isPrintableChar(ch)) {
+          // Normal character
+          offer(new KeyPress(KeyPress.Type.Character, ch, false, false, false));
+          continue;
+        }
+
+        offer(new KeyPress(KeyPress.Type.Unknown, null, false, false, false));
+        continue;
+      }
+
+      if (parseState == ParseState.ESCAPE) {
+        if (ch == 0x1B) {
+          offer(new KeyPress(KeyPress.Type.Escape, null, false, false, false));
+          continue;
+        }
+
+        if (ch < 32 && ch != 0x08) {
+          ctrlAltAndCharacter(ch);
+          initState();
+          continue;
+        } else if (ch == 0x7F || ch == 0x08) {
+          offer(new KeyPress(KeyPress.Type.Backspace, '\b', false, false, false));
+          initState();
+          continue;
+        }
+
+        if (ch == '[' || ch == 'O') {
+          parseState = ParseState.ESCAPE_SEQUENCE_PARAM1;
+          continue;
+        }
+
+        if (isPrintableChar(ch)) {
+          // Alt and character
+          offer(new KeyPress(KeyPress.Type.Character, ch, true, false, false));
+          initState();
+          continue;
+        }
+
+        offer(new KeyPress(KeyPress.Type.Escape, null, false, false, false));
+        offer(new KeyPress(KeyPress.Type.Unknown, null, false, false, false));
+        initState();
+        continue;
+      }
+
+      escapeSequenceCharacter(ch);
+    }
+  }
+
+  private void ctrlAndCharacter(char ch) {
+    char ctrlCode;
+    switch (ch) {
+      case 0:
+        ctrlCode = ' ';
+        break;
+
+      case 28:
+        ctrlCode = '\\';
+        break;
+
+      case 29:
+        ctrlCode = ']';
+        break;
+
+      case 30:
+        ctrlCode = '^';
+        break;
+
+      case 31:
+        ctrlCode = '_';
+        break;
+
+      default:
+        ctrlCode = (char) ('a' - 1 + ch);
+        break;
+    }
+    offer(new KeyPress(KeyPress.Type.Character, ctrlCode, false, true, false));
+  }
+
+  private boolean isPrintableChar(char ch) {
+    if (Character.isISOControl(ch)) {
+      return false;
+    }
+    Character.UnicodeBlock block = Character.UnicodeBlock.of(ch);
+    return block != null && !block.equals(Character.UnicodeBlock.SPECIALS);
+  }
+
+  private void ctrlAltAndCharacter(char ch) {
+    char ctrlCode;
+    switch (ch) {
+      case 0:
+        ctrlCode = ' ';
+        break;
+
+      case 28:
+        ctrlCode = '\\';
+        break;
+
+      case 29:
+        ctrlCode = ']';
+        break;
+
+      case 30:
+        ctrlCode = '^';
+        break;
+
+      case 31:
+        ctrlCode = '_';
+        break;
+
+      default:
+        ctrlCode = (char) ('a' - 1 + ch);
+        break;
+    }
+    offer(new KeyPress(KeyPress.Type.Character, ctrlCode, true, true, false));
+  }
+
+  private void escapeSequenceCharacter(char ch) {
+    switch (parseState) {
+      case ESCAPE_SEQUENCE_PARAM1:
+        if (ch == ';') {
+          parseState = ParseState.ESCAPE_SEQUENCE_PARAM2;
+        } else if (Character.isDigit(ch)) {
+          param1 = param1 * 10 + Character.digit(ch, 10);
+        } else {
+          doneEscapeSequenceCharacter(ch);
+        }
+        break;
+
+      case ESCAPE_SEQUENCE_PARAM2:
+        if (Character.isDigit(ch)) {
+          param2 = param2 * 10 + Character.digit(ch, 10);
+        } else {
+          doneEscapeSequenceCharacter(ch);
+        }
+        break;
+
+      default:
+        throw new AssertionError();
+    }
+  }
+
+  private void doneEscapeSequenceCharacter(char last) {
+    boolean alt = false;
+    boolean ctrl = false;
+    boolean shift = false;
+    if (param2 != 0) {
+      alt = isAlt(param2);
+      ctrl = isCtrl(param2);
+      shift = isShift(param2);
+    }
+
+    if (last != '~') {
+      switch (last) {
+        case 'A':
+          offer(new KeyPress(KeyPress.Type.ArrowUp, null, alt, ctrl, shift));
+          break;
+
+        case 'B':
+          offer(new KeyPress(KeyPress.Type.ArrowDown, null, alt, ctrl, shift));
+          break;
+
+        case 'C':
+          offer(new KeyPress(KeyPress.Type.ArrowRight, null, alt, ctrl, shift));
+          break;
+
+        case 'D':
+          offer(new KeyPress(KeyPress.Type.ArrowLeft, null, alt, ctrl, shift));
+          break;
+
+        case 'H':
+          offer(new KeyPress(KeyPress.Type.Home, null, alt, ctrl, shift));
+          break;
+
+        case 'F':
+          offer(new KeyPress(KeyPress.Type.End, null, alt, ctrl, shift));
+          break;
+
+        case 'P':
+          offer(new KeyPress(KeyPress.Type.F1, null, alt, ctrl, shift));
+          break;
+
+        case 'Q':
+          offer(new KeyPress(KeyPress.Type.F2, null, alt, ctrl, shift));
+          break;
+
+        case 'R':
+          offer(new KeyPress(KeyPress.Type.F3, null, alt, ctrl, shift));
+          break;
+
+        case 'S':
+          offer(new KeyPress(KeyPress.Type.F4, null, alt, ctrl, shift));
+          break;
+
+        case 'Z':
+          offer(new KeyPress(KeyPress.Type.ReverseTab, null, alt, ctrl, shift));
+          break;
+
+        default:
+          offer(new KeyPress(KeyPress.Type.Unknown, null, alt, ctrl, shift));
+          break;
+      }
+      initState();
+      return;
+    }
+
+    switch (param1) {
+      case 1:
+        offer(new KeyPress(KeyPress.Type.Home, null, alt, ctrl, shift));
+        break;
+
+      case 2:
+        offer(new KeyPress(KeyPress.Type.Insert, null, alt, ctrl, shift));
+        break;
+
+      case 3:
+        offer(new KeyPress(KeyPress.Type.Delete, null, alt, ctrl, shift));
+        break;
+
+      case 4:
+        offer(new KeyPress(KeyPress.Type.End, null, alt, ctrl, shift));
+        break;
+
+      case 5:
+        offer(new KeyPress(KeyPress.Type.PageUp, null, alt, ctrl, shift));
+        break;
+
+      case 6:
+        offer(new KeyPress(KeyPress.Type.PageDown, null, alt, ctrl, shift));
+        break;
+
+      case 11:
+        offer(new KeyPress(KeyPress.Type.F1, null, alt, ctrl, shift));
+        break;
+
+      case 12:
+        offer(new KeyPress(KeyPress.Type.F2, null, alt, ctrl, shift));
+        break;
+
+      case 13:
+        offer(new KeyPress(KeyPress.Type.F3, null, alt, ctrl, shift));
+        break;
+
+      case 14:
+        offer(new KeyPress(KeyPress.Type.F4, null, alt, ctrl, shift));
+        break;
+
+      case 15:
+        offer(new KeyPress(KeyPress.Type.F5, null, alt, ctrl, shift));
+        break;
+
+      case 17:
+        offer(new KeyPress(KeyPress.Type.F6, null, alt, ctrl, shift));
+        break;
+
+      case 18:
+        offer(new KeyPress(KeyPress.Type.F7, null, alt, ctrl, shift));
+        break;
+
+      case 19:
+        offer(new KeyPress(KeyPress.Type.F8, null, alt, ctrl, shift));
+        break;
+
+      case 20:
+        offer(new KeyPress(KeyPress.Type.F9, null, alt, ctrl, shift));
+        break;
+
+      case 21:
+        offer(new KeyPress(KeyPress.Type.F10, null, alt, ctrl, shift));
+        break;
+
+      case 23:
+        offer(new KeyPress(KeyPress.Type.F11, null, alt, ctrl, shift));
+        break;
+
+      case 24:
+        offer(new KeyPress(KeyPress.Type.F12, null, alt, ctrl, shift));
+        break;
+
+      default:
+        offer(new KeyPress(KeyPress.Type.Unknown, null, false, false, false));
+        break;
+    }
+
+    initState();
+  }
+
+  private boolean isShift(int param) {
+    return (param & 1) != 0;
+  }
+
+  private boolean isAlt(int param) {
+    return (param & 2) != 0;
+  }
+
+  private boolean isCtrl(int param) {
+    return (param & 4) != 0;
+  }
+
+  private void offer(KeyPress keyPress) {
+    // Handle ctrl + c
+    if (keyPress.isCtrl() && keyPress.getType() == KeyPress.Type.Character &&
+      keyPress.getCharacter() == 'c') {
+      System.exit(0);
+    }
+
+    keyPressQueue.offer(keyPress);
+  }
+
+  public void stop() {
+    stopThreads.set(true);
+
+    executorService.shutdown();
+    try {
+      while (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
+        LOG.warn("Waiting for thread-pool to terminate");
+      }
+    } catch (InterruptedException e) {
+      LOG.warn("Interrupted while waiting for thread-pool termination", e);
+    }
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/ScreenBuffer.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/ScreenBuffer.java
new file mode 100644
index 0000000..d710749
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/ScreenBuffer.java
@@ -0,0 +1,170 @@
+/**
+ * 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.hadoop.hbase.hbtop.terminal.impl;
+
+import static org.apache.hadoop.hbase.hbtop.terminal.impl.EscapeSequences.clearRemainingLine;
+import static org.apache.hadoop.hbase.hbtop.terminal.impl.EscapeSequences.color;
+import static org.apache.hadoop.hbase.hbtop.terminal.impl.EscapeSequences.cursor;
+import static org.apache.hadoop.hbase.hbtop.terminal.impl.EscapeSequences.moveCursor;
+import static org.apache.hadoop.hbase.hbtop.terminal.impl.EscapeSequences.normal;
+
+import java.io.PrintWriter;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.terminal.Attributes;
+import org.apache.hadoop.hbase.hbtop.terminal.CursorPosition;
+
+/**
+ * Represents a buffer of the terminal screen for double-buffering.
+ */
+@InterfaceAudience.Private
+public class ScreenBuffer {
+  private int columns;
+  private int rows;
+
+  private Cell[][] buffer;
+  private Cell[][] physical;
+
+  private boolean cursorVisible;
+  private int cursorColumn;
+  private int cursorRow;
+
+  public void reallocate(int columns, int rows) {
+    buffer = new Cell[columns][rows];
+    physical = new Cell[columns][rows];
+
+    for (int row = 0; row < rows; row++) {
+      for (int column = 0; column < columns; column++) {
+        buffer[column][row] = new Cell();
+
+        physical[column][row] = new Cell();
+        physical[column][row].unset();
+      }
+    }
+
+    this.columns = columns;
+    this.rows = rows;
+  }
+
+  public void clear() {
+    for (int row = 0; row < rows; row++) {
+      for (int col = 0; col < columns; col++) {
+        buffer[col][row].reset();
+      }
+    }
+  }
+
+  public void flush(PrintWriter output) {
+    StringBuilder sb = new StringBuilder();
+
+    sb.append(normal());
+    Attributes attributes = new Attributes();
+    for (int row = 0; row < rows; row++) {
+      flushRow(row, sb, attributes);
+    }
+
+    if (cursorVisible && cursorRow >= 0 && cursorColumn >= 0 && cursorRow < rows &&
+      cursorColumn < columns) {
+      sb.append(cursor(true));
+      sb.append(moveCursor(cursorColumn, cursorRow));
+    } else {
+      sb.append(cursor(false));
+    }
+
+    output.write(sb.toString());
+    output.flush();
+  }
+
+  private void flushRow(int row, StringBuilder sb, Attributes lastAttributes) {
+    int lastColumn = -1;
+    for (int column = 0; column < columns; column++) {
+      Cell cell = buffer[column][row];
+      Cell pCell = physical[column][row];
+
+      if (!cell.equals(pCell)) {
+        if (lastColumn != column - 1 || lastColumn == -1) {
+          sb.append(moveCursor(column, row));
+        }
+
+        if (cell.isEndOfLine()) {
+          for (int i = column; i < columns; i++) {
+            physical[i][row].set(buffer[i][row]);
+          }
+
+          sb.append(clearRemainingLine());
+          lastAttributes.reset();
+          return;
+        }
+
+        if (!cell.getAttributes().equals(lastAttributes)) {
+          sb.append(color(cell.getForegroundColor(), cell.getBackgroundColor(), cell.isBold(),
+            cell.isReverse(), cell.isBlink(), cell.isUnderline()));
+        }
+
+        sb.append(cell.getChar());
+
+        lastColumn = column;
+        lastAttributes.set(cell.getAttributes());
+
+        physical[column][row].set(cell);
+      }
+    }
+  }
+
+  public CursorPosition getCursorPosition() {
+    return new CursorPosition(cursorColumn, cursorRow);
+  }
+
+  public void setCursorPosition(int column, int row) {
+    cursorVisible = true;
+    cursorColumn = column;
+    cursorRow = row;
+  }
+
+  public void hideCursor() {
+    cursorVisible = false;
+  }
+
+  public void putString(int column, int row, String string, Attributes attributes) {
+    int i = column;
+    for (int j = 0; j < string.length(); j++) {
+      char ch = string.charAt(j);
+      putChar(i, row, ch, attributes);
+      i += 1;
+      if (i == columns) {
+        break;
+      }
+    }
+  }
+
+  public void putChar(int column, int row, char ch, Attributes attributes) {
+    if (column >= 0 && column < columns && row >= 0 && row < rows) {
+      buffer[column][row].setAttributes(attributes);
+      buffer[column][row].setChar(ch);
+    }
+  }
+
+  public void endOfLine(int column, int row) {
+    if (column >= 0 && column < columns && row >= 0 && row < rows) {
+      buffer[column][row].endOfLine();
+      for (int i = column + 1; i < columns; i++) {
+        buffer[i][row].reset();
+      }
+    }
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/TerminalImpl.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/TerminalImpl.java
new file mode 100644
index 0000000..0084e23
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/TerminalImpl.java
@@ -0,0 +1,229 @@
+/**
+ * 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.hadoop.hbase.hbtop.terminal.impl;
+
+import static org.apache.hadoop.hbase.hbtop.terminal.impl.EscapeSequences.clearAll;
+import static org.apache.hadoop.hbase.hbtop.terminal.impl.EscapeSequences.cursor;
+import static org.apache.hadoop.hbase.hbtop.terminal.impl.EscapeSequences.moveCursor;
+import static org.apache.hadoop.hbase.hbtop.terminal.impl.EscapeSequences.normal;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.Queue;
+import java.util.StringTokenizer;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.terminal.CursorPosition;
+import org.apache.hadoop.hbase.hbtop.terminal.KeyPress;
+import org.apache.hadoop.hbase.hbtop.terminal.Terminal;
+import org.apache.hadoop.hbase.hbtop.terminal.TerminalPrinter;
+import org.apache.hadoop.hbase.hbtop.terminal.TerminalSize;
+
+/**
+ * The implementation of the {@link Terminal} interface.
+ */
+@InterfaceAudience.Private
+public class TerminalImpl implements Terminal {
+
+  private static final Log LOG = LogFactory.getLog(TerminalImpl.class);
+
+  private TerminalSize cachedTerminalSize;
+
+  private final PrintWriter output;
+
+  private final ScreenBuffer screenBuffer;
+
+  private final Queue<KeyPress> keyPressQueue;
+  private final KeyPressGenerator keyPressGenerator;
+
+  public TerminalImpl() {
+    this(null);
+  }
+
+  public TerminalImpl(@Nullable String title) {
+    output = new PrintWriter(new OutputStreamWriter(System.out, StandardCharsets.UTF_8));
+    sttyRaw();
+
+    if (title != null) {
+      setTitle(title);
+    }
+
+    screenBuffer = new ScreenBuffer();
+
+    cachedTerminalSize = queryTerminalSize();
+    updateTerminalSize(cachedTerminalSize.getColumns(), cachedTerminalSize.getRows());
+
+    keyPressQueue = new ConcurrentLinkedQueue<>();
+    keyPressGenerator = new KeyPressGenerator(System.in, keyPressQueue);
+    keyPressGenerator.start();
+
+    Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
+      @Override
+      public void run() {
+        output.printf("%s%s%s%s", moveCursor(0, 0), cursor(true), normal(), clearAll());
+        output.flush();
+        sttyCooked();
+      }
+    }));
+
+    // Clear the terminal
+    output.write(clearAll());
+    output.flush();
+  }
+
+  private void setTitle(String title) {
+    output.write(EscapeSequences.setTitle(title));
+    output.flush();
+  }
+
+  private void updateTerminalSize(int columns, int rows) {
+    screenBuffer.reallocate(columns, rows);
+  }
+
+  @Override
+  public void clear() {
+    screenBuffer.clear();
+  }
+
+  @Override
+  public void refresh() {
+    screenBuffer.flush(output);
+  }
+
+  @Override
+  public TerminalSize getSize() {
+    return cachedTerminalSize;
+  }
+
+  @Nullable
+  @Override
+  public TerminalSize doResizeIfNecessary() {
+    TerminalSize currentTerminalSize = queryTerminalSize();
+    if (!currentTerminalSize.equals(cachedTerminalSize)) {
+      cachedTerminalSize = currentTerminalSize;
+      updateTerminalSize(cachedTerminalSize.getColumns(), cachedTerminalSize.getRows());
+      return cachedTerminalSize;
+    }
+    return null;
+  }
+
+  @Nullable
+  @Override
+  public KeyPress pollKeyPress() {
+    return keyPressQueue.poll();
+  }
+
+  @Override
+  public CursorPosition getCursorPosition() {
+    return screenBuffer.getCursorPosition();
+  }
+
+  @Override
+  public void setCursorPosition(int column, int row) {
+    screenBuffer.setCursorPosition(column, row);
+  }
+
+  @Override
+  public void hideCursor() {
+    screenBuffer.hideCursor();
+  }
+
+  @Override
+  public TerminalPrinter getTerminalPrinter(int startRow) {
+    return new TerminalPrinterImpl(screenBuffer, startRow);
+  }
+
+  @Override
+  public void close() {
+    keyPressGenerator.stop();
+  }
+
+  private TerminalSize queryTerminalSize() {
+    String sizeString = doStty("size");
+
+    int rows = 0;
+    int columns = 0;
+
+    StringTokenizer tokenizer = new StringTokenizer(sizeString);
+    int rc = Integer.parseInt(tokenizer.nextToken());
+    if (rc > 0) {
+      rows = rc;
+    }
+
+    rc = Integer.parseInt(tokenizer.nextToken());
+    if (rc > 0) {
+      columns = rc;
+    }
+    return new TerminalSize(columns, rows);
+  }
+
+  private void sttyRaw() {
+    doStty("-ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost " +
+      "-echo -echonl -icanon -isig -iexten -parenb cs8 min 1");
+  }
+
+  private void sttyCooked() {
+    doStty("sane cooked");
+  }
+
+  private String doStty(String sttyOptionsString) {
+    String [] cmd = {"/bin/sh", "-c", "stty " + sttyOptionsString + " < /dev/tty"};
+
+    try {
+      Process process = Runtime.getRuntime().exec(cmd);
+
+      String ret;
+
+      // stdout
+      try (BufferedReader stdout = new BufferedReader(new InputStreamReader(
+        process.getInputStream(), StandardCharsets.UTF_8))) {
+        ret = stdout.readLine();
+      }
+
+      // stderr
+      try (BufferedReader stderr = new BufferedReader(new InputStreamReader(
+        process.getErrorStream(), StandardCharsets.UTF_8))) {
+        String line = stderr.readLine();
+        if ((line != null) && (line.length() > 0)) {
+          LOG.error("Error output from stty: " + line);
+        }
+      }
+
+      try {
+        process.waitFor();
+      } catch (InterruptedException ignored) {
+      }
+
+      int exitValue = process.exitValue();
+      if (exitValue != 0) {
+        LOG.error("stty returned error code: " + exitValue);
+      }
+      return ret;
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/TerminalPrinterImpl.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/TerminalPrinterImpl.java
new file mode 100644
index 0000000..0d698b0
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/TerminalPrinterImpl.java
@@ -0,0 +1,83 @@
+/**
+ * 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.hadoop.hbase.hbtop.terminal.impl;
+
+import java.util.Objects;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.terminal.AbstractTerminalPrinter;
+import org.apache.hadoop.hbase.hbtop.terminal.Attributes;
+import org.apache.hadoop.hbase.hbtop.terminal.Color;
+import org.apache.hadoop.hbase.hbtop.terminal.TerminalPrinter;
+
+/**
+ * The implementation of the {@link TerminalPrinter} interface.
+ */
+@InterfaceAudience.Private
+public class TerminalPrinterImpl extends AbstractTerminalPrinter {
+  private final ScreenBuffer screenBuffer;
+  private int row;
+  private int column;
+
+  private final Attributes attributes = new Attributes();
+
+  TerminalPrinterImpl(ScreenBuffer screenBuffer, int startRow) {
+    this.screenBuffer = Objects.requireNonNull(screenBuffer);
+    this.row = startRow;
+  }
+
+  @Override
+  public TerminalPrinter print(String value) {
+    screenBuffer.putString(column, row, value, attributes);
+    column += value.length();
+    return this;
+  }
+
+  @Override
+  public TerminalPrinter startHighlight() {
+    attributes.setForegroundColor(Color.BLACK);
+    attributes.setBackgroundColor(Color.WHITE);
+    return this;
+  }
+
+  @Override
+  public TerminalPrinter stopHighlight() {
+    attributes.setForegroundColor(Color.WHITE);
+    attributes.setBackgroundColor(Color.BLACK);
+    return this;
+  }
+
+  @Override
+  public TerminalPrinter startBold() {
+    attributes.setBold(true);
+    return this;
+  }
+
+  @Override
+  public TerminalPrinter stopBold() {
+    attributes.setBold(false);
+    return this;
+  }
+
+  @Override
+  public void endOfLine() {
+    screenBuffer.endOfLine(column, row);
+    row += 1;
+    column = 0;
+  }
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/TestRecord.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/TestRecord.java
new file mode 100644
index 0000000..eca1d04
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/TestRecord.java
@@ -0,0 +1,87 @@
+/**
+ * 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.hadoop.hbase.hbtop;
+
+import static org.apache.hadoop.hbase.hbtop.Record.entry;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+@Category(SmallTests.class)
+public class TestRecord {
+
+  @Test
+  public void testBuilder() {
+    Record actual1 = Record.builder().put(Field.TABLE, "tableName")
+      .put(entry(Field.REGION_COUNT, 3))
+      .put(Field.REQUEST_COUNT_PER_SECOND, Field.REQUEST_COUNT_PER_SECOND.newValue(100L))
+      .build();
+
+    assertThat(actual1.size(), is(3));
+    assertThat(actual1.get(Field.TABLE).asString(), is("tableName"));
+    assertThat(actual1.get(Field.REGION_COUNT).asInt(), is(3));
+    assertThat(actual1.get(Field.REQUEST_COUNT_PER_SECOND).asLong(), is(100L));
+
+    Record actual2 = Record.builder().putAll(actual1).build();
+
+    assertThat(actual2.size(), is(3));
+    assertThat(actual2.get(Field.TABLE).asString(), is("tableName"));
+    assertThat(actual2.get(Field.REGION_COUNT).asInt(), is(3));
+    assertThat(actual2.get(Field.REQUEST_COUNT_PER_SECOND).asLong(), is(100L));
+  }
+
+  @Test
+  public void testOfEntries() {
+    Record actual = Record.ofEntries(
+      entry(Field.TABLE, "tableName"),
+      entry(Field.REGION_COUNT, 3),
+      entry(Field.REQUEST_COUNT_PER_SECOND, 100L)
+    );
+
+    assertThat(actual.size(), is(3));
+    assertThat(actual.get(Field.TABLE).asString(), is("tableName"));
+    assertThat(actual.get(Field.REGION_COUNT).asInt(), is(3));
+    assertThat(actual.get(Field.REQUEST_COUNT_PER_SECOND).asLong(), is(100L));
+  }
+
+  @Test
+  public void testCombine() {
+    Record record1 = Record.ofEntries(
+      entry(Field.TABLE, "tableName"),
+      entry(Field.REGION_COUNT, 3),
+      entry(Field.REQUEST_COUNT_PER_SECOND, 100L)
+    );
+
+    Record record2 = Record.ofEntries(
+      entry(Field.TABLE, "tableName"),
+      entry(Field.REGION_COUNT, 5),
+      entry(Field.REQUEST_COUNT_PER_SECOND, 500L)
+    );
+
+    Record actual = record1.combine(record2);
+
+    assertThat(actual.size(), is(3));
+    assertThat(actual.get(Field.TABLE).asString(), is("tableName"));
+    assertThat(actual.get(Field.REGION_COUNT).asInt(), is(8));
+    assertThat(actual.get(Field.REQUEST_COUNT_PER_SECOND).asLong(), is(600L));
+  }
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/TestRecordFilter.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/TestRecordFilter.java
new file mode 100644
index 0000000..27c6b9e
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/TestRecordFilter.java
@@ -0,0 +1,209 @@
+/**
+ * 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.hadoop.hbase.hbtop;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.junit.Assert.assertThat;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.hbtop.field.Size;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+@Category(SmallTests.class)
+public class TestRecordFilter {
+
+  @Test
+  public void testParseAndBuilder() {
+    testParseAndBuilder("REGION=region1", false,
+      RecordFilter.newBuilder(Field.REGION).equal("region1"));
+
+    testParseAndBuilder("REGION=", false,
+      RecordFilter.newBuilder(Field.REGION).equal(""));
+
+    testParseAndBuilder("!REGION=region1", false,
+      RecordFilter.newBuilder(Field.REGION).notEqual("region1"));
+
+    testParseAndBuilder("REGION==region2", true,
+      RecordFilter.newBuilder(Field.REGION, true).doubleEquals("region2"));
+
+    testParseAndBuilder("!REGION==region2", true,
+      RecordFilter.newBuilder(Field.REGION, true).notDoubleEquals("region2"));
+
+    testParseAndBuilder("#REQ/S>100", false,
+      RecordFilter.newBuilder(Field.REQUEST_COUNT_PER_SECOND).greater(100L));
+
+    testParseAndBuilder("!#REQ/S>100", false,
+      RecordFilter.newBuilder(Field.REQUEST_COUNT_PER_SECOND).notGreater(100L));
+
+    testParseAndBuilder("SF>=50MB", true,
+      RecordFilter.newBuilder(Field.STORE_FILE_SIZE, true).greaterOrEqual("50MB"));
+
+    testParseAndBuilder("!SF>=50MB", true,
+      RecordFilter.newBuilder(Field.STORE_FILE_SIZE, true).notGreaterOrEqual("50MB"));
+
+    testParseAndBuilder("#REQ/S<20", false,
+      RecordFilter.newBuilder(Field.REQUEST_COUNT_PER_SECOND).less(20L));
+
+    testParseAndBuilder("!#REQ/S<20", false,
+      RecordFilter.newBuilder(Field.REQUEST_COUNT_PER_SECOND).notLess(20L));
+
+    testParseAndBuilder("%COMP<=50%", true,
+      RecordFilter.newBuilder(Field.COMPACTION_PROGRESS, true).lessOrEqual("50%"));
+
+    testParseAndBuilder("!%COMP<=50%", true,
+      RecordFilter.newBuilder(Field.COMPACTION_PROGRESS, true).notLessOrEqual("50%"));
+  }
+
+  private void testParseAndBuilder(String filterString, boolean ignoreCase, RecordFilter expected) {
+    RecordFilter actual = RecordFilter.parse(filterString, ignoreCase);
+    assertThat(expected, is(actual));
+  }
+
+  @Test
+  public void testParseFailure() {
+    RecordFilter filter = RecordFilter.parse("REGIO=region1", false);
+    assertThat(filter, is(nullValue()));
+
+    filter = RecordFilter.parse("", false);
+    assertThat(filter, is(nullValue()));
+
+    filter = RecordFilter.parse("#REQ/S==aaa", false);
+    assertThat(filter, is(nullValue()));
+
+    filter = RecordFilter.parse("SF>=50", false);
+    assertThat(filter, is(nullValue()));
+  }
+
+  @Test
+  public void testToString() {
+    testToString("REGION=region1");
+    testToString("!REGION=region1");
+    testToString("REGION==region2");
+    testToString("!REGION==region2");
+    testToString("#REQ/S>100");
+    testToString("!#REQ/S>100");
+    testToString("SF>=50.0MB");
+    testToString("!SF>=50.0MB");
+    testToString("#REQ/S<20");
+    testToString("!#REQ/S<20");
+    testToString("%COMP<=50.00%");
+    testToString("!%COMP<=50.00%");
+  }
+
+  private void testToString(String filterString) {
+    RecordFilter filter = RecordFilter.parse(filterString, false);
+    assertThat(filter, is(notNullValue()));
+    assertThat(filterString, is(filter.toString()));
+  }
+
+  @Test
+  public void testFilters() {
+    List<Record> records = createTestRecords();
+
+    testFilter(records, "REGION=region", false,
+      "region1", "region2", "region3", "region4", "region5");
+    testFilter(records, "!REGION=region", false);
+    testFilter(records, "REGION=Region", false);
+
+    testFilter(records, "REGION==region", false);
+    testFilter(records, "REGION==region1", false, "region1");
+    testFilter(records, "!REGION==region1", false, "region2", "region3", "region4", "region5");
+
+    testFilter(records, "#REQ/S==100", false, "region1");
+    testFilter(records, "#REQ/S>100", false, "region2", "region5");
+    testFilter(records, "SF>=100MB", false, "region1", "region2", "region4", "region5");
+    testFilter(records, "!#SF>=10", false, "region1", "region4");
+    testFilter(records, "LOCALITY<0.5", false, "region5");
+    testFilter(records, "%COMP<=50%", false, "region2", "region3", "region4", "region5");
+
+    testFilters(records, Arrays.asList("SF>=100MB", "#REQ/S>100"), false,
+      "region2", "region5");
+    testFilters(records, Arrays.asList("%COMP<=50%", "!#SF>=10"), false, "region4");
+    testFilters(records, Arrays.asList("!REGION==region1", "LOCALITY<0.5", "#REQ/S>100"), false,
+      "region5");
+  }
+
+  @Test
+  public void testFiltersIgnoreCase() {
+    List<Record> records = createTestRecords();
+
+    testFilter(records, "REGION=Region", true,
+      "region1", "region2", "region3", "region4", "region5");
+    testFilter(records, "REGION=REGION", true,
+      "region1", "region2", "region3", "region4", "region5");
+  }
+
+  private List<Record> createTestRecords() {
+    List<Record> ret = new ArrayList<>();
+    ret.add(createTestRecord("region1", 100L, new Size(100, Size.Unit.MEGABYTE), 2, 1.0f, 80f));
+    ret.add(createTestRecord("region2", 120L, new Size(100, Size.Unit.GIGABYTE), 10, 0.5f, 20f));
+    ret.add(createTestRecord("region3", 50L, new Size(500, Size.Unit.KILOBYTE), 15, 0.8f, 50f));
+    ret.add(createTestRecord("region4", 90L, new Size(10, Size.Unit.TERABYTE), 5, 0.9f, 30f));
+    ret.add(createTestRecord("region5", 200L, new Size(1, Size.Unit.PETABYTE), 13, 0.1f, 40f));
+    return ret;
+  }
+
+  private Record createTestRecord(String region, long requestCountPerSecond,
+    Size storeFileSize, int numStoreFiles, float locality, float compactionProgress) {
+    Record.Builder builder = Record.builder();
+    builder.put(Field.REGION, region);
+    builder.put(Field.REQUEST_COUNT_PER_SECOND, requestCountPerSecond);
+    builder.put(Field.STORE_FILE_SIZE, storeFileSize);
+    builder.put(Field.NUM_STORE_FILES, numStoreFiles);
+    builder.put(Field.LOCALITY, locality);
+    builder.put(Field.COMPACTION_PROGRESS, compactionProgress);
+    return builder.build();
+  }
+
+  private void testFilter(List<Record> records, String filterString, boolean ignoreCase,
+    String... expectedRegions) {
+    testFilters(records, Collections.singletonList(filterString), ignoreCase, expectedRegions);
+  }
+
+  private void testFilters(List<Record> records, List<String> filterStrings, boolean ignoreCase,
+    String... expectedRegions) {
+
+    List<String> actual = new ArrayList<>();
+    for (Record record : records) {
+      boolean filter = false;
+      for (String filterString : filterStrings) {
+        if (!RecordFilter.parse(filterString, ignoreCase).execute(record)) {
+          filter = true;
+        }
+      }
+      if (!filter) {
+        actual.add(record.get(Field.REGION).asString());
+      }
+    }
+
+    assertThat(actual.size(), is(expectedRegions.length));
+    for (int i = 0; i < actual.size(); i++) {
+      String actualRegion = actual.get(i);
+      String expectedRegion = expectedRegions[i];
+      assertThat(actualRegion, is(expectedRegion));
+    }
+  }
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/TestUtils.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/TestUtils.java
new file mode 100644
index 0000000..3e7fbbc
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/TestUtils.java
@@ -0,0 +1,373 @@
+/**
+ * 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.hadoop.hbase.hbtop;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.protobuf.ByteString;
+import java.text.ParseException;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.commons.lang3.time.FastDateFormat;
+import org.apache.hadoop.hbase.ClusterStatus;
+import org.apache.hadoop.hbase.HRegionInfo;
+import org.apache.hadoop.hbase.ServerLoad;
+import org.apache.hadoop.hbase.ServerName;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.hbtop.field.Size;
+import org.apache.hadoop.hbase.hbtop.field.Size.Unit;
+import org.apache.hadoop.hbase.hbtop.screen.top.Summary;
+import org.apache.hadoop.hbase.master.RegionState;
+import org.apache.hadoop.hbase.protobuf.generated.ClusterStatusProtos;
+import org.apache.hadoop.hbase.protobuf.generated.HBaseProtos;
+
+public final class TestUtils {
+
+  static final String HBASE_VERSION = "1.5.0-SNAPSHOT";
+  static final String CLUSTER_UUID = "01234567-89ab-cdef-0123-456789abcdef";
+
+  private TestUtils() { }
+
+  public static ClusterStatus createDummyClusterStatus() {
+    Map<ServerName, ServerLoad> serverLoads = Maps.newHashMap();
+    List<ServerName> deadServers = Lists.newArrayList();
+    Set<RegionState> rit = Sets.newHashSet();
+
+    ServerName host1 = ServerName.valueOf("host1.apache.com", 1000, 1);
+
+    serverLoads.put(host1,
+      createServerLoad(100,
+        new Size(100, Size.Unit.MEGABYTE), new Size(200, Size.Unit.MEGABYTE), 100,
+        Lists.newArrayList(
+          createRegionLoad("table1,,1.00000000000000000000000000000000.", 100, 100,
+            new Size(100, Size.Unit.MEGABYTE), new Size(200, Size.Unit.MEGABYTE), 1,
+            new Size(100, Size.Unit.MEGABYTE), 0.1f, 100, 100, "2019-07-22 00:00:00"),
+          createRegionLoad("table2,1,2.00000000000000000000000000000001.", 200, 200,
+            new Size(200, Size.Unit.MEGABYTE), new Size(400, Size.Unit.MEGABYTE), 2,
+            new Size(200, Size.Unit.MEGABYTE), 0.2f, 50, 200, "2019-07-22 00:00:01"),
+          createRegionLoad(
+            "namespace:table3,,3_0001.00000000000000000000000000000002.", 300, 300,
+            new Size(300, Size.Unit.MEGABYTE), new Size(600, Size.Unit.MEGABYTE), 3,
+            new Size(300, Size.Unit.MEGABYTE), 0.3f, 100, 300, "2019-07-22 00:00:02"))));
+
+    ServerName host2 = ServerName.valueOf("host2.apache.com", 1001, 2);
+
+    serverLoads.put(host2,
+      createServerLoad(200,
+        new Size(16, Size.Unit.GIGABYTE), new Size(32, Size.Unit.GIGABYTE), 200,
+        Lists.newArrayList(
+          createRegionLoad("table1,1,4.00000000000000000000000000000003.", 100, 100,
+            new Size(100, Size.Unit.MEGABYTE), new Size(200, Size.Unit.MEGABYTE), 1,
+            new Size(100, Size.Unit.MEGABYTE), 0.4f, 50, 100, "2019-07-22 00:00:03"),
+          createRegionLoad("table2,,5.00000000000000000000000000000004.", 200, 200,
+            new Size(200, Size.Unit.MEGABYTE), new Size(400, Size.Unit.MEGABYTE), 2,
+            new Size(200, Size.Unit.MEGABYTE), 0.5f, 150, 200, "2019-07-22 00:00:04"),
+          createRegionLoad("namespace:table3,,6.00000000000000000000000000000005.", 300, 300,
+            new Size(300, Size.Unit.MEGABYTE), new Size(600, Size.Unit.MEGABYTE), 3,
+            new Size(300, Size.Unit.MEGABYTE), 0.6f, 200, 300, "2019-07-22 00:00:05"))));
+
+    ServerName host3 = ServerName.valueOf("host3.apache.com", 1002, 3);
+
+    deadServers.add(host3);
+
+    rit.add(new RegionState(new HRegionInfo(0, TableName.valueOf("table4"), 0),
+      RegionState.State.OFFLINE, host3));
+
+    return new ClusterStatus(HBASE_VERSION, CLUSTER_UUID, serverLoads, deadServers, null, null,
+      rit, new String[0], true);
+  }
+
+  private static ClusterStatusProtos.RegionLoad createRegionLoad(String regionName,
+    long readRequestCount, long writeRequestCount, Size storeFileSize,
+    Size uncompressedStoreFileSize, int storeFileCount, Size memStoreSize, float locality,
+    long compactedCellCount, long compactingCellCount, String lastMajorCompactionTime) {
+    FastDateFormat df = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss");
+    try {
+      return ClusterStatusProtos.RegionLoad.newBuilder()
+        .setRegionSpecifier(HBaseProtos.RegionSpecifier.newBuilder()
+          .setType(HBaseProtos.RegionSpecifier.RegionSpecifierType.REGION_NAME)
+          .setValue(ByteString.copyFromUtf8(regionName)).build())
+        .setReadRequestsCount(readRequestCount)
+        .setWriteRequestsCount(writeRequestCount)
+        .setStorefileSizeMB((int)storeFileSize.get(Unit.MEGABYTE))
+        .setStoreUncompressedSizeMB((int)uncompressedStoreFileSize.get(Unit.MEGABYTE))
+        .setStorefiles(storeFileCount)
+        .setMemstoreSizeMB((int)memStoreSize.get(Unit.MEGABYTE))
+        .setDataLocality(locality)
+        .setCurrentCompactedKVs(compactedCellCount)
+        .setTotalCompactingKVs(compactingCellCount)
+        .setLastMajorCompactionTs(df.parse(lastMajorCompactionTime).getTime())
+        .build();
+    } catch (ParseException e) {
+      throw new IllegalArgumentException(e);
+    }
+  }
+
+  private static ServerLoad createServerLoad(long reportTimestamp,
+    Size usedHeapSize, Size maxHeapSize, long requestCountPerSecond,
+    List<ClusterStatusProtos.RegionLoad> regionLoads) {
+    return new ServerLoad(ClusterStatusProtos.ServerLoad.newBuilder()
+        .setReportStartTime(reportTimestamp)
+        .setReportEndTime(reportTimestamp)
+        .setUsedHeapMB((int)usedHeapSize.get(Unit.MEGABYTE))
+        .setMaxHeapMB((int)maxHeapSize.get(Unit.MEGABYTE))
+        .setNumberOfRequests(requestCountPerSecond)
+        .addAllRegionLoads(regionLoads)
+        .build());
+  }
+
+  public static void assertRecordsInRegionMode(List<Record> records) {
+    assertEquals(6, records.size());
+
+    for (Record record : records) {
+      switch (record.get(Field.REGION_NAME).asString()) {
+        case "table1,,1.00000000000000000000000000000000.":
+          assertRecordInRegionMode(record, "default", "1", "", "table1",
+            "00000000000000000000000000000000", "host1:1000", "host1.apache.com,1000,1",
+            0L, 0L, 0L, new Size(100, Size.Unit.MEGABYTE), new Size(200, Size.Unit.MEGABYTE), 1,
+            new Size(100, Size.Unit.MEGABYTE), 0.1f, "", 100L, 100L, 100f,
+            "2019-07-22 00:00:00");
+          break;
+
+        case "table1,1,4.00000000000000000000000000000003.":
+          assertRecordInRegionMode(record, "default", "4", "", "table1",
+            "00000000000000000000000000000003", "host2:1001", "host2.apache.com,1001,2",
+            0L, 0L, 0L, new Size(100, Size.Unit.MEGABYTE), new Size(200, Size.Unit.MEGABYTE), 1,
+            new Size(100, Size.Unit.MEGABYTE), 0.4f, "1", 100L, 50L, 50f,
+            "2019-07-22 00:00:03");
+          break;
+
+        case "table2,,5.00000000000000000000000000000004.":
+          assertRecordInRegionMode(record, "default", "5", "", "table2",
+            "00000000000000000000000000000004", "host2:1001", "host2.apache.com,1001,2",
+            0L, 0L, 0L, new Size(200, Size.Unit.MEGABYTE), new Size(400, Size.Unit.MEGABYTE), 2,
+            new Size(200, Size.Unit.MEGABYTE), 0.5f, "", 200L, 150L, 75f,
+            "2019-07-22 00:00:04");
+          break;
+
+        case "table2,1,2.00000000000000000000000000000001.":
+          assertRecordInRegionMode(record, "default", "2", "", "table2",
+            "00000000000000000000000000000001", "host1:1000", "host1.apache.com,1000,1",
+            0L, 0L, 0L, new Size(200, Size.Unit.MEGABYTE), new Size(400, Size.Unit.MEGABYTE), 2,
+            new Size(200, Size.Unit.MEGABYTE), 0.2f, "1", 200L, 50L, 25f,
+            "2019-07-22 00:00:01");
+          break;
+
+        case "namespace:table3,,6.00000000000000000000000000000005.":
+          assertRecordInRegionMode(record, "namespace", "6", "", "table3",
+            "00000000000000000000000000000005", "host2:1001", "host2.apache.com,1001,2",
+            0L, 0L, 0L, new Size(300, Size.Unit.MEGABYTE), new Size(600, Size.Unit.MEGABYTE), 3,
+            new Size(300, Size.Unit.MEGABYTE), 0.6f, "", 300L, 200L, 66.66667f,
+            "2019-07-22 00:00:05");
+          break;
+
+        case "namespace:table3,,3_0001.00000000000000000000000000000002.":
+          assertRecordInRegionMode(record, "namespace", "3", "1", "table3",
+            "00000000000000000000000000000002", "host1:1000", "host1.apache.com,1000,1",
+            0L, 0L, 0L, new Size(300, Size.Unit.MEGABYTE), new Size(600, Size.Unit.MEGABYTE), 3,
+            new Size(300, Size.Unit.MEGABYTE), 0.3f, "", 300L, 100L, 33.333336f,
+            "2019-07-22 00:00:02");
+          break;
+
+        default:
+          fail();
+      }
+    }
+  }
+
+  private static void assertRecordInRegionMode(Record record, String namespace, String startCode,
+    String replicaId, String table, String region, String regionServer, String longRegionServer,
+    long requestCountPerSecond, long readRequestCountPerSecond, long writeCountRequestPerSecond,
+    Size storeFileSize, Size uncompressedStoreFileSize, int numStoreFiles,
+    Size memStoreSize, float locality, String startKey, long compactingCellCount,
+    long compactedCellCount, float compactionProgress, String lastMajorCompactionTime) {
+    assertEquals(21, record.size());
+    assertEquals(namespace, record.get(Field.NAMESPACE).asString());
+    assertEquals(startCode, record.get(Field.START_CODE).asString());
+    assertEquals(replicaId, record.get(Field.REPLICA_ID).asString());
+    assertEquals(table, record.get(Field.TABLE).asString());
+    assertEquals(region, record.get(Field.REGION).asString());
+    assertEquals(regionServer, record.get(Field.REGION_SERVER).asString());
+    assertEquals(longRegionServer, record.get(Field.LONG_REGION_SERVER).asString());
+    assertEquals(requestCountPerSecond, record.get(Field.REQUEST_COUNT_PER_SECOND).asLong());
+    assertEquals(readRequestCountPerSecond,
+      record.get(Field.READ_REQUEST_COUNT_PER_SECOND).asLong());
+    assertEquals(writeCountRequestPerSecond,
+      record.get(Field.WRITE_REQUEST_COUNT_PER_SECOND).asLong());
+    assertEquals(storeFileSize, record.get(Field.STORE_FILE_SIZE).asSize());
+    assertEquals(uncompressedStoreFileSize,
+      record.get(Field.UNCOMPRESSED_STORE_FILE_SIZE).asSize());
+    assertEquals(numStoreFiles, record.get(Field.NUM_STORE_FILES).asInt());
+    assertEquals(record.get(Field.MEM_STORE_SIZE).asSize(), memStoreSize);
+    assertEquals(locality, record.get(Field.LOCALITY).asFloat(), 0.001);
+    assertEquals(startKey, record.get(Field.START_KEY).asString());
+    assertEquals(compactingCellCount, record.get(Field.COMPACTING_CELL_COUNT).asLong());
+    assertEquals(compactedCellCount, record.get(Field.COMPACTED_CELL_COUNT).asLong());
+    assertEquals(compactionProgress, record.get(Field.COMPACTION_PROGRESS).asFloat(), 0.001);
+    assertEquals(lastMajorCompactionTime,
+      record.get(Field.LAST_MAJOR_COMPACTION_TIME).asString());
+  }
+
+  public static void assertRecordsInNamespaceMode(List<Record> records) {
+    assertEquals(2, records.size());
+
+    for (Record record : records) {
+      switch (record.get(Field.NAMESPACE).asString()) {
+        case "default":
+          assertRecordInNamespaceMode(record, 0L, 0L, 0L, new Size(600, Size.Unit.MEGABYTE),
+            new Size(1200, Size.Unit.MEGABYTE), 6, new Size(600, Size.Unit.MEGABYTE), 4);
+          break;
+
+        case "namespace":
+          assertRecordInNamespaceMode(record, 0L, 0L, 0L, new Size(600, Size.Unit.MEGABYTE),
+            new Size(1200, Size.Unit.MEGABYTE), 6, new Size(600, Size.Unit.MEGABYTE), 2);
+          break;
+
+        default:
+          fail();
+      }
+    }
+  }
+
+  private static void assertRecordInNamespaceMode(Record record, long requestCountPerSecond,
+    long readRequestCountPerSecond, long writeCountRequestPerSecond, Size storeFileSize,
+    Size uncompressedStoreFileSize, int numStoreFiles, Size memStoreSize, int regionCount) {
+    assertEquals(9, record.size());
+    assertEquals(requestCountPerSecond, record.get(Field.REQUEST_COUNT_PER_SECOND).asLong());
+    assertEquals(readRequestCountPerSecond,
+      record.get(Field.READ_REQUEST_COUNT_PER_SECOND).asLong());
+    assertEquals(writeCountRequestPerSecond,
+      record.get(Field.WRITE_REQUEST_COUNT_PER_SECOND).asLong());
+    assertEquals(storeFileSize, record.get(Field.STORE_FILE_SIZE).asSize());
+    assertEquals(uncompressedStoreFileSize,
+      record.get(Field.UNCOMPRESSED_STORE_FILE_SIZE).asSize());
+    assertEquals(numStoreFiles, record.get(Field.NUM_STORE_FILES).asInt());
+    assertEquals(memStoreSize, record.get(Field.MEM_STORE_SIZE).asSize());
+    assertEquals(regionCount, record.get(Field.REGION_COUNT).asInt());
+  }
+
+  public static void assertRecordsInTableMode(List<Record> records) {
+    assertEquals(3, records.size());
+
+    for (Record record : records) {
+      String tableName = String.format("%s:%s", record.get(Field.NAMESPACE).asString(),
+        record.get(Field.TABLE).asString());
+
+      switch (tableName) {
+        case "default:table1":
+          assertRecordInTableMode(record, 0L, 0L, 0L, new Size(200, Size.Unit.MEGABYTE),
+            new Size(400, Size.Unit.MEGABYTE), 2, new Size(200, Size.Unit.MEGABYTE), 2);
+          break;
+
+        case "default:table2":
+          assertRecordInTableMode(record, 0L, 0L, 0L, new Size(400, Size.Unit.MEGABYTE),
+            new Size(800, Size.Unit.MEGABYTE), 4, new Size(400, Size.Unit.MEGABYTE), 2);
+          break;
+
+        case "namespace:table3":
+          assertRecordInTableMode(record, 0L, 0L, 0L, new Size(600, Size.Unit.MEGABYTE),
+            new Size(1200, Size.Unit.MEGABYTE), 6, new Size(600, Size.Unit.MEGABYTE), 2);
+          break;
+
+        default:
+          fail();
+      }
+    }
+  }
+
+  private static void assertRecordInTableMode(Record record, long requestCountPerSecond,
+    long readRequestCountPerSecond,  long writeCountRequestPerSecond, Size storeFileSize,
+    Size uncompressedStoreFileSize, int numStoreFiles, Size memStoreSize, int regionCount) {
+    assertEquals(10, record.size());
+    assertEquals(requestCountPerSecond, record.get(Field.REQUEST_COUNT_PER_SECOND).asLong());
+    assertEquals(readRequestCountPerSecond,
+      record.get(Field.READ_REQUEST_COUNT_PER_SECOND).asLong());
+    assertEquals(writeCountRequestPerSecond,
+      record.get(Field.WRITE_REQUEST_COUNT_PER_SECOND).asLong());
+    assertEquals(storeFileSize, record.get(Field.STORE_FILE_SIZE).asSize());
+    assertEquals(uncompressedStoreFileSize,
+      record.get(Field.UNCOMPRESSED_STORE_FILE_SIZE).asSize());
+    assertEquals(numStoreFiles, record.get(Field.NUM_STORE_FILES).asInt());
+    assertEquals(memStoreSize, record.get(Field.MEM_STORE_SIZE).asSize());
+    assertEquals(regionCount, record.get(Field.REGION_COUNT).asInt());
+  }
+
+  public static void assertRecordsInRegionServerMode(List<Record> records) {
+    assertEquals(2, records.size());
+
+    for (Record record : records) {
+      switch (record.get(Field.REGION_SERVER).asString()) {
+        case "host1:1000":
+          assertRecordInRegionServerMode(record, "host1.apache.com,1000,1", 0L, 0L, 0L,
+            new Size(600, Size.Unit.MEGABYTE), new Size(1200, Size.Unit.MEGABYTE), 6,
+            new Size(600, Size.Unit.MEGABYTE), 3, new Size(100, Size.Unit.MEGABYTE),
+            new Size(200, Size.Unit.MEGABYTE));
+          break;
+
+        case "host2:1001":
+          assertRecordInRegionServerMode(record, "host2.apache.com,1001,2", 0L, 0L, 0L,
+            new Size(600, Size.Unit.MEGABYTE), new Size(1200, Size.Unit.MEGABYTE), 6,
+            new Size(600, Size.Unit.MEGABYTE), 3, new Size(16, Size.Unit.GIGABYTE),
+            new Size(32, Size.Unit.GIGABYTE));
+          break;
+
+        default:
+          fail();
+      }
+    }
+  }
+
+  private static void assertRecordInRegionServerMode(Record record, String longRegionServer,
+    long requestCountPerSecond, long readRequestCountPerSecond, long writeCountRequestPerSecond,
+    Size storeFileSize, Size uncompressedStoreFileSize, int numStoreFiles,
+    Size memStoreSize, int regionCount, Size usedHeapSize, Size maxHeapSize) {
+    assertEquals(12, record.size());
+    assertEquals(longRegionServer, record.get(Field.LONG_REGION_SERVER).asString());
+    assertEquals(requestCountPerSecond, record.get(Field.REQUEST_COUNT_PER_SECOND).asLong());
+    assertEquals(readRequestCountPerSecond,
+      record.get(Field.READ_REQUEST_COUNT_PER_SECOND).asLong());
+    assertEquals(writeCountRequestPerSecond,
+      record.get(Field.WRITE_REQUEST_COUNT_PER_SECOND).asLong());
+    assertEquals(storeFileSize, record.get(Field.STORE_FILE_SIZE).asSize());
+    assertEquals(uncompressedStoreFileSize,
+      record.get(Field.UNCOMPRESSED_STORE_FILE_SIZE).asSize());
+    assertEquals(numStoreFiles, record.get(Field.NUM_STORE_FILES).asInt());
+    assertEquals(memStoreSize, record.get(Field.MEM_STORE_SIZE).asSize());
+    assertEquals(regionCount, record.get(Field.REGION_COUNT).asInt());
+    assertEquals(usedHeapSize, record.get(Field.USED_HEAP_SIZE).asSize());
+    assertEquals(maxHeapSize, record.get(Field.MAX_HEAP_SIZE).asSize());
+  }
+
+  public static void assertSummary(Summary summary) {
+    assertEquals(HBASE_VERSION, summary.getVersion());
+    assertEquals(CLUSTER_UUID, summary.getClusterId());
+    assertEquals(3, summary.getServers());
+    assertEquals(2, summary.getLiveServers());
+    assertEquals(1, summary.getDeadServers());
+    assertEquals(6, summary.getRegionCount());
+    assertEquals(1, summary.getRitCount());
+    assertEquals(3.0, summary.getAverageLoad(), 0.001);
+    assertEquals(300L, summary.getAggregateRequestPerSecond());
+  }
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/field/TestFieldValue.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/field/TestFieldValue.java
new file mode 100644
index 0000000..8b75e14
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/field/TestFieldValue.java
@@ -0,0 +1,290 @@
+/**
+ * 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.hadoop.hbase.hbtop.field;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+@Category(SmallTests.class)
+public class TestFieldValue {
+
+  @Test
+  public void testParseAndAsSomethingMethod() {
+    // String
+    FieldValue stringFieldValue = new FieldValue("aaa", FieldValueType.STRING);
+    assertThat(stringFieldValue.asString(), is("aaa"));
+
+    try {
+      new FieldValue(1, FieldValueType.STRING);
+      fail();
+    } catch (IllegalArgumentException ignored) {
+    }
+
+    // Integer
+    FieldValue integerFieldValue = new FieldValue(100, FieldValueType.INTEGER);
+    assertThat(integerFieldValue.asInt(), is(100));
+
+    integerFieldValue = new FieldValue("100", FieldValueType.INTEGER);
+    assertThat(integerFieldValue.asInt(), is(100));
+
+    try {
+      new FieldValue("aaa", FieldValueType.INTEGER);
+      fail();
+    } catch (IllegalArgumentException ignored) {
+    }
+
+    // Long
+    FieldValue longFieldValue = new FieldValue(100L, FieldValueType.LONG);
+    assertThat(longFieldValue.asLong(), is(100L));
+
+    longFieldValue = new FieldValue("100", FieldValueType.LONG);
+    assertThat(longFieldValue.asLong(), is(100L));
+
+    try {
+      new FieldValue("aaa", FieldValueType.LONG);
+      fail();
+    } catch (IllegalArgumentException ignored) {
+    }
+
+    try {
+      new FieldValue(100, FieldValueType.LONG);
+      fail();
+    } catch (IllegalArgumentException ignored) {
+    }
+
+    // Float
+    FieldValue floatFieldValue = new FieldValue(1.0f, FieldValueType.FLOAT);
+    assertThat(floatFieldValue.asFloat(), is(1.0f));
+
+    floatFieldValue = new FieldValue("1", FieldValueType.FLOAT);
+    assertThat(floatFieldValue.asFloat(), is(1.0f));
+
+    try {
+      new FieldValue("aaa", FieldValueType.FLOAT);
+      fail();
+    } catch (IllegalArgumentException ignored) {
+    }
+
+    try {
+      new FieldValue(1, FieldValueType.FLOAT);
+      fail();
+    } catch (IllegalArgumentException ignored) {
+    }
+
+    // Size
+    FieldValue sizeFieldValue =
+      new FieldValue(new Size(100, Size.Unit.MEGABYTE), FieldValueType.SIZE);
+    assertThat(sizeFieldValue.asString(), is("100.0MB"));
+    assertThat(sizeFieldValue.asSize(), is(new Size(100, Size.Unit.MEGABYTE)));
+
+    sizeFieldValue = new FieldValue("100MB", FieldValueType.SIZE);
+    assertThat(sizeFieldValue.asString(), is("100.0MB"));
+    assertThat(sizeFieldValue.asSize(), is(new Size(100, Size.Unit.MEGABYTE)));
+
+    try {
+      new FieldValue("100", FieldValueType.SIZE);
+      fail();
+    } catch (IllegalArgumentException ignored) {
+    }
+
+    try {
+      new FieldValue(100, FieldValueType.SIZE);
+      fail();
+    } catch (IllegalArgumentException ignored) {
+    }
+
+    // Percent
+    FieldValue percentFieldValue =
+      new FieldValue(100f, FieldValueType.PERCENT);
+    assertThat(percentFieldValue.asString(), is("100.00%"));
+    assertThat(percentFieldValue.asFloat(), is(100f));
+
+    percentFieldValue = new FieldValue("100%", FieldValueType.PERCENT);
+    assertThat(percentFieldValue.asString(), is("100.00%"));
+    assertThat(percentFieldValue.asFloat(), is(100f));
+
+    percentFieldValue = new FieldValue("100", FieldValueType.PERCENT);
+    assertThat(percentFieldValue.asString(), is("100.00%"));
+    assertThat(percentFieldValue.asFloat(), is(100f));
+
+    try {
+      new FieldValue(100, FieldValueType.PERCENT);
+      fail();
+    } catch (IllegalArgumentException ignored) {
+    }
+  }
+
+  @Test
+  public void testCompareTo() {
+    // String
+    FieldValue stringAFieldValue = new FieldValue("a", FieldValueType.STRING);
+    FieldValue stringAFieldValue2 = new FieldValue("a", FieldValueType.STRING);
+    FieldValue stringBFieldValue = new FieldValue("b", FieldValueType.STRING);
+    FieldValue stringCapitalAFieldValue = new FieldValue("A", FieldValueType.STRING);
+
+    assertThat(stringAFieldValue.compareTo(stringAFieldValue2), is(0));
+    assertThat(stringBFieldValue.compareTo(stringAFieldValue), is(1));
+    assertThat(stringAFieldValue.compareTo(stringBFieldValue), is(-1));
+    assertThat(stringAFieldValue.compareTo(stringCapitalAFieldValue), is(32));
+
+    // Integer
+    FieldValue integer1FieldValue = new FieldValue(1, FieldValueType.INTEGER);
+    FieldValue integer1FieldValue2 = new FieldValue(1, FieldValueType.INTEGER);
+    FieldValue integer2FieldValue = new FieldValue(2, FieldValueType.INTEGER);
+
+    assertThat(integer1FieldValue.compareTo(integer1FieldValue2), is(0));
+    assertThat(integer2FieldValue.compareTo(integer1FieldValue), is(1));
+    assertThat(integer1FieldValue.compareTo(integer2FieldValue), is(-1));
+
+    // Long
+    FieldValue long1FieldValue = new FieldValue(1L, FieldValueType.LONG);
+    FieldValue long1FieldValue2 = new FieldValue(1L, FieldValueType.LONG);
+    FieldValue long2FieldValue = new FieldValue(2L, FieldValueType.LONG);
+
+    assertThat(long1FieldValue.compareTo(long1FieldValue2), is(0));
+    assertThat(long2FieldValue.compareTo(long1FieldValue), is(1));
+    assertThat(long1FieldValue.compareTo(long2FieldValue), is(-1));
+
+    // Float
+    FieldValue float1FieldValue = new FieldValue(1.0f, FieldValueType.FLOAT);
+    FieldValue float1FieldValue2 = new FieldValue(1.0f, FieldValueType.FLOAT);
+    FieldValue float2FieldValue = new FieldValue(2.0f, FieldValueType.FLOAT);
+
+    assertThat(float1FieldValue.compareTo(float1FieldValue2), is(0));
+    assertThat(float2FieldValue.compareTo(float1FieldValue), is(1));
+    assertThat(float1FieldValue.compareTo(float2FieldValue), is(-1));
+
+    // Size
+    FieldValue size100MBFieldValue =
+      new FieldValue(new Size(100, Size.Unit.MEGABYTE), FieldValueType.SIZE);
+    FieldValue size100MBFieldValue2 =
+      new FieldValue(new Size(100, Size.Unit.MEGABYTE), FieldValueType.SIZE);
+    FieldValue size200MBFieldValue =
+      new FieldValue(new Size(200, Size.Unit.MEGABYTE), FieldValueType.SIZE);
+
+    assertThat(size100MBFieldValue.compareTo(size100MBFieldValue2), is(0));
+    assertThat(size200MBFieldValue.compareTo(size100MBFieldValue), is(1));
+    assertThat(size100MBFieldValue.compareTo(size200MBFieldValue), is(-1));
+
+    // Percent
+    FieldValue percent50FieldValue = new FieldValue(50.0f, FieldValueType.PERCENT);
+    FieldValue percent50FieldValue2 = new FieldValue(50.0f, FieldValueType.PERCENT);
+    FieldValue percent100FieldValue = new FieldValue(100.0f, FieldValueType.PERCENT);
+
+    assertThat(percent50FieldValue.compareTo(percent50FieldValue2), is(0));
+    assertThat(percent100FieldValue.compareTo(percent50FieldValue), is(1));
+    assertThat(percent50FieldValue.compareTo(percent100FieldValue), is(-1));
+  }
+
+  @Test
+  public void testPlus() {
+    // String
+    FieldValue stringFieldValue = new FieldValue("a", FieldValueType.STRING);
+    FieldValue stringFieldValue2 = new FieldValue("b", FieldValueType.STRING);
+    assertThat(stringFieldValue.plus(stringFieldValue2).asString(), is("ab"));
+
+    // Integer
+    FieldValue integerFieldValue = new FieldValue(1, FieldValueType.INTEGER);
+    FieldValue integerFieldValue2 = new FieldValue(2, FieldValueType.INTEGER);
+    assertThat(integerFieldValue.plus(integerFieldValue2).asInt(), is(3));
+
+    // Long
+    FieldValue longFieldValue = new FieldValue(1L, FieldValueType.LONG);
+    FieldValue longFieldValue2 = new FieldValue(2L, FieldValueType.LONG);
+    assertThat(longFieldValue.plus(longFieldValue2).asLong(), is(3L));
+
+    // Float
+    FieldValue floatFieldValue = new FieldValue(1.2f, FieldValueType.FLOAT);
+    FieldValue floatFieldValue2 = new FieldValue(2.2f, FieldValueType.FLOAT);
+    assertThat(floatFieldValue.plus(floatFieldValue2).asFloat(), is(3.4f));
+
+    // Size
+    FieldValue sizeFieldValue =
+      new FieldValue(new Size(100, Size.Unit.MEGABYTE), FieldValueType.SIZE);
+    FieldValue sizeFieldValue2 =
+      new FieldValue(new Size(200, Size.Unit.MEGABYTE), FieldValueType.SIZE);
+    assertThat(sizeFieldValue.plus(sizeFieldValue2).asString(), is("300.0MB"));
+    assertThat(sizeFieldValue.plus(sizeFieldValue2).asSize(),
+      is(new Size(300, Size.Unit.MEGABYTE)));
+
+    // Percent
+    FieldValue percentFieldValue = new FieldValue(30f, FieldValueType.PERCENT);
+    FieldValue percentFieldValue2 = new FieldValue(60f, FieldValueType.PERCENT);
+    assertThat(percentFieldValue.plus(percentFieldValue2).asString(), is("90.00%"));
+    assertThat(percentFieldValue.plus(percentFieldValue2).asFloat(), is(90f));
+  }
+
+  @Test
+  public void testCompareToIgnoreCase() {
+    FieldValue stringAFieldValue = new FieldValue("a", FieldValueType.STRING);
+    FieldValue stringCapitalAFieldValue = new FieldValue("A", FieldValueType.STRING);
+    FieldValue stringCapitalBFieldValue = new FieldValue("B", FieldValueType.STRING);
+
+    assertThat(stringAFieldValue.compareToIgnoreCase(stringCapitalAFieldValue), is(0));
+    assertThat(stringCapitalBFieldValue.compareToIgnoreCase(stringAFieldValue), is(1));
+    assertThat(stringAFieldValue.compareToIgnoreCase(stringCapitalBFieldValue), is(-1));
+  }
+
+  @Test
+  public void testOptimizeSize() {
+    FieldValue sizeFieldValue =
+      new FieldValue(new Size(1, Size.Unit.BYTE), FieldValueType.SIZE);
+    assertThat(sizeFieldValue.asString(), is("1.0B"));
+
+    sizeFieldValue =
+      new FieldValue(new Size(1024, Size.Unit.BYTE), FieldValueType.SIZE);
+    assertThat(sizeFieldValue.asString(), is("1.0KB"));
+
+    sizeFieldValue =
+      new FieldValue(new Size(2 * 1024, Size.Unit.BYTE), FieldValueType.SIZE);
+    assertThat(sizeFieldValue.asString(), is("2.0KB"));
+
+    sizeFieldValue =
+      new FieldValue(new Size(2 * 1024, Size.Unit.KILOBYTE), FieldValueType.SIZE);
+    assertThat(sizeFieldValue.asString(), is("2.0MB"));
+
+    sizeFieldValue =
+      new FieldValue(new Size(1024 * 1024, Size.Unit.KILOBYTE), FieldValueType.SIZE);
+    assertThat(sizeFieldValue.asString(), is("1.0GB"));
+
+    sizeFieldValue =
+      new FieldValue(new Size(2 * 1024 * 1024, Size.Unit.MEGABYTE), FieldValueType.SIZE);
+    assertThat(sizeFieldValue.asString(), is("2.0TB"));
+
+    sizeFieldValue =
+      new FieldValue(new Size(2 * 1024, Size.Unit.TERABYTE), FieldValueType.SIZE);
+    assertThat(sizeFieldValue.asString(), is("2.0PB"));
+
+    sizeFieldValue =
+      new FieldValue(new Size(1024 * 1024, Size.Unit.TERABYTE), FieldValueType.SIZE);
+    assertThat(sizeFieldValue.asString(), is("1024.0PB"));
+
+    sizeFieldValue =
+      new FieldValue(new Size(1, Size.Unit.PETABYTE), FieldValueType.SIZE);
+    assertThat(sizeFieldValue.asString(), is("1.0PB"));
+
+    sizeFieldValue =
+      new FieldValue(new Size(1024, Size.Unit.PETABYTE), FieldValueType.SIZE);
+    assertThat(sizeFieldValue.asString(), is("1024.0PB"));
+  }
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/field/TestSize.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/field/TestSize.java
new file mode 100644
index 0000000..3d6b285
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/field/TestSize.java
@@ -0,0 +1,80 @@
+/**
+ * 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.hadoop.hbase.hbtop.field;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.Set;
+import java.util.TreeSet;
+import org.apache.hadoop.hbase.testclassification.MiscTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+@Category({MiscTests.class, SmallTests.class})
+public class TestSize {
+
+  @Test
+  public void testConversion() {
+    Size kbSize = new Size(1024D, Size.Unit.MEGABYTE);
+    assertEquals(1D, kbSize.get(Size.Unit.GIGABYTE), 0);
+    assertEquals(1024D, kbSize.get(), 0);
+    assertEquals(1024D * 1024D, kbSize.get(Size.Unit.KILOBYTE), 0);
+    assertEquals(1024D * 1024D * 1024D, kbSize.get(Size.Unit.BYTE), 0);
+  }
+
+  @Test
+  public void testCompare() {
+    Size size00 = new Size(100D, Size.Unit.GIGABYTE);
+    Size size01 = new Size(100D, Size.Unit.MEGABYTE);
+    Size size02 = new Size(100D, Size.Unit.BYTE);
+    Set<Size> sizes = new TreeSet<>();
+    sizes.add(size00);
+    sizes.add(size01);
+    sizes.add(size02);
+    int count = 0;
+    for (Size s : sizes) {
+      switch (count++) {
+        case 0:
+          assertEquals(size02, s);
+          break;
+        case 1:
+          assertEquals(size01, s);
+          break;
+        default:
+          assertEquals(size00, s);
+          break;
+      }
+    }
+    assertEquals(3, count);
+  }
+
+  @Test
+  public void testEqual() {
+    assertEquals(new Size(1024D, Size.Unit.TERABYTE),
+      new Size(1D, Size.Unit.PETABYTE));
+    assertEquals(new Size(1024D, Size.Unit.GIGABYTE),
+      new Size(1D, Size.Unit.TERABYTE));
+    assertEquals(new Size(1024D, Size.Unit.MEGABYTE),
+      new Size(1D, Size.Unit.GIGABYTE));
+    assertEquals(new Size(1024D, Size.Unit.KILOBYTE),
+      new Size(1D, Size.Unit.MEGABYTE));
+    assertEquals(new Size(1024D, Size.Unit.BYTE),
+      new Size(1D, Size.Unit.KILOBYTE));
+  }
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestModeBase.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestModeBase.java
new file mode 100644
index 0000000..0b99ef4
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestModeBase.java
@@ -0,0 +1,46 @@
+/**
+ * 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.hadoop.hbase.hbtop.mode;
+
+import java.util.List;
+import org.apache.hadoop.hbase.hbtop.Record;
+import org.apache.hadoop.hbase.hbtop.TestUtils;
+import org.junit.Test;
+
+
+public abstract class TestModeBase {
+
+  @Test
+  public void testGetRecords() {
+    List<Record> records = getMode().getRecords(TestUtils.createDummyClusterStatus());
+    assertRecords(records);
+  }
+
+  protected abstract Mode getMode();
+  protected abstract void assertRecords(List<Record> records);
+
+  @Test
+  public void testDrillDown() {
+    List<Record> records = getMode().getRecords(TestUtils.createDummyClusterStatus());
+    for (Record record : records) {
+      assertDrillDown(record, getMode().drillDown(record));
+    }
+  }
+
+  protected abstract void assertDrillDown(Record currentRecord, DrillDownInfo drillDownInfo);
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestNamespaceMode.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestNamespaceMode.java
new file mode 100644
index 0000000..afaf073
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestNamespaceMode.java
@@ -0,0 +1,63 @@
+/**
+ * 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.hadoop.hbase.hbtop.mode;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+
+import java.util.List;
+import org.apache.hadoop.hbase.hbtop.Record;
+import org.apache.hadoop.hbase.hbtop.TestUtils;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.experimental.categories.Category;
+
+@Category(SmallTests.class)
+public class TestNamespaceMode extends TestModeBase {
+
+  @Override
+  protected Mode getMode() {
+    return Mode.NAMESPACE;
+  }
+
+  @Override
+  protected void assertRecords(List<Record> records) {
+    TestUtils.assertRecordsInNamespaceMode(records);
+  }
+
+  @Override
+  protected void assertDrillDown(Record currentRecord, DrillDownInfo drillDownInfo) {
+    assertThat(drillDownInfo.getNextMode(), is(Mode.TABLE));
+    assertThat(drillDownInfo.getInitialFilters().size(), is(1));
+
+    switch (currentRecord.get(Field.NAMESPACE).asString()) {
+      case "default":
+        assertThat(drillDownInfo.getInitialFilters().get(0).toString(), is("NAMESPACE==default"));
+        break;
+
+      case "namespace":
+        assertThat(drillDownInfo.getInitialFilters().get(0).toString(),
+          is("NAMESPACE==namespace"));
+        break;
+
+      default:
+        fail();
+    }
+  }
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestRegionMode.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestRegionMode.java
new file mode 100644
index 0000000..2cbaf1b
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestRegionMode.java
@@ -0,0 +1,47 @@
+/**
+ * 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.hadoop.hbase.hbtop.mode;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.junit.Assert.assertThat;
+
+import java.util.List;
+import org.apache.hadoop.hbase.hbtop.Record;
+import org.apache.hadoop.hbase.hbtop.TestUtils;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.experimental.categories.Category;
+
+@Category(SmallTests.class)
+public class TestRegionMode extends TestModeBase {
+
+  @Override
+  protected Mode getMode() {
+    return Mode.REGION;
+  }
+
+  @Override
+  protected void assertRecords(List<Record> records) {
+    TestUtils.assertRecordsInRegionMode(records);
+  }
+
+  @Override
+  protected void assertDrillDown(Record currentRecord, DrillDownInfo drillDownInfo) {
+    assertThat(drillDownInfo, is(nullValue()));
+  }
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestRegionServerMode.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestRegionServerMode.java
new file mode 100644
index 0000000..93788d1
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestRegionServerMode.java
@@ -0,0 +1,62 @@
+/**
+ * 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.hadoop.hbase.hbtop.mode;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+
+import java.util.List;
+import org.apache.hadoop.hbase.hbtop.Record;
+import org.apache.hadoop.hbase.hbtop.TestUtils;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.experimental.categories.Category;
+
+@Category(SmallTests.class)
+public class TestRegionServerMode extends TestModeBase {
+
+  @Override
+  protected Mode getMode() {
+    return Mode.REGION_SERVER;
+  }
+
+  @Override
+  protected void assertRecords(List<Record> records) {
+    TestUtils.assertRecordsInRegionServerMode(records);
+  }
+
+  @Override
+  protected void assertDrillDown(Record currentRecord, DrillDownInfo drillDownInfo) {
+    assertThat(drillDownInfo.getNextMode(), is(Mode.REGION));
+    assertThat(drillDownInfo.getInitialFilters().size(), is(1));
+
+    switch (currentRecord.get(Field.REGION_SERVER).asString()) {
+      case "host1:1000":
+        assertThat(drillDownInfo.getInitialFilters().get(0).toString(), is("RS==host1:1000"));
+        break;
+
+      case "host2:1001":
+        assertThat(drillDownInfo.getInitialFilters().get(0).toString(), is("RS==host2:1001"));
+        break;
+
+      default:
+        fail();
+    }
+  }
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestRequestCountPerSecond.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestRequestCountPerSecond.java
new file mode 100644
index 0000000..0152f44
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestRequestCountPerSecond.java
@@ -0,0 +1,49 @@
+/**
+ * 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.hadoop.hbase.hbtop.mode;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+@Category(SmallTests.class)
+public class TestRequestCountPerSecond {
+
+  @Test
+  public void test() {
+    RequestCountPerSecond requestCountPerSecond = new RequestCountPerSecond();
+
+    requestCountPerSecond.refresh(1000, 300, 200);
+    assertThat(requestCountPerSecond.getRequestCountPerSecond(), is(0L));
+    assertThat(requestCountPerSecond.getReadRequestCountPerSecond(), is(0L));
+    assertThat(requestCountPerSecond.getWriteRequestCountPerSecond(), is(0L));
+
+    requestCountPerSecond.refresh(2000, 1300, 1200);
+    assertThat(requestCountPerSecond.getRequestCountPerSecond(), is(2000L));
+    assertThat(requestCountPerSecond.getReadRequestCountPerSecond(), is(1000L));
+    assertThat(requestCountPerSecond.getWriteRequestCountPerSecond(), is(1000L));
+
+    requestCountPerSecond.refresh(12000, 5300, 2200);
+    assertThat(requestCountPerSecond.getRequestCountPerSecond(), is(500L));
+    assertThat(requestCountPerSecond.getReadRequestCountPerSecond(), is(400L));
+    assertThat(requestCountPerSecond.getWriteRequestCountPerSecond(), is(100L));
+  }
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestTableMode.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestTableMode.java
new file mode 100644
index 0000000..8376591
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestTableMode.java
@@ -0,0 +1,73 @@
+/**
+ * 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.hadoop.hbase.hbtop.mode;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+
+import java.util.List;
+import org.apache.hadoop.hbase.hbtop.Record;
+import org.apache.hadoop.hbase.hbtop.TestUtils;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.experimental.categories.Category;
+
+@Category(SmallTests.class)
+public class TestTableMode extends TestModeBase {
+
+  @Override
+  protected Mode getMode() {
+    return Mode.TABLE;
+  }
+
+  @Override
+  protected void assertRecords(List<Record> records) {
+    TestUtils.assertRecordsInTableMode(records);
+  }
+
+  @Override
+  protected void assertDrillDown(Record currentRecord, DrillDownInfo drillDownInfo) {
+    assertThat(drillDownInfo.getNextMode(), is(Mode.REGION));
+    assertThat(drillDownInfo.getInitialFilters().size(), is(2));
+
+    String tableName = String.format("%s:%s", currentRecord.get(Field.NAMESPACE).asString(),
+      currentRecord.get(Field.TABLE).asString());
+
+    switch (tableName) {
+      case "default:table1":
+        assertThat(drillDownInfo.getInitialFilters().get(0).toString(), is("NAMESPACE==default"));
+        assertThat(drillDownInfo.getInitialFilters().get(1).toString(), is("TABLE==table1"));
+        break;
+
+      case "default:table2":
+        assertThat(drillDownInfo.getInitialFilters().get(0).toString(), is("NAMESPACE==default"));
+        assertThat(drillDownInfo.getInitialFilters().get(1).toString(), is("TABLE==table2"));
+        break;
+
+      case "namespace:table3":
+        assertThat(drillDownInfo.getInitialFilters().get(0).toString(),
+          is("NAMESPACE==namespace"));
+        assertThat(drillDownInfo.getInitialFilters().get(1).toString(), is("TABLE==table3"));
+        break;
+
+      default:
+        fail();
+    }
+  }
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/field/TestFieldScreenPresenter.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/field/TestFieldScreenPresenter.java
new file mode 100644
index 0000000..b8baa9a
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/field/TestFieldScreenPresenter.java
@@ -0,0 +1,151 @@
+/**
+ * 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.hadoop.hbase.hbtop.screen.field;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyBoolean;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.verify;
+
+import java.util.ArrayList;
+import java.util.EnumMap;
+import java.util.List;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.hbtop.field.FieldInfo;
+import org.apache.hadoop.hbase.hbtop.mode.Mode;
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+import org.apache.hadoop.hbase.hbtop.screen.top.TopScreenView;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+@Category(SmallTests.class)
+@RunWith(MockitoJUnitRunner.class)
+public class TestFieldScreenPresenter {
+
+  @Mock
+  private FieldScreenView fieldScreenView;
+
+  private int sortFieldPosition = -1;
+  private List<Field> fields;
+  private EnumMap<Field, Boolean> fieldDisplayMap;
+
+  @Mock
+  private FieldScreenPresenter.ResultListener resultListener;
+
+  @Mock
+  private TopScreenView topScreenView;
+
+  private FieldScreenPresenter fieldScreenPresenter;
+
+  @Before
+  public void setup() {
+    Field sortField = Mode.REGION.getDefaultSortField();
+
+    fields = new ArrayList<>();
+    for (FieldInfo fieldInfo : Mode.REGION.getFieldInfos()) {
+      fields.add(fieldInfo.getField());
+    }
+
+    fieldDisplayMap = new EnumMap<>(Field.class);
+    for (FieldInfo fieldInfo : Mode.REGION.getFieldInfos()) {
+      fieldDisplayMap.put(fieldInfo.getField(), fieldInfo.isDisplayByDefault());
+    }
+
+    fieldScreenPresenter =
+      new FieldScreenPresenter(fieldScreenView, sortField, fields, fieldDisplayMap, resultListener,
+        topScreenView);
+
+    for (int i = 0; i < fields.size(); i++) {
+      Field field = fields.get(i);
+      if (field == sortField) {
+        sortFieldPosition = i;
+        break;
+      }
+    }
+  }
+
+  @Test
+  public void testInit() {
+    fieldScreenPresenter.init();
+
+    int modeHeaderMaxLength = "#COMPingCell".length();
+    int modeDescriptionMaxLength = "Write Request Count per second".length();
+
+    verify(fieldScreenView).showFieldScreen(eq("#REQ/S"), eq(fields), eq(fieldDisplayMap),
+      eq(sortFieldPosition), eq(modeHeaderMaxLength), eq(modeDescriptionMaxLength), eq(false));
+  }
+
+  @Test
+  public void testChangeSortField() {
+    fieldScreenPresenter.arrowUp();
+    fieldScreenPresenter.setSortField();
+
+    fieldScreenPresenter.arrowDown();
+    fieldScreenPresenter.arrowDown();
+    fieldScreenPresenter.setSortField();
+
+    fieldScreenPresenter.pageUp();
+    fieldScreenPresenter.setSortField();
+
+    fieldScreenPresenter.pageDown();
+    fieldScreenPresenter.setSortField();
+
+    InOrder inOrder = inOrder(fieldScreenView);
+    inOrder.verify(fieldScreenView).showScreenDescription(eq("LRS"));
+    inOrder.verify(fieldScreenView).showScreenDescription(eq("#READ/S"));
+    inOrder.verify(fieldScreenView).showScreenDescription(eq(fields.get(0).getHeader()));
+    inOrder.verify(fieldScreenView).showScreenDescription(
+      eq(fields.get(fields.size() - 1).getHeader()));
+  }
+
+  @Test
+  public void testSwitchFieldDisplay() {
+    fieldScreenPresenter.switchFieldDisplay();
+    fieldScreenPresenter.switchFieldDisplay();
+
+    InOrder inOrder = inOrder(fieldScreenView);
+    inOrder.verify(fieldScreenView).showField(anyInt(), any(Field.class), eq(false),
+      anyBoolean(), anyInt(), anyInt(), anyBoolean());
+    inOrder.verify(fieldScreenView).showField(anyInt(), any(Field.class), eq(true),
+      anyBoolean(), anyInt(), anyInt(), anyBoolean());
+  }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  public void testChangeFieldsOrder() {
+    fieldScreenPresenter.turnOnMoveMode();
+    fieldScreenPresenter.arrowUp();
+    fieldScreenPresenter.turnOffMoveMode();
+
+    Field removed = fields.remove(sortFieldPosition);
+    fields.add(sortFieldPosition - 1, removed);
+
+    assertThat(fieldScreenPresenter.transitionToNextScreen(), is((ScreenView) topScreenView));
+    verify(resultListener).accept(any(Field.class), eq(fields), any(EnumMap.class));
+  }
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/help/TestHelpScreenPresenter.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/help/TestHelpScreenPresenter.java
new file mode 100644
index 0000000..51d2e95
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/help/TestHelpScreenPresenter.java
@@ -0,0 +1,75 @@
+/**
+ * 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.hadoop.hbase.hbtop.screen.help;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.argThat;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.verify;
+
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+import org.apache.hadoop.hbase.hbtop.screen.top.TopScreenView;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatcher;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+
+@Category(SmallTests.class)
+@RunWith(MockitoJUnitRunner.class)
+public class TestHelpScreenPresenter {
+
+  private static final long TEST_REFRESH_DELAY = 5;
+
+  @Mock
+  private HelpScreenView helpScreenView;
+
+  @Mock
+  private TopScreenView topScreenView;
+
+  private HelpScreenPresenter helpScreenPresenter;
+
+  @Before
+  public void setup() {
+    helpScreenPresenter = new HelpScreenPresenter(helpScreenView, TEST_REFRESH_DELAY,
+      topScreenView);
+  }
+
+  @Test
+  public void testInit() {
+    helpScreenPresenter.init();
+
+    verify(helpScreenView).showHelpScreen(eq(TEST_REFRESH_DELAY), argThat(
+      new ArgumentMatcher<CommandDescription[]>() {
+        @Override
+        public boolean matches(Object o) {
+          return ((CommandDescription[]) o).length == 14;
+        }
+      }));
+  }
+
+  @Test
+  public void testTransitionToTopScreen() {
+    assertThat(helpScreenPresenter.transitionToNextScreen(), is((ScreenView) topScreenView));
+  }
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/mode/TestModeScreenPresenter.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/mode/TestModeScreenPresenter.java
new file mode 100644
index 0000000..276a5ed
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/mode/TestModeScreenPresenter.java
@@ -0,0 +1,140 @@
+/**
+ * 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.hadoop.hbase.hbtop.screen.mode;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import java.util.Arrays;
+import org.apache.hadoop.hbase.hbtop.mode.Mode;
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+import org.apache.hadoop.hbase.hbtop.screen.top.TopScreenView;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+@Category(SmallTests.class)
+@RunWith(MockitoJUnitRunner.class)
+public class TestModeScreenPresenter {
+
+  @Mock
+  private ModeScreenView modeScreenView;
+
+  @Mock
+  private TopScreenView topScreenView;
+
+  @Mock
+  private ModeScreenPresenter.ResultListener resultListener;
+
+  private ModeScreenPresenter createModeScreenPresenter(Mode currentMode) {
+    return new ModeScreenPresenter(modeScreenView, currentMode, resultListener, topScreenView);
+  }
+
+  @Test
+  public void testInit() {
+    ModeScreenPresenter modeScreenPresenter = createModeScreenPresenter(Mode.REGION);
+
+    modeScreenPresenter.init();
+
+    int modeHeaderMaxLength = Mode.REGION_SERVER.getHeader().length();
+    int modeDescriptionMaxLength = Mode.REGION_SERVER.getDescription().length();
+
+    verify(modeScreenView).showModeScreen(eq(Mode.REGION), eq(Arrays.asList(Mode.values())),
+      eq(Mode.REGION.ordinal()) , eq(modeHeaderMaxLength), eq(modeDescriptionMaxLength));
+  }
+
+  @Test
+  public void testSelectNamespaceMode() {
+    ModeScreenPresenter modeScreenPresenter = createModeScreenPresenter(Mode.REGION);
+
+    modeScreenPresenter.arrowUp();
+    modeScreenPresenter.arrowUp();
+
+    assertThat(modeScreenPresenter.transitionToNextScreen(true), is((ScreenView) topScreenView));
+    verify(resultListener).accept(eq(Mode.NAMESPACE));
+  }
+
+  @Test
+  public void testSelectTableMode() {
+    ModeScreenPresenter modeScreenPresenter = createModeScreenPresenter(Mode.REGION);
+
+    modeScreenPresenter.arrowUp();
+    assertThat(modeScreenPresenter.transitionToNextScreen(true), is((ScreenView) topScreenView));
+    verify(resultListener).accept(eq(Mode.TABLE));
+  }
+
+  @Test
+  public void testSelectRegionMode() {
+    ModeScreenPresenter modeScreenPresenter = createModeScreenPresenter(Mode.NAMESPACE);
+
+    modeScreenPresenter.arrowDown();
+    modeScreenPresenter.arrowDown();
+
+    assertThat(modeScreenPresenter.transitionToNextScreen(true), is((ScreenView) topScreenView));
+    verify(resultListener).accept(eq(Mode.REGION));
+  }
+
+  @Test
+  public void testSelectRegionServerMode() {
+    ModeScreenPresenter modeScreenPresenter = createModeScreenPresenter(Mode.REGION);
+
+    modeScreenPresenter.arrowDown();
+
+    assertThat(modeScreenPresenter.transitionToNextScreen(true), is((ScreenView) topScreenView));
+    verify(resultListener).accept(eq(Mode.REGION_SERVER));
+  }
+
+  @Test
+  public void testCancelSelectingMode() {
+    ModeScreenPresenter modeScreenPresenter = createModeScreenPresenter(Mode.REGION);
+
+    modeScreenPresenter.arrowDown();
+    modeScreenPresenter.arrowDown();
+
+    assertThat(modeScreenPresenter.transitionToNextScreen(false), is((ScreenView) topScreenView));
+    verify(resultListener, never()).accept(any(Mode.class));
+  }
+
+  @Test
+  public void testPageUp() {
+    ModeScreenPresenter modeScreenPresenter = createModeScreenPresenter(Mode.REGION);
+
+    modeScreenPresenter.pageUp();
+
+    assertThat(modeScreenPresenter.transitionToNextScreen(true), is((ScreenView) topScreenView));
+    verify(resultListener).accept(eq(Mode.values()[0]));
+  }
+
+  @Test
+  public void testPageDown() {
+    ModeScreenPresenter modeScreenPresenter = createModeScreenPresenter(Mode.REGION);
+
+    modeScreenPresenter.pageDown();
+
+    assertThat(modeScreenPresenter.transitionToNextScreen(true), is((ScreenView) topScreenView));
+    Mode[] modes = Mode.values();
+    verify(resultListener).accept(eq(modes[modes.length - 1]));
+  }
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/top/TestFilterDisplayModeScreenPresenter.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/top/TestFilterDisplayModeScreenPresenter.java
new file mode 100644
index 0000000..41a5fde
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/top/TestFilterDisplayModeScreenPresenter.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.hadoop.hbase.hbtop.screen.top;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.argThat;
+import static org.mockito.Mockito.verify;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hadoop.hbase.hbtop.RecordFilter;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.hbtop.field.FieldInfo;
+import org.apache.hadoop.hbase.hbtop.mode.Mode;
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatcher;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+
+@Category(SmallTests.class)
+@RunWith(MockitoJUnitRunner.class)
+public class TestFilterDisplayModeScreenPresenter {
+
+  @Mock
+  private FilterDisplayModeScreenView filterDisplayModeScreenView;
+
+  @Mock
+  private TopScreenView topScreenView;
+
+  private FilterDisplayModeScreenPresenter filterDisplayModeScreenPresenter;
+
+  @Before
+  public void setup() {
+    List<Field> fields = new ArrayList<>();
+    for (FieldInfo fieldInfo : Mode.REGION.getFieldInfos()) {
+      fields.add(fieldInfo.getField());
+    }
+
+    List<RecordFilter>  filters = new ArrayList<>();
+    filters.add(RecordFilter.parse("NAMESPACE==namespace", fields, true));
+    filters.add(RecordFilter.parse("TABLE==table", fields, true));
+
+    filterDisplayModeScreenPresenter = new FilterDisplayModeScreenPresenter(
+      filterDisplayModeScreenView, filters, topScreenView);
+  }
+
+  @Test
+  public void testInit() {
+    filterDisplayModeScreenPresenter.init();
+
+    verify(filterDisplayModeScreenView).showFilters(argThat(
+      new ArgumentMatcher<List<RecordFilter>>() {
+        @Override
+        @SuppressWarnings("unchecked")
+        public boolean matches(Object argument) {
+          List<RecordFilter> filters = (List<RecordFilter>) argument;
+          return filters.size() == 2
+            && filters.get(0).toString().equals("NAMESPACE==namespace")
+            && filters.get(1).toString().equals("TABLE==table");
+        }
+      }));
+  }
+
+  @Test
+  public void testReturnToTopScreen() {
+    assertThat(filterDisplayModeScreenPresenter.returnToNextScreen(),
+      is((ScreenView) topScreenView));
+  }
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/top/TestInputModeScreenPresenter.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/top/TestInputModeScreenPresenter.java
new file mode 100644
index 0000000..ec38284
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/top/TestInputModeScreenPresenter.java
@@ -0,0 +1,198 @@
+/**
+ * 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.hadoop.hbase.hbtop.screen.top;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+@Category(SmallTests.class)
+@RunWith(MockitoJUnitRunner.class)
+public class TestInputModeScreenPresenter {
+
+  private static final String TEST_INPUT_MESSAGE = "test input message";
+
+  @Mock
+  private InputModeScreenView inputModeScreenView;
+
+  @Mock
+  private TopScreenView topScreenView;
+
+  @Mock
+  private InputModeScreenPresenter.ResultListener resultListener;
+
+  private InputModeScreenPresenter inputModeScreenPresenter;
+
+  @Before
+  public void setup() {
+    List<String> histories = new ArrayList<>();
+    histories.add("history1");
+    histories.add("history2");
+
+    inputModeScreenPresenter = new InputModeScreenPresenter(inputModeScreenView,
+      TEST_INPUT_MESSAGE, histories, resultListener);
+  }
+
+  @Test
+  public void testInit() {
+    inputModeScreenPresenter.init();
+
+    verify(inputModeScreenView).showInput(eq(TEST_INPUT_MESSAGE), eq(""), eq(0));
+  }
+
+  @Test
+  public void testCharacter() {
+    inputModeScreenPresenter.character('a');
+    inputModeScreenPresenter.character('b');
+    inputModeScreenPresenter.character('c');
+
+    InOrder inOrder = inOrder(inputModeScreenView);
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("a"), eq(1));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("ab"), eq(2));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("abc"), eq(3));
+  }
+
+  @Test
+  public void testArrowLeftAndRight() {
+    inputModeScreenPresenter.character('a');
+    inputModeScreenPresenter.character('b');
+    inputModeScreenPresenter.character('c');
+    inputModeScreenPresenter.arrowLeft();
+    inputModeScreenPresenter.arrowLeft();
+    inputModeScreenPresenter.arrowLeft();
+    inputModeScreenPresenter.arrowLeft();
+    inputModeScreenPresenter.arrowRight();
+    inputModeScreenPresenter.arrowRight();
+    inputModeScreenPresenter.arrowRight();
+    inputModeScreenPresenter.arrowRight();
+
+    InOrder inOrder = inOrder(inputModeScreenView);
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("a"), eq(1));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("ab"), eq(2));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("abc"), eq(3));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("abc"), eq(2));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("abc"), eq(1));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("abc"), eq(0));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("abc"), eq(1));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("abc"), eq(2));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("abc"), eq(3));
+  }
+
+  @Test
+  public void testHomeAndEnd() {
+    inputModeScreenPresenter.character('a');
+    inputModeScreenPresenter.character('b');
+    inputModeScreenPresenter.character('c');
+    inputModeScreenPresenter.home();
+    inputModeScreenPresenter.home();
+    inputModeScreenPresenter.end();
+    inputModeScreenPresenter.end();
+
+    InOrder inOrder = inOrder(inputModeScreenView);
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("a"), eq(1));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("ab"), eq(2));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("abc"), eq(3));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("abc"), eq(0));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("abc"), eq(3));
+  }
+
+  @Test
+  public void testBackspace() {
+    inputModeScreenPresenter.character('a');
+    inputModeScreenPresenter.character('b');
+    inputModeScreenPresenter.character('c');
+    inputModeScreenPresenter.backspace();
+    inputModeScreenPresenter.backspace();
+    inputModeScreenPresenter.backspace();
+    inputModeScreenPresenter.backspace();
+
+    InOrder inOrder = inOrder(inputModeScreenView);
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("a"), eq(1));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("ab"), eq(2));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("abc"), eq(3));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("ab"), eq(2));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("a"), eq(1));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq(""), eq(0));
+  }
+
+  @Test
+  public void testDelete() {
+    inputModeScreenPresenter.character('a');
+    inputModeScreenPresenter.character('b');
+    inputModeScreenPresenter.character('c');
+    inputModeScreenPresenter.delete();
+    inputModeScreenPresenter.arrowLeft();
+    inputModeScreenPresenter.delete();
+
+    InOrder inOrder = inOrder(inputModeScreenView);
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("a"), eq(1));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("ab"), eq(2));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("abc"), eq(3));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("abc"), eq(2));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("ab"), eq(2));
+  }
+
+  @Test
+  public void testHistories() {
+    inputModeScreenPresenter.character('a');
+    inputModeScreenPresenter.character('b');
+    inputModeScreenPresenter.character('c');
+    inputModeScreenPresenter.arrowUp();
+    inputModeScreenPresenter.arrowUp();
+    inputModeScreenPresenter.arrowUp();
+    inputModeScreenPresenter.arrowDown();
+    inputModeScreenPresenter.arrowDown();
+    inputModeScreenPresenter.arrowDown();
+
+    InOrder inOrder = inOrder(inputModeScreenView);
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("a"), eq(1));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("ab"), eq(2));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("abc"), eq(3));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("history2"), eq(8));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("history1"), eq(8));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("history2"), eq(8));
+  }
+
+  @Test
+  public void testReturnToTopScreen() {
+    when(resultListener.apply(any(String.class))).thenReturn(topScreenView);
+
+    inputModeScreenPresenter.character('a');
+    inputModeScreenPresenter.character('b');
+    inputModeScreenPresenter.character('c');
+
+    assertThat(inputModeScreenPresenter.returnToNextScreen(), is((ScreenView) topScreenView));
+    verify(resultListener).apply(eq("abc"));
+  }
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/top/TestMessageModeScreenPresenter.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/top/TestMessageModeScreenPresenter.java
new file mode 100644
index 0000000..9c17dca
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/top/TestMessageModeScreenPresenter.java
@@ -0,0 +1,65 @@
+/**
+ * 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.hadoop.hbase.hbtop.screen.top;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.verify;
+
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+@Category(SmallTests.class)
+@RunWith(MockitoJUnitRunner.class)
+public class TestMessageModeScreenPresenter {
+
+  private static final String TEST_MESSAGE = "test message";
+
+  @Mock
+  private MessageModeScreenView messageModeScreenView;
+
+  @Mock
+  private TopScreenView topScreenView;
+
+  private MessageModeScreenPresenter messageModeScreenPresenter;
+
+  @Before
+  public void setup() {
+    messageModeScreenPresenter = new MessageModeScreenPresenter(messageModeScreenView,
+      TEST_MESSAGE, topScreenView);
+  }
+
+  @Test
+  public void testInit() {
+    messageModeScreenPresenter.init();
+
+    verify(messageModeScreenView).showMessage(eq(TEST_MESSAGE));
+  }
+
+  @Test
+  public void testReturnToTopScreen() {
+    assertThat(messageModeScreenPresenter.returnToNextScreen(), is((ScreenView) topScreenView));
+  }
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/top/TestPaging.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/top/TestPaging.java
new file mode 100644
index 0000000..53500bc
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/top/TestPaging.java
@@ -0,0 +1,293 @@
+/**
+ * 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.hadoop.hbase.hbtop.screen.top;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+@Category(SmallTests.class)
+public class TestPaging {
+
+  @Test
+  public void testArrowUpAndArrowDown() {
+    Paging paging = new Paging();
+    paging.updatePageSize(3);
+    paging.updateRecordsSize(5);
+
+    assertPaging(paging, 0, 0, 3);
+
+    paging.arrowDown();
+    assertPaging(paging, 1, 0, 3);
+
+    paging.arrowDown();
+    assertPaging(paging, 2, 0, 3);
+
+    paging.arrowDown();
+    assertPaging(paging, 3, 1, 4);
+
+    paging.arrowDown();
+    assertPaging(paging, 4, 2, 5);
+
+    paging.arrowDown();
+    assertPaging(paging, 4, 2, 5);
+
+    paging.arrowUp();
+    assertPaging(paging, 3, 2, 5);
+
+    paging.arrowUp();
+    assertPaging(paging, 2, 2, 5);
+
+    paging.arrowUp();
+    assertPaging(paging, 1, 1, 4);
+
+    paging.arrowUp();
+    assertPaging(paging, 0, 0, 3);
+
+    paging.arrowUp();
+    assertPaging(paging, 0, 0, 3);
+  }
+
+  @Test
+  public void testPageUpAndPageDown() {
+    Paging paging = new Paging();
+    paging.updatePageSize(3);
+    paging.updateRecordsSize(8);
+
+    assertPaging(paging, 0, 0, 3);
+
+    paging.pageDown();
+    assertPaging(paging, 3, 3, 6);
+
+    paging.pageDown();
+    assertPaging(paging, 6, 5, 8);
+
+    paging.pageDown();
+    assertPaging(paging, 7, 5, 8);
+
+    paging.pageDown();
+    assertPaging(paging, 7, 5, 8);
+
+    paging.pageUp();
+    assertPaging(paging, 4, 4, 7);
+
+    paging.pageUp();
+    assertPaging(paging, 1, 1, 4);
+
+    paging.pageUp();
+    assertPaging(paging, 0, 0, 3);
+
+    paging.pageUp();
+    assertPaging(paging, 0, 0, 3);
+  }
+
+  @Test
+  public void testInit() {
+    Paging paging = new Paging();
+    paging.updatePageSize(3);
+    paging.updateRecordsSize(5);
+
+    assertPaging(paging, 0, 0, 3);
+
+    paging.pageDown();
+    paging.pageDown();
+    paging.pageDown();
+    paging.pageDown();
+    paging.init();
+
+    assertPaging(paging, 0, 0, 3);
+  }
+
+  @Test
+  public void testWhenPageSizeGraterThanRecordsSize() {
+    Paging paging = new Paging();
+    paging.updatePageSize(5);
+    paging.updateRecordsSize(3);
+
+    assertPaging(paging, 0, 0, 3);
+
+    paging.arrowDown();
+    assertPaging(paging, 1, 0, 3);
+
+    paging.arrowDown();
+    assertPaging(paging, 2, 0, 3);
+
+    paging.arrowDown();
+    assertPaging(paging, 2, 0, 3);
+
+    paging.arrowUp();
+    assertPaging(paging, 1, 0, 3);
+
+    paging.arrowUp();
+    assertPaging(paging, 0, 0, 3);
+
+    paging.arrowUp();
+    assertPaging(paging, 0, 0, 3);
+
+    paging.pageDown();
+    assertPaging(paging, 2, 0, 3);
+
+    paging.pageDown();
+    assertPaging(paging, 2, 0, 3);
+
+    paging.pageUp();
+    assertPaging(paging, 0, 0, 3);
+
+    paging.pageUp();
+    assertPaging(paging, 0, 0, 3);
+  }
+
+  @Test
+  public void testWhenPageSizeIsZero() {
+    Paging paging = new Paging();
+    paging.updatePageSize(0);
+    paging.updateRecordsSize(5);
+
+    assertPaging(paging, 0, 0, 0);
+
+    paging.arrowDown();
+    assertPaging(paging, 1, 0, 0);
+
+    paging.arrowUp();
+    assertPaging(paging, 0, 0, 0);
+
+    paging.pageDown();
+    assertPaging(paging, 0, 0, 0);
+
+    paging.pageUp();
+    assertPaging(paging, 0, 0, 0);
+  }
+
+  @Test
+  public void testWhenRecordsSizeIsZero() {
+    Paging paging = new Paging();
+    paging.updatePageSize(3);
+    paging.updateRecordsSize(0);
+
+    assertPaging(paging, 0, 0, 0);
+
+    paging.arrowDown();
+    assertPaging(paging, 0, 0, 0);
+
+    paging.arrowUp();
+    assertPaging(paging, 0, 0, 0);
+
+    paging.pageDown();
+    assertPaging(paging, 0, 0, 0);
+
+    paging.pageUp();
+    assertPaging(paging, 0, 0, 0);
+  }
+
+  @Test
+  public void testWhenChangingPageSizeDynamically() {
+    Paging paging = new Paging();
+    paging.updatePageSize(3);
+    paging.updateRecordsSize(5);
+
+    assertPaging(paging, 0, 0, 3);
+
+    paging.arrowDown();
+    assertPaging(paging, 1, 0, 3);
+
+    paging.updatePageSize(2);
+    assertPaging(paging, 1, 0, 2);
+
+    paging.arrowDown();
+    assertPaging(paging, 2, 1, 3);
+
+    paging.arrowDown();
+    assertPaging(paging, 3, 2, 4);
+
+    paging.updatePageSize(4);
+    assertPaging(paging, 3, 1, 5);
+
+    paging.updatePageSize(5);
+    assertPaging(paging, 3, 0, 5);
+
+    paging.updatePageSize(0);
+    assertPaging(paging, 3, 0, 0);
+
+    paging.arrowDown();
+    assertPaging(paging, 4, 0, 0);
+
+    paging.arrowUp();
+    assertPaging(paging, 3, 0, 0);
+
+    paging.pageDown();
+    assertPaging(paging, 3, 0, 0);
+
+    paging.pageUp();
+    assertPaging(paging, 3, 0, 0);
+
+    paging.updatePageSize(1);
+    assertPaging(paging, 3, 3, 4);
+  }
+
+  @Test
+  public void testWhenChangingRecordsSizeDynamically() {
+    Paging paging = new Paging();
+    paging.updatePageSize(3);
+    paging.updateRecordsSize(5);
+
+    assertPaging(paging, 0, 0, 3);
+
+    paging.updateRecordsSize(2);
+    assertPaging(paging, 0, 0, 2);
+    assertThat(paging.getCurrentPosition(), is(0));
+    assertThat(paging.getPageStartPosition(), is(0));
+    assertThat(paging.getPageEndPosition(), is(2));
+
+    paging.arrowDown();
+    assertPaging(paging, 1, 0, 2);
+
+    paging.updateRecordsSize(3);
+    assertPaging(paging, 1, 0, 3);
+
+    paging.arrowDown();
+    assertPaging(paging, 2, 0, 3);
+
+    paging.updateRecordsSize(1);
+    assertPaging(paging, 0, 0, 1);
+
+    paging.updateRecordsSize(0);
+    assertPaging(paging, 0, 0, 0);
+
+    paging.arrowDown();
+    assertPaging(paging, 0, 0, 0);
+
+    paging.arrowUp();
+    assertPaging(paging, 0, 0, 0);
+
+    paging.pageDown();
+    assertPaging(paging, 0, 0, 0);
+
+    paging.pageUp();
+    assertPaging(paging, 0, 0, 0);
+  }
+
+  private void assertPaging(Paging paging, int currentPosition, int pageStartPosition,
+    int pageEndPosition) {
+    assertThat(paging.getCurrentPosition(), is(currentPosition));
+    assertThat(paging.getPageStartPosition(), is(pageStartPosition));
+    assertThat(paging.getPageEndPosition(), is(pageEndPosition));
+  }
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/top/TestTopScreenModel.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/top/TestTopScreenModel.java
new file mode 100644
index 0000000..9dec535
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/top/TestTopScreenModel.java
@@ -0,0 +1,200 @@
+/**
+ * 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.hadoop.hbase.hbtop.screen.top;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.apache.hadoop.hbase.client.Admin;
+import org.apache.hadoop.hbase.hbtop.Record;
+import org.apache.hadoop.hbase.hbtop.RecordFilter;
+import org.apache.hadoop.hbase.hbtop.TestUtils;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.hbtop.field.FieldInfo;
+import org.apache.hadoop.hbase.hbtop.field.FieldValue;
+import org.apache.hadoop.hbase.hbtop.mode.Mode;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+@Category(SmallTests.class)
+@RunWith(MockitoJUnitRunner.class)
+public class TestTopScreenModel {
+
+  @Mock
+  private Admin admin;
+
+  private TopScreenModel topScreenModel;
+
+  private List<Field> fields;
+
+  @Before
+  public void setup() throws IOException {
+    when(admin.getClusterStatus()).thenReturn(TestUtils.createDummyClusterStatus());
+    topScreenModel = new TopScreenModel(admin, Mode.REGION);
+
+    fields = new ArrayList<>();
+    for (FieldInfo fieldInfo : Mode.REGION.getFieldInfos()) {
+      fields.add(fieldInfo.getField());
+    }
+  }
+
+  @Test
+  public void testSummary() {
+    topScreenModel.refreshMetricsData();
+    Summary summary = topScreenModel.getSummary();
+    TestUtils.assertSummary(summary);
+  }
+
+  @Test
+  public void testRecords() {
+    // Region Mode
+    topScreenModel.refreshMetricsData();
+    TestUtils.assertRecordsInRegionMode(topScreenModel.getRecords());
+
+    // Namespace Mode
+    topScreenModel.switchMode(Mode.NAMESPACE, null, false);
+    topScreenModel.refreshMetricsData();
+    TestUtils.assertRecordsInNamespaceMode(topScreenModel.getRecords());
+
+    // Table Mode
+    topScreenModel.switchMode(Mode.TABLE, null, false);
+    topScreenModel.refreshMetricsData();
+    TestUtils.assertRecordsInTableMode(topScreenModel.getRecords());
+
+    // Namespace Mode
+    topScreenModel.switchMode(Mode.REGION_SERVER, null, false);
+    topScreenModel.refreshMetricsData();
... 642 lines suppressed ...


[hbase] 01/02: HBASE-21947 TestShell is broken after we remove the jackson dependencies

Posted by ap...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

apurtell pushed a commit to branch branch-1
in repository https://gitbox.apache.org/repos/asf/hbase.git

commit 37e5e47faa750395852bfcf542ac7c77c04c2494
Author: zhangduo <zh...@apache.org>
AuthorDate: Mon Feb 25 21:32:14 2019 +0800

    HBASE-21947 TestShell is broken after we remove the jackson dependencies
    
    Signed-off-by: Andrew Purtell <ap...@apache.org>
---
 hbase-shell/src/main/ruby/hbase/taskmonitor.rb | 47 ++++++++++++++------------
 1 file changed, 26 insertions(+), 21 deletions(-)

diff --git a/hbase-shell/src/main/ruby/hbase/taskmonitor.rb b/hbase-shell/src/main/ruby/hbase/taskmonitor.rb
index 27aaa02..f52d13a 100644
--- a/hbase-shell/src/main/ruby/hbase/taskmonitor.rb
+++ b/hbase-shell/src/main/ruby/hbase/taskmonitor.rb
@@ -33,20 +33,21 @@ module Hbase
     # Represents information reported by a server on a single MonitoredTask
     class Task
 
-      def initialize(taskMap,host)
-
-        taskMap.each_pair do |k,v|
+      def initialize(taskMap, host)
+        taskMap.entrySet.each do |entry|
+          k = entry.getKey
+          v = entry.getValue
           case k
-            when "statustimems"
-              @statustime = Time.at(v/1000)
-            when "status"
-              @status = v
-            when "starttimems"
-              @starttime = Time.at(v/1000)
-            when "description"
-              @description = v
-            when "state"
-              @state = v
+          when 'statustimems'
+            @statustime = Time.at(v.getAsLong / 1000)
+          when 'status'
+            @status = v.getAsString
+          when 'starttimems'
+            @starttime = Time.at(v.getAsLong / 1000)
+          when 'description'
+            @description = v.getAsString
+          when 'state'
+            @state = v.getAsString
           end
         end
 
@@ -82,7 +83,8 @@ module Hbase
     def tasksOnHost(filter,host)
 
       java_import 'java.net.URL'
-      java_import 'com.fasterxml.jackson.databind.ObjectMapper'
+      java_import 'java.io.InputStreamReader'
+      java_import 'org.apache.hbase.thirdparty.com.google.gson.JsonParser'
 
       infoport = @admin.getClusterStatus().getLoad(host).getInfoServerPort().to_s
 
@@ -96,16 +98,19 @@ module Hbase
       schema = "http://"
       url = schema + host.hostname + ":" + infoport + "/rs-status?format=json&filter=" + filter
 
-      json = URL.new(url)
-      mapper = ObjectMapper.new
+      json = URL.new(url).openStream
+      parser = JsonParser.new
 
       # read and parse JSON
-      tasksArrayList = mapper.readValue(json,java.lang.Object.java_class)
-
+      begin
+        tasks_array_list = parser.parse(InputStreamReader.new(json, 'UTF-8')).getAsJsonArray
+      ensure
+        json.close
+      end
       # convert to an array of TaskMonitor::Task instances
-      tasks = Array.new
-      tasksArrayList.each do |t|
-        tasks.unshift Task.new(t,host)
+      tasks = []
+      tasks_array_list.each do |t|
+        tasks.unshift Task.new(t.getAsJsonObject, host)
       end
 
       return tasks