Repository: Alluxio/alluxio (language: java)

Issue:
Need a master page show masters informations like worker page
**Is your feature request related to a problem? Please describe.**

Now, we have no master page, user cannot get masters state by webui, when a master lost, we cannot know the lost time.

**Describe the solution you'd like**

We can get masters info from the following objects within DefaultMetaMaster.java
```java
  private final IndexedSet<MasterInfo> mMasters =
      new IndexedSet<>(ID_INDEX, ADDRESS_INDEX);
  private final IndexedSet<MasterInfo> mLostMasters =
      new IndexedSet<>(ID_INDEX, ADDRESS_INDEX);
```

We can display the MasterInfo like the following part.

```java
public final class MasterInfo {
  /** Master's address. */
  private final Address mAddress;
  /** The id of the master. */
  private final long mId;
  /** Master's last updated time in ms. */
  private long mLastUpdatedTimeMs;
```

**Describe alternatives you've considered**

Add a master page like worker page

<img width="2266" alt="wecom-temp-20279f43a77ad2bfcb027b359898a751" src="https://user-images.githubusercontent.com/17329931/198506182-3efabd18-95be-4c52-9299-8dda9e3fdad7.png">


**Urgency**

Normal


**Additional context**

No



Discussion / hints:
@lucaspeng12138 Would you like to take this tickets?
> @lucaspeng12138 Would you like to take this tickets?

Yes, I will work on this issue!

Fix patch:
diff --git a/core/common/src/main/java/alluxio/util/webui/UIMasterInfo.java b/core/common/src/main/java/alluxio/util/webui/UIMasterInfo.java
new file mode 100644
index 000000000000..e391b4a3b53e
--- /dev/null
+++ b/core/common/src/main/java/alluxio/util/webui/UIMasterInfo.java
@@ -0,0 +1,63 @@
+/*
+ * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0
+ * (the "License"). You may not use this work except in compliance with the License, which is
+ * available at www.apache.org/licenses/LICENSE-2.0
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied, as more fully set forth in the License.
+ *
+ * See the NOTICE file distributed with this work for information regarding copyright ownership.
+ */
+
+package alluxio.util.webui;
+
+import alluxio.util.CommonUtils;
+
+/**
+ * Displays information about a master in the UI.
+ */
+public class UIMasterInfo {
+  private final String mMasterAddress;
+  private final long mId;
+  private final long mLastUpdatedTimeMs;
+
+  /**
+   * Creates a new instance of {@link UIMasterInfo}.
+   *
+   * @param masterAddress The master address
+   * @param id The master id
+   * @param lastUpdatedTimeMs The last heart beat in ms
+   */
+  public UIMasterInfo(String masterAddress, long id, long lastUpdatedTimeMs) {
+    mMasterAddress = masterAddress;
+    mId = id;
+    mLastUpdatedTimeMs = lastUpdatedTimeMs;
+  }
+
+  /**
+   * Gets master address.
+   *
+   * @return the master address
+   */
+  public String getAddress() {
+    return mMasterAddress;
+  }
+
+  /**
+   * Get id.
+   *
+   * @return the id
+   */
+  public String getId() {
+    return Long.toString(mId);
+  }
+
+  /**
+   * Get master last update time.
+   *
+   * @return the master last update time
+   */
+  public String getLastUpdatedTime() {
+    return CommonUtils.convertMsToClockTime(mLastUpdatedTimeMs);
+  }
+}
diff --git a/core/common/src/main/java/alluxio/wire/MasterInfo.java b/core/common/src/main/java/alluxio/wire/MasterInfo.java
new file mode 100644
index 000000000000..c3df43fe5d09
--- /dev/null
+++ b/core/common/src/main/java/alluxio/wire/MasterInfo.java
@@ -0,0 +1,143 @@
+/*
+ * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0
+ * (the "License"). You may not use this work except in compliance with the License, which is
+ * available at www.apache.org/licenses/LICENSE-2.0
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied, as more fully set forth in the License.
+ *
+ * See the NOTICE file distributed with this work for information regarding copyright ownership.
+ */
+
+package alluxio.wire;
+
+import alluxio.util.CommonUtils;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+/**
+ * Master information.
+ */
+@NotThreadSafe
+public final class MasterInfo {
+  /** Master's address. */
+  private Address mAddress;
+  /** The id of the master. */
+  private long mId;
+  /** Master's last updated time in ms. */
+  private long mLastUpdatedTimeMs;
+
+  /**
+   * Creates a new instance of {@link MasterInfo}.
+   */
+  public MasterInfo() {}
+
+  /**
+   * Creates a new instance of {@link MasterInfo}.
+   *
+   * @param id the master id to use
+   * @param address the master address to use
+   * @param lastUpdatedTimeMs the master lastUpdatedTimeMs to use
+   */
+  public MasterInfo(long id, Address address, long lastUpdatedTimeMs) {
+    mAddress = Preconditions.checkNotNull(address, "address");
+    mId = id;
+    mLastUpdatedTimeMs = lastUpdatedTimeMs;
+  }
+
+  /**
+   * Creates a new instance of {@link MasterInfo}.
+   *
+   * @param id the master id to use
+   * @param address the master address to use
+   */
+  public MasterInfo(long id, Address address) {
+    mAddress = Preconditions.checkNotNull(address, "address");
+    mId = id;
+    mLastUpdatedTimeMs = System.currentTimeMillis();
+  }
+
+  /**
+   * @return the master's address
+   */
+  public Address getAddress() {
+    return mAddress;
+  }
+
+  /**
+   * @return the id of the master
+   */
+  public long getId() {
+    return mId;
+  }
+
+  /**
+   * @return the last updated time of the master in ms
+   */
+  public long getLastUpdatedTimeMs() {
+    return mLastUpdatedTimeMs;
+  }
+
+  /**
+   * @param address the master address information
+   * @return the master information
+   */
+  public MasterInfo setAddress(Address address) {
+    mAddress = address;
+    return this;
+  }
+
+  /**
+   * @param id the master id
+   * @return the master information
+   */
+  public MasterInfo setId(long id) {
+    mId = id;
+    return this;
+  }
+
+  /**
+   * @param lastUpdatedTimeMs the last update time in ms
+   * @return the master information
+   */
+  public MasterInfo setLastUpdatedTimeMs(long lastUpdatedTimeMs) {
+    mLastUpdatedTimeMs = lastUpdatedTimeMs;
+    return this;
+  }
+
+  @Override
+  public String toString() {
+    return MoreObjects.toStringHelper(this).add("id", mId).add("address", mAddress)
+        .add("lastUpdatedTime", CommonUtils.convertMsToClockTime(mLastUpdatedTimeMs))
+        .toString();
+  }
+
+  /**
+   * Updates the last updated time of the master (in milliseconds).
+   */
+  public void updateLastUpdatedTimeMs() {
+    mLastUpdatedTimeMs = System.currentTimeMillis();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof MasterInfo)) {
+      return false;
+    }
+    MasterInfo that = (MasterInfo) o;
+    return mId == that.mId && Objects.equal(mAddress, that.mAddress)
+            && mLastUpdatedTimeMs == that.mLastUpdatedTimeMs;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(mId, mAddress, mLastUpdatedTimeMs);
+  }
+}
diff --git a/core/common/src/main/java/alluxio/wire/MasterWebUIMasters.java b/core/common/src/main/java/alluxio/wire/MasterWebUIMasters.java
new file mode 100644
index 000000000000..fac2ada17a25
--- /dev/null
+++ b/core/common/src/main/java/alluxio/wire/MasterWebUIMasters.java
@@ -0,0 +1,124 @@
+/*
+ * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0
+ * (the "License"). You may not use this work except in compliance with the License, which is
+ * available at www.apache.org/licenses/LICENSE-2.0
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied, as more fully set forth in the License.
+ *
+ * See the NOTICE file distributed with this work for information regarding copyright ownership.
+ */
+
+package alluxio.wire;
+
+import com.google.common.base.MoreObjects;
+
+import java.io.Serializable;
+import javax.annotation.concurrent.NotThreadSafe;
+
+/**
+ * Alluxio WebUI masters information.
+ */
+@NotThreadSafe
+public final class MasterWebUIMasters implements Serializable {
+  private static final long serialVersionUID = -2709466215687255197L;
+
+  private boolean mDebug;
+  private MasterInfo[] mFailedMasterInfos;
+  private MasterInfo[] mNormalMasterInfos;
+  private MasterInfo mLeaderMasterInfo;
+
+  /**
+   * Creates a new instance of {@link MasterWebUIMasters}.
+   */
+  public MasterWebUIMasters() {
+  }
+
+  /**
+   * Gets debug.
+   *
+   * @return the debug
+   */
+  public boolean getDebug() {
+    return mDebug;
+  }
+
+  /**
+   * Get failed master infos master info [ ].
+   *
+   * @return the master info [ ]
+   */
+  public MasterInfo[] getFailedMasterInfos() {
+    return mFailedMasterInfos;
+  }
+
+  /**
+   * Get leader master info master info.
+   *
+   * @return the master info
+   */
+  public MasterInfo[] getNormalMasterInfos() {
+    return mNormalMasterInfos;
+  }
+
+  /**
+   * Get normal master infos master info [ ].
+   *
+   * @return the master info [ ]
+   */
+  public MasterInfo getLeaderMasterInfo() {
+    return mLeaderMasterInfo;
+  }
+
+  /**
+   * Sets debug.
+   *
+   * @param debug the debug
+   * @return the debug master infos
+   */
+  public MasterWebUIMasters setDebug(boolean debug) {
+    mDebug = debug;
+    return this;
+  }
+
+  /**
+   * Sets failed master infos.
+   *
+   * @param failedMasterInfos the failed master infos
+   * @return the failed master infos
+   */
+  public MasterWebUIMasters setFailedMasterInfos(MasterInfo[] failedMasterInfos) {
+    mFailedMasterInfos = failedMasterInfos.clone();
+    return this;
+  }
+
+  /**
+   * Sets normal master infos.
+   *
+   * @param normalMasterInfos the normal master infos
+   * @return the normal master infos
+   */
+  public MasterWebUIMasters setNormalMasterInfos(MasterInfo[] normalMasterInfos) {
+    mNormalMasterInfos = normalMasterInfos.clone();
+    return this;
+  }
+
+  /**
+   * Sets leader master info.
+   *
+   * @param leaderMasterInfo the normal master info
+   * @return the leader master info
+   */
+  public MasterWebUIMasters setLeaderMasterInfo(MasterInfo leaderMasterInfo) {
+    mLeaderMasterInfo = leaderMasterInfo;
+    return this;
+  }
+
+  @Override
+  public String toString() {
+    return MoreObjects.toStringHelper(this).add("debug", mDebug)
+        .add("failedMasterInfos", mFailedMasterInfos)
+        .add("normalMasterInfos", mNormalMasterInfos)
+        .add("leaderMasterInfo", mLeaderMasterInfo).toString();
+  }
+}
diff --git a/core/server/master/src/main/java/alluxio/master/meta/AlluxioMasterRestServiceHandler.java b/core/server/master/src/main/java/alluxio/master/meta/AlluxioMasterRestServiceHandler.java
index 369c6adfa642..c818f58314df 100644
--- a/core/server/master/src/main/java/alluxio/master/meta/AlluxioMasterRestServiceHandler.java
+++ b/core/server/master/src/main/java/alluxio/master/meta/AlluxioMasterRestServiceHandler.java
@@ -57,17 +57,20 @@
 import alluxio.util.webui.UIFileInfo;
 import alluxio.util.webui.WebUtils;
 import alluxio.web.MasterWebServer;
+import alluxio.wire.Address;
 import alluxio.wire.AlluxioMasterInfo;
 import alluxio.wire.BlockLocation;
 import alluxio.wire.Capacity;
 import alluxio.wire.ConfigCheckReport;
 import alluxio.wire.FileBlockInfo;
 import alluxio.wire.FileInfo;
+import alluxio.wire.MasterInfo;
 import alluxio.wire.MasterWebUIBrowse;
 import alluxio.wire.MasterWebUIConfiguration;
 import alluxio.wire.MasterWebUIData;
 import alluxio.wire.MasterWebUIInit;
 import alluxio.wire.MasterWebUILogs;
+import alluxio.wire.MasterWebUIMasters;
 import alluxio.wire.MasterWebUIMetrics;
 import alluxio.wire.MasterWebUIMountTable;
 import alluxio.wire.MasterWebUIOverview;
@@ -95,6 +98,7 @@
 import java.io.FilenameFilter;
 import java.io.IOException;
 import java.io.InputStream;
+import java.net.InetSocketAddress;
 import java.net.URLDecoder;
 import java.time.Instant;
 import java.time.ZoneOffset;
@@ -146,6 +150,7 @@ public final class AlluxioMasterRestServiceHandler {
   public static final String WEBUI_WORKERS = "webui_workers";
   public static final String WEBUI_METRICS = "webui_metrics";
   public static final String WEBUI_MOUNTTABLE = "webui_mounttable";
+  public static final String WEBUI_MASTERS = "webui_masters";
 
   // queries
   public static final String QUERY_RAW_CONFIGURATION = "raw_configuration";
@@ -161,6 +166,8 @@ public final class AlluxioMasterRestServiceHandler {
   private final MetaMaster mMetaMaster;
   private final FileSystem mFsClient;
 
+  private static final int MASTER_ID_NULL = -1;
+
   /**
    * Constructs a new {@link AlluxioMasterRestServiceHandler}.
    *
@@ -848,6 +855,34 @@ public Response getWebUIWorkers() {
     }, Configuration.global());
   }
 
+  /**
+   * Gets Web UI Master page data.
+   *
+   * @return the response object
+   */
+  @GET
+  @Path(WEBUI_MASTERS)
+  public Response getWebUIMasters() {
+    return RestUtils.call(() -> {
+      MasterWebUIMasters response = new MasterWebUIMasters();
+
+      response.setDebug(Configuration.getBoolean(PropertyKey.DEBUG));
+
+      MasterInfo[] failedMasterInfos = mMetaMaster.getLostMasterInfos();
+      response.setFailedMasterInfos(failedMasterInfos);
+
+      MasterInfo[] normalMasterInfos = mMetaMaster.getMasterInfos();
+      response.setNormalMasterInfos(normalMasterInfos);
+
+      InetSocketAddress leaderMasterAddress = mMasterProcess.getRpcAddress();
+      MasterInfo leaderMasterInfo = new MasterInfo(MASTER_ID_NULL,
+              new Address(leaderMasterAddress.getHostString(), leaderMasterAddress.getPort()),
+              System.currentTimeMillis());
+      response.setLeaderMasterInfo(leaderMasterInfo);
+      return response;
+    }, Configuration.global());
+  }
+
   /**
    * Gets Web UI mount table page data.
    *
diff --git a/core/server/master/src/main/java/alluxio/master/meta/DefaultMetaMaster.java b/core/server/master/src/main/java/alluxio/master/meta/DefaultMetaMaster.java
index d167e9926989..f44d773f816d 100644
--- a/core/server/master/src/main/java/alluxio/master/meta/DefaultMetaMaster.java
+++ b/core/server/master/src/main/java/alluxio/master/meta/DefaultMetaMaster.java
@@ -487,6 +487,30 @@ public List<Address> getWorkerAddresses() {
     return mWorkerConfigStore.getLiveNodeAddresses();
   }
 
+  @Override
+  public alluxio.wire.MasterInfo[] getMasterInfos() {
+    alluxio.wire.MasterInfo[] masterInfos = new alluxio.wire.MasterInfo[mMasters.size()];
+    int indexNum = 0;
+    for (MasterInfo master : mMasters) {
+      masterInfos[indexNum] = new alluxio.wire.MasterInfo(master.getId(),
+          master.getAddress(), master.getLastUpdatedTimeMs());
+      indexNum++;
+    }
+    return masterInfos;
+  }
+
+  @Override
+  public alluxio.wire.MasterInfo[] getLostMasterInfos() {
+    alluxio.wire.MasterInfo[] masterInfos = new alluxio.wire.MasterInfo[mLostMasters.size()];
+    int indexNum = 0;
+    for (MasterInfo master : mLostMasters) {
+      masterInfos[indexNum] = new alluxio.wire.MasterInfo(master.getId(),
+          master.getAddress(), master.getLastUpdatedTimeMs());
+      indexNum++;
+    }
+    return masterInfos;
+  }
+
   @Override
   public long getMasterId(Address address) {
     MasterInfo existingMaster = mMasters.getFirstByField(ADDRESS_INDEX, address);
diff --git a/core/server/master/src/main/java/alluxio/master/meta/MetaMaster.java b/core/server/master/src/main/java/alluxio/master/meta/MetaMaster.java
index c59b4ca3e7f4..3038e7830ca4 100644
--- a/core/server/master/src/main/java/alluxio/master/meta/MetaMaster.java
+++ b/core/server/master/src/main/java/alluxio/master/meta/MetaMaster.java
@@ -23,6 +23,7 @@
 import alluxio.wire.ConfigCheckReport;
 import alluxio.wire.ConfigHash;
 import alluxio.wire.Configuration;
+import alluxio.wire.MasterInfo;
 
 import java.io.IOException;
 import java.net.InetSocketAddress;
@@ -131,6 +132,16 @@ void setPathConfiguration(String path, Map<PropertyKey, String> properties)
    */
   int getWebPort();
 
+  /**
+   * @return a array of {@link MasterInfo}s of masters
+   */
+  MasterInfo[] getMasterInfos();
+
+  /**
+   * @return a array of {@link MasterInfo}s of lost masters
+   */
+  MasterInfo[] getLostMasterInfos();
+
   /**
    * @return the addresses of live workers
    */
diff --git a/webui/master/src/constants/data/footerNavigation.tsx b/webui/master/src/constants/data/footerNavigation.tsx
index 16e83cae6d0b..4a86d6d20615 100644
--- a/webui/master/src/constants/data/footerNavigation.tsx
+++ b/webui/master/src/constants/data/footerNavigation.tsx
@@ -23,6 +23,10 @@ export const footerNavigationData: INavigationData[] = [
     innerText: 'User Mailing List',
     url: 'https://groups.google.com/forum/#!forum/alluxio-users',
   },
+  {
+    innerText: 'Masters',
+    url: routePaths.masters,
+  },
   {
     innerText: 'Workers',
     url: routePaths.workers,
diff --git a/webui/master/src/constants/data/headerNavigation.tsx b/webui/master/src/constants/data/headerNavigation.tsx
index 7ff49291687b..2b3b61dcb0e9 100644
--- a/webui/master/src/constants/data/headerNavigation.tsx
+++ b/webui/master/src/constants/data/headerNavigation.tsx
@@ -33,6 +33,10 @@ export const headerNavigationData: INavigationData[] = [
     innerText: 'Configuration',
     url: routePaths.config,
   },
+  {
+    innerText: 'Masters',
+    url: routePaths.masters,
+  },
   {
     innerText: 'Workers',
     url: routePaths.workers,
diff --git a/webui/master/src/constants/data/routePaths.tsx b/webui/master/src/constants/data/routePaths.tsx
index d26d3973f4b1..8f960e599524 100644
--- a/webui/master/src/constants/data/routePaths.tsx
+++ b/webui/master/src/constants/data/routePaths.tsx
@@ -20,4 +20,5 @@ export const routePaths = {
   workers: '/workers',
   stacks: '/stacks',
   mounttable: '/mounttable',
+  masters: '/masters',
 };
diff --git a/webui/master/src/constants/index.tsx b/webui/master/src/constants/index.tsx
index b8070089bbe3..6e5a834d6134 100644
--- a/webui/master/src/constants/index.tsx
+++ b/webui/master/src/constants/index.tsx
@@ -14,6 +14,7 @@ export * from './data/footerNavigation';
 export * from './data/routePaths';
 
 export * from './types/INodeInfo';
+export * from './types/IMasterInfo';
 export * from './types/IScopedPropertyInfo';
 export * from './types/IStorageTierInfo';
 export * from '@alluxio/common-ui/src/constants/types/IConfigTriple';
diff --git a/webui/master/src/constants/types/IMasterInfo.tsx b/webui/master/src/constants/types/IMasterInfo.tsx
new file mode 100644
index 000000000000..001b1ecdcf56
--- /dev/null
+++ b/webui/master/src/constants/types/IMasterInfo.tsx
@@ -0,0 +1,19 @@
+/*
+ * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0
+ * (the "License"). You may not use this work except in compliance with the License, which is
+ * available at www.apache.org/licenses/LICENSE-2.0
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied, as more fully set forth in the License.
+ *
+ * See the NOTICE file distributed with this work for information regarding copyright ownership.
+ */
+
+export interface IMasterInfo {
+  address: {
+    rpcPort: number;
+    host: string;
+  };
+  id: string;
+  lastUpdatedTimeMs: number;
+}
diff --git a/webui/master/src/containers/App/App.tsx b/webui/master/src/containers/App/App.tsx
index c2e60fc9fee8..161bfc8fdeb5 100644
--- a/webui/master/src/containers/App/App.tsx
+++ b/webui/master/src/containers/App/App.tsx
@@ -24,7 +24,18 @@ import {
   SlackButton,
 } from '@alluxio/common-ui/src/components';
 import { triggerRefresh } from '@alluxio/common-ui/src/store/refresh/actions';
-import { Browse, MasterConfiguration, Data, MasterLogs, Metrics, Overview, Workers, Stacks, MountTable } from '..';
+import {
+  Browse,
+  MasterConfiguration,
+  Data,
+  MasterLogs,
+  Metrics,
+  Overview,
+  Workers,
+  Stacks,
+  MountTable,
+  Masters,
+} from '..';
 import { footerNavigationData, headerNavigationData, routePaths } from '../../constants';
 import { IApplicationState } from '../../store';
 import { fetchRequest } from '../../store/init/actions';
@@ -84,6 +95,7 @@ export class App extends React.Component<AllProps> {
               <Route path={routePaths.data} exact={true} render={this.renderView(Data)} />
               <Route path={routePaths.logs} exact={true} render={this.renderView(MasterLogs, { history })} />
               <Route path={routePaths.metrics} exact={true} render={this.renderView(Metrics)} />
+              <Route path={routePaths.masters} exact={true} render={this.renderView(Masters)} />
               <Route path={routePaths.workers} exact={true} render={this.renderView(Workers)} />
               <Route path={routePaths.stacks} exact={true} render={this.renderView(Stacks)} />
               <Route path={routePaths.mounttable} exact={true} render={this.renderView(MountTable)} />
diff --git a/webui/master/src/containers/index.tsx b/webui/master/src/containers/index.tsx
index 711a2c5d179a..c6e0925b2dc9 100644
--- a/webui/master/src/containers/index.tsx
+++ b/webui/master/src/containers/index.tsx
@@ -16,6 +16,7 @@ export { default as MasterConfiguration } from './pages/MasterConfiguration/Mast
 export { default as Data } from './pages/Data/Data';
 export { default as Overview } from './pages/Overview/Overview';
 export { default as MasterLogs } from './pages/MasterLogs/MasterLogs';
+export { default as Masters } from './pages/Masters/Masters';
 export { default as Workers } from './pages/Workers/Workers';
 export { default as Metrics } from './pages/Metrics/Metrics';
 export { default as Stacks } from './pages/MasterStacks/MasterStacks';
diff --git a/webui/master/src/containers/pages/Masters/Masters.tsx b/webui/master/src/containers/pages/Masters/Masters.tsx
new file mode 100644
index 000000000000..21f5bd3420b3
--- /dev/null
+++ b/webui/master/src/containers/pages/Masters/Masters.tsx
@@ -0,0 +1,140 @@
+/*
+ * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0
+ * (the "License"). You may not use this work except in compliance with the License, which is
+ * available at www.apache.org/licenses/LICENSE-2.0
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied, as more fully set forth in the License.
+ *
+ * See the NOTICE file distributed with this work for information regarding copyright ownership.
+ */
+
+import React from 'react';
+import { connect } from 'react-redux';
+import { Table } from 'reactstrap';
+import { AnyAction, compose, Dispatch } from 'redux';
+
+import { withErrors, withLoadingMessage, withFetchData } from '@alluxio/common-ui/src/components';
+import { IMasterInfo } from '../../../constants';
+import { IApplicationState } from '../../../store';
+import { fetchRequest } from '../../../store/masters/actions';
+import { IMasters } from '../../../store/masters/types';
+import { IInit } from '../../../store/init/types';
+import { createAlertErrors } from '@alluxio/common-ui/src/utilities';
+import { ICommonState } from '@alluxio/common-ui/src/constants';
+
+interface IPropsFromState extends ICommonState {
+  initData: IInit;
+  mastersData: IMasters;
+}
+
+interface IPropsFromDispatch {
+  fetchRequest: typeof fetchRequest;
+}
+
+export type AllProps = IPropsFromState & IPropsFromDispatch;
+
+export class MastersPresenter extends React.Component<AllProps> {
+  public render(): JSX.Element {
+    const { initData, mastersData } = this.props;
+
+    return (
+      <div className="masters-page">
+        <div className="container-fluid">
+          <div className="row">
+            <div className="col-12">
+              <h5>Leader Master</h5>
+              <Table hover={true}>
+                <thead>
+                  <tr>
+                    <th>Master Host</th>
+                    <th>Master Port</th>
+                  </tr>
+                </thead>
+                <tbody>
+                  <tr>
+                    <td>{mastersData.leaderMasterInfo.address.host}</td>
+                    <td>{mastersData.leaderMasterInfo.address.rpcPort}</td>
+                  </tr>
+                </tbody>
+              </Table>
+            </div>
+          </div>
+          <div className="row">
+            <div className="col-12">
+              <h5>Standby Masters</h5>
+              <Table hover={true}>
+                <thead>
+                  <tr>
+                    {initData.debug ? <th>[D]Master Id</th> : null}
+                    <th>Master Host</th>
+                    <th>Master Port</th>
+                    <th>Last Heartbeat</th>
+                  </tr>
+                </thead>
+                <tbody>
+                  {mastersData.normalMasterInfos.map((masterInfo: IMasterInfo) => (
+                    <tr key={masterInfo.id}>
+                      {initData.debug ? <td>{masterInfo.id}</td> : null}
+                      <td>{masterInfo.address.host}</td>
+                      <td>{masterInfo.address.rpcPort}</td>
+                      <td>{new Date(masterInfo.lastUpdatedTimeMs).toTimeString()}</td>
+                    </tr>
+                  ))}
+                </tbody>
+              </Table>
+            </div>
+          </div>
+          <div className="row">
+            <div className="col-12">
+              <h5>Lost Masters</h5>
+              <Table hover={true}>
+                <thead>
+                  <tr>
+                    {initData.debug ? <th>[D]Master Id</th> : null}
+                    <th>Master Host</th>
+                    <th>Master Port</th>
+                    <th>Last Heartbeat</th>
+                  </tr>
+                </thead>
+                <tbody>
+                  {mastersData.failedMasterInfos.map((masterInfo: IMasterInfo) => (
+                    <tr key={masterInfo.id}>
+                      {initData.debug ? <td>{masterInfo.id}</td> : null}
+                      <td>{masterInfo.address.host}</td>
+                      <td>{masterInfo.address.rpcPort}</td>
+                      <td>{new Date(masterInfo.lastUpdatedTimeMs).toTimeString()}</td>
+                    </tr>
+                  ))}
+                </tbody>
+              </Table>
+            </div>
+          </div>
+        </div>
+      </div>
+    );
+  }
+}
+
+const mapStateToProps = ({ init, refresh, masters }: IApplicationState): IPropsFromState => ({
+  initData: init.data,
+  errors: createAlertErrors(init.errors !== undefined || masters.errors !== undefined),
+  loading: init.loading || masters.loading,
+  refresh: refresh.data,
+  mastersData: masters.data,
+  class: 'masters-page',
+});
+
+const mapDispatchToProps = (dispatch: Dispatch): { fetchRequest: () => AnyAction } => ({
+  fetchRequest: (): AnyAction => dispatch(fetchRequest()),
+});
+
+export default compose(
+  connect(
+    mapStateToProps,
+    mapDispatchToProps,
+  ),
+  withFetchData,
+  withErrors,
+  withLoadingMessage,
+)(MastersPresenter) as typeof React.Component;
diff --git a/webui/master/src/store/index.tsx b/webui/master/src/store/index.tsx
index 0c36052db956..90f6de5fd9e8 100644
--- a/webui/master/src/store/index.tsx
+++ b/webui/master/src/store/index.tsx
@@ -46,6 +46,9 @@ import { mountTableSaga } from './mounttable/sagas';
 import { stackSaga } from '@alluxio/common-ui/src/store/stacks/sagas';
 import { IStacksState } from '@alluxio/common-ui/src/store/stacks/types';
 import { initialStacksState, stacksReducer } from '@alluxio/common-ui/src/store/stacks/reducer';
+import { initialMastersState, mastersReducer } from './masters/reducer';
+import { mastersSaga } from './masters/sagas';
+import { IMastersState } from './masters/types';
 
 export interface IApplicationState {
   browse: IBrowseState;
@@ -57,6 +60,7 @@ export interface IApplicationState {
   overview: IOverviewState;
   refresh: IRefreshState;
   router?: RouterState;
+  masters: IMastersState;
   workers: IWorkersState;
   stacks: IStacksState;
   mountTable: IMountTableState;
@@ -74,6 +78,7 @@ export const rootReducer = (history: History): Reducer<IApplicationState> =>
     refresh: refreshReducer,
     router: connectRouter(history),
     stacks: stacksReducer,
+    masters: mastersReducer,
     workers: workersReducer,
     mountTable: mountTableReducer,
   });
@@ -88,6 +93,7 @@ export const rootSaga = function*(): IterableIterator<AllEffect<ForkEffect>> {
     fork(metricsSaga),
     fork(overviewSaga),
     fork(stackSaga),
+    fork(mastersSaga),
     fork(workersSaga),
     fork(mountTableSaga),
   ]);
@@ -103,6 +109,7 @@ export const initialState: IApplicationState = {
   overview: initialOverviewState,
   refresh: initialRefreshState,
   stacks: initialStacksState,
+  masters: initialMastersState,
   workers: initialWorkersState,
   mountTable: initialMountTableState,
 };
diff --git a/webui/master/src/store/masters/actions.tsx b/webui/master/src/store/masters/actions.tsx
new file mode 100644
index 000000000000..610d4b496c70
--- /dev/null
+++ b/webui/master/src/store/masters/actions.tsx
@@ -0,0 +1,20 @@
+/*
+ * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0
+ * (the "License"). You may not use this work except in compliance with the License, which is
+ * available at www.apache.org/licenses/LICENSE-2.0
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied, as more fully set forth in the License.
+ *
+ * See the NOTICE file distributed with this work for information regarding copyright ownership.
+ */
+
+import { AxiosResponse } from 'axios';
+import { action } from 'typesafe-actions';
+
+import { MastersActionTypes } from './types';
+import { AnyAction } from 'redux';
+
+export const fetchRequest = (): AnyAction => action(MastersActionTypes.FETCH_REQUEST);
+export const fetchSuccess = (response: AxiosResponse): AnyAction => action(MastersActionTypes.FETCH_SUCCESS, response);
+export const fetchError = (message: string): AnyAction => action(MastersActionTypes.FETCH_ERROR, message);
diff --git a/webui/master/src/store/masters/reducer.tsx b/webui/master/src/store/masters/reducer.tsx
new file mode 100644
index 000000000000..7bf5282324a6
--- /dev/null
+++ b/webui/master/src/store/masters/reducer.tsx
@@ -0,0 +1,45 @@
+/*
+ * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0
+ * (the "License"). You may not use this work except in compliance with the License, which is
+ * available at www.apache.org/licenses/LICENSE-2.0
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied, as more fully set forth in the License.
+ *
+ * See the NOTICE file distributed with this work for information regarding copyright ownership.
+ */
+
+import { Reducer } from 'redux';
+
+import { IMastersState, MastersActionTypes } from './types';
+
+export const initialMastersState: IMastersState = {
+  data: {
+    debug: false,
+    failedMasterInfos: [],
+    normalMasterInfos: [],
+    leaderMasterInfo: {
+      address: {
+        rpcPort: 0,
+        host: '',
+      },
+      id: '',
+      lastUpdatedTimeMs: 0,
+    },
+  },
+  errors: undefined,
+  loading: false,
+};
+
+export const mastersReducer: Reducer<IMastersState> = (state = initialMastersState, action) => {
+  switch (action.type) {
+    case MastersActionTypes.FETCH_REQUEST:
+      return { ...state, loading: true };
+    case MastersActionTypes.FETCH_SUCCESS:
+      return { ...state, loading: false, data: action.payload.data, response: action.payload, errors: undefined };
+    case MastersActionTypes.FETCH_ERROR:
+      return { ...state, loading: false, errors: action.payload };
+    default:
+      return state;
+  }
+};
diff --git a/webui/master/src/store/masters/sagas.tsx b/webui/master/src/store/masters/sagas.tsx
new file mode 100644
index 000000000000..c97c2a62bbcd
--- /dev/null
+++ b/webui/master/src/store/masters/sagas.tsx
@@ -0,0 +1,27 @@
+/*
+ * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0
+ * (the "License"). You may not use this work except in compliance with the License, which is
+ * available at www.apache.org/licenses/LICENSE-2.0
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied, as more fully set forth in the License.
+ *
+ * See the NOTICE file distributed with this work for information regarding copyright ownership.
+ */
+
+import axios from 'axios';
+import { all, AllEffect, fork, ForkEffect, takeLatest } from 'redux-saga/effects';
+
+import { getSagaRequest } from '@alluxio/common-ui/src/utilities';
+import { fetchError, fetchSuccess } from './actions';
+import { MastersActionTypes } from './types';
+
+const API_ENDPOINT = `${process.env.REACT_APP_API_ROOT}/webui_masters`;
+
+const watchRequest = function*(): IterableIterator<ForkEffect> {
+  yield takeLatest(MastersActionTypes.FETCH_REQUEST, getSagaRequest(axios.get, API_ENDPOINT, fetchSuccess, fetchError));
+};
+
+export const mastersSaga = function*(): IterableIterator<AllEffect<ForkEffect>> {
+  yield all([fork(watchRequest)]);
+};
diff --git a/webui/master/src/store/masters/types.tsx b/webui/master/src/store/masters/types.tsx
new file mode 100644
index 000000000000..a9e6d672784d
--- /dev/null
+++ b/webui/master/src/store/masters/types.tsx
@@ -0,0 +1,34 @@
+/*
+ * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0
+ * (the "License"). You may not use this work except in compliance with the License, which is
+ * available at www.apache.org/licenses/LICENSE-2.0
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied, as more fully set forth in the License.
+ *
+ * See the NOTICE file distributed with this work for information regarding copyright ownership.
+ */
+
+import { AxiosResponse } from 'axios';
+
+import { IMasterInfo } from '../../constants';
+
+export interface IMasters {
+  debug: boolean;
+  failedMasterInfos: IMasterInfo[];
+  normalMasterInfos: IMasterInfo[];
+  leaderMasterInfo: IMasterInfo;
+}
+
+export enum MastersActionTypes {
+  FETCH_REQUEST = '@@masters/FETCH_REQUEST',
+  FETCH_SUCCESS = '@@masters/FETCH_SUCCESS',
+  FETCH_ERROR = '@@masters/FETCH_ERROR',
+}
+
+export interface IMastersState {
+  readonly data: IMasters;
+  readonly errors?: AxiosResponse;
+  readonly loading: boolean;
+  readonly response?: AxiosResponse;
+}


Test patch:
diff --git a/core/common/src/test/java/alluxio/wire/AlluxioMasterInfoTest.java b/core/common/src/test/java/alluxio/wire/AlluxioMasterInfoTest.java
index c078cbe990ac..fe4b15925ac5 100644
--- a/core/common/src/test/java/alluxio/wire/AlluxioMasterInfoTest.java
+++ b/core/common/src/test/java/alluxio/wire/AlluxioMasterInfoTest.java
@@ -92,6 +92,16 @@ private static AlluxioMasterInfo createRandom() {
     for (int i = 0; i < numWorkers; i++) {
       workers.add(WorkerInfoTest.createRandom());
     }
+    List<MasterInfo> masters = new ArrayList<>();
+    long numMasters = random.nextInt(10);
+    for (int i = 0; i < numMasters; i++) {
+      masters.add(MasterInfoTest.createRandom());
+    }
+    List<MasterInfo> lostMasters = new ArrayList<>();
+    long numLostMasters = random.nextInt(10);
+    for (int i = 0; i < numLostMasters; i++) {
+      lostMasters.add(MasterInfoTest.createRandom());
+    }
 
     result.setCapacity(capacity);
     result.setConfiguration(configuration);
diff --git a/core/common/src/test/java/alluxio/wire/MasterInfoTest.java b/core/common/src/test/java/alluxio/wire/MasterInfoTest.java
new file mode 100644
index 000000000000..51b41f6cc1ca
--- /dev/null
+++ b/core/common/src/test/java/alluxio/wire/MasterInfoTest.java
@@ -0,0 +1,48 @@
+/*
+ * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0
+ * (the "License"). You may not use this work except in compliance with the License, which is
+ * available at www.apache.org/licenses/LICENSE-2.0
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied, as more fully set forth in the License.
+ *
+ * See the NOTICE file distributed with this work for information regarding copyright ownership.
+ */
+
+package alluxio.wire;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.Random;
+
+public class MasterInfoTest {
+
+  @Test
+  public void json() throws Exception {
+    MasterInfo masterInfo = createRandom();
+    ObjectMapper mapper = new ObjectMapper();
+    MasterInfo other =
+        mapper.readValue(mapper.writeValueAsBytes(masterInfo), MasterInfo.class);
+    checkEquality(masterInfo, other);
+  }
+
+  public void checkEquality(MasterInfo a, MasterInfo b) {
+    Assert.assertEquals(a.getId(), b.getId());
+    Assert.assertEquals(a.getAddress(), b.getAddress());
+    Assert.assertEquals(a.getLastUpdatedTimeMs(), b.getLastUpdatedTimeMs());
+    Assert.assertEquals(a, b);
+  }
+
+  public static MasterInfo createRandom() {
+    Random random = new Random();
+    long id = random.nextLong();
+    Address address = new Address(RandomStringUtils.randomAlphanumeric(10), random.nextInt());
+
+    MasterInfo result = new MasterInfo(id, address);
+    result.updateLastUpdatedTimeMs();
+    return result;
+  }
+}
diff --git a/webui/master/src/containers/App/__snapshots__/App.test.tsx.snap b/webui/master/src/containers/App/__snapshots__/App.test.tsx.snap
index 7712c2d56f11..0f0c421664ae 100644
--- a/webui/master/src/containers/App/__snapshots__/App.test.tsx.snap
+++ b/webui/master/src/containers/App/__snapshots__/App.test.tsx.snap
@@ -53,6 +53,10 @@ exports[`App Shallow component Matches snapshot 1`] = `
               "innerText": "Configuration",
               "url": "/config",
             },
+            Object {
+              "innerText": "Masters",
+              "url": "/masters",
+            },
             Object {
               "innerText": "Workers",
               "url": "/workers",
@@ -133,6 +137,11 @@ exports[`App Shallow component Matches snapshot 1`] = `
           path="/metrics"
           render={[Function]}
         />
+        <Route
+          exact={true}
+          path="/masters"
+          render={[Function]}
+        />
         <Route
           exact={true}
           path="/workers"
@@ -173,6 +182,10 @@ exports[`App Shallow component Matches snapshot 1`] = `
               "innerText": "User Mailing List",
               "url": "https://groups.google.com/forum/#!forum/alluxio-users",
             },
+            Object {
+              "innerText": "Masters",
+              "url": "/masters",
+            },
             Object {
               "innerText": "Workers",
               "url": "/workers",
diff --git a/webui/master/src/containers/pages/Masters/Masters.test.tsx b/webui/master/src/containers/pages/Masters/Masters.test.tsx
new file mode 100644
index 000000000000..ba205123531a
--- /dev/null
+++ b/webui/master/src/containers/pages/Masters/Masters.test.tsx
@@ -0,0 +1,59 @@
+/*
+ * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0
+ * (the "License"). You may not use this work except in compliance with the License, which is
+ * available at www.apache.org/licenses/LICENSE-2.0
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied, as more fully set forth in the License.
+ *
+ * See the NOTICE file distributed with this work for information regarding copyright ownership.
+ */
+
+import { configure, shallow, ShallowWrapper } from 'enzyme';
+import Adapter from 'enzyme-adapter-react-16';
+import { createBrowserHistory, History, LocationState } from 'history';
+import React from 'react';
+import sinon from 'sinon';
+
+import { initialState } from '../../../store';
+import { initialInitState } from '../../../store/init/reducer';
+import { AllProps, MastersPresenter } from './Masters';
+import { routePaths } from '../../../constants';
+import { createAlertErrors } from '@alluxio/common-ui/src/utilities';
+
+configure({ adapter: new Adapter() });
+
+describe('Masters', () => {
+  let history: History<LocationState>;
+  let props: AllProps;
+
+  beforeAll(() => {
+    history = createBrowserHistory({ keyLength: 0 });
+    history.push(routePaths.masters);
+    props = {
+      initData: initialInitState.data,
+      mastersData: initialState.masters.data,
+      errors: createAlertErrors(false),
+      loading: false,
+      refresh: initialState.refresh.data,
+      class: '',
+      fetchRequest: sinon.spy(() => {}),
+    };
+  });
+
+  afterEach(() => {
+    sinon.restore();
+  });
+
+  describe('Shallow component', () => {
+    let shallowWrapper: ShallowWrapper;
+
+    beforeAll(() => {
+      shallowWrapper = shallow(<MastersPresenter {...props} />);
+    });
+
+    it('Matches snapshot', () => {
+      expect(shallowWrapper).toMatchSnapshot();
+    });
+  });
+});
diff --git a/webui/master/src/containers/pages/Masters/__snapshots__/Masters.test.tsx.snap b/webui/master/src/containers/pages/Masters/__snapshots__/Masters.test.tsx.snap
new file mode 100644
index 000000000000..7d9e1f04f38b
--- /dev/null
+++ b/webui/master/src/containers/pages/Masters/__snapshots__/Masters.test.tsx.snap
@@ -0,0 +1,109 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Masters Shallow component Matches snapshot 1`] = `
+<div
+  className="masters-page"
+>
+  <div
+    className="container-fluid"
+  >
+    <div
+      className="row"
+    >
+      <div
+        className="col-12"
+      >
+        <h5>
+          Leader Master
+        </h5>
+        <Table
+          hover={true}
+          responsiveTag="div"
+          tag="table"
+        >
+          <thead>
+            <tr>
+              <th>
+                Master Host
+              </th>
+              <th>
+                Master Port
+              </th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr>
+              <td />
+              <td>
+                0
+              </td>
+            </tr>
+          </tbody>
+        </Table>
+      </div>
+    </div>
+    <div
+      className="row"
+    >
+      <div
+        className="col-12"
+      >
+        <h5>
+          Standby Masters
+        </h5>
+        <Table
+          hover={true}
+          responsiveTag="div"
+          tag="table"
+        >
+          <thead>
+            <tr>
+              <th>
+                Master Host
+              </th>
+              <th>
+                Master Port
+              </th>
+              <th>
+                Last Heartbeat
+              </th>
+            </tr>
+          </thead>
+          <tbody />
+        </Table>
+      </div>
+    </div>
+    <div
+      className="row"
+    >
+      <div
+        className="col-12"
+      >
+        <h5>
+          Lost Masters
+        </h5>
+        <Table
+          hover={true}
+          responsiveTag="div"
+          tag="table"
+        >
+          <thead>
+            <tr>
+              <th>
+                Master Host
+              </th>
+              <th>
+                Master Port
+              </th>
+              <th>
+                Last Heartbeat
+              </th>
+            </tr>
+          </thead>
+          <tbody />
+        </Table>
+      </div>
+    </div>
+  </div>
+</div>
+`;