Repository: maproulette/maproulette3
Pull request #1919 (2023-01-03 17:30:36)

Admin metrics v2
Developed with @javtran 

#### Project table: 
- A sortable list of  all projects with core metrics (id,  name, owner, discoverable, archived, virtual, date created, date last modified).
- Clicking on a project will take you to the existing project admin page.
- Sort projects by name, newest/oldest date, # of challenges, default.
- Hide undiscoverable/archived projects with toggle.
- Filter virtual only projects with toggle.
- Search to filter by project name.
- Filter projects by date created.
- Export table as CSV.
- Customizable paginations(default to 50).

#### User table: 
- A sortable list of  all users with core metrics (id, name, score, date created, date last active).
- Sort users by name, newest/oldest date, score, default.
- Search to filter by osm username.
- Filter users by date created.
- Export table as CSV.
- Customizable paginations(default to 50).

#### New features/changes to challenge table: 
- Filter challenges by date created.
- Hide undiscoverable/archived challenges with toggle.
- Display project by name.

codecov-commenter: # [Codecov](https://codecov.io/gh/maproulette/maproulette3/pull/1919?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=maproulette) Report
> Merging [#1919](https://codecov.io/gh/maproulette/maproulette3/pull/1919?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=maproulette) (ff44661) into [develop](https://codecov.io/gh/maproulette/maproulette3/commit/5a2e7ba5c6c0a40d6457900ba61897585522e53e?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=maproulette) (5a2e7ba) will **increase** coverage by `0.92%`.
> The diff coverage is `31.25%`.

```diff
@@             Coverage Diff             @@
##           develop    #1919      +/-   ##
===========================================
+ Coverage    23.76%   24.68%   +0.92%     
===========================================
  Files          611      616       +5     
  Lines        20323    21512    +1189     
  Branches      6120     6592     +472     
===========================================
+ Hits          4829     5311     +482     
- Misses       13013    13523     +510     
- Partials      2481     2678     +197     
```


| [Impacted Files](https://codecov.io/gh/maproulette/maproulette3/pull/1919?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=maproulette) | Coverage Δ | |
|---|---|---|
| [src/PersistedStore.js](https://codecov.io/gh/maproulette/maproulette3/pull/1919/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=maproulette#diff-c3JjL1BlcnNpc3RlZFN0b3JlLmpz) | `100.00% <ø> (ø)` | |
| [...ane/ChallengeFilterSubnav/ChallengeFilterSubnav.js](https://codecov.io/gh/maproulette/maproulette3/pull/1919/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=maproulette#diff-c3JjL2NvbXBvbmVudHMvQ2hhbGxlbmdlUGFuZS9DaGFsbGVuZ2VGaWx0ZXJTdWJuYXYvQ2hhbGxlbmdlRmlsdGVyU3VibmF2Lmpz) | `54.54% <0.00%> (-12.13%)` | :arrow_down: |
| [src/components/HOCs/WithSearch/WithSearch.js](https://codecov.io/gh/maproulette/maproulette3/pull/1919/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=maproulette#diff-c3JjL2NvbXBvbmVudHMvSE9Dcy9XaXRoU2VhcmNoL1dpdGhTZWFyY2guanM=) | `59.40% <0.00%> (-1.82%)` | :arrow_down: |
| [...components/HOCs/WithSearchRoute/WithSearchRoute.js](https://codecov.io/gh/maproulette/maproulette3/pull/1919/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=maproulette#diff-c3JjL2NvbXBvbmVudHMvSE9Dcy9XaXRoU2VhcmNoUm91dGUvV2l0aFNlYXJjaFJvdXRlLmpz) | `50.57% <0.00%> (ø)` | |
| [src/components/SuperAdmin/MetricsTable.js](https://codecov.io/gh/maproulette/maproulette3/pull/1919/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=maproulette#diff-c3JjL2NvbXBvbmVudHMvU3VwZXJBZG1pbi9NZXRyaWNzVGFibGUuanM=) | `28.57% <0.00%> (-7.80%)` | :arrow_down: |
| [src/components/SuperAdmin/SuperAdminContainer.js](https://codecov.io/gh/maproulette/maproulette3/pull/1919/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=maproulette#diff-c3JjL2NvbXBvbmVudHMvU3VwZXJBZG1pbi9TdXBlckFkbWluQ29udGFpbmVyLmpz) | `13.04% <0.00%> (-4.61%)` | :arrow_down: |
| [src/components/SuperAdmin/WithExportCsv.js](https://codecov.io/gh/maproulette/maproulette3/pull/1919/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=maproulette#diff-c3JjL2NvbXBvbmVudHMvU3VwZXJBZG1pbi9XaXRoRXhwb3J0Q3N2Lmpz) | `5.35% <0.00%> (-1.47%)` | :arrow_down: |
| [src/components/SuperAdmin/WithMetricsFilter.js](https://codecov.io/gh/maproulette/maproulette3/pull/1919/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=maproulette#diff-c3JjL2NvbXBvbmVudHMvU3VwZXJBZG1pbi9XaXRoTWV0cmljc0ZpbHRlci5qcw==) | `4.34% <0.00%> (-11.45%)` | :arrow_down: |
| [src/services/Challenge/Challenge.js](https://codecov.io/gh/maproulette/maproulette3/pull/1919/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=maproulette#diff-c3JjL3NlcnZpY2VzL0NoYWxsZW5nZS9DaGFsbGVuZ2UuanM=) | `14.14% <0.00%> (+3.36%)` | :arrow_up: |
| [src/services/Server/APIRoutes.js](https://codecov.io/gh/maproulette/maproulette3/pull/1919/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=maproulette#diff-c3JjL3NlcnZpY2VzL1NlcnZlci9BUElSb3V0ZXMuanM=) | `100.00% <ø> (ø)` | |
| ... and [38 more](https://codecov.io/gh/maproulette/maproulette3/pull/1919/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=maproulette) | |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=maproulette)


Patch:
diff --git a/.env b/.env
index 4338c63b..25ef535b 100644
--- a/.env
+++ b/.env
@@ -180,3 +180,8 @@ REACT_APP_BROWSE_CHALLENGES_OWNER_LIMIT=5
 # Determine how many months before system will auto-archive stale challenges
 # Supported values = [numbers, string numbers, undefined (6 months)]
 REACT_APP_ARCHIVE_STALE_TIME_IN_MONTHS=6
+
+# Github issues api access info. 
+REACT_APP_GITHUB_ISSUES_API_TOKEN=''
+REACT_APP_GITHUB_ISSUES_API_OWNER=''
+REACT_APP_GITHUB_ISSUES_API_REPO=''
diff --git a/src/PersistedStore.js b/src/PersistedStore.js
index d5491364..370f4287 100644
--- a/src/PersistedStore.js
+++ b/src/PersistedStore.js
@@ -5,6 +5,7 @@ import { visibleOverlays } from './services/VisibleLayer/VisibleOverlays'
 import { currentUser, userEntities } from './services/User/User'
 import { projectEntities } from './services/Project/Project'
 import { adminProjectEntities } from './services/SuperAdmin/SuperAdminProjects'
+import { adminUserEntities } from './services/SuperAdmin/SuperAdminUsers'
 import { challengeEntities, adminChallengeEntities } from './services/Challenge/Challenge'
 import { virtualChallengeEntities }
        from './services/VirtualChallenge/VirtualChallenge'
@@ -43,7 +44,8 @@ export const initializeStore = function() {
     comments: commentEntities,
     places: placeEntities,
     adminChallenges: adminChallengeEntities,
-    adminProjects: adminProjectEntities
+    adminProjects: adminProjectEntities,
+    adminUsers: adminUserEntities,
   })
 
   // redux root reducer defining the top-level reducers in the store
diff --git a/src/components/AutosuggestTextBox/AutosuggestMentionTextArea.js b/src/components/AutosuggestTextBox/AutosuggestMentionTextArea.js
index 20e8297e..c1f13a81 100644
--- a/src/components/AutosuggestTextBox/AutosuggestMentionTextArea.js
+++ b/src/components/AutosuggestTextBox/AutosuggestMentionTextArea.js
@@ -28,14 +28,16 @@ export default class AutosuggestMentionTextArea extends Component {
   }
 
   inputChanged = (inputText, downshift) => {
-    const searchOn = this.findMatch(inputText,
-      _get(this.props.inputRef, 'current.selectionStart'))
-    if (searchOn !== undefined && searchOn !== null) {
-      this.props.search(searchOn)
-      this.setState({showSuggestions: true})
-    }
-    else if (this.state.showSuggestions) {
-      this.setState({showSuggestions: false})
+    if (!this.props.disableShowSuggestions) {
+      const searchOn = this.findMatch(inputText,
+        _get(this.props.inputRef, 'current.selectionStart'))
+      if (searchOn !== undefined && searchOn !== null) {
+        this.props.search(searchOn)
+        this.setState({showSuggestions: true})
+      }
+      else if (this.state.showSuggestions) {
+        this.setState({showSuggestions: false})
+      }
     }
 
     // inputChanged() gets called with the item if an item is
diff --git a/src/components/ChallengeDetail/ChallengeDetail.js b/src/components/ChallengeDetail/ChallengeDetail.js
index 63758744..7278bf89 100644
--- a/src/components/ChallengeDetail/ChallengeDetail.js
+++ b/src/components/ChallengeDetail/ChallengeDetail.js
@@ -25,11 +25,18 @@ import WithChallengeTaskClusters from "../HOCs/WithChallengeTaskClusters/WithCha
 import WithTaskClusterMarkers from "../HOCs/WithTaskClusterMarkers/WithTaskClusterMarkers";
 import { fromLatLngBounds } from "../../services/MapBounds/MapBounds";
 import { ChallengeCommentsPane } from "./ChallengeCommentsPane";
+import SvgSymbol from "../SvgSymbol/SvgSymbol";
+import FlagModal from "./FlagModal";
 
 const ClusterMap = WithChallengeTaskClusters(
   WithTaskClusterMarkers(TaskClusterMap("challengeDetail"))
 );
 
+const FLAG_REPO_NAME = process.env.REACT_APP_GITHUB_ISSUES_API_REPO
+const FLAG_REPO_OWNER = process.env.REACT_APP_GITHUB_ISSUES_API_OWNER
+const FLAG_TOKEN = process.env.REACT_APP_GITHUB_ISSUES_API_TOKEN
+const FLAGGING_ACTIVE = FLAG_REPO_NAME && FLAG_REPO_OWNER && FLAG_TOKEN
+
 const DETAIL_TABS = {
   OVERVIEW: "OVERVIEW",
   COMMENTS: "COMMENTS",
@@ -45,11 +52,22 @@ const DETAIL_TABS = {
  */
 export class ChallengeDetail extends Component {
   state = {
-    detailTab: _isObject(this.props.user) && this.props.location.search.includes("conversation") ? DETAIL_TABS.COMMENTS : DETAIL_TABS.OVERVIEW
+    detailTab: _isObject(this.props.user) && this.props.location.search.includes("conversation") ? DETAIL_TABS.COMMENTS : DETAIL_TABS.OVERVIEW,
+    flagLoading: true,
+    issue: undefined,
+    displayInputError: false,
+    displayCheckboxError: false,
+    submittingFlag: false,
   };
 
   componentDidMount() {
-    window.scrollTo(0, 0);
+    window.scrollTo(0, 0)
+
+    const { url, params } = this.props.match
+
+    if (FLAGGING_ACTIVE && !url.includes('virtual')) {
+      this.queryForIssue(params.challengeId)
+    }
   }
 
   componentDidUpdate() {
@@ -58,10 +76,61 @@ export class ChallengeDetail extends Component {
     }
   }
 
+  queryForIssue = async (id) => {
+    this.setState({ flagLoading: true });
+
+    const owner = process.env.REACT_APP_GITHUB_ISSUES_API_OWNER
+    const repo = process.env.REACT_APP_GITHUB_ISSUES_API_REPO
+    const query = `q='Reported+Challenge+${encodeURIComponent('#') + id}'+in:title+state:open+repo:${owner}/${repo}`;
+    const response = await fetch(`https://api.github.com/search/issues?${query}`, {
+      method: 'GET',
+      headers: {
+        'Accept': 'application/vnd.github.text-match+json'
+      },
+    })
+
+    if (response.ok) {
+      const body = await response.json()
+      if (body?.total_count) {
+        this.setState({ issue: body.items[0] })
+      }
+
+      this.setState({ flagLoading: false })
+    }
+  }
+
   onClickTab = (detailTab) => {
     this.setState({ detailTab });
   };
 
+  onCancel = () => {
+    this.setState({ flagModal: false });
+  }
+
+  onModalSubmit = (data) => {
+    this.setState({ flagModal: false, displayInputError: false, issue: data });
+  }
+
+  handleInputError = () => {
+    this.setState({displayInputError: !this.state.displayInputError})
+  }
+
+  handleCheckboxError = () => {
+    this.setState({ displayInputError: false, displayCheckboxError: !this.state.displayCheckboxError})
+  }
+
+  handleViewCommentsSubmit = () => {
+     this.setState({ viewComments: true})
+  }
+
+  handleFlagClick = () => {
+    if (this.state.issue) {
+      window.open(this.state.issue?.html_url, "_blank")
+    } else {
+      this.setState({ flagModal: true })
+    }
+  }
+
   renderDetailTabs = () => {
     const challenge = this.props.browsedChallenge;
     if (!challenge.isVirtual) {
@@ -320,7 +389,32 @@ export class ChallengeDetail extends Component {
                   </div>
                 )}
                 <Taxonomy {...challenge} isSaved={isSaved} />
-                <h1 className="mr-card-challenge__title">{challenge.name}</h1>
+                <div className="mr-flex mr-items-center">
+                  <h1 className="mr-card-challenge__title mr-mr-3">{challenge.name}</h1>
+                  {FLAGGING_ACTIVE && !this.state.flagLoading && !challenge.isVirtual &&
+                    <div title={!this.state.issue ? 'Flag challenge' : 'View github issue'} className="mr-flex mr-align-center mr-cursor-pointer" onClick={this.handleFlagClick}>
+                      <SvgSymbol sym="flag-icon" viewBox="0 0 20 20" className={`mr-w-4 mr-h-4 mr-fill-current mr-mr-2${this.state.issue ? ' mr-fill-red-light mr-mt-4px' : ''}`} />
+                      {this.state.issue &&
+                        <div className="mr-text-red-light">
+                          <FormattedMessage {...messages.flaggedText} />
+                        </div>
+                      }
+                    </div>
+                  }
+                </div>
+                {this.state.flagModal &&
+                  <FlagModal
+                    {...this.props}
+                    challenge={challenge}
+                    onCancel={this.onCancel}
+                    onModalSubmit={this.onModalSubmit}
+                    handleInputError={this.handleInputError}
+                    displayInputError={this.state.displayInputError}
+                    displayCheckboxError={this.state.displayCheckboxError}
+                    handleCheckboxError={this.handleCheckboxError}
+                    handleViewCommentsSubmit={this.handleViewCommentsSubmit}
+                  />
+                }
 
                 {challenge.parent && ( // virtual challenges don't have projects
                   <Link
diff --git a/src/components/ChallengeDetail/FlagCommentInput.js b/src/components/ChallengeDetail/FlagCommentInput.js
new file mode 100644
index 00000000..63583bc7
--- /dev/null
+++ b/src/components/ChallengeDetail/FlagCommentInput.js
@@ -0,0 +1,173 @@
+import React, { Component } from "react";
+import classNames from "classnames";
+import { FormattedMessage, injectIntl } from "react-intl";
+import MarkdownContent from "../MarkdownContent/MarkdownContent";
+import AutosuggestMentionTextArea from "../AutosuggestTextBox/AutosuggestMentionTextArea";
+import { postChallengeComment } from "../../services/Challenge/ChallengeComments";
+import messages from "./Messages";
+
+export class FlagCommentInput extends Component {
+  state = {
+    showingPreview: false,
+    characterCount: 0,
+    value: '',
+    checked: false,
+    emailValue: this.props.user.settings.email || '',
+    submittingFlag: false
+  };
+
+  handleSubmit = async () => {
+    this.setState({ submittingFlag: true })
+
+    if (this.state.characterCount < 100) {
+      this.props.handleInputError()
+    } else if (!this.state.checked) {
+      this.props.handleCheckboxError()
+    } else {
+      const challenge = this.props.challenge
+      const owner = process.env.REACT_APP_GITHUB_ISSUES_API_OWNER
+      const repo = process.env.REACT_APP_GITHUB_ISSUES_API_REPO
+      const body = `Challenge: [#${challenge.id} - ${challenge.name}](${process.env.REACT_APP_URL}/browse/challenges/${challenge.id}) \n\n Reported by: [${this.props.user.osmProfile.displayName}](https://www.openstreetmap.org/user/${this.props.user.osmProfile.displayName})\n\n${this.state.value}`
+      const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/issues`, {
+        method: 'POST',
+        body: JSON.stringify({
+          title: `Reported Challenge #${challenge.id} - ${challenge.name}`,
+          owner,
+          repo,
+          body,
+          state: 'open'
+        }),
+        headers: {
+          'Authorization': `token ${process.env.REACT_APP_GITHUB_ISSUES_API_TOKEN}`,
+          'Content-Type': 'application/json',
+          'Accept': 'application/vnd.github.v3+json',
+        },
+      })
+
+      if (response.ok) {
+        const responseBody = await response.json()
+        this.props.onModalSubmit(responseBody)
+        const issue_link = responseBody.html_url
+        const comment = `This challenge has been flagged by [${this.props.user.osmProfile.displayName}](${process.env.REACT_APP_OSM_SERVER}/user/${this.props.user.osmProfile.displayName}). Please use [this GitHub issue](${issue_link}) to discuss. \n\n ${this.state.value}`
+        await postChallengeComment(challenge.id, comment)
+        this.props.handleViewCommentsSubmit()
+      }
+    }
+
+    this.setState({ submittingFlag: false })
+  };
+
+  handleChange = (val) => {
+    if (val.length <= 1000) {
+      this.setState({ ...this.state, value: val, characterCount: val.length });
+    }
+  };
+
+  handleToggle = () => {
+    this.setState({ checked: !this.state.checked })
+  };
+
+  render() {
+    const maxCharacterCount = 1000
+    const minCharacterCount = 100
+    return (
+      <div className="mr-mt-2">
+        <label className="mr-text-white-50">
+        <FormattedMessage {...messages.email} />
+        </label>
+        <input className="form-control mr-mb-4" type="email" id="root_email" label="Email address" placeholder="Enter your email" value={this.state.emailValue} onChange={(event) => this.setState({ emailValue: event.target.value })} />
+        <div className="mr-flex mr-justify-between mr-mb-2 mr-leading-tight mr-text-xxs">
+          <div className="mr-flex mr-items-center">
+            <button
+              className={classNames(
+                "mr-pr-2 mr-mr-2 mr-border-r mr-border-green mr-uppercase mr-font-medium",
+                this.state.showingPreview
+                  ? "mr-text-green-lighter"
+                  : "mr-text-white"
+              )}
+              onClick={() => this.setState({ showingPreview: false })}
+            >
+              <FormattedMessage {...messages.write} />
+            </button>
+            <button
+              className={classNames(
+                "mr-uppercase mr-font-medium",
+                !this.state.showingPreview
+                  ? "mr-text-green-lighter"
+                  : "mr-text-white"
+              )}
+              onClick={() => this.setState({ showingPreview: true })}
+            >
+              <FormattedMessage {...messages.preview} />
+            </button>
+          </div>
+          <div
+            className={classNames({
+              "mr-text-dark-yellow":
+                this.state.characterCount < maxCharacterCount &&
+                this.state.characterCount > maxCharacterCount * 0.9,
+              "mr-text-red-light":
+                this.state.characterCount >= maxCharacterCount || this.state.characterCount < minCharacterCount
+            })}
+          >
+            {this.state.characterCount}/{maxCharacterCount}
+          </div>
+        </div>
+        {this.state.showingPreview ? (
+          <div
+            className={
+              this.props.previewClassName
+                ? this.props.previewClassName
+                : "mr-border-2 mr-rounded mr-border-black-15 mr-px-2 mr-min-h-8"
+            }
+          >
+            <MarkdownContent markdown={this.state.value} />
+          </div>
+        ) : (
+          <AutosuggestMentionTextArea
+            inputClassName="mr-appearance-none mr-outline-none mr-input mr-text-white mr-placeholder-medium mr-bg-grey-lighter-10 mr-border-none mr-shadow-inner mr-p-3 mr-font-mono mr-text-sm"
+            previewClassName="mr-border-2 mr-rounded mr-border-grey-lighter-10 mr-p-2 mr-max-h-48 mr-overflow-y-scroll"
+            rows={4}
+            cols="1"
+            inputValue={this.state.value}
+            onInputValueChange={this.handleChange}
+            placeholder='Enter text here'
+            disableResize={true}
+            search={() => null}
+            disableShowSuggestions
+          />
+        )}
+        <div className="form mr-flex mr-items-baseline">
+          <input
+            type="checkbox"
+            className="mr-mr-2"
+            checked={this.state.checked}
+            onChange={this.handleToggle}
+          />
+          <label className="mr-text-white-50">
+            <FormattedMessage {...messages.review} />
+          </label>
+        </div>
+        {this.props.displayInputError &&
+          <div className="mr-text-red">
+            <FormattedMessage {...messages.textInputError} />
+          </div>}
+        {this.props.displayCheckboxError &&
+          <div className="mr-text-red">
+            <FormattedMessage {...messages.checkboxError} />
+          </div>}
+        <div className="mr-flex mr-items-center mr-mt-6">
+          <button
+            className="mr-button mr-button--white mr-mr-12 mr-px-8"
+            onClick={this.handleSubmit}
+            disabled={this.state.submittingFlag}
+          >
+            <FormattedMessage {...messages.submitFlag} />
+          </button>
+        </div>
+      </div>
+    );
+  }
+}
+
+export default injectIntl(FlagCommentInput);
diff --git a/src/components/ChallengeDetail/FlagModal.js b/src/components/ChallengeDetail/FlagModal.js
new file mode 100644
index 00000000..9cef4d61
--- /dev/null
+++ b/src/components/ChallengeDetail/FlagModal.js
@@ -0,0 +1,29 @@
+import React from "react";
+import External from "../External/External";
+import Modal from "../Modal/Modal";
+import FlagCommentInput from "./FlagCommentInput";
+import messages from "./Messages";
+import { FormattedMessage } from "react-intl";
+
+const FlagModel = (props) => {
+  return (
+    <External>
+      <Modal
+        contentClassName="mr-pb-6"
+        isActive
+        allowOverflow
+        onClose={props.onCancel}
+      >
+        <h2 className="mr-text-grey-light-more mr-text-4xl mr-mt-4">
+          <FormattedMessage {...messages.email} />
+        </h2>
+        <div className="mr-text-base mr-mt-4 mr-text-yellow">
+         <FormattedMessage {...messages.modalSubtitle} />
+        </div>
+        <FlagCommentInput challenge={props.challenge} {...props} />
+      </Modal>
+    </External>
+  )
+}
+
+export default FlagModel;
diff --git a/src/components/ChallengeDetail/Messages.js b/src/components/ChallengeDetail/Messages.js
index 3df365b1..074a5860 100644
--- a/src/components/ChallengeDetail/Messages.js
+++ b/src/components/ChallengeDetail/Messages.js
@@ -82,5 +82,50 @@ export default defineMessages({
   overpassQL: {
     id: "ChallengeDetails.fields.overpassQL.label",
     defaultMessage: "Overpass Query"
+  },
+
+  write: {
+    id: "ChallengeDetails.controls.write.label",
+    defaultMessage: "Write"
+  },
+
+  preview: {
+    id: "ChallengeDetails.controls.preview.label",
+    defaultMessage: "Preview"
+  },
+
+  review: {
+    id: "ChallengeDetails.controls.review.label",
+    defaultMessage: "I have attempted to contact the Challenge creator"
+  },
+
+  modalSubtitle: {
+    id: "ChallengeDetails.controls.modal.subtitle",
+    defaultMessage:  "You are about to flag a Challenge. An issue will be created here and the Challenge creator will be notified by email. Any follow-up discussion should take place there. Flagging a Challenge does not disable it immediately. Please explain in detail what your issue is with this challenge, if possible linking to specific OSM changesets."
+  },
+
+  submitFlag: {
+    id: "ChallengeDetails.controls.submit.flag.label",
+    defaultMessage: "Flag Challenge"
+  },
+
+  textInputError: {
+    id: "ChallengeDetails.controls.text.input.error",
+    defaultMessage: "Text Input should have minimum 100 characters"
+  },
+
+  checkboxError: {
+    id: "ChallengeDetails.controls.checkbox.error",
+    defaultMessage: "Please ensure that checkbox is checked before continue"
+  },
+
+  flaggedText: {
+    id: "ChallengeDetails.controls.flagged_text",
+    defaultMessage: "This challenge has been flagged"
+  },
+
+  email: {
+    id: "ChallengeDetails.controls.email",
+    defaultMessage: "Email"
   }
 })
diff --git a/src/components/HOCs/WithSearch/WithSearch.js b/src/components/HOCs/WithSearch/WithSearch.js
index 5f8f9cb1..bb3cf551 100644
--- a/src/components/HOCs/WithSearch/WithSearch.js
+++ b/src/components/HOCs/WithSearch/WithSearch.js
@@ -7,7 +7,7 @@ import _isFunction from 'lodash/isFunction'
 import _isEmpty from 'lodash/isEmpty'
 import _isEqual from 'lodash/isEqual'
 import _includes from 'lodash/includes'
-import { SORT_NAME, SORT_CREATED, SORT_OLDEST, SORT_POPULARITY, SORT_COOPERATIVE_WORK, SORT_COMPLETION, SORT_TASKS_REMAINING,
+import { SORT_NAME, SORT_CREATED, SORT_OLDEST, SORT_POPULARITY, SORT_COOPERATIVE_WORK, SORT_COMPLETION, SORT_TASKS_REMAINING, SORT_SCORE,
          setSort, removeSort, setPage,
          setFilters, removeFilters, clearFilters,
          setSearch, clearSearch,
@@ -170,6 +170,9 @@ export const mapDispatchToProps = (dispatch, ownProps, searchGroup) => ({
       case SORT_COOPERATIVE_WORK:
         sort = {sortBy, direction: 'desc'}
         break
+      case SORT_SCORE:
+        sort = { sortBy, direction: 'desc' }
+        break
       default:
         sort = {sortBy: null, direction: null}
         break
diff --git a/src/components/Navbar/Navbar.js b/src/components/Navbar/Navbar.js
index 6f9cf922..8e1fdb1f 100644
--- a/src/components/Navbar/Navbar.js
+++ b/src/components/Navbar/Navbar.js
@@ -60,6 +60,7 @@ export default class Navbar extends Component {
   }
 
   render() {
+    const isSuperUser = AsManager(this.props.user).isSuperUser()
     return (
       <header className="mr-relative mr-bg-gradient-r-green-blue mr-shadow mr-p-6 mr-flex mr-items-center mr-justify-between">
         <nav className="mr-flex mr-items-center">
@@ -152,6 +153,7 @@ export default class Navbar extends Component {
               {...this.props}
               signout={this.signout}
               closeMobileMenu={this.closeMobileMenu}
+              isSuperUser={isSuperUser}
             />
           </MobileMenu>
 
@@ -360,6 +362,13 @@ const MobileNav = props => (
             <FormattedMessage {...messages.metrics} />
           </NavLink>
         </li>
+        {props.isSuperUser ? (
+          <li>
+            <NavLink to='/superadmin' onClick={props.closeMobileMenu}>
+              <FormattedMessage {...messages.superAdmin} />
+            </NavLink>
+          </li>
+        ) : null}
         <li>
           <NavLink to="/user/achievements" onClick={props.closeMobileMenu}>
             <FormattedMessage {...messages.achievements} />
diff --git a/src/components/SuperAdmin/Messages.js b/src/components/SuperAdmin/Messages.js
index 93a193b0..4ea7e320 100644
--- a/src/components/SuperAdmin/Messages.js
+++ b/src/components/SuperAdmin/Messages.js
@@ -7,9 +7,9 @@ export default defineMessages({
     id: 'Metrics.header',
     defaultMessage: 'Metrics',
   },
-  visible: {
+  discoverable: {
     id: 'Metrics.discoverable',
-    defaultMessage: 'Visible',
+    defaultMessage: 'Discoverable',
   },
   archived: {
     id: 'Metrics.archived',
@@ -19,24 +19,36 @@ export default defineMessages({
     id: 'Metrics.virtual',
     defaultMessage: 'Virtual',
   },
-  download: {
-    id: 'Metrics.download',
-    defaultMessage: 'download',
+  hideUndiscoverable: {
+    id: 'Metrics.hideUndiscoverable',
+    defaultMessage: 'Hide Undiscoverable'
+  },
+  hideArchived: {
+    id: 'Metrics.hideArchived',
+    defaultMessage: 'Hide Archived'
   },
   download: {
     id: 'Metrics.download',
     defaultMessage: 'download',
   },
+  clear: {
+    id: 'Metrics.clear',
+    defaultMessage: 'clear'
+  },
   challengeLabel: {
-    id: 'Metrics.challenge',
-    defaultMessage: 'Challenge',
+    id: 'Metrics.challenges',
+    defaultMessage: 'Challenges',
   },
   projectLabel: {
-    id: 'Metrics.project',
-    defaultMessage: 'Project',
+    id: 'Metrics.projects',
+    defaultMessage: 'Projects',
   },
   userLabel: {
-    id: 'Metrics.user',
-    defaultMessage: 'User',
-  }
+    id: 'Metrics.users',
+    defaultMessage: 'Users',
+  },
+  sortByLabel: {
+    id: 'Metrics.sortBy',
+    defaultMessage: 'Sort By'
+  },
 })
diff --git a/src/components/SuperAdmin/MetricsData.js b/src/components/SuperAdmin/MetricsData.js
index 1062d0f7..1a7d7c1a 100644
--- a/src/components/SuperAdmin/MetricsData.js
+++ b/src/components/SuperAdmin/MetricsData.js
@@ -1,236 +1,286 @@
 import React from 'react'
 import { FormattedDate } from 'react-intl'
-import AsManageableProject from '../../interactions/Project/AsManageableProject'
 
 const OSM_USER_LINK = `${process.env.REACT_APP_OSM_SERVER}/user/`
 
-// Total Number of Tasks, Number of user engaged in task
 const setChallengeTab = () => {
   return [
     {
       id: 'id',
       Header: 'ID',
       maxWidth: 80,
-      accessor: challenge => challenge.id,
+      accessor: (challenge) => challenge.id,
     },
     {
       id: 'name',
       Header: 'NAME',
-      accessor: challenge => challenge.name,
-      Cell: props => {
+      accessor: (challenge) => challenge.name,
+      Cell: (props) => {
         if (props.value) {
           return (
-            <a href={`/admin/project/${props.original.parent}` +
-            `/challenge/${props.original.id}`}> {props.value} </a>
+            <a
+              href={
+                `/admin/project/${props.original?.parent?.id}` +
+                `/challenge/${props.original.id}`
+              }
+              target='_blank'
+              rel='noopener noreferrer'
+            >
+              {props.value}
+            </a>
           )
         }
 
         return null
-      }
+      },
+      maxWidth: 180,
+      sortable: true,
     },
     {
       id: 'owner',
       Header: 'OWNER',
-      accessor: challenge => challenge.owner,
-      maxWidth: 100,
+      accessor: (challenge) => challenge.owner,
     },
     {
       id: 'numOfTasks',
       Header: 'TASKS REMAINING',
-      accessor: challenge => challenge.tasksRemaining,
+      accessor: (challenge) => challenge.tasksRemaining,
       maxWidth: 150,
     },
     {
       id: 'tasksCompletionPercentage',
       Header: '% COMPLETED TASKS',
-      accessor: challenge => {
+      accessor: (challenge) => {
         return challenge.completionPercentage
       },
       sortable: true,
       maxWidth: 180,
-      Cell: props => <div>{props.value}%</div>
+      Cell: (props) => <div>{props.value}%</div>,
     },
     {
       id: 'project',
       Header: 'PROJECT',
-      accessor: challenge => challenge.parent,
+      accessor: (challenge) => challenge.parent?.displayName,
       maxWidth: 120,
       sortable: true,
-      Cell: props => {
+      Cell: (props) => {
         if (props.value) {
           return (
-            <a href={`/admin/project/${props.value}`}> {props.value} </a>
+            <a
+              href={`/admin/project/${props.original?.parent?.id}`}
+              target='_blank'
+              rel='noopener noreferrer'
+            >
+              {props.value}
+            </a>
           )
         }
-
         return null
-      }
+      },
     },
     {
-      id: 'visible',
-      Header: 'VISIBLE',
-      accessor: challenge => challenge.enabled.toString(),
-      maxWidth: 120,
+      id: 'discoverable',
+      Header: 'DISCOVERABLE',
+      accessor: (challenge) => challenge.enabled.toString(),
+      maxWidth: 150,
     },
     {
       id: 'archived',
       Header: 'ARCHIVED',
-      accessor: challenge => challenge.isArchived.toString(),
+      accessor: (challenge) => challenge.isArchived.toString(),
       maxWidth: 120,
     },
     {
       id: 'dateCreated',
       Header: 'DATE CREATED',
-      accessor: challenge => {
+      accessor: (challenge) => {
         return challenge.created
       },
       maxWidth: 150,
       sortable: true,
-      Cell: props => (
-        !props.value ? null :
+      Cell: (props) =>
+        !props.value ? null : (
           <span>
             <FormattedDate value={props.value} />
           </span>
-      )
+        ),
     },
     {
       id: 'dataOriginDate',
       Header: 'DATA ORIGIN DATE',
-      accessor: challenge => challenge.dataOriginDate,
+      accessor: (challenge) => challenge.dataOriginDate,
       maxWidth: 180,
       sortable: true,
-      Cell: props => (
-        !props.value ? null :
+      Cell: (props) =>
+        !props.value ? null : (
           <span>
             <FormattedDate value={props.value} />
           </span>
-      )
+        ),
     },
     {
       id: 'lastTaskRefreshDate',
       Header: 'LAST TASK REFRESH',
-      accessor: challenge => challenge.lastTaskRefresh,
+      accessor: (challenge) => challenge.lastTaskRefresh,
       maxWidth: 180,
       sortable: true,
-      Cell: props => (
-        !props.value ? null :
+      Cell: (props) =>
+        !props.value ? null : (
           <span>
             <FormattedDate value={props.value} />
           </span>
-      )
-    }
+        ),
+    },
   ]
 }
 
-const setProjectTab = (challenges) => {
+const setProjectTab = () => {
   return [
     {
       id: 'id',
       Header: 'ID',
       maxWidth: 80,
-      accessor: project => project.id
+      accessor: (project) => project.id,
     },
-
     {
       id: 'name',
       Header: 'NAME',
-      accessor: project => {
-        return <a href={`/admin/project/${project.id}`}> {project.displayName} </a>
+      accessor: (project) => project.displayName,
+      Cell: (props) => {
+        if (props.value) {
+          return (
+            <a
+              href={`/admin/project/${props.original.id}`}
+              target='_blank'
+              rel='noopener noreferrer'
+            >
+              {props.value}
+            </a>
+          )
+        }
+        return null
       },
+      sortable: true,
+      maxWidth: 180,
     },
     {
       id: 'owner',
       Header: 'OWNER',
-      accessor: project => project.owner,
-      maxWidth: 100,
+      accessor: (project) => project.owner,
+      maxWidth: 120,
     },
     {
-      id: 'numOfChallenge',
-      Header: '# OF CHALLENGES',
-      accessor: project => {
-        const projectManage= AsManageableProject(project)
-        return(projectManage.childChallenges(challenges).length)
-      },
+      id: 'discoverable',
+      Header: 'DISCOVERABLE',
+      accessor: (project) => project.enabled.toString(),
       maxWidth: 150,
     },
-    {
-      id: 'visible',
-      Header: 'VISIBLE',
-      accessor: project => project.enabled.toString(),
-      maxWidth: 120,
-    },
     {
       id: 'archived',
       Header: 'ARCHIVED',
-      accessor: project => project.isArchived.toString(),
+      accessor: (project) => project.isArchived.toString(),
       maxWidth: 120,
     },
     {
       id: 'Virtual',
       Header: 'VIRTUAL',
-      accessor: project => project.isVirtual.toString(),
+      accessor: (project) => project.isVirtual.toString(),
       maxWidth: 120,
     },
     {
       id: 'dateCreated',
       Header: 'DATE CREATED',
-      accessor: project => {
-        return <FormattedDate value={project.created} />
+      accessor: (project) => {
+        return project.created
       },
       maxWidth: 150,
-      sortable: false
+      sortable: true,
+      Cell: (props) =>
+        !props.value ? null : (
+          <span>
+            <FormattedDate value={props.value} />
+          </span>
+        ),
     },
     {
       id: 'dateLastModified',
       Header: 'DATE LAST MODIFIED',
-      accessor: project => {
-        return <FormattedDate value={project.modified} />
+      accessor: (project) => {
+        return project.modified
       },
-      maxWidth: 180,
-      sortable: false
-    }
+      maxWidth: 150,
+      sortable: true,
+      Cell: (props) =>
+        !props.value ? null : (
+          <span>
+            <FormattedDate value={props.value} />
+          </span>
+        ),
+    },
   ]
 }
 
-// number of challenges participated, total task completed, total days active
 const setUserTab = () => {
   return [
     {
       id: 'id',
       Header: 'ID',
       maxWidth: 80,
-      accessor: user => user.id,
+      accessor: (user) => user.id,
     },
     {
       id: 'name',
       Header: 'NAME',
-      accessor: user => user.osmProfile.displayName,
-      Cell: cell => <a href={OSM_USER_LINK + cell.value} target='_blank' rel='noreferrer' > {cell.value} </a>,
-      maxWidth: 100,
+      accessor: (user) => user.displayName,
+      Cell: (cell) => (
+        <a
+          href={OSM_USER_LINK + cell.value}
+          target='_blank'
+          rel='noopener noreferrer'
+        >
+          {cell.value}
+        </a>
+      ),
+      sortable: true,
+      maxWidth: 180,
     },
     {
       id: 'score',
       Header: 'SCORE',
-      accessor: user => user.score,
+      accessor: (user) => user.score,
       maxWidth: 120,
     },
     {
       id: 'dateCreated',
       Header: 'DATE CREATED',
-      accessor: user => {
-        return <FormattedDate {...user.created} />
+      accessor: (user) => {
+        return user.created
       },
       maxWidth: 150,
+      sortable: true,
+      Cell: (props) =>
+        !props.value ? null : (
+          <span>
+            <FormattedDate value={props.value} />
+          </span>
+        ),
     },
     {
       id: 'lastActive',
       Header: 'DATE LAST ACTIVE',
-      accessor: user => {
-        return <FormattedDate {...user.modified} />
+      accessor: (user) => {
+        return user.modified
       },
-      maxWidth: 180,
-    }
+      maxWidth: 150,
+      sortable: true,
+      Cell: (props) =>
+        !props.value ? null : (
+          <span>
+            <FormattedDate value={props.value} />
+          </span>
+        ),
+    },
   ]
 }
 
-export { setChallengeTab, setProjectTab, setUserTab}
+export { setChallengeTab, setProjectTab, setUserTab }
diff --git a/src/components/SuperAdmin/MetricsFilterToggle.js b/src/components/SuperAdmin/MetricsFilterToggle.js
deleted file mode 100644
index 6769604f..00000000
--- a/src/components/SuperAdmin/MetricsFilterToggle.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import React from 'react'
-import { FormattedMessage } from 'react-intl'
-import messages from './Messages'
-const MetricsFilterToggle = (props) => {
-  const filter = props.filterName
-  return(
-     <div className='mr-flex mr-leading-none'>
-            <input
-              type='checkbox'
-              className='mr-checkbox-toggle mr-ml-4 mr-mr-1'
-              checked={props.showingFilter}
-              onChange={() => {
-                props.setSearchFilters({ [filter]: !props.showingFilter })
-              }}
-            />
-            <div className="mr-text-sm mr-mx-1"><FormattedMessage {...messages.archived} /></div>
-          </div>
-  )
-}
-
-export default MetricsFilterToggle
\ No newline at end of file
diff --git a/src/components/SuperAdmin/MetricsHeader.js b/src/components/SuperAdmin/MetricsHeader.js
index 1d7c38cd..1ec0b0b8 100644
--- a/src/components/SuperAdmin/MetricsHeader.js
+++ b/src/components/SuperAdmin/MetricsHeader.js
@@ -5,11 +5,20 @@ import messages from './Messages'
 import SortChallengesSelector from '../ChallengePane/ChallengeFilterSubnav/SortChallengesSelector'
 import FilterByDifficulty from '../ChallengePane/ChallengeFilterSubnav/FilterByDifficulty'
 import FilterByKeyword from '../ChallengePane/ChallengeFilterSubnav/FilterByKeyword'
+import SortProjectsSelector from './SortProjectsSelector'
+import SortUsersSelector from './SortUsersSelector'
 
 const MetricsHeader = (props) => {
-
   const handleTabToggle = (val) => {
-    props.setCurrentTab(val)
+    props.clearSearchFilters()
+    props.clearSearch()
+    props.clearDate()
+    const searchQuery = `?tab=${val}&searchType=${val}`
+    props.history.push({
+      pathname: '/superadmin',
+      search: searchQuery,
+    })
+    props.setSearchSort({ sortBy: 'default' })
   }
 
   return (
@@ -18,7 +27,7 @@ const MetricsHeader = (props) => {
         <h1 className='mr-hidden xl:mr-flex mr-text-3xl mr-leading-tight mr-font-normal mr-mr-6'>
           <FormattedMessage {...messages.header} />
         </h1>
-        <div className={'mr-flex mr-items-center ' + (props.currentTab !== 'challenge' ? 'mr-py-6' : '')}>
+        <div className={'mr-flex mr-items-center '}>
           <div className='admin__manage__controls mr-flex'>
             <button
               className='mr-button mr-button--dark mr-button--small mr-mr-4'
@@ -26,16 +35,37 @@ const MetricsHeader = (props) => {
             >
               <FormattedMessage {...messages.challengeLabel} />
             </button>
+            <button
+              className='mr-button mr-button--dark mr-button--small mr-mr-4'
+              onClick={() => handleTabToggle('projects')}
+            >
+              <FormattedMessage {...messages.projectLabel} />
+            </button>
+            <button
+              className='mr-button mr-button--dark mr-button--small mr-mr-4'
+              onClick={() => handleTabToggle('users')}
+            >
+              <FormattedMessage {...messages.userLabel} />
+            </button>
           </div>
-          {props.currentTab === 'challenges' && <>
-            <SortChallengesSelector {...props} />
-            <FilterByKeyword {...props} />
-            <FilterByDifficulty {...props} />
-          </>}
-          <SearchBox 
-            {...props}
-            setSearch={props.setSearch}
-          />
+          {props.currentTab === 'challenges' && (
+            <>
+              <SortChallengesSelector {...props} />
+              <FilterByKeyword {...props} />
+              <FilterByDifficulty {...props} />
+            </>
+          )}
+          {props.currentTab === 'projects' && (
+            <>
+              <SortProjectsSelector {...props} />
+            </>
+          )}
+          {props.currentTab === 'users' && (
+            <>
+              <SortUsersSelector {...props} />
+            </>
+          )}
+          <SearchBox {...props} setSearch={props.setSearch} />
         </div>
       </div>
     </header>
diff --git a/src/components/SuperAdmin/MetricsTable.js b/src/components/SuperAdmin/MetricsTable.js
index a3ce1cc4..3195b400 100644
--- a/src/components/SuperAdmin/MetricsTable.js
+++ b/src/components/SuperAdmin/MetricsTable.js
@@ -1,10 +1,10 @@
-
 import React from 'react'
-import WithPagedChallenges from '../HOCs/WithPagedChallenges/WithPagedChallenges'
 import WithSortedChallenges from '../HOCs/WithSortedChallenges/WithSortedChallenges'
-import WithSearchResults from '../HOCs/WithSearchResults/WithSearchResults'
+import WithMetricsSearchResults from './WithMetricsSearchResults'
+import WithSortedProjects from './WithSortedProjects'
+import WithSortedUsers from './WithSortedUsers'
 import ReactTable from 'react-table-6'
-import { setChallengeTab, setProjectTab } from './MetricsData'
+import { setChallengeTab, setProjectTab, setUserTab } from './MetricsData'
 import { injectIntl } from 'react-intl'
 import BusySpinner from '../BusySpinner/BusySpinner'
 
@@ -12,27 +12,68 @@ const MetricsTable = (props) => {
   let data
   const constructHeader = () => {
     if (props.currentTab === 'challenges') {
-      data = props.challenges
-      return setChallengeTab()
-    }
-    else if (props.currentTab === 'projects') {
-      data = props.projects
-      return setProjectTab(props.challenges)
+      data = props.challenges.map((c) => ({
+        id: c.id,
+        name: c.name,
+        parent: c.parent,
+        owner: c.owner,
+        tasksRemaining: c.tasksRemaining,
+        completionPercentage: c.completionPercentage,
+        enabled: c.enabled,
+        isArchived: c.isArchived,
+        created: c.created,
+        dataOriginDate: c.dataOriginDate,
+        lastTaskRefresh: c.lastTaskRefresh,
+      }))
+
+      return setChallengeTab(props)
+    } else if (props.currentTab === 'projects') {
+      data = props.projects.map((p) => ({
+        id: p.id,
+        displayName: p.displayName,
+        owner: p.owner,
+        enabled: p.enabled,
+        isArchived: p.isArchived,
+        isVirtual: p.isVirtual,
+        created: p.created,
+        modified: p.modified,
+      }))
+
+      return setProjectTab(props)
+    } else if (props.currentTab === 'users') {
+      data = props.users.map((u) => ({
+        id: u.id,
+        displayName: u.osmProfile.displayName,
+        score: u.score,
+        created: u.created,
+        modified: u.modified,
+      }))
+
+      return setUserTab()
     }
   }
+
   return !props.isloadingCompleted ? (
     <div className='admin mr-flex mr-justify-center mr-py-8 mr-w-full mr-bg-blue'>
       <BusySpinner />
     </div>
   ) : (
-    <ReactTable
-      columns={constructHeader()}
-      data={data}
-      defaultPageSize={50}
-    />
+    <ReactTable columns={constructHeader()} data={data} defaultPageSize={50} />
   )
 }
 
-export default WithSearchResults(WithSortedChallenges(
-  WithPagedChallenges(injectIntl(MetricsTable),'challenges', 'pagedChallenges'), 'challenges', null, { frontendSearch: true }
-),'challenges', 'challenges')
+export default WithMetricsSearchResults(
+  WithSortedUsers(
+    WithSortedProjects(
+      WithSortedChallenges(injectIntl(MetricsTable), 'challenges', null, {
+        frontendSearch: true,
+      }),
+      'projects',
+      null
+    ),
+    'users',
+    null
+  ),
+  'challenges',
+  'challenges'
+)
diff --git a/src/components/SuperAdmin/SortProjectsSelector.js b/src/components/SuperAdmin/SortProjectsSelector.js
new file mode 100644
index 00000000..f3e318b6
--- /dev/null
+++ b/src/components/SuperAdmin/SortProjectsSelector.js
@@ -0,0 +1,72 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import { FormattedMessage, injectIntl } from 'react-intl'
+import _map from 'lodash/map'
+import _get from 'lodash/get'
+import {
+  sortLabels,
+  SORT_DEFAULT,
+  PROJECT_SORT_OPTIONS,
+} from '../../services/Search/Search'
+import Dropdown from '../Dropdown/Dropdown'
+import ButtonFilter from '../ChallengePane/ChallengeFilterSubnav/ButtonFilter'
+import messages from './Messages'
+/**
+ * SortProjectsSelector renders an unmanaged dropdown button that can be used
+ * to modify the sort order of project results.
+ */
+export class SortProjectsSelector extends Component {
+  makeSelection = (option, closeDropdownMenu) => {
+    this.props.setSearchSort({ sortBy: option })
+    closeDropdownMenu()
+  }
+
+  render() {
+    const localizedLabels = sortLabels(this.props.intl)
+    const currentSortCriteria = _get(this.props, 'searchSort.sortBy')
+    const activeLabel = currentSortCriteria
+      ? localizedLabels[currentSortCriteria]
+      : localizedLabels[SORT_DEFAULT]
+
+    return (
+      <Dropdown
+        className='mr-dropdown--flush xl:mr-border-l xl:mr-border-white-10 mr-p-6 mr-pl-0 xl:mr-pl-6'
+        dropdownButton={(dropdown) => (
+          <ButtonFilter
+            type={<FormattedMessage {...messages.sortByLabel} />}
+            selection={activeLabel}
+            onClick={dropdown.toggleDropdownVisible}
+          />
+        )}
+        dropdownContent={(dropdown) => (
+          <ListSortItems
+            sortLabels={localizedLabels}
+            makeSelection={this.makeSelection}
+            closeDropdown={dropdown.closeDropdown}
+          />
+        )}
+      />
+    )
+  }
+}
+
+const ListSortItems = function (props) {
+  const menuItems = _map(PROJECT_SORT_OPTIONS, (sortByOption) => (
+    <li key={sortByOption}>
+      <a onClick={() => props.makeSelection(sortByOption, props.closeDropdown)}>
+        {props.sortLabels[sortByOption]}
+      </a>
+    </li>
+  ))
+
+  return (
+    <ol className='mr-list-dropdown mr-list-dropdown--ruled'>{menuItems}</ol>
+  )
+}
+
+SortProjectsSelector.propTypes = {
+  /** Invoked to sort the challenges when a value is selected */
+  setSearchSort: PropTypes.func.isRequired,
+}
+
+export default injectIntl(SortProjectsSelector)
diff --git a/src/components/SuperAdmin/SortUsersSelector.js b/src/components/SuperAdmin/SortUsersSelector.js
new file mode 100644
index 00000000..53a69232
--- /dev/null
+++ b/src/components/SuperAdmin/SortUsersSelector.js
@@ -0,0 +1,71 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import { FormattedMessage, injectIntl } from 'react-intl'
+import _map from 'lodash/map'
+import _get from 'lodash/get'
+import {
+  sortLabels,
+  SORT_DEFAULT,
+  USER_SORT_OPTIONS,
+} from '../../services/Search/Search'
+import Dropdown from '../Dropdown/Dropdown'
+import ButtonFilter from '../ChallengePane/ChallengeFilterSubnav/ButtonFilter'
+import messages from './Messages'
+/**
+ * SortUsersSelector renders an unmanaged dropdown button that can be used
+ * to modify the sort order of user results.
+ */
+export class SortUsersSelector extends Component {
+  makeSelection = (option, closeDropdownMenu) => {
+    this.props.setSearchSort({ sortBy: option })
+    closeDropdownMenu()
+  }
+
+  render() {
+    const localizedLabels = sortLabels(this.props.intl)
+    const currentSortCriteria = _get(this.props, 'searchSort.sortBy')
+    const activeLabel = currentSortCriteria
+      ? localizedLabels[currentSortCriteria]
+      : localizedLabels[SORT_DEFAULT]
+    return (
+      <Dropdown
+        className='mr-dropdown--flush xl:mr-border-l xl:mr-border-white-10 mr-p-6 mr-pl-0 xl:mr-pl-6'
+        dropdownButton={(dropdown) => (
+          <ButtonFilter
+            type={<FormattedMessage {...messages.sortByLabel} />}
+            selection={activeLabel}
+            onClick={dropdown.toggleDropdownVisible}
+          />
+        )}
+        dropdownContent={(dropdown) => (
+          <ListSortItems
+            sortLabels={localizedLabels}
+            makeSelection={this.makeSelection}
+            closeDropdown={dropdown.closeDropdown}
+          />
+        )}
+      />
+    )
+  }
+}
+
+const ListSortItems = function (props) {
+  const menuItems = _map(USER_SORT_OPTIONS, (sortByOption) => (
+    <li key={sortByOption}>
+      <a onClick={() => props.makeSelection(sortByOption, props.closeDropdown)}>
+        {props.sortLabels[sortByOption]}
+      </a>
+    </li>
+  ))
+
+  return (
+    <ol className='mr-list-dropdown mr-list-dropdown--ruled'>{menuItems}</ol>
+  )
+}
+
+SortUsersSelector.propTypes = {
+  /** Invoked to sort the challenges when a value is selected */
+  setSearchSort: PropTypes.func.isRequired,
+}
+
+export default injectIntl(SortUsersSelector)
diff --git a/src/components/SuperAdmin/SuperAdmin.js b/src/components/SuperAdmin/SuperAdmin.js
index fa6efc3d..c5905168 100644
--- a/src/components/SuperAdmin/SuperAdmin.js
+++ b/src/components/SuperAdmin/SuperAdmin.js
@@ -1,7 +1,6 @@
 import React from 'react'
 import PropTypes from 'prop-types'
 import { FormattedMessage } from 'react-intl'
-import { useState } from 'react'
 import AsManager from '../../interactions/User/AsManager'
 import SignIn from '../../pages/SignIn/SignIn'
 import MetricsTable from './MetricsTable'
@@ -9,19 +8,57 @@ import BusySpinner from '../BusySpinner/BusySpinner'
 import internalFilterToggle from './internalFilterToggle'
 import MetricsHeader from './MetricsHeader'
 import messages from './Messages'
+import { useEffect } from 'react'
+import queryString from 'query-string'
+import DatePicker from 'react-datepicker'
+import { useState } from 'react'
+import SvgSymbol from '../SvgSymbol/SvgSymbol'
 /**
  * SuperAdminPane is the top-level component for super administration functions. It has a
- * User/Project/Challenge metrics tab for management of users, projects and challenges, and display of various summary metrics. 
+ * User/Project/Challenge metrics tab for management of users, projects and challenges, and display of various summary metrics.
  * It's worth noting that only super admins have access to SuperAdminPane.
  *
  */
 export const SuperAdminPane = (props) => {
-  const [currentTab, setCurrentTab] = useState('challenges')
+  const [startDate, setStartDate] = useState(null)
+  const [endDate, setEndDate] = useState(null)
+
+  const params = queryString.parse(props.location.search)
+  const currentTab = params['tab'] ? params['tab'] : 'challenges'
+
+  const formatDateFromTab = (date) => {
+    const offset = date.getTimezoneOffset()
+    date = new Date(date.getTime() + offset * 60 * 1000)
+    return date
+  }
+
+  const fromDateTab = params['from']
+    ? formatDateFromTab(new Date(params['from']))
+    : null
+  const endDateTab = params['to']
+    ? formatDateFromTab(new Date(params['to']))
+    : null
+
+  useEffect(() => {
+    if (props.location.search === '') {
+      props.clearSearch()
+      props.clearSearchFilters()
+      const searchQuery = `?tab=challenges&searchType=challenges`
+      props.history.push({
+        pathname: '/superadmin',
+        search: searchQuery,
+      })
+      props.setSearchSort({ sortBy: 'default' })
+    }
+    setStartDate(fromDateTab)
+    setEndDate(endDateTab)
+  }, [])
+
   //HOC
-  const VisibleFilterToggle = internalFilterToggle('challenge', 'visible');
-  const ArchivedFilterToggle = internalFilterToggle('challenge', 'archived');
-  const VirtualProjectFilterToggle = internalFilterToggle('project', 'virtual');
-  const manager = AsManager(props.user);
+  const VisibleFilterToggle = internalFilterToggle('visible')
+  const ArchivedFilterToggle = internalFilterToggle('archived')
+  const VirtualProjectFilterToggle = internalFilterToggle('virtual')
+  const manager = AsManager(props.user)
   if (!manager.isLoggedIn()) {
     return props.checkingLoginStatus ? (
       <div className='admin mr-flex mr-justify-center mr-py-8 mr-w-full mr-bg-blue'>
@@ -29,51 +66,123 @@ export const SuperAdminPane = (props) => {
       </div>
     ) : (
       <SignIn {...props} />
-    );
+    )
+  }
+
+  const formatDate = (date) => {
+    const offset = date.getTimezoneOffset()
+    date = new Date(date.getTime() - offset * 60 * 1000)
+    return date.toISOString().split('T')[0]
+  }
+
+  const handleStartDate = (date) => {
+    setStartDate(date)
+    const formattedDate = formatDate(date)
+    props.toggleStartDate(formattedDate)
+  }
+
+  const handleEndDate = (date) => {
+    setEndDate(date)
+    const formattedDate = formatDate(date)
+    props.toggleEndDate(formattedDate)
+  }
+
+  const clearDate = () => {
+    props.clearDateFilter()
+    setStartDate(null)
+    setEndDate(null)
   }
 
   return manager.isSuperUser() ? (
     <div className='mr-bg-gradient-r-green-dark-blue mr-text-white mr-px-6 mr-py-8 mr-cards-inverse'>
-      <MetricsHeader {...props} setCurrentTab={setCurrentTab} currentTab={currentTab} />
-      {currentTab !== 'user' && <div className='mr-flex mr-justify-end mr-p-4 mr-pt-6'>
-        <VisibleFilterToggle
-          {...props}
-          dashboardEntityFilters={props.entityFilters}
-          toggleEntityFilter={props.toggleFilter}
-          filterToggleLabel={<FormattedMessage {...messages.visible} />}
-        />
-        <ArchivedFilterToggle
-          {...props}
-          dashboardEntityFilters={props.entityFilters}
-          toggleEntityFilter={props.toggleFilter}
-          filterToggleLabel={<FormattedMessage {...messages.archived} />}
-        />
-        {currentTab === 'project' && <VirtualProjectFilterToggle
-          {...props}
-          dashboardEntityFilters={props.entityFilters}
-          toggleEntityFilter={props.toggleFilter}
-          filterToggleLabel={<FormattedMessage {...messages.virtual} />}
-        />}
-        <button
-          color='primary'
-          type='button'
-          className='mr-leading-none mr-button--dark mr-ml-4 mr-mr-1'
-          onClick={() => {
-            props.downloadCsv(currentTab, props)
-          }}>
-          <FormattedMessage {...messages.download} />
-        </button>
-      </div>}
-      <MetricsTable {...props} currentTab={currentTab}/>
+      <MetricsHeader {...props} currentTab={currentTab} clearDate={clearDate} />
+      {
+        <div className='mr-flex mr-justify-between mr-p-4 mr-pt-6'>
+          <div>
+            <div className='mr-flex mr-items-center'>
+              <div className='mr-w-32'>
+                <DatePicker
+                  selected={startDate}
+                  placeholderText={'Start date'}
+                  onChange={(date) => handleStartDate(date)}
+                  maxDate={endDate}
+                />
+              </div>
+              <SvgSymbol
+                viewBox='0 0 20 20'
+                sym='arrow-right-icon'
+                className='mr-fill-current mr-w-4 mr-h-4 mr-ml-2 mr-mr-2'
+              />
+              <div className='mr-w-32'>
+                <DatePicker
+                  selected={endDate}
+                  placeholderText={'End date'}
+                  onChange={(date) => handleEndDate(date)}
+                  minDate={startDate}
+                />
+              </div>
+              <button
+                color='primary'
+                type='button'
+                className='mr-leading-none mr-button--dark mr-ml-4 mr-mr-1'
+                onClick={() => {
+                  clearDate()
+                }}
+              >
+                <FormattedMessage {...messages.clear} />
+              </button>
+            </div>
+          </div>
+          <div className='mr-flex mr-items-center'>
+            {currentTab !== 'users' && (
+              <VisibleFilterToggle
+                {...props}
+                dashboardEntityFilters={props.entityFilters}
+                toggleEntityFilter={props.toggleFilter}
+                filterToggleLabel={
+                  <FormattedMessage {...messages.hideUndiscoverable} />
+                }
+              />
+            )}
+            {currentTab !== 'users' && (
+              <ArchivedFilterToggle
+                {...props}
+                dashboardEntityFilters={props.entityFilters}
+                toggleEntityFilter={props.toggleFilter}
+                filterToggleLabel={
+                  <FormattedMessage {...messages.hideArchived} />
+                }
+              />
+            )}
+            {currentTab === 'projects' && (
+              <VirtualProjectFilterToggle
+                {...props}
+                dashboardEntityFilters={props.entityFilters}
+                toggleEntityFilter={props.toggleFilter}
+                filterToggleLabel={<FormattedMessage {...messages.virtual} />}
+              />
+            )}
+            <button
+              color='primary'
+              type='button'
+              className='mr-leading-none mr-button--dark mr-ml-4 mr-mr-1'
+              onClick={() => {
+                props.downloadCsv(currentTab, props)
+              }}
+            >
+              <FormattedMessage {...messages.download} />
+            </button>
+          </div>
+        </div>
+      }
+      <MetricsTable {...props} currentTab={currentTab} />
     </div>
   ) : (
-    <div>
-      You are not a super admin
-    </div>
+    <div>You are not a super admin</div>
   )
 }
 
 SuperAdminPane.propTypes = {
   /** router location */
   location: PropTypes.object.isRequired,
-};
+}
diff --git a/src/components/SuperAdmin/SuperAdminContainer.js b/src/components/SuperAdmin/SuperAdminContainer.js
index abd2091b..0259031b 100644
--- a/src/components/SuperAdmin/SuperAdminContainer.js
+++ b/src/components/SuperAdmin/SuperAdminContainer.js
@@ -1,8 +1,9 @@
-import React, {Component} from 'react'
+import React, { Component } from 'react'
 import { connect } from 'react-redux'
 import { SuperAdminPane } from './SuperAdmin'
 import { fetchAdminChallenges } from '../../services/SuperAdmin/SuperAdminChallenges'
 import { fetchAdminProjects } from '../../services/SuperAdmin/SuperAdminProjects'
+import { fetchAdminUsers } from '../../services/SuperAdmin/SuperAdminUsers'
 import WithCurrentUser from '../HOCs/WithCurrentUser/WithCurrentUser'
 import { withRouter } from 'react-router'
 import WithMetricsSearch from './WithMetricsSearch'
@@ -10,50 +11,67 @@ import WithFilteredChallenges from '../HOCs/WithFilteredChallenges/WithFilteredC
 import WithMetricsFilter from './WithMetricsFilter'
 import WithExportCsv from './WithExportCsv'
 import { injectIntl } from 'react-intl'
+import { denormalize } from 'normalizr'
+import { challengeSchema } from '../../services/Challenge/Challenge'
 
-const WrappedSuperAdminPane = 
-  WithCurrentUser(
-    withRouter(
-      WithMetricsSearch(
-        WithMetricsFilter(
-          WithFilteredChallenges(
-            WithExportCsv(
-              injectIntl(SuperAdminPane),
-            )
-          )
-        )
-      ),
-    ))
+const WrappedSuperAdminPane = WithCurrentUser(
+  withRouter(
+    WithMetricsSearch(
+      WithMetricsFilter(
+        WithFilteredChallenges(WithExportCsv(injectIntl(SuperAdminPane)))
+      )
+    )
+  )
+)
 
 class SuperAdminContainer extends Component {
   componentDidMount() {
-    const searchQuery = {onlyEnabled: false}
+    const searchQuery = { onlyEnabled: false }
     this.props.fetchAdminChallenges(searchQuery)
     this.props.fetchAdminProjects()
+    this.props.fetchAdminUsers()
   }
 
   render() {
-    return(
-      <WrappedSuperAdminPane challenges={this.props.adminChallenges} projects={this.props.adminProjects} isloadingCompleted={this.props.loadingChallenges || this.props.loadingProjects} />
+    return (
+      <WrappedSuperAdminPane
+        challenges={this.props.adminChallenges}
+        projects={this.props.adminProjects}
+        users={this.props.adminUsers}
+        isloadingCompleted={
+          this.props.loadingChallenges &&
+          this.props.loadingProjects &&
+          this.props.loadingUsers
+        }
+      />
     )
   }
 }
 
-const mapStateToProps = state => {
+const mapStateToProps = (state) => {
+  const adminChallenges = state.entities?.adminChallenges?.data.map(
+    (challenge) => denormalize(challenge, challengeSchema(), state.entities)
+  )
+
   return {
-    adminChallenges: state.entities?.adminChallenges?.data || [],
+    adminChallenges: adminChallenges || [],
     adminProjects: state.entities?.adminProjects?.data || [],
-    loadingChallenges: state.entities?.adminChallenges?.loading,
-    loadingProjects: state.entities?.adminProjects?.loading
+    adminUsers: state.entities?.adminUsers?.data || [],
+    loadingChallenges: state.entities?.adminChallenges?.loadingCompleted,
+    loadingProjects: state.entities?.adminProjects?.loadingCompleted,
+    loadingUsers: state.entities?.adminUsers?.loadingCompleted,
   }
 }
-const mapDispatchToProps = dispatch => ({
+const mapDispatchToProps = (dispatch) => ({
   fetchAdminChallenges: (query) => {
-    dispatch(fetchAdminChallenges(query));
+    dispatch(fetchAdminChallenges(query))
   },
   fetchAdminProjects: () => {
     dispatch(fetchAdminProjects())
-  }
+  },
+  fetchAdminUsers: () => {
+    dispatch(fetchAdminUsers())
+  },
 })
 
-export default connect(mapStateToProps, mapDispatchToProps)(SuperAdminContainer);
\ No newline at end of file
+export default connect(mapStateToProps, mapDispatchToProps)(SuperAdminContainer)
diff --git a/src/components/SuperAdmin/WithExportCsv.js b/src/components/SuperAdmin/WithExportCsv.js
index 1f1d6b70..938dc74d 100644
--- a/src/components/SuperAdmin/WithExportCsv.js
+++ b/src/components/SuperAdmin/WithExportCsv.js
@@ -1,91 +1,140 @@
 import React, { Component } from 'react'
-import AsManageableProject from '../../interactions/Project/AsManageableProject'
+/**
+ * WithExportCsv handles CSV file formatting/downloading for challenges/projects/users table in super admin.
+ */
 const WithExportCsv = function (WrappedComponent) {
   return class extends Component {
     render() {
       function formatChallengeData(props) {
         let json_pre = props.challenges.map((item) => {
-          const created = new Date(item.created);
-          const modified = new Date(item.modified);
-          
+          const created = item.created ? new Date(item.created) : ''
+          const dataOriginDate = item.dataOriginDate
+            ? new Date(item.dataOriginDate)
+            : ''
+          const lastTaskRefresh = item.lastTaskRefresh
+            ? new Date(item.lastTaskRefresh)
+            : ''
+
           return {
-            'ID': item.id,
-            'NAME': item.name,
-            'OWNER': item.owner,
+            ID: item.id,
+            NAME: item.name,
+            OWNER: item.owner,
             'TASKS REMAINING': item.tasksRemaining,
             '% COMPLETED TASKS': item.completionPercentage,
-            'PROJECT': item.parent,
-            'VISIBLE': item.enabled.toString(),
-            'ARCHIVED': item.isArchived.toString(),
-            'DATE CREATED': `${created.getMonth() + 1}/${created.getDate()}/${created.getFullYear()}`,
-            'DATE LAST MODIFIED': `${modified.getMonth() + 1}/${modified.getDate()}/${modified.getFullYear()}`,
-          };
+            PROJECT: item.parent?.displayName,
+            DISCOVERABLE: item.enabled.toString(),
+            ARCHIVED: item.isArchived.toString(),
+            'DATE CREATED': created
+              ? `${
+                  created.getMonth() + 1
+                }/${created.getDate()}/${created.getFullYear()}`
+              : '',
+            'DATA ORIGIN DATE': dataOriginDate
+              ? `${
+                  dataOriginDate.getMonth() + 1
+                }/${dataOriginDate.getDate()}/${dataOriginDate.getFullYear()}`
+              : '',
+            'LAST TASK REFRESH': lastTaskRefresh
+              ? `${
+                  lastTaskRefresh.getMonth() + 1
+                }/${lastTaskRefresh.getDate()}/${lastTaskRefresh.getFullYear()}`
+              : '',
+          }
         })
-        console.log(json_pre)
-        return json_pre;
+
+        return json_pre
       }
 
-    function formatProjectData(props) {
+      function formatProjectData(props) {
         let json_pre = props.projects.map((item) => {
-          const created = new Date(item.created);
-          const modified = new Date(item.modified);
-          const projectManage= AsManageableProject(item)
-          const numOfChallenges = projectManage.childChallenges(props.challenges).length
+          const created = item.created ? new Date(item.created) : ''
+          const modified = item.modified ? new Date(item.modified) : ''
+
           return {
-            'ID': item.id,
-            'NAME': item.name,
-            'OWNER': item.owner,
-            '# OF CHALLENGES': numOfChallenges,
-            'VISIBLE': item.enabled.toString(),
-            'ARCHIVED': item.isArchived.toString(),
-            'VIRTUAL': item.isVirtual.toString(),
-            'DATE CREATED': `${created.getMonth() + 1}/${created.getDate()}/${created.getFullYear()}`,
-            'DATE LAST MODIFIED': `${modified.getMonth() + 1}/${modified.getDate()}/${modified.getFullYear()}`,
-          };
+            ID: item.id,
+            NAME: item.displayName,
+            OWNER: item.owner,
+            DISCOVERABLE: item.enabled.toString(),
+            ARCHIVED: item.isArchived.toString(),
+            VIRTUAL: item.isVirtual.toString(),
+            'DATE CREATED': created
+              ? `${
+                  created.getMonth() + 1
+                }/${created.getDate()}/${created.getFullYear()}`
+              : '',
+            'DATE LAST MODIFIED': modified
+              ? `${
+                  modified.getMonth() + 1
+                }/${modified.getDate()}/${modified.getFullYear()}`
+              : '',
+          }
         })
-        return json_pre;
+
+        return json_pre
+      }
+
+      function formatUserData(props) {
+        let json_pre = props.users.map((item) => {
+          const created = item.created ? new Date(item.created) : ''
+          const modified = item.modified ? new Date(item.modified) : ''
+
+          return {
+            ID: item.id,
+            NAME: item.osmProfile.displayName,
+            SCORE: item.score,
+            'DATE CREATED': created
+              ? `${
+                  created.getMonth() + 1
+                }/${created.getDate()}/${created.getFullYear()}`
+              : '',
+            'DATE LAST ACTIVE': modified
+              ? `${
+                  modified.getMonth() + 1
+                }/${modified.getDate()}/${modified.getFullYear()}`
+              : '',
+          }
+        })
+
+        return json_pre
       }
 
       function download(currentTab, props) {
         let json_pre
-        console.log(currentTab)
-        if(currentTab === 'challenges'){
+        if (currentTab === 'challenges') {
           json_pre = formatChallengeData(props)
-        }
-        else if(currentTab === 'projects'){
+        } else if (currentTab === 'projects') {
           json_pre = formatProjectData(props)
+        } else {
+          json_pre = formatUserData(props)
         }
-        const csv = jsonToCsv(json_pre);
-        let downloadLink = document.createElement('a');
-        const blob = new Blob(['\ufeff', csv]);
-        const url = URL.createObjectURL(blob);
-        downloadLink.href = url;
-        downloadLink.download = 'data.csv';
-        document.body.appendChild(downloadLink);
-        downloadLink.click();
-        document.body.removeChild(downloadLink);
+        const csv = jsonToCsv(json_pre)
+        let downloadLink = document.createElement('a')
+        const blob = new Blob(['\ufeff', csv])
+        const url = URL.createObjectURL(blob)
+        downloadLink.href = url
+        downloadLink.download = 'data.csv'
+        document.body.appendChild(downloadLink)
+        downloadLink.click()
+        document.body.removeChild(downloadLink)
       }
 
       function jsonToCsv(jsonData) {
-        const header = Object.keys(jsonData[0]);
-        const headerString = header.join(',');
+        const header = Object.keys(jsonData[0])
+        const headerString = header.join(',')
 
         // handle null or undefined values here
-        const replacer = (key, value) => value ?? '';
+        const replacer = (key, value) => value ?? ''
         const rowItems = jsonData.map((row) =>
           header
             .map((fieldName) => JSON.stringify(row[fieldName], replacer))
             .join(',')
-        );
+        )
         // join header and body, and break into separate lines
-        const csv = [headerString, ...rowItems].join('\r\n');
-        return csv;
+        const csv = [headerString, ...rowItems].join('\r\n')
+        return csv
       }
 
-      return <WrappedComponent
-        {...this.props}
-        downloadCsv={download}
-      />
+      return <WrappedComponent {...this.props} downloadCsv={download} />
     }
   }
 }
diff --git a/src/components/SuperAdmin/WithMetricsFilter.js b/src/components/SuperAdmin/WithMetricsFilter.js
index a2dd479d..88f69714 100644
--- a/src/components/SuperAdmin/WithMetricsFilter.js
+++ b/src/components/SuperAdmin/WithMetricsFilter.js
@@ -1,41 +1,141 @@
 import React, { Component } from 'react'
 import queryString from 'query-string'
+/**
+ * WithMetricsFilter handles routes parsing/querying for super admin metrics.
+ * Toggle(checkbox) filter logic here as well.
+ */
+const WithMetricsFilter = function (WrappedComponent) {
 
-const WithMetricsFilter = function(WrappedComponent) {
   return class extends Component {
     render() {
       const params = queryString.parse(this.props.location.search)
-
+      const tab = params['tab']
       let entityFilters = {
-        visible: params['visible'] === 'true',
-        archived: params['archived'] === 'true',
-        virtual: params['virtual'] === 'true'
+        visible: params['hideUndiscoverable'] === 'true',
+        archived: params['hideArchived'] === 'true',
+        virtual: params['virtual'] === 'true',
+        from: params['from'] || undefined,
+        to: params['to'] || undefined,
       }
 
       const toggleFilter = (filterName) => {
         entityFilters[filterName] = !entityFilters[filterName]
         let searchquery = `?`
-        searchquery += `visible=${entityFilters.visible}&`
-        searchquery += `archived=${entityFilters.archived}&`
-        searchquery += `virtual=${entityFilters.virtual}`
+        searchquery += `tab=${tab}`
+        searchquery += `&searchType=${tab}`
+        searchquery +=
+          tab !== 'users' ? `&hideUndiscoverable=${entityFilters.visible}` : ''
+        searchquery +=
+          tab !== 'users' ? `&hideArchived=${entityFilters.archived}` : ''
+        searchquery +=
+          tab === 'projects' ? `&virtual=${entityFilters.virtual}` : ''
+        searchquery += entityFilters.from ? `&from=${entityFilters.from}` : ''
+        searchquery += entityFilters.to ? `&to=${entityFilters.to}` : ''
+        this.props.history.push({
+          pathname: '/superadmin',
+          search: searchquery,
+        })
+      }
+
+      const toggleStartDate = (startDate) => {
+        entityFilters.from = startDate
+        const newQueries = { ...params, from: startDate }
         this.props.history.push({
           pathname: '/superadmin',
-          search: searchquery
-        })  
+          search: queryString.stringify(newQueries),
+        })
       }
-      let challenges = entityFilters.visible ? this.props.challenges.filter(c => c.enabled) : this.props.challenges
-      challenges = entityFilters.archived ? challenges.filter(c => c.isArchived) : challenges
 
-      let projects = entityFilters.visible? this.props.projects.filter(p => p.enabled): this.props.projects
-      projects = entityFilters.archived? projects.filter(p => p.isArchived): projects
-      projects = entityFilters.virtual? projects.filter(p => p.isVirtual): projects
+      const toggleEndDate = (endDate) => {
+        entityFilters.to = endDate
+        const newQueries = { ...params, to: endDate }
+        this.props.history.push({
+          pathname: '/superadmin',
+          search: queryString.stringify(newQueries),
+        })
+      }
+
+      const clearDateFilter = () => {
+        const newQueries = { ...params, from: undefined, to: undefined }
+        this.props.history.push({
+          pathname: '/superadmin',
+          search: queryString.stringify(newQueries),
+        })
+      }
+
+      let challenges = this.props.challenges
+      let projects = this.props.projects
+      let users = this.props.users
+      let startDate = new Date(entityFilters.from)
+      let endDate = new Date(entityFilters.to)
+      if (tab === 'challenges') {
+        challenges = entityFilters.visible
+          ? challenges.filter((c) => c.enabled)
+          : challenges
+        challenges = entityFilters.archived
+          ? challenges.filter((c) => !c.isArchived)
+          : challenges
+        challenges = entityFilters.from
+          ? challenges.filter((c) => {
+              const date = new Date(c.created)
+              return date >= startDate
+            })
+          : challenges
+        challenges = entityFilters.to
+          ? challenges.filter((c) => {
+              const date = new Date(c.created)
+              return date <= endDate
+            })
+          : challenges
+      } else if (tab === 'projects') {
+        projects = entityFilters.visible
+          ? projects.filter((p) => p.enabled)
+          : projects
+        projects = entityFilters.archived
+          ? projects.filter((p) => !p.isArchived)
+          : projects
+        projects = entityFilters.virtual
+          ? projects.filter((p) => p.isVirtual)
+          : projects
+        projects = entityFilters.from
+          ? projects.filter((p) => {
+              const date = new Date(p.created)
+              return date >= startDate
+            })
+          : projects
+        projects = entityFilters.to
+          ? projects.filter((p) => {
+              const date = new Date(p.created)
+              return date <= endDate
+            })
+          : projects
+      } else {
+        users = entityFilters.from
+          ? users.filter((user) => {
+              const date = new Date(user.created)
+              return date >= startDate
+            })
+          : users
+
+        users = entityFilters.to
+          ? users.filter((user) => {
+              const date = new Date(user.created)
+              return date <= endDate
+            })
+          : users
+      }
 
       return (
-        <WrappedComponent {...this.props} 
-          challenges = {challenges} 
+        <WrappedComponent
+          {...this.props}
+          challenges={challenges}
           projects={projects}
-          entityFilters = {entityFilters}
-          toggleFilter = {toggleFilter}
+          users={users}
+          entityFilters={entityFilters}
+          toggleFilter={toggleFilter}
+          toggleStartDate={toggleStartDate}
+          toggleEndDate={toggleEndDate}
+          clearDateFilter={clearDateFilter}
         />
       )
     }
diff --git a/src/components/SuperAdmin/WithMetricsSearch.js b/src/components/SuperAdmin/WithMetricsSearch.js
index 2076dd52..bb0afd63 100644
--- a/src/components/SuperAdmin/WithMetricsSearch.js
+++ b/src/components/SuperAdmin/WithMetricsSearch.js
@@ -3,11 +3,10 @@ import WithSearchRoute from '../HOCs/WithSearchRoute/WithSearchRoute'
 
 const SEARCH_GROUP = 'challenges'
 
-export default WrappedComponent => {
-  return (
-    WithSearch(
-      WithSearchRoute(WrappedComponent, SEARCH_GROUP),
-      SEARCH_GROUP,
-      () => null
-    ))
-}
\ No newline at end of file
+export default (WrappedComponent) => {
+  return WithSearch(
+    WithSearchRoute(WrappedComponent, SEARCH_GROUP),
+    SEARCH_GROUP,
+    () => null
+  )
+}
diff --git a/src/components/SuperAdmin/WithMetricsSearchResults.js b/src/components/SuperAdmin/WithMetricsSearchResults.js
new file mode 100644
index 00000000..731689a2
--- /dev/null
+++ b/src/components/SuperAdmin/WithMetricsSearchResults.js
@@ -0,0 +1,98 @@
+import React, { Component } from 'react'
+import _get from 'lodash/get'
+import _isEmpty from 'lodash/isEmpty'
+import _filter from 'lodash/filter'
+import _omit from 'lodash/omit'
+import WithSearch from '../HOCs/WithSearch/WithSearch'
+import queryString from 'query-string'
+
+/**
+ * WithMetricsSearchResults acts as a filter that applies the named search query to an
+ * array of candidate items, presenting to the wrapped component only those
+ * items that match the query.
+ *
+ * @param {string} searchName - the name of the search/query to work with
+ * @param {string} itemsProp - the name of the prop containing the array
+ *        of items to search (e.g. 'challenges' or 'projects' or 'users').
+ * @param {string} outputProp - optional name of the prop to use when passing
+ *        down the filtered results. By default it will be the same as
+ *        itemsProp.
+ **/
+
+export const WithMetricsSearchResults = function (
+  WrappedComponent,
+  searchName,
+  itemsProp,
+  outputProp
+) {
+  return class extends Component {
+    /**
+     * @private
+     */
+    render() {
+      const query = _get(this.props, `searchCriteria.query`, '')
+      let items, searchType
+
+      const params = queryString.parse(this.props.location.search)
+      searchType = params['searchType'] || 'challenges'
+      items = this.props[searchType]
+
+      let searchResults = this.props[searchType]
+      let searchActive = false
+
+      if (searchType === 'challenges' && query) {
+        searchResults = _filter(
+          items,
+          (item) => _get(item, 'name', '').toLowerCase().indexOf(query) !== -1
+        )
+      } else if (searchType === 'projects' && query) {
+        searchResults = _filter(
+          items,
+          (item) =>
+            _get(item, 'displayName', '').toLowerCase().indexOf(query) !== -1
+        )
+      } else if (searchType === 'users' && query) {
+        searchResults = _filter(
+          items,
+          (item) =>
+            _get(item, 'osmProfile.displayName', '')
+              .toLowerCase()
+              .indexOf(query) !== -1
+        )
+      }
+
+      if (_isEmpty(outputProp)) {
+        outputProp = itemsProp
+      }
+      outputProp = searchType
+      searchName = searchType
+      return (
+        <WrappedComponent
+          {...{
+            [outputProp]: searchResults,
+            [`${searchName}SearchActive`]: searchActive,
+          }}
+          {..._omit(this.props, outputProp)}
+        />
+      )
+    }
+  }
+}
+
+export default (
+  WrappedComponent,
+  searchName,
+  itemsProp,
+  outputProp,
+  searchFunction = null
+) =>
+  WithSearch(
+    WithMetricsSearchResults(
+      WrappedComponent,
+      searchName,
+      itemsProp,
+      outputProp
+    ),
+    searchName,
+    searchFunction
+  )
diff --git a/src/components/SuperAdmin/WithSortedProjects.js b/src/components/SuperAdmin/WithSortedProjects.js
new file mode 100644
index 00000000..1d522a55
--- /dev/null
+++ b/src/components/SuperAdmin/WithSortedProjects.js
@@ -0,0 +1,59 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import _get from 'lodash/get'
+import _sortBy from 'lodash/sortBy'
+import _reverse from 'lodash/reverse'
+import _isEmpty from 'lodash/isEmpty'
+import _omit from 'lodash/omit'
+import _toLower from 'lodash/toLower'
+import {
+  SORT_NAME,
+  SORT_CREATED,
+  SORT_OLDEST,
+} from '../../services/Search/Search'
+
+export const sortProjects = function (props, projectsProp = 'projects') {
+  const sortCriteria = _get(props, 'searchSort.sortBy')
+  let sortedProjects = props[projectsProp]
+  if (sortCriteria === SORT_NAME) {
+    sortedProjects = _sortBy(sortedProjects, (p) => _toLower(p.displayName))
+  } else if (sortCriteria === SORT_CREATED) {
+    sortedProjects = _reverse(
+      _sortBy(sortedProjects, (p) => (p.created ? p.created : ''))
+    )
+  } else if (sortCriteria === SORT_OLDEST) {
+    sortedProjects = _sortBy(sortedProjects, (p) =>
+      p.created ? p.created : ''
+    )
+  }
+  return sortedProjects
+}
+
+export default function (
+  WrappedComponent,
+  projectsProp = 'projects',
+  outputProp
+) {
+  class WithSortedProjects extends Component {
+    render() {
+      const sortedProjects = sortProjects(this.props, projectsProp)
+
+      if (_isEmpty(outputProp)) {
+        outputProp = projectsProp
+      }
+
+      return (
+        <WrappedComponent
+          {...{ [outputProp]: sortedProjects }}
+          {..._omit(this.props, outputProp)}
+        />
+      )
+    }
+  }
+
+  WithSortedProjects.propTypes = {
+    projects: PropTypes.array,
+  }
+
+  return WithSortedProjects
+}
diff --git a/src/components/SuperAdmin/WithSortedUsers.js b/src/components/SuperAdmin/WithSortedUsers.js
new file mode 100644
index 00000000..ab311512
--- /dev/null
+++ b/src/components/SuperAdmin/WithSortedUsers.js
@@ -0,0 +1,56 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import _get from 'lodash/get'
+import _sortBy from 'lodash/sortBy'
+import _reverse from 'lodash/reverse'
+import _isEmpty from 'lodash/isEmpty'
+import _omit from 'lodash/omit'
+import _toLower from 'lodash/toLower'
+import {
+  SORT_NAME,
+  SORT_CREATED,
+  SORT_OLDEST,
+  SORT_SCORE,
+} from '../../services/Search/Search'
+
+export const sortUsers = function (props, usersProp = 'users') {
+  const sortCriteria = _get(props, 'searchSort.sortBy')
+  let sortedUsers = props[usersProp]
+  if (sortCriteria === SORT_NAME) {
+    sortedUsers = _sortBy(sortedUsers, (u) => _toLower(u.name))
+  } else if (sortCriteria === SORT_CREATED) {
+    sortedUsers = _reverse(
+      _sortBy(sortedUsers, (u) => (u.created ? u.created : ''))
+    )
+  } else if (sortCriteria === SORT_OLDEST) {
+    sortedUsers = _sortBy(sortedUsers, (u) => (u.created ? u.created : ''))
+  } else if (sortCriteria === SORT_SCORE) {
+    sortedUsers = _sortBy(sortedUsers, (u) => (u.score ? u.score : ''))
+  }
+  return sortedUsers
+}
+
+export default function (WrappedComponent, usersProp = 'users', outputProp) {
+  class WithSortedUsers extends Component {
+    render() {
+      const sortedUsers = sortUsers(this.props, usersProp)
+
+      if (_isEmpty(outputProp)) {
+        outputProp = usersProp
+      }
+
+      return (
+        <WrappedComponent
+          {...{ [outputProp]: sortedUsers }}
+          {..._omit(this.props, outputProp)}
+        />
+      )
+    }
+  }
+
+  WithSortedUsers.propTypes = {
+    users: PropTypes.array,
+  }
+
+  return WithSortedUsers
+}
diff --git a/src/components/SuperAdmin/internalFilterToggle.js b/src/components/SuperAdmin/internalFilterToggle.js
index 9a506a5a..50eba915 100644
--- a/src/components/SuperAdmin/internalFilterToggle.js
+++ b/src/components/SuperAdmin/internalFilterToggle.js
@@ -1,16 +1,17 @@
 import React, { Component } from 'react'
 
-const internalFilterToggle = (filterType, filterName) => {
+const internalFilterToggle = (filterName) => {
   return class extends Component {
     render() {
       return (
-        <div className="mr-leading-none">
+        <div className='mr-leading-none'>
           <input
-            type="checkbox"
-            className="mr-checkbox-toggle mr-ml-4 mr-mr-1"
+            type='checkbox'
+            className='mr-checkbox-toggle mr-ml-4 mr-mr-1'
             checked={this.props.dashboardEntityFilters?.[filterName] || false}
             onChange={() => this.props.toggleEntityFilter(filterName)}
-          /> {this.props.filterToggleLabel}
+          />
+          {this.props.filterToggleLabel}
         </div>
       )
     }
diff --git a/src/lang/en-US.json b/src/lang/en-US.json
index b558b1d3..70f0e64e 100644
--- a/src/lang/en-US.json
+++ b/src/lang/en-US.json
@@ -208,7 +208,6 @@
   "Admin.EditChallenge.form.steps.allOptions.description": "Options",
   "Admin.EditChallenge.form.steps.basemap.description": "Set a different basemap",
   "Admin.EditChallenge.form.steps.basemap.header": "Basemap",
-  "Admin.EditChallenge.form.steps.changesetUrl.description": "Automatically append challenge link to the changeset comment",
   "Admin.EditChallenge.form.steps.changesetUrl.title": "Add Changeset URL",
   "Admin.EditChallenge.form.steps.contactInfo.header": "Contact Information",
   "Admin.EditChallenge.form.steps.dataSource.description": "Change Name or Data Source",
@@ -517,12 +516,21 @@
   "ChallengeCard.controls.visibilityToggle.tooltip": "Toggle challenge discoverability",
   "ChallengeCard.totalTasks": "Total Tasks",
   "ChallengeDetails.Task.fields.featured.label": "Featured",
+  "ChallengeDetails.controls.checkbox.error": "Please ensure that checkbox is checked before continue",
+  "ChallengeDetails.controls.email": "Email",
   "ChallengeDetails.controls.favorite.label": "Favorite",
   "ChallengeDetails.controls.favorite.tooltip": "Save to favorites",
+  "ChallengeDetails.controls.flagged_text": "This challenge has been flagged",
   "ChallengeDetails.controls.goBack.label": "Go Back",
+  "ChallengeDetails.controls.modal.subtitle": "You are about to flag a Challenge. An issue will be created here and the Challenge creator will be notified by email. Any follow-up discussion should take place there. Flagging a Challenge does not disable it immediately. Please explain in detail what your issue is with this challenge, if possible linking to specific OSM changesets.",
+  "ChallengeDetails.controls.preview.label": "Preview",
+  "ChallengeDetails.controls.review.label": "I have attempted to contact the Challenge creator",
   "ChallengeDetails.controls.start.label": "Start",
+  "ChallengeDetails.controls.submit.flag.label": "Flag Challenge",
+  "ChallengeDetails.controls.text.input.error": "Text Input should have minimum 100 characters",
   "ChallengeDetails.controls.unfavorite.label": "Unfavorite",
   "ChallengeDetails.controls.unfavorite.tooltip": "Remove from favorites",
+  "ChallengeDetails.controls.write.label": "Write",
   "ChallengeDetails.fields.difficulty.label": "Difficulty",
   "ChallengeDetails.fields.lastChallengeDetails.DataOriginDate.label": "Tasks built on {refreshDate} from data sourced on {sourceDate}.",
   "ChallengeDetails.fields.lastChallengeDetails.TaskRefresh.label": "Task Data Sourced",
@@ -1034,18 +1042,21 @@
   "Locale.zh-TW.label": "zh-TW (Chinese - Taiwan)",
   "Map.layerSelectionList.header": "Select Desired Feature",
   "Metrics.archived": "Archived",
-  "Metrics.challenge": "Challenge",
+  "Metrics.challenges": "Challenges",
+  "Metrics.clear": "clear",
   "Metrics.completedTasksTitle": "Completed Tasks",
-  "Metrics.discoverable": "Visible",
+  "Metrics.discoverable": "Discoverable",
   "Metrics.download": "download",
   "Metrics.header": "Metrics",
+  "Metrics.hideArchived": "Hide Archived",
+  "Metrics.hideUndiscoverable": "Hide Undiscoverable",
   "Metrics.leaderboard.globalRank.label": "Global Rank",
   "Metrics.leaderboard.topChallenges.label": "Top Challenges",
   "Metrics.leaderboard.totalPoints.label": "Total Points",
   "Metrics.leaderboardTitle": "Leaderboard",
   "Metrics.links.osmCha.label": "OSMCha",
   "Metrics.links.osmProfile.label": "OSM Profile",
-  "Metrics.project": "Project",
+  "Metrics.projects": "Projects",
   "Metrics.reviewStats.approved.label": "Reviewed tasks that passed",
   "Metrics.reviewStats.asReviewer.additionalReviews.label": "Provided an additional review",
   "Metrics.reviewStats.asReviewer.approved.label": "Reviews marked as passed",
@@ -1061,10 +1072,11 @@
   "Metrics.reviewedTasksTitle.asReviewer": "Tasks Reviewed by {username}",
   "Metrics.reviewedTasksTitle.asReviewer.totalTasks": "Total tasks you reviewed",
   "Metrics.reviewedTasksTitle.asReviewer.you": "Reviews Performed by You",
+  "Metrics.sortBy": "Sort By",
   "Metrics.totalCompletedTasksTitle": "Total Completed Tasks",
-  "Metrics.user": "User",
   "Metrics.userOptedOut": "This user has opted out of public display of their stats.",
   "Metrics.userSince": "User since:",
+  "Metrics.users": "Users",
   "Metrics.virtual": "Virtual",
   "MobileNotSupported.header": "Please Visit on your Computer",
   "MobileNotSupported.message": "Sorry, MapRoulette does not currently support mobile devices.",
@@ -1507,6 +1519,7 @@
   "TaskHistory.controls.viewAttic.label": "View Attic",
   "TaskHistory.errorTags.label": "Error Tags",
   "TaskHistory.fields.startedOn.label": "Started on task",
+  "TaskHistory.fields.startedReviewOn.label": "Started review on task",
   "TaskHistory.fields.taskUpdated.label": "Task updated by challenge manager",
   "TaskHistory.fields.userType.mapper": "Mapper",
   "TaskHistory.fields.userType.metaReviewer": "Meta-Reviewer",
@@ -1570,6 +1583,7 @@
   "Team.noTeams": "You are not a member of any teams",
   "TopUserChallenges.widget.label": "Your Top Challenges",
   "TopUserChallenges.widget.noChallenges": "No Challenges",
+  "User.sort.numOfChallenges": "Score",
   "UserEditorSelector.currentEditor.label": "Current Editor:",
   "UserProfile.favoriteChallenges.header": "Your Favorite Challenges",
   "UserProfile.savedTasks.header": "Tracked Tasks",
diff --git a/src/services/Challenge/Challenge.js b/src/services/Challenge/Challenge.js
index b6211e5e..ebf3fee4 100644
--- a/src/services/Challenge/Challenge.js
+++ b/src/services/Challenge/Challenge.js
@@ -1424,13 +1424,13 @@ export const challengeEntities = function (state, action) {
 
 const ADMIN_CHALLENGES_INITIAL_STATE = {
   data: [],
-  loading: false
+  loadingCompleted: false
 }
 
 export const adminChallengeEntities = function(state = ADMIN_CHALLENGES_INITIAL_STATE, action) {
   switch (action.type) {
     case SET_ADMIN_CHALLENGES:
-      return { data: action.payload, loading: action.loading };
+      return { data: action.payload, loadingCompleted: action.loadingCompleted };
     default:
       return state;
   }
diff --git a/src/services/Error/AppErrors.js b/src/services/Error/AppErrors.js
index 9bd3f6d6..7c5054f5 100644
--- a/src/services/Error/AppErrors.js
+++ b/src/services/Error/AppErrors.js
@@ -14,7 +14,7 @@ export default {
     updateFailure: messages.userUpdateFailure,
     fetchFailure: messages.userFetchFailure,
     notFound: messages.userNotFound,
-    followFailure: messages.userFollowFailure,
+    followFailure: messages.userFollowFailure
   },
 
   leaderboard: {
diff --git a/src/services/Search/Messages.js b/src/services/Search/Messages.js
index 1b2b91ed..2d9f1207 100644
--- a/src/services/Search/Messages.js
+++ b/src/services/Search/Messages.js
@@ -32,6 +32,10 @@ export default defineMessages({
     id: "Challenge.sort.cooperativeWork",
     defaultMessage: "Cooperative",
   },
+  score: {
+    id: "User.sort.numOfChallenges",
+    defaultMessage: "Score"
+  },
   default: {
     id: "Challenge.sort.default",
     defaultMessage: "Default",
diff --git a/src/services/Search/Search.js b/src/services/Search/Search.js
index ee595c7e..e7d9a627 100644
--- a/src/services/Search/Search.js
+++ b/src/services/Search/Search.js
@@ -54,7 +54,11 @@ export const SORT_COMPLETION = 'completion_percentage'
 export const SORT_TASKS_REMAINING = 'tasks_remaining'
 export const SORT_COOPERATIVE_WORK = 'cooperative_type'
 export const SORT_DEFAULT = 'default'
+export const SORT_NUM_OF_CHALLENGES = 'num_of_challenges'
+export const SORT_SCORE = 'score'
 export const ALL_SORT_OPTIONS = [SORT_NAME, SORT_CREATED, SORT_OLDEST, SORT_POPULARITY, SORT_COOPERATIVE_WORK, SORT_COMPLETION, SORT_TASKS_REMAINING, SORT_DEFAULT]
+export const PROJECT_SORT_OPTIONS = [SORT_NAME, SORT_CREATED, SORT_OLDEST, SORT_DEFAULT]
+export const USER_SORT_OPTIONS = [SORT_NAME, SORT_CREATED, SORT_OLDEST, SORT_SCORE, SORT_DEFAULT]
 
 // Default Results Per page
 export const RESULTS_PER_PAGE = 50
diff --git a/src/services/Server/APIRoutes.js b/src/services/Server/APIRoutes.js
index 9931cad6..ff7025dd 100644
--- a/src/services/Server/APIRoutes.js
+++ b/src/services/Server/APIRoutes.js
@@ -163,6 +163,7 @@ const apiRoutes = (factory) => {
       reviewerLeaderboard: factory.get("/data/reviewer/leaderboard"),
       find: factory.get("/users/find/:username"),
       findPreferred: factory.get("/users/find"),
+      all: factory.get("/users")
     },
     user: {
       whoami: factory.get("/user/whoami"),
diff --git a/src/services/SuperAdmin/SuperAdminChallenges.js b/src/services/SuperAdmin/SuperAdminChallenges.js
index 23f92292..0169342a 100644
--- a/src/services/SuperAdmin/SuperAdminChallenges.js
+++ b/src/services/SuperAdmin/SuperAdminChallenges.js
@@ -1,31 +1,32 @@
 import { performChallengeSearch } from '../Challenge/Challenge'
 import { SET_ADMIN_CHALLENGES } from '../Challenge/ChallengeActions'
 
-export const receiveAdminChallenges = function (
-  normalizedEntities,
-  dispatch
-) {
+export const receiveAdminChallenges = function (normalizedEntities, dispatch) {
   dispatch({
     type: SET_ADMIN_CHALLENGES,
     payload: [],
-    loading: false
+    loadingCompleted: false,
   })
 
-  const results = Object.keys(normalizedEntities.challenges).map(i => normalizedEntities.challenges[i]);
+  const results = Object.keys(normalizedEntities.challenges).map(
+    (i) => normalizedEntities.challenges[i]
+  )
 
   return {
     type: SET_ADMIN_CHALLENGES,
     payload: results || [],
-    loading: true
-  };
-};
+    loadingCompleted: true,
+  }
+}
 
-export const fetchAdminChallenges = function(query) {
-  return function(dispatch) {
-    return (
-      dispatch(performChallengeSearch({ ...query, admin: true }, 50000)).then(normalizedResults => {
-        return dispatch(receiveAdminChallenges(normalizedResults.entities, dispatch))
-      })
+export const fetchAdminChallenges = function (query) {
+  return function (dispatch) {
+    return dispatch(performChallengeSearch(query, 50000, true)).then(
+      (normalizedResults) => {
+        return dispatch(
+          receiveAdminChallenges(normalizedResults.entities, dispatch)
+        )
+      }
     )
   }
 }
diff --git a/src/services/SuperAdmin/SuperAdminProjects.js b/src/services/SuperAdmin/SuperAdminProjects.js
index 0beb7119..8e48e6f0 100644
--- a/src/services/SuperAdmin/SuperAdminProjects.js
+++ b/src/services/SuperAdmin/SuperAdminProjects.js
@@ -1,46 +1,48 @@
-import { fetchProjects } from "../Project/Project";
+import { fetchProjects } from '../Project/Project'
 
 const SET_ADMIN_PROJECTS = 'SET_ADMIN_PROJECTS'
 
-export const receiveAdminProjects = function (
-  normalizedEntities,
-  dispatch
-) {
+export const receiveAdminProjects = function (normalizedEntities, dispatch) {
   dispatch({
     type: SET_ADMIN_PROJECTS,
     payload: [],
-    loading: false
+    loadingCompleted: false,
   })
 
-  const results = Object.keys(normalizedEntities.projects).map(i => normalizedEntities.projects[i]);
+  const results = Object.keys(normalizedEntities.projects).map(
+    (i) => normalizedEntities.projects[i]
+  )
 
   return {
     type: SET_ADMIN_PROJECTS,
     payload: results || [],
-    loading: true
-  };
-};
+    loadingCompleted: true,
+  }
+}
 
-export const fetchAdminProjects = function() {
-  return function(dispatch) {
-    return (
-      dispatch(fetchProjects(50000)).then(normalizedResults => {
-        return dispatch(receiveAdminProjects(normalizedResults.entities, dispatch))
-      })
-    )
+export const fetchAdminProjects = function () {
+  return function (dispatch) {
+    return dispatch(fetchProjects(50000)).then((normalizedResults) => {
+      return dispatch(
+        receiveAdminProjects(normalizedResults.entities, dispatch)
+      )
+    })
   }
 }
 
 const ADMIN_PROJECTS_INITIAL_STATE = {
   data: [],
-  loading: false
+  loadingCompleted: false,
 }
 
-export const adminProjectEntities = function(state = ADMIN_PROJECTS_INITIAL_STATE, action) {
+export const adminProjectEntities = function (
+  state = ADMIN_PROJECTS_INITIAL_STATE,
+  action
+) {
   switch (action.type) {
     case SET_ADMIN_PROJECTS:
-      return { data: action.payload, loading: action.loading };
+      return { data: action.payload, loadingCompleted: action.loadingCompleted }
     default:
-      return state;
+      return state
   }
 }
diff --git a/src/services/SuperAdmin/SuperAdminUsers.js b/src/services/SuperAdmin/SuperAdminUsers.js
new file mode 100644
index 00000000..a0c3874f
--- /dev/null
+++ b/src/services/SuperAdmin/SuperAdminUsers.js
@@ -0,0 +1,46 @@
+import { fetchUsers } from '../User/User'
+
+const SET_ADMIN_USERS = 'SET_ADMIN_USERS'
+
+export const receiveAdminUsers = function (normalizedEntities, dispatch) {
+  dispatch({
+    type: SET_ADMIN_USERS,
+    payload: [],
+    loadingCompleted: false,
+  })
+
+  const results = Object.keys(normalizedEntities.users).map(
+    (i) => normalizedEntities.users[i]
+  )
+
+  return {
+    type: SET_ADMIN_USERS,
+    payload: results || [],
+    loadingCompleted: true,
+  }
+}
+
+export const fetchAdminUsers = function () {
+  return function (dispatch) {
+    return dispatch(fetchUsers(50000)).then((normalizedResults) => {
+      return dispatch(receiveAdminUsers(normalizedResults.entities, dispatch))
+    })
+  }
+}
+
+const ADMIN_USERS_INITIAL_STATE = {
+  data: [],
+  loadingCompleted: false,
+}
+
+export const adminUserEntities = function (
+  state = ADMIN_USERS_INITIAL_STATE,
+  action
+) {
+  switch (action.type) {
+    case SET_ADMIN_USERS:
+      return { data: action.payload, loadingCompleted: action.loadingCompleted }
+    default:
+      return state
+  }
+}
diff --git a/src/services/User/User.js b/src/services/User/User.js
index 1f520280..1d6a46e9 100644
--- a/src/services/User/User.js
+++ b/src/services/User/User.js
@@ -240,6 +240,26 @@ export const fetchUser = function(userId) {
   }
 }
 
+/**
+ * Fetch data on all users (up to the given limit).
+ */
+ export const fetchUsers = function (limit = 50) {
+  return function (dispatch) {
+    return new Endpoint(api.users.all, {
+      schema: [userSchema()],
+      params: { limit },
+    })
+      .execute()
+      .then((normalizedResults) => {
+        return normalizedResults;
+      })
+      .catch((error) => {
+        dispatch(addError(AppErrors.user.fetchFailure));
+        console.log(error.response || error);
+      });
+  };
+};
+
 /**
  * Fetch the public user data for the given user.
  *


Test patch:
diff --git a/src/components/ChallengeDetail/ChallengeDetail.test.js b/src/components/ChallengeDetail/ChallengeDetail.test.js
index 9164a2f6..f2d97068 100644
--- a/src/components/ChallengeDetail/ChallengeDetail.test.js
+++ b/src/components/ChallengeDetail/ChallengeDetail.test.js
@@ -8,7 +8,7 @@ describe("ChallengeDetail", () => {
   it("doesn't break if only required props are provided", () => {
     const { getByText } = render(
       <IntlProvider locale="en">
-        <ChallengeDetail />
+        <ChallengeDetail match={{ url: "", params: {} }} />
         <div>Test Passes</div>
       </IntlProvider>
     );
diff --git a/src/components/SuperAdmin/SuperAdmin.test.js b/src/components/SuperAdmin/SuperAdmin.test.js
index 6d35611d..e3f85d21 100644
--- a/src/components/SuperAdmin/SuperAdmin.test.js
+++ b/src/components/SuperAdmin/SuperAdmin.test.js
@@ -1,52 +1,155 @@
 import '@testing-library/jest-dom'
 import * as React from 'react'
 import { SuperAdminPane } from './SuperAdmin'
-import DashboardFilterToggle from '../AdminPane/Manage/DashboardFilterToggle/DashboardFilterToggle'
+import { fireEvent } from '@testing-library/react'
 
-describe("SuperAdminPane", () => {
-  it("redirects to Sign In if no user data", () => {
+const props = {
+  user: {
+    id: 1,
+    grants: [
+      {
+        id: 13,
+        name: 'Super User Override',
+        grantee: {
+          granteeType: 5,
+          granteeId: 1,
+        },
+        role: -1,
+        target: {
+          objectType: 0,
+          objectId: 0,
+        },
+      },
+    ],
+  },
+  location: { search: '?tab=challenges&searchType=challenges' },
+  history: { push: () => null, location: { search: null } },
+  match: {},
+  clearSearchFilters: () => null,
+  clearSearch: () => null,
+  filterName: { visible: null, archived: null },
+  showingFilter: () => null,
+  setSearch: () => null,
+  clearSearch: () => null,
+  setSearchSort: () => null,
+  setKeywordFilter: () => null,
+  removeSearchFilters: () => null,
+  setSearchFilters: () => null,
+  allUsers: {},
+  dashboardEntityFilters: { visible: null },
+  toggleFilter: () => null,
+  clearDateFilter: () => null,
+  filterToggleLabel: null,
+  projects: [],
+  challenges: [
+    { id: 1, name: 'challenge1', isArchived: true },
+    { id: 2, name: 'challenge2', isArchived: true },
+    { id: 3, name: 'challenge3', isArchived: false },
+  ],
+}
+describe('SuperAdminPane', () => {
+  it('redirects to Sign In if no user data', () => {
     const { getByText } = global.withProvider(
-      <SuperAdminPane location={{}}  dashboardEntityFilters = {{visible: null, archived: null}}/>
-    );
-    const text = getByText("Sign in");
-    expect(text).toBeInTheDocument();
-  });
+      <SuperAdminPane
+        location={props.location}
+        match={props.match}
+        clearSearchFilters={props.clearSearch}
+        clearSearch={props.clearSearch}
+        dashboardEntityFilters={props.dashboardEntityFilters}
+      />
+    )
+    const text = getByText('Sign in')
+    expect(text).toBeInTheDocument()
+  })
 
-  it("grants super user access to Metrics page", () => {
-    const VisibleFilterToggle = DashboardFilterToggle("challenge", "visible");
-    const archivedFilterToggle = DashboardFilterToggle("challenge", "archived");
+  it('grants super user access to Metrics page', () => {
     const { getByText } = global.withProvider(
-      <SuperAdminPane 
-        user={{ id: 1, grants: [{
-          "id": 13,
-          "name": "Super User Override",
-          "grantee": {
-            "granteeType": 5,
-            "granteeId": 1
-          },
-          "role": -1,
-          "target": {
-            "objectType": 0,
-            "objectId": 0
-          }
-        }] }} 
-        location={{}}
-        filterName={{visible: null, archived: null}}
-        showingFilter={() => null}
-        setSearch={() => null}
-        clearSearch={() => null}
-        setSearchSort={() => null}
-        setKeywordFilter={() => null}
-        removeSearchFilters={() => null}
-        setSearchFilters={() => null}
-        allUsers={{}}
-        dashboardEntityFilters = {{visible: null}}
-        toggleEntityFilter={() => null} 
-        filterToggleLabel={null}
+      <SuperAdminPane
+        user={props.user}
+        location={props.location}
+        match={props.match}
+        clearSearchFilters={props.clearSearchFilters}
+        clearSearch={props.clearSearch}
+        filterName={props.filterName}
+        showingFilter={props.showingFilter}
+        setSearch={props.setSearch}
+        clearSearch={props.clearSearch}
+        setSearchSort={props.setSearchSort}
+        setKeywordFilter={props.setKeywordFilter}
+        removeSearchFilters={props.removeSearchFilters}
+        setSearchFilters={props.setSearchFilters}
+        allUsers={props.allUsers}
+        dashboardEntityFilters={props.dashboardEntityFilters}
+        toggleFilter={props.toggleFilter}
+        filterToggleLabel={props.filterToggleLabel}
+      />
+    )
+
+    const text = getByText('Metrics')
+    expect(text).toBeInTheDocument()
+  })
+
+  it('can switch tab', () => {
+    const clearSearch = jest.fn()
+    const { container } = global.withProvider(
+      <SuperAdminPane
+        user={props.user}
+        location={props.location}
+        match={props.match}
+        clearSearchFilters={props.clearSearchFilters}
+        clearSearch={props.clearSearch}
+        filterName={props.filterName}
+        showingFilter={props.showingFilter}
+        setSearch={props.setSearch}
+        clearSearch={() => clearSearch()}
+        setSearchSort={props.setSearchSort}
+        clearDateFilter={props.clearDateFilter}
+        history={props.history}
+        setKeywordFilter={props.setKeywordFilter}
+        removeSearchFilters={props.removeSearchFilters}
+        setSearchFilters={props.setSearchFilters}
+        allUsers={props.allUsers}
+        dashboardEntityFilters={props.dashboardEntityFilters}
+        toggleFilter={props.toggleFilter}
+        filterToggleLabel={props.filterToggleLabel}
+      />
+    )
+
+    const element = Array.from(container.querySelectorAll('button')).find(
+      (e) => e.textContent === 'Projects'
+    )
+    fireEvent.click(element)
+    expect(clearSearch).toHaveBeenCalledTimes(1)
+  })
+
+  it('can toggle for challenge table', () => {
+    const toggleFilter = jest.fn()
+    const { container } = global.withProvider(
+      <SuperAdminPane
+        user={props.user}
+        location={props.location}
+        match={props.match}
+        clearSearchFilters={props.clearSearchFilters}
+        clearSearch={props.clearSearch}
+        filterName={props.filterName}
+        showingFilter={props.showingFilter}
+        setSearch={props.setSearch}
+        clearSearch={props.clearSearch}
+        setSearchSort={props.setSearchSort}
+        history={props.history}
+        setKeywordFilter={props.setKeywordFilter}
+        removeSearchFilters={props.removeSearchFilters}
+        setSearchFilters={props.setSearchFilters}
+        allUsers={props.allUsers}
+        entityFilters={{ visible: true, archived: true, virtual: true }}
+        toggleFilter={() => toggleFilter()}
+        filterToggleLabel={props.filterToggleLabel}
+        challenges={props.challenges}
       />
-    );
+    )
 
-    const text = getByText("Metrics");
-    expect(text).toBeInTheDocument();
-  });
-});
+    const element = container.querySelectorAll('input[type=checkbox]')[1]
+    fireEvent.click(element)
+    expect(toggleFilter).toHaveBeenCalledTimes(1)
+  })
+})