diff --git a/custom/conf/app.ini.sample b/custom/conf/app.ini.sample
index 701374d4b8..1617d64973 100644
--- a/custom/conf/app.ini.sample
+++ b/custom/conf/app.ini.sample
@@ -511,6 +511,8 @@ DEFAULT_ALLOW_ONLY_CONTRIBUTORS_TO_TRACK_TIME = true
 NO_REPLY_ADDRESS = noreply.%(DOMAIN)s
 ; Show Registration button
 SHOW_REGISTRATION_BUTTON = true
+; Show milestones dashboard page - a view of all the user's milestones
+SHOW_MILESTONES_DASHBOARD_PAGE = true
 ; Default value for AutoWatchNewRepos
 ; When adding a repo to a team or creating a new repo all team members will watch the
 ; repo automatically if enabled
diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
index 36e56c3fed..c059fe55b5 100644
--- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md
+++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
@@ -310,6 +310,7 @@ relation to port exhaustion.
 - `EMAIL_DOMAIN_WHITELIST`: **\<empty\>**: If non-empty, list of domain names that can only be used to register
   on this instance.
 - `SHOW_REGISTRATION_BUTTON`: **! DISABLE\_REGISTRATION**: Show Registration Button
+- `SHOW_MILESTONES_DASHBOARD_PAGE`: **true** Enable this to show the milestones dashboard page - a view of all the user's milestones
 - `AUTO_WATCH_NEW_REPOS`: **true**: Enable this to let all organisation users watch new repos when they are created
 - `AUTO_WATCH_ON_CHANGES`: **false**: Enable this to make users watch a repository after their first commit to it
 - `DEFAULT_ORG_VISIBILITY`: **public**: Set default visibility mode for organisations, either "public", "limited" or "private".
diff --git a/integrations/links_test.go b/integrations/links_test.go
index fc0f164552..329e54528a 100644
--- a/integrations/links_test.go
+++ b/integrations/links_test.go
@@ -86,6 +86,12 @@ func testLinksAsUser(userName string, t *testing.T) {
 		"/pulls?type=your_repositories&repos=[0]&sort=&state=closed",
 		"/pulls?type=assigned&repos=[0]&sort=&state=closed",
 		"/pulls?type=created_by&repos=[0]&sort=&state=closed",
+		"/milestones",
+		"/milestones?sort=mostcomplete&state=closed",
+		"/milestones?type=your_repositories&sort=mostcomplete&state=closed",
+		"/milestones?sort=&repos=[1]&state=closed",
+		"/milestones?sort=&repos=[1]&state=open",
+		"/milestones?repos=[0]&sort=mostissues&state=open",
 		"/notifications",
 		"/repo/create",
 		"/repo/migrate",
diff --git a/models/issue_milestone.go b/models/issue_milestone.go
index 0b854a8671..5fd1e1b8cc 100644
--- a/models/issue_milestone.go
+++ b/models/issue_milestone.go
@@ -17,8 +17,9 @@ import (
 
 // Milestone represents a milestone of repository.
 type Milestone struct {
-	ID              int64 `xorm:"pk autoincr"`
-	RepoID          int64 `xorm:"INDEX"`
+	ID              int64       `xorm:"pk autoincr"`
+	RepoID          int64       `xorm:"INDEX"`
+	Repo            *Repository `xorm:"-"`
 	Name            string
 	Content         string `xorm:"TEXT"`
 	RenderedContent string `xorm:"-"`
@@ -177,11 +178,38 @@ func (milestones MilestoneList) loadTotalTrackedTimes(e Engine) error {
 	return nil
 }
 
+func (m *Milestone) loadTotalTrackedTime(e Engine) error {
+	type totalTimesByMilestone struct {
+		MilestoneID int64
+		Time        int64
+	}
+	totalTime := &totalTimesByMilestone{MilestoneID: m.ID}
+	has, err := e.Table("issue").
+		Join("INNER", "milestone", "issue.milestone_id = milestone.id").
+		Join("LEFT", "tracked_time", "tracked_time.issue_id = issue.id").
+		Select("milestone_id, sum(time) as time").
+		Where("milestone_id = ?", m.ID).
+		GroupBy("milestone_id").
+		Get(totalTime)
+	if err != nil {
+		return err
+	} else if !has {
+		return nil
+	}
+	m.TotalTrackedTime = totalTime.Time
+	return nil
+}
+
 // LoadTotalTrackedTimes loads for every milestone in the list the TotalTrackedTime by a batch request
 func (milestones MilestoneList) LoadTotalTrackedTimes() error {
 	return milestones.loadTotalTrackedTimes(x)
 }
 
+// LoadTotalTrackedTime loads the tracked time for the milestone
+func (m *Milestone) LoadTotalTrackedTime() error {
+	return m.loadTotalTrackedTime(x)
+}
+
 func (milestones MilestoneList) getMilestoneIDs() []int64 {
 	var ids = make([]int64, 0, len(milestones))
 	for _, ms := range milestones {
@@ -465,3 +493,78 @@ func DeleteMilestoneByRepoID(repoID, id int64) error {
 	}
 	return sess.Commit()
 }
+
+// CountMilestonesByRepoIDs map from repoIDs to number of milestones matching the options`
+func CountMilestonesByRepoIDs(repoIDs []int64, isClosed bool) (map[int64]int64, error) {
+	sess := x.Where("is_closed = ?", isClosed)
+	sess.In("repo_id", repoIDs)
+
+	countsSlice := make([]*struct {
+		RepoID int64
+		Count  int64
+	}, 0, 10)
+	if err := sess.GroupBy("repo_id").
+		Select("repo_id AS repo_id, COUNT(*) AS count").
+		Table("milestone").
+		Find(&countsSlice); err != nil {
+		return nil, err
+	}
+
+	countMap := make(map[int64]int64, len(countsSlice))
+	for _, c := range countsSlice {
+		countMap[c.RepoID] = c.Count
+	}
+	return countMap, nil
+}
+
+// GetMilestonesByRepoIDs returns a list of milestones of given repositories and status.
+func GetMilestonesByRepoIDs(repoIDs []int64, page int, isClosed bool, sortType string) (MilestoneList, error) {
+	miles := make([]*Milestone, 0, setting.UI.IssuePagingNum)
+	sess := x.Where("is_closed = ?", isClosed)
+	sess.In("repo_id", repoIDs)
+	if page > 0 {
+		sess = sess.Limit(setting.UI.IssuePagingNum, (page-1)*setting.UI.IssuePagingNum)
+	}
+
+	switch sortType {
+	case "furthestduedate":
+		sess.Desc("deadline_unix")
+	case "leastcomplete":
+		sess.Asc("completeness")
+	case "mostcomplete":
+		sess.Desc("completeness")
+	case "leastissues":
+		sess.Asc("num_issues")
+	case "mostissues":
+		sess.Desc("num_issues")
+	default:
+		sess.Asc("deadline_unix")
+	}
+	return miles, sess.Find(&miles)
+}
+
+// MilestonesStats represents milestone statistic information.
+type MilestonesStats struct {
+	OpenCount, ClosedCount int64
+}
+
+// GetMilestonesStats returns milestone statistic information for dashboard by given conditions.
+func GetMilestonesStats(userRepoIDs []int64) (*MilestonesStats, error) {
+	var err error
+	stats := &MilestonesStats{}
+
+	stats.OpenCount, err = x.Where("is_closed = ?", false).
+		And(builder.In("repo_id", userRepoIDs)).
+		Count(new(Milestone))
+	if err != nil {
+		return nil, err
+	}
+	stats.ClosedCount, err = x.Where("is_closed = ?", true).
+		And(builder.In("repo_id", userRepoIDs)).
+		Count(new(Milestone))
+	if err != nil {
+		return nil, err
+	}
+
+	return stats, nil
+}
diff --git a/models/issue_milestone_test.go b/models/issue_milestone_test.go
index 6f8548ec67..787b849cce 100644
--- a/models/issue_milestone_test.go
+++ b/models/issue_milestone_test.go
@@ -289,3 +289,88 @@ func TestMilestoneList_LoadTotalTrackedTimes(t *testing.T) {
 
 	assert.Equal(t, miles[0].TotalTrackedTime, int64(3662))
 }
+
+func TestCountMilestonesByRepoIDs(t *testing.T) {
+	assert.NoError(t, PrepareTestDatabase())
+	milestonesCount := func(repoID int64) (int, int) {
+		repo := AssertExistsAndLoadBean(t, &Repository{ID: repoID}).(*Repository)
+		return repo.NumOpenMilestones, repo.NumClosedMilestones
+	}
+	repo1OpenCount, repo1ClosedCount := milestonesCount(1)
+	repo2OpenCount, repo2ClosedCount := milestonesCount(2)
+
+	openCounts, err := CountMilestonesByRepoIDs([]int64{1, 2}, false)
+	assert.NoError(t, err)
+	assert.EqualValues(t, repo1OpenCount, openCounts[1])
+	assert.EqualValues(t, repo2OpenCount, openCounts[2])
+
+	closedCounts, err := CountMilestonesByRepoIDs([]int64{1, 2}, true)
+	assert.NoError(t, err)
+	assert.EqualValues(t, repo1ClosedCount, closedCounts[1])
+	assert.EqualValues(t, repo2ClosedCount, closedCounts[2])
+}
+
+func TestGetMilestonesByRepoIDs(t *testing.T) {
+	assert.NoError(t, PrepareTestDatabase())
+	repo1 := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
+	repo2 := AssertExistsAndLoadBean(t, &Repository{ID: 2}).(*Repository)
+	test := func(sortType string, sortCond func(*Milestone) int) {
+		for _, page := range []int{0, 1} {
+			openMilestones, err := GetMilestonesByRepoIDs([]int64{repo1.ID, repo2.ID}, page, false, sortType)
+			assert.NoError(t, err)
+			assert.Len(t, openMilestones, repo1.NumOpenMilestones+repo2.NumOpenMilestones)
+			values := make([]int, len(openMilestones))
+			for i, milestone := range openMilestones {
+				values[i] = sortCond(milestone)
+			}
+			assert.True(t, sort.IntsAreSorted(values))
+
+			closedMilestones, err := GetMilestonesByRepoIDs([]int64{repo1.ID, repo2.ID}, page, true, sortType)
+			assert.NoError(t, err)
+			assert.Len(t, closedMilestones, repo1.NumClosedMilestones+repo2.NumClosedMilestones)
+			values = make([]int, len(closedMilestones))
+			for i, milestone := range closedMilestones {
+				values[i] = sortCond(milestone)
+			}
+			assert.True(t, sort.IntsAreSorted(values))
+		}
+	}
+	test("furthestduedate", func(milestone *Milestone) int {
+		return -int(milestone.DeadlineUnix)
+	})
+	test("leastcomplete", func(milestone *Milestone) int {
+		return milestone.Completeness
+	})
+	test("mostcomplete", func(milestone *Milestone) int {
+		return -milestone.Completeness
+	})
+	test("leastissues", func(milestone *Milestone) int {
+		return milestone.NumIssues
+	})
+	test("mostissues", func(milestone *Milestone) int {
+		return -milestone.NumIssues
+	})
+	test("soonestduedate", func(milestone *Milestone) int {
+		return int(milestone.DeadlineUnix)
+	})
+}
+
+func TestLoadTotalTrackedTime(t *testing.T) {
+	assert.NoError(t, PrepareTestDatabase())
+	milestone := AssertExistsAndLoadBean(t, &Milestone{ID: 1}).(*Milestone)
+
+	assert.NoError(t, milestone.LoadTotalTrackedTime())
+
+	assert.Equal(t, milestone.TotalTrackedTime, int64(3662))
+}
+
+func TestGetMilestonesStats(t *testing.T) {
+	assert.NoError(t, PrepareTestDatabase())
+	repo1 := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
+	repo2 := AssertExistsAndLoadBean(t, &Repository{ID: 2}).(*Repository)
+
+	milestoneStats, err := GetMilestonesStats([]int64{repo1.ID, repo2.ID})
+	assert.NoError(t, err)
+	assert.EqualValues(t, repo1.NumOpenMilestones+repo2.NumOpenMilestones, milestoneStats.OpenCount)
+	assert.EqualValues(t, repo1.NumClosedMilestones+repo2.NumClosedMilestones, milestoneStats.ClosedCount)
+}
diff --git a/modules/context/context.go b/modules/context/context.go
index ef6c19ed12..4b590a7181 100644
--- a/modules/context/context.go
+++ b/modules/context/context.go
@@ -334,6 +334,7 @@ func Contexter() macaron.Handler {
 		ctx.Data["IsLandingPageOrganizations"] = setting.LandingPageURL == setting.LandingPageOrganizations
 
 		ctx.Data["ShowRegistrationButton"] = setting.Service.ShowRegistrationButton
+		ctx.Data["ShowMilestonesDashboardPage"] = setting.Service.ShowMilestonesDashboardPage
 		ctx.Data["ShowFooterBranding"] = setting.ShowFooterBranding
 		ctx.Data["ShowFooterVersion"] = setting.ShowFooterVersion
 
diff --git a/modules/setting/service.go b/modules/setting/service.go
index 9407231ac6..c463b0a9d5 100644
--- a/modules/setting/service.go
+++ b/modules/setting/service.go
@@ -21,6 +21,7 @@ var Service struct {
 	DisableRegistration                     bool
 	AllowOnlyExternalRegistration           bool
 	ShowRegistrationButton                  bool
+	ShowMilestonesDashboardPage             bool
 	RequireSignInView                       bool
 	EnableNotifyMail                        bool
 	EnableBasicAuth                         bool
@@ -62,6 +63,7 @@ func newService() {
 	Service.AllowOnlyExternalRegistration = sec.Key("ALLOW_ONLY_EXTERNAL_REGISTRATION").MustBool()
 	Service.EmailDomainWhitelist = sec.Key("EMAIL_DOMAIN_WHITELIST").Strings(",")
 	Service.ShowRegistrationButton = sec.Key("SHOW_REGISTRATION_BUTTON").MustBool(!(Service.DisableRegistration || Service.AllowOnlyExternalRegistration))
+	Service.ShowMilestonesDashboardPage = sec.Key("SHOW_MILESTONES_DASHBOARD_PAGE").MustBool(true)
 	Service.RequireSignInView = sec.Key("REQUIRE_SIGNIN_VIEW").MustBool()
 	Service.EnableBasicAuth = sec.Key("ENABLE_BASIC_AUTHENTICATION").MustBool(true)
 	Service.EnableReverseProxyAuth = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION").MustBool()
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index c6fd3b863f..c5cfb1663f 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -66,6 +66,7 @@ forks = Forks
 activities = Activities
 pull_requests = Pull Requests
 issues = Issues
+milestones = Milestones
 
 cancel = Cancel
 add = Add
diff --git a/routers/routes/routes.go b/routers/routes/routes.go
index cfd4a60974..60fd93df9c 100644
--- a/routers/routes/routes.go
+++ b/routers/routes/routes.go
@@ -254,6 +254,13 @@ func RegisterRoutes(m *macaron.Macaron) {
 		}
 	}
 
+	reqMilestonesDashboardPageEnabled := func(ctx *context.Context) {
+		if !setting.Service.ShowMilestonesDashboardPage {
+			ctx.Error(403)
+			return
+		}
+	}
+
 	m.Use(user.GetNotificationCount)
 
 	// FIXME: not all routes need go through same middlewares.
@@ -276,6 +283,7 @@ func RegisterRoutes(m *macaron.Macaron) {
 	m.Combo("/install", routers.InstallInit).Get(routers.Install).
 		Post(bindIgnErr(auth.InstallForm{}), routers.InstallPost)
 	m.Get("/^:type(issues|pulls)$", reqSignIn, user.Issues)
+	m.Get("/milestones", reqSignIn, reqMilestonesDashboardPageEnabled, user.Milestones)
 
 	// ***** START: User *****
 	m.Group("/user", func() {
@@ -556,6 +564,7 @@ func RegisterRoutes(m *macaron.Macaron) {
 		m.Group("/:org", func() {
 			m.Get("/dashboard", user.Dashboard)
 			m.Get("/^:type(issues|pulls)$", user.Issues)
+			m.Get("/milestones", reqMilestonesDashboardPageEnabled, user.Milestones)
 			m.Get("/members", org.Members)
 			m.Get("/members/action/:action", org.MembersAction)
 
diff --git a/routers/user/home.go b/routers/user/home.go
index a1060f371f..426f15bfa7 100644
--- a/routers/user/home.go
+++ b/routers/user/home.go
@@ -18,17 +18,20 @@ import (
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/markup/markdown"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 
 	"github.com/keybase/go-crypto/openpgp"
 	"github.com/keybase/go-crypto/openpgp/armor"
+	"github.com/unknwon/com"
 )
 
 const (
-	tplDashboard base.TplName = "user/dashboard/dashboard"
-	tplIssues    base.TplName = "user/dashboard/issues"
-	tplProfile   base.TplName = "user/profile"
+	tplDashboard  base.TplName = "user/dashboard/dashboard"
+	tplIssues     base.TplName = "user/dashboard/issues"
+	tplMilestones base.TplName = "user/dashboard/milestones"
+	tplProfile    base.TplName = "user/profile"
 )
 
 // getDashboardContextUser finds out dashboard is viewing as which context user.
@@ -150,6 +153,190 @@ func Dashboard(ctx *context.Context) {
 	ctx.HTML(200, tplDashboard)
 }
 
+// Milestones render the user milestones page
+func Milestones(ctx *context.Context) {
+	ctx.Data["Title"] = ctx.Tr("milestones")
+	ctx.Data["PageIsMilestonesDashboard"] = true
+
+	ctxUser := getDashboardContextUser(ctx)
+	if ctx.Written() {
+		return
+	}
+
+	sortType := ctx.Query("sort")
+	page := ctx.QueryInt("page")
+	if page <= 1 {
+		page = 1
+	}
+
+	reposQuery := ctx.Query("repos")
+	isShowClosed := ctx.Query("state") == "closed"
+
+	// Get repositories.
+	var err error
+	var userRepoIDs []int64
+	if ctxUser.IsOrganization() {
+		env, err := ctxUser.AccessibleReposEnv(ctx.User.ID)
+		if err != nil {
+			ctx.ServerError("AccessibleReposEnv", err)
+			return
+		}
+		userRepoIDs, err = env.RepoIDs(1, ctxUser.NumRepos)
+		if err != nil {
+			ctx.ServerError("env.RepoIDs", err)
+			return
+		}
+	} else {
+		unitType := models.UnitTypeIssues
+		userRepoIDs, err = ctxUser.GetAccessRepoIDs(unitType)
+		if err != nil {
+			ctx.ServerError("ctxUser.GetAccessRepoIDs", err)
+			return
+		}
+	}
+	if len(userRepoIDs) == 0 {
+		userRepoIDs = []int64{-1}
+	}
+
+	var repoIDs []int64
+	if issueReposQueryPattern.MatchString(reposQuery) {
+		// remove "[" and "]" from string
+		reposQuery = reposQuery[1 : len(reposQuery)-1]
+		//for each ID (delimiter ",") add to int to repoIDs
+		reposSet := false
+		for _, rID := range strings.Split(reposQuery, ",") {
+			// Ensure nonempty string entries
+			if rID != "" && rID != "0" {
+				reposSet = true
+				rIDint64, err := strconv.ParseInt(rID, 10, 64)
+				if err == nil && com.IsSliceContainsInt64(userRepoIDs, rIDint64) {
+					repoIDs = append(repoIDs, rIDint64)
+				}
+			}
+		}
+		if reposSet && len(repoIDs) == 0 {
+			// force an empty result
+			repoIDs = []int64{-1}
+		}
+	} else {
+		log.Error("issueReposQueryPattern not match with query")
+	}
+
+	if len(repoIDs) == 0 {
+		repoIDs = userRepoIDs
+	}
+
+	counts, err := models.CountMilestonesByRepoIDs(userRepoIDs, isShowClosed)
+	if err != nil {
+		ctx.ServerError("CountMilestonesByRepoIDs", err)
+		return
+	}
+
+	milestones, err := models.GetMilestonesByRepoIDs(repoIDs, page, isShowClosed, sortType)
+	if err != nil {
+		ctx.ServerError("GetMilestonesByRepoIDs", err)
+		return
+	}
+
+	showReposMap := make(map[int64]*models.Repository, len(counts))
+	for rID := range counts {
+		if rID == -1 {
+			break
+		}
+		repo, err := models.GetRepositoryByID(rID)
+		if err != nil {
+			if models.IsErrRepoNotExist(err) {
+				ctx.NotFound("GetRepositoryByID", err)
+				return
+			} else if err != nil {
+				ctx.ServerError("GetRepositoryByID", fmt.Errorf("[%d]%v", rID, err))
+				return
+			}
+		}
+		showReposMap[rID] = repo
+
+		// Check if user has access to given repository.
+		perm, err := models.GetUserRepoPermission(repo, ctxUser)
+		if err != nil {
+			ctx.ServerError("GetUserRepoPermission", fmt.Errorf("[%d]%v", rID, err))
+			return
+		}
+
+		if !perm.CanRead(models.UnitTypeIssues) {
+			if log.IsTrace() {
+				log.Trace("Permission Denied: User %-v cannot read %-v of repo %-v\n"+
+					"User in repo has Permissions: %-+v",
+					ctxUser,
+					models.UnitTypeIssues,
+					repo,
+					perm)
+			}
+			ctx.Status(404)
+			return
+		}
+	}
+
+	showRepos := models.RepositoryListOfMap(showReposMap)
+	sort.Sort(showRepos)
+	if err = showRepos.LoadAttributes(); err != nil {
+		ctx.ServerError("LoadAttributes", err)
+		return
+	}
+
+	for _, m := range milestones {
+		m.Repo = showReposMap[m.RepoID]
+		m.RenderedContent = string(markdown.Render([]byte(m.Content), m.Repo.Link(), m.Repo.ComposeMetas()))
+		if m.Repo.IsTimetrackerEnabled() {
+			err := m.LoadTotalTrackedTime()
+			if err != nil {
+				ctx.ServerError("LoadTotalTrackedTime", err)
+				return
+			}
+		}
+	}
+
+	milestoneStats, err := models.GetMilestonesStats(repoIDs)
+	if err != nil {
+		ctx.ServerError("GetMilestoneStats", err)
+		return
+	}
+
+	totalMilestoneStats, err := models.GetMilestonesStats(userRepoIDs)
+	if err != nil {
+		ctx.ServerError("GetMilestoneStats", err)
+		return
+	}
+
+	var pagerCount int
+	if isShowClosed {
+		ctx.Data["State"] = "closed"
+		ctx.Data["Total"] = totalMilestoneStats.ClosedCount
+		pagerCount = int(milestoneStats.ClosedCount)
+	} else {
+		ctx.Data["State"] = "open"
+		ctx.Data["Total"] = totalMilestoneStats.OpenCount
+		pagerCount = int(milestoneStats.OpenCount)
+	}
+
+	ctx.Data["Milestones"] = milestones
+	ctx.Data["Repos"] = showRepos
+	ctx.Data["Counts"] = counts
+	ctx.Data["MilestoneStats"] = milestoneStats
+	ctx.Data["SortType"] = sortType
+	if len(repoIDs) != len(userRepoIDs) {
+		ctx.Data["RepoIDs"] = repoIDs
+	}
+	ctx.Data["IsShowClosed"] = isShowClosed
+
+	pager := context.NewPagination(pagerCount, setting.UI.IssuePagingNum, page, 5)
+	pager.AddParam(ctx, "repos", "RepoIDs")
+	pager.AddParam(ctx, "sort", "SortType")
+	pager.AddParam(ctx, "state", "State")
+	ctx.Data["Page"] = pager
+
+	ctx.HTML(200, tplMilestones)
+}
+
 // Regexp for repos query
 var issueReposQueryPattern = regexp.MustCompile(`^\[\d+(,\d+)*,?\]$`)
 
diff --git a/routers/user/home_test.go b/routers/user/home_test.go
index 9d4136ac8c..e5bbd0e98e 100644
--- a/routers/user/home_test.go
+++ b/routers/user/home_test.go
@@ -31,3 +31,42 @@ func TestIssues(t *testing.T) {
 	assert.Len(t, ctx.Data["Issues"], 1)
 	assert.Len(t, ctx.Data["Repos"], 1)
 }
+
+func TestMilestones(t *testing.T) {
+	setting.UI.IssuePagingNum = 1
+	assert.NoError(t, models.LoadFixtures())
+
+	ctx := test.MockContext(t, "milestones")
+	test.LoadUser(t, ctx, 2)
+	ctx.SetParams("sort", "issues")
+	ctx.Req.Form.Set("state", "closed")
+	ctx.Req.Form.Set("sort", "furthestduedate")
+	Milestones(ctx)
+	assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+	assert.EqualValues(t, map[int64]int64{1: 1}, ctx.Data["Counts"])
+	assert.EqualValues(t, true, ctx.Data["IsShowClosed"])
+	assert.EqualValues(t, "furthestduedate", ctx.Data["SortType"])
+	assert.EqualValues(t, 1, ctx.Data["Total"])
+	assert.Len(t, ctx.Data["Milestones"], 1)
+	assert.Len(t, ctx.Data["Repos"], 1)
+}
+
+func TestMilestonesForSpecificRepo(t *testing.T) {
+	setting.UI.IssuePagingNum = 1
+	assert.NoError(t, models.LoadFixtures())
+
+	ctx := test.MockContext(t, "milestones")
+	test.LoadUser(t, ctx, 2)
+	ctx.SetParams("sort", "issues")
+	ctx.SetParams("repo", "1")
+	ctx.Req.Form.Set("state", "closed")
+	ctx.Req.Form.Set("sort", "furthestduedate")
+	Milestones(ctx)
+	assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+	assert.EqualValues(t, map[int64]int64{1: 1}, ctx.Data["Counts"])
+	assert.EqualValues(t, true, ctx.Data["IsShowClosed"])
+	assert.EqualValues(t, "furthestduedate", ctx.Data["SortType"])
+	assert.EqualValues(t, 1, ctx.Data["Total"])
+	assert.Len(t, ctx.Data["Milestones"], 1)
+	assert.Len(t, ctx.Data["Repos"], 1)
+}
diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl
index fdba57d5bf..a09b4b832e 100644
--- a/templates/base/head_navbar.tmpl
+++ b/templates/base/head_navbar.tmpl
@@ -12,6 +12,7 @@
 		<a class="item {{if .PageIsDashboard}}active{{end}}" href="{{AppSubUrl}}/">{{.i18n.Tr "dashboard"}}</a>
 		<a class="item {{if .PageIsIssues}}active{{end}}" href="{{AppSubUrl}}/issues">{{.i18n.Tr "issues"}}</a>
 		<a class="item {{if .PageIsPulls}}active{{end}}" href="{{AppSubUrl}}/pulls">{{.i18n.Tr "pull_requests"}}</a>
+		{{if .ShowMilestonesDashboardPage}}<a class="item {{if .PageIsMilestonesDashboard}}active{{end}}" href="{{AppSubUrl}}/milestones">{{.i18n.Tr "milestones"}}</a>{{end}}
 		<a class="item {{if .PageIsExplore}}active{{end}}" href="{{AppSubUrl}}/explore/repos">{{.i18n.Tr "explore"}}</a>
 	{{else if .IsLandingPageHome}}
 		<a class="item {{if .PageIsHome}}active{{end}}" href="{{AppSubUrl}}/">{{.i18n.Tr "home"}}</a>
diff --git a/templates/user/dashboard/milestones.tmpl b/templates/user/dashboard/milestones.tmpl
new file mode 100644
index 0000000000..495119f442
--- /dev/null
+++ b/templates/user/dashboard/milestones.tmpl
@@ -0,0 +1,119 @@
+{{template "base/head" .}}
+<div class="dashboard issues repository milestones">
+	{{template "user/dashboard/navbar" .}}
+	<div class="ui container">
+		<div class="ui stackable grid">
+			<div class="four wide column">
+				<div class="ui secondary vertical filter menu">
+					<a class="item" href="{{.Link}}?type=your_repositories&sort={{$.SortType}}&state={{.State}}">
+						{{.i18n.Tr "home.issues.in_your_repos"}}
+						<strong class="ui right">{{.Total}}</strong>
+					</a>
+					<div class="ui divider"></div>
+					{{range .Repos}}
+						{{with $Repo := .}}
+							<a class="{{range $.RepoIDs}}{{if eq . $Repo.ID}}ui basic blue button{{end}}{{end}} repo name item" href="{{$.Link}}?repos=[
+								{{with $include := true}}
+									{{range $.RepoIDs}}
+										{{if eq . $Repo.ID}}
+											{{$include = false}}
+										{{else}}
+											{{.}}%2C
+										{{end}}
+									{{end}}
+									{{if eq $include true}}
+										{{$Repo.ID}}%2C
+									{{end}}
+								{{end}}
+								]&sort={{$.SortType}}&state={{$.State}}" title="{{.FullName}}">
+								<span class="text truncate">{{$Repo.FullName}}</span>
+								<div class="floating ui {{if $.IsShowClosed}}red{{else}}green{{end}} label">{{index $.Counts $Repo.ID}}</div>
+							</a>
+						{{end}}
+					{{end}}
+				</div>
+			</div>
+			<div class="twelve wide column content">
+				<div class="ui tiny basic status buttons">
+					<a class="ui {{if not .IsShowClosed}}green active{{end}} basic button" href="{{.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=open">
+						<i class="octicon octicon-issue-opened"></i>
+						{{.i18n.Tr "repo.milestones.open_tab" .MilestoneStats.OpenCount}}
+					</a>
+					<a class="ui {{if .IsShowClosed}}red active{{end}} basic button" href="{{.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=closed">
+						<i class="octicon octicon-issue-closed"></i>
+						{{.i18n.Tr "repo.milestones.close_tab" .MilestoneStats.ClosedCount}}
+					</a>
+				</div>
+				<div class="ui right floated secondary filter menu">
+					<!-- Sort -->
+					<div class="ui dropdown type jump item">
+						<span class="text">
+							{{.i18n.Tr "repo.issues.filter_sort"}}
+							<i class="dropdown icon"></i>
+						</span>
+						<div class="menu">
+							<a class="{{if or (eq .SortType "closestduedate") (not .SortType)}}active{{end}} item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=closestduedate&state={{$.State}}">{{.i18n.Tr "repo.milestones.filter_sort.closest_due_date"}}</a>
+              <a class="{{if eq .SortType "furthestduedate"}}active{{end}} item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=furthestduedate&state={{$.State}}">{{.i18n.Tr "repo.milestones.filter_sort.furthest_due_date"}}</a>
+              <a class="{{if eq .SortType "leastcomplete"}}active{{end}} item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=leastcomplete&state={{$.State}}">{{.i18n.Tr "repo.milestones.filter_sort.least_complete"}}</a>
+              <a class="{{if eq .SortType "mostcomplete"}}active{{end}} item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=mostcomplete&state={{$.State}}">{{.i18n.Tr "repo.milestones.filter_sort.most_complete"}}</a>
+              <a class="{{if eq .SortType "mostissues"}}active{{end}} item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=mostissues&state={{$.State}}">{{.i18n.Tr "repo.milestones.filter_sort.most_issues"}}</a>
+              <a class="{{if eq .SortType "leastissues"}}active{{end}} item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=leastissues&state={{$.State}}">{{.i18n.Tr "repo.milestones.filter_sort.least_issues"}}</a>
+						</div>
+					</div>
+				</div>
+
+                <div class="milestone list">
+                    {{range .Milestones}}
+                        <li class="item">
+                            <div class="ui label">{{if not $.RepoIDs}}{{.Repo.FullName}}{{end}}</div>
+                            <i class="octicon octicon-milestone"></i> <a href="{{.Repo.Link }}/milestone/{{.ID}}">{{.Name}}</a>
+                            <div class="ui right green progress" data-percent="{{.Completeness}}">
+                                <div class="bar" {{if not .Completeness}}style="background-color: transparent"{{end}}>
+                                    <div class="progress"></div>
+                                </div>
+                            </div>
+                            <div class="meta">
+                                {{ $closedDate:= TimeSinceUnix .ClosedDateUnix $.Lang }}
+                                {{if .IsClosed}}
+                                    <span class="octicon octicon-clock"></span> {{$.i18n.Tr "repo.milestones.closed" $closedDate|Str2html}}
+                                {{else}}
+                                    <span class="octicon octicon-calendar"></span>
+                                    {{if .DeadlineString}}
+                                        <span {{if .IsOverdue}}class="overdue"{{end}}>{{.DeadlineString}}</span>
+                                    {{else}}
+                                        {{$.i18n.Tr "repo.milestones.no_due_date"}}
+                                    {{end}}
+                                {{end}}
+                                <span class="issue-stats">
+                                    <i class="octicon octicon-issue-opened"></i> {{$.i18n.Tr "repo.milestones.open_tab" .NumOpenIssues}}
+                                    <i class="octicon octicon-issue-closed"></i> {{$.i18n.Tr "repo.milestones.close_tab" .NumClosedIssues}}
+                                    {{if .TotalTrackedTime}}<i class="octicon octicon-clock"></i> {{.TotalTrackedTime|Sec2Time}}{{end}}
+                                </span>
+                            </div>
+                            {{if and (or $.CanWriteIssues $.CanWritePulls) (not $.Repository.IsArchived)}}
+                                <div class="ui right operate">
+                                    <a href="{{$.Link}}/{{.ID}}/edit" data-id={{.ID}} data-title={{.Name}}><i class="octicon octicon-pencil"></i> {{$.i18n.Tr "repo.issues.label_edit"}}</a>
+                                    {{if .IsClosed}}
+                                        <a href="{{$.Link}}/{{.ID}}/open" data-id={{.ID}} data-title={{.Name}}><i class="octicon octicon-check"></i> {{$.i18n.Tr "repo.milestones.open"}}</a>
+                                    {{else}}
+                                        <a href="{{$.Link}}/{{.ID}}/close" data-id={{.ID}} data-title={{.Name}}><i class="octicon octicon-x"></i> {{$.i18n.Tr "repo.milestones.close"}}</a>
+                                    {{end}}
+                                    <a class="delete-button" href="#" data-url="{{$.RepoLink}}/milestones/delete" data-id="{{.ID}}"><i class="octicon octicon-trashcan"></i> {{$.i18n.Tr "repo.issues.label_delete"}}</a>
+                                </div>
+                            {{end}}
+                            {{if .Content}}
+                                <div class="content">
+                                    {{.RenderedContent|Str2html}}
+                                </div>
+                            {{end}}
+                        </li>
+                    {{end}}
+
+                    {{template "base/paginate" .}}
+                </div>
+
+			</div>
+		</div>
+	</div>
+</div>
+{{template "base/footer" .}}
diff --git a/templates/user/dashboard/navbar.tmpl b/templates/user/dashboard/navbar.tmpl
index 25c45325a6..ed44c35377 100644
--- a/templates/user/dashboard/navbar.tmpl
+++ b/templates/user/dashboard/navbar.tmpl
@@ -12,12 +12,12 @@
 						{{.i18n.Tr "home.switch_dashboard_context"}}
 					</div>
 					<div class="scrolling menu items">
-						<a class="{{if eq .ContextUser.ID .SignedUser.ID}}active selected{{end}} item" href="{{AppSubUrl}}/{{if .PageIsIssues}}issues{{else if .PageIsPulls}}pulls{{end}}">
+						<a class="{{if eq .ContextUser.ID .SignedUser.ID}}active selected{{end}} item" href="{{AppSubUrl}}/{{if .PageIsIssues}}issues{{else if .PageIsPulls}}pulls{{else if .PageIsMilestonesDashboard}}milestones{{end}}">
 							<img class="ui avatar image" src="{{.SignedUser.RelAvatarLink}}">
 							{{.SignedUser.Name}}
 						</a>
 						{{range .Orgs}}
-							<a class="{{if eq $.ContextUser.ID .ID}}active selected{{end}} item" title="{{.Name}}" href="{{AppSubUrl}}/org/{{.Name}}/{{if $.PageIsIssues}}issues{{else if $.PageIsPulls}}pulls{{else}}dashboard{{end}}">
+							<a class="{{if eq $.ContextUser.ID .ID}}active selected{{end}} item" title="{{.Name}}" href="{{AppSubUrl}}/org/{{.Name}}/{{if $.PageIsIssues}}issues{{else if $.PageIsPulls}}pulls{{else if $.PageIsMilestonesDashboard}}milestones{{else}}dashboard{{end}}">
 								<img class="ui avatar image" src="{{.RelAvatarLink}}">
 								{{.ShortName 20}}
 							</a>
@@ -43,6 +43,11 @@
 				<a class="{{if .PageIsPulls}}active{{end}} item" href="{{AppSubUrl}}/org/{{.ContextUser.Name}}/pulls">
 					<i class="octicon octicon-git-pull-request"></i>&nbsp;{{.i18n.Tr "pull_requests"}}
 				</a>
+				{{if .ShowMilestonesDashboardPage}}
+					<a class="{{if .PageIsMilestonesDashboard}}active{{end}} item" href="{{AppSubUrl}}/org/{{.ContextUser.Name}}/milestones">
+						<i class="octicon octicon-milestone"></i>&nbsp;{{.i18n.Tr "milestones"}}
+					</a>
+				{{end}}
 				<div class="item">
 					<a class="ui blue basic button" href="{{.ContextUser.HomeLink}}" title='{{.i18n.Tr "home.view_home" .ContextUser.Name}}'>
 						{{.i18n.Tr "home.view_home" (.ContextUser.ShortName 10)}}