about summary refs log tree commit diff
diff options
context:
space:
mode:
authorClaire <claire.github-309c@sitedethib.com>2021-10-14 22:57:41 +0200
committerGitHub <noreply@github.com>2021-10-14 22:57:41 +0200
commitb6f24ef0fb8b594c38c29090518c21af051b63b7 (patch)
tree5cec6a6b13dfd062848c74ae8e041c589ae40d00
parentebf2c3195615bb524f6908e84f99887c8775cbc3 (diff)
parent6964952d5f242ca936de9df361185b3e70a99ca4 (diff)
Merge pull request #1622 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes
-rw-r--r--Dockerfile1
-rw-r--r--Gemfile.lock14
-rw-r--r--Vagrantfile12
-rw-r--r--app/controllers/admin/dashboard_controller.rb37
-rw-r--r--app/controllers/api/v1/admin/dimensions_controller.rb23
-rw-r--r--app/controllers/api/v1/admin/measures_controller.rb22
-rw-r--r--app/controllers/api/v1/admin/retention_controller.rb22
-rw-r--r--app/controllers/api/v1/admin/trends_controller.rb16
-rw-r--r--app/controllers/api/v1/instances/activity_controller.rb27
-rw-r--r--app/helpers/application_helper.rb4
-rw-r--r--app/helpers/settings_helper.rb1
-rw-r--r--app/javascript/flavours/glitch/components/admin/Counter.js115
-rw-r--r--app/javascript/flavours/glitch/components/admin/Dimension.js92
-rw-r--r--app/javascript/flavours/glitch/components/admin/Retention.js141
-rw-r--r--app/javascript/flavours/glitch/components/admin/Trends.js73
-rw-r--r--app/javascript/flavours/glitch/components/hashtag.js61
-rw-r--r--app/javascript/flavours/glitch/components/skeleton.js11
-rw-r--r--app/javascript/flavours/glitch/containers/admin_component.js26
-rw-r--r--app/javascript/flavours/glitch/containers/media_container.js2
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/search_results.js2
-rw-r--r--app/javascript/flavours/glitch/features/getting_started/components/trends.js2
-rw-r--r--app/javascript/flavours/glitch/packs/admin.js24
-rw-r--r--app/javascript/flavours/glitch/styles/admin.scss193
-rw-r--r--app/javascript/flavours/glitch/styles/components/search.scss53
-rw-r--r--app/javascript/flavours/glitch/styles/dashboard.scss57
-rw-r--r--app/javascript/flavours/glitch/theme.yml2
-rw-r--r--app/javascript/flavours/glitch/util/numbers.js8
-rw-r--r--app/javascript/flavours/vanilla/theme.yml2
-rw-r--r--app/javascript/mastodon/components/admin/Counter.js115
-rw-r--r--app/javascript/mastodon/components/admin/Dimension.js92
-rw-r--r--app/javascript/mastodon/components/admin/Retention.js141
-rw-r--r--app/javascript/mastodon/components/admin/Trends.js73
-rw-r--r--app/javascript/mastodon/components/hashtag.js61
-rw-r--r--app/javascript/mastodon/components/skeleton.js11
-rw-r--r--app/javascript/mastodon/containers/admin_component.js26
-rw-r--r--app/javascript/mastodon/containers/media_container.js2
-rw-r--r--app/javascript/mastodon/features/compose/components/search_results.js2
-rw-r--r--app/javascript/mastodon/features/getting_started/components/trends.js2
-rw-r--r--app/javascript/mastodon/utils/numbers.js8
-rw-r--r--app/javascript/packs/admin.js24
-rw-r--r--app/javascript/styles/fonts/montserrat.scss2
-rw-r--r--app/javascript/styles/fonts/roboto-mono.scss1
-rw-r--r--app/javascript/styles/fonts/roboto.scss4
-rw-r--r--app/javascript/styles/mastodon/admin.scss193
-rw-r--r--app/javascript/styles/mastodon/components.scss53
-rw-r--r--app/javascript/styles/mastodon/dashboard.scss57
-rw-r--r--app/lib/activity_tracker.rb70
-rw-r--r--app/lib/admin/metrics/dimension.rb15
-rw-r--r--app/lib/admin/metrics/dimension/base_dimension.rb31
-rw-r--r--app/lib/admin/metrics/dimension/languages_dimension.rb23
-rw-r--r--app/lib/admin/metrics/dimension/servers_dimension.rb23
-rw-r--r--app/lib/admin/metrics/dimension/software_versions_dimension.rb69
-rw-r--r--app/lib/admin/metrics/dimension/sources_dimension.rb23
-rw-r--r--app/lib/admin/metrics/dimension/space_usage_dimension.rb70
-rw-r--r--app/lib/admin/metrics/measure.rb15
-rw-r--r--app/lib/admin/metrics/measure/active_users_measure.rb33
-rw-r--r--app/lib/admin/metrics/measure/base_measure.rb46
-rw-r--r--app/lib/admin/metrics/measure/interactions_measure.rb33
-rw-r--r--app/lib/admin/metrics/measure/new_users_measure.rb35
-rw-r--r--app/lib/admin/metrics/measure/opened_reports_measure.rb35
-rw-r--r--app/lib/admin/metrics/measure/resolved_reports_measure.rb36
-rw-r--r--app/lib/admin/metrics/retention.rb67
-rw-r--r--app/models/account_statuses_cleanup_policy.rb4
-rw-r--r--app/models/admin/action_log_filter.rb2
-rw-r--r--app/models/status.rb2
-rw-r--r--app/presenters/instance_presenter.rb4
-rw-r--r--app/serializers/rest/admin/cohort_serializer.rb19
-rw-r--r--app/serializers/rest/admin/dimension_serializer.rb5
-rw-r--r--app/serializers/rest/admin/measure_serializer.rb13
-rw-r--r--app/serializers/rest/admin/tag_serializer.rb13
-rw-r--r--app/services/post_status_service.rb3
-rw-r--r--app/validators/reaction_validator.rb2
-rw-r--r--app/views/admin/dashboard/index.html.haml184
-rw-r--r--chart/Chart.lock6
-rw-r--r--chart/Chart.yaml2
-rw-r--r--config/application.rb1
-rw-r--r--config/environments/production.rb2
-rw-r--r--config/initializers/twitter_regex.rb31
-rw-r--r--config/locales/en.yml50
-rw-r--r--config/routes.rb6
-rw-r--r--lib/cli.rb22
-rw-r--r--lib/mastodon/accounts_cli.rb11
-rw-r--r--package.json10
-rw-r--r--spec/models/account_statuses_cleanup_policy_spec.rb8
-rw-r--r--spec/services/post_status_service_spec.rb50
-rw-r--r--yarn.lock763
86 files changed, 2934 insertions, 813 deletions
diff --git a/Dockerfile b/Dockerfile
index 0b88b49a8..ab29e79b8 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -56,6 +56,7 @@ COPY Gemfile* package.json yarn.lock /opt/mastodon/
 RUN cd /opt/mastodon && \
   bundle config set deployment 'true' && \
   bundle config set without 'development test' && \
+  bundle config set silence_root_warning true && \
 	bundle install -j"$(nproc)" && \
 	yarn install --pure-lockfile
 
diff --git a/Gemfile.lock b/Gemfile.lock
index 2693140da..48287af96 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -188,7 +188,7 @@ GEM
     docile (1.3.4)
     domain_name (0.5.20190701)
       unf (>= 0.0.5, < 1.0.0)
-    doorkeeper (5.5.3)
+    doorkeeper (5.5.4)
       railties (>= 5)
     dotenv (2.7.6)
     dotenv-rails (2.7.6)
@@ -262,7 +262,7 @@ GEM
     hiredis (0.6.3)
     hkdf (0.3.0)
     htmlentities (4.3.4)
-    http (5.0.2)
+    http (5.0.4)
       addressable (~> 2.8)
       http-cookie (~> 1.0)
       http-form_data (~> 2.2)
@@ -326,7 +326,7 @@ GEM
       addressable (~> 2.7)
     letter_opener (1.7.0)
       launchy (~> 2.2)
-    letter_opener_web (1.4.0)
+    letter_opener_web (1.4.1)
       actionmailer (>= 3.2)
       letter_opener (~> 1.0)
       railties (>= 3.2)
@@ -357,7 +357,7 @@ GEM
     mime-types (3.3.1)
       mime-types-data (~> 3.2015)
     mime-types-data (3.2020.0512)
-    mini_mime (1.1.1)
+    mini_mime (1.1.2)
     mini_portile2 (2.6.1)
     minitest (5.14.4)
     msgpack (1.4.2)
@@ -424,7 +424,7 @@ GEM
     pry-rails (0.3.9)
       pry (>= 0.10.4)
     public_suffix (4.0.6)
-    puma (5.5.0)
+    puma (5.5.1)
       nio4r (~> 2.0)
     pundit (2.1.1)
       activesupport (>= 3.0.0)
@@ -531,7 +531,7 @@ GEM
       unicode-display_width (>= 1.4.0, < 3.0)
     rubocop-ast (1.12.0)
       parser (>= 3.0.1.1)
-    rubocop-rails (2.12.2)
+    rubocop-rails (2.12.3)
       activesupport (>= 4.2.0)
       rack (>= 1.1)
       rubocop (>= 1.7.0, < 2.0)
@@ -627,7 +627,7 @@ GEM
       tzinfo (>= 1.0.0)
     unf (0.1.4)
       unf_ext
-    unf_ext (0.0.7.7)
+    unf_ext (0.0.8)
     unicode-display_width (1.8.0)
     uniform_notifier (1.14.2)
     warden (1.2.9)
diff --git a/Vagrantfile b/Vagrantfile
index 25f563d95..aeff2f233 100644
--- a/Vagrantfile
+++ b/Vagrantfile
@@ -45,16 +45,8 @@ sudo apt-get install \
 # Install rvm
 read RUBY_VERSION < .ruby-version
 
-gpg_command="gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB"
-$($gpg_command)
-if [ $? -ne 0 ];then
-  echo "GPG command failed, This prevented RVM from installing."
-  echo "Retrying once..." && $($gpg_command)
-  if [ $? -ne 0 ];then
-    echo "GPG failed for the second time, please ensure network connectivity."
-    echo "Exiting..." && exit 1
-  fi
-fi
+curl -sSL https://rvm.io/mpapis.asc | gpg --import
+curl -sSL https://rvm.io/pkuczynski.asc | gpg --import
 
 curl -sSL https://raw.githubusercontent.com/rvm/rvm/stable/binscripts/rvm-installer | bash -s stable --ruby=$RUBY_VERSION
 source /home/vagrant/.rvm/scripts/rvm
diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index a00d7ed96..cbfff2707 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -1,50 +1,17 @@
 # frozen_string_literal: true
-require 'sidekiq/api'
 
 module Admin
   class DashboardController < BaseController
     def index
       @system_checks         = Admin::SystemCheck.perform
-      @users_count           = User.count
+      @time_period           = (1.month.ago.to_date...Time.now.utc.to_date)
       @pending_users_count   = User.pending.count
-      @registrations_week    = Redis.current.get("activity:accounts:local:#{current_week}") || 0
-      @logins_week           = Redis.current.pfcount("activity:logins:#{current_week}")
-      @interactions_week     = Redis.current.get("activity:interactions:#{current_week}") || 0
-      @relay_enabled         = Relay.enabled.exists?
-      @single_user_mode      = Rails.configuration.x.single_user_mode
-      @registrations_enabled = Setting.registrations_mode != 'none'
-      @deletions_enabled     = Setting.open_deletion
-      @invites_enabled       = Setting.min_invite_role == 'user'
-      @search_enabled        = Chewy.enabled?
-      @version               = Mastodon::Version.to_s
-      @database_version      = ActiveRecord::Base.connection.execute('SELECT VERSION()').first['version'].match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1]
-      @redis_version         = redis_info['redis_version']
-      @reports_count         = Report.unresolved.count
-      @queue_backlog         = Sidekiq::Stats.new.enqueued
-      @recent_users          = User.confirmed.recent.includes(:account).limit(8)
-      @database_size         = ActiveRecord::Base.connection.execute('SELECT pg_database_size(current_database())').first['pg_database_size']
-      @redis_size            = redis_info['used_memory']
-      @ldap_enabled          = ENV['LDAP_ENABLED'] == 'true'
-      @cas_enabled           = ENV['CAS_ENABLED'] == 'true'
-      @saml_enabled          = ENV['SAML_ENABLED'] == 'true'
-      @pam_enabled           = ENV['PAM_ENABLED'] == 'true'
-      @hidden_service        = ENV['ALLOW_ACCESS_TO_HIDDEN_SERVICE'] == 'true'
-      @trending_hashtags     = TrendingTags.get(10, filtered: false)
+      @pending_reports_count = Report.unresolved.count
       @pending_tags_count    = Tag.pending_review.count
-      @authorized_fetch      = authorized_fetch_mode?
-      @whitelist_enabled     = whitelist_mode?
-      @profile_directory     = Setting.profile_directory
-      @timeline_preview      = Setting.timeline_preview
-      @keybase_integration   = Setting.enable_keybase
-      @trends_enabled        = Setting.trends
     end
 
     private
 
-    def current_week
-      @current_week ||= Time.now.utc.to_date.cweek
-    end
-
     def redis_info
       @redis_info ||= begin
         if Redis.current.is_a?(Redis::Namespace)
diff --git a/app/controllers/api/v1/admin/dimensions_controller.rb b/app/controllers/api/v1/admin/dimensions_controller.rb
new file mode 100644
index 000000000..170596d27
--- /dev/null
+++ b/app/controllers/api/v1/admin/dimensions_controller.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class Api::V1::Admin::DimensionsController < Api::BaseController
+  protect_from_forgery with: :exception
+
+  before_action :require_staff!
+  before_action :set_dimensions
+
+  def create
+    render json: @dimensions, each_serializer: REST::Admin::DimensionSerializer
+  end
+
+  private
+
+  def set_dimensions
+    @dimensions = Admin::Metrics::Dimension.retrieve(
+      params[:keys],
+      params[:start_at],
+      params[:end_at],
+      params[:limit]
+    )
+  end
+end
diff --git a/app/controllers/api/v1/admin/measures_controller.rb b/app/controllers/api/v1/admin/measures_controller.rb
new file mode 100644
index 000000000..a3ac6fe85
--- /dev/null
+++ b/app/controllers/api/v1/admin/measures_controller.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class Api::V1::Admin::MeasuresController < Api::BaseController
+  protect_from_forgery with: :exception
+
+  before_action :require_staff!
+  before_action :set_measures
+
+  def create
+    render json: @measures, each_serializer: REST::Admin::MeasureSerializer
+  end
+
+  private
+
+  def set_measures
+    @measures = Admin::Metrics::Measure.retrieve(
+      params[:keys],
+      params[:start_at],
+      params[:end_at]
+    )
+  end
+end
diff --git a/app/controllers/api/v1/admin/retention_controller.rb b/app/controllers/api/v1/admin/retention_controller.rb
new file mode 100644
index 000000000..a8ff64f21
--- /dev/null
+++ b/app/controllers/api/v1/admin/retention_controller.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class Api::V1::Admin::RetentionController < Api::BaseController
+  protect_from_forgery with: :exception
+
+  before_action :require_staff!
+  before_action :set_cohorts
+
+  def create
+    render json: @cohorts, each_serializer: REST::Admin::CohortSerializer
+  end
+
+  private
+
+  def set_cohorts
+    @cohorts = Admin::Metrics::Retention.new(
+      params[:start_at],
+      params[:end_at],
+      params[:frequency]
+    ).cohorts
+  end
+end
diff --git a/app/controllers/api/v1/admin/trends_controller.rb b/app/controllers/api/v1/admin/trends_controller.rb
new file mode 100644
index 000000000..e32ab5d2c
--- /dev/null
+++ b/app/controllers/api/v1/admin/trends_controller.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class Api::V1::Admin::TrendsController < Api::BaseController
+  before_action :require_staff!
+  before_action :set_trends
+
+  def index
+    render json: @trends, each_serializer: REST::Admin::TagSerializer
+  end
+
+  private
+
+  def set_trends
+    @trends = TrendingTags.get(10, filtered: false)
+  end
+end
diff --git a/app/controllers/api/v1/instances/activity_controller.rb b/app/controllers/api/v1/instances/activity_controller.rb
index 4f6b4bcbf..bad61425a 100644
--- a/app/controllers/api/v1/instances/activity_controller.rb
+++ b/app/controllers/api/v1/instances/activity_controller.rb
@@ -14,22 +14,21 @@ class Api::V1::Instances::ActivityController < Api::BaseController
   private
 
   def activity
-    weeks = []
-
-    12.times do |i|
-      day     = i.weeks.ago.to_date
-      week_id = day.cweek
-      week    = Date.commercial(day.cwyear, week_id)
-
-      weeks << {
-        week: week.to_time.to_i.to_s,
-        statuses: Redis.current.get("activity:statuses:local:#{week_id}") || '0',
-        logins: Redis.current.pfcount("activity:logins:#{week_id}").to_s,
-        registrations: Redis.current.get("activity:accounts:local:#{week_id}") || '0',
+    statuses_tracker      = ActivityTracker.new('activity:statuses:local', :basic)
+    logins_tracker        = ActivityTracker.new('activity:logins', :unique)
+    registrations_tracker = ActivityTracker.new('activity:accounts:local', :basic)
+
+    (0...12).map do |i|
+      start_of_week = i.weeks.ago
+      end_of_week   = start_of_week + 6.days
+
+      {
+        week: start_of_week.to_i.to_s,
+        statuses: statuses_tracker.sum(start_of_week, end_of_week).to_s,
+        logins: logins_tracker.sum(start_of_week, end_of_week).to_s,
+        registrations: registrations_tracker.sum(start_of_week, end_of_week).to_s,
       }
     end
-
-    weeks
   end
 
   def require_enabled_api!
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index cc622478d..3fbc418cb 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -137,6 +137,10 @@ module ApplicationHelper
     end
   end
 
+  def react_admin_component(name, props = {})
+    content_tag(:div, nil, data: { 'admin-component': name.to_s.camelcase, props: Oj.dump({ locale: I18n.locale }.merge(props)) })
+  end
+
   def body_classes
     output = (@body_classes || '').split(' ')
     output << "flavour-#{current_flavour.parameterize}"
diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb
index 0ebfab75d..ac4c18746 100644
--- a/app/helpers/settings_helper.rb
+++ b/app/helpers/settings_helper.rb
@@ -41,6 +41,7 @@ module SettingsHelper
     ka: 'ქართული',
     kab: 'Taqbaylit',
     kk: 'Қазақша',
+    kmr: 'Kurmancî',
     kn: 'ಕನ್ನಡ',
     ko: '한국어',
     ku: 'سۆرانی',
diff --git a/app/javascript/flavours/glitch/components/admin/Counter.js b/app/javascript/flavours/glitch/components/admin/Counter.js
new file mode 100644
index 000000000..39ef216bd
--- /dev/null
+++ b/app/javascript/flavours/glitch/components/admin/Counter.js
@@ -0,0 +1,115 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import api from 'flavours/glitch/util/api';
+import { FormattedNumber } from 'react-intl';
+import { Sparklines, SparklinesCurve } from 'react-sparklines';
+import classNames from 'classnames';
+import Skeleton from 'flavours/glitch/components/skeleton';
+
+const percIncrease = (a, b) => {
+  let percent;
+
+  if (b !== 0) {
+    if (a !== 0) {
+      percent = (b - a) / a;
+    } else {
+      percent = 1;
+    }
+  } else if (b === 0 && a === 0) {
+    percent = 0;
+  } else {
+    percent = - 1;
+  }
+
+  return percent;
+};
+
+export default class Counter extends React.PureComponent {
+
+  static propTypes = {
+    measure: PropTypes.string.isRequired,
+    start_at: PropTypes.string.isRequired,
+    end_at: PropTypes.string.isRequired,
+    label: PropTypes.string.isRequired,
+    href: PropTypes.string,
+  };
+
+  state = {
+    loading: true,
+    data: null,
+  };
+
+  componentDidMount () {
+    const { measure, start_at, end_at } = this.props;
+
+    api().post('/api/v1/admin/measures', { keys: [measure], start_at, end_at }).then(res => {
+      this.setState({
+        loading: false,
+        data: res.data,
+      });
+    }).catch(err => {
+      console.error(err);
+    });
+  }
+
+  render () {
+    const { label, href } = this.props;
+    const { loading, data } = this.state;
+
+    let content;
+
+    if (loading) {
+      content = (
+        <React.Fragment>
+          <span className='sparkline__value__total'><Skeleton width={43} /></span>
+          <span className='sparkline__value__change'><Skeleton width={43} /></span>
+        </React.Fragment>
+      );
+    } else {
+      const measure = data[0];
+      const percentChange = percIncrease(measure.previous_total * 1, measure.total * 1);
+
+      content = (
+        <React.Fragment>
+          <span className='sparkline__value__total'><FormattedNumber value={measure.total} /></span>
+          <span className={classNames('sparkline__value__change', { positive: percentChange > 0, negative: percentChange < 0 })}>{percentChange > 0 && '+'}<FormattedNumber value={percentChange} style='percent' /></span>
+        </React.Fragment>
+      );
+    }
+
+    const inner = (
+      <React.Fragment>
+        <div className='sparkline__value'>
+          {content}
+        </div>
+
+        <div className='sparkline__label'>
+          {label}
+        </div>
+
+        <div className='sparkline__graph'>
+          {!loading && (
+            <Sparklines width={259} height={55} data={data[0].data.map(x => x.value * 1)}>
+              <SparklinesCurve />
+            </Sparklines>
+          )}
+        </div>
+      </React.Fragment>
+    );
+
+    if (href) {
+      return (
+        <a href={href} className='sparkline'>
+          {inner}
+        </a>
+      );
+    } else {
+      return (
+        <div className='sparkline'>
+          {inner}
+        </div>
+      );
+    }
+  }
+
+}
diff --git a/app/javascript/flavours/glitch/components/admin/Dimension.js b/app/javascript/flavours/glitch/components/admin/Dimension.js
new file mode 100644
index 000000000..b4fbf86c8
--- /dev/null
+++ b/app/javascript/flavours/glitch/components/admin/Dimension.js
@@ -0,0 +1,92 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import api from 'flavours/glitch/util/api';
+import { FormattedNumber } from 'react-intl';
+import { roundTo10 } from 'flavours/glitch/util/numbers';
+import Skeleton from 'flavours/glitch/components/skeleton';
+
+export default class Dimension extends React.PureComponent {
+
+  static propTypes = {
+    dimension: PropTypes.string.isRequired,
+    start_at: PropTypes.string.isRequired,
+    end_at: PropTypes.string.isRequired,
+    limit: PropTypes.number.isRequired,
+    label: PropTypes.string.isRequired,
+  };
+
+  state = {
+    loading: true,
+    data: null,
+  };
+
+  componentDidMount () {
+    const { start_at, end_at, dimension, limit } = this.props;
+
+    api().post('/api/v1/admin/dimensions', { keys: [dimension], start_at, end_at, limit }).then(res => {
+      this.setState({
+        loading: false,
+        data: res.data,
+      });
+    }).catch(err => {
+      console.error(err);
+    });
+  }
+
+  render () {
+    const { label, limit } = this.props;
+    const { loading, data } = this.state;
+
+    let content;
+
+    if (loading) {
+      content = (
+        <table>
+          <tbody>
+            {Array.from(Array(limit)).map((_, i) => (
+              <tr className='dimension__item' key={i}>
+                <td className='dimension__item__key'>
+                  <Skeleton width={100} />
+                </td>
+
+                <td className='dimension__item__value'>
+                  <Skeleton width={60} />
+                </td>
+              </tr>
+            ))}
+          </tbody>
+        </table>
+      );
+    } else {
+      const sum = data[0].data.reduce((sum, cur) => sum + (cur.value * 1), 0);
+
+      content = (
+        <table>
+          <tbody>
+            {data[0].data.map(item => (
+              <tr className='dimension__item' key={item.key}>
+                <td className='dimension__item__key'>
+                  <span className={`dimension__item__indicator dimension__item__indicator--${roundTo10(((item.value * 1) / sum) * 100)}`} />
+                  <span title={item.key}>{item.human_key}</span>
+                </td>
+
+                <td className='dimension__item__value'>
+                  {typeof item.human_value !== 'undefined' ? item.human_value : <FormattedNumber value={item.value} />}
+                </td>
+              </tr>
+            ))}
+          </tbody>
+        </table>
+      );
+    }
+
+    return (
+      <div className='dimension'>
+        <h4>{label}</h4>
+
+        {content}
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/glitch/components/admin/Retention.js b/app/javascript/flavours/glitch/components/admin/Retention.js
new file mode 100644
index 000000000..8295362a4
--- /dev/null
+++ b/app/javascript/flavours/glitch/components/admin/Retention.js
@@ -0,0 +1,141 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import api from 'flavours/glitch/util/api';
+import { FormattedMessage, FormattedNumber, FormattedDate } from 'react-intl';
+import classNames from 'classnames';
+import { roundTo10 } from 'flavours/glitch/util/numbers';
+
+const dateForCohort = cohort => {
+  switch(cohort.frequency) {
+  case 'day':
+    return <FormattedDate value={cohort.period} month='long' day='2-digit' />;
+  default:
+    return <FormattedDate value={cohort.period} month='long' year='numeric' />;
+  }
+};
+
+export default class Retention extends React.PureComponent {
+
+  static propTypes = {
+    start_at: PropTypes.string,
+    end_at: PropTypes.string,
+    frequency: PropTypes.string,
+  };
+
+  state = {
+    loading: true,
+    data: null,
+  };
+
+  componentDidMount () {
+    const { start_at, end_at, frequency } = this.props;
+
+    api().post('/api/v1/admin/retention', { start_at, end_at, frequency }).then(res => {
+      this.setState({
+        loading: false,
+        data: res.data,
+      });
+    }).catch(err => {
+      console.error(err);
+    });
+  }
+
+  render () {
+    const { loading, data } = this.state;
+
+    let content;
+
+    if (loading) {
+      content = <FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' />;
+    } else {
+      content = (
+        <table className='retention__table'>
+          <thead>
+            <tr>
+              <th>
+                <div className='retention__table__date retention__table__label'>
+                  <FormattedMessage id='admin.dashboard.retention.cohort' defaultMessage='Sign-up month' />
+                </div>
+              </th>
+
+              <th>
+                <div className='retention__table__number retention__table__label'>
+                  <FormattedMessage id='admin.dashboard.retention.cohort_size' defaultMessage='New users' />
+                </div>
+              </th>
+
+              {data[0].data.slice(1).map((retention, i) => (
+                <th key={retention.date}>
+                  <div className='retention__table__number retention__table__label'>
+                    {i + 1}
+                  </div>
+                </th>
+              ))}
+            </tr>
+
+            <tr>
+              <td>
+                <div className='retention__table__date retention__table__average'>
+                  <FormattedMessage id='admin.dashboard.retention.average' defaultMessage='Average' />
+                </div>
+              </td>
+
+              <td>
+                <div className='retention__table__size'>
+                  <FormattedNumber value={data.reduce((sum, cohort, i) => sum + ((cohort.data[0].value * 1) - sum) / (i + 1), 0)} maximumFractionDigits={0} />
+                </div>
+              </td>
+
+              {data[0].data.slice(1).map((retention, i) => {
+                const average = data.reduce((sum, cohort, k) => cohort.data[i + 1] ? sum + (cohort.data[i + 1].percent - sum)/(k + 1) : sum, 0);
+
+                return (
+                  <td key={retention.date}>
+                    <div className={classNames('retention__table__box', 'retention__table__average', `retention__table__box--${roundTo10(average * 100)}`)}>
+                      <FormattedNumber value={average} style='percent' />
+                    </div>
+                  </td>
+                );
+              })}
+            </tr>
+          </thead>
+
+          <tbody>
+            {data.slice(0, -1).map(cohort => (
+              <tr key={cohort.period}>
+                <td>
+                  <div className='retention__table__date'>
+                    {dateForCohort(cohort)}
+                  </div>
+                </td>
+
+                <td>
+                  <div className='retention__table__size'>
+                    <FormattedNumber value={cohort.data[0].value} />
+                  </div>
+                </td>
+
+                {cohort.data.slice(1).map(retention => (
+                  <td key={retention.date}>
+                    <div className={classNames('retention__table__box', `retention__table__box--${roundTo10(retention.percent * 100)}`)}>
+                      <FormattedNumber value={retention.percent} style='percent' />
+                    </div>
+                  </td>
+                ))}
+              </tr>
+            ))}
+          </tbody>
+        </table>
+      );
+    }
+
+    return (
+      <div className='retention'>
+        <h4><FormattedMessage id='admin.dashboard.retention' defaultMessage='Retention' /></h4>
+
+        {content}
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/glitch/components/admin/Trends.js b/app/javascript/flavours/glitch/components/admin/Trends.js
new file mode 100644
index 000000000..d7c4eb72c
--- /dev/null
+++ b/app/javascript/flavours/glitch/components/admin/Trends.js
@@ -0,0 +1,73 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import api from 'flavours/glitch/util/api';
+import { FormattedMessage } from 'react-intl';
+import classNames from 'classnames';
+import Hashtag from 'flavours/glitch/components/hashtag';
+
+export default class Trends extends React.PureComponent {
+
+  static propTypes = {
+    limit: PropTypes.number.isRequired,
+  };
+
+  state = {
+    loading: true,
+    data: null,
+  };
+
+  componentDidMount () {
+    const { limit } = this.props;
+
+    api().get('/api/v1/admin/trends', { params: { limit } }).then(res => {
+      this.setState({
+        loading: false,
+        data: res.data,
+      });
+    }).catch(err => {
+      console.error(err);
+    });
+  }
+
+  render () {
+    const { limit } = this.props;
+    const { loading, data } = this.state;
+
+    let content;
+
+    if (loading) {
+      content = (
+        <div>
+          {Array.from(Array(limit)).map((_, i) => (
+            <Hashtag key={i} />
+          ))}
+        </div>
+      );
+    } else {
+      content = (
+        <div>
+          {data.map(hashtag => (
+            <Hashtag
+              key={hashtag.name}
+              name={hashtag.name}
+              href={`/admin/tags/${hashtag.id}`}
+              people={hashtag.history[0].accounts * 1 + hashtag.history[1].accounts * 1}
+              uses={hashtag.history[0].uses * 1 + hashtag.history[1].uses * 1}
+              history={hashtag.history.reverse().map(day => day.uses)}
+              className={classNames(hashtag.requires_review && 'trends__item--requires-review', !hashtag.trendable && !hashtag.requires_review && 'trends__item--disabled')}
+            />
+          ))}
+        </div>
+      );
+    }
+
+    return (
+      <div className='trends trends--compact'>
+        <h4><FormattedMessage id='trends.trending_now' defaultMessage='Trending now' /></h4>
+
+        {content}
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/glitch/components/hashtag.js b/app/javascript/flavours/glitch/components/hashtag.js
index d00c01e77..769185a2b 100644
--- a/app/javascript/flavours/glitch/components/hashtag.js
+++ b/app/javascript/flavours/glitch/components/hashtag.js
@@ -6,6 +6,8 @@ import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import Permalink from './permalink';
 import ShortNumber from 'flavours/glitch/components/short_number';
+import Skeleton from 'flavours/glitch/components/skeleton';
+import classNames from 'classnames';
 
 class SilentErrorBoundary extends React.Component {
 
@@ -47,45 +49,38 @@ const accountsCountRenderer = (displayNumber, pluralReady) => (
   />
 );
 
-const Hashtag = ({ hashtag }) => (
-  <div className='trends__item'>
+export const ImmutableHashtag = ({ hashtag }) => (
+  <Hashtag
+    name={hashtag.get('name')}
+    href={hashtag.get('url')}
+    to={`/tags/${hashtag.get('name')}`}
+    people={hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1}
+    uses={hashtag.getIn(['history', 0, 'uses']) * 1 + hashtag.getIn(['history', 1, 'uses']) * 1}
+    history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()}
+  />
+);
+
+ImmutableHashtag.propTypes = {
+  hashtag: ImmutablePropTypes.map.isRequired,
+};
+
+const Hashtag = ({ name, href, to, people, uses, history, className }) => (
+  <div className={classNames('trends__item', className)}>
     <div className='trends__item__name'>
-      <Permalink
-        href={hashtag.get('url')}
-        to={`/tags/${hashtag.get('name')}`}
-      >
-        #<span>{hashtag.get('name')}</span>
+      <Permalink href={href} to={to}>
+        {name ? <React.Fragment>#<span>{name}</span></React.Fragment> : <Skeleton width={50} />}
       </Permalink>
 
-      <ShortNumber
-        value={
-          hashtag.getIn(['history', 0, 'accounts']) * 1 +
-          hashtag.getIn(['history', 1, 'accounts']) * 1
-        }
-        renderer={accountsCountRenderer}
-      />
+      {typeof people !== 'undefined' ? <ShortNumber value={people} renderer={accountsCountRenderer} /> : <Skeleton width={100} />}
     </div>
 
     <div className='trends__item__current'>
-      <ShortNumber
-        value={
-          hashtag.getIn(['history', 0, 'uses']) * 1 +
-          hashtag.getIn(['history', 1, 'uses']) * 1
-        }
-      />
+      {typeof uses !== 'undefined' ? <ShortNumber value={uses} /> : <Skeleton width={42} height={36} />}
     </div>
 
     <div className='trends__item__sparkline'>
       <SilentErrorBoundary>
-        <Sparklines
-          width={50}
-          height={28}
-          data={hashtag
-            .get('history')
-            .reverse()
-            .map((day) => day.get('uses'))
-            .toArray()}
-        >
+        <Sparklines width={50} height={28} data={history ? history : Array.from(Array(7)).map(() => 0)}>
           <SparklinesCurve style={{ fill: 'none' }} />
         </Sparklines>
       </SilentErrorBoundary>
@@ -94,7 +89,13 @@ const Hashtag = ({ hashtag }) => (
 );
 
 Hashtag.propTypes = {
-  hashtag: ImmutablePropTypes.map.isRequired,
+  name: PropTypes.string,
+  href: PropTypes.string,
+  to: PropTypes.string,
+  people: PropTypes.number,
+  uses: PropTypes.number,
+  history: PropTypes.arrayOf(PropTypes.number),
+  className: PropTypes.string,
 };
 
 export default Hashtag;
diff --git a/app/javascript/flavours/glitch/components/skeleton.js b/app/javascript/flavours/glitch/components/skeleton.js
new file mode 100644
index 000000000..09093e99c
--- /dev/null
+++ b/app/javascript/flavours/glitch/components/skeleton.js
@@ -0,0 +1,11 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+const Skeleton = ({ width, height }) => <span className='skeleton' style={{ width, height }}>&zwnj;</span>;
+
+Skeleton.propTypes = {
+  width: PropTypes.number,
+  height: PropTypes.number,
+};
+
+export default Skeleton;
diff --git a/app/javascript/flavours/glitch/containers/admin_component.js b/app/javascript/flavours/glitch/containers/admin_component.js
new file mode 100644
index 000000000..64dabac8b
--- /dev/null
+++ b/app/javascript/flavours/glitch/containers/admin_component.js
@@ -0,0 +1,26 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { IntlProvider, addLocaleData } from 'react-intl';
+import { getLocale } from 'mastodon/locales';
+
+const { localeData, messages } = getLocale();
+addLocaleData(localeData);
+
+export default class AdminComponent extends React.PureComponent {
+
+  static propTypes = {
+    locale: PropTypes.string.isRequired,
+    children: PropTypes.node.isRequired,
+  };
+
+  render () {
+    const { locale, children } = this.props;
+
+    return (
+      <IntlProvider locale={locale} messages={messages}>
+        {children}
+      </IntlProvider>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/glitch/containers/media_container.js b/app/javascript/flavours/glitch/containers/media_container.js
index 8657b8064..1ddbc706b 100644
--- a/app/javascript/flavours/glitch/containers/media_container.js
+++ b/app/javascript/flavours/glitch/containers/media_container.js
@@ -7,7 +7,7 @@ import { getLocale } from 'mastodon/locales';
 import { getScrollbarWidth } from 'flavours/glitch/util/scrollbar';
 import MediaGallery from 'flavours/glitch/components/media_gallery';
 import Poll from 'flavours/glitch/components/poll';
-import Hashtag from 'flavours/glitch/components/hashtag';
+import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag';
 import ModalRoot from 'flavours/glitch/components/modal_root';
 import MediaModal from 'flavours/glitch/features/ui/components/media_modal';
 import Video from 'flavours/glitch/features/video';
diff --git a/app/javascript/flavours/glitch/features/compose/components/search_results.js b/app/javascript/flavours/glitch/features/compose/components/search_results.js
index 9a76e5418..cbc1f35e5 100644
--- a/app/javascript/flavours/glitch/features/compose/components/search_results.js
+++ b/app/javascript/flavours/glitch/features/compose/components/search_results.js
@@ -5,7 +5,7 @@ import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
 import AccountContainer from 'flavours/glitch/containers/account_container';
 import StatusContainer from 'flavours/glitch/containers/status_container';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import Hashtag from 'flavours/glitch/components/hashtag';
+import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag';
 import Icon from 'flavours/glitch/components/icon';
 import { searchEnabled } from 'flavours/glitch/util/initial_state';
 import LoadMore from 'flavours/glitch/components/load_more';
diff --git a/app/javascript/flavours/glitch/features/getting_started/components/trends.js b/app/javascript/flavours/glitch/features/getting_started/components/trends.js
index 0734ec72b..ce4d94c64 100644
--- a/app/javascript/flavours/glitch/features/getting_started/components/trends.js
+++ b/app/javascript/flavours/glitch/features/getting_started/components/trends.js
@@ -2,7 +2,7 @@ import React from 'react';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import Hashtag from 'flavours/glitch/components/hashtag';
+import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag';
 import { FormattedMessage } from 'react-intl';
 
 export default class Trends extends ImmutablePureComponent {
diff --git a/app/javascript/flavours/glitch/packs/admin.js b/app/javascript/flavours/glitch/packs/admin.js
new file mode 100644
index 000000000..4c09ddb05
--- /dev/null
+++ b/app/javascript/flavours/glitch/packs/admin.js
@@ -0,0 +1,24 @@
+import 'packs/public-path';
+import ready from 'flavours/glitch/util/ready';
+
+ready(() => {
+  const React    = require('react');
+  const ReactDOM = require('react-dom');
+
+  [].forEach.call(document.querySelectorAll('[data-admin-component]'), element => {
+    const componentName  = element.getAttribute('data-admin-component');
+    const { locale, ...componentProps } = JSON.parse(element.getAttribute('data-props'));
+
+    import('flavours/glitch/containers/admin_component').then(({ default: AdminComponent }) => {
+      return import('flavours/glitch/components/admin/' + componentName).then(({ default: Component }) => {
+        ReactDOM.render((
+          <AdminComponent locale={locale}>
+            <Component {...componentProps} />
+          </AdminComponent>
+        ), element);
+      });
+    }).catch(error => {
+      console.error(error);
+    });
+  });
+});
diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss
index 4801a4644..24618c29f 100644
--- a/app/javascript/flavours/glitch/styles/admin.scss
+++ b/app/javascript/flavours/glitch/styles/admin.scss
@@ -1,3 +1,5 @@
+@use "sass:math";
+
 $no-columns-breakpoint: 600px;
 $sidebar-width: 240px;
 $content-width: 840px;
@@ -925,10 +927,197 @@ a.name-tag,
   }
 }
 
+.dashboard__counters.admin-account-counters {
+  margin-top: 10px;
+}
+
 .account-badges {
   margin: -2px 0;
 }
 
-.dashboard__counters.admin-account-counters {
-  margin-top: 10px;
+.retention {
+  &__table {
+    &__number {
+      color: $secondary-text-color;
+      padding: 10px;
+    }
+
+    &__date {
+      white-space: nowrap;
+      padding: 10px 0;
+      text-align: left;
+      min-width: 120px;
+
+      &.retention__table__average {
+        font-weight: 700;
+      }
+    }
+
+    &__size {
+      text-align: center;
+      padding: 10px;
+    }
+
+    &__label {
+      font-weight: 700;
+      color: $darker-text-color;
+    }
+
+    &__box {
+      box-sizing: border-box;
+      background: $ui-highlight-color;
+      padding: 10px;
+      font-weight: 500;
+      color: $primary-text-color;
+      width: 52px;
+      margin: 1px;
+
+      @for $i from 0 through 10 {
+        &--#{10 * $i} {
+          background-color: rgba($ui-highlight-color, 1 * (math.div(max(1, $i), 10)));
+        }
+      }
+    }
+  }
+}
+
+.sparkline {
+  display: block;
+  text-decoration: none;
+  background: lighten($ui-base-color, 4%);
+  border-radius: 4px;
+  padding: 0;
+  position: relative;
+  padding-bottom: 55px + 20px;
+  overflow: hidden;
+
+  &__value {
+    display: flex;
+    line-height: 33px;
+    align-items: flex-end;
+    padding: 20px;
+    padding-bottom: 10px;
+
+    &__total {
+      display: block;
+      margin-right: 10px;
+      font-weight: 500;
+      font-size: 28px;
+      color: $primary-text-color;
+    }
+
+    &__change {
+      display: block;
+      font-weight: 500;
+      font-size: 18px;
+      color: $darker-text-color;
+      margin-bottom: -3px;
+
+      &.positive {
+        color: $valid-value-color;
+      }
+
+      &.negative {
+        color: $error-value-color;
+      }
+    }
+  }
+
+  &__label {
+    padding: 0 20px;
+    padding-bottom: 10px;
+    text-transform: uppercase;
+    color: $darker-text-color;
+    font-weight: 500;
+  }
+
+  &__graph {
+    position: absolute;
+    bottom: 0;
+
+    svg {
+      display: block;
+      margin: 0;
+    }
+
+    path:first-child {
+      fill: rgba($highlight-text-color, 0.25) !important;
+      fill-opacity: 1 !important;
+    }
+
+    path:last-child {
+      stroke: lighten($highlight-text-color, 6%) !important;
+      fill: none !important;
+    }
+  }
+}
+
+a.sparkline {
+  &:hover,
+  &:focus,
+  &:active {
+    background: lighten($ui-base-color, 6%);
+  }
+}
+
+.skeleton {
+  background-color: lighten($ui-base-color, 8%);
+  background-image: linear-gradient(90deg, lighten($ui-base-color, 8%), lighten($ui-base-color, 12%), lighten($ui-base-color, 8%));
+  background-size: 200px 100%;
+  background-repeat: no-repeat;
+  border-radius: 4px;
+  display: inline-block;
+  line-height: 1;
+  width: 100%;
+  animation: skeleton 1.2s ease-in-out infinite;
+}
+
+@keyframes skeleton {
+  0% {
+    background-position: -200px 0;
+  }
+
+  100% {
+    background-position: calc(200px + 100%) 0;
+  }
+}
+
+.dimension {
+  table {
+    width: 100%;
+  }
+
+  &__item {
+    border-bottom: 1px solid lighten($ui-base-color, 4%);
+
+    &__key {
+      font-weight: 500;
+      padding: 11px 10px;
+    }
+
+    &__value {
+      text-align: right;
+      color: $darker-text-color;
+      padding: 11px 10px;
+    }
+
+    &__indicator {
+      display: inline-block;
+      width: 8px;
+      height: 8px;
+      border-radius: 50%;
+      background: $ui-highlight-color;
+      margin-right: 10px;
+
+      @for $i from 0 through 10 {
+        &--#{10 * $i} {
+          background-color: rgba($ui-highlight-color, 1 * (math.div(max(1, $i), 10)));
+        }
+      }
+    }
+
+    &:last-child {
+      border-bottom: 0;
+    }
+  }
 }
diff --git a/app/javascript/flavours/glitch/styles/components/search.scss b/app/javascript/flavours/glitch/styles/components/search.scss
index 929769130..f7415368b 100644
--- a/app/javascript/flavours/glitch/styles/components/search.scss
+++ b/app/javascript/flavours/glitch/styles/components/search.scss
@@ -171,7 +171,6 @@
     &__current {
       flex: 0 0 auto;
       font-size: 24px;
-      line-height: 36px;
       font-weight: 500;
       text-align: right;
       padding-right: 15px;
@@ -193,5 +192,57 @@
         fill: none !important;
       }
     }
+
+    &--requires-review {
+      .trends__item__name {
+        color: $gold-star;
+
+        a {
+          color: $gold-star;
+        }
+      }
+
+      .trends__item__current {
+        color: $gold-star;
+      }
+
+      .trends__item__sparkline {
+        path:first-child {
+          fill: rgba($gold-star, 0.25) !important;
+        }
+
+        path:last-child {
+          stroke: lighten($gold-star, 6%) !important;
+        }
+      }
+    }
+
+    &--disabled {
+      .trends__item__name {
+        color: lighten($ui-base-color, 12%);
+
+        a {
+          color: lighten($ui-base-color, 12%);
+        }
+      }
+
+      .trends__item__current {
+        color: lighten($ui-base-color, 12%);
+      }
+
+      .trends__item__sparkline {
+        path:first-child {
+          fill: rgba(lighten($ui-base-color, 12%), 0.25) !important;
+        }
+
+        path:last-child {
+          stroke: lighten(lighten($ui-base-color, 12%), 6%) !important;
+        }
+      }
+    }
+  }
+
+  &--compact &__item {
+    padding: 10px;
   }
 }
diff --git a/app/javascript/flavours/glitch/styles/dashboard.scss b/app/javascript/flavours/glitch/styles/dashboard.scss
index c0944d417..cad5a105b 100644
--- a/app/javascript/flavours/glitch/styles/dashboard.scss
+++ b/app/javascript/flavours/glitch/styles/dashboard.scss
@@ -56,23 +56,56 @@
   }
 }
 
-.dashboard__widgets {
-  display: flex;
-  flex-wrap: wrap;
-  margin: 0 -5px;
+.dashboard {
+  display: grid;
+  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr);
+  grid-gap: 10px;
 
-  & > div {
-    flex: 0 0 33.333%;
-    margin-bottom: 20px;
+  &__item {
+    &--span-double-column {
+      grid-column: span 2;
+    }
 
-    & > div {
-      padding: 0 5px;
+    &--span-double-row {
+      grid-row: span 2;
+    }
+
+    h4 {
+      padding-top: 20px;
     }
   }
 
-  a:not(.name-tag) {
-    color: $ui-secondary-color;
-    font-weight: 500;
+  &__quick-access {
+    display: flex;
+    align-items: baseline;
+    border-radius: 4px;
+    background: $ui-highlight-color;
+    color: $primary-text-color;
+    transition: all 100ms ease-in;
+    font-size: 14px;
+    padding: 0 16px;
+    line-height: 36px;
+    height: 36px;
     text-decoration: none;
+    margin-bottom: 4px;
+
+    &:active,
+    &:focus,
+    &:hover {
+      background-color: lighten($ui-highlight-color, 10%);
+      transition: all 200ms ease-out;
+    }
+
+    span {
+      flex: 1 1 auto;
+    }
+
+    .fa {
+      flex: 0 0 auto;
+    }
+
+    strong {
+      font-weight: 700;
+    }
   }
 }
diff --git a/app/javascript/flavours/glitch/theme.yml b/app/javascript/flavours/glitch/theme.yml
index 2a98e4c29..ee2b699b2 100644
--- a/app/javascript/flavours/glitch/theme.yml
+++ b/app/javascript/flavours/glitch/theme.yml
@@ -1,7 +1,7 @@
 #  (REQUIRED) The location of the pack files.
 pack:
   about: packs/about.js
-  admin: packs/public.js
+  admin: packs/admin.js
   auth: packs/public.js
   common:
     filename: packs/common.js
diff --git a/app/javascript/flavours/glitch/util/numbers.js b/app/javascript/flavours/glitch/util/numbers.js
index 6f2505cae..6ef563ad8 100644
--- a/app/javascript/flavours/glitch/util/numbers.js
+++ b/app/javascript/flavours/glitch/util/numbers.js
@@ -69,3 +69,11 @@ export function pluralReady(sourceNumber, division) {
 
   return Math.trunc(sourceNumber / closestScale) * closestScale;
 }
+
+/**
+ * @param {number} num
+ * @returns {number}
+ */
+export function roundTo10(num) {
+  return Math.round(num * 0.1) / 0.1;
+}
diff --git a/app/javascript/flavours/vanilla/theme.yml b/app/javascript/flavours/vanilla/theme.yml
index 74e9fb1b5..3263fd7d4 100644
--- a/app/javascript/flavours/vanilla/theme.yml
+++ b/app/javascript/flavours/vanilla/theme.yml
@@ -1,7 +1,7 @@
 #  (REQUIRED) The location of the pack files inside `pack_directory`.
 pack:
   about: about.js
-  admin: public.js
+  admin: admin.js
   auth: public.js
   common:
     filename: common.js
diff --git a/app/javascript/mastodon/components/admin/Counter.js b/app/javascript/mastodon/components/admin/Counter.js
new file mode 100644
index 000000000..cda572dce
--- /dev/null
+++ b/app/javascript/mastodon/components/admin/Counter.js
@@ -0,0 +1,115 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import api from 'mastodon/api';
+import { FormattedNumber } from 'react-intl';
+import { Sparklines, SparklinesCurve } from 'react-sparklines';
+import classNames from 'classnames';
+import Skeleton from 'mastodon/components/skeleton';
+
+const percIncrease = (a, b) => {
+  let percent;
+
+  if (b !== 0) {
+    if (a !== 0) {
+      percent = (b - a) / a;
+    } else {
+      percent = 1;
+    }
+  } else if (b === 0 && a === 0) {
+    percent = 0;
+  } else {
+    percent = - 1;
+  }
+
+  return percent;
+};
+
+export default class Counter extends React.PureComponent {
+
+  static propTypes = {
+    measure: PropTypes.string.isRequired,
+    start_at: PropTypes.string.isRequired,
+    end_at: PropTypes.string.isRequired,
+    label: PropTypes.string.isRequired,
+    href: PropTypes.string,
+  };
+
+  state = {
+    loading: true,
+    data: null,
+  };
+
+  componentDidMount () {
+    const { measure, start_at, end_at } = this.props;
+
+    api().post('/api/v1/admin/measures', { keys: [measure], start_at, end_at }).then(res => {
+      this.setState({
+        loading: false,
+        data: res.data,
+      });
+    }).catch(err => {
+      console.error(err);
+    });
+  }
+
+  render () {
+    const { label, href } = this.props;
+    const { loading, data } = this.state;
+
+    let content;
+
+    if (loading) {
+      content = (
+        <React.Fragment>
+          <span className='sparkline__value__total'><Skeleton width={43} /></span>
+          <span className='sparkline__value__change'><Skeleton width={43} /></span>
+        </React.Fragment>
+      );
+    } else {
+      const measure = data[0];
+      const percentChange = percIncrease(measure.previous_total * 1, measure.total * 1);
+
+      content = (
+        <React.Fragment>
+          <span className='sparkline__value__total'><FormattedNumber value={measure.total} /></span>
+          <span className={classNames('sparkline__value__change', { positive: percentChange > 0, negative: percentChange < 0 })}>{percentChange > 0 && '+'}<FormattedNumber value={percentChange} style='percent' /></span>
+        </React.Fragment>
+      );
+    }
+
+    const inner = (
+      <React.Fragment>
+        <div className='sparkline__value'>
+          {content}
+        </div>
+
+        <div className='sparkline__label'>
+          {label}
+        </div>
+
+        <div className='sparkline__graph'>
+          {!loading && (
+            <Sparklines width={259} height={55} data={data[0].data.map(x => x.value * 1)}>
+              <SparklinesCurve />
+            </Sparklines>
+          )}
+        </div>
+      </React.Fragment>
+    );
+
+    if (href) {
+      return (
+        <a href={href} className='sparkline'>
+          {inner}
+        </a>
+      );
+    } else {
+      return (
+        <div className='sparkline'>
+          {inner}
+        </div>
+      );
+    }
+  }
+
+}
diff --git a/app/javascript/mastodon/components/admin/Dimension.js b/app/javascript/mastodon/components/admin/Dimension.js
new file mode 100644
index 000000000..ac6dbd1c7
--- /dev/null
+++ b/app/javascript/mastodon/components/admin/Dimension.js
@@ -0,0 +1,92 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import api from 'mastodon/api';
+import { FormattedNumber } from 'react-intl';
+import { roundTo10 } from 'mastodon/utils/numbers';
+import Skeleton from 'mastodon/components/skeleton';
+
+export default class Dimension extends React.PureComponent {
+
+  static propTypes = {
+    dimension: PropTypes.string.isRequired,
+    start_at: PropTypes.string.isRequired,
+    end_at: PropTypes.string.isRequired,
+    limit: PropTypes.number.isRequired,
+    label: PropTypes.string.isRequired,
+  };
+
+  state = {
+    loading: true,
+    data: null,
+  };
+
+  componentDidMount () {
+    const { start_at, end_at, dimension, limit } = this.props;
+
+    api().post('/api/v1/admin/dimensions', { keys: [dimension], start_at, end_at, limit }).then(res => {
+      this.setState({
+        loading: false,
+        data: res.data,
+      });
+    }).catch(err => {
+      console.error(err);
+    });
+  }
+
+  render () {
+    const { label, limit } = this.props;
+    const { loading, data } = this.state;
+
+    let content;
+
+    if (loading) {
+      content = (
+        <table>
+          <tbody>
+            {Array.from(Array(limit)).map((_, i) => (
+              <tr className='dimension__item' key={i}>
+                <td className='dimension__item__key'>
+                  <Skeleton width={100} />
+                </td>
+
+                <td className='dimension__item__value'>
+                  <Skeleton width={60} />
+                </td>
+              </tr>
+            ))}
+          </tbody>
+        </table>
+      );
+    } else {
+      const sum = data[0].data.reduce((sum, cur) => sum + (cur.value * 1), 0);
+
+      content = (
+        <table>
+          <tbody>
+            {data[0].data.map(item => (
+              <tr className='dimension__item' key={item.key}>
+                <td className='dimension__item__key'>
+                  <span className={`dimension__item__indicator dimension__item__indicator--${roundTo10(((item.value * 1) / sum) * 100)}`} />
+                  <span title={item.key}>{item.human_key}</span>
+                </td>
+
+                <td className='dimension__item__value'>
+                  {typeof item.human_value !== 'undefined' ? item.human_value : <FormattedNumber value={item.value} />}
+                </td>
+              </tr>
+            ))}
+          </tbody>
+        </table>
+      );
+    }
+
+    return (
+      <div className='dimension'>
+        <h4>{label}</h4>
+
+        {content}
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/components/admin/Retention.js b/app/javascript/mastodon/components/admin/Retention.js
new file mode 100644
index 000000000..aa06722f7
--- /dev/null
+++ b/app/javascript/mastodon/components/admin/Retention.js
@@ -0,0 +1,141 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import api from 'mastodon/api';
+import { FormattedMessage, FormattedNumber, FormattedDate } from 'react-intl';
+import classNames from 'classnames';
+import { roundTo10 } from 'mastodon/utils/numbers';
+
+const dateForCohort = cohort => {
+  switch(cohort.frequency) {
+  case 'day':
+    return <FormattedDate value={cohort.period} month='long' day='2-digit' />;
+  default:
+    return <FormattedDate value={cohort.period} month='long' year='numeric' />;
+  }
+};
+
+export default class Retention extends React.PureComponent {
+
+  static propTypes = {
+    start_at: PropTypes.string,
+    end_at: PropTypes.string,
+    frequency: PropTypes.string,
+  };
+
+  state = {
+    loading: true,
+    data: null,
+  };
+
+  componentDidMount () {
+    const { start_at, end_at, frequency } = this.props;
+
+    api().post('/api/v1/admin/retention', { start_at, end_at, frequency }).then(res => {
+      this.setState({
+        loading: false,
+        data: res.data,
+      });
+    }).catch(err => {
+      console.error(err);
+    });
+  }
+
+  render () {
+    const { loading, data } = this.state;
+
+    let content;
+
+    if (loading) {
+      content = <FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' />;
+    } else {
+      content = (
+        <table className='retention__table'>
+          <thead>
+            <tr>
+              <th>
+                <div className='retention__table__date retention__table__label'>
+                  <FormattedMessage id='admin.dashboard.retention.cohort' defaultMessage='Sign-up month' />
+                </div>
+              </th>
+
+              <th>
+                <div className='retention__table__number retention__table__label'>
+                  <FormattedMessage id='admin.dashboard.retention.cohort_size' defaultMessage='New users' />
+                </div>
+              </th>
+
+              {data[0].data.slice(1).map((retention, i) => (
+                <th key={retention.date}>
+                  <div className='retention__table__number retention__table__label'>
+                    {i + 1}
+                  </div>
+                </th>
+              ))}
+            </tr>
+
+            <tr>
+              <td>
+                <div className='retention__table__date retention__table__average'>
+                  <FormattedMessage id='admin.dashboard.retention.average' defaultMessage='Average' />
+                </div>
+              </td>
+
+              <td>
+                <div className='retention__table__size'>
+                  <FormattedNumber value={data.reduce((sum, cohort, i) => sum + ((cohort.data[0].value * 1) - sum) / (i + 1), 0)} maximumFractionDigits={0} />
+                </div>
+              </td>
+
+              {data[0].data.slice(1).map((retention, i) => {
+                const average = data.reduce((sum, cohort, k) => cohort.data[i + 1] ? sum + (cohort.data[i + 1].percent - sum)/(k + 1) : sum, 0);
+
+                return (
+                  <td key={retention.date}>
+                    <div className={classNames('retention__table__box', 'retention__table__average', `retention__table__box--${roundTo10(average * 100)}`)}>
+                      <FormattedNumber value={average} style='percent' />
+                    </div>
+                  </td>
+                );
+              })}
+            </tr>
+          </thead>
+
+          <tbody>
+            {data.slice(0, -1).map(cohort => (
+              <tr key={cohort.period}>
+                <td>
+                  <div className='retention__table__date'>
+                    {dateForCohort(cohort)}
+                  </div>
+                </td>
+
+                <td>
+                  <div className='retention__table__size'>
+                    <FormattedNumber value={cohort.data[0].value} />
+                  </div>
+                </td>
+
+                {cohort.data.slice(1).map(retention => (
+                  <td key={retention.date}>
+                    <div className={classNames('retention__table__box', `retention__table__box--${roundTo10(retention.percent * 100)}`)}>
+                      <FormattedNumber value={retention.percent} style='percent' />
+                    </div>
+                  </td>
+                ))}
+              </tr>
+            ))}
+          </tbody>
+        </table>
+      );
+    }
+
+    return (
+      <div className='retention'>
+        <h4><FormattedMessage id='admin.dashboard.retention' defaultMessage='Retention' /></h4>
+
+        {content}
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/components/admin/Trends.js b/app/javascript/mastodon/components/admin/Trends.js
new file mode 100644
index 000000000..46307a28a
--- /dev/null
+++ b/app/javascript/mastodon/components/admin/Trends.js
@@ -0,0 +1,73 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import api from 'mastodon/api';
+import { FormattedMessage } from 'react-intl';
+import classNames from 'classnames';
+import Hashtag from 'mastodon/components/hashtag';
+
+export default class Trends extends React.PureComponent {
+
+  static propTypes = {
+    limit: PropTypes.number.isRequired,
+  };
+
+  state = {
+    loading: true,
+    data: null,
+  };
+
+  componentDidMount () {
+    const { limit } = this.props;
+
+    api().get('/api/v1/admin/trends', { params: { limit } }).then(res => {
+      this.setState({
+        loading: false,
+        data: res.data,
+      });
+    }).catch(err => {
+      console.error(err);
+    });
+  }
+
+  render () {
+    const { limit } = this.props;
+    const { loading, data } = this.state;
+
+    let content;
+
+    if (loading) {
+      content = (
+        <div>
+          {Array.from(Array(limit)).map((_, i) => (
+            <Hashtag key={i} />
+          ))}
+        </div>
+      );
+    } else {
+      content = (
+        <div>
+          {data.map(hashtag => (
+            <Hashtag
+              key={hashtag.name}
+              name={hashtag.name}
+              href={`/admin/tags/${hashtag.id}`}
+              people={hashtag.history[0].accounts * 1 + hashtag.history[1].accounts * 1}
+              uses={hashtag.history[0].uses * 1 + hashtag.history[1].uses * 1}
+              history={hashtag.history.reverse().map(day => day.uses)}
+              className={classNames(hashtag.requires_review && 'trends__item--requires-review', !hashtag.trendable && !hashtag.requires_review && 'trends__item--disabled')}
+            />
+          ))}
+        </div>
+      );
+    }
+
+    return (
+      <div className='trends trends--compact'>
+        <h4><FormattedMessage id='trends.trending_now' defaultMessage='Trending now' /></h4>
+
+        {content}
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/components/hashtag.js b/app/javascript/mastodon/components/hashtag.js
index 1a7edf6e0..a793a32f5 100644
--- a/app/javascript/mastodon/components/hashtag.js
+++ b/app/javascript/mastodon/components/hashtag.js
@@ -6,6 +6,8 @@ import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import Permalink from './permalink';
 import ShortNumber from 'mastodon/components/short_number';
+import Skeleton from 'mastodon/components/skeleton';
+import classNames from 'classnames';
 
 class SilentErrorBoundary extends React.Component {
 
@@ -47,45 +49,38 @@ const accountsCountRenderer = (displayNumber, pluralReady) => (
   />
 );
 
-const Hashtag = ({ hashtag }) => (
-  <div className='trends__item'>
+export const ImmutableHashtag = ({ hashtag }) => (
+  <Hashtag
+    name={hashtag.get('name')}
+    href={hashtag.get('url')}
+    to={`/tags/${hashtag.get('name')}`}
+    people={hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1}
+    uses={hashtag.getIn(['history', 0, 'uses']) * 1 + hashtag.getIn(['history', 1, 'uses']) * 1}
+    history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()}
+  />
+);
+
+ImmutableHashtag.propTypes = {
+  hashtag: ImmutablePropTypes.map.isRequired,
+};
+
+const Hashtag = ({ name, href, to, people, uses, history, className }) => (
+  <div className={classNames('trends__item', className)}>
     <div className='trends__item__name'>
-      <Permalink
-        href={hashtag.get('url')}
-        to={`/tags/${hashtag.get('name')}`}
-      >
-        #<span>{hashtag.get('name')}</span>
+      <Permalink href={href} to={to}>
+        {name ? <React.Fragment>#<span>{name}</span></React.Fragment> : <Skeleton width={50} />}
       </Permalink>
 
-      <ShortNumber
-        value={
-          hashtag.getIn(['history', 0, 'accounts']) * 1 +
-          hashtag.getIn(['history', 1, 'accounts']) * 1
-        }
-        renderer={accountsCountRenderer}
-      />
+      {typeof people !== 'undefined' ? <ShortNumber value={people} renderer={accountsCountRenderer} /> : <Skeleton width={100} />}
     </div>
 
     <div className='trends__item__current'>
-      <ShortNumber
-        value={
-          hashtag.getIn(['history', 0, 'uses']) * 1 +
-          hashtag.getIn(['history', 1, 'uses']) * 1
-        }
-      />
+      {typeof uses !== 'undefined' ? <ShortNumber value={uses} /> : <Skeleton width={42} height={36} />}
     </div>
 
     <div className='trends__item__sparkline'>
       <SilentErrorBoundary>
-        <Sparklines
-          width={50}
-          height={28}
-          data={hashtag
-            .get('history')
-            .reverse()
-            .map((day) => day.get('uses'))
-            .toArray()}
-        >
+        <Sparklines width={50} height={28} data={history ? history : Array.from(Array(7)).map(() => 0)}>
           <SparklinesCurve style={{ fill: 'none' }} />
         </Sparklines>
       </SilentErrorBoundary>
@@ -94,7 +89,13 @@ const Hashtag = ({ hashtag }) => (
 );
 
 Hashtag.propTypes = {
-  hashtag: ImmutablePropTypes.map.isRequired,
+  name: PropTypes.string,
+  href: PropTypes.string,
+  to: PropTypes.string,
+  people: PropTypes.number,
+  uses: PropTypes.number,
+  history: PropTypes.arrayOf(PropTypes.number),
+  className: PropTypes.string,
 };
 
 export default Hashtag;
diff --git a/app/javascript/mastodon/components/skeleton.js b/app/javascript/mastodon/components/skeleton.js
new file mode 100644
index 000000000..09093e99c
--- /dev/null
+++ b/app/javascript/mastodon/components/skeleton.js
@@ -0,0 +1,11 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+const Skeleton = ({ width, height }) => <span className='skeleton' style={{ width, height }}>&zwnj;</span>;
+
+Skeleton.propTypes = {
+  width: PropTypes.number,
+  height: PropTypes.number,
+};
+
+export default Skeleton;
diff --git a/app/javascript/mastodon/containers/admin_component.js b/app/javascript/mastodon/containers/admin_component.js
new file mode 100644
index 000000000..816b44bd1
--- /dev/null
+++ b/app/javascript/mastodon/containers/admin_component.js
@@ -0,0 +1,26 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { IntlProvider, addLocaleData } from 'react-intl';
+import { getLocale } from '../locales';
+
+const { localeData, messages } = getLocale();
+addLocaleData(localeData);
+
+export default class AdminComponent extends React.PureComponent {
+
+  static propTypes = {
+    locale: PropTypes.string.isRequired,
+    children: PropTypes.node.isRequired,
+  };
+
+  render () {
+    const { locale, children } = this.props;
+
+    return (
+      <IntlProvider locale={locale} messages={messages}>
+        {children}
+      </IntlProvider>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/containers/media_container.js b/app/javascript/mastodon/containers/media_container.js
index 52fdc9294..2f42a084f 100644
--- a/app/javascript/mastodon/containers/media_container.js
+++ b/app/javascript/mastodon/containers/media_container.js
@@ -7,7 +7,7 @@ import { getLocale } from 'mastodon/locales';
 import { getScrollbarWidth } from 'mastodon/utils/scrollbar';
 import MediaGallery from 'mastodon/components/media_gallery';
 import Poll from 'mastodon/components/poll';
-import Hashtag from 'mastodon/components/hashtag';
+import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag';
 import ModalRoot from 'mastodon/components/modal_root';
 import MediaModal from 'mastodon/features/ui/components/media_modal';
 import Video from 'mastodon/features/video';
diff --git a/app/javascript/mastodon/features/compose/components/search_results.js b/app/javascript/mastodon/features/compose/components/search_results.js
index 958a65286..9b3d01cfd 100644
--- a/app/javascript/mastodon/features/compose/components/search_results.js
+++ b/app/javascript/mastodon/features/compose/components/search_results.js
@@ -5,7 +5,7 @@ import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
 import AccountContainer from '../../../containers/account_container';
 import StatusContainer from '../../../containers/status_container';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import Hashtag from '../../../components/hashtag';
+import { ImmutableHashtag as Hashtag } from '../../../components/hashtag';
 import Icon from 'mastodon/components/icon';
 import { searchEnabled } from '../../../initial_state';
 import LoadMore from 'mastodon/components/load_more';
diff --git a/app/javascript/mastodon/features/getting_started/components/trends.js b/app/javascript/mastodon/features/getting_started/components/trends.js
index 3b9a3075f..71c7c458d 100644
--- a/app/javascript/mastodon/features/getting_started/components/trends.js
+++ b/app/javascript/mastodon/features/getting_started/components/trends.js
@@ -2,7 +2,7 @@ import React from 'react';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import Hashtag from 'mastodon/components/hashtag';
+import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag';
 import { FormattedMessage } from 'react-intl';
 
 export default class Trends extends ImmutablePureComponent {
diff --git a/app/javascript/mastodon/utils/numbers.js b/app/javascript/mastodon/utils/numbers.js
index 6f2505cae..6ef563ad8 100644
--- a/app/javascript/mastodon/utils/numbers.js
+++ b/app/javascript/mastodon/utils/numbers.js
@@ -69,3 +69,11 @@ export function pluralReady(sourceNumber, division) {
 
   return Math.trunc(sourceNumber / closestScale) * closestScale;
 }
+
+/**
+ * @param {number} num
+ * @returns {number}
+ */
+export function roundTo10(num) {
+  return Math.round(num * 0.1) / 0.1;
+}
diff --git a/app/javascript/packs/admin.js b/app/javascript/packs/admin.js
new file mode 100644
index 000000000..599015000
--- /dev/null
+++ b/app/javascript/packs/admin.js
@@ -0,0 +1,24 @@
+import './public-path';
+import ready from '../mastodon/ready';
+
+ready(() => {
+  const React    = require('react');
+  const ReactDOM = require('react-dom');
+
+  [].forEach.call(document.querySelectorAll('[data-admin-component]'), element => {
+    const componentName  = element.getAttribute('data-admin-component');
+    const { locale, ...componentProps } = JSON.parse(element.getAttribute('data-props'));
+
+    import('../mastodon/containers/admin_component').then(({ default: AdminComponent }) => {
+      return import('../mastodon/components/admin/' + componentName).then(({ default: Component }) => {
+        ReactDOM.render((
+          <AdminComponent locale={locale}>
+            <Component {...componentProps} />
+          </AdminComponent>
+        ), element);
+      });
+    }).catch(error => {
+      console.error(error);
+    });
+  });
+});
diff --git a/app/javascript/styles/fonts/montserrat.scss b/app/javascript/styles/fonts/montserrat.scss
index 80c2329b0..2abcb0c2b 100644
--- a/app/javascript/styles/fonts/montserrat.scss
+++ b/app/javascript/styles/fonts/montserrat.scss
@@ -5,6 +5,7 @@
     url('~fonts/montserrat/Montserrat-Regular.woff') format('woff'),
     url('~fonts/montserrat/Montserrat-Regular.ttf') format('truetype');
   font-weight: 400;
+  font-display: swap;
   font-style: normal;
 }
 
@@ -13,5 +14,6 @@
   src: local('Montserrat Medium'),
     url('~fonts/montserrat/Montserrat-Medium.ttf') format('truetype');
   font-weight: 500;
+  font-display: swap;
   font-style: normal;
 }
diff --git a/app/javascript/styles/fonts/roboto-mono.scss b/app/javascript/styles/fonts/roboto-mono.scss
index c793aa6ed..a9513dcce 100644
--- a/app/javascript/styles/fonts/roboto-mono.scss
+++ b/app/javascript/styles/fonts/roboto-mono.scss
@@ -6,5 +6,6 @@
     url('~fonts/roboto-mono/robotomono-regular-webfont.ttf') format('truetype'),
     url('~fonts/roboto-mono/robotomono-regular-webfont.svg#roboto_monoregular') format('svg');
   font-weight: 400;
+  font-display: swap;
   font-style: normal;
 }
diff --git a/app/javascript/styles/fonts/roboto.scss b/app/javascript/styles/fonts/roboto.scss
index b75fdb927..817b448c1 100644
--- a/app/javascript/styles/fonts/roboto.scss
+++ b/app/javascript/styles/fonts/roboto.scss
@@ -6,6 +6,7 @@
     url('~fonts/roboto/roboto-italic-webfont.ttf') format('truetype'),
     url('~fonts/roboto/roboto-italic-webfont.svg#roboto-italic-webfont') format('svg');
   font-weight: normal;
+  font-display: swap;
   font-style: italic;
 }
 
@@ -17,6 +18,7 @@
     url('~fonts/roboto/roboto-bold-webfont.ttf') format('truetype'),
     url('~fonts/roboto/roboto-bold-webfont.svg#roboto-bold-webfont') format('svg');
   font-weight: bold;
+  font-display: swap;
   font-style: normal;
 }
 
@@ -28,6 +30,7 @@
     url('~fonts/roboto/roboto-medium-webfont.ttf') format('truetype'),
     url('~fonts/roboto/roboto-medium-webfont.svg#roboto-medium-webfont') format('svg');
   font-weight: 500;
+  font-display: swap;
   font-style: normal;
 }
 
@@ -39,5 +42,6 @@
     url('~fonts/roboto/roboto-regular-webfont.ttf') format('truetype'),
     url('~fonts/roboto/roboto-regular-webfont.svg#roboto-regular-webfont') format('svg');
   font-weight: normal;
+  font-display: swap;
   font-style: normal;
 }
diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss
index 4801a4644..24618c29f 100644
--- a/app/javascript/styles/mastodon/admin.scss
+++ b/app/javascript/styles/mastodon/admin.scss
@@ -1,3 +1,5 @@
+@use "sass:math";
+
 $no-columns-breakpoint: 600px;
 $sidebar-width: 240px;
 $content-width: 840px;
@@ -925,10 +927,197 @@ a.name-tag,
   }
 }
 
+.dashboard__counters.admin-account-counters {
+  margin-top: 10px;
+}
+
 .account-badges {
   margin: -2px 0;
 }
 
-.dashboard__counters.admin-account-counters {
-  margin-top: 10px;
+.retention {
+  &__table {
+    &__number {
+      color: $secondary-text-color;
+      padding: 10px;
+    }
+
+    &__date {
+      white-space: nowrap;
+      padding: 10px 0;
+      text-align: left;
+      min-width: 120px;
+
+      &.retention__table__average {
+        font-weight: 700;
+      }
+    }
+
+    &__size {
+      text-align: center;
+      padding: 10px;
+    }
+
+    &__label {
+      font-weight: 700;
+      color: $darker-text-color;
+    }
+
+    &__box {
+      box-sizing: border-box;
+      background: $ui-highlight-color;
+      padding: 10px;
+      font-weight: 500;
+      color: $primary-text-color;
+      width: 52px;
+      margin: 1px;
+
+      @for $i from 0 through 10 {
+        &--#{10 * $i} {
+          background-color: rgba($ui-highlight-color, 1 * (math.div(max(1, $i), 10)));
+        }
+      }
+    }
+  }
+}
+
+.sparkline {
+  display: block;
+  text-decoration: none;
+  background: lighten($ui-base-color, 4%);
+  border-radius: 4px;
+  padding: 0;
+  position: relative;
+  padding-bottom: 55px + 20px;
+  overflow: hidden;
+
+  &__value {
+    display: flex;
+    line-height: 33px;
+    align-items: flex-end;
+    padding: 20px;
+    padding-bottom: 10px;
+
+    &__total {
+      display: block;
+      margin-right: 10px;
+      font-weight: 500;
+      font-size: 28px;
+      color: $primary-text-color;
+    }
+
+    &__change {
+      display: block;
+      font-weight: 500;
+      font-size: 18px;
+      color: $darker-text-color;
+      margin-bottom: -3px;
+
+      &.positive {
+        color: $valid-value-color;
+      }
+
+      &.negative {
+        color: $error-value-color;
+      }
+    }
+  }
+
+  &__label {
+    padding: 0 20px;
+    padding-bottom: 10px;
+    text-transform: uppercase;
+    color: $darker-text-color;
+    font-weight: 500;
+  }
+
+  &__graph {
+    position: absolute;
+    bottom: 0;
+
+    svg {
+      display: block;
+      margin: 0;
+    }
+
+    path:first-child {
+      fill: rgba($highlight-text-color, 0.25) !important;
+      fill-opacity: 1 !important;
+    }
+
+    path:last-child {
+      stroke: lighten($highlight-text-color, 6%) !important;
+      fill: none !important;
+    }
+  }
+}
+
+a.sparkline {
+  &:hover,
+  &:focus,
+  &:active {
+    background: lighten($ui-base-color, 6%);
+  }
+}
+
+.skeleton {
+  background-color: lighten($ui-base-color, 8%);
+  background-image: linear-gradient(90deg, lighten($ui-base-color, 8%), lighten($ui-base-color, 12%), lighten($ui-base-color, 8%));
+  background-size: 200px 100%;
+  background-repeat: no-repeat;
+  border-radius: 4px;
+  display: inline-block;
+  line-height: 1;
+  width: 100%;
+  animation: skeleton 1.2s ease-in-out infinite;
+}
+
+@keyframes skeleton {
+  0% {
+    background-position: -200px 0;
+  }
+
+  100% {
+    background-position: calc(200px + 100%) 0;
+  }
+}
+
+.dimension {
+  table {
+    width: 100%;
+  }
+
+  &__item {
+    border-bottom: 1px solid lighten($ui-base-color, 4%);
+
+    &__key {
+      font-weight: 500;
+      padding: 11px 10px;
+    }
+
+    &__value {
+      text-align: right;
+      color: $darker-text-color;
+      padding: 11px 10px;
+    }
+
+    &__indicator {
+      display: inline-block;
+      width: 8px;
+      height: 8px;
+      border-radius: 50%;
+      background: $ui-highlight-color;
+      margin-right: 10px;
+
+      @for $i from 0 through 10 {
+        &--#{10 * $i} {
+          background-color: rgba($ui-highlight-color, 1 * (math.div(max(1, $i), 10)));
+        }
+      }
+    }
+
+    &:last-child {
+      border-bottom: 0;
+    }
+  }
 }
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 12bc472f5..c239d4c78 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -6955,7 +6955,6 @@ noscript {
     &__current {
       flex: 0 0 auto;
       font-size: 24px;
-      line-height: 36px;
       font-weight: 500;
       text-align: right;
       padding-right: 15px;
@@ -6977,6 +6976,58 @@ noscript {
         fill: none !important;
       }
     }
+
+    &--requires-review {
+      .trends__item__name {
+        color: $gold-star;
+
+        a {
+          color: $gold-star;
+        }
+      }
+
+      .trends__item__current {
+        color: $gold-star;
+      }
+
+      .trends__item__sparkline {
+        path:first-child {
+          fill: rgba($gold-star, 0.25) !important;
+        }
+
+        path:last-child {
+          stroke: lighten($gold-star, 6%) !important;
+        }
+      }
+    }
+
+    &--disabled {
+      .trends__item__name {
+        color: lighten($ui-base-color, 12%);
+
+        a {
+          color: lighten($ui-base-color, 12%);
+        }
+      }
+
+      .trends__item__current {
+        color: lighten($ui-base-color, 12%);
+      }
+
+      .trends__item__sparkline {
+        path:first-child {
+          fill: rgba(lighten($ui-base-color, 12%), 0.25) !important;
+        }
+
+        path:last-child {
+          stroke: lighten(lighten($ui-base-color, 12%), 6%) !important;
+        }
+      }
+    }
+  }
+
+  &--compact &__item {
+    padding: 10px;
   }
 }
 
diff --git a/app/javascript/styles/mastodon/dashboard.scss b/app/javascript/styles/mastodon/dashboard.scss
index c0944d417..cad5a105b 100644
--- a/app/javascript/styles/mastodon/dashboard.scss
+++ b/app/javascript/styles/mastodon/dashboard.scss
@@ -56,23 +56,56 @@
   }
 }
 
-.dashboard__widgets {
-  display: flex;
-  flex-wrap: wrap;
-  margin: 0 -5px;
+.dashboard {
+  display: grid;
+  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr);
+  grid-gap: 10px;
 
-  & > div {
-    flex: 0 0 33.333%;
-    margin-bottom: 20px;
+  &__item {
+    &--span-double-column {
+      grid-column: span 2;
+    }
 
-    & > div {
-      padding: 0 5px;
+    &--span-double-row {
+      grid-row: span 2;
+    }
+
+    h4 {
+      padding-top: 20px;
     }
   }
 
-  a:not(.name-tag) {
-    color: $ui-secondary-color;
-    font-weight: 500;
+  &__quick-access {
+    display: flex;
+    align-items: baseline;
+    border-radius: 4px;
+    background: $ui-highlight-color;
+    color: $primary-text-color;
+    transition: all 100ms ease-in;
+    font-size: 14px;
+    padding: 0 16px;
+    line-height: 36px;
+    height: 36px;
     text-decoration: none;
+    margin-bottom: 4px;
+
+    &:active,
+    &:focus,
+    &:hover {
+      background-color: lighten($ui-highlight-color, 10%);
+      transition: all 200ms ease-out;
+    }
+
+    span {
+      flex: 1 1 auto;
+    }
+
+    .fa {
+      flex: 0 0 auto;
+    }
+
+    strong {
+      font-weight: 700;
+    }
   }
 }
diff --git a/app/lib/activity_tracker.rb b/app/lib/activity_tracker.rb
index 81303b715..6d3401b37 100644
--- a/app/lib/activity_tracker.rb
+++ b/app/lib/activity_tracker.rb
@@ -1,29 +1,73 @@
 # frozen_string_literal: true
 
 class ActivityTracker
+  include Redisable
+
   EXPIRE_AFTER = 6.months.seconds
 
-  class << self
-    include Redisable
+  def initialize(prefix, type)
+    @prefix = prefix
+    @type   = type
+  end
 
-    def increment(prefix)
-      key = [prefix, current_week].join(':')
+  def add(value = 1, at_time = Time.now.utc)
+    key = key_at(at_time)
 
-      redis.incrby(key, 1)
-      redis.expire(key, EXPIRE_AFTER)
+    case @type
+    when :basic
+      redis.incrby(key, value)
+    when :unique
+      redis.pfadd(key, value)
     end
 
-    def record(prefix, value)
-      key = [prefix, current_week].join(':')
+    redis.expire(key, EXPIRE_AFTER)
+  end
 
-      redis.pfadd(key, value)
-      redis.expire(key, EXPIRE_AFTER)
+  def get(start_at, end_at = Time.now.utc)
+    (start_at.to_date...end_at.to_date).map do |date|
+      key = key_at(date.to_time(:utc))
+
+      value = begin
+        case @type
+        when :basic
+          redis.get(key).to_i
+        when :unique
+          redis.pfcount(key)
+        end
+      end
+
+      [date, value]
+    end
+  end
+
+  def sum(start_at, end_at = Time.now.utc)
+    keys = (start_at.to_date...end_at.to_date).flat_map { |date| [key_at(date.to_time(:utc)), legacy_key_at(date)] }.uniq
+
+    case @type
+    when :basic
+      redis.mget(*keys).map(&:to_i).sum
+    when :unique
+      redis.pfcount(*keys)
     end
+  end
 
-    private
+  class << self
+    def increment(prefix)
+      new(prefix, :basic).add
+    end
 
-    def current_week
-      Time.zone.today.cweek
+    def record(prefix, value)
+      new(prefix, :unique).add(value)
     end
   end
+
+  private
+
+  def key_at(at_time)
+    "#{@prefix}:#{at_time.beginning_of_day.to_i}"
+  end
+
+  def legacy_key_at(at_time)
+    "#{@prefix}:#{at_time.to_date.cweek}"
+  end
 end
diff --git a/app/lib/admin/metrics/dimension.rb b/app/lib/admin/metrics/dimension.rb
new file mode 100644
index 000000000..279539f68
--- /dev/null
+++ b/app/lib/admin/metrics/dimension.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class Admin::Metrics::Dimension
+  DIMENSIONS = {
+    languages: Admin::Metrics::Dimension::LanguagesDimension,
+    sources: Admin::Metrics::Dimension::SourcesDimension,
+    servers: Admin::Metrics::Dimension::ServersDimension,
+    space_usage: Admin::Metrics::Dimension::SpaceUsageDimension,
+    software_versions: Admin::Metrics::Dimension::SoftwareVersionsDimension,
+  }.freeze
+
+  def self.retrieve(dimension_keys, start_at, end_at, limit)
+    Array(dimension_keys).map { |key| DIMENSIONS[key.to_sym]&.new(start_at, end_at, limit) }.compact
+  end
+end
diff --git a/app/lib/admin/metrics/dimension/base_dimension.rb b/app/lib/admin/metrics/dimension/base_dimension.rb
new file mode 100644
index 000000000..8ed8d7683
--- /dev/null
+++ b/app/lib/admin/metrics/dimension/base_dimension.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class Admin::Metrics::Dimension::BaseDimension
+  def initialize(start_at, end_at, limit)
+    @start_at = start_at&.to_datetime
+    @end_at   = end_at&.to_datetime
+    @limit    = limit&.to_i
+  end
+
+  def key
+    raise NotImplementedError
+  end
+
+  def data
+    raise NotImplementedError
+  end
+
+  def self.model_name
+    self.class.name
+  end
+
+  def read_attribute_for_serialization(key)
+    send(key) if respond_to?(key)
+  end
+
+  protected
+
+  def time_period
+    (@start_at...@end_at)
+  end
+end
diff --git a/app/lib/admin/metrics/dimension/languages_dimension.rb b/app/lib/admin/metrics/dimension/languages_dimension.rb
new file mode 100644
index 000000000..2d0ac124e
--- /dev/null
+++ b/app/lib/admin/metrics/dimension/languages_dimension.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class Admin::Metrics::Dimension::LanguagesDimension < Admin::Metrics::Dimension::BaseDimension
+  def key
+    'languages'
+  end
+
+  def data
+    sql = <<-SQL.squish
+      SELECT locale, count(*) AS value
+      FROM users
+      WHERE current_sign_in_at BETWEEN $1 AND $2
+        AND locale IS NOT NULL
+      GROUP BY locale
+      ORDER BY count(*) DESC
+      LIMIT $3
+    SQL
+
+    rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, @start_at], [nil, @end_at], [nil, @limit]])
+
+    rows.map { |row| { key: row['locale'], human_key: SettingsHelper::HUMAN_LOCALES[row['locale'].to_sym], value: row['value'].to_s } }
+  end
+end
diff --git a/app/lib/admin/metrics/dimension/servers_dimension.rb b/app/lib/admin/metrics/dimension/servers_dimension.rb
new file mode 100644
index 000000000..3e80b6625
--- /dev/null
+++ b/app/lib/admin/metrics/dimension/servers_dimension.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class Admin::Metrics::Dimension::ServersDimension < Admin::Metrics::Dimension::BaseDimension
+  def key
+    'servers'
+  end
+
+  def data
+    sql = <<-SQL.squish
+      SELECT accounts.domain, count(*) AS value
+      FROM statuses
+      INNER JOIN accounts ON accounts.id = statuses.account_id
+      WHERE statuses.id BETWEEN $1 AND $2
+      GROUP BY accounts.domain
+      ORDER BY count(*) DESC
+      LIMIT $3
+    SQL
+
+    rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, Mastodon::Snowflake.id_at(@start_at)], [nil, Mastodon::Snowflake.id_at(@end_at)], [nil, @limit]])
+
+    rows.map { |row| { key: row['domain'] || Rails.configuration.x.local_domain, human_key: row['domain'] || Rails.configuration.x.local_domain, value: row['value'].to_s } }
+  end
+end
diff --git a/app/lib/admin/metrics/dimension/software_versions_dimension.rb b/app/lib/admin/metrics/dimension/software_versions_dimension.rb
new file mode 100644
index 000000000..34917404d
--- /dev/null
+++ b/app/lib/admin/metrics/dimension/software_versions_dimension.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+class Admin::Metrics::Dimension::SoftwareVersionsDimension < Admin::Metrics::Dimension::BaseDimension
+  include Redisable
+
+  def key
+    'software_versions'
+  end
+
+  def data
+    [mastodon_version, ruby_version, postgresql_version, redis_version]
+  end
+
+  private
+
+  def mastodon_version
+    value = Mastodon::Version.to_s
+
+    {
+      key: 'mastodon',
+      human_key: 'Mastodon',
+      value: value,
+      human_value: value,
+    }
+  end
+
+  def ruby_version
+    value = "#{RUBY_VERSION}p#{RUBY_PATCHLEVEL}"
+
+    {
+      key: 'ruby',
+      human_key: 'Ruby',
+      value: value,
+      human_value: value,
+    }
+  end
+
+  def postgresql_version
+    value = ActiveRecord::Base.connection.execute('SELECT VERSION()').first['version'].match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1]
+
+    {
+      key: 'postgresql',
+      human_key: 'PostgreSQL',
+      value: value,
+      human_value: value,
+    }
+  end
+
+  def redis_version
+    value = redis_info['redis_version']
+
+    {
+      key: 'redis',
+      human_key: 'Redis',
+      value: value,
+      human_value: value,
+    }
+  end
+
+  def redis_info
+    @redis_info ||= begin
+      if redis.is_a?(Redis::Namespace)
+        redis.redis.info
+      else
+        redis.info
+      end
+    end
+  end
+end
diff --git a/app/lib/admin/metrics/dimension/sources_dimension.rb b/app/lib/admin/metrics/dimension/sources_dimension.rb
new file mode 100644
index 000000000..a9f061809
--- /dev/null
+++ b/app/lib/admin/metrics/dimension/sources_dimension.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class Admin::Metrics::Dimension::SourcesDimension < Admin::Metrics::Dimension::BaseDimension
+  def key
+    'sources'
+  end
+
+  def data
+    sql = <<-SQL.squish
+      SELECT oauth_applications.name, count(*) AS value
+      FROM users
+      LEFT JOIN oauth_applications ON oauth_applications.id = users.created_by_application_id
+      WHERE users.created_at BETWEEN $1 AND $2
+      GROUP BY oauth_applications.name
+      ORDER BY count(*) DESC
+      LIMIT $3
+    SQL
+
+    rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, @start_at], [nil, @end_at], [nil, @limit]])
+
+    rows.map { |row| { key: row['name'] || 'web', human_key: row['name'] || I18n.t('admin.dashboard.website'), value: row['value'].to_s } }
+  end
+end
diff --git a/app/lib/admin/metrics/dimension/space_usage_dimension.rb b/app/lib/admin/metrics/dimension/space_usage_dimension.rb
new file mode 100644
index 000000000..aa00a2e18
--- /dev/null
+++ b/app/lib/admin/metrics/dimension/space_usage_dimension.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+class Admin::Metrics::Dimension::SpaceUsageDimension < Admin::Metrics::Dimension::BaseDimension
+  include Redisable
+  include ActionView::Helpers::NumberHelper
+
+  def key
+    'space_usage'
+  end
+
+  def data
+    [postgresql_size, redis_size, media_size]
+  end
+
+  private
+
+  def postgresql_size
+    value = ActiveRecord::Base.connection.execute('SELECT pg_database_size(current_database())').first['pg_database_size']
+
+    {
+      key: 'postgresql',
+      human_key: 'PostgreSQL',
+      value: value.to_s,
+      unit: 'bytes',
+      human_value: number_to_human_size(value),
+    }
+  end
+
+  def redis_size
+    value = redis_info['used_memory']
+
+    {
+      key: 'redis',
+      human_key: 'Redis',
+      value: value.to_s,
+      unit: 'bytes',
+      human_value: number_to_human_size(value),
+    }
+  end
+
+  def media_size
+    value = [
+      MediaAttachment.sum(Arel.sql('COALESCE(file_file_size, 0) + COALESCE(thumbnail_file_size, 0)')),
+      CustomEmoji.sum(:image_file_size),
+      PreviewCard.sum(:image_file_size),
+      Account.sum(Arel.sql('COALESCE(avatar_file_size, 0) + COALESCE(header_file_size, 0)')),
+      Backup.sum(:dump_file_size),
+      Import.sum(:data_file_size),
+      SiteUpload.sum(:file_file_size),
+    ].sum
+
+    {
+      key: 'media',
+      human_key: I18n.t('admin.dashboard.media_storage'),
+      value: value.to_s,
+      unit: 'bytes',
+      human_value: number_to_human_size(value),
+    }
+  end
+
+  def redis_info
+    @redis_info ||= begin
+      if redis.is_a?(Redis::Namespace)
+        redis.redis.info
+      else
+        redis.info
+      end
+    end
+  end
+end
diff --git a/app/lib/admin/metrics/measure.rb b/app/lib/admin/metrics/measure.rb
new file mode 100644
index 000000000..5cebf0331
--- /dev/null
+++ b/app/lib/admin/metrics/measure.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class Admin::Metrics::Measure
+  MEASURES = {
+    active_users: Admin::Metrics::Measure::ActiveUsersMeasure,
+    new_users: Admin::Metrics::Measure::NewUsersMeasure,
+    interactions: Admin::Metrics::Measure::InteractionsMeasure,
+    opened_reports: Admin::Metrics::Measure::OpenedReportsMeasure,
+    resolved_reports: Admin::Metrics::Measure::ResolvedReportsMeasure,
+  }.freeze
+
+  def self.retrieve(measure_keys, start_at, end_at)
+    Array(measure_keys).map { |key| MEASURES[key.to_sym]&.new(start_at, end_at) }.compact
+  end
+end
diff --git a/app/lib/admin/metrics/measure/active_users_measure.rb b/app/lib/admin/metrics/measure/active_users_measure.rb
new file mode 100644
index 000000000..ac022eb9d
--- /dev/null
+++ b/app/lib/admin/metrics/measure/active_users_measure.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+class Admin::Metrics::Measure::ActiveUsersMeasure < Admin::Metrics::Measure::BaseMeasure
+  def key
+    'active_users'
+  end
+
+  def total
+    activity_tracker.sum(time_period.first, time_period.last)
+  end
+
+  def previous_total
+    activity_tracker.sum(previous_time_period.first, previous_time_period.last)
+  end
+
+  def data
+    activity_tracker.get(time_period.first, time_period.last).map { |date, value| { date: date.to_time(:utc).iso8601, value: value.to_s } }
+  end
+
+  protected
+
+  def activity_tracker
+    @activity_tracker ||= ActivityTracker.new('activity:logins', :unique)
+  end
+
+  def time_period
+    (@start_at.to_date...@end_at.to_date)
+  end
+
+  def previous_time_period
+    ((@start_at.to_date - length_of_period)...(@end_at.to_date - length_of_period))
+  end
+end
diff --git a/app/lib/admin/metrics/measure/base_measure.rb b/app/lib/admin/metrics/measure/base_measure.rb
new file mode 100644
index 000000000..4c336a69e
--- /dev/null
+++ b/app/lib/admin/metrics/measure/base_measure.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+class Admin::Metrics::Measure::BaseMeasure
+  def initialize(start_at, end_at)
+    @start_at = start_at&.to_datetime
+    @end_at   = end_at&.to_datetime
+  end
+
+  def key
+    raise NotImplementedError
+  end
+
+  def total
+    raise NotImplementedError
+  end
+
+  def previous_total
+    raise NotImplementedError
+  end
+
+  def data
+    raise NotImplementedError
+  end
+
+  def self.model_name
+    self.class.name
+  end
+
+  def read_attribute_for_serialization(key)
+    send(key) if respond_to?(key)
+  end
+
+  protected
+
+  def time_period
+    (@start_at...@end_at)
+  end
+
+  def previous_time_period
+    ((@start_at - length_of_period)...(@end_at - length_of_period))
+  end
+
+  def length_of_period
+    @length_of_period ||= @end_at - @start_at
+  end
+end
diff --git a/app/lib/admin/metrics/measure/interactions_measure.rb b/app/lib/admin/metrics/measure/interactions_measure.rb
new file mode 100644
index 000000000..9a4ef6d63
--- /dev/null
+++ b/app/lib/admin/metrics/measure/interactions_measure.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+class Admin::Metrics::Measure::InteractionsMeasure < Admin::Metrics::Measure::BaseMeasure
+  def key
+    'interactions'
+  end
+
+  def total
+    activity_tracker.sum(time_period.first, time_period.last)
+  end
+
+  def previous_total
+    activity_tracker.sum(previous_time_period.first, previous_time_period.last)
+  end
+
+  def data
+    activity_tracker.get(time_period.first, time_period.last).map { |date, value| { date: date.to_time(:utc).iso8601, value: value.to_s } }
+  end
+
+  protected
+
+  def activity_tracker
+    @activity_tracker ||= ActivityTracker.new('activity:interactions', :basic)
+  end
+
+  def time_period
+    (@start_at.to_date...@end_at.to_date)
+  end
+
+  def previous_time_period
+    ((@start_at.to_date - length_of_period)...(@end_at.to_date - length_of_period))
+  end
+end
diff --git a/app/lib/admin/metrics/measure/new_users_measure.rb b/app/lib/admin/metrics/measure/new_users_measure.rb
new file mode 100644
index 000000000..b31679ad3
--- /dev/null
+++ b/app/lib/admin/metrics/measure/new_users_measure.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+class Admin::Metrics::Measure::NewUsersMeasure < Admin::Metrics::Measure::BaseMeasure
+  def key
+    'new_users'
+  end
+
+  def total
+    User.where(created_at: time_period).count
+  end
+
+  def previous_total
+    User.where(created_at: previous_time_period).count
+  end
+
+  def data
+    sql = <<-SQL.squish
+      SELECT axis.*, (
+        WITH new_users AS (
+          SELECT users.id
+          FROM users
+          WHERE date_trunc('day', users.created_at)::date = axis.period
+        )
+        SELECT count(*) FROM new_users
+      ) AS value
+      FROM (
+        SELECT generate_series(date_trunc('day', $1::timestamp)::date, date_trunc('day', $2::timestamp)::date, interval '1 day') AS period
+      ) AS axis
+    SQL
+
+    rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, @start_at], [nil, @end_at]])
+
+    rows.map { |row| { date: row['period'], value: row['value'].to_s } }
+  end
+end
diff --git a/app/lib/admin/metrics/measure/opened_reports_measure.rb b/app/lib/admin/metrics/measure/opened_reports_measure.rb
new file mode 100644
index 000000000..9acc2c33d
--- /dev/null
+++ b/app/lib/admin/metrics/measure/opened_reports_measure.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+class Admin::Metrics::Measure::OpenedReportsMeasure < Admin::Metrics::Measure::BaseMeasure
+  def key
+    'opened_reports'
+  end
+
+  def total
+    Report.where(created_at: time_period).count
+  end
+
+  def previous_total
+    Report.where(created_at: previous_time_period).count
+  end
+
+  def data
+    sql = <<-SQL.squish
+      SELECT axis.*, (
+        WITH new_reports AS (
+          SELECT reports.id
+          FROM reports
+          WHERE date_trunc('day', reports.created_at)::date = axis.period
+        )
+        SELECT count(*) FROM new_reports
+      ) AS value
+      FROM (
+        SELECT generate_series(date_trunc('day', $1::timestamp)::date, date_trunc('day', $2::timestamp)::date, interval '1 day') AS period
+      ) AS axis
+    SQL
+
+    rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, @start_at], [nil, @end_at]])
+
+    rows.map { |row| { date: row['period'], value: row['value'].to_s } }
+  end
+end
diff --git a/app/lib/admin/metrics/measure/resolved_reports_measure.rb b/app/lib/admin/metrics/measure/resolved_reports_measure.rb
new file mode 100644
index 000000000..0dcecbbad
--- /dev/null
+++ b/app/lib/admin/metrics/measure/resolved_reports_measure.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+class Admin::Metrics::Measure::ResolvedReportsMeasure < Admin::Metrics::Measure::BaseMeasure
+  def key
+    'resolved_reports'
+  end
+
+  def total
+    Report.resolved.where(updated_at: time_period).count
+  end
+
+  def previous_total
+    Report.resolved.where(updated_at: previous_time_period).count
+  end
+
+  def data
+    sql = <<-SQL.squish
+      SELECT axis.*, (
+        WITH resolved_reports AS (
+          SELECT reports.id
+          FROM reports
+          WHERE action_taken
+            AND date_trunc('day', reports.updated_at)::date = axis.period
+        )
+        SELECT count(*) FROM resolved_reports
+      ) AS value
+      FROM (
+        SELECT generate_series(date_trunc('day', $1::timestamp)::date, date_trunc('day', $2::timestamp)::date, interval '1 day') AS period
+      ) AS axis
+    SQL
+
+    rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, @start_at], [nil, @end_at]])
+
+    rows.map { |row| { date: row['period'], value: row['value'].to_s } }
+  end
+end
diff --git a/app/lib/admin/metrics/retention.rb b/app/lib/admin/metrics/retention.rb
new file mode 100644
index 000000000..6b9dfde49
--- /dev/null
+++ b/app/lib/admin/metrics/retention.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+class Admin::Metrics::Retention
+  class Cohort < ActiveModelSerializers::Model
+    attributes :period, :frequency, :data
+  end
+
+  class CohortData < ActiveModelSerializers::Model
+    attributes :date, :percent, :value
+  end
+
+  def initialize(start_at, end_at, frequency)
+    @start_at  = start_at&.to_date
+    @end_at    = end_at&.to_date
+    @frequency = %w(day month).include?(frequency) ? frequency : 'day'
+  end
+
+  def cohorts
+    sql = <<-SQL.squish
+      SELECT axis.*, (
+        WITH new_users AS (
+          SELECT users.id
+          FROM users
+          WHERE date_trunc($3, users.created_at)::date = axis.cohort_period
+        ),
+        retained_users AS (
+          SELECT users.id
+          FROM users
+          INNER JOIN new_users on new_users.id = users.id
+          WHERE date_trunc($3, users.current_sign_in_at) >= axis.retention_period
+        )
+        SELECT ARRAY[count(*), (count(*))::float / (SELECT GREATEST(count(*), 1) FROM new_users)] AS retention_value_and_rate
+        FROM retained_users
+      )
+      FROM (
+        WITH cohort_periods AS (
+          SELECT generate_series(date_trunc($3, $1::timestamp)::date, date_trunc($3, $2::timestamp)::date, ('1 ' || $3)::interval) AS cohort_period
+        ),
+        retention_periods AS (
+          SELECT cohort_period AS retention_period FROM cohort_periods
+        )
+        SELECT *
+        FROM cohort_periods, retention_periods
+        WHERE retention_period >= cohort_period
+      ) as axis
+    SQL
+
+    rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, @start_at], [nil, @end_at], [nil, @frequency]])
+
+    rows.each_with_object([]) do |row, arr|
+      current_cohort = arr.last
+
+      if current_cohort.nil? || current_cohort.period != row['cohort_period']
+        current_cohort = Cohort.new(period: row['cohort_period'], frequency: @frequency, data: [])
+        arr << current_cohort
+      end
+
+      value, rate = row['retention_value_and_rate'].delete('{}').split(',')
+
+      current_cohort.data << CohortData.new(
+        date: row['retention_period'],
+        percent: rate.to_f,
+        value: value.to_s
+      )
+    end
+  end
+end
diff --git a/app/models/account_statuses_cleanup_policy.rb b/app/models/account_statuses_cleanup_policy.rb
index 705ccff54..0a9551ec2 100644
--- a/app/models/account_statuses_cleanup_policy.rb
+++ b/app/models/account_statuses_cleanup_policy.rb
@@ -164,8 +164,8 @@ class AccountStatusesCleanupPolicy < ApplicationRecord
 
   def without_popular_scope
     scope = Status.left_joins(:status_stat)
-    scope = scope.where('COALESCE(status_stats.reblogs_count, 0) <= ?', min_reblogs) unless min_reblogs.nil?
-    scope = scope.where('COALESCE(status_stats.favourites_count, 0) <= ?', min_favs) unless min_favs.nil?
+    scope = scope.where('COALESCE(status_stats.reblogs_count, 0) < ?', min_reblogs) unless min_reblogs.nil?
+    scope = scope.where('COALESCE(status_stats.favourites_count, 0) < ?', min_favs) unless min_favs.nil?
     scope
   end
 end
diff --git a/app/models/admin/action_log_filter.rb b/app/models/admin/action_log_filter.rb
index a1c156a8b..6e19dcf70 100644
--- a/app/models/admin/action_log_filter.rb
+++ b/app/models/admin/action_log_filter.rb
@@ -76,7 +76,7 @@ class Admin::ActionLogFilter
     when 'account_id'
       Admin::ActionLog.where(account_id: value)
     when 'target_account_id'
-      account = Account.find(value)
+      account = Account.find_or_initialize_by(id: value)
       Admin::ActionLog.where(target: [account, account.user].compact)
     else
       raise "Unknown filter: #{key}"
diff --git a/app/models/status.rb b/app/models/status.rb
index 9f673ee53..7b11709fa 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -494,7 +494,7 @@ class Status < ApplicationRecord
   end
 
   def decrement_counter_caches
-    return if direct_visibility?
+    return if direct_visibility? || new_record?
 
     account&.decrement_count!(:statuses_count)
     reblog&.decrement_count!(:reblogs_count) if reblog?
diff --git a/app/presenters/instance_presenter.rb b/app/presenters/instance_presenter.rb
index 345a5e5e9..a0f1ebd0a 100644
--- a/app/presenters/instance_presenter.rb
+++ b/app/presenters/instance_presenter.rb
@@ -24,8 +24,8 @@ class InstancePresenter
     Rails.cache.fetch('user_count') { User.confirmed.joins(:account).merge(Account.without_suspended).count }
   end
 
-  def active_user_count(weeks = 4)
-    Rails.cache.fetch("active_user_count/#{weeks}") { Redis.current.pfcount(*(0...weeks).map { |i| "activity:logins:#{i.weeks.ago.utc.to_date.cweek}" }) }
+  def active_user_count(num_weeks = 4)
+    Rails.cache.fetch("active_user_count/#{num_weeks}") { ActivityTracker.new('activity:logins', :unique).sum(num_weeks.weeks.ago) }
   end
 
   def status_count
diff --git a/app/serializers/rest/admin/cohort_serializer.rb b/app/serializers/rest/admin/cohort_serializer.rb
new file mode 100644
index 000000000..56b35c699
--- /dev/null
+++ b/app/serializers/rest/admin/cohort_serializer.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class REST::Admin::CohortSerializer < ActiveModel::Serializer
+  attributes :period, :frequency
+
+  class CohortDataSerializer < ActiveModel::Serializer
+    attributes :date, :percent, :value
+
+    def date
+      object.date.iso8601
+    end
+  end
+
+  has_many :data, serializer: CohortDataSerializer
+
+  def period
+    object.period.iso8601
+  end
+end
diff --git a/app/serializers/rest/admin/dimension_serializer.rb b/app/serializers/rest/admin/dimension_serializer.rb
new file mode 100644
index 000000000..a00b6ecd7
--- /dev/null
+++ b/app/serializers/rest/admin/dimension_serializer.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class REST::Admin::DimensionSerializer < ActiveModel::Serializer
+  attributes :key, :data
+end
diff --git a/app/serializers/rest/admin/measure_serializer.rb b/app/serializers/rest/admin/measure_serializer.rb
new file mode 100644
index 000000000..81d655c1a
--- /dev/null
+++ b/app/serializers/rest/admin/measure_serializer.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class REST::Admin::MeasureSerializer < ActiveModel::Serializer
+  attributes :key, :total, :previous_total, :data
+
+  def total
+    object.total.to_s
+  end
+
+  def previous_total
+    object.previous_total.to_s
+  end
+end
diff --git a/app/serializers/rest/admin/tag_serializer.rb b/app/serializers/rest/admin/tag_serializer.rb
new file mode 100644
index 000000000..425ba4ba3
--- /dev/null
+++ b/app/serializers/rest/admin/tag_serializer.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class REST::Admin::TagSerializer < REST::TagSerializer
+  attributes :id, :trendable, :usable, :requires_review
+
+  def id
+    object.id.to_s
+  end
+
+  def requires_review
+    object.requires_review?
+  end
+end
diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb
index 250d0e8ed..bc3258025 100644
--- a/app/services/post_status_service.rb
+++ b/app/services/post_status_service.rb
@@ -83,6 +83,9 @@ class PostStatusService < BaseService
     status_for_validation = @account.statuses.build(status_attributes)
 
     if status_for_validation.valid?
+      # Marking the status as destroyed is necessary to prevent the status from being
+      # persisted when the associated media attachments get updated when creating the
+      # scheduled status.
       status_for_validation.destroy
 
       # The following transaction block is needed to wrap the UPDATEs to
diff --git a/app/validators/reaction_validator.rb b/app/validators/reaction_validator.rb
index 494b6041b..4ed3376e8 100644
--- a/app/validators/reaction_validator.rb
+++ b/app/validators/reaction_validator.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class ReactionValidator < ActiveModel::Validator
-  SUPPORTED_EMOJIS = Oj.load(File.read(Rails.root.join('app', 'javascript', 'mastodon', 'features', 'emoji', 'emoji_map.json'))).keys.freeze
+  SUPPORTED_EMOJIS = Oj.load_file(Rails.root.join('app', 'javascript', 'mastodon', 'features', 'emoji', 'emoji_map.json').to_s).keys.freeze
 
   LIMIT = 8
 
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index bd36580e6..49e725142 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -1,6 +1,11 @@
 - content_for :page_title do
   = t('admin.dashboard.title')
 
+- content_for :heading_actions do
+  = l(@time_period.first)
+  = ' - '
+  = l(@time_period.last)
+
 - unless @system_checks.empty?
   .flash-message-stack
     - @system_checks.each do |message|
@@ -9,133 +14,52 @@
         - if message.action
           = link_to t("admin.system_checks.#{message.key}.action"), message.action
 
-.dashboard__counters
-  %div
-    = link_to admin_accounts_url(local: 1, recent: 1) do
-      .dashboard__counters__num{ title: number_with_delimiter(@users_count, strip_insignificant_zeros: true) }
-        = friendly_number_to_human @users_count
-      .dashboard__counters__label= t 'admin.dashboard.total_users'
-  %div
-    %div
-      .dashboard__counters__num{ title: number_with_delimiter(@registrations_week, strip_insignificant_zeros: true) }
-        = friendly_number_to_human @registrations_week
-      .dashboard__counters__label= t 'admin.dashboard.week_users_new'
-  %div
-    %div
-      .dashboard__counters__num{ title: number_with_delimiter(@logins_week, strip_insignificant_zeros: true) }
-        = friendly_number_to_human @logins_week
-      .dashboard__counters__label= t 'admin.dashboard.week_users_active'
-  %div
-    = link_to admin_pending_accounts_path do
-      .dashboard__counters__num{ title: number_with_delimiter(@pending_users_count, strip_insignificant_zeros: true) }
-        = friendly_number_to_human @pending_users_count
-      .dashboard__counters__label= t 'admin.dashboard.pending_users'
-  %div
-    = link_to admin_reports_url do
-      .dashboard__counters__num{ title: number_with_delimiter(@reports_count, strip_insignificant_zeros: true) }
-        = friendly_number_to_human @reports_count
-      .dashboard__counters__label= t 'admin.dashboard.open_reports'
-  %div
-    = link_to admin_tags_path(pending_review: '1') do
-      .dashboard__counters__num{ title: number_with_delimiter(@pending_tags_count, strip_insignificant_zeros: true) }
-        = friendly_number_to_human @pending_tags_count
-      .dashboard__counters__label= t 'admin.dashboard.pending_tags'
-  %div
-    %div
-      .dashboard__counters__num{ title: number_with_delimiter(@interactions_week, strip_insignificant_zeros: true) }
-        = friendly_number_to_human @interactions_week
-      .dashboard__counters__label= t 'admin.dashboard.week_interactions'
-  %div
-    = link_to sidekiq_url do
-      .dashboard__counters__num{ title: number_with_delimiter(@queue_backlog, strip_insignificant_zeros: true) }
-        = friendly_number_to_human @queue_backlog
-      .dashboard__counters__label= t 'admin.dashboard.backlog'
-
-.dashboard__widgets
-  .dashboard__widgets__users
-    %div
-      %h4= t 'admin.dashboard.recent_users'
-      %ul
-        - @recent_users.each do |user|
-          %li= admin_account_link_to(user.account)
-
-  .dashboard__widgets__features
-    %div
-      %h4= t 'admin.dashboard.features'
-      %ul
-        %li
-          = feature_hint(link_to(t('admin.dashboard.feature_registrations'), edit_admin_settings_path), @registrations_enabled)
-        %li
-          = feature_hint(link_to(t('admin.dashboard.feature_invites'), edit_admin_settings_path), @invites_enabled)
-        %li
-          = feature_hint(link_to(t('admin.dashboard.feature_deletions'), edit_admin_settings_path), @deletions_enabled)
-        %li
-          = feature_hint(link_to(t('admin.dashboard.feature_profile_directory'), edit_admin_settings_path), @profile_directory)
-        %li
-          = feature_hint(link_to(t('admin.dashboard.feature_timeline_preview'), edit_admin_settings_path), @timeline_preview)
-        %li
-          = feature_hint(link_to(t('admin.dashboard.keybase'), edit_admin_settings_path), @keybase_integration)
-        %li
-          = feature_hint(link_to(t('admin.dashboard.trends'), edit_admin_settings_path), @trends_enabled)
-        %li
-          = feature_hint(link_to(t('admin.dashboard.feature_relay'), admin_relays_path), @relay_enabled)
-
-  .dashboard__widgets__versions
-    %div
-      %h4= t 'admin.dashboard.software'
-      %ul
-        %li
-          Mastodon
-          %span.pull-right= @version
-        %li
-          Ruby
-          %span.pull-right= "#{RUBY_VERSION}p#{RUBY_PATCHLEVEL}"
-        %li
-          PostgreSQL
-          %span.pull-right= @database_version
-        %li
-          Redis
-          %span.pull-right= @redis_version
-
-  .dashboard__widgets__space
-    %div
-      %h4= t 'admin.dashboard.space'
-      %ul
-        %li
-          PostgreSQL
-          %span.pull-right= number_to_human_size @database_size
-        %li
-          Redis
-          %span.pull-right= number_to_human_size @redis_size
-
-  .dashboard__widgets__config
-    %div
-      %h4= t 'admin.dashboard.config'
-      %ul
-        %li
-          = feature_hint(t('admin.dashboard.search'), @search_enabled)
-        %li
-          = feature_hint(t('admin.dashboard.single_user_mode'), @single_user_mode)
-        %li
-          = feature_hint(t('admin.dashboard.authorized_fetch_mode'), @authorized_fetch)
-        %li
-          = feature_hint(t('admin.dashboard.whitelist_mode'), @whitelist_enabled)
-        %li
-          = feature_hint('LDAP', @ldap_enabled)
-        %li
-          = feature_hint('CAS', @cas_enabled)
-        %li
-          = feature_hint('SAML', @saml_enabled)
-        %li
-          = feature_hint('PAM', @pam_enabled)
-        %li
-          = feature_hint(t('admin.dashboard.hidden_service'), @hidden_service)
-
-  .dashboard__widgets__trends
-    %div
-      %h4= t 'admin.dashboard.trends'
-      %ul
-        - @trending_hashtags.each do |tag|
-          %li
-            = link_to content_tag(:span, "##{tag.name}", class: !tag.trendable? && !tag.reviewed? ? 'warning-hint' : (!tag.trendable? ? 'negative-hint' : nil)), admin_tag_path(tag.id)
-            %span.pull-right= number_with_delimiter(tag.history[0][:accounts].to_i)
+.dashboard
+  .dashboard__item
+    = react_admin_component :counter, measure: 'new_users', start_at: @time_period.first, end_at: @time_period.last, label: t('admin.dashboard.new_users'), href: admin_accounts_path
+
+  .dashboard__item
+    = react_admin_component :counter, measure: 'active_users', start_at: @time_period.first, end_at: @time_period.last, label: t('admin.dashboard.active_users'), href: admin_accounts_path
+
+  .dashboard__item
+    = react_admin_component :counter, measure: 'interactions', start_at: @time_period.first, end_at: @time_period.last, label: t('admin.dashboard.interactions')
+
+  .dashboard__item
+    = react_admin_component :counter, measure: 'opened_reports', start_at: @time_period.first, end_at: @time_period.last, label: t('admin.dashboard.opened_reports'), href: admin_reports_path
+
+  .dashboard__item
+    = react_admin_component :counter, measure: 'resolved_reports', start_at: @time_period.first, end_at: @time_period.last, label: t('admin.dashboard.resolved_reports'), href: admin_reports_path(resolved: '1')
+
+  .dashboard__item
+    = link_to admin_reports_path, class: 'dashboard__quick-access' do
+      %span= t('admin.dashboard.pending_reports_html', count: @pending_reports_count)
+      = fa_icon 'chevron-right fw'
+
+    = link_to admin_pending_accounts_path, class: 'dashboard__quick-access' do
+      %span= t('admin.dashboard.pending_users_html', count: @pending_users_count)
+      = fa_icon 'chevron-right fw'
+
+    = link_to admin_tags_path(pending_review: '1'), class: 'dashboard__quick-access' do
+      %span= t('admin.dashboard.pending_tags_html', count: @pending_tags_count)
+      = fa_icon 'chevron-right fw'
+
+  .dashboard__item
+    = react_admin_component :dimension, dimension: 'sources', start_at: @time_period.first, end_at: @time_period.last, limit: 8, label: t('admin.dashboard.sources')
+
+  .dashboard__item
+    = react_admin_component :dimension, dimension: 'languages', start_at: @time_period.first, end_at: @time_period.last, limit: 8, label: t('admin.dashboard.top_languages')
+
+  .dashboard__item
+    = react_admin_component :dimension, dimension: 'servers', start_at: @time_period.first, end_at: @time_period.last, limit: 8, label: t('admin.dashboard.top_servers')
+
+  .dashboard__item.dashboard__item--span-double-column
+    = react_admin_component :retention, start_at: @time_period.last - 6.months,   end_at: @time_period.last, frequency: 'month'
+
+  .dashboard__item.dashboard__item--span-double-row
+    = react_admin_component :trends, limit: 7
+
+  .dashboard__item
+    = react_admin_component :dimension, dimension: 'software_versions', start_at: @time_period.first, end_at: @time_period.last, limit: 4, label: t('admin.dashboard.software')
+
+  .dashboard__item
+    = react_admin_component :dimension, dimension: 'space_usage', start_at: @time_period.first, end_at: @time_period.last, limit: 3, label: t('admin.dashboard.space')
diff --git a/chart/Chart.lock b/chart/Chart.lock
index 7b68a2ed2..d74e7570c 100644
--- a/chart/Chart.lock
+++ b/chart/Chart.lock
@@ -1,12 +1,12 @@
 dependencies:
 - name: elasticsearch
   repository: https://charts.bitnami.com/bitnami
-  version: 14.2.3
+  version: 15.10.3
 - name: postgresql
   repository: https://charts.bitnami.com/bitnami
   version: 8.10.14
 - name: redis
   repository: https://charts.bitnami.com/bitnami
   version: 10.9.0
-digest: sha256:9e3e7b987c6ffba9295a30b7fae2613fe680c2b1a1832ff5afb185414ce1898e
-generated: "2021-02-27T01:01:12.776919968Z"
+digest: sha256:f5c57108f7768fd16391c1a050991c7809f84a640cca308d7d24d87379d04000
+generated: "2021-08-05T08:01:01.457727804Z"
diff --git a/chart/Chart.yaml b/chart/Chart.yaml
index 8f1976cf7..a09ae8e8a 100644
--- a/chart/Chart.yaml
+++ b/chart/Chart.yaml
@@ -24,7 +24,7 @@ appVersion: 3.3.0
 
 dependencies:
   - name: elasticsearch
-    version: 14.2.3
+    version: 15.10.3
     repository: https://charts.bitnami.com/bitnami
     condition: elasticsearch.enabled
   - name: postgresql
diff --git a/config/application.rb b/config/application.rb
index 68855a567..b92d749c1 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -107,6 +107,7 @@ module Mastodon
       :ka,
       :kab,
       :kk,
+      :kmr,
       :kn,
       :ko,
       :ku,
diff --git a/config/environments/production.rb b/config/environments/production.rb
index bf6b5d88e..417d53a8f 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -105,7 +105,7 @@ Rails.application.configure do
     :password             => ENV['SMTP_PASSWORD'].presence,
     :domain               => ENV['SMTP_DOMAIN'] || ENV['LOCAL_DOMAIN'],
     :authentication       => ENV['SMTP_AUTH_METHOD'] == 'none' ? nil : ENV['SMTP_AUTH_METHOD'] || :plain,
-    :ca_file              => ENV['SMTP_CA_FILE'].presence,
+    :ca_file              => ENV['SMTP_CA_FILE'].presence || '/etc/ssl/certs/ca-certificates.crt',
     :openssl_verify_mode  => ENV['SMTP_OPENSSL_VERIFY_MODE'],
     :enable_starttls_auto => ENV['SMTP_ENABLE_STARTTLS_AUTO'] || true,
     :tls                  => ENV['SMTP_TLS'].presence,
diff --git a/config/initializers/twitter_regex.rb b/config/initializers/twitter_regex.rb
index 84c09ff35..d2ea5f974 100644
--- a/config/initializers/twitter_regex.rb
+++ b/config/initializers/twitter_regex.rb
@@ -24,10 +24,9 @@ module Twitter::TwitterText
         )
       \)
     /iox
-    REGEXEN[:valid_iri_ucschar] = /[\u{A0}-\u{D7FF}\u{F900}-\u{FDCF}\u{FDF0}-\u{FFEF}\u{10000}-\u{1FFFD}\u{20000}-\u{2FFFD}\u{30000}-\u{3FFFD}\u{40000}-\u{4FFFD}\u{50000}-\u{5FFFD}\u{60000}-\u{6FFFD}\u{70000}-\u{7FFFD}\u{80000}-\u{8FFFD}\u{90000}-\u{9FFFD}\u{A0000}-\u{AFFFD}\u{B0000}-\u{BFFFD}\u{C0000}-\u{CFFFD}\u{D0000}-\u{DFFFD}\u{E1000}-\u{EFFFD}]/iou
-    REGEXEN[:valid_iri_iprivate] = /[\u{E000}-\u{F8FF}\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}]/iou
-    REGEXEN[:valid_url_query_chars] = /(?:#{REGEXEN[:valid_iri_ucschar]})|(?:#{REGEXEN[:valid_iri_iprivate]})|[a-z0-9!?\*'\(\);:&=\+\$\/%#\[\]\-_\.,~|@]/iou
-    REGEXEN[:valid_url_query_ending_chars] = /(?:#{REGEXEN[:valid_iri_ucschar]})|(?:#{REGEXEN[:valid_iri_iprivate]})|[a-z0-9_&=#\/\-]/iou
+    UCHARS = '\u{A0}-\u{D7FF}\u{F900}-\u{FDCF}\u{FDF0}-\u{FFEF}\u{10000}-\u{1FFFD}\u{20000}-\u{2FFFD}\u{30000}-\u{3FFFD}\u{40000}-\u{4FFFD}\u{50000}-\u{5FFFD}\u{60000}-\u{6FFFD}\u{70000}-\u{7FFFD}\u{80000}-\u{8FFFD}\u{90000}-\u{9FFFD}\u{A0000}-\u{AFFFD}\u{B0000}-\u{BFFFD}\u{C0000}-\u{CFFFD}\u{D0000}-\u{DFFFD}\u{E1000}-\u{EFFFD}\u{E000}-\u{F8FF}\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}'
+    REGEXEN[:valid_url_query_chars] = /[a-z0-9!?\*'\(\);:&=\+\$\/%#\[\]\-_\.,~|@#{UCHARS}]/iou
+    REGEXEN[:valid_url_query_ending_chars] = /[a-z0-9_&=#\/\-#{UCHARS}]/iou
     REGEXEN[:valid_url_path] = /(?:
       (?:
         #{REGEXEN[:valid_general_url_path_chars]}*
@@ -57,23 +56,21 @@ module Twitter::TwitterText
       #{REGEXEN[:validate_url_pct_encoded]}|
       #{REGEXEN[:validate_url_sub_delims]}
     )/iox
-    REGEXEN[:xmpp_uri] = %r{
-      (xmpp:)                                                                           # Protocol
-      (//#{REGEXEN[:validate_nodeid]}+@#{REGEXEN[:valid_domain]}/)?                     # Authority (optional)
-      (#{REGEXEN[:validate_nodeid]}+@)?                                                 # Username in path (optional)
-      (#{REGEXEN[:valid_domain]})                                                       # Domain in path
-      (/#{REGEXEN[:validate_resid]}+)?                                                  # Resource in path (optional)
-      (\?#{REGEXEN[:valid_url_query_chars]}*#{REGEXEN[:valid_url_query_ending_chars]})? # Query String
-    }iox
-    REGEXEN[:magnet_uri] = %r{
-      (magnet:)                                                                         # Protocol
-      (\?#{REGEXEN[:valid_url_query_chars]}*#{REGEXEN[:valid_url_query_ending_chars]})  # Query String
-    }iox
     REGEXEN[:valid_extended_uri] = %r{
       (                                                                                 #   $1 total match
         (#{REGEXEN[:valid_url_preceding_chars]})                                        #   $2 Preceding character
         (                                                                               #   $3 URL
-          (#{REGEXEN[:xmpp_uri]}) | (#{REGEXEN[:magnet_uri]})
+          (
+            (xmpp:)                                                                           # Protocol
+            (//#{REGEXEN[:validate_nodeid]}+@#{REGEXEN[:valid_domain]}/)?                     # Authority (optional)
+            (#{REGEXEN[:validate_nodeid]}+@)?                                                 # Username in path (optional)
+            (#{REGEXEN[:valid_domain]})                                                       # Domain in path
+            (/#{REGEXEN[:validate_resid]}+)?                                                  # Resource in path (optional)
+            (\?#{REGEXEN[:valid_url_query_chars]}*#{REGEXEN[:valid_url_query_ending_chars]})? # Query String
+          ) | (
+            (magnet:)                                                                         # Protocol
+            (\?#{REGEXEN[:valid_url_query_chars]}*#{REGEXEN[:valid_url_query_ending_chars]})  # Query String
+          )
         )
       )
     }iox
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 486592b29..be15ad4b0 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -371,32 +371,28 @@ en:
       updated_msg: Emoji successfully updated!
       upload: Upload
     dashboard:
-      authorized_fetch_mode: Secure mode
-      backlog: backlogged jobs
-      config: Configuration
-      feature_deletions: Account deletions
-      feature_invites: Invite links
-      feature_profile_directory: Profile directory
-      feature_registrations: Registrations
-      feature_relay: Federation relay
-      feature_timeline_preview: Timeline preview
-      features: Features
-      hidden_service: Federation with hidden services
-      open_reports: open reports
-      pending_tags: hashtags waiting for review
-      pending_users: users waiting for review
-      recent_users: Recent users
-      search: Full-text search
-      single_user_mode: Single user mode
+      active_users: active users
+      interactions: interactions
+      media_storage: Media storage
+      new_users: new users
+      opened_reports: reports opened
+      pending_reports_html:
+        one: "<strong>1</strong> pending reports"
+        other: "<strong>%{count}</strong> pending reports"
+      pending_tags_html:
+        one: "<strong>1</strong> pending hashtags"
+        other: "<strong>%{count}</strong> pending hashtags"
+      pending_users_html:
+        one: "<strong>1</strong> pending users"
+        other: "<strong>%{count}</strong> pending users"
+      resolved_reports: reports resolved
       software: Software
+      sources: Sign-up sources
       space: Space usage
       title: Dashboard
-      total_users: users in total
-      trends: Trends
-      week_interactions: interactions this week
-      week_users_active: active this week
-      week_users_new: users this week
-      whitelist_mode: Limited federation mode
+      top_languages: Top active languages
+      top_servers: Top active servers
+      website: Website
     domain_allows:
       add_new: Allow federation with domain
       created_msg: Domain has been successfully allowed for federation
@@ -1336,10 +1332,10 @@ en:
       '63113904': 2 years
       '7889238': 3 months
     min_age_label: Age threshold
-    min_favs: Keep posts favourited more than
-    min_favs_hint: Doesn't delete any of your posts that has received more than this amount of favourites. Leave blank to delete posts regardless of their number of favourites
-    min_reblogs: Keep posts boosted more than
-    min_reblogs_hint: Doesn't delete any of your posts that has been boosted more than this number of times. Leave blank to delete posts regardless of their number of boosts
+    min_favs: Keep posts favourited at least
+    min_favs_hint: Doesn't delete any of your posts that has received at least this amount of favourites. Leave blank to delete posts regardless of their number of favourites
+    min_reblogs: Keep posts boosted at least
+    min_reblogs_hint: Doesn't delete any of your posts that has been boosted at least this number of times. Leave blank to delete posts regardless of their number of boosts
   stream_entries:
     pinned: Pinned post
     reblogged: boosted
diff --git a/config/routes.rb b/config/routes.rb
index d4ca4389d..007fba5f2 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -514,6 +514,12 @@ Rails.application.routes.draw do
             post :resolve
           end
         end
+
+        resources :trends, only: [:index]
+
+        post :measures, to: 'measures#create'
+        post :dimensions, to: 'dimensions#create'
+        post :retention, to: 'retention#create'
       end
     end
 
diff --git a/lib/cli.rb b/lib/cli.rb
index 3f1658566..8815e137a 100644
--- a/lib/cli.rb
+++ b/lib/cli.rb
@@ -94,17 +94,22 @@ module Mastodon
 
       exit(1) unless prompt.ask('Type in the domain of the server to confirm:', required: true) == Rails.configuration.x.local_domain
 
-      prompt.warn('This operation WILL NOT be reversible. It can also take a long time.')
-      prompt.warn('While the data won\'t be erased locally, the server will be in a BROKEN STATE afterwards.')
-      prompt.warn('A running Sidekiq process is required. Do not shut it down until queues clear.')
+      unless options[:dry_run]
+        prompt.warn('This operation WILL NOT be reversible. It can also take a long time.')
+        prompt.warn('While the data won\'t be erased locally, the server will be in a BROKEN STATE afterwards.')
+        prompt.warn('A running Sidekiq process is required. Do not shut it down until queues clear.')
 
-      exit(1) if prompt.no?('Are you sure you want to proceed?')
+        exit(1) if prompt.no?('Are you sure you want to proceed?')
+      end
 
       inboxes   = Account.inboxes
       processed = 0
       dry_run   = options[:dry_run] ? ' (DRY RUN)' : ''
 
+      Setting.registrations_mode = 'none' unless options[:dry_run]
+
       if inboxes.empty?
+        Account.local.without_suspended.in_batches.update_all(suspended_at: Time.now.utc, suspension_origin: :local) unless options[:dry_run]
         prompt.ok('It seems like your server has not federated with anything')
         prompt.ok('You can shut it down and delete it any time')
         return
@@ -112,9 +117,7 @@ module Mastodon
 
       prompt.warn('Do NOT interrupt this process...')
 
-      Setting.registrations_mode = 'none'
-
-      Account.local.without_suspended.find_each do |account|
+      delete_account = ->(account) do
         payload = ActiveModelSerializers::SerializableResource.new(
           account,
           serializer: ActivityPub::DeleteActorSerializer,
@@ -128,12 +131,15 @@ module Mastodon
             [json, account.id, inbox_url]
           end
 
-          account.suspend!
+          account.suspend!(block_email: false)
         end
 
         processed += 1
       end
 
+      Account.local.without_suspended.find_each { |account| delete_account.call(account) }
+      Account.local.suspended.joins(:deletion_request).find_each { |account| delete_account.call(account) }
+
       prompt.ok("Queued #{inboxes.size * processed} items into Sidekiq for #{processed} accounts#{dry_run}")
       prompt.ok('Wait until Sidekiq processes all items, then you can shut everything down and delete the data')
     rescue TTY::Reader::InputInterrupt
diff --git a/lib/mastodon/accounts_cli.rb b/lib/mastodon/accounts_cli.rb
index 050194801..2ef85d0a9 100644
--- a/lib/mastodon/accounts_cli.rb
+++ b/lib/mastodon/accounts_cli.rb
@@ -287,7 +287,7 @@ module Mastodon
 
     option :concurrency, type: :numeric, default: 5, aliases: [:c]
     option :dry_run, type: :boolean
-    desc 'cull', 'Remove remote accounts that no longer exist'
+    desc 'cull [DOMAIN...]', 'Remove remote accounts that no longer exist'
     long_desc <<-LONG_DESC
       Query every single remote account in the database to determine
       if it still exists on the origin server, and if it doesn't,
@@ -296,19 +296,22 @@ module Mastodon
       Accounts that have had confirmed activity within the last week
       are excluded from the checks.
     LONG_DESC
-    def cull
+    def cull(*domains)
       skip_threshold = 7.days.ago
       dry_run        = options[:dry_run] ? ' (DRY RUN)' : ''
       skip_domains   = Concurrent::Set.new
 
-      processed, culled = parallelize_with_progress(Account.remote.where(protocol: :activitypub).partitioned) do |account|
+      query = Account.remote.where(protocol: :activitypub)
+      query = query.where(domain: domains) unless domains.empty?
+
+      processed, culled = parallelize_with_progress(query.partitioned) do |account|
         next if account.updated_at >= skip_threshold || (account.last_webfingered_at.present? && account.last_webfingered_at >= skip_threshold) || skip_domains.include?(account.domain)
 
         code = 0
 
         begin
           code = Request.new(:head, account.uri).perform(&:code)
-        rescue HTTP::ConnectionError
+        rescue HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError
           skip_domains << account.domain
         end
 
diff --git a/package.json b/package.json
index 36631d743..5193237c4 100644
--- a/package.json
+++ b/package.json
@@ -61,11 +61,11 @@
   },
   "private": true,
   "dependencies": {
-    "@babel/core": "^7.15.5",
+    "@babel/core": "^7.15.8",
     "@babel/plugin-proposal-decorators": "^7.15.8",
     "@babel/plugin-transform-react-inline-elements": "^7.14.5",
     "@babel/plugin-transform-runtime": "^7.15.8",
-    "@babel/preset-env": "^7.15.6",
+    "@babel/preset-env": "^7.15.8",
     "@babel/preset-react": "^7.14.5",
     "@babel/runtime": "^7.15.4",
     "@gamestdio/websocket": "^0.3.2",
@@ -102,7 +102,7 @@
     "glob": "^7.2.0",
     "history": "^4.10.1",
     "http-link-header": "^1.0.3",
-    "immutable": "^3.8.2",
+    "immutable": "^4.0.0",
     "imports-loader": "^1.2.0",
     "intersection-observer": "^0.12.0",
     "intl": "^1.2.5",
@@ -141,7 +141,7 @@
     "react-redux-loading-bar": "^4.0.8",
     "react-router-dom": "^4.1.1",
     "react-router-scroll-4": "^1.0.0-beta.1",
-    "react-select": "^4.3.1",
+    "react-select": "^5.1.0",
     "react-sparklines": "^1.7.0",
     "react-swipeable-views": "^0.14.0",
     "react-textarea-autosize": "^8.3.3",
@@ -184,7 +184,7 @@
     "eslint-plugin-jsx-a11y": "~6.4.1",
     "eslint-plugin-promise": "~5.1.0",
     "eslint-plugin-react": "~7.26.1",
-    "jest": "^27.2.3",
+    "jest": "^27.2.5",
     "raf": "^3.4.1",
     "react-intl-translations-manager": "^5.0.3",
     "react-test-renderer": "^16.14.0",
diff --git a/spec/models/account_statuses_cleanup_policy_spec.rb b/spec/models/account_statuses_cleanup_policy_spec.rb
index 63e9c5d20..4732ad625 100644
--- a/spec/models/account_statuses_cleanup_policy_spec.rb
+++ b/spec/models/account_statuses_cleanup_policy_spec.rb
@@ -499,9 +499,9 @@ RSpec.describe AccountStatusesCleanupPolicy, type: :model do
       end
     end
 
-    context 'when policy is to keep statuses with more than 4 boosts' do
+    context 'when policy is to keep statuses with at least 5 boosts' do
       before do
-        account_statuses_cleanup_policy.min_reblogs = 4
+        account_statuses_cleanup_policy.min_reblogs = 5
       end
 
       it 'does not return the recent toot' do
@@ -521,9 +521,9 @@ RSpec.describe AccountStatusesCleanupPolicy, type: :model do
       end
     end
 
-    context 'when policy is to keep statuses with more than 4 favs' do
+    context 'when policy is to keep statuses with at least 5 favs' do
       before do
-        account_statuses_cleanup_policy.min_favs = 4
+        account_statuses_cleanup_policy.min_favs = 5
       end
 
       it 'does not return the recent toot' do
diff --git a/spec/services/post_status_service_spec.rb b/spec/services/post_status_service_spec.rb
index 147a59fc3..d21270c79 100644
--- a/spec/services/post_status_service_spec.rb
+++ b/spec/services/post_status_service_spec.rb
@@ -25,29 +25,33 @@ RSpec.describe PostStatusService, type: :service do
     expect(status.thread).to eq in_reply_to_status
   end
 
-  it 'schedules a status' do
-    account = Fabricate(:account)
-    future  = Time.now.utc + 2.hours
-
-    status = subject.call(account, text: 'Hi future!', scheduled_at: future)
-
-    expect(status).to be_a ScheduledStatus
-    expect(status.scheduled_at).to eq future
-    expect(status.params['text']).to eq 'Hi future!'
-  end
-
-  it 'does not immediately create a status when scheduling a status' do
-    account = Fabricate(:account)
-    media = Fabricate(:media_attachment)
-    future  = Time.now.utc + 2.hours
-
-    status = subject.call(account, text: 'Hi future!', media_ids: [media.id], scheduled_at: future)
-
-    expect(status).to be_a ScheduledStatus
-    expect(status.scheduled_at).to eq future
-    expect(status.params['text']).to eq 'Hi future!'
-    expect(media.reload.status).to be_nil
-    expect(Status.where(text: 'Hi future!').exists?).to be_falsey
+  context 'when scheduling a status' do
+    let!(:account)         { Fabricate(:account) }
+    let!(:future)          { Time.now.utc + 2.hours }
+    let!(:previous_status) { Fabricate(:status, account: account) }
+
+    it 'schedules a status' do
+      status = subject.call(account, text: 'Hi future!', scheduled_at: future)
+      expect(status).to be_a ScheduledStatus
+      expect(status.scheduled_at).to eq future
+      expect(status.params['text']).to eq 'Hi future!'
+    end
+
+    it 'does not immediately create a status' do
+      media = Fabricate(:media_attachment, account: account)
+      status = subject.call(account, text: 'Hi future!', media_ids: [media.id], scheduled_at: future)
+
+      expect(status).to be_a ScheduledStatus
+      expect(status.scheduled_at).to eq future
+      expect(status.params['text']).to eq 'Hi future!'
+      expect(status.params['media_ids']).to eq [media.id]
+      expect(media.reload.status).to be_nil
+      expect(Status.where(text: 'Hi future!').exists?).to be_falsey
+    end
+
+    it 'does not change statuses count' do
+      expect { subject.call(account, text: 'Hi future!', scheduled_at: future, thread: previous_status) }.not_to change { [account.statuses_count, previous_status.replies_count] }
+    end
   end
 
   it 'creates response to the original status of boost' do
diff --git a/yarn.lock b/yarn.lock
index 27d12c413..ce4fc91d0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9,10 +9,10 @@
   dependencies:
     "@babel/highlight" "^7.10.4"
 
-"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.14.5":
-  version "7.14.5"
-  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.14.5.tgz#23b08d740e83f49c5e59945fbf1b43e80bbf4edb"
-  integrity sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==
+"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.14.5", "@babel/code-frame@^7.15.8":
+  version "7.15.8"
+  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.15.8.tgz#45990c47adadb00c03677baa89221f7cc23d2503"
+  integrity sha512-2IAnmn8zbvC/jKYhq5Ki9I+DwjlrtMPUCH/CpHvqI4dNnlwHwsxoIhlc8WcYY5LSYknXQtAlFYuHfqAFCvQ4Wg==
   dependencies:
     "@babel/highlight" "^7.14.5"
 
@@ -21,20 +21,20 @@
   resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.15.0.tgz#2dbaf8b85334796cafbb0f5793a90a2fc010b176"
   integrity sha512-0NqAC1IJE0S0+lL1SWFMxMkz1pKCNCjI4tr2Zx4LJSXxCLAdr6KyArnY+sno5m3yH9g737ygOyPABDsnXkpxiA==
 
-"@babel/core@^7.1.0", "@babel/core@^7.15.5", "@babel/core@^7.7.2", "@babel/core@^7.7.5":
-  version "7.15.5"
-  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.15.5.tgz#f8ed9ace730722544609f90c9bb49162dc3bf5b9"
-  integrity sha512-pYgXxiwAgQpgM1bNkZsDEq85f0ggXMA5L7c+o3tskGMh2BunCI9QUwB9Z4jpvXUOuMdyGKiGKQiRe11VS6Jzvg==
+"@babel/core@^7.1.0", "@babel/core@^7.15.8", "@babel/core@^7.7.2", "@babel/core@^7.7.5":
+  version "7.15.8"
+  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.15.8.tgz#195b9f2bffe995d2c6c159e72fe525b4114e8c10"
+  integrity sha512-3UG9dsxvYBMYwRv+gS41WKHno4K60/9GPy1CJaH6xy3Elq8CTtvtjT5R5jmNhXfCYLX2mTw+7/aq5ak/gOE0og==
   dependencies:
-    "@babel/code-frame" "^7.14.5"
-    "@babel/generator" "^7.15.4"
+    "@babel/code-frame" "^7.15.8"
+    "@babel/generator" "^7.15.8"
     "@babel/helper-compilation-targets" "^7.15.4"
-    "@babel/helper-module-transforms" "^7.15.4"
+    "@babel/helper-module-transforms" "^7.15.8"
     "@babel/helpers" "^7.15.4"
-    "@babel/parser" "^7.15.5"
+    "@babel/parser" "^7.15.8"
     "@babel/template" "^7.15.4"
     "@babel/traverse" "^7.15.4"
-    "@babel/types" "^7.15.4"
+    "@babel/types" "^7.15.6"
     convert-source-map "^1.7.0"
     debug "^4.1.0"
     gensync "^1.0.0-beta.2"
@@ -42,12 +42,12 @@
     semver "^6.3.0"
     source-map "^0.5.0"
 
-"@babel/generator@^7.15.4", "@babel/generator@^7.7.2":
-  version "7.15.4"
-  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.15.4.tgz#85acb159a267ca6324f9793986991ee2022a05b0"
-  integrity sha512-d3itta0tu+UayjEORPNz6e1T3FtvWlP5N4V5M+lhp/CxT4oAA7/NcScnpRyspUMLK6tu9MNHmQHxRykuN2R7hw==
+"@babel/generator@^7.15.4", "@babel/generator@^7.15.8", "@babel/generator@^7.7.2":
+  version "7.15.8"
+  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.15.8.tgz#fa56be6b596952ceb231048cf84ee499a19c0cd1"
+  integrity sha512-ECmAKstXbp1cvpTTZciZCgfOt6iN64lR0d+euv3UZisU5awfRawOvg07Utn/qBGuH4bRIEZKrA/4LzZyXhZr8g==
   dependencies:
-    "@babel/types" "^7.15.4"
+    "@babel/types" "^7.15.6"
     jsesc "^2.5.1"
     source-map "^0.5.0"
 
@@ -192,19 +192,19 @@
   dependencies:
     "@babel/types" "^7.15.4"
 
-"@babel/helper-module-transforms@^7.14.5", "@babel/helper-module-transforms@^7.15.4":
-  version "7.15.4"
-  resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.15.4.tgz#962cc629a7f7f9a082dd62d0307fa75fe8788d7c"
-  integrity sha512-9fHHSGE9zTC++KuXLZcB5FKgvlV83Ox+NLUmQTawovwlJ85+QMhk1CnVk406CQVj97LaWod6KVjl2Sfgw9Aktw==
+"@babel/helper-module-transforms@^7.14.5", "@babel/helper-module-transforms@^7.15.4", "@babel/helper-module-transforms@^7.15.8":
+  version "7.15.8"
+  resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.15.8.tgz#d8c0e75a87a52e374a8f25f855174786a09498b2"
+  integrity sha512-DfAfA6PfpG8t4S6npwzLvTUpp0sS7JrcuaMiy1Y5645laRJIp/LiLGIBbQKaXSInK8tiGNI7FL7L8UvB8gdUZg==
   dependencies:
     "@babel/helper-module-imports" "^7.15.4"
     "@babel/helper-replace-supers" "^7.15.4"
     "@babel/helper-simple-access" "^7.15.4"
     "@babel/helper-split-export-declaration" "^7.15.4"
-    "@babel/helper-validator-identifier" "^7.14.9"
+    "@babel/helper-validator-identifier" "^7.15.7"
     "@babel/template" "^7.15.4"
     "@babel/traverse" "^7.15.4"
-    "@babel/types" "^7.15.4"
+    "@babel/types" "^7.15.6"
 
 "@babel/helper-optimise-call-expression@^7.14.5":
   version "7.14.5"
@@ -306,6 +306,11 @@
   resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz#6654d171b2024f6d8ee151bf2509699919131d48"
   integrity sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==
 
+"@babel/helper-validator-identifier@^7.15.7":
+  version "7.15.7"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389"
+  integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==
+
 "@babel/helper-validator-option@^7.14.5":
   version "7.14.5"
   resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz#6e72a1fff18d5dfcb878e1e62f1a021c4b72d5a3"
@@ -358,10 +363,10 @@
     chalk "^2.0.0"
     js-tokens "^4.0.0"
 
-"@babel/parser@^7.1.0", "@babel/parser@^7.15.4", "@babel/parser@^7.15.5", "@babel/parser@^7.7.0", "@babel/parser@^7.7.2":
-  version "7.15.5"
-  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.15.5.tgz#d33a58ca69facc05b26adfe4abebfed56c1c2dac"
-  integrity sha512-2hQstc6I7T6tQsWzlboMh3SgMRPaS4H6H7cPQsJkdzTzEGqQrpLDsE2BGASU5sBPoEQyHzeqU6C8uKbFeEk6sg==
+"@babel/parser@^7.1.0", "@babel/parser@^7.15.4", "@babel/parser@^7.15.8", "@babel/parser@^7.7.0", "@babel/parser@^7.7.2":
+  version "7.15.8"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.15.8.tgz#7bacdcbe71bdc3ff936d510c15dcea7cf0b99016"
+  integrity sha512-BRYa3wcQnjS/nqI8Ac94pYYpJfojHVvVXJ97+IDCImX4Jc8W8Xv1+47enbruk+q1etOpsQNwnfFcNGw+gtPGxA==
 
 "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.15.4":
   version "7.15.4"
@@ -372,10 +377,10 @@
     "@babel/helper-skip-transparent-expression-wrappers" "^7.15.4"
     "@babel/plugin-proposal-optional-chaining" "^7.14.5"
 
-"@babel/plugin-proposal-async-generator-functions@^7.15.4":
-  version "7.15.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.15.4.tgz#f82aabe96c135d2ceaa917feb9f5fca31635277e"
-  integrity sha512-2zt2g5vTXpMC3OmK6uyjvdXptbhBXfA77XGrd3gh93zwG8lZYBLOBImiGBEG0RANu3JqKEACCz5CGk73OJROBw==
+"@babel/plugin-proposal-async-generator-functions@^7.15.8":
+  version "7.15.8"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.15.8.tgz#a3100f785fab4357987c4223ab1b02b599048403"
+  integrity sha512-2Z5F2R2ibINTc63mY7FLqGfEbmofrHU9FitJW1Q7aPaKFhiPvSq6QEt/BoWN5oME3GVyjcRuNNSRbb9LC0CSWA==
   dependencies:
     "@babel/helper-plugin-utils" "^7.14.5"
     "@babel/helper-remap-async-to-generator" "^7.15.4"
@@ -899,13 +904,13 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.14.5"
 
-"@babel/plugin-transform-spread@^7.14.6":
-  version "7.14.6"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.14.6.tgz#6bd40e57fe7de94aa904851963b5616652f73144"
-  integrity sha512-Zr0x0YroFJku7n7+/HH3A2eIrGMjbmAIbJSVv0IZ+t3U2WUQUA64S/oeied2e+MaGSjmt4alzBCsK9E8gh+fag==
+"@babel/plugin-transform-spread@^7.15.8":
+  version "7.15.8"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.15.8.tgz#79d5aa27f68d700449b2da07691dfa32d2f6d468"
+  integrity sha512-/daZ8s2tNaRekl9YJa9X4bzjpeRZLt122cpgFnQPLGUe61PH8zMEBmYqKkW5xF5JUEh5buEGXJoQpqBmIbpmEQ==
   dependencies:
     "@babel/helper-plugin-utils" "^7.14.5"
-    "@babel/helper-skip-transparent-expression-wrappers" "^7.14.5"
+    "@babel/helper-skip-transparent-expression-wrappers" "^7.15.4"
 
 "@babel/plugin-transform-sticky-regex@^7.14.5":
   version "7.14.5"
@@ -943,17 +948,17 @@
     "@babel/helper-create-regexp-features-plugin" "^7.14.5"
     "@babel/helper-plugin-utils" "^7.14.5"
 
-"@babel/preset-env@^7.15.6":
-  version "7.15.6"
-  resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.15.6.tgz#0f3898db9d63d320f21b17380d8462779de57659"
-  integrity sha512-L+6jcGn7EWu7zqaO2uoTDjjMBW+88FXzV8KvrBl2z6MtRNxlsmUNRlZPaNNPUTgqhyC5DHNFk/2Jmra+ublZWw==
+"@babel/preset-env@^7.15.8":
+  version "7.15.8"
+  resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.15.8.tgz#f527ce5bcb121cd199f6b502bf23e420b3ff8dba"
+  integrity sha512-rCC0wH8husJgY4FPbHsiYyiLxSY8oMDJH7Rl6RQMknbN9oDDHhM9RDFvnGM2MgkbUJzSQB4gtuwygY5mCqGSsA==
   dependencies:
     "@babel/compat-data" "^7.15.0"
     "@babel/helper-compilation-targets" "^7.15.4"
     "@babel/helper-plugin-utils" "^7.14.5"
     "@babel/helper-validator-option" "^7.14.5"
     "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.15.4"
-    "@babel/plugin-proposal-async-generator-functions" "^7.15.4"
+    "@babel/plugin-proposal-async-generator-functions" "^7.15.8"
     "@babel/plugin-proposal-class-properties" "^7.14.5"
     "@babel/plugin-proposal-class-static-block" "^7.15.4"
     "@babel/plugin-proposal-dynamic-import" "^7.14.5"
@@ -1008,7 +1013,7 @@
     "@babel/plugin-transform-regenerator" "^7.14.5"
     "@babel/plugin-transform-reserved-words" "^7.14.5"
     "@babel/plugin-transform-shorthand-properties" "^7.14.5"
-    "@babel/plugin-transform-spread" "^7.14.6"
+    "@babel/plugin-transform-spread" "^7.15.8"
     "@babel/plugin-transform-sticky-regex" "^7.14.5"
     "@babel/plugin-transform-template-literals" "^7.14.5"
     "@babel/plugin-transform-typeof-symbol" "^7.14.5"
@@ -1017,7 +1022,7 @@
     "@babel/preset-modules" "^0.1.4"
     "@babel/types" "^7.15.6"
     babel-plugin-polyfill-corejs2 "^0.2.2"
-    babel-plugin-polyfill-corejs3 "^0.2.2"
+    babel-plugin-polyfill-corejs3 "^0.2.5"
     babel-plugin-polyfill-regenerator "^0.2.2"
     core-js-compat "^3.16.0"
     semver "^6.3.0"
@@ -1236,94 +1241,94 @@
   resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd"
   integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==
 
-"@jest/console@^27.2.3":
-  version "27.2.3"
-  resolved "https://registry.yarnpkg.com/@jest/console/-/console-27.2.3.tgz#c87fe48397dc7511089be71da93fb41869b75b7e"
-  integrity sha512-7akAz7p6T31EEYVVKxs6fKaR7CUgem22M/0TjCP7a64FIhNif2EiWcRzMkkDZbYhImG+Tz5qy9gMk2Wtl5GV1g==
+"@jest/console@^27.2.5":
+  version "27.2.5"
+  resolved "https://registry.yarnpkg.com/@jest/console/-/console-27.2.5.tgz#bddbf8d41c191f17b52bf0c9e6c0d18605e35d6e"
+  integrity sha512-smtlRF9vNKorRMCUtJ+yllIoiY8oFmfFG7xlzsAE76nKEwXNhjPOJIsc7Dv+AUitVt76t+KjIpUP9m98Crn2LQ==
   dependencies:
-    "@jest/types" "^27.2.3"
+    "@jest/types" "^27.2.5"
     "@types/node" "*"
     chalk "^4.0.0"
-    jest-message-util "^27.2.3"
-    jest-util "^27.2.3"
+    jest-message-util "^27.2.5"
+    jest-util "^27.2.5"
     slash "^3.0.0"
 
-"@jest/core@^27.2.3":
-  version "27.2.3"
-  resolved "https://registry.yarnpkg.com/@jest/core/-/core-27.2.3.tgz#b21a3ffb69bef017c4562d27689bb798c0194501"
-  integrity sha512-I+VX+X8pkw2I057swT3ufNp6V5EBeFO1dl+gvIexdV0zg1kZ+cz9CrPbWL75dYrJIInf5uWPwDwOoJCALrTxWw==
+"@jest/core@^27.2.5":
+  version "27.2.5"
+  resolved "https://registry.yarnpkg.com/@jest/core/-/core-27.2.5.tgz#854c314708cee0d892ac4f531b9129f00a21ee69"
+  integrity sha512-VR7mQ+jykHN4WO3OvusRJMk4xCa2MFLipMS+43fpcRGaYrN1KwMATfVEXif7ccgFKYGy5D1TVXTNE4mGq/KMMA==
   dependencies:
-    "@jest/console" "^27.2.3"
-    "@jest/reporters" "^27.2.3"
-    "@jest/test-result" "^27.2.3"
-    "@jest/transform" "^27.2.3"
-    "@jest/types" "^27.2.3"
+    "@jest/console" "^27.2.5"
+    "@jest/reporters" "^27.2.5"
+    "@jest/test-result" "^27.2.5"
+    "@jest/transform" "^27.2.5"
+    "@jest/types" "^27.2.5"
     "@types/node" "*"
     ansi-escapes "^4.2.1"
     chalk "^4.0.0"
     emittery "^0.8.1"
     exit "^0.1.2"
     graceful-fs "^4.2.4"
-    jest-changed-files "^27.2.3"
-    jest-config "^27.2.3"
-    jest-haste-map "^27.2.3"
-    jest-message-util "^27.2.3"
+    jest-changed-files "^27.2.5"
+    jest-config "^27.2.5"
+    jest-haste-map "^27.2.5"
+    jest-message-util "^27.2.5"
     jest-regex-util "^27.0.6"
-    jest-resolve "^27.2.3"
-    jest-resolve-dependencies "^27.2.3"
-    jest-runner "^27.2.3"
-    jest-runtime "^27.2.3"
-    jest-snapshot "^27.2.3"
-    jest-util "^27.2.3"
-    jest-validate "^27.2.3"
-    jest-watcher "^27.2.3"
+    jest-resolve "^27.2.5"
+    jest-resolve-dependencies "^27.2.5"
+    jest-runner "^27.2.5"
+    jest-runtime "^27.2.5"
+    jest-snapshot "^27.2.5"
+    jest-util "^27.2.5"
+    jest-validate "^27.2.5"
+    jest-watcher "^27.2.5"
     micromatch "^4.0.4"
-    p-each-series "^2.1.0"
     rimraf "^3.0.0"
     slash "^3.0.0"
     strip-ansi "^6.0.0"
 
-"@jest/environment@^27.2.3":
-  version "27.2.3"
-  resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-27.2.3.tgz#3ae328d778a67e027bad27541d1c09ed94312609"
-  integrity sha512-xXZk/Uhq6TTRydg4RyNawNZ82lX88r3997t5ykzQBfB3Wd+mqzSyC4XWzw4lTZJISldwn9/FunexTSGBFcvVAg==
+"@jest/environment@^27.2.5":
+  version "27.2.5"
+  resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-27.2.5.tgz#b85517ccfcec55690c82c56f5a01a3b30c5e3c84"
+  integrity sha512-XvUW3q6OUF+54SYFCgbbfCd/BKTwm5b2MGLoc2jINXQLKQDTCS2P2IrpPOtQ08WWZDGzbhAzVhOYta3J2arubg==
   dependencies:
-    "@jest/fake-timers" "^27.2.3"
-    "@jest/types" "^27.2.3"
+    "@jest/fake-timers" "^27.2.5"
+    "@jest/types" "^27.2.5"
     "@types/node" "*"
-    jest-mock "^27.2.3"
+    jest-mock "^27.2.5"
 
-"@jest/fake-timers@^27.2.3":
-  version "27.2.3"
-  resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-27.2.3.tgz#21cdef9cb9edd30c80026a0176eba58f5fbcaa67"
-  integrity sha512-A8+X35briNiabUPcLqYQY+dsvBUozX9DCa7HgJLdvRK/JPAKUpthYHjnI9y6QUYaDTqGZEo4rLf7LXE51MwP3Q==
+"@jest/fake-timers@^27.2.5":
+  version "27.2.5"
+  resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-27.2.5.tgz#0c7e5762d7bfe6e269e7b49279b097a52a42f0a0"
+  integrity sha512-ZGUb6jg7BgwY+nmO0TW10bc7z7Hl2G/UTAvmxEyZ/GgNFoa31tY9/cgXmqcxnnZ7o5Xs7RAOz3G1SKIj8IVDlg==
   dependencies:
-    "@jest/types" "^27.2.3"
+    "@jest/types" "^27.2.5"
     "@sinonjs/fake-timers" "^8.0.1"
     "@types/node" "*"
-    jest-message-util "^27.2.3"
-    jest-mock "^27.2.3"
-    jest-util "^27.2.3"
+    jest-message-util "^27.2.5"
+    jest-mock "^27.2.5"
+    jest-util "^27.2.5"
 
-"@jest/globals@^27.2.3":
-  version "27.2.3"
-  resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-27.2.3.tgz#6b8d652083d78709b243d9571457058f1c6c5fea"
-  integrity sha512-JVjQDs5z34XvFME0qHmKwWtgzRnBa/i22nfWjzlIUvkdFCzndN+JTLEWNXAgyBbGnNYuMZ8CpvgF9uhKt/cR3g==
+"@jest/globals@^27.2.5":
+  version "27.2.5"
+  resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-27.2.5.tgz#4115538f98ed6cee4051a90fdbd0854062902099"
+  integrity sha512-naRI537GM+enFVJQs6DcwGYPn/0vgJNb06zGVbzXfDfe/epDPV73hP1vqO37PqSKDeOXM2KInr6ymYbL1HTP7g==
   dependencies:
-    "@jest/environment" "^27.2.3"
-    "@jest/types" "^27.2.3"
-    expect "^27.2.3"
+    "@jest/environment" "^27.2.5"
+    "@jest/types" "^27.2.5"
+    expect "^27.2.5"
 
-"@jest/reporters@^27.2.3":
-  version "27.2.3"
-  resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-27.2.3.tgz#47c27be7c3e2069042b6fba12c1f8f62f91302db"
-  integrity sha512-zc9gQDjUAnkRQ5C0LW2u4JU9Ojqp9qc8OXQkMSmAbou6lN0mvDGEl4PG5HrZxpW4nE2FjIYyX6JAn05QT3gLbw==
+"@jest/reporters@^27.2.5":
+  version "27.2.5"
+  resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-27.2.5.tgz#65198ed1f3f4449e3f656129764dc6c5bb27ebe3"
+  integrity sha512-zYuR9fap3Q3mxQ454VWF8I6jYHErh368NwcKHWO2uy2fwByqBzRHkf9j2ekMDM7PaSTWcLBSZyd7NNxR1iHxzQ==
   dependencies:
     "@bcoe/v8-coverage" "^0.2.3"
-    "@jest/console" "^27.2.3"
-    "@jest/test-result" "^27.2.3"
-    "@jest/transform" "^27.2.3"
-    "@jest/types" "^27.2.3"
+    "@jest/console" "^27.2.5"
+    "@jest/test-result" "^27.2.5"
+    "@jest/transform" "^27.2.5"
+    "@jest/types" "^27.2.5"
+    "@types/node" "*"
     chalk "^4.0.0"
     collect-v8-coverage "^1.0.0"
     exit "^0.1.2"
@@ -1334,10 +1339,10 @@
     istanbul-lib-report "^3.0.0"
     istanbul-lib-source-maps "^4.0.0"
     istanbul-reports "^3.0.2"
-    jest-haste-map "^27.2.3"
-    jest-resolve "^27.2.3"
-    jest-util "^27.2.3"
-    jest-worker "^27.2.3"
+    jest-haste-map "^27.2.5"
+    jest-resolve "^27.2.5"
+    jest-util "^27.2.5"
+    jest-worker "^27.2.5"
     slash "^3.0.0"
     source-map "^0.6.0"
     string-length "^4.0.1"
@@ -1353,27 +1358,27 @@
     graceful-fs "^4.2.4"
     source-map "^0.6.0"
 
-"@jest/test-result@^27.2.3":
-  version "27.2.3"
-  resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-27.2.3.tgz#7d8f790186c7ec7600edc1d8781656268f038255"
-  integrity sha512-+pRxO4xSJyUxoA0ENiTq8wT+5RCFOxK4nlNY2lUes/VF33uB54GBkZeXlljZcZjuzS1Yarz4hZI/a4mBtv9jQA==
+"@jest/test-result@^27.2.5":
+  version "27.2.5"
+  resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-27.2.5.tgz#e9f73cf6cd5e2cc6eb3105339248dea211f9320e"
+  integrity sha512-ub7j3BrddxZ0BdSnM5JCF6cRZJ/7j3wgdX0+Dtwhw2Po+HKsELCiXUTvh+mgS4/89mpnU1CPhZxe2mTvuLPJJg==
   dependencies:
-    "@jest/console" "^27.2.3"
-    "@jest/types" "^27.2.3"
+    "@jest/console" "^27.2.5"
+    "@jest/types" "^27.2.5"
     "@types/istanbul-lib-coverage" "^2.0.0"
     collect-v8-coverage "^1.0.0"
 
-"@jest/test-sequencer@^27.2.3":
-  version "27.2.3"
-  resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-27.2.3.tgz#a9e376b91a64c6f5ab37f05e9d304340609125d7"
-  integrity sha512-QskUVqLU2zzRYchI2Q9I9A2xnbDqGo70WIWkKf4+tD+BAkohDxOF46Q7iYxznPiRTcoYtqttSZiNSS4rgQDxrQ==
+"@jest/test-sequencer@^27.2.5":
+  version "27.2.5"
+  resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-27.2.5.tgz#ed5ae91c00e623fb719111d58e380395e16cefbb"
+  integrity sha512-8j8fHZRfnjbbdMitMAGFKaBZ6YqvFRFJlMJzcy3v75edTOqc7RY65S9JpMY6wT260zAcL2sTQRga/P4PglCu3Q==
   dependencies:
-    "@jest/test-result" "^27.2.3"
+    "@jest/test-result" "^27.2.5"
     graceful-fs "^4.2.4"
-    jest-haste-map "^27.2.3"
-    jest-runtime "^27.2.3"
+    jest-haste-map "^27.2.5"
+    jest-runtime "^27.2.5"
 
-"@jest/transform@^27.2.3", "@jest/transform@^27.2.5":
+"@jest/transform@^27.2.5":
   version "27.2.5"
   resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-27.2.5.tgz#02b08862a56dbedddf0ba3c2eae41e049a250e29"
   integrity sha512-29lRtAHHYGALbZOx343v0zKmdOg4Sb0rsA1uSv0818bvwRhs3TyElOmTVXlrw0v1ZTqXJCAH/cmoDXimBhQOJQ==
@@ -1404,7 +1409,7 @@
     "@types/yargs" "^15.0.0"
     chalk "^3.0.0"
 
-"@jest/types@^27.0.2", "@jest/types@^27.2.3", "@jest/types@^27.2.5":
+"@jest/types@^27.0.2", "@jest/types@^27.2.5":
   version "27.2.5"
   resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.2.5.tgz#420765c052605e75686982d24b061b4cbba22132"
   integrity sha512-nmuM4VuDtCZcY+eTpw+0nvstwReMsjPoj7ZR80/BbixulhLaiX+fbv8oeLW8WZlJMcsGQsTmMKT/iTZu1Uy/lQ==
@@ -1652,6 +1657,13 @@
     hoist-non-react-statics "^3.3.0"
     redux "^4.0.0"
 
+"@types/react-transition-group@^4.4.0":
+  version "4.4.3"
+  resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.3.tgz#b0994da0a7023d67dbb4a8910a62112bc00d5688"
+  integrity sha512-fUx5muOWSYP8Bw2BUQ9M9RK9+W1XBK/7FLJ8PTQpnpTEkn0ccyMffyEQvan4C3h53gHdx7KE5Qrxi/LnUGQtdg==
+  dependencies:
+    "@types/react" "*"
+
 "@types/react@*":
   version "17.0.3"
   resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.3.tgz#ba6e215368501ac3826951eef2904574c262cc79"
@@ -2308,7 +2320,7 @@ babel-eslint@^10.1.0:
     eslint-visitor-keys "^1.0.0"
     resolve "^1.12.0"
 
-babel-jest@^27.2.3, babel-jest@^27.2.5:
+babel-jest@^27.2.5:
   version "27.2.5"
   resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.2.5.tgz#6bbbc1bb4200fe0bfd1b1fbcbe02fc62ebed16aa"
   integrity sha512-GC9pWCcitBhSuF7H3zl0mftoKizlswaF0E3qi+rPL417wKkCB0d+Sjjb0OfXvxj7gWiBf497ldgRMii68Xz+2g==
@@ -2389,7 +2401,7 @@ babel-plugin-polyfill-corejs2@^0.2.2:
     "@babel/helper-define-polyfill-provider" "^0.2.2"
     semver "^6.1.1"
 
-babel-plugin-polyfill-corejs3@^0.2.2, babel-plugin-polyfill-corejs3@^0.2.5:
+babel-plugin-polyfill-corejs3@^0.2.5:
   version "0.2.5"
   resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.2.5.tgz#2779846a16a1652244ae268b1e906ada107faf92"
   integrity sha512-ninF5MQNwAX9Z7c9ED+H2pGt1mXdP4TqzlHKyPIYmJIYz0N+++uwdM7RnJukklhzJ54Q84vA4ZJkgs7lu5vqcw==
@@ -3300,15 +3312,7 @@ copy-descriptor@^0.1.0:
   resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
   integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
 
-core-js-compat@^3.16.0:
-  version "3.16.0"
-  resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.16.0.tgz#fced4a0a534e7e02f7e084bff66c701f8281805f"
-  integrity sha512-5D9sPHCdewoUK7pSUPfTF7ZhLh8k9/CoJXWUEo+F1dZT5Z1DVgcuRqUKhjeKW+YLb8f21rTFgWwQJiNw1hoZ5Q==
-  dependencies:
-    browserslist "^4.16.6"
-    semver "7.0.0"
-
-core-js-compat@^3.16.2:
+core-js-compat@^3.16.0, core-js-compat@^3.16.2:
   version "3.18.2"
   resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.18.2.tgz#e40c266fbd613948dd8d2d2156345da8ac03c142"
   integrity sha512-25VJYCJtGjZwLguj7d66oiHfmnVw3TMOZ0zV8DyMJp/aeQ3OjR519iOOeck08HMyVVRAqXxafc2Hl+5QstJrsQ==
@@ -4629,16 +4633,16 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2:
   dependencies:
     homedir-polyfill "^1.0.1"
 
-expect@^27.2.3:
-  version "27.2.3"
-  resolved "https://registry.yarnpkg.com/expect/-/expect-27.2.3.tgz#9ce766b50c6a5f22edd07ca5510845ac8bcb0b10"
-  integrity sha512-qT+ItBIdpS2QkRzZNGFmqpV2xTjK20gftUnJ4CLmpjdGzpoEtjxb43Y80GraXLtwB+wt5kRmXURINeM3s2fQtQ==
+expect@^27.2.5:
+  version "27.2.5"
+  resolved "https://registry.yarnpkg.com/expect/-/expect-27.2.5.tgz#16154aaa60b4d9a5b0adacfea3e4d6178f4b93fd"
+  integrity sha512-ZrO0w7bo8BgGoP/bLz+HDCI+0Hfei9jUSZs5yI/Wyn9VkG9w8oJ7rHRgYj+MA7yqqFa0IwHA3flJzZtYugShJA==
   dependencies:
-    "@jest/types" "^27.2.3"
+    "@jest/types" "^27.2.5"
     ansi-styles "^5.0.0"
     jest-get-type "^27.0.6"
-    jest-matcher-utils "^27.2.3"
-    jest-message-util "^27.2.3"
+    jest-matcher-utils "^27.2.5"
+    jest-message-util "^27.2.5"
     jest-regex-util "^27.0.6"
 
 express@^4.17.1:
@@ -5574,10 +5578,10 @@ ignore@^4.0.6:
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
   integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
 
-immutable@^3.8.2:
-  version "3.8.2"
-  resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3"
-  integrity sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=
+immutable@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23"
+  integrity sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==
 
 import-cwd@^2.0.0:
   version "2.1.0"
@@ -6231,84 +6235,84 @@ istanbul-reports@^3.0.2:
     html-escaper "^2.0.0"
     istanbul-lib-report "^3.0.0"
 
-jest-changed-files@^27.2.3:
-  version "27.2.3"
-  resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-27.2.3.tgz#83c42171d87c26d5a72e8464412cc4e239c01dda"
-  integrity sha512-UiT98eMtPySry7E0RmkDTL/GyoZBvJVWZBlHpHYc3ilRLxHBUxPkbMK/bcImDJKqyKbj83EaeIpeaMXPlPQ72A==
+jest-changed-files@^27.2.5:
+  version "27.2.5"
+  resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-27.2.5.tgz#9dfd550d158260bcb6fa80aff491f5647f7daeca"
+  integrity sha512-jfnNJzF89csUKRPKJ4MwZ1SH27wTmX2xiAIHUHrsb/OYd9Jbo4/SXxJ17/nnx6RIifpthk3Y+LEeOk+/dDeGdw==
   dependencies:
-    "@jest/types" "^27.2.3"
+    "@jest/types" "^27.2.5"
     execa "^5.0.0"
     throat "^6.0.1"
 
-jest-circus@^27.2.3:
-  version "27.2.3"
-  resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-27.2.3.tgz#e46ed567b316323f0b7c12dc72cd12fe46656356"
-  integrity sha512-msCZkvudSDhUtCCEU/Dsnp5DRzX5MQGwfuRjDwhxJxjSJ0g4c3Qwhk5Q2AjFjZS9EVm4qs9fGCf+W3BU69h3pw==
+jest-circus@^27.2.5:
+  version "27.2.5"
+  resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-27.2.5.tgz#573256a6fb6e447ac2fc7e0ade9375013309037f"
+  integrity sha512-eyL9IcrAxm3Saq3rmajFCwpaxaRMGJ1KJs+7hlTDinXpJmeR3P02bheM3CYohE7UfwOBmrFMJHjgo/WPcLTM+Q==
   dependencies:
-    "@jest/environment" "^27.2.3"
-    "@jest/test-result" "^27.2.3"
-    "@jest/types" "^27.2.3"
+    "@jest/environment" "^27.2.5"
+    "@jest/test-result" "^27.2.5"
+    "@jest/types" "^27.2.5"
     "@types/node" "*"
     chalk "^4.0.0"
     co "^4.6.0"
     dedent "^0.7.0"
-    expect "^27.2.3"
+    expect "^27.2.5"
     is-generator-fn "^2.0.0"
-    jest-each "^27.2.3"
-    jest-matcher-utils "^27.2.3"
-    jest-message-util "^27.2.3"
-    jest-runtime "^27.2.3"
-    jest-snapshot "^27.2.3"
-    jest-util "^27.2.3"
-    pretty-format "^27.2.3"
+    jest-each "^27.2.5"
+    jest-matcher-utils "^27.2.5"
+    jest-message-util "^27.2.5"
+    jest-runtime "^27.2.5"
+    jest-snapshot "^27.2.5"
+    jest-util "^27.2.5"
+    pretty-format "^27.2.5"
     slash "^3.0.0"
     stack-utils "^2.0.3"
     throat "^6.0.1"
 
-jest-cli@^27.2.3:
-  version "27.2.3"
-  resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-27.2.3.tgz#68add5b1626bd5502df6c7a4a4d574ebf797221a"
-  integrity sha512-QHXxxqE1zxMlti6wIHSbkl4Brg5Dnc0xzAVqRlVa6y2Ygv2X4ejhfMjl4VB5gWeHNsVA9C+KOm8TawpjZX8d3g==
+jest-cli@^27.2.5:
+  version "27.2.5"
+  resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-27.2.5.tgz#88718c8f05f1c0f209152952ecd61afe4c3311bb"
+  integrity sha512-XzfcOXi5WQrXqFYsDxq5RDOKY4FNIgBgvgf3ZBz4e/j5/aWep5KnsAYH5OFPMdX/TP/LFsYQMRH7kzJUMh6JKg==
   dependencies:
-    "@jest/core" "^27.2.3"
-    "@jest/test-result" "^27.2.3"
-    "@jest/types" "^27.2.3"
+    "@jest/core" "^27.2.5"
+    "@jest/test-result" "^27.2.5"
+    "@jest/types" "^27.2.5"
     chalk "^4.0.0"
     exit "^0.1.2"
     graceful-fs "^4.2.4"
     import-local "^3.0.2"
-    jest-config "^27.2.3"
-    jest-util "^27.2.3"
-    jest-validate "^27.2.3"
+    jest-config "^27.2.5"
+    jest-util "^27.2.5"
+    jest-validate "^27.2.5"
     prompts "^2.0.1"
-    yargs "^16.0.3"
+    yargs "^16.2.0"
 
-jest-config@^27.2.3:
-  version "27.2.3"
-  resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-27.2.3.tgz#64606cd1f194fb9527cbbc3e4ff23b324653b992"
-  integrity sha512-15fKPBZ+eiDUj02bENeBNL6IrH9ZQg7mcOlJ+SG8HwEkjpy0K+NHAREFIJbPFBaq75syWk9SYkB77fH0XtoZOQ==
+jest-config@^27.2.5:
+  version "27.2.5"
+  resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-27.2.5.tgz#c2e4ec6ea2bf4ffd2cae3d927999fe6159cba207"
+  integrity sha512-QdENtn9b5rIIYGlbDNEcgY9LDL5kcokJnXrp7x8AGjHob/XFqw1Z6p+gjfna2sUulQsQ3ce2Fvntnv+7fKYDhQ==
   dependencies:
     "@babel/core" "^7.1.0"
-    "@jest/test-sequencer" "^27.2.3"
-    "@jest/types" "^27.2.3"
-    babel-jest "^27.2.3"
+    "@jest/test-sequencer" "^27.2.5"
+    "@jest/types" "^27.2.5"
+    babel-jest "^27.2.5"
     chalk "^4.0.0"
     deepmerge "^4.2.2"
     glob "^7.1.1"
     graceful-fs "^4.2.4"
     is-ci "^3.0.0"
-    jest-circus "^27.2.3"
-    jest-environment-jsdom "^27.2.3"
-    jest-environment-node "^27.2.3"
+    jest-circus "^27.2.5"
+    jest-environment-jsdom "^27.2.5"
+    jest-environment-node "^27.2.5"
     jest-get-type "^27.0.6"
-    jest-jasmine2 "^27.2.3"
+    jest-jasmine2 "^27.2.5"
     jest-regex-util "^27.0.6"
-    jest-resolve "^27.2.3"
-    jest-runner "^27.2.3"
-    jest-util "^27.2.3"
-    jest-validate "^27.2.3"
+    jest-resolve "^27.2.5"
+    jest-runner "^27.2.5"
+    jest-util "^27.2.5"
+    jest-validate "^27.2.5"
     micromatch "^4.0.4"
-    pretty-format "^27.2.3"
+    pretty-format "^27.2.5"
 
 jest-diff@^25.2.1:
   version "25.5.0"
@@ -6320,15 +6324,15 @@ jest-diff@^25.2.1:
     jest-get-type "^25.2.6"
     pretty-format "^25.5.0"
 
-jest-diff@^27.2.3:
-  version "27.2.3"
-  resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.2.3.tgz#4298ecc53f7476571d0625e8fda3ade13607a864"
-  integrity sha512-ihRKT1mbm/Lw+vaB1un4BEof3WdfYIXT0VLvEyLUTU3XbIUgyiljis3YzFf2RFn+ECFAeyilqJa35DeeRV2NeQ==
+jest-diff@^27.2.5:
+  version "27.2.5"
+  resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.2.5.tgz#908f7a6aca5653824516ad30e0a9fd9767e53623"
+  integrity sha512-7gfwwyYkeslOOVQY4tVq5TaQa92mWfC9COsVYMNVYyJTOYAqbIkoD3twi5A+h+tAPtAelRxkqY6/xu+jwTr0dA==
   dependencies:
     chalk "^4.0.0"
     diff-sequences "^27.0.6"
     jest-get-type "^27.0.6"
-    pretty-format "^27.2.3"
+    pretty-format "^27.2.5"
 
 jest-docblock@^27.0.6:
   version "27.0.6"
@@ -6337,41 +6341,41 @@ jest-docblock@^27.0.6:
   dependencies:
     detect-newline "^3.0.0"
 
-jest-each@^27.2.3:
-  version "27.2.3"
-  resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-27.2.3.tgz#7eaf7c7b362019f23c5a7998b57d82e78e6f6672"
-  integrity sha512-Aza5Lr+tml8x+rBGsi3A8VLqhYN1UBa2M7FLtgkUvVFQBORlV9irLl/ZE0tvk4hRqp4jW7nbGDrRo2Ey8Wl9rg==
+jest-each@^27.2.5:
+  version "27.2.5"
+  resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-27.2.5.tgz#378118d516db730b92096a9607b8711165946353"
+  integrity sha512-HUPWIbJT0bXarRwKu/m7lYzqxR4GM5EhKOsu0z3t0SKtbFN6skQhpAUADM4qFShBXb9zoOuag5lcrR1x/WM+Ag==
   dependencies:
-    "@jest/types" "^27.2.3"
+    "@jest/types" "^27.2.5"
     chalk "^4.0.0"
     jest-get-type "^27.0.6"
-    jest-util "^27.2.3"
-    pretty-format "^27.2.3"
+    jest-util "^27.2.5"
+    pretty-format "^27.2.5"
 
-jest-environment-jsdom@^27.2.3:
-  version "27.2.3"
-  resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-27.2.3.tgz#36ad673f93f1948dd5daa6dcb1c9b1ad09d60165"
-  integrity sha512-QEcgd5bloEfugjvYFACFtFkn5sW9fGYS/vJaTQZ2kj8/q1semDYWssbUWeT8Lmm/4utv9G50+bTq/vGP/LZwvQ==
+jest-environment-jsdom@^27.2.5:
+  version "27.2.5"
+  resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-27.2.5.tgz#21de3ad0e89441d961b592ba7561b16241279208"
+  integrity sha512-QtRpOh/RQKuXniaWcoFE2ElwP6tQcyxHu0hlk32880g0KczdonCs5P1sk5+weu/OVzh5V4Bt1rXuQthI01mBLg==
   dependencies:
-    "@jest/environment" "^27.2.3"
-    "@jest/fake-timers" "^27.2.3"
-    "@jest/types" "^27.2.3"
+    "@jest/environment" "^27.2.5"
+    "@jest/fake-timers" "^27.2.5"
+    "@jest/types" "^27.2.5"
     "@types/node" "*"
-    jest-mock "^27.2.3"
-    jest-util "^27.2.3"
+    jest-mock "^27.2.5"
+    jest-util "^27.2.5"
     jsdom "^16.6.0"
 
-jest-environment-node@^27.2.3:
-  version "27.2.3"
-  resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-27.2.3.tgz#651b15f52310b12660a5fd53812b8b2e696ee9b9"
-  integrity sha512-OmxFyQ81n1pQ+WJW7tOkGPQL/nt0+UeubHlZJEdAzuOvYAA8zleamw0BpK7QsITdJ5euSI6t/HW3a5ihqMB4yQ==
+jest-environment-node@^27.2.5:
+  version "27.2.5"
+  resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-27.2.5.tgz#ffa1afb3604c640ec841f044d526c65912e02cef"
+  integrity sha512-0o1LT4grm7iwrS8fIoLtwJxb/hoa3GsH7pP10P02Jpj7Mi4BXy65u46m89vEM2WfD1uFJQ2+dfDiWZNA2e6bJg==
   dependencies:
-    "@jest/environment" "^27.2.3"
-    "@jest/fake-timers" "^27.2.3"
-    "@jest/types" "^27.2.3"
+    "@jest/environment" "^27.2.5"
+    "@jest/fake-timers" "^27.2.5"
+    "@jest/types" "^27.2.5"
     "@types/node" "*"
-    jest-mock "^27.2.3"
-    jest-util "^27.2.3"
+    jest-mock "^27.2.5"
+    jest-util "^27.2.5"
 
 jest-get-type@^25.2.6:
   version "25.2.6"
@@ -6383,26 +6387,6 @@ jest-get-type@^27.0.6:
   resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.0.6.tgz#0eb5c7f755854279ce9b68a9f1a4122f69047cfe"
   integrity sha512-XTkK5exIeUbbveehcSR8w0bhH+c0yloW/Wpl+9vZrjzztCPWrxhHwkIFpZzCt71oRBsgxmuUfxEqOYoZI2macg==
 
-jest-haste-map@^27.2.3:
-  version "27.2.3"
-  resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.2.3.tgz#cec807c59c312872f0ea4cc1b6b5ca7b46131705"
-  integrity sha512-5KE0vRSGv1Ymhd6s1t6xhTm/77otdkzqJl+9pSIYfKKCKJ7cniyE2zVC/Xj2HKuMX++aJYzQvQCIS0kqIFukAw==
-  dependencies:
-    "@jest/types" "^27.2.3"
-    "@types/graceful-fs" "^4.1.2"
-    "@types/node" "*"
-    anymatch "^3.0.3"
-    fb-watchman "^2.0.0"
-    graceful-fs "^4.2.4"
-    jest-regex-util "^27.0.6"
-    jest-serializer "^27.0.6"
-    jest-util "^27.2.3"
-    jest-worker "^27.2.3"
-    micromatch "^4.0.4"
-    walker "^1.0.7"
-  optionalDependencies:
-    fsevents "^2.3.2"
-
 jest-haste-map@^27.2.5:
   version "27.2.5"
   resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.2.5.tgz#0247b7299250643472bbcf5b4ad85c72d5178e2e"
@@ -6423,69 +6407,69 @@ jest-haste-map@^27.2.5:
   optionalDependencies:
     fsevents "^2.3.2"
 
-jest-jasmine2@^27.2.3:
-  version "27.2.3"
-  resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-27.2.3.tgz#19fe549b7e86128cd7d0d1668ebf4377dd3981f9"
-  integrity sha512-pjgANGYj1l6qxBkSPEYuxGvqVVf20uJ26XpNnYV/URC7ayt+UdRavUhEwzDboiewq/lCgNFCDBEqd6eeQVEs8w==
+jest-jasmine2@^27.2.5:
+  version "27.2.5"
+  resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-27.2.5.tgz#baaf96c69913c52bce0100000cf0721027c0fd66"
+  integrity sha512-hdxY9Cm/CjLqu2tXeAoQHPgA4vcqlweVXYOg1+S9FeFdznB9Rti+eEBKDDkmOy9iqr4Xfbq95OkC4NFbXXPCAQ==
   dependencies:
     "@babel/traverse" "^7.1.0"
-    "@jest/environment" "^27.2.3"
+    "@jest/environment" "^27.2.5"
     "@jest/source-map" "^27.0.6"
-    "@jest/test-result" "^27.2.3"
-    "@jest/types" "^27.2.3"
+    "@jest/test-result" "^27.2.5"
+    "@jest/types" "^27.2.5"
     "@types/node" "*"
     chalk "^4.0.0"
     co "^4.6.0"
-    expect "^27.2.3"
+    expect "^27.2.5"
     is-generator-fn "^2.0.0"
-    jest-each "^27.2.3"
-    jest-matcher-utils "^27.2.3"
-    jest-message-util "^27.2.3"
-    jest-runtime "^27.2.3"
-    jest-snapshot "^27.2.3"
-    jest-util "^27.2.3"
-    pretty-format "^27.2.3"
+    jest-each "^27.2.5"
+    jest-matcher-utils "^27.2.5"
+    jest-message-util "^27.2.5"
+    jest-runtime "^27.2.5"
+    jest-snapshot "^27.2.5"
+    jest-util "^27.2.5"
+    pretty-format "^27.2.5"
     throat "^6.0.1"
 
-jest-leak-detector@^27.2.3:
-  version "27.2.3"
-  resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-27.2.3.tgz#6c60a795fe9b07442c604140373571559d4d467d"
-  integrity sha512-hoV8d7eJvayIaPrISBoLaMN0DE+GRSR2/vbAcOONffO+RYzbuW3klsOievx+pCShYKxSKlhxxO90zWice+LLew==
+jest-leak-detector@^27.2.5:
+  version "27.2.5"
+  resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-27.2.5.tgz#e2edc3b37d38e8d9a527e10e456b403c3151b206"
+  integrity sha512-HYsi3GUR72bYhOGB5C5saF9sPdxGzSjX7soSQS+BqDRysc7sPeBwPbhbuT8DnOpijnKjgwWQ8JqvbmReYnt3aQ==
   dependencies:
     jest-get-type "^27.0.6"
-    pretty-format "^27.2.3"
+    pretty-format "^27.2.5"
 
-jest-matcher-utils@^27.2.3:
-  version "27.2.3"
-  resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.2.3.tgz#db7f992f3921f5004f4de36aafa0c03f2565122a"
-  integrity sha512-8n2/iAEOtNoDxVtUuaGtQdbSVYtZn6saT+PyV8UIf9fJErzDdozjB4fUxJm7TX1DzhhoAKFpIFH8UNvG4942PA==
+jest-matcher-utils@^27.2.5:
+  version "27.2.5"
+  resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.2.5.tgz#4684faaa8eb32bf15e6edaead6834031897e2980"
+  integrity sha512-qNR/kh6bz0Dyv3m68Ck2g1fLW5KlSOUNcFQh87VXHZwWc/gY6XwnKofx76Qytz3x5LDWT09/2+yXndTkaG4aWg==
   dependencies:
     chalk "^4.0.0"
-    jest-diff "^27.2.3"
+    jest-diff "^27.2.5"
     jest-get-type "^27.0.6"
-    pretty-format "^27.2.3"
+    pretty-format "^27.2.5"
 
-jest-message-util@^27.2.3:
-  version "27.2.3"
-  resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-27.2.3.tgz#cd1091a3f0f3ff919756b15cfccc0ba43eeeeff0"
-  integrity sha512-yjVqTQ2Ds1WCGXsTuW0m1uK8RXOE44SJDw7tWUrhn6ZttWDbPmLhH8npDsGGfAmSayKFSo2C0NX0tP2qblc3Gw==
+jest-message-util@^27.2.5:
+  version "27.2.5"
+  resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-27.2.5.tgz#ed8b7b0965247bb875a49c1f9b9ab2d1d0820028"
+  integrity sha512-ggXSLoPfIYcbmZ8glgEJZ8b+e0Msw/iddRmgkoO7lDAr9SmI65IIfv7VnvTnV4FGnIIUIjzM+fHRHO5RBvyAbQ==
   dependencies:
     "@babel/code-frame" "^7.12.13"
-    "@jest/types" "^27.2.3"
+    "@jest/types" "^27.2.5"
     "@types/stack-utils" "^2.0.0"
     chalk "^4.0.0"
     graceful-fs "^4.2.4"
     micromatch "^4.0.4"
-    pretty-format "^27.2.3"
+    pretty-format "^27.2.5"
     slash "^3.0.0"
     stack-utils "^2.0.3"
 
-jest-mock@^27.2.3:
-  version "27.2.3"
-  resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-27.2.3.tgz#f532d2c8c158e8b899f2a0a5bd3077af37619c29"
-  integrity sha512-IvgCdUQBU/XDJl9/NLYtKG9o2XlJOQ8hFYDiX7QmNv2195Y1nNGM7hw1H58wT01zz7bohfhJplqwFfULZlrXjg==
+jest-mock@^27.2.5:
+  version "27.2.5"
+  resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-27.2.5.tgz#0ec38d5ff1e49c4802e7a4a8179e8d7a2fd84de0"
+  integrity sha512-HiMB3LqE9RzmeMzZARi2Bz3NoymxyP0gCid4y42ca1djffNtYFKgI220aC1VP1mUZ8rbpqZbHZOJ15093bZV/Q==
   dependencies:
-    "@jest/types" "^27.2.3"
+    "@jest/types" "^27.2.5"
     "@types/node" "*"
 
 jest-pnp-resolver@^1.2.2:
@@ -6498,72 +6482,72 @@ jest-regex-util@^27.0.6:
   resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.0.6.tgz#02e112082935ae949ce5d13b2675db3d8c87d9c5"
   integrity sha512-SUhPzBsGa1IKm8hx2F4NfTGGp+r7BXJ4CulsZ1k2kI+mGLG+lxGrs76veN2LF/aUdGosJBzKgXmNCw+BzFqBDQ==
 
-jest-resolve-dependencies@^27.2.3:
-  version "27.2.3"
-  resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-27.2.3.tgz#fcb620684108fe7a099185052434d26f17de98e6"
-  integrity sha512-H03NyzmKfYHCciaYBJqbJOrWCVCdwdt32xZDPFP5dBbe39wsfz41aOkhw8FUZ6qVYVO6rz0nLZ3G7wgbsQQsYQ==
+jest-resolve-dependencies@^27.2.5:
+  version "27.2.5"
+  resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-27.2.5.tgz#fcd8eca005b3d11ba32da443045c028164b83be1"
+  integrity sha512-BSjefped31bcvvCh++/pN9ueqqN1n0+p8/58yScuWfklLm2tbPbS9d251vJhAy0ZI2pL/0IaGhOTJrs9Y4FJlg==
   dependencies:
-    "@jest/types" "^27.2.3"
+    "@jest/types" "^27.2.5"
     jest-regex-util "^27.0.6"
-    jest-snapshot "^27.2.3"
+    jest-snapshot "^27.2.5"
 
-jest-resolve@^27.2.3:
-  version "27.2.3"
-  resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-27.2.3.tgz#868911cf705c537f433befcfc65e6ddc70c9d7f9"
-  integrity sha512-+tbm53gKpwuRsqCV+zhhjq/6NxMs/I9zECEMzu0LtmbYD5Gusj+rU497f6lkl5LG/GndvfTjJlysYrnSCcZUJA==
+jest-resolve@^27.2.5:
+  version "27.2.5"
+  resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-27.2.5.tgz#04dadbfc1312a2541f5c199c5011945e9cfe5cef"
+  integrity sha512-q5irwS3oS73SKy3+FM/HL2T7WJftrk9BRzrXF92f7net5HMlS7lJMg/ZwxLB4YohKqjSsdksEw7n/jvMxV7EKg==
   dependencies:
-    "@jest/types" "^27.2.3"
+    "@jest/types" "^27.2.5"
     chalk "^4.0.0"
     escalade "^3.1.1"
     graceful-fs "^4.2.4"
-    jest-haste-map "^27.2.3"
+    jest-haste-map "^27.2.5"
     jest-pnp-resolver "^1.2.2"
-    jest-util "^27.2.3"
-    jest-validate "^27.2.3"
+    jest-util "^27.2.5"
+    jest-validate "^27.2.5"
     resolve "^1.20.0"
     slash "^3.0.0"
 
-jest-runner@^27.2.3:
-  version "27.2.3"
-  resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-27.2.3.tgz#22f6ef7bd4140fec74cec18eef29c24d07cb6ad5"
-  integrity sha512-bvGlIh3wR/LGjSHPW/IpQU6K2atO45U5p7UDqWThPKT622Wm/ZJ2DNbgNzb4P9ZO/UxB22jXoKJPsMAdWGEdmA==
+jest-runner@^27.2.5:
+  version "27.2.5"
+  resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-27.2.5.tgz#3d9d0626f351480bb2cffcfbbfac240c0097ebd4"
+  integrity sha512-n41vw9RLg5TKAnEeJK9d6pGOsBOpwE89XBniK+AD1k26oIIy3V7ogM1scbDjSheji8MUPC9pNgCrZ/FHLVDNgg==
   dependencies:
-    "@jest/console" "^27.2.3"
-    "@jest/environment" "^27.2.3"
-    "@jest/test-result" "^27.2.3"
-    "@jest/transform" "^27.2.3"
-    "@jest/types" "^27.2.3"
+    "@jest/console" "^27.2.5"
+    "@jest/environment" "^27.2.5"
+    "@jest/test-result" "^27.2.5"
+    "@jest/transform" "^27.2.5"
+    "@jest/types" "^27.2.5"
     "@types/node" "*"
     chalk "^4.0.0"
     emittery "^0.8.1"
     exit "^0.1.2"
     graceful-fs "^4.2.4"
     jest-docblock "^27.0.6"
-    jest-environment-jsdom "^27.2.3"
-    jest-environment-node "^27.2.3"
-    jest-haste-map "^27.2.3"
-    jest-leak-detector "^27.2.3"
-    jest-message-util "^27.2.3"
-    jest-resolve "^27.2.3"
-    jest-runtime "^27.2.3"
-    jest-util "^27.2.3"
-    jest-worker "^27.2.3"
+    jest-environment-jsdom "^27.2.5"
+    jest-environment-node "^27.2.5"
+    jest-haste-map "^27.2.5"
+    jest-leak-detector "^27.2.5"
+    jest-message-util "^27.2.5"
+    jest-resolve "^27.2.5"
+    jest-runtime "^27.2.5"
+    jest-util "^27.2.5"
+    jest-worker "^27.2.5"
     source-map-support "^0.5.6"
     throat "^6.0.1"
 
-jest-runtime@^27.2.3:
-  version "27.2.3"
-  resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-27.2.3.tgz#e6fc25bbbc63b19fae50c3994060efb1b2922b7e"
-  integrity sha512-8WPgxENQchmUM0jpDjK1IxacseK9vDDz6T471xs5pNIQrj8typeT0coRigRCb1sPYeXQ66SqVERMgPj6SEeblQ==
+jest-runtime@^27.2.5:
+  version "27.2.5"
+  resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-27.2.5.tgz#d144c3f6889b927aae1e695b63a41a3323b7016b"
+  integrity sha512-N0WRZ3QszKyZ3Dm27HTBbBuestsSd3Ud5ooVho47XZJ8aSKO/X1Ag8M1dNx9XzfGVRNdB/xCA3lz8MJwIzPLLA==
   dependencies:
-    "@jest/console" "^27.2.3"
-    "@jest/environment" "^27.2.3"
-    "@jest/fake-timers" "^27.2.3"
-    "@jest/globals" "^27.2.3"
+    "@jest/console" "^27.2.5"
+    "@jest/environment" "^27.2.5"
+    "@jest/fake-timers" "^27.2.5"
+    "@jest/globals" "^27.2.5"
     "@jest/source-map" "^27.0.6"
-    "@jest/test-result" "^27.2.3"
-    "@jest/transform" "^27.2.3"
-    "@jest/types" "^27.2.3"
+    "@jest/test-result" "^27.2.5"
+    "@jest/transform" "^27.2.5"
+    "@jest/types" "^27.2.5"
     "@types/yargs" "^16.0.0"
     chalk "^4.0.0"
     cjs-module-lexer "^1.0.0"
@@ -6572,17 +6556,17 @@ jest-runtime@^27.2.3:
     exit "^0.1.2"
     glob "^7.1.3"
     graceful-fs "^4.2.4"
-    jest-haste-map "^27.2.3"
-    jest-message-util "^27.2.3"
-    jest-mock "^27.2.3"
+    jest-haste-map "^27.2.5"
+    jest-message-util "^27.2.5"
+    jest-mock "^27.2.5"
     jest-regex-util "^27.0.6"
-    jest-resolve "^27.2.3"
-    jest-snapshot "^27.2.3"
-    jest-util "^27.2.3"
-    jest-validate "^27.2.3"
+    jest-resolve "^27.2.5"
+    jest-snapshot "^27.2.5"
+    jest-util "^27.2.5"
+    jest-validate "^27.2.5"
     slash "^3.0.0"
     strip-bom "^4.0.0"
-    yargs "^16.0.3"
+    yargs "^16.2.0"
 
 jest-serializer@^27.0.6:
   version "27.0.6"
@@ -6592,10 +6576,10 @@ jest-serializer@^27.0.6:
     "@types/node" "*"
     graceful-fs "^4.2.4"
 
-jest-snapshot@^27.2.3:
-  version "27.2.3"
-  resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-27.2.3.tgz#e3f39e1708a4d93dfa1297e73b5d2feec44f6d0c"
-  integrity sha512-NJz+PNvTNTxVfNdLXccKUMeVH5O7jZ+9dNXH5TP2WtkLR+CiPRiPveWDgM8o3aaxB6R0Mm8vsD7ieEkEh6ZBBQ==
+jest-snapshot@^27.2.5:
+  version "27.2.5"
+  resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-27.2.5.tgz#8a612fe31e2967f58ad364542198dff61f92ef32"
+  integrity sha512-2/Jkn+VN6Abwz0llBltZaiJMnL8b1j5Bp/gRIxe9YR3FCEh9qp0TXVV0dcpTGZ8AcJV1SZGQkczewkI9LP5yGw==
   dependencies:
     "@babel/core" "^7.7.2"
     "@babel/generator" "^7.7.2"
@@ -6603,37 +6587,25 @@ jest-snapshot@^27.2.3:
     "@babel/plugin-syntax-typescript" "^7.7.2"
     "@babel/traverse" "^7.7.2"
     "@babel/types" "^7.0.0"
-    "@jest/transform" "^27.2.3"
-    "@jest/types" "^27.2.3"
+    "@jest/transform" "^27.2.5"
+    "@jest/types" "^27.2.5"
     "@types/babel__traverse" "^7.0.4"
     "@types/prettier" "^2.1.5"
     babel-preset-current-node-syntax "^1.0.0"
     chalk "^4.0.0"
-    expect "^27.2.3"
+    expect "^27.2.5"
     graceful-fs "^4.2.4"
-    jest-diff "^27.2.3"
+    jest-diff "^27.2.5"
     jest-get-type "^27.0.6"
-    jest-haste-map "^27.2.3"
-    jest-matcher-utils "^27.2.3"
-    jest-message-util "^27.2.3"
-    jest-resolve "^27.2.3"
-    jest-util "^27.2.3"
+    jest-haste-map "^27.2.5"
+    jest-matcher-utils "^27.2.5"
+    jest-message-util "^27.2.5"
+    jest-resolve "^27.2.5"
+    jest-util "^27.2.5"
     natural-compare "^1.4.0"
-    pretty-format "^27.2.3"
+    pretty-format "^27.2.5"
     semver "^7.3.2"
 
-jest-util@^27.2.3:
-  version "27.2.3"
-  resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.2.3.tgz#f766354b7c489c1f9ea88cd1d96d044fbd2b5d4d"
-  integrity sha512-78BEka2+77lqD7LN4mSzUdZMngHZtVAsmZ5B8+qOWfN4bCYNUmi/eGNLm91jA77gG1QZJSXsDOCWB0qbXDT1Fw==
-  dependencies:
-    "@jest/types" "^27.2.3"
-    "@types/node" "*"
-    chalk "^4.0.0"
-    graceful-fs "^4.2.4"
-    is-ci "^3.0.0"
-    picomatch "^2.2.3"
-
 jest-util@^27.2.5:
   version "27.2.5"
   resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.2.5.tgz#88740c4024d223634a82ce7c2263e8bc6df3b3ba"
@@ -6646,29 +6618,29 @@ jest-util@^27.2.5:
     is-ci "^3.0.0"
     picomatch "^2.2.3"
 
-jest-validate@^27.2.3:
-  version "27.2.3"
-  resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-27.2.3.tgz#4fcc49e581f13fbe260a77e711a80f0256138a7a"
-  integrity sha512-HUfTZ/W87zoxOuEGC01ujXzoLzRpJqvhMdIrRilpXGmso2vJWw3bHpbWKhivYMr0X/BjitLrHywj/+niNfIcEA==
+jest-validate@^27.2.5:
+  version "27.2.5"
+  resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-27.2.5.tgz#2d59bf1627d180f395ba58f24599b0ee0efcfbdf"
+  integrity sha512-XgYtjS89nhVe+UfkbLgcm+GgXKWgL80t9nTcNeejyO3t0Sj/yHE8BtIJqjZu9NXQksYbGImoQRXmQ1gP+Guffw==
   dependencies:
-    "@jest/types" "^27.2.3"
+    "@jest/types" "^27.2.5"
     camelcase "^6.2.0"
     chalk "^4.0.0"
     jest-get-type "^27.0.6"
     leven "^3.1.0"
-    pretty-format "^27.2.3"
+    pretty-format "^27.2.5"
 
-jest-watcher@^27.2.3:
-  version "27.2.3"
-  resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-27.2.3.tgz#2989228bdd05138094f7ec19a23cbb2665f2efb7"
-  integrity sha512-SvUmnL/QMb55B6iWJ3Jpq6bG2fSRcrMaGakY60i6j8p9+Ct42mpkq90qaYB+rnSLaiW/QQN+lTJZmK+lA6vksA==
+jest-watcher@^27.2.5:
+  version "27.2.5"
+  resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-27.2.5.tgz#41cd3e64dc5bea8a4327083d71ba7667be400567"
+  integrity sha512-umV4qGozg2Dn6DTTtqAh9puPw+DGLK9AQas7+mWjiK8t0fWMpxKg8ZXReZw7L4C88DqorsGUiDgwHNZ+jkVrkQ==
   dependencies:
-    "@jest/test-result" "^27.2.3"
-    "@jest/types" "^27.2.3"
+    "@jest/test-result" "^27.2.5"
+    "@jest/types" "^27.2.5"
     "@types/node" "*"
     ansi-escapes "^4.2.1"
     chalk "^4.0.0"
-    jest-util "^27.2.3"
+    jest-util "^27.2.5"
     string-length "^4.0.1"
 
 jest-worker@^26.5.0:
@@ -6680,15 +6652,6 @@ jest-worker@^26.5.0:
     merge-stream "^2.0.0"
     supports-color "^7.0.0"
 
-jest-worker@^27.2.3:
-  version "27.2.3"
-  resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.2.3.tgz#396e83d04ca575230a9bcb255c2b66aec07cb931"
-  integrity sha512-ZwOvv4GCIPviL+Ie4pVguz4N5w/6IGbTaHBYOl3ZcsZZktaL7d8JOU0rmovoED7AJZKA8fvmLbBg8yg80u/tGA==
-  dependencies:
-    "@types/node" "*"
-    merge-stream "^2.0.0"
-    supports-color "^8.0.0"
-
 jest-worker@^27.2.5:
   version "27.2.5"
   resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.2.5.tgz#ed42865661959488aa020e8a325df010597c36d4"
@@ -6698,14 +6661,14 @@ jest-worker@^27.2.5:
     merge-stream "^2.0.0"
     supports-color "^8.0.0"
 
-jest@^27.2.3:
-  version "27.2.3"
-  resolved "https://registry.yarnpkg.com/jest/-/jest-27.2.3.tgz#9c2af9ce874a3eb202f83d92fbc1cc61ccc73248"
-  integrity sha512-r4ggA29J5xUg93DpvbsX+AXlFMWE3hZ5Y6BfgTl8PJvWelVezNPkmrsixuGoDBTHTCwScRSH0O4wsoeUgLie2w==
+jest@^27.2.5:
+  version "27.2.5"
+  resolved "https://registry.yarnpkg.com/jest/-/jest-27.2.5.tgz#7d8a5c8781a160f693beeb7c68e46c16ef948148"
+  integrity sha512-vDMzXcpQN4Ycaqu+vO7LX8pZwNNoKMhc+gSp6q1D8S6ftRk8gNW8cni3YFxknP95jxzQo23Lul0BI2FrWgnwYQ==
   dependencies:
-    "@jest/core" "^27.2.3"
+    "@jest/core" "^27.2.5"
     import-local "^3.0.2"
-    jest-cli "^27.2.3"
+    jest-cli "^27.2.5"
 
 js-base64@^2.1.9:
   version "2.6.4"
@@ -7873,11 +7836,6 @@ os-homedir@^1.0.0:
   resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
   integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
 
-p-each-series@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.1.0.tgz#961c8dd3f195ea96c747e636b262b800a6b1af48"
-  integrity sha512-ZuRs1miPT4HrjFa+9fRfOFXxGJfORgelKV9f9nNOWw2gl6gVsRaVDOQP0+MI0G0wGKns1Yacsu0GjOFbTK0JFQ==
-
 p-finally@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
@@ -8705,12 +8663,12 @@ pretty-format@^27.0.2:
     ansi-styles "^5.0.0"
     react-is "^17.0.1"
 
-pretty-format@^27.2.3:
-  version "27.2.3"
-  resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.2.3.tgz#c76710de6ebd8b1b412a5668bacf4a6c2f21a029"
-  integrity sha512-wvg2HzuGKKEE/nKY4VdQ/LM8w8pRZvp0XpqhwgaZBbjTwd5UdF2I4wvwZjyUwu8G+HI6g4t6u9b2FZlKhlzxcQ==
+pretty-format@^27.2.5:
+  version "27.2.5"
+  resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.2.5.tgz#7cfe2a8e8f01a5b5b29296a0b70f4140df0830c5"
+  integrity sha512-+nYn2z9GgicO9JiqmY25Xtq8SYfZ/5VCpEU3pppHHNAhd1y+ZXxmNPd1evmNcAd6Hz4iBV2kf0UpGth5A/VJ7g==
   dependencies:
-    "@jest/types" "^27.2.3"
+    "@jest/types" "^27.2.5"
     ansi-regex "^5.0.1"
     ansi-styles "^5.0.0"
     react-is "^17.0.1"
@@ -8960,13 +8918,6 @@ react-infinite-scroller@^1.0.12:
   dependencies:
     prop-types "^15.5.8"
 
-react-input-autosize@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-3.0.0.tgz#6b5898c790d4478d69420b55441fcc31d5c50a85"
-  integrity sha512-nL9uS7jEs/zu8sqwFE5MAPx6pPkNAriACQ2rGLlqmKr2sPGtN7TXTyDdQt4lbNXVx7Uzadb40x8qotIuru6Rhg==
-  dependencies:
-    prop-types "^15.5.8"
-
 react-intl-translations-manager@^5.0.3:
   version "5.0.3"
   resolved "https://registry.yarnpkg.com/react-intl-translations-manager/-/react-intl-translations-manager-5.0.3.tgz#aee010ecf35975673e033ca5d7d3f4147894324d"
@@ -9093,17 +9044,17 @@ react-router@^4.3.1:
     prop-types "^15.6.1"
     warning "^4.0.1"
 
-react-select@^4.3.1:
-  version "4.3.1"
-  resolved "https://registry.yarnpkg.com/react-select/-/react-select-4.3.1.tgz#389fc07c9bc7cf7d3c377b7a05ea18cd7399cb81"
-  integrity sha512-HBBd0dYwkF5aZk1zP81Wx5UsLIIT2lSvAY2JiJo199LjoLHoivjn9//KsmvQMEFGNhe58xyuOITjfxKCcGc62Q==
+react-select@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.1.0.tgz#ac384c8e25ba6f03126026192b2bdad0f53fbf50"
+  integrity sha512-SkEBD1AYsSXrIdNj5HBt7+Ehe+jxdiB448J0atJqR6lE3l/GcFlRf4JYB3NlHe/02jrW4AnIQLo1t0IqWrxXOw==
   dependencies:
     "@babel/runtime" "^7.12.0"
     "@emotion/cache" "^11.4.0"
     "@emotion/react" "^11.1.1"
+    "@types/react-transition-group" "^4.4.0"
     memoize-one "^5.0.0"
     prop-types "^15.6.0"
-    react-input-autosize "^3.0.0"
     react-transition-group "^4.3.0"
 
 react-sparklines@^1.7.0:
@@ -11582,7 +11533,7 @@ yargs@^13.3.2:
     y18n "^4.0.0"
     yargs-parser "^13.1.2"
 
-yargs@^16.0.3:
+yargs@^16.2.0:
   version "16.2.0"
   resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
   integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==