From 5ae395ba9eb8f9d7d2e9c838e39319834101d897 Mon Sep 17 00:00:00 2001
From: Daniel Siepmann <daniel.siepmann@typo3.org>
Date: Fri, 6 Mar 2020 12:25:31 +0100
Subject: [PATCH] [!!!][TASK] Allow creation of widgets through configuration
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Provide concrete classes that will be instantiated multiple times
with different options. E.g. provide an RssWidget implementation, that
can be used multiple times with different rss feed urls.

Split configuration for UI (title, icon, …) from concrete
implementation. A new WidgetConfigurationInterface is in place and used.

Resolves: #90660
Resolves: #90852
Releases: master
Change-Id: I194615164b7d3d1cbd63751ddfd1f66f71db64cf
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/63563
Tested-by: Koen Wouters <koen.wouters@maxserv.com>
Tested-by: Richard Haeser <richard@maxserv.com>
Tested-by: Riny van Tiggelen <info@online-gamer.nl>
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Susanne Moog <look@susi.dev>
Reviewed-by: Richard Haeser <richard@maxserv.com>
Reviewed-by: Oliver Bartsch <bo@cedev.de>
Reviewed-by: Susanne Moog <look@susi.dev>
---
 Build/Sources/Sass/dashboard/_grid.scss       |  10 +-
 ...ing-90660-RegistrationOfWidgetsChanged.rst | 138 ++++++++++
 .../Controller/DashboardController.php        | 217 ++--------------
 .../Controller/WidgetAjaxController.php       |  13 +-
 typo3/sysext/dashboard/Classes/Dashboard.php  |   7 +-
 .../DashboardInitializationService.php        | 235 ++++++++++++++++++
 .../DashboardWidgetPass.php                   |  75 +++++-
 .../dashboard/Classes/ServiceProvider.php     |  26 --
 .../Classes/Utility/ButtonUtility.php         |  34 +++
 .../dashboard/Classes/Views/Factory.php       |  30 +++
 typo3/sysext/dashboard/Classes/WidgetApi.php  |  38 +++
 .../WidgetGroupInitializationService.php      |  73 ++++++
 .../dashboard/Classes/WidgetRegistry.php      |  53 ++--
 .../Widgets/AbstractBarChartWidget.php        |  61 -----
 .../Classes/Widgets/AbstractChartWidget.php   | 174 -------------
 .../Widgets/AbstractCtaButtonWidget.php       |  75 ------
 .../Widgets/AbstractDoughnutChartWidget.php   |  52 ----
 .../Classes/Widgets/AbstractListWidget.php    |  75 ------
 .../Widgets/AbstractNumberWithIconWidget.php  |  68 -----
 .../Classes/Widgets/AbstractRssWidget.php     | 105 --------
 .../Classes/Widgets/AbstractWidget.php        | 164 ------------
 .../Classes/Widgets/BarChartWidget.php        | 135 ++++++++++
 .../dashboard/Classes/Widgets/CtaWidget.php   |  81 ++++++
 .../DocumentationGettingStartedWidget.php     |  53 ----
 .../DocumentationTSconfigReferenceWidget.php  |  53 ----
 .../DocumentationTypoScriptReference.php      |  53 ----
 .../Classes/Widgets/DoughnutChartWidget.php   | 121 +++++++++
 .../Interfaces/AdditionalCssInterface.php     |  15 +-
 .../AdditionalJavaScriptInterface.php         |  15 +-
 .../Interfaces/ButtonProviderInterface.php    |  45 ++++
 .../Interfaces/ChartDataProviderInterface.php |  34 +++
 .../Widgets/Interfaces/EventDataInterface.php |  15 +-
 .../Interfaces/ListDataProviderInterface.php  |  30 +++
 .../NumberWithIconDataProviderInterface.php   |  29 +++
 .../Interfaces/RequireJsModuleInterface.php   |  15 +-
 .../WidgetConfigurationInterface.php          |  87 +++++++
 .../Widgets/Interfaces/WidgetInterface.php    |  57 ++---
 .../dashboard/Classes/Widgets/ListWidget.php  | 106 ++++++++
 .../Classes/Widgets/NumberWithIconWidget.php  |  88 +++++++
 .../Widgets/Provider/ButtonProvider.php       |  63 +++++
 .../NumberOfFailedLoginsDataProvider.php}     |  36 +--
 .../Widgets/Provider/SysLogButtonProvider.php |  68 +++++
 .../SysLogErrorsDataProvider.php}             |  71 ++----
 .../TypeOfUsersChartDataProvider.php}         |  32 +--
 .../dashboard/Classes/Widgets/RssWidget.php   | 144 +++++++++++
 ...ion.php => T3GeneralInformationWidget.php} |  51 ++--
 .../Classes/Widgets/T3NewsWidget.php          |  53 ----
 .../Widgets/T3SecurityAdvisoriesWidget.php    |  53 ----
 .../Classes/Widgets/WidgetConfiguration.php   | 144 +++++++++++
 .../Backend/DashboardWidgets.yaml             | 208 ++++++++++++++++
 .../dashboard/Configuration/Services.yaml     |  67 +----
 .../Private/Layouts/Widget/Widget.html        |   2 +-
 .../Private/Templates/Dashboard/Main.html     |   2 +-
 .../Private/Templates/Widget/ChartWidget.html |   2 +-
 .../Private/Templates/Widget/CtaWidget.html   |  17 +-
 .../Private/Templates/Widget/ListWidget.html  |   2 +-
 .../Widget/NumberWithIconWidget.html          |   4 +-
 .../Private/Templates/Widget/RssWidget.html   |   8 +-
 ...n.html => T3GeneralInformationWidget.html} |   0
 .../Resources/Public/Css/dashboard.css        |   2 +-
 .../DashboardWidgetPassTest.php               | 226 +++++++++++++++++
 .../Tests/Unit/WidgetRegistryTest.php         | 232 +++++++++++++++--
 .../ExtensionScanner/Php/ClassNameMatcher.php |  65 +++++
 63 files changed, 2722 insertions(+), 1585 deletions(-)
 create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Breaking-90660-RegistrationOfWidgetsChanged.rst
 create mode 100644 typo3/sysext/dashboard/Classes/DashboardInitializationService.php
 create mode 100644 typo3/sysext/dashboard/Classes/Utility/ButtonUtility.php
 create mode 100644 typo3/sysext/dashboard/Classes/Views/Factory.php
 create mode 100644 typo3/sysext/dashboard/Classes/WidgetApi.php
 create mode 100644 typo3/sysext/dashboard/Classes/WidgetGroupInitializationService.php
 delete mode 100644 typo3/sysext/dashboard/Classes/Widgets/AbstractBarChartWidget.php
 delete mode 100644 typo3/sysext/dashboard/Classes/Widgets/AbstractChartWidget.php
 delete mode 100644 typo3/sysext/dashboard/Classes/Widgets/AbstractCtaButtonWidget.php
 delete mode 100644 typo3/sysext/dashboard/Classes/Widgets/AbstractDoughnutChartWidget.php
 delete mode 100644 typo3/sysext/dashboard/Classes/Widgets/AbstractListWidget.php
 delete mode 100644 typo3/sysext/dashboard/Classes/Widgets/AbstractNumberWithIconWidget.php
 delete mode 100644 typo3/sysext/dashboard/Classes/Widgets/AbstractRssWidget.php
 delete mode 100644 typo3/sysext/dashboard/Classes/Widgets/AbstractWidget.php
 create mode 100644 typo3/sysext/dashboard/Classes/Widgets/BarChartWidget.php
 create mode 100644 typo3/sysext/dashboard/Classes/Widgets/CtaWidget.php
 delete mode 100644 typo3/sysext/dashboard/Classes/Widgets/DocumentationGettingStartedWidget.php
 delete mode 100644 typo3/sysext/dashboard/Classes/Widgets/DocumentationTSconfigReferenceWidget.php
 delete mode 100644 typo3/sysext/dashboard/Classes/Widgets/DocumentationTypoScriptReference.php
 create mode 100644 typo3/sysext/dashboard/Classes/Widgets/DoughnutChartWidget.php
 create mode 100644 typo3/sysext/dashboard/Classes/Widgets/Interfaces/ButtonProviderInterface.php
 create mode 100644 typo3/sysext/dashboard/Classes/Widgets/Interfaces/ChartDataProviderInterface.php
 create mode 100644 typo3/sysext/dashboard/Classes/Widgets/Interfaces/ListDataProviderInterface.php
 create mode 100644 typo3/sysext/dashboard/Classes/Widgets/Interfaces/NumberWithIconDataProviderInterface.php
 create mode 100644 typo3/sysext/dashboard/Classes/Widgets/Interfaces/WidgetConfigurationInterface.php
 create mode 100644 typo3/sysext/dashboard/Classes/Widgets/ListWidget.php
 create mode 100644 typo3/sysext/dashboard/Classes/Widgets/NumberWithIconWidget.php
 create mode 100644 typo3/sysext/dashboard/Classes/Widgets/Provider/ButtonProvider.php
 rename typo3/sysext/dashboard/Classes/Widgets/{FailedLoginsWidget.php => Provider/NumberOfFailedLoginsDataProvider.php} (66%)
 create mode 100644 typo3/sysext/dashboard/Classes/Widgets/Provider/SysLogButtonProvider.php
 rename typo3/sysext/dashboard/Classes/Widgets/{SysLogErrorsWidget.php => Provider/SysLogErrorsDataProvider.php} (61%)
 rename typo3/sysext/dashboard/Classes/Widgets/{TypeOfUsersWidget.php => Provider/TypeOfUsersChartDataProvider.php} (61%)
 create mode 100644 typo3/sysext/dashboard/Classes/Widgets/RssWidget.php
 rename typo3/sysext/dashboard/Classes/Widgets/{T3GeneralInformation.php => T3GeneralInformationWidget.php} (54%)
 delete mode 100644 typo3/sysext/dashboard/Classes/Widgets/T3NewsWidget.php
 delete mode 100644 typo3/sysext/dashboard/Classes/Widgets/T3SecurityAdvisoriesWidget.php
 create mode 100644 typo3/sysext/dashboard/Classes/Widgets/WidgetConfiguration.php
 create mode 100644 typo3/sysext/dashboard/Configuration/Backend/DashboardWidgets.yaml
 rename typo3/sysext/dashboard/Resources/Private/Templates/Widget/{T3GeneralInformation.html => T3GeneralInformationWidget.html} (100%)
 create mode 100644 typo3/sysext/dashboard/Tests/Unit/DependencyInjection/DashboardWidgetPassTest.php

diff --git a/Build/Sources/Sass/dashboard/_grid.scss b/Build/Sources/Sass/dashboard/_grid.scss
index bb6eb4a0b425..e79c19e7146e 100644
--- a/Build/Sources/Sass/dashboard/_grid.scss
+++ b/Build/Sources/Sass/dashboard/_grid.scss
@@ -72,19 +72,19 @@
     user-select: auto !important;
 }
 
-.dashboard-item--h4 {
+.dashboard-item--h-medium {
     @media screen and (min-width: 750px) {
         height: $widget-height * 2;
     }
 }
 
-.dashboard-item--h6 {
+.dashboard-item--h-large {
     @media screen and (min-width: 750px) {
         height: $widget-height * 3;
     }
 }
 
-.dashboard-item--w4 {
+.dashboard-item--w-medium {
     width: 100%;
 
     @media screen and (min-width: 1285px) {
@@ -92,6 +92,10 @@
     }
 }
 
+.dashboard-item--w-large {
+    width: 100%;
+}
+
 .dashboard-item-content {
     position: relative;
     width: 100%;
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Breaking-90660-RegistrationOfWidgetsChanged.rst b/typo3/sysext/core/Documentation/Changelog/master/Breaking-90660-RegistrationOfWidgetsChanged.rst
new file mode 100644
index 000000000000..7c32bc0f8868
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/master/Breaking-90660-RegistrationOfWidgetsChanged.rst
@@ -0,0 +1,138 @@
+.. include:: ../../Includes.txt
+
+============================================================
+Breaking: #90660 - Registration of dashboard widgets changed
+============================================================
+
+See :issue:`90660`
+
+Description
+===========
+
+As the registration of dashboard widgets changed to allow creation of widgets
+through configuration, it is necessary to change your registration of widgets you
+registered yourself in version 10.3. The abstracts used to kick start your
+widgets are removed and the widgets shipped with EXT:dashboard are refactored.
+
+Impact
+======
+As the abstracts previously used to kick-start a widget are removed, you need
+to change to the new way of registering widgets. It will break the dashboard
+module if you do not update your registration.
+
+Affected Installations
+======================
+
+All 3rd party extensions that registered an own widget with TYPO3 v10.3, will be
+affected and need to update the widget registration. If you only used the widgets
+shipped with core, you don't have to do anything yourself.
+
+Migration
+=========
+
+Migration of widgets based on default widget types
+--------------------------------------------------
+
+This section is showing you how to migrate widgets that are based on one of
+the existing widget types that are shipped by core. If your widgets are extending
+one of the following classes, you can use this section to migrate your registration
+to the new syntax.
+
+- :php:`\TYPO3\CMS\Dashboard\Widgets\AbstractBarChartWidget`
+- :php:`\TYPO3\CMS\Dashboard\Widgets\AbstractChartWidget`
+- :php:`\TYPO3\CMS\Dashboard\Widgets\AbstractCtaButtonWidget`
+- :php:`\TYPO3\CMS\Dashboard\Widgets\AbstractDoughnutChartWidget`
+- :php:`\TYPO3\CMS\Dashboard\Widgets\AbstractListWidget`
+- :php:`\TYPO3\CMS\Dashboard\Widgets\AbstractNumberWithIconWidget`
+- :php:`\TYPO3\CMS\Dashboard\Widgets\AbstractRssWidget`
+
+First of all you need to update your registration in the :file:`Services.yaml` file.
+An example of a registration of an RSS widget in the old version.
+
+**Before**
+
+.. code-block:: yaml
+
+ Vendor\Package\Widgets\MyOwnRSSWidget:
+   arguments: [‘myOwnRSSWidget’]
+   tags:
+     - name: dashboard.widget
+       identifier: myOwnRSSWidget
+       widgetGroups: ‘general’
+
+As you can now use the predefined widgets and only have to register your own
+implementation with your own configuration, we have to alter this registration
+a little bit.
+
+**Now**
+
+.. code-block:: yaml
+
+   dashboard.widget.myOwnRSSWidget:
+     class: 'TYPO3\CMS\Dashboard\Widgets\RssWidget'
+     arguments:
+       $view: '@dashboard.views.widget'
+       $cache: '@cache.dashboard.rss'
+       $options:
+         rssFile: 'https://typo3.org/rss'
+         # 12 hours cache
+         lifeTime: 43200
+     tags:
+       - name: dashboard.widget
+         identifier: 'myOwnRSSWidget'
+         groupNames: ‘general’
+         title: 'LLL:EXT:extension/Resources/Private/Language/locallang.xlf:widgets.myOwnRSSWidget.title'
+         description: 'LLL:EXT:extension/Resources/Private/Language/locallang.xlf:widgets.myOwnRSSWidget.description'
+         iconIdentifier: 'content-widget-rss'
+         height: 4
+         width: 4
+
+It starts with the name of the service. Best practise is to use a dot-styled
+name as there will be now class with that name. On the second line, we define
+which widget to use. In this case we choose the RssWidget from the dashboard
+core extension. In the documentation, we explain all the arguments like :php:`$view`
+and :php:`$cache`. For the migration you need the :php:`$options` argument.
+As you can see we specify the RSS File and the cache lifetime for this feed.
+In the old situation you have set these values in the class.
+Now you can just put those values in the registration.
+
+The second part that changed a little bit, is that you need to set the title,
+description, icon, height and width in the tags section of the registration.
+You can still use translatable strings like
+``LLL:EXT:extension/Resources/Private/Language/locallang.xlf:widgets.myOwnRSSWidget.title``.
+Important to remember is that the :yaml:`widgetGroups`` property changed to :yaml:`groupNames`
+to stay consistent with other service registrations.
+
+In the following table you can see which WidgetType to use now based on the
+abstract you used previously.
+
++--------------------------------------+----------------------------------------------------------------------+
+| Previously used abstract             | Widget class to use for your registration                            |
++======================================+======================================================================+
+| :php:`AbstractBarChartWidget`        | :php:`TYPO3\CMS\Dashboard\Widgets\BarChartWidget`                    |
++--------------------------------------+----------------------------------------------------------------------+
+| :php:`AbstractChartWidget`           | This was only used as an abstract of the other chart widgets and is  |
+|                                      | not used anymore. If you want another graph type, you have to create |
+|                                      | your own widget.                                                     |
++--------------------------------------+----------------------------------------------------------------------+
+| :php:`AbstractCtaButtonWidget`       | :php:`TYPO3\CMS\Dashboard\Widgets\CtaWidget`                         |
++--------------------------------------+----------------------------------------------------------------------+
+| :php:`AbstractDoughnutChartWidget`   | :php:`TYPO3\CMS\Dashboard\Widgets\DoughnutChartWidget`               |
++--------------------------------------+----------------------------------------------------------------------+
+| :php:`AbstractListWidget`            | :php:`TYPO3\CMS\Dashboard\Widgets\ListWidget`                        |
++--------------------------------------+----------------------------------------------------------------------+
+| :php:`AbstractNumberWithIconWidget`  | :php:`TYPO3\CMS\Dashboard\Widgets\NumberWithIconWidget`              |
++--------------------------------------+----------------------------------------------------------------------+
+| :php:`AbstractRssWidget`             | :php:`TYPO3\CMS\Dashboard\Widgets\RssWidget`                         |
++--------------------------------------+----------------------------------------------------------------------+
+
+You can check the documentation of EXT:dashboard to see the exact options for every type of widget.
+
+Migration of widgets based on own widget type
+---------------------------------------------
+When you created your complete own widget type, the main thing to check is you
+use the Dependency Injection options you have now. Please check the documentation
+of EXT:dashboard to see how to create your own widget type and what options you
+have.
+
+.. index:: Backend, ext:dashboard, NotScanned
diff --git a/typo3/sysext/dashboard/Classes/Controller/DashboardController.php b/typo3/sysext/dashboard/Classes/Controller/DashboardController.php
index 47c683341d0f..671b2278b109 100644
--- a/typo3/sysext/dashboard/Classes/Controller/DashboardController.php
+++ b/typo3/sysext/dashboard/Classes/Controller/DashboardController.php
@@ -27,15 +27,10 @@ use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\PathUtility;
 use TYPO3\CMS\Dashboard\Dashboard;
-use TYPO3\CMS\Dashboard\DashboardPreset;
+use TYPO3\CMS\Dashboard\DashboardInitializationService;
 use TYPO3\CMS\Dashboard\DashboardPresetRegistry;
 use TYPO3\CMS\Dashboard\DashboardRepository;
-use TYPO3\CMS\Dashboard\WidgetGroupRegistry;
-use TYPO3\CMS\Dashboard\WidgetRegistry;
-use TYPO3\CMS\Dashboard\Widgets\Interfaces\AdditionalCssInterface;
-use TYPO3\CMS\Dashboard\Widgets\Interfaces\AdditionalJavaScriptInterface;
-use TYPO3\CMS\Dashboard\Widgets\Interfaces\RequireJsModuleInterface;
-use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
+use TYPO3\CMS\Dashboard\WidgetGroupInitializationService;
 use TYPO3\CMS\Extbase\Mvc\View\ViewInterface;
 use TYPO3\CMS\Fluid\View\StandaloneView;
 
@@ -56,36 +51,11 @@ class DashboardController extends AbstractController
      */
     protected $view;
 
-    /**
-     * @var array
-     */
-    protected $cssFiles = [];
-
-    /**
-     * @var array
-     */
-    protected $jsFiles = [];
-
-    /**
-     * @var mixed[]
-     */
-    protected $requireJsModules = [];
-
     /**
      * @var Dashboard
      */
     protected $currentDashboard;
 
-    /**
-     * @var Dashboard[]
-     */
-    protected $dashboardsForCurrentUser;
-
-    /**
-     * @var DashboardPreset[]
-     */
-    protected $availableDashboardPresets;
-
     /**
      * @var DashboardPresetRegistry
      */
@@ -97,76 +67,32 @@ class DashboardController extends AbstractController
     protected $dashboardRepository;
 
     /**
-     * @var WidgetGroupRegistry
+     * @var DashboardInitializationService
      */
-    protected $widgetGroupRepository;
+    private $dashboardInitializationService;
 
     /**
-     * @var WidgetRegistry
+     * @var WidgetGroupInitializationService
      */
-    protected $widgetRegistry;
-
-    /**
-     * @var ConfigurationManagerInterface
-     */
-    protected $configurationManager;
+    private $widgetGroupInitializationService;
 
     public function __construct(
         ModuleTemplate $moduleTemplate,
         UriBuilder $uriBuilder,
         DashboardPresetRegistry $dashboardPresetRepository,
         DashboardRepository $dashboardRepository,
-        WidgetGroupRegistry $widgetGroupRepository,
-        WidgetRegistry $widgetRegistry
+        DashboardInitializationService $dashboardInitializationService,
+        WidgetGroupInitializationService $widgetGroupInitializationService
     ) {
         $this->moduleTemplate = $moduleTemplate;
         $this->uriBuilder = $uriBuilder;
         $this->dashboardPresetRepository = $dashboardPresetRepository;
         $this->dashboardRepository = $dashboardRepository;
-        $this->widgetGroupRepository = $widgetGroupRepository;
-        $this->widgetRegistry = $widgetRegistry;
+        $this->dashboardInitializationService = $dashboardInitializationService;
 
-        $this->initializeDashboardsForCurrentUser();
-    }
-
-    protected function initializeDashboardsForCurrentUser(): void
-    {
-        $this->dashboardsForCurrentUser = $this->getDashboardsForCurrentUser();
-        $this->currentDashboard = $this->dashboardRepository->getDashboardByIdentifier($this->loadCurrentDashboard());
-
-        $this->availableDashboardPresets = $this->dashboardPresetRepository->getDashboardPresets();
-
-        if (empty($this->dashboardsForCurrentUser)) {
-            $this->dashboardsForCurrentUser = [];
-
-            $userConfig = $this->getBackendUser()->getTSConfig();
-            $dashboardsToCreate = GeneralUtility::trimExplode(
-                ',',
-                $userConfig['options.']['dashboard.']['dashboardPresetsForNewUsers'] ?? 'default'
-            );
-
-            /** @var DashboardPreset $dashboardPreset */
-            foreach ($this->availableDashboardPresets as $dashboardPreset) {
-                if (in_array($dashboardPreset->getIdentifier(), $dashboardsToCreate, true)) {
-                    $dashboard = $this->dashboardRepository->create(
-                        $dashboardPreset,
-                        (int)$this->getBackendUser()->user['uid']
-                    );
-
-                    if ($dashboard instanceof Dashboard) {
-                        $this->dashboardsForCurrentUser[$dashboard->getIdentifier()] = $dashboard;
-                    }
-                }
-            }
-        }
-
-        if (!$this->currentDashboard instanceof Dashboard) {
-            $this->currentDashboard = reset($this->dashboardsForCurrentUser);
-            $this->saveCurrentDashboard($this->currentDashboard->getIdentifier());
-        }
-
-        $this->currentDashboard->initializeWidgets();
-        $this->defineResourcesOfWidgets($this->currentDashboard->getWidgets());
+        $this->dashboardInitializationService->initializeDashboards($this->getBackendUser());
+        $this->currentDashboard = $this->dashboardInitializationService->getCurrentDashboard();
+        $this->widgetGroupInitializationService = $widgetGroupInitializationService;
     }
 
     /**
@@ -178,9 +104,9 @@ class DashboardController extends AbstractController
     public function mainAction(): void
     {
         $this->view->assignMultiple([
-            'availableDashboards' => $this->dashboardsForCurrentUser,
-            'dashboardPresets' => $this->availableDashboardPresets,
-            'widgetGroups' => $this->buildWidgetGroupsConfiguration(),
+            'availableDashboards' => $this->dashboardInitializationService->getDashboardsForUser(),
+            'dashboardPresets' => $this->dashboardPresetRepository->getDashboardPresets(),
+            'widgetGroups' => $this->widgetGroupInitializationService->buildWidgetGroupsConfiguration(),
             'currentDashboard' => $this->currentDashboard,
             'addWidgetUri' => (string)$this->uriBuilder->buildUriFromRoute('dashboard', ['action' => 'addWidget']),
             'addDashboardUri' => (string)$this->uriBuilder->buildUriFromRoute('dashboard', ['action' => 'addDashboard']),
@@ -333,17 +259,17 @@ class DashboardController extends AbstractController
      */
     protected function addFrontendResources(PageRenderer $pageRenderer): void
     {
-        foreach ($this->requireJsModules as $requireJsModule) {
+        foreach ($this->dashboardInitializationService->getRequireJsModules() as $requireJsModule) {
             if (is_array($requireJsModule)) {
                 $pageRenderer->loadRequireJsModule($requireJsModule[0], $requireJsModule[1]);
             } else {
                 $pageRenderer->loadRequireJsModule($requireJsModule);
             }
         }
-        foreach ($this->cssFiles as $cssFile) {
+        foreach ($this->dashboardInitializationService->getCssFiles() as $cssFile) {
             $pageRenderer->addCssFile($cssFile);
         }
-        foreach ($this->jsFiles as $jsFile) {
+        foreach ($this->dashboardInitializationService->getJsFiles() as $jsFile) {
             $pageRenderer->addJsFile($jsFile);
         }
     }
@@ -377,111 +303,4 @@ class DashboardController extends AbstractController
         $pageRenderer->loadRequireJsModule('TYPO3/CMS/Dashboard/DashboardDelete');
         $pageRenderer->addCssFile($publicResourcesPath . 'Css/dashboard.css');
     }
-
-    /**
-     * @return Dashboard[]
-     */
-    protected function getDashboardsForCurrentUser(): array
-    {
-        $dashboards = [];
-        foreach ($this->dashboardRepository->getDashboardsForUser((int)$this->getBackendUser()->user['uid']) as $dashboard) {
-            $dashboards[$dashboard->getIdentifier()] = $dashboard;
-        }
-        return $dashboards;
-    }
-
-    /**
-     * Define the different groups of widgets as shown in the modal when adding a widget to the current dashboard
-     *
-     * @return array
-     */
-    protected function buildWidgetGroupsConfiguration(): array
-    {
-        $groupConfigurations = [];
-        foreach ($this->widgetGroupRepository->getWidgetGroups() as $widgetGroup) {
-            $widgetInstances = [];
-            $widgetGroupIdentifier = $widgetGroup->getIdentifier();
-
-            $widgetsForGroup = $this->widgetRegistry->getAvailableWidgetsForWidgetGroup($widgetGroupIdentifier);
-            foreach ($widgetsForGroup as $identifier => $widgetService) {
-                $widgetInstances[$identifier] = GeneralUtility::makeInstance($widgetService);
-            }
-
-            $groupConfigurations[$widgetGroupIdentifier] = [
-                'identifier' => $widgetGroupIdentifier,
-                'title' => $widgetGroup->getTitle(),
-                'widgets' => $widgetInstances
-            ];
-        }
-
-        return $groupConfigurations;
-    }
-
-    /**
-     * @param array $widgets
-     */
-    protected function defineResourcesOfWidgets(array $widgets): void
-    {
-        foreach ($widgets as $widget) {
-            if ($widget instanceof RequireJsModuleInterface) {
-                $this->defineRequireJsModules($widget);
-            }
-            if ($widget instanceof AdditionalCssInterface) {
-                $this->defineCssFiles($widget);
-            }
-            if ($widget instanceof AdditionalJavaScriptInterface) {
-                $this->defineJsFiles($widget);
-            }
-        }
-    }
-
-    /**
-     * Add the RequireJS modules needed by some widgets
-     *
-     * @param RequireJsModuleInterface $widgetInstance
-     */
-    protected function defineRequireJsModules(RequireJsModuleInterface $widgetInstance): void
-    {
-        foreach ($widgetInstance->getRequireJsModules() as $moduleNameOrIndex => $callbackOrModuleName) {
-            if (is_string($moduleNameOrIndex)) {
-                $this->requireJsModules[] = [$moduleNameOrIndex, $callbackOrModuleName];
-            } else {
-                $this->requireJsModules[] = $callbackOrModuleName;
-            }
-        }
-    }
-
-    /**
-     * Define the correct path of the JS files of a widget and add them to the list of JS files that needs to be
-     * included
-     *
-     * @param AdditionalJavaScriptInterface $widgetInstance
-     */
-    protected function defineJsFiles(AdditionalJavaScriptInterface $widgetInstance): void
-    {
-        foreach ($widgetInstance->getJsFiles() as $key => $jsFile) {
-            if (strpos($jsFile, 'EXT:') === 0) {
-                $jsFile = PathUtility::getAbsoluteWebPath(GeneralUtility::getFileAbsFileName($jsFile));
-            }
-            $this->jsFiles[$jsFile] = $jsFile;
-        }
-    }
-
-    /**
-     * Define the correct path of the CSS files of a widget and add them to the list of CSS files that needs to be
-     * included
-     *
-     * @param AdditionalCssInterface $widgetInstance
-     */
-    protected function defineCssFiles(AdditionalCssInterface $widgetInstance): void
-    {
-        foreach ($widgetInstance->getCssFiles() as $cssFile) {
-            if (strpos($cssFile, 'EXT:') === 0) {
-                $cssFile = PathUtility::getAbsoluteWebPath(GeneralUtility::getFileAbsFileName($cssFile));
-            }
-            if (!in_array($cssFile, $this->cssFiles, true)) {
-                $this->cssFiles[$cssFile] = $cssFile;
-            }
-        }
-    }
 }
diff --git a/typo3/sysext/dashboard/Classes/Controller/WidgetAjaxController.php b/typo3/sysext/dashboard/Classes/Controller/WidgetAjaxController.php
index 1f3bc7b825a0..dbe46bd9527a 100644
--- a/typo3/sysext/dashboard/Classes/Controller/WidgetAjaxController.php
+++ b/typo3/sysext/dashboard/Classes/Controller/WidgetAjaxController.php
@@ -18,7 +18,6 @@ namespace TYPO3\CMS\Dashboard\Controller;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use TYPO3\CMS\Core\Http\JsonResponse;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Dashboard\Dashboard;
 use TYPO3\CMS\Dashboard\DashboardRepository;
 use TYPO3\CMS\Dashboard\WidgetRegistry;
@@ -58,23 +57,21 @@ class WidgetAjaxController extends AbstractController
     public function getContent(ServerRequestInterface $request): ResponseInterface
     {
         $queryParams = $request->getQueryParams();
-        $availableWidgets = $this->widgetRegistry->getAvailableWidgets();
 
-        if (empty((string)$queryParams['widget']) || !array_key_exists((string)$queryParams['widget'], $availableWidgets)) {
+        try {
+            $widgetObject = $this->widgetRegistry->getAvailableWidget((string)$queryParams['widget']);
+        } catch (\InvalidArgumentException $e) {
             return new JsonResponse(['error' => 'Widget is not available!']);
         }
 
-        $widgetObject = GeneralUtility::makeInstance($availableWidgets[(string)$queryParams['widget']]);
-
-        $eventData = $widgetObject instanceof EventDataInterface ? $widgetObject->getEventData() : [];
         if (!$widgetObject instanceof WidgetInterface) {
-            return new JsonResponse(['error' => 'Widget doesnt have a valid widget class']);
+            return new JsonResponse(['error' => 'Widget doesn\'t have a valid widget class']);
         }
 
         $data = [
             'widget' => $queryParams['widget'],
             'content' => $widgetObject->renderWidgetContent(),
-            'eventdata' => $eventData,
+            'eventdata' => $widgetObject instanceof EventDataInterface ? $widgetObject->getEventData() : [],
         ];
 
         return new JsonResponse($data);
diff --git a/typo3/sysext/dashboard/Classes/Dashboard.php b/typo3/sysext/dashboard/Classes/Dashboard.php
index e9ea58199151..df66eeaf7cff 100644
--- a/typo3/sysext/dashboard/Classes/Dashboard.php
+++ b/typo3/sysext/dashboard/Classes/Dashboard.php
@@ -17,6 +17,7 @@ namespace TYPO3\CMS\Dashboard;
 
 use Psr\Container\ContainerInterface;
 use TYPO3\CMS\Core\Localization\LanguageService;
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\WidgetConfigurationInterface;
 use TYPO3\CMS\Dashboard\Widgets\Interfaces\WidgetInterface;
 
 class Dashboard
@@ -105,11 +106,9 @@ class Dashboard
     {
         $availableWidgets = $this->widgetRegistry->getAvailableWidgets();
         foreach ($this->widgetConfig as $hash => $widgetConfig) {
+            /* @var WidgetConfigurationInterface $widgetConfig */
             if (array_key_exists($widgetConfig['identifier'], $availableWidgets)) {
-                $widgetObject = $this->container->get($availableWidgets[$widgetConfig['identifier']]);
-                if ($widgetObject instanceof WidgetInterface) {
-                    $this->widgets[$hash] = $widgetObject;
-                }
+                $this->widgets[$hash] = $availableWidgets[$widgetConfig['identifier']];
             }
         }
     }
diff --git a/typo3/sysext/dashboard/Classes/DashboardInitializationService.php b/typo3/sysext/dashboard/Classes/DashboardInitializationService.php
new file mode 100644
index 000000000000..088e2c4177f8
--- /dev/null
+++ b/typo3/sysext/dashboard/Classes/DashboardInitializationService.php
@@ -0,0 +1,235 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Dashboard;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\PathUtility;
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\AdditionalCssInterface;
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\AdditionalJavaScriptInterface;
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\RequireJsModuleInterface;
+
+class DashboardInitializationService
+{
+    protected const MODULE_DATA_CURRENT_DASHBOARD_IDENTIFIER = 'dashboard/current_dashboard/';
+
+    /**
+     * @var DashboardRepository
+     */
+    private $dashboardRepository;
+
+    /**
+     * @var DashboardPresetRegistry
+     */
+    private $dashboardPresetRegistry;
+
+    /**
+     * @var Dashboard
+     */
+    private $currentDashboard;
+
+    /**
+     * @var BackendUserAuthentication
+     */
+    private $user;
+
+    /**
+     * @var array
+     */
+    private $requireJsModules = [];
+    private $jsFiles = [];
+    private $cssFiles = [];
+
+    public function __construct(
+        DashboardRepository $dashboardRepository,
+        DashboardPresetRegistry $dashboardPresetRegistry
+    ) {
+        $this->dashboardRepository = $dashboardRepository;
+        $this->dashboardPresetRegistry = $dashboardPresetRegistry;
+    }
+
+    public function initializeDashboards(BackendUserAuthentication $user): void
+    {
+        $this->user = $user;
+        $this->currentDashboard = $this->defineCurrentDashboard();
+
+        $this->currentDashboard->initializeWidgets();
+        $this->defineResourcesOfWidgets($this->currentDashboard->getWidgets());
+    }
+
+    public function getCurrentDashboard(): Dashboard
+    {
+        return $this->currentDashboard;
+    }
+
+    protected function defineCurrentDashboard(): Dashboard
+    {
+        $currentDashboard = $this->dashboardRepository->getDashboardByIdentifier($this->loadCurrentDashboard($this->user));
+        if (!$currentDashboard instanceof Dashboard) {
+            $dashboards = $this->getDashboardsForUser();
+            $currentDashboard = reset($dashboards);
+            $this->saveCurrentDashboard($this->user, $currentDashboard->getIdentifier());
+        }
+
+        return $currentDashboard;
+    }
+
+    protected function createDefaultDashboards(): array
+    {
+        $dashboardsForUser = [];
+
+        $userConfig = $this->user->getTSConfig();
+        $dashboardsToCreate = GeneralUtility::trimExplode(
+            ',',
+            $userConfig['options.']['dashboard.']['dashboardPresetsForNewUsers'] ?? 'default'
+        );
+
+        /** @var DashboardPreset $dashboardPreset */
+        foreach ($this->dashboardPresetRegistry->getDashboardPresets() as $dashboardPreset) {
+            if (in_array($dashboardPreset->getIdentifier(), $dashboardsToCreate, true)) {
+                $dashboard = $this->dashboardRepository->create(
+                    $dashboardPreset,
+                    (int)$this->user->user['uid']
+                );
+
+                if ($dashboard instanceof Dashboard) {
+                    $dashboardsForUser[$dashboard->getIdentifier()] = $dashboard;
+                }
+            }
+        }
+
+        return $dashboardsForUser;
+    }
+
+    /**
+     * @return Dashboard[]
+     */
+    public function getDashboardsForUser(): array
+    {
+        $dashboards = [];
+        foreach ($this->dashboardRepository->getDashboardsForUser((int)$this->user->user['uid']) as $dashboard) {
+            $dashboards[$dashboard->getIdentifier()] = $dashboard;
+        }
+
+        if ($dashboards === []) {
+            $dashboards = $this->createDefaultDashboards();
+        }
+
+        return $dashboards;
+    }
+
+    /**
+     * @param array $widgets
+     */
+    protected function defineResourcesOfWidgets(array $widgets): void
+    {
+        foreach ($widgets as $widget) {
+            $concreteInstance = GeneralUtility::makeInstance($widget->getServiceName());
+            if ($concreteInstance instanceof RequireJsModuleInterface) {
+                $this->defineRequireJsModules($concreteInstance);
+            }
+            if ($concreteInstance instanceof AdditionalCssInterface) {
+                $this->defineCssFiles($concreteInstance);
+            }
+            if ($concreteInstance instanceof AdditionalJavaScriptInterface) {
+                $this->defineJsFiles($concreteInstance);
+            }
+        }
+    }
+
+    /**
+     * Add the RequireJS modules needed by some widgets
+     *
+     * @param RequireJsModuleInterface $widgetInstance
+     */
+    protected function defineRequireJsModules(RequireJsModuleInterface $widgetInstance): void
+    {
+        foreach ($widgetInstance->getRequireJsModules() as $moduleNameOrIndex => $callbackOrModuleName) {
+            if (is_string($moduleNameOrIndex)) {
+                $this->requireJsModules[] = [$moduleNameOrIndex, $callbackOrModuleName];
+            } else {
+                $this->requireJsModules[] = $callbackOrModuleName;
+            }
+        }
+    }
+
+    /**
+     * Define the correct path of the JS files of a widget and add them to the list of JS files that needs to be
+     * included
+     *
+     * @param AdditionalJavaScriptInterface $widgetInstance
+     */
+    protected function defineJsFiles(AdditionalJavaScriptInterface $widgetInstance): void
+    {
+        foreach ($widgetInstance->getJsFiles() as $jsFile) {
+            if (strpos($jsFile, 'EXT:') === 0) {
+                $jsFile = PathUtility::getAbsoluteWebPath(GeneralUtility::getFileAbsFileName($jsFile));
+            }
+            $this->jsFiles[$jsFile] = $jsFile;
+        }
+    }
+
+    /**
+     * Define the correct path of the CSS files of a widget and add them to the list of CSS files that needs to be
+     * included
+     *
+     * @param AdditionalCssInterface $widgetInstance
+     */
+    protected function defineCssFiles(AdditionalCssInterface $widgetInstance): void
+    {
+        foreach ($widgetInstance->getCssFiles() as $cssFile) {
+            if (strpos($cssFile, 'EXT:') === 0) {
+                $cssFile = PathUtility::getAbsoluteWebPath(GeneralUtility::getFileAbsFileName($cssFile));
+            }
+            $this->cssFiles[$cssFile] = $cssFile;
+        }
+    }
+
+    protected function loadCurrentDashboard(BackendUserAuthentication $user): string
+    {
+        return $user->getModuleData(self::MODULE_DATA_CURRENT_DASHBOARD_IDENTIFIER) ?? '';
+    }
+
+    protected function saveCurrentDashboard(BackendUserAuthentication $user, string $identifier): void
+    {
+        $user->pushModuleData(self::MODULE_DATA_CURRENT_DASHBOARD_IDENTIFIER, $identifier);
+    }
+
+    /**
+     * @return array
+     */
+    public function getRequireJsModules(): array
+    {
+        return $this->requireJsModules;
+    }
+
+    /**
+     * @return array
+     */
+    public function getJsFiles(): array
+    {
+        return $this->jsFiles;
+    }
+
+    /**
+     * @return array
+     */
+    public function getCssFiles(): array
+    {
+        return $this->cssFiles;
+    }
+}
diff --git a/typo3/sysext/dashboard/Classes/DependencyInjection/DashboardWidgetPass.php b/typo3/sysext/dashboard/Classes/DependencyInjection/DashboardWidgetPass.php
index 47f0b475d7db..f22edef4d980 100644
--- a/typo3/sysext/dashboard/Classes/DependencyInjection/DashboardWidgetPass.php
+++ b/typo3/sysext/dashboard/Classes/DependencyInjection/DashboardWidgetPass.php
@@ -17,8 +17,11 @@ namespace TYPO3\CMS\Dashboard\DependencyInjection;
 
 use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
 use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Definition;
+use Symfony\Component\DependencyInjection\Reference;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Dashboard\WidgetRegistry;
+use TYPO3\CMS\Dashboard\Widgets\WidgetConfiguration;
 
 /**
  * @internal
@@ -44,6 +47,7 @@ final class DashboardWidgetPass implements CompilerPassInterface
     public function process(ContainerBuilder $container): void
     {
         $widgetRegistryDefinition = $container->findDefinition(WidgetRegistry::class);
+        /* @var Definition|bool $widgetRegistryDefinition */
         if (!$widgetRegistryDefinition) {
             return;
         }
@@ -51,19 +55,72 @@ final class DashboardWidgetPass implements CompilerPassInterface
         foreach ($container->findTaggedServiceIds($this->tagName) as $serviceName => $tags) {
             $definition = $container->findDefinition($serviceName);
             $definition->setPublic(true);
-            // Widgets are handled like prototypes right now (will require state)
-            // @todo: Widgets should preferably be services, but that will require WidgetInterface
-            // to change
-            $definition->setShared(false);
-            $className = $definition->getClass() ?? $serviceName;
+
             foreach ($tags as $attributes) {
                 $identifier = $attributes['identifier'] ?? $serviceName;
-                $widgetRegistryDefinition->addMethodCall('registerWidget', [
+                $attributes['identifier'] = $identifier;
+                $attributes['serviceName'] = $serviceName;
+                $attributes = $this->convertAttributes($attributes);
+
+                $configurationServiceName = $this->registerWidgetConfigurationService(
+                    $container,
                     $identifier,
-                    $serviceName,
-                    GeneralUtility::trimExplode(',', $attributes['widgetGroups'] ?? '', true)
-                ]);
+                    $attributes
+                );
+                $definition->setArgument('$configuration', new Reference($configurationServiceName));
+
+                $widgetRegistryDefinition->addMethodCall('registerWidget', [$identifier . 'WidgetConfiguration']);
             }
         }
     }
+
+    private function convertAttributes(array $attributes): array
+    {
+        $attributes = array_merge([
+            'iconIdentifier' => 'content-dashboard',
+            'height' => 'small',
+            'width' => 'small',
+        ], $attributes);
+
+        if (isset($attributes['groupNames'])) {
+            $attributes['groupNames'] = GeneralUtility::trimExplode(',', $attributes['groupNames'], true);
+        } else {
+            $attributes['groupNames'] = [];
+        }
+
+        if (isset($attributes['additionalCssClasses'])) {
+            $attributes['additionalCssClasses'] = GeneralUtility::trimExplode(' ', $attributes['additionalCssClasses'], true);
+        } else {
+            $attributes['additionalCssClasses'] = [];
+        }
+
+        return $attributes;
+    }
+
+    private function registerWidgetConfigurationService(
+        ContainerBuilder $container,
+        string $widgetIdentifier,
+        array $arguments
+    ): string {
+        $serviceName = $widgetIdentifier . 'WidgetConfiguration';
+
+        $definition = new Definition(
+            WidgetConfiguration::class,
+            $this->adjustArgumentsForDi($arguments)
+        );
+        $definition->setPublic(true);
+        $container->addDefinitions([$serviceName => $definition]);
+
+        return $serviceName;
+    }
+
+    private function adjustArgumentsForDi(array $arguments): array
+    {
+        foreach ($arguments as $key => $value) {
+            $arguments['$' . $key] = $value;
+            unset($arguments[$key]);
+        }
+
+        return $arguments;
+    }
 }
diff --git a/typo3/sysext/dashboard/Classes/ServiceProvider.php b/typo3/sysext/dashboard/Classes/ServiceProvider.php
index 52f06253759e..51784438545d 100644
--- a/typo3/sysext/dashboard/Classes/ServiceProvider.php
+++ b/typo3/sysext/dashboard/Classes/ServiceProvider.php
@@ -131,32 +131,6 @@ class ServiceProvider extends AbstractServiceProvider
         return $widgetGroupRegistry;
     }
 
-    public static function configureWidgetRegistry(
-        ContainerInterface $container,
-        WidgetRegistry $widgetRegistry = null
-    ): WidgetRegistry {
-        $widgetRegistry = $widgetRegistry ?? self::new($container, WidgetRegistry::class);
-        $cache = $container->get('cache.core');
-
-        $cacheIdentifier = 'Dashboard_' . sha1((string)(new Typo3Version()) . Environment::getProjectPath() . 'Widgets');
-        if ($cache->has($cacheIdentifier)) {
-            $widgetsFromPackages = $cache->require($cacheIdentifier);
-        } else {
-            $widgetsFromPackages = $container->get('dashboard.widgets')->getArrayCopy();
-            $cache->set($cacheIdentifier, 'return ' . var_export($widgetsFromPackages, true) . ';');
-        }
-
-        foreach ($widgetsFromPackages as $identifier => $options) {
-            $widgetRegistry->registerWidget(
-                $identifier,
-                $options['class'],
-                (array)$options['widgetGroups']
-            );
-        }
-
-        return $widgetRegistry;
-    }
-
     /**
      * @param ContainerInterface $container
      * @param ArrayObject $presets
diff --git a/typo3/sysext/dashboard/Classes/Utility/ButtonUtility.php b/typo3/sysext/dashboard/Classes/Utility/ButtonUtility.php
new file mode 100644
index 000000000000..890cc98ade0f
--- /dev/null
+++ b/typo3/sysext/dashboard/Classes/Utility/ButtonUtility.php
@@ -0,0 +1,34 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Dashboard\Utility;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\ButtonProviderInterface;
+
+class ButtonUtility
+{
+    public static function generateButtonConfig(?ButtonProviderInterface $buttonProvider): array
+    {
+        if ($buttonProvider instanceof ButtonProviderInterface && $buttonProvider->getTitle() && $buttonProvider->getLink()) {
+            return [
+                'text' => $buttonProvider->getTitle(),
+                'link' => $buttonProvider->getLink(),
+                'target' => $buttonProvider->getTarget(),
+            ];
+        }
+
+        return [];
+    }
+}
diff --git a/typo3/sysext/dashboard/Classes/Views/Factory.php b/typo3/sysext/dashboard/Classes/Views/Factory.php
new file mode 100644
index 000000000000..bcc072325696
--- /dev/null
+++ b/typo3/sysext/dashboard/Classes/Views/Factory.php
@@ -0,0 +1,30 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Dashboard\Views;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Fluid\View\StandaloneView;
+
+class Factory
+{
+    public static function widgetTemplate(): StandaloneView
+    {
+        $view = GeneralUtility::makeInstance(StandaloneView::class);
+        $view->getRenderingContext()->getTemplatePaths()->fillDefaultsByPackageName('dashboard');
+
+        return $view;
+    }
+}
diff --git a/typo3/sysext/dashboard/Classes/WidgetApi.php b/typo3/sysext/dashboard/Classes/WidgetApi.php
new file mode 100644
index 000000000000..2fce884df365
--- /dev/null
+++ b/typo3/sysext/dashboard/Classes/WidgetApi.php
@@ -0,0 +1,38 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Dashboard;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Provides API for widgets.
+ */
+class WidgetApi
+{
+    /**
+     * Provides default colors to use for charts.
+     *
+     * @return array Hex codes of default colors.
+     */
+    public static function getDefaultChartColors(): array
+    {
+        return [
+            '#ff8700',
+            '#a4276a',
+            '#1a568f',
+            '#4c7e3a',
+            '#69bbb5',
+        ];
+    }
+}
diff --git a/typo3/sysext/dashboard/Classes/WidgetGroupInitializationService.php b/typo3/sysext/dashboard/Classes/WidgetGroupInitializationService.php
new file mode 100644
index 000000000000..8fec06754d78
--- /dev/null
+++ b/typo3/sysext/dashboard/Classes/WidgetGroupInitializationService.php
@@ -0,0 +1,73 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Dashboard;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Localization\LanguageService;
+
+class WidgetGroupInitializationService
+{
+    /**
+     * @var WidgetGroupRegistry
+     */
+    private $widgetGroupRegistry;
+
+    /**
+     * @var WidgetRegistry
+     */
+    private $widgetRegistry;
+
+    public function __construct(WidgetGroupRegistry $widgetGroupRegistry, WidgetRegistry $widgetRegistry)
+    {
+        $this->widgetGroupRegistry = $widgetGroupRegistry;
+        $this->widgetRegistry = $widgetRegistry;
+    }
+
+    /**
+     * Define the different groups of widgets as shown in the modal when adding a widget to the current dashboard
+     *
+     * @return array
+     */
+    public function buildWidgetGroupsConfiguration(): array
+    {
+        $groupConfigurations = [];
+        foreach ($this->widgetGroupRegistry->getWidgetGroups() as $widgetGroup) {
+            $widgets = [];
+            $widgetGroupIdentifier = $widgetGroup->getIdentifier();
+
+            $widgetsForGroup = $this->widgetRegistry->getAvailableWidgetsForWidgetGroup($widgetGroupIdentifier);
+            foreach ($widgetsForGroup as $widgetConfiguration) {
+                $widgets[$widgetConfiguration->getIdentifier()] = [
+                    'iconIdentifier' => $widgetConfiguration->getIconIdentifier(),
+                    'title' => $this->getLanguageService()->sl($widgetConfiguration->getTitle()),
+                    'description' => $this->getLanguageService()->sl($widgetConfiguration->getDescription()),
+                ];
+            }
+
+            $groupConfigurations[$widgetGroupIdentifier] = [
+                'identifier' => $widgetGroupIdentifier,
+                'title' => $widgetGroup->getTitle(),
+                'widgets' => $widgets,
+            ];
+        }
+
+        return $groupConfigurations;
+    }
+
+    protected function getLanguageService(): LanguageService
+    {
+        return $GLOBALS['LANG'];
+    }
+}
diff --git a/typo3/sysext/dashboard/Classes/WidgetRegistry.php b/typo3/sysext/dashboard/Classes/WidgetRegistry.php
index f5cd73138292..97172c257a2e 100644
--- a/typo3/sysext/dashboard/Classes/WidgetRegistry.php
+++ b/typo3/sysext/dashboard/Classes/WidgetRegistry.php
@@ -18,6 +18,7 @@ namespace TYPO3\CMS\Dashboard;
 use Psr\Container\ContainerInterface;
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
 use TYPO3\CMS\Core\SingletonInterface;
+use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Dashboard\Widgets\Interfaces\WidgetInterface;
 
 class WidgetRegistry implements SingletonInterface
@@ -52,6 +53,18 @@ class WidgetRegistry implements SingletonInterface
         return $this->widgets;
     }
 
+    /**
+     * @throws \InvalidArgumentException If requested identifier does not exist.
+     */
+    public function getAvailableWidget(string $identifier): WidgetInterface
+    {
+        if (array_key_exists($identifier, $this->getAvailableWidgets())) {
+            return $this->container->get($this->widgets[$identifier]->getServiceName());
+        }
+
+        throw new \InvalidArgumentException('Requested widget "' . $identifier . '" does not exist.', 1584777201);
+    }
+
     public function getAvailableWidgetsForWidgetGroup(string $widgetGroupIdentifier): array
     {
         if (!array_key_exists($widgetGroupIdentifier, $this->widgetsPerWidgetGroup)) {
@@ -60,36 +73,40 @@ class WidgetRegistry implements SingletonInterface
         return $this->checkPermissionOfWidgets($this->widgetsPerWidgetGroup[$widgetGroupIdentifier]);
     }
 
-    public function registerWidget(string $identifier, string $widgetServiceName, array $widgetGroupIdentifiers): void
+    public function registerWidget(string $serviceName): void
     {
-        $this->widgets[$identifier] = $widgetServiceName;
-        foreach ($widgetGroupIdentifiers as $widgetGroupIdentifier) {
-            $this->widgetsPerWidgetGroup[$widgetGroupIdentifier][$identifier] = $widgetServiceName;
+        $widgetConfiguration = $this->container->get($serviceName);
+        $this->widgets[$widgetConfiguration->getIdentifier()] = $widgetConfiguration;
+        foreach ($widgetConfiguration->getGroupNames() as $groupIdentifier) {
+            $this->widgetsPerWidgetGroup = ArrayUtility::setValueByPath(
+                $this->widgetsPerWidgetGroup,
+                $groupIdentifier . '/' . $widgetConfiguration->getIdentifier(),
+                $widgetConfiguration
+            );
         }
     }
 
     protected function checkPermissionOfWidgets(array $widgets): array
     {
-        return array_filter($widgets, function ($widgetIdentifier) {
-            return $this->getBackendUser()->check('available_widgets', $widgetIdentifier);
+        return array_filter($widgets, function ($identifier) {
+            return $this->getBackendUser()->check('available_widgets', $identifier);
         }, ARRAY_FILTER_USE_KEY);
     }
 
-    protected function getBackendUser(): BackendUserAuthentication
+    public function widgetItemsProcFunc(array &$parameters): void
     {
-        return $GLOBALS['BE_USER'];
-    }
-
-    public function widgetItemsProcFunc(array $parameters): void
-    {
-        foreach ($this->widgets as $identifier => $widget) {
-            $widgetObject = $this->container->get($widget);
+        foreach ($this->widgets as $widget) {
             $parameters['items'][] = [
-                $widgetObject->getTitle() ,
-                $identifier,
-                $widgetObject->getIconIdentifier() ?? 'content-dashboard',
-                $widgetObject->getDescription()
+                $widget->getTitle(),
+                $widget->getIdentifier(),
+                $widget->getIconIdentifier(),
+                $widget->getDescription(),
             ];
         }
     }
+
+    protected function getBackendUser(): BackendUserAuthentication
+    {
+        return $GLOBALS['BE_USER'];
+    }
 }
diff --git a/typo3/sysext/dashboard/Classes/Widgets/AbstractBarChartWidget.php b/typo3/sysext/dashboard/Classes/Widgets/AbstractBarChartWidget.php
deleted file mode 100644
index 875dd583f1a3..000000000000
--- a/typo3/sysext/dashboard/Classes/Widgets/AbstractBarChartWidget.php
+++ /dev/null
@@ -1,61 +0,0 @@
-<?php
-declare(strict_types = 1);
-namespace TYPO3\CMS\Dashboard\Widgets;
-
-/*
- * This file is part of the TYPO3 CMS project.
- *
- * It is free software; you can redistribute it and/or modify it under
- * the terms of the GNU General Public License, either version 2
- * of the License, or any later version.
- *
- * For the full copyright and license information, please read the
- * LICENSE.txt file that was distributed with this source code.
- *
- * The TYPO3 project - inspiring people to share!
- */
-
-/**
- * The AbstractBarChartWidget class is the basic widget class for bar charts.
- * It is possible to extend this class for custom widgets.
- * In your class you have to store the data to display in $this->chartData.
- * More information can be found in the documentation.
- */
-abstract class AbstractBarChartWidget extends AbstractChartWidget
-{
-    /**
-     * @var string
-     */
-    protected $iconIdentifier = 'content-widget-chart-bar';
-
-    /**
-     * @var string
-     */
-    protected $chartType = 'bar';
-
-    /**
-     * @var mixed[]
-     */
-    protected $chartOptions = [
-        'maintainAspectRatio' => false,
-        'legend' => [
-            'display' => false
-        ],
-        'scales' => [
-            'yAxes' => [
-                [
-                    'ticks' => [
-                        'beginAtZero' => true
-                    ]
-                ]
-            ],
-            'xAxes' => [
-                [
-                    'ticks' => [
-                        'maxTicksLimit' => 15
-                    ]
-                ]
-            ]
-        ]
-    ];
-}
diff --git a/typo3/sysext/dashboard/Classes/Widgets/AbstractChartWidget.php b/typo3/sysext/dashboard/Classes/Widgets/AbstractChartWidget.php
deleted file mode 100644
index 7e93c6bd067f..000000000000
--- a/typo3/sysext/dashboard/Classes/Widgets/AbstractChartWidget.php
+++ /dev/null
@@ -1,174 +0,0 @@
-<?php
-declare(strict_types = 1);
-namespace TYPO3\CMS\Dashboard\Widgets;
-
-/*
- * This file is part of the TYPO3 CMS project.
- *
- * It is free software; you can redistribute it and/or modify it under
- * the terms of the GNU General Public License, either version 2
- * of the License, or any later version.
- *
- * For the full copyright and license information, please read the
- * LICENSE.txt file that was distributed with this source code.
- *
- * The TYPO3 project - inspiring people to share!
- */
-
-use TYPO3\CMS\Dashboard\Widgets\Interfaces\AdditionalCssInterface;
-use TYPO3\CMS\Dashboard\Widgets\Interfaces\EventDataInterface;
-use TYPO3\CMS\Dashboard\Widgets\Interfaces\RequireJsModuleInterface;
-
-/**
- * The AbstractChartWidget class is the basic widget class for all chart widgets.
- * It is possible to extend this class for custom widgets. EXT:dashboard also provides
- * more special chart types for widgets (bar chart and doughnut chart).
- */
-abstract class AbstractChartWidget extends AbstractWidget implements AdditionalCssInterface, RequireJsModuleInterface, EventDataInterface
-{
-    /**
-     * The type of chart you want to show. The types of charts that are available can be found in the
-     * chart.js documentation.
-     *
-     * @link https://www.chartjs.org/docs/latest/charts/
-     *
-     * Currently the following chart types are implemented:
-     * @see AbstractBarChartWidget
-     * @see AbstractDoughnutChartWidget
-     *
-     * @var string
-     */
-    protected $chartType = '';
-
-    /**
-     * This property should contain the data for the graph. The data and options you have depend on the type
-     * of chart. More information can be found in the documentation of the specific type.
-     *
-     * @link https://www.chartjs.org/docs/latest/charts/bar.html#data-structure
-     * @link https://www.chartjs.org/docs/latest/charts/doughnut.html#data-structure
-     *
-     * @var array
-     */
-    protected $chartData = [];
-
-    /**
-     * This property should contain the options to configure your graph. The options available are based on the type
-     * of chart. The implementations of the charts will contain the default options for that type of graph.
-     *
-     * @see AbstractBarChartWidget::$chartOptions
-     * @see AbstractDoughnutChartWidget::$chartOptions
-     *
-     * @var array
-     */
-    protected $chartOptions = [];
-
-    /**
-     * This property can be used to pass data to the JavaScript that will handle the content rendering. For
-     * charts, the only property necessary for charts is the graphConfig element. This will be set in the
-     * getEventData method of this class. Setting this property manually is therefore not needed.
-     *
-     * @see getEventData
-     *
-     * @var array
-     */
-    protected $eventData = [];
-
-    /**
-     * The default colors that will be used for the graphs.
-     *
-     * @var string[]
-     */
-    protected $chartColors = ['#ff8700', '#a4276a', '#1a568f', '#4c7e3a', '#69bbb5'];
-
-    /**
-     * If you want to show a button below the graph, you need to set the title and the link to the button.
-     * This text can be a fixed string or can contain a translatable string.
-     *
-     * @var string
-     */
-    protected $buttonText = '';
-
-    /**
-     * The link of the button. Besides the link, also the buttonText should be set before the buttons will be
-     * rendered.
-     *
-     * @var string
-     */
-    protected $buttonLink = '';
-
-    /**
-     * By default the link of the button will be opened in the current frame. By setting the target, you can specify
-     * where the link should be opened.
-     *
-     * @var string
-     */
-    protected $buttonTarget = '';
-
-    /**
-     * This CSS class is used so the JavaScript can identify the widgets containing graphs. Overriding this
-     * property could lead to the widget not working.
-     *
-     * @internal
-     *
-     * @var string
-     */
-    protected $additionalClasses = 'dashboard-item--chart';
-
-    /**
-     * @var string
-     */
-    protected $iconIdentifier = 'content-widget-chart';
-
-    /**
-     * @var string
-     */
-    protected $templateName = 'ChartWidget';
-
-    /**
-     * This method is used to define the data that will be shown in the graph. This method should be implemented
-     * and should set the data in the chartData property.
-     *
-     * @see chartData
-     */
-    abstract protected function prepareChartData(): void;
-
-    protected function initializeView(): void
-    {
-        parent::initializeView();
-
-        if ($this->buttonLink && $this->buttonText) {
-            $this->view->assign(
-                'button',
-                [
-                    'text' => $this->getLanguageService()->sL($this->buttonText) ?: $this->buttonText,
-                    'link' => $this->buttonLink,
-                    'target' => $this->buttonTarget
-                ]
-            );
-        }
-    }
-
-    public function getEventData(): array
-    {
-        $this->prepareChartData();
-        $this->eventData['graphConfig'] = [
-            'type' => $this->chartType,
-            'data' => $this->chartData,
-            'options' => $this->chartOptions
-        ];
-        return $this->eventData;
-    }
-
-    public function getCssFiles(): array
-    {
-        return ['EXT:dashboard/Resources/Public/Css/Contrib/chart.css'];
-    }
-
-    public function getRequireJsModules(): array
-    {
-        return [
-            'TYPO3/CMS/Dashboard/Contrib/chartjs',
-            'TYPO3/CMS/Dashboard/ChartInitializer',
-        ];
-    }
-}
diff --git a/typo3/sysext/dashboard/Classes/Widgets/AbstractCtaButtonWidget.php b/typo3/sysext/dashboard/Classes/Widgets/AbstractCtaButtonWidget.php
deleted file mode 100644
index 7ec370a767d9..000000000000
--- a/typo3/sysext/dashboard/Classes/Widgets/AbstractCtaButtonWidget.php
+++ /dev/null
@@ -1,75 +0,0 @@
-<?php
-declare(strict_types = 1);
-namespace TYPO3\CMS\Dashboard\Widgets;
-
-/*
- * This file is part of the TYPO3 CMS project.
- *
- * It is free software; you can redistribute it and/or modify it under
- * the terms of the GNU General Public License, either version 2
- * of the License, or any later version.
- *
- * For the full copyright and license information, please read the
- * LICENSE.txt file that was distributed with this source code.
- *
- * The TYPO3 project - inspiring people to share!
- */
-
-/**
- * The AbstractCtaButtonWidget class is the basic widget class for simple CTA widgets.
- * It is possible to extend this class for custom widgets.
- * Simply overwrite $this->link and $this->label to make use of this widget type.
- */
-abstract class AbstractCtaButtonWidget extends AbstractWidget
-{
-    /**
-     * This link will be the main data in the widget
-     *
-     * @var string
-     */
-    protected $link = '';
-
-    /**
-     * When filled, this is used as the button label
-     *
-     * @var string
-     */
-    protected $label = '';
-
-    /**
-     * When filled, a text is shown above the link
-     *
-     * @var string
-     */
-    protected $text = '';
-
-    /**
-     * This property contains the identifier of the icon that should be shown in the widget
-     *
-     * @var string
-     */
-    protected $icon;
-
-    /**
-     * @var string
-     */
-    protected $iconIdentifier = 'content-widget-calltoaction';
-
-    /**
-     * @var string
-     */
-    protected $templateName = 'CtaWidget';
-
-    public function __construct(string $identifier)
-    {
-        parent::__construct($identifier);
-        $this->height = 1;
-
-        $this->view->assignMultiple([
-            'link' => $this->link,
-            'label' => $this->label,
-            'icon' => $this->icon,
-            'text' => $this->getLanguageService()->sL($this->text)
-        ]);
-    }
-}
diff --git a/typo3/sysext/dashboard/Classes/Widgets/AbstractDoughnutChartWidget.php b/typo3/sysext/dashboard/Classes/Widgets/AbstractDoughnutChartWidget.php
deleted file mode 100644
index c5b5b79f70f1..000000000000
--- a/typo3/sysext/dashboard/Classes/Widgets/AbstractDoughnutChartWidget.php
+++ /dev/null
@@ -1,52 +0,0 @@
-<?php
-declare(strict_types = 1);
-namespace TYPO3\CMS\Dashboard\Widgets;
-
-/*
- * This file is part of the TYPO3 CMS project.
- *
- * It is free software; you can redistribute it and/or modify it under
- * the terms of the GNU General Public License, either version 2
- * of the License, or any later version.
- *
- * For the full copyright and license information, please read the
- * LICENSE.txt file that was distributed with this source code.
- *
- * The TYPO3 project - inspiring people to share!
- */
-
-/**
- * The AbstractDoughnutChartWidget class is the basic widget class for doughnut charts.
- * It is possible to extend this class for custom widgets.
- * In your class you have to store the data to display in $this->chartData.
- * More information can be found in the documentation.
- */
-abstract class AbstractDoughnutChartWidget extends AbstractChartWidget
-{
-    /**
-     * @var string
-     */
-    protected $iconIdentifier = 'content-widget-chart-pie';
-
-    /**
-     * @var string
-     */
-    protected $chartType = 'doughnut';
-
-    /**
-     * @var mixed[]
-     */
-    protected $chartOptions = [
-        'maintainAspectRatio' => false,
-        'legend' => [
-            'display' => true,
-            'position' => 'bottom'
-        ],
-        'cutoutPercentage' => 60
-    ];
-
-    /**
-     * @var int
-     */
-    protected $height = 4;
-}
diff --git a/typo3/sysext/dashboard/Classes/Widgets/AbstractListWidget.php b/typo3/sysext/dashboard/Classes/Widgets/AbstractListWidget.php
deleted file mode 100644
index 09f3d1ed2281..000000000000
--- a/typo3/sysext/dashboard/Classes/Widgets/AbstractListWidget.php
+++ /dev/null
@@ -1,75 +0,0 @@
-<?php
-declare(strict_types = 1);
-namespace TYPO3\CMS\Dashboard\Widgets;
-
-/*
- * This file is part of the TYPO3 CMS project.
- *
- * It is free software; you can redistribute it and/or modify it under
- * the terms of the GNU General Public License, either version 2
- * of the License, or any later version.
- *
- * For the full copyright and license information, please read the
- * LICENSE.txt file that was distributed with this source code.
- *
- * The TYPO3 project - inspiring people to share!
- */
-
-/**
- * The AbstractListWidget class is the basic widget class for structured content.
- * It is possible to extend this class for custom widgets.
- * In your class you have to set $this->items with the data to display.
- */
-abstract class AbstractListWidget extends AbstractWidget
-{
-    /**
-     * This property should contain the items to be displayed
-     *
-     * @var array
-     */
-    protected $items = [];
-
-    /**
-     * Limit the amount of items to be displayed
-     *
-     * @var int
-     */
-    protected $limit = 5;
-
-    /**
-     * Link to e.g. an external resource with the full items list
-     *
-     * @var string
-     */
-    protected $moreItemsLink = '';
-
-    /**
-     * This should be the text for the more items link
-     *
-     * @var string
-     */
-    protected $moreItemsText = '';
-
-    /**
-     * @var int
-     */
-    protected $height = 4;
-
-    /**
-     * @var string
-     */
-    protected $iconIdentifier = 'content-widget-list';
-
-    /**
-     * @var string
-     */
-    protected $templateName = 'ListWidget';
-
-    public function renderWidgetContent(): string
-    {
-        $this->view->assign('items', $this->items);
-        $this->view->assign('moreItemsLink', $this->moreItemsLink);
-        $this->view->assign('moreItemsText', $this->moreItemsText);
-        return $this->view->render();
-    }
-}
diff --git a/typo3/sysext/dashboard/Classes/Widgets/AbstractNumberWithIconWidget.php b/typo3/sysext/dashboard/Classes/Widgets/AbstractNumberWithIconWidget.php
deleted file mode 100644
index cb1672939f38..000000000000
--- a/typo3/sysext/dashboard/Classes/Widgets/AbstractNumberWithIconWidget.php
+++ /dev/null
@@ -1,68 +0,0 @@
-<?php
-declare(strict_types = 1);
-namespace TYPO3\CMS\Dashboard\Widgets;
-
-/*
- * This file is part of the TYPO3 CMS project.
- *
- * It is free software; you can redistribute it and/or modify it under
- * the terms of the GNU General Public License, either version 2
- * of the License, or any later version.
- *
- * For the full copyright and license information, please read the
- * LICENSE.txt file that was distributed with this source code.
- *
- * The TYPO3 project - inspiring people to share!
- */
-
-/**
- * The AbstractNumberWithIconWidget class is the basic widget class to display a number next to an icon.
- * It is possible to extend this class for own widgets.
- * Simply overwrite $this->subtitle, $this->number and $this->icon to make use of this widget type.
- */
-abstract class AbstractNumberWithIconWidget extends AbstractWidget
-{
-    /**
-     * When filled, a subtitle is shown below the title
-     *
-     * @var string
-     */
-    protected $subtitle;
-
-    /**
-     * This number will be the main data in the widget
-     *
-     * @var int
-     */
-    protected $number;
-
-    /**
-     * This property contains the identifier of the icon that should be shown in the widget
-     *
-     * @var string
-     */
-    protected $icon;
-
-    /**
-     * @var string
-     */
-    protected $iconIdentifier = 'content-widget-number';
-
-    /**
-     * @var string
-     */
-    protected $templateName = 'NumberWithIconWidget';
-
-    protected function initializeView(): void
-    {
-        parent::initializeView();
-        $this->view->assign('icon', $this->icon);
-        $this->view->assign('subtitle', $this->getSubTitle());
-        $this->view->assign('number', $this->number);
-    }
-
-    public function getSubTitle(): string
-    {
-        return $this->getLanguageService()->sL($this->subtitle) ?: $this->subtitle;
-    }
-}
diff --git a/typo3/sysext/dashboard/Classes/Widgets/AbstractRssWidget.php b/typo3/sysext/dashboard/Classes/Widgets/AbstractRssWidget.php
deleted file mode 100644
index 55a2be2983e1..000000000000
--- a/typo3/sysext/dashboard/Classes/Widgets/AbstractRssWidget.php
+++ /dev/null
@@ -1,105 +0,0 @@
-<?php
-declare(strict_types = 1);
-namespace TYPO3\CMS\Dashboard\Widgets;
-
-/*
- * This file is part of the TYPO3 CMS project.
- *
- * It is free software; you can redistribute it and/or modify it under
- * the terms of the GNU General Public License, either version 2
- * of the License, or any later version.
- *
- * For the full copyright and license information, please read the
- * LICENSE.txt file that was distributed with this source code.
- *
- * The TYPO3 project - inspiring people to share!
- */
-
-use RuntimeException;
-use TYPO3\CMS\Core\Cache\CacheManager;
-use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
-
-/**
- * The AbstractRssWidget class is the basic widget class to display items from a RSS feed.
- * It is possible to extend this class for custom widgets.
- * In your class you have to set $this->rssFile with the URL to the feed.
- */
-abstract class AbstractRssWidget extends AbstractListWidget
-{
-    /**
-     * The rss file (resource) the data should be fetched from
-     *
-     * @var string
-     */
-    protected $rssFile = '';
-
-    /**
-     * Lifetime of the items cache
-     *
-     * @var int
-     */
-    protected $lifeTime = 900;
-
-    /**
-     * @var int
-     */
-    protected $height = 6;
-
-    /**
-     * @var int
-     */
-    protected $width = 4;
-
-    /**
-     * @var string
-     */
-    protected $iconIdentifier = 'content-widget-rss';
-
-    /**
-     * @var string
-     */
-    protected $templateName = 'RssWidget';
-
-    /**
-     * @var FrontendInterface
-     */
-    protected $cache;
-
-    public function __construct(string $identifier)
-    {
-        AbstractListWidget::__construct($identifier);
-        $this->cache = GeneralUtility::makeInstance(CacheManager::class)
-            ->getCache('dashboard_rss');
-        $this->loadRssFeed();
-    }
-
-    protected function loadRssFeed(): void
-    {
-        $cacheHash = md5($this->rssFile);
-        if ($this->items = $this->cache->get($cacheHash)) {
-            return;
-        }
-
-        $rssContent = GeneralUtility::getUrl($this->rssFile);
-        if ($rssContent === false) {
-            throw new RuntimeException('RSS URL could not be fetched', 1573385431);
-        }
-        $rssFeed = simplexml_load_string($rssContent);
-        $itemCount = 0;
-        foreach ($rssFeed->channel->item as $item) {
-            if ($itemCount < $this->limit) {
-                $this->items[] = [
-                    'title' => (string)$item->title,
-                    'link' => trim((string)$item->link),
-                    'pubDate' => (string)$item->pubDate,
-                    'description' => (string)$item->description,
-                ];
-            } else {
-                continue;
-            }
-            $itemCount++;
-        }
-        $this->cache->set($cacheHash, $this->items, ['dashboard_rss'], $this->lifeTime);
-    }
-}
diff --git a/typo3/sysext/dashboard/Classes/Widgets/AbstractWidget.php b/typo3/sysext/dashboard/Classes/Widgets/AbstractWidget.php
deleted file mode 100644
index c55f7f7c04d7..000000000000
--- a/typo3/sysext/dashboard/Classes/Widgets/AbstractWidget.php
+++ /dev/null
@@ -1,164 +0,0 @@
-<?php
-declare(strict_types = 1);
-namespace TYPO3\CMS\Dashboard\Widgets;
-
-/*
- * This file is part of the TYPO3 CMS project.
- *
- * It is free software; you can redistribute it and/or modify it under
- * the terms of the GNU General Public License, either version 2
- * of the License, or any later version.
- *
- * For the full copyright and license information, please read the
- * LICENSE.txt file that was distributed with this source code.
- *
- * The TYPO3 project - inspiring people to share!
- */
-
-use TYPO3\CMS\Core\Localization\LanguageService;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
-use TYPO3\CMS\Dashboard\Widgets\Interfaces\WidgetInterface;
-use TYPO3\CMS\Fluid\View\StandaloneView;
-use TYPO3Fluid\Fluid\View\ViewInterface;
-
-/**
- * The AbstractWidget class is the basic widget class for all widgets.
- * It is possible to extend this class for custom widgets, but EXT:dashboard provides
- * some more specific types of widgets to extend from. For more details, please check:
- *
- * @see AbstractBarChartWidget
- * @see AbstractChartWidget
- * @see AbstractCtaButtonWidget
- * @see AbstractDoughnutChartWidget
- * @see AbstractListWidget
- * @see AbstractNumberWithIconWidget
- * @see AbstractRssWidget
- */
-abstract class AbstractWidget implements WidgetInterface
-{
-    /**
-     * The unique identifier of the widget
-     *
-     * @var string
-     */
-    protected $identifier;
-
-    /**
-     * The title is used for the widget selector
-     *
-     * @var string
-     */
-    protected $title;
-
-    /**
-     * The description is used for the widget selector
-     *
-     * @var string
-     */
-    protected $description = '';
-
-    /**
-     * The height of the widget in rows (1-6)
-     *
-     * @var int
-     */
-    protected $height = 2;
-
-    /**
-     * The width of the widget in rows (1-4)
-     *
-     * @var int
-     */
-    protected $width = 2;
-
-    /**
-     * The icon identifier is used for the widget selector
-     *
-     * @var string
-     */
-    protected $iconIdentifier = '';
-
-    /**
-     * The template name of the widget
-     *
-     * @var string
-     */
-    protected $templateName = 'Widget';
-
-    /**
-     * Additional CSS classes which should be added to the rendered widget
-     *
-     * @var string
-     */
-    protected $additionalClasses = '';
-
-    /**
-     * @var ViewInterface
-     */
-    protected $view;
-
-    public function __construct(string $identifier)
-    {
-        $this->identifier = $identifier;
-
-        $this->initializeView();
-    }
-
-    public function getIdentifier(): string
-    {
-        return $this->identifier;
-    }
-
-    public function getTitle(): string
-    {
-        return $this->getLanguageService()->sL($this->title) ?: $this->title;
-    }
-
-    public function getDescription(): string
-    {
-        return $this->getLanguageService()->sL($this->description) ?: $this->description;
-    }
-
-    public function getIconIdentifier(): string
-    {
-        return $this->iconIdentifier;
-    }
-
-    public function getHeight(): int
-    {
-        return $this->height;
-    }
-
-    public function getWidth(): int
-    {
-        return $this->width;
-    }
-
-    public function renderWidgetContent(): string
-    {
-        return $this->view->render();
-    }
-
-    public function getAdditionalClasses(): string
-    {
-        return $this->additionalClasses;
-    }
-
-    protected function initializeView(): void
-    {
-        $this->view = GeneralUtility::makeInstance(StandaloneView::class);
-        $this->view->setTemplate('Widget/' . $this->templateName);
-
-        $dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['USdateFormat'] ? '%m-%d-%Y' : '%d-%m-%Y';
-        $this->view->assign('dateFormat', $dateFormat);
-
-        $this->view->getRenderingContext()->getTemplatePaths()->fillDefaultsByPackageName('dashboard');
-
-        $this->view->assign('title', $this->getTitle());
-    }
-
-    protected function getLanguageService(): LanguageService
-    {
-        return $GLOBALS['LANG'];
-    }
-}
diff --git a/typo3/sysext/dashboard/Classes/Widgets/BarChartWidget.php b/typo3/sysext/dashboard/Classes/Widgets/BarChartWidget.php
new file mode 100644
index 000000000000..66052161b892
--- /dev/null
+++ b/typo3/sysext/dashboard/Classes/Widgets/BarChartWidget.php
@@ -0,0 +1,135 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Dashboard\Widgets;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Dashboard\Utility\ButtonUtility;
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\AdditionalCssInterface;
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\ButtonProviderInterface;
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\ChartDataProviderInterface;
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\EventDataInterface;
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\RequireJsModuleInterface;
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\WidgetConfigurationInterface;
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\WidgetInterface;
+use TYPO3\CMS\Fluid\View\StandaloneView;
+
+/**
+ * Concrete Bar Chart widget implementation
+ *
+ * Shows a widget with a bar chart. The data for this chart will be provided by the data provider you will set.
+ * You can add a button to the widget by defining a button provider.
+ *
+ * There are no options available for this widget
+ *
+ * @see ChartDataProviderInterface
+ * @see ButtonProviderInterface
+ */
+class BarChartWidget implements WidgetInterface, EventDataInterface, AdditionalCssInterface, RequireJsModuleInterface
+{
+    /**
+     * @var WidgetConfigurationInterface
+     */
+    private $configuration;
+
+    /**
+     * @var ChartDataProviderInterface
+     */
+    private $dataProvider;
+
+    /**
+     * @var StandaloneView
+     */
+    private $view;
+
+    /**
+     * @var ButtonProviderInterface|null
+     */
+    private $buttonProvider;
+
+    /**
+     * @var array
+     */
+    private $options;
+
+    public function __construct(
+        WidgetConfigurationInterface $configuration,
+        ChartDataProviderInterface $dataProvider,
+        StandaloneView $view,
+        $buttonProvider = null,
+        array $options = []
+    ) {
+        $this->configuration = $configuration;
+        $this->dataProvider = $dataProvider;
+        $this->view = $view;
+        $this->options = $options;
+        $this->buttonProvider = $buttonProvider;
+    }
+
+    public function renderWidgetContent(): string
+    {
+        $this->view->setTemplate('Widget/ChartWidget');
+        $this->view->assignMultiple([
+            'button' => ButtonUtility::generateButtonConfig($this->buttonProvider),
+            'options' => $this->options,
+            'configuration' => $this->configuration,
+        ]);
+        return $this->view->render();
+    }
+
+    public function getEventData(): array
+    {
+        return [
+            'graphConfig' => [
+                'type' => 'bar',
+                'options' => [
+                    'maintainAspectRatio' => false,
+                    'legend' => [
+                        'display' => false,
+                    ],
+                    'scales' => [
+                        'yAxes' => [
+                            [
+                                'ticks' => [
+                                    'beginAtZero' => true,
+                                ],
+                            ],
+                        ],
+                        'xAxes' => [
+                            [
+                                'ticks' => [
+                                    'maxTicksLimit' => 15,
+                                ],
+                            ],
+                        ],
+                    ],
+                ],
+                'data' => $this->dataProvider->getChartData(),
+            ],
+        ];
+    }
+
+    public function getCssFiles(): array
+    {
+        return ['EXT:dashboard/Resources/Public/Css/Contrib/chart.css'];
+    }
+
+    public function getRequireJsModules(): array
+    {
+        return [
+            'TYPO3/CMS/Dashboard/Contrib/chartjs',
+            'TYPO3/CMS/Dashboard/ChartInitializer',
+        ];
+    }
+}
diff --git a/typo3/sysext/dashboard/Classes/Widgets/CtaWidget.php b/typo3/sysext/dashboard/Classes/Widgets/CtaWidget.php
new file mode 100644
index 000000000000..aeb490d78940
--- /dev/null
+++ b/typo3/sysext/dashboard/Classes/Widgets/CtaWidget.php
@@ -0,0 +1,81 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Dashboard\Widgets;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Dashboard\Utility\ButtonUtility;
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\ButtonProviderInterface;
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\WidgetConfigurationInterface;
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\WidgetInterface;
+use TYPO3\CMS\Fluid\View\StandaloneView;
+
+/**
+ * Concrete CTA button implementation
+ *
+ * Shows a widget with a CTA button to easily go to a specific page or do a specific action. You can add a button to the
+ * widget by defining a button provider.
+ *
+ * The following options are available during registration:
+ * - text           string          Adds a text to the widget to give some more background information about
+ *                                  what a user can expect when clicking the button. You can either enter a
+ *                                  normal string or a translation string (eg. LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.gettingStarted.text)
+ * @see ButtonProviderInterface
+ */
+class CtaWidget implements WidgetInterface
+{
+    /**
+     * @var WidgetConfigurationInterface
+     */
+    private $configuration;
+
+    /**
+     * @var StandaloneView
+     */
+    private $view;
+
+    /**
+     * @var array
+     */
+    private $options;
+
+    /**
+     * @var ButtonProviderInterface|null
+     */
+    private $buttonProvider;
+
+    public function __construct(
+        WidgetConfigurationInterface $configuration,
+        StandaloneView $view,
+        $buttonProvider = null,
+        array $options = []
+    ) {
+        $this->configuration = $configuration;
+        $this->view = $view;
+        $this->options =  array_merge(['text' => ''], $options);
+        $this->buttonProvider = $buttonProvider;
+    }
+
+    public function renderWidgetContent(): string
+    {
+        $this->view->setTemplate('Widget/CtaWidget');
+        $this->view->assignMultiple([
+            'text' => $this->options['text'],
+            'options' => $this->options,
+            'button' => ButtonUtility::generateButtonConfig($this->buttonProvider),
+            'configuration' => $this->configuration
+        ]);
+        return $this->view->render();
+    }
+}
diff --git a/typo3/sysext/dashboard/Classes/Widgets/DocumentationGettingStartedWidget.php b/typo3/sysext/dashboard/Classes/Widgets/DocumentationGettingStartedWidget.php
deleted file mode 100644
index 9f4b4190e8d3..000000000000
--- a/typo3/sysext/dashboard/Classes/Widgets/DocumentationGettingStartedWidget.php
+++ /dev/null
@@ -1,53 +0,0 @@
-<?php
-declare(strict_types = 1);
-namespace TYPO3\CMS\Dashboard\Widgets;
-
-/*
- * This file is part of the TYPO3 CMS project.
- *
- * It is free software; you can redistribute it and/or modify it under
- * the terms of the GNU General Public License, either version 2
- * of the License, or any later version.
- *
- * For the full copyright and license information, please read the
- * LICENSE.txt file that was distributed with this source code.
- *
- * The TYPO3 project - inspiring people to share!
- */
-
-/**
- * This widget will show a link to the documentation of TYPO3 to make it easier for people
- * to find the right documentation.
- */
-class DocumentationGettingStartedWidget extends AbstractCtaButtonWidget
-{
-    /**
-     * @var string
-     */
-    protected $title = 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.gettingStarted.title';
-
-    /**
-     * @var string
-     */
-    protected $text = 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.gettingStarted.text';
-
-    /**
-     * @var string
-     */
-    protected $description = 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.gettingStarted.description';
-
-    /**
-     * @var string
-     */
-    protected $label = 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.gettingStarted.content.label';
-
-    /**
-     * @var string
-     */
-    protected $link = 'https://docs.typo3.org/m/typo3/tutorial-getting-started/master/en-us/Index.html';
-
-    /**
-     * @var string
-     */
-    protected $iconIdentifier = 'content-widget-text';
-}
diff --git a/typo3/sysext/dashboard/Classes/Widgets/DocumentationTSconfigReferenceWidget.php b/typo3/sysext/dashboard/Classes/Widgets/DocumentationTSconfigReferenceWidget.php
deleted file mode 100644
index c3038d202462..000000000000
--- a/typo3/sysext/dashboard/Classes/Widgets/DocumentationTSconfigReferenceWidget.php
+++ /dev/null
@@ -1,53 +0,0 @@
-<?php
-declare(strict_types = 1);
-namespace TYPO3\CMS\Dashboard\Widgets;
-
-/*
- * This file is part of the TYPO3 CMS project.
- *
- * It is free software; you can redistribute it and/or modify it under
- * the terms of the GNU General Public License, either version 2
- * of the License, or any later version.
- *
- * For the full copyright and license information, please read the
- * LICENSE.txt file that was distributed with this source code.
- *
- * The TYPO3 project - inspiring people to share!
- */
-
-/**
- * This widget will show a link to the documentation of TYPO3 to make it easier for people
- * to find the right documentation.
- */
-class DocumentationTSconfigReferenceWidget extends AbstractCtaButtonWidget
-{
-    /**
-     * @var string
-     */
-    protected $title = 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.TSconfigReference.title';
-
-    /**
-     * @var string
-     */
-    protected $text = 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.TSconfigReference.text';
-
-    /**
-     * @var string
-     */
-    protected $description = 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.TSconfigReference.description';
-
-    /**
-     * @var string
-     */
-    protected $label = 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.TSconfigReference.content.label';
-
-    /**
-     * @var string
-     */
-    protected $link = 'https://docs.typo3.org/m/typo3/reference-tsconfig/master/en-us/Index.html';
-
-    /**
-     * @var string
-     */
-    protected $iconIdentifier = 'content-widget-text';
-}
diff --git a/typo3/sysext/dashboard/Classes/Widgets/DocumentationTypoScriptReference.php b/typo3/sysext/dashboard/Classes/Widgets/DocumentationTypoScriptReference.php
deleted file mode 100644
index b816da81b2e4..000000000000
--- a/typo3/sysext/dashboard/Classes/Widgets/DocumentationTypoScriptReference.php
+++ /dev/null
@@ -1,53 +0,0 @@
-<?php
-declare(strict_types = 1);
-namespace TYPO3\CMS\Dashboard\Widgets;
-
-/*
- * This file is part of the TYPO3 CMS project.
- *
- * It is free software; you can redistribute it and/or modify it under
- * the terms of the GNU General Public License, either version 2
- * of the License, or any later version.
- *
- * For the full copyright and license information, please read the
- * LICENSE.txt file that was distributed with this source code.
- *
- * The TYPO3 project - inspiring people to share!
- */
-
-/**
- * This widget will show a link to the documentation of TYPO3 to make it easier for people
- * to find the right documentation.
- */
-class DocumentationTypoScriptReference extends AbstractCtaButtonWidget
-{
-    /**
-     * @var string
-     */
-    protected $title = 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.typoscriptReference.title';
-
-    /**
-     * @var string
-     */
-    protected $text = 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.typoscriptReference.text';
-
-    /**
-     * @var string
-     */
-    protected $description = 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.typoscriptReference.description';
-
-    /**
-     * @var string
-     */
-    protected $label = 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.typoscriptReference.content.label';
-
-    /**
-     * @var string
-     */
-    protected $link = 'https://docs.typo3.org/m/typo3/reference-typoscript/master/en-us/Index.html';
-
-    /**
-     * @var string
-     */
-    protected $iconIdentifier = 'content-widget-text';
-}
diff --git a/typo3/sysext/dashboard/Classes/Widgets/DoughnutChartWidget.php b/typo3/sysext/dashboard/Classes/Widgets/DoughnutChartWidget.php
new file mode 100644
index 000000000000..8a2860302035
--- /dev/null
+++ b/typo3/sysext/dashboard/Classes/Widgets/DoughnutChartWidget.php
@@ -0,0 +1,121 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Dashboard\Widgets;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Dashboard\Utility\ButtonUtility;
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\AdditionalCssInterface;
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\ButtonProviderInterface;
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\ChartDataProviderInterface;
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\EventDataInterface;
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\RequireJsModuleInterface;
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\WidgetConfigurationInterface;
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\WidgetInterface;
+use TYPO3\CMS\Fluid\View\StandaloneView;
+
+/**
+ * Concrete Doughnut Chart widget implementation
+ *
+ * Shows a widget with a doughnut chart. The data for this chart will be provided by the data provider you will set.
+ * You can add a button to the widget by defining a button provider.
+ *
+ * There are no options available for this widget
+ *
+ * @see ChartDataProviderInterface
+ * @see ButtonProviderInterface
+ */
+class DoughnutChartWidget implements WidgetInterface, EventDataInterface, AdditionalCssInterface, RequireJsModuleInterface
+{
+    /**
+     * @var WidgetConfigurationInterface
+     */
+    private $configuration;
+
+    /**
+     * @var ChartDataProviderInterface
+     */
+    private $dataProvider;
+
+    /**
+     * @var StandaloneView
+     */
+    private $view;
+
+    /**
+     * @var ButtonProviderInterface|null
+     */
+    private $buttonProvider;
+
+    /**
+     * @var array
+     */
+    private $options;
+
+    public function __construct(
+        WidgetConfigurationInterface $configuration,
+        ChartDataProviderInterface $dataProvider,
+        StandaloneView $view,
+        $buttonProvider = null,
+        array $options = []
+    ) {
+        $this->configuration = $configuration;
+        $this->dataProvider = $dataProvider;
+        $this->view = $view;
+        $this->options = $options;
+        $this->buttonProvider = $buttonProvider;
+    }
+
+    public function renderWidgetContent(): string
+    {
+        $this->view->setTemplate('Widget/ChartWidget');
+        $this->view->assignMultiple([
+            'button' => ButtonUtility::generateButtonConfig($this->buttonProvider),
+            'options' => $this->options,
+            'configuration' => $this->configuration,
+        ]);
+        return $this->view->render();
+    }
+
+    public function getEventData(): array
+    {
+        return [
+            'graphConfig' => [
+                'type' => 'doughnut',
+                'options' => [
+                    'maintainAspectRatio' => false,
+                    'legend' => [
+                        'display' => true,
+                        'position' => 'bottom'
+                    ],
+                    'cutoutPercentage' => 60
+                ],
+                'data' => $this->dataProvider->getChartData(),
+            ],
+        ];
+    }
+
+    public function getCssFiles(): array
+    {
+        return ['EXT:dashboard/Resources/Public/Css/Contrib/chart.css'];
+    }
+
+    public function getRequireJsModules(): array
+    {
+        return [
+            'TYPO3/CMS/Dashboard/Contrib/chartjs',
+            'TYPO3/CMS/Dashboard/ChartInitializer',
+        ];
+    }
+}
diff --git a/typo3/sysext/dashboard/Classes/Widgets/Interfaces/AdditionalCssInterface.php b/typo3/sysext/dashboard/Classes/Widgets/Interfaces/AdditionalCssInterface.php
index 83d152c7bd60..2e7e95c359ad 100644
--- a/typo3/sysext/dashboard/Classes/Widgets/Interfaces/AdditionalCssInterface.php
+++ b/typo3/sysext/dashboard/Classes/Widgets/Interfaces/AdditionalCssInterface.php
@@ -2,8 +2,20 @@
 declare(strict_types = 1);
 namespace TYPO3\CMS\Dashboard\Widgets\Interfaces;
 
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
 /**
- * Interface AdditionalCssInterface
  * In case a widget should provide additional CSS files, the widget must implement this interface.
  */
 interface AdditionalCssInterface
@@ -11,6 +23,7 @@ interface AdditionalCssInterface
     /**
      * This method returns an array with paths to required CSS files.
      * e.g. ['EXT:myext/Resources/Public/Css/my_widget.css']
+     *
      * @return array
      */
     public function getCssFiles(): array;
diff --git a/typo3/sysext/dashboard/Classes/Widgets/Interfaces/AdditionalJavaScriptInterface.php b/typo3/sysext/dashboard/Classes/Widgets/Interfaces/AdditionalJavaScriptInterface.php
index 6691a2bf56d4..acb38d693233 100644
--- a/typo3/sysext/dashboard/Classes/Widgets/Interfaces/AdditionalJavaScriptInterface.php
+++ b/typo3/sysext/dashboard/Classes/Widgets/Interfaces/AdditionalJavaScriptInterface.php
@@ -2,8 +2,20 @@
 declare(strict_types = 1);
 namespace TYPO3\CMS\Dashboard\Widgets\Interfaces;
 
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
 /**
- * Interface AdditionalJavaScriptInterface
  * In case a widget should provide additional JavaScript files, the widget must implement this interface.
  */
 interface AdditionalJavaScriptInterface
@@ -11,6 +23,7 @@ interface AdditionalJavaScriptInterface
     /**
      * This method returns an array with paths to required JS files.
      * e.g. ['EXT:myext/Resources/Public/JavaScript/my_widget.js']
+     *
      * @return array
      */
     public function getJsFiles(): array;
diff --git a/typo3/sysext/dashboard/Classes/Widgets/Interfaces/ButtonProviderInterface.php b/typo3/sysext/dashboard/Classes/Widgets/Interfaces/ButtonProviderInterface.php
new file mode 100644
index 000000000000..a308a6ade615
--- /dev/null
+++ b/typo3/sysext/dashboard/Classes/Widgets/Interfaces/ButtonProviderInterface.php
@@ -0,0 +1,45 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Dashboard\Widgets\Interfaces;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * In case a widget should have a button in the footer of the widget, this button must implement this interface.
+ */
+interface ButtonProviderInterface
+{
+    /**
+     * This method should return the title that will be shown as the text on the button. As the title will be
+     * translated within the template, you can also return a localization string like
+     * 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:button'
+     *
+     * @return string
+     */
+    public function getTitle(): string;
+
+    /**
+     * Return the link
+     *
+     * @return string
+     */
+    public function getLink(): string;
+
+    /**
+     * Specify the target of the link like '_blank'
+     *
+     * @return string
+     */
+    public function getTarget(): string;
+}
diff --git a/typo3/sysext/dashboard/Classes/Widgets/Interfaces/ChartDataProviderInterface.php b/typo3/sysext/dashboard/Classes/Widgets/Interfaces/ChartDataProviderInterface.php
new file mode 100644
index 000000000000..78ee384ba8e8
--- /dev/null
+++ b/typo3/sysext/dashboard/Classes/Widgets/Interfaces/ChartDataProviderInterface.php
@@ -0,0 +1,34 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Dashboard\Widgets\Interfaces;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Defines API for provider, used for chart widgets.
+ */
+interface ChartDataProviderInterface
+{
+    /**
+     * This method should provide the data for the graph.
+     * The data and options you have depend on the type of chart.
+     * More information can be found in the documentation of the specific type.
+     *
+     * @link https://www.chartjs.org/docs/latest/charts/bar.html#data-structure
+     * @link https://www.chartjs.org/docs/latest/charts/doughnut.html#data-structure
+     *
+     * @return array
+     */
+    public function getChartData(): array;
+}
diff --git a/typo3/sysext/dashboard/Classes/Widgets/Interfaces/EventDataInterface.php b/typo3/sysext/dashboard/Classes/Widgets/Interfaces/EventDataInterface.php
index fae69e9c6d56..b8ee4398c5b3 100644
--- a/typo3/sysext/dashboard/Classes/Widgets/Interfaces/EventDataInterface.php
+++ b/typo3/sysext/dashboard/Classes/Widgets/Interfaces/EventDataInterface.php
@@ -2,14 +2,27 @@
 declare(strict_types = 1);
 namespace TYPO3\CMS\Dashboard\Widgets\Interfaces;
 
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
 /**
- * Interface EventDataInterface
  * In case a widget should provide additional data as JSON payload, the widget must implement this interface.
  */
 interface EventDataInterface
 {
     /**
      * This method returns data which should be send to the widget as JSON encoded value.
+     *
      * @return array
      */
     public function getEventData(): array;
diff --git a/typo3/sysext/dashboard/Classes/Widgets/Interfaces/ListDataProviderInterface.php b/typo3/sysext/dashboard/Classes/Widgets/Interfaces/ListDataProviderInterface.php
new file mode 100644
index 000000000000..e2899767c21c
--- /dev/null
+++ b/typo3/sysext/dashboard/Classes/Widgets/Interfaces/ListDataProviderInterface.php
@@ -0,0 +1,30 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Dashboard\Widgets\Interfaces;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * The dataprovider of a ListWidget, should implement this interface
+ */
+interface ListDataProviderInterface
+{
+    /**
+     * Return the items to be shown. This should be an array like ['item 1', 'item 2', 'item 3']. This is a
+     * real simple list of items.
+     *
+     * @return array
+     */
+    public function getItems(): array;
+}
diff --git a/typo3/sysext/dashboard/Classes/Widgets/Interfaces/NumberWithIconDataProviderInterface.php b/typo3/sysext/dashboard/Classes/Widgets/Interfaces/NumberWithIconDataProviderInterface.php
new file mode 100644
index 000000000000..21e6dbb4a6f1
--- /dev/null
+++ b/typo3/sysext/dashboard/Classes/Widgets/Interfaces/NumberWithIconDataProviderInterface.php
@@ -0,0 +1,29 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Dashboard\Widgets\Interfaces;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * The dataprovider of a NumberWithIcon widget should implement this interface
+ */
+interface NumberWithIconDataProviderInterface
+{
+    /**
+     * Return the number that should be shown in the widget
+     *
+     * @return int
+     */
+    public function getNumber(): int;
+}
diff --git a/typo3/sysext/dashboard/Classes/Widgets/Interfaces/RequireJsModuleInterface.php b/typo3/sysext/dashboard/Classes/Widgets/Interfaces/RequireJsModuleInterface.php
index 7add78da29a7..71071bc4efcc 100644
--- a/typo3/sysext/dashboard/Classes/Widgets/Interfaces/RequireJsModuleInterface.php
+++ b/typo3/sysext/dashboard/Classes/Widgets/Interfaces/RequireJsModuleInterface.php
@@ -2,8 +2,20 @@
 declare(strict_types = 1);
 namespace TYPO3\CMS\Dashboard\Widgets\Interfaces;
 
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
 /**
- * Interface RequireJsModuleInterface
  * In case a widget should provide additional requireJS modules, the widget must implement this interface.
  */
 interface RequireJsModuleInterface
@@ -14,6 +26,7 @@ interface RequireJsModuleInterface
      *   'TYPO3/CMS/Backend/Modal',
      *   'TYPO3/CMS/MyExt/FooBar' => 'function(FooBar) { ... }'
      * ]
+     *
      * @return array
      */
     public function getRequireJsModules(): array;
diff --git a/typo3/sysext/dashboard/Classes/Widgets/Interfaces/WidgetConfigurationInterface.php b/typo3/sysext/dashboard/Classes/Widgets/Interfaces/WidgetConfigurationInterface.php
new file mode 100644
index 000000000000..37e0964f56eb
--- /dev/null
+++ b/typo3/sysext/dashboard/Classes/Widgets/Interfaces/WidgetConfigurationInterface.php
@@ -0,0 +1,87 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Dashboard\Widgets\Interfaces;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Defines API of configuration for a widget.
+ * This is separated by concrete implementation of a widget (WidgetInterface).
+ * The configuration is used to generate UX, and other stuff.
+ */
+interface WidgetConfigurationInterface
+{
+    /**
+     * Returns the unique identifer of a widget
+     *
+     * @return string
+     */
+    public function getIdentifier(): string;
+
+    /**
+     * Returns the service name providing the widget implementation
+     *
+     * @return string
+     */
+    public function getServiceName(): string;
+
+    /**
+     * Returns array of group names associated to this widget
+     *
+     * @return array
+     */
+    public function getGroupNames(): array;
+
+    /**
+     * Returns the title of a widget, this is used for the widget selector
+     *
+     * @return string
+     */
+    public function getTitle(): string;
+
+    /**
+     * Returns the description of a widget, this is used for the widget selector
+     *
+     * @return string
+     */
+    public function getDescription(): string;
+
+    /**
+     * Returns the icon identifier of a widget, this is used for the widget selector
+     *
+     * @return string
+     */
+    public function getIconIdentifier(): string;
+
+    /**
+     * Returns the height of a widget (small, medium, large)
+     *
+     * @return string
+     */
+    public function getHeight(): string;
+
+    /**
+     * Returns the width of a widget (small, medium, large)
+     *
+     * @return string
+     */
+    public function getWidth(): string;
+
+    /**
+     * This method returns additional CSS classes which should be added to the rendered widget
+     *
+     * @return string
+     */
+    public function getAdditionalCssClasses(): string;
+}
diff --git a/typo3/sysext/dashboard/Classes/Widgets/Interfaces/WidgetInterface.php b/typo3/sysext/dashboard/Classes/Widgets/Interfaces/WidgetInterface.php
index b122bb81d207..c59c6521d801 100644
--- a/typo3/sysext/dashboard/Classes/Widgets/Interfaces/WidgetInterface.php
+++ b/typo3/sysext/dashboard/Classes/Widgets/Interfaces/WidgetInterface.php
@@ -2,6 +2,19 @@
 declare(strict_types = 1);
 namespace TYPO3\CMS\Dashboard\Widgets\Interfaces;
 
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
 /**
  * The WidgetInterface is the base interface for all kind of widgets.
  * All widgets must implement this interface.
@@ -9,54 +22,12 @@ namespace TYPO3\CMS\Dashboard\Widgets\Interfaces;
  */
 interface WidgetInterface
 {
-    /**
-     * Returns the unique identifer of a widget
-     *
-     * @return string
-     */
-    public function getIdentifier(): string;
-
-    /**
-     * Returns the title of a widget, this is used for the widget selector
-     * @return string
-     */
-    public function getTitle(): string;
-
-    /**
-     * Returns the description of a widget, this is used for the widget selector
-     * @return string
-     */
-    public function getDescription(): string;
-
-    /**
-     * Returns the icon identifier of a widget, this is used for the widget selector
-     * @return string
-     */
-    public function getIconIdentifier(): string;
-
-    /**
-     * Returns the height of a widget in rows (1-6)
-     * @return int
-     */
-    public function getHeight(): int;
-
-    /**
-     * Returns the width of a widget in columns (1-4)
-     * @return int
-     */
-    public function getWidth(): int;
-
     /**
      * This method returns the content of a widget. The returned markup will be delivered
      * by an AJAX call and will not be escaped.
      * Be aware of XSS and ensure that the content is well encoded.
+     *
      * @return string
      */
     public function renderWidgetContent(): string;
-
-    /**
-     * This method returns additional CSS classes which should be added to the rendered widget
-     * @return string
-     */
-    public function getAdditionalClasses(): string;
 }
diff --git a/typo3/sysext/dashboard/Classes/Widgets/ListWidget.php b/typo3/sysext/dashboard/Classes/Widgets/ListWidget.php
new file mode 100644
index 000000000000..5ca2ea49f8e3
--- /dev/null
+++ b/typo3/sysext/dashboard/Classes/Widgets/ListWidget.php
@@ -0,0 +1,106 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Dashboard\Widgets;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\ButtonProviderInterface;
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\ListDataProviderInterface;
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\WidgetConfigurationInterface;
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\WidgetInterface;
+use TYPO3\CMS\Fluid\View\StandaloneView;
+
+/**
+ * Concrete List Widget implementation
+ *
+ * The widget will show a simple list with items provided by a data provider. You can add a button to the widget by
+ * defining a button provider.
+ *
+ * There are no options available for this widget
+ *
+ * @see ListDataProviderInterface
+ * @see ButtonProviderInterface
+ */
+class ListWidget implements WidgetInterface
+{
+    /**
+     * @var WidgetConfigurationInterface
+     */
+    private $configuration;
+
+    /**
+     * @var StandaloneView
+     */
+    private $view;
+
+    /**
+     * @var array
+     */
+    private $options;
+    /**
+     * @var ButtonProviderInterface|null
+     */
+    private $buttonProvider;
+
+    /**
+     * @var ListDataProviderInterface
+     */
+    private $dataProvider;
+
+    public function __construct(
+        WidgetConfigurationInterface $configuration,
+        ListDataProviderInterface $dataProvider,
+        StandaloneView $view,
+        $buttonProvider = null,
+        array $options = []
+    ) {
+        $this->configuration = $configuration;
+        $this->view = $view;
+        $this->options = $options;
+        $this->buttonProvider = $buttonProvider;
+        $this->dataProvider = $dataProvider;
+    }
+
+    public function renderWidgetContent(): string
+    {
+        $this->view->setTemplate('Widget/ListWidget');
+        $this->view->assignMultiple([
+            'items' => $this->getItems(),
+            'options' => $this->options,
+            'button' => $this->getButton(),
+            'configuration' => $this->configuration,
+        ]);
+        return $this->view->render();
+    }
+
+    protected function getItems(): array
+    {
+        return $this->dataProvider->getItems();
+    }
+
+    private function getButton(): array
+    {
+        $button = [];
+
+        if ($this->buttonProvider instanceof ButtonProviderInterface && $this->buttonProvider->getTitle() && $this->buttonProvider->getLink()) {
+            $button = [
+                'text' => $this->buttonProvider->getTitle(),
+                'link' => $this->buttonProvider->getLink(),
+                'target' => $this->buttonProvider->getTarget(),
+            ];
+        }
+
+        return $button;
+    }
+}
diff --git a/typo3/sysext/dashboard/Classes/Widgets/NumberWithIconWidget.php b/typo3/sysext/dashboard/Classes/Widgets/NumberWithIconWidget.php
new file mode 100644
index 000000000000..41446748d217
--- /dev/null
+++ b/typo3/sysext/dashboard/Classes/Widgets/NumberWithIconWidget.php
@@ -0,0 +1,88 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Dashboard\Widgets;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\NumberWithIconDataProviderInterface;
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\WidgetConfigurationInterface;
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\WidgetInterface;
+use TYPO3\CMS\Fluid\View\StandaloneView;
+
+/**
+ * Concrete Number with Icon implementation
+ *
+ * The widget will show widget with an icon, a number, a title and a subtitle. The number is provided by a data
+ * provider.
+ *
+ * The following options are available during registration:
+ * - icon           string        The icon-identifier of the icon that should be shown in the widget. You should
+ *                                register your icon with the Icon API
+ * - title          string        The main title that will be shown in the widget as an explanation of the shown number.
+ *                                You can either enter a normal string or a translation string
+ *                                (eg. LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.failedLogins.title)
+ * - subtitle       string        The subtitle that will give some additional information about the number and title.
+ *                                You can either enter a normal string or a translation string
+ *                                (eg. LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.failedLogins.subtitle)
+ *
+ * @see NumberWithIconDataProviderInterface
+ */
+class NumberWithIconWidget implements WidgetInterface
+{
+    /**
+     * @var WidgetConfigurationInterface
+     */
+    private $configuration;
+    /**
+     * @var StandaloneView
+     */
+    private $view;
+    /**
+     * @var array
+     */
+    private $options;
+    /**
+     * @var NumberWithIconDataProviderInterface
+     */
+    private $dataProvider;
+
+    public function __construct(
+        WidgetConfigurationInterface $configuration,
+        NumberWithIconDataProviderInterface $dataProvider,
+        StandaloneView $view,
+        array $options = []
+    ) {
+        $this->configuration = $configuration;
+        $this->view = $view;
+        $this->options = $options;
+        $this->dataProvider = $dataProvider;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function renderWidgetContent(): string
+    {
+        $this->view->setTemplate('Widget/NumberWithIconWidget');
+        $this->view->assignMultiple([
+            'icon' => $this->options['icon'],
+            'title' => $this->options['title'],
+            'subtitle' => $this->options['subtitle'],
+            'number' => $this->dataProvider->getNumber(),
+            'options' => $this->options,
+            'configuration' => $this->configuration
+        ]);
+        return $this->view->render();
+    }
+}
diff --git a/typo3/sysext/dashboard/Classes/Widgets/Provider/ButtonProvider.php b/typo3/sysext/dashboard/Classes/Widgets/Provider/ButtonProvider.php
new file mode 100644
index 000000000000..5a8a9d812489
--- /dev/null
+++ b/typo3/sysext/dashboard/Classes/Widgets/Provider/ButtonProvider.php
@@ -0,0 +1,63 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Dashboard\Widgets\Provider;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\ButtonProviderInterface;
+
+/**
+ * Provide link for sys log button.
+ * Check whether belog is enabled and add link to module.
+ * No link is returned if not enabled.
+ */
+class ButtonProvider implements ButtonProviderInterface
+{
+    /**
+     * @var string
+     */
+    private $title;
+
+    /**
+     * @var string
+     */
+    private $target;
+
+    /**
+     * @var string
+     */
+    private $link;
+
+    public function __construct(string $title, string $link, string $target = '')
+    {
+        $this->title = $title;
+        $this->target = $target;
+        $this->link = $link;
+    }
+
+    public function getTitle(): string
+    {
+        return $this->title;
+    }
+
+    public function getLink(): string
+    {
+        return $this->link;
+    }
+
+    public function getTarget(): string
+    {
+        return $this->target;
+    }
+}
diff --git a/typo3/sysext/dashboard/Classes/Widgets/FailedLoginsWidget.php b/typo3/sysext/dashboard/Classes/Widgets/Provider/NumberOfFailedLoginsDataProvider.php
similarity index 66%
rename from typo3/sysext/dashboard/Classes/Widgets/FailedLoginsWidget.php
rename to typo3/sysext/dashboard/Classes/Widgets/Provider/NumberOfFailedLoginsDataProvider.php
index 3ed002e9bf0b..159c52b03a2d 100644
--- a/typo3/sysext/dashboard/Classes/Widgets/FailedLoginsWidget.php
+++ b/typo3/sysext/dashboard/Classes/Widgets/Provider/NumberOfFailedLoginsDataProvider.php
@@ -1,6 +1,6 @@
 <?php
 declare(strict_types = 1);
-namespace TYPO3\CMS\Dashboard\Widgets;
+namespace TYPO3\CMS\Dashboard\Widgets\Provider;
 
 /*
  * This file is part of the TYPO3 CMS project.
@@ -21,39 +21,11 @@ use TYPO3\CMS\Core\SysLog\Action\Login as SystemLogLoginAction;
 use TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
 use TYPO3\CMS\Core\SysLog\Type as SystemLogType;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\NumberWithIconDataProviderInterface;
 
-/**
- * This widget will show the number of failed logins during a given period
- */
-class FailedLoginsWidget extends AbstractNumberWithIconWidget
+class NumberOfFailedLoginsDataProvider implements NumberWithIconDataProviderInterface
 {
-    /**
-     * @var string
-     */
-    protected $title = 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.failedLogins.title';
-
-    /**
-     * @var string
-     */
-    protected $description = 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.failedLogins.description';
-
-    /**
-     * @var string
-     */
-    protected $subtitle = 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.failedLogins.subtitle';
-
-    /**
-     * @var string
-     */
-    protected $icon = 'content-elements-login';
-
-    protected function initializeView(): void
-    {
-        $this->number = $this->getNumberOfFailedLogins();
-        parent::initializeView();
-    }
-
-    public function getNumberOfFailedLogins(int $secondsBack = 86400): int
+    public function getNumber(int $secondsBack = 86400): int
     {
         $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
         $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_log');
diff --git a/typo3/sysext/dashboard/Classes/Widgets/Provider/SysLogButtonProvider.php b/typo3/sysext/dashboard/Classes/Widgets/Provider/SysLogButtonProvider.php
new file mode 100644
index 000000000000..6be6af5c5a9d
--- /dev/null
+++ b/typo3/sysext/dashboard/Classes/Widgets/Provider/SysLogButtonProvider.php
@@ -0,0 +1,68 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Dashboard\Widgets\Provider;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Backend\Routing\UriBuilder;
+use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\ButtonProviderInterface;
+
+/**
+ * Provide link for sys log button.
+ * Check whether belog is enabled and add link to module.
+ * No link is returned if not enabled.
+ */
+class SysLogButtonProvider implements ButtonProviderInterface
+{
+    /**
+     * @var string
+     */
+    private $title;
+
+    /**
+     * @var string
+     */
+    private $target;
+
+    public function __construct(string $title, string $target = '')
+    {
+        $this->title = $title;
+        $this->target = $target;
+    }
+
+    public function getTitle(): string
+    {
+        return $this->title;
+    }
+
+    public function getLink(): string
+    {
+        if (ExtensionManagementUtility::isLoaded('belog')) {
+            $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
+            return (string)$uriBuilder->buildUriFromRoute(
+                'system_BelogLog',
+                ['tx_belog_system_beloglog[constraint][action]' => -1]
+            );
+        }
+
+        return '';
+    }
+
+    public function getTarget(): string
+    {
+        return $this->target;
+    }
+}
diff --git a/typo3/sysext/dashboard/Classes/Widgets/SysLogErrorsWidget.php b/typo3/sysext/dashboard/Classes/Widgets/Provider/SysLogErrorsDataProvider.php
similarity index 61%
rename from typo3/sysext/dashboard/Classes/Widgets/SysLogErrorsWidget.php
rename to typo3/sysext/dashboard/Classes/Widgets/Provider/SysLogErrorsDataProvider.php
index 96f16eedcaa2..0a6b70c0dd96 100644
--- a/typo3/sysext/dashboard/Classes/Widgets/SysLogErrorsWidget.php
+++ b/typo3/sysext/dashboard/Classes/Widgets/Provider/SysLogErrorsDataProvider.php
@@ -1,6 +1,6 @@
 <?php
 declare(strict_types = 1);
-namespace TYPO3\CMS\Dashboard\Widgets;
+namespace TYPO3\CMS\Dashboard\Widgets\Provider;
 
 /*
  * This file is part of the TYPO3 CMS project.
@@ -15,78 +15,54 @@ namespace TYPO3\CMS\Dashboard\Widgets;
  * The TYPO3 project - inspiring people to share!
  */
 
-use TYPO3\CMS\Backend\Routing\UriBuilder;
 use TYPO3\CMS\Core\Database\Connection;
 use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Localization\LanguageService;
 use TYPO3\CMS\Core\SysLog\Type as SystemLogType;
-use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Dashboard\WidgetApi;
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\ChartDataProviderInterface;
 
 /**
- * This widget will show the number of system log errors in a bar chart
+ * Provides chart data for sys log errors.
  */
-class SysLogErrorsWidget extends AbstractBarChartWidget
+class SysLogErrorsDataProvider implements ChartDataProviderInterface
 {
     /**
-     * @var string
-     */
-    protected $title = 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.sysLogErrors.title';
-
-    /**
-     * @var string
-     */
-    protected $description = 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.sysLogErrors.description';
-
-    /**
-     * @var string
-     */
-    protected $buttonText = 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.sysLogErrors.buttonText';
-
-    /**
+     * Number of days to gather information for.
+     *
      * @var int
      */
-    protected $width = 4;
+    protected $days = 31;
 
     /**
-     * @var int
+     * @var array
      */
-    protected $height = 4;
+    protected $labels = [];
 
     /**
-     * @var mixed[]
+     * @var array
      */
     protected $data = [];
 
-    /**
-     * @var mixed[]
-     */
-    protected $labels = [];
-
-    protected function initializeView(): void
+    public function __construct(int $days = 31)
     {
-        if (ExtensionManagementUtility::isLoaded('belog')) {
-            $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
-            $this->buttonLink = (string)$uriBuilder->buildUriFromRoute(
-                'system_BelogLog',
-                ['tx_belog_system_beloglog[constraint][action]' => -1]
-            );
-        }
-
-        parent::initializeView();
+        $this->days = $days;
     }
+
     /**
      * @inheritDoc
      */
-    protected function prepareChartData(): void
+    public function getChartData(): array
     {
-        $this->calculateDataForLastDays(31);
+        $this->calculateDataForLastDays();
 
-        $this->chartData = [
+        return [
             'labels' => $this->labels,
             'datasets' => [
                 [
                     'label' => $this->getLanguageService()->sL('LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.sysLogErrors.chart.dataSet.0'),
-                    'backgroundColor' => $this->chartColors[0],
+                    'backgroundColor' => WidgetApi::getDefaultChartColors()[0],
                     'border' => 0,
                     'data' => $this->data
                 ]
@@ -118,11 +94,11 @@ class SysLogErrorsWidget extends AbstractBarChartWidget
             ->fetchColumn();
     }
 
-    protected function calculateDataForLastDays(int $days): void
+    protected function calculateDataForLastDays(): void
     {
         $format = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] ?: 'Y-m-d';
 
-        for ($daysBefore=$days; $daysBefore>=0; $daysBefore--) {
+        for ($daysBefore = $this->days; $daysBefore >= 0; $daysBefore--) {
             $this->labels[] = date($format, strtotime('-' . $daysBefore . ' day'));
             $startPeriod = strtotime('-' . $daysBefore . ' day 0:00:00');
             $endPeriod =  strtotime('-' . $daysBefore . ' day 23:59:59');
@@ -130,4 +106,9 @@ class SysLogErrorsWidget extends AbstractBarChartWidget
             $this->data[] = $this->getNumberOfErrorsInPeriod($startPeriod, $endPeriod);
         }
     }
+
+    protected function getLanguageService(): LanguageService
+    {
+        return $GLOBALS['LANG'];
+    }
 }
diff --git a/typo3/sysext/dashboard/Classes/Widgets/TypeOfUsersWidget.php b/typo3/sysext/dashboard/Classes/Widgets/Provider/TypeOfUsersChartDataProvider.php
similarity index 61%
rename from typo3/sysext/dashboard/Classes/Widgets/TypeOfUsersWidget.php
rename to typo3/sysext/dashboard/Classes/Widgets/Provider/TypeOfUsersChartDataProvider.php
index 4b627d4649b9..06d330b7f1f8 100644
--- a/typo3/sysext/dashboard/Classes/Widgets/TypeOfUsersWidget.php
+++ b/typo3/sysext/dashboard/Classes/Widgets/Provider/TypeOfUsersChartDataProvider.php
@@ -1,6 +1,6 @@
 <?php
 declare(strict_types = 1);
-namespace TYPO3\CMS\Dashboard\Widgets;
+namespace TYPO3\CMS\Dashboard\Widgets\Provider;
 
 /*
  * This file is part of the TYPO3 CMS project.
@@ -17,39 +17,39 @@ namespace TYPO3\CMS\Dashboard\Widgets;
 
 use TYPO3\CMS\Core\Database\Connection;
 use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Localization\LanguageService;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Dashboard\WidgetApi;
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\ChartDataProviderInterface;
 
-/**
- * This widget will show the type of users (admin / non-admin) in a doughnut chart
- */
-class TypeOfUsersWidget extends AbstractDoughnutChartWidget
+class TypeOfUsersChartDataProvider implements ChartDataProviderInterface
 {
     /**
-     * @var string
+     * @var LanguageService
      */
-    protected $title = 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.typeOfUsers.title';
+    private $languageService;
 
-    /**
-     * @var string
-     */
-    protected $description = 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.typeOfUsers.description';
+    public function __construct(LanguageService $languageService)
+    {
+        $this->languageService = $languageService;
+    }
 
     /**
      * @inheritDoc
      */
-    protected function prepareChartData(): void
+    public function getChartData(): array
     {
         $adminUsers = $this->getNumberOfUsers(true);
         $normalUsers = $this->getNumberOfUsers(false);
 
-        $this->chartData = [
+        return [
             'labels' => [
-                $this->getLanguageService()->sL('LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.typeOfUsers.normalUsers'),
-                $this->getLanguageService()->sL('LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.typeOfUsers.adminUsers')
+                $this->languageService ->sL('LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.typeOfUsers.normalUsers'),
+                $this->languageService ->sL('LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.typeOfUsers.adminUsers')
             ],
             'datasets' => [
                 [
-                    'backgroundColor' => [$this->chartColors[0], $this->chartColors[1]],
+                    'backgroundColor' => WidgetApi::getDefaultChartColors(),
                     'data' => [$normalUsers, $adminUsers]
                 ]
             ],
diff --git a/typo3/sysext/dashboard/Classes/Widgets/RssWidget.php b/typo3/sysext/dashboard/Classes/Widgets/RssWidget.php
new file mode 100644
index 000000000000..14ba8673d04d
--- /dev/null
+++ b/typo3/sysext/dashboard/Classes/Widgets/RssWidget.php
@@ -0,0 +1,144 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Dashboard\Widgets;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface as Cache;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\ButtonProviderInterface;
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\WidgetConfigurationInterface;
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\WidgetInterface;
+use TYPO3\CMS\Fluid\View\StandaloneView;
+
+/**
+ * Concrete RSS widget implementation
+ *
+ * The widget will show a certain number of items of the given RSS feed. The feed will be set by the feedUrl option. You
+ * can add a button to the widget by defining a button provider.
+ *
+ * The following options are available during registration:
+ * - feedUrl        string                      Defines the URL or file providing the RSS Feed.
+ *                                              This is read by the widget in order to fetch entries to show.
+ * - limit          int     default: 5          Defines how many RSS items should be shown.
+ * - lifetime       int     default: 43200      Defines how long to wait, in seconds, until fetching RSS Feed again
+ *
+ * @see ButtonProviderInterface
+ */
+class RssWidget implements WidgetInterface
+{
+    /**
+     * @var WidgetConfigurationInterface
+     */
+    private $configuration;
+
+    /**
+     * @var StandaloneView
+     */
+    private $view;
+
+    /**
+     * @var Cache
+     */
+    private $cache;
+
+    /**
+     * @var array
+     */
+    private $options;
+
+    /**
+     * @var ButtonProviderInterface|null
+     */
+    private $buttonProvider;
+
+    public function __construct(
+        WidgetConfigurationInterface $configuration,
+        Cache $cache,
+        StandaloneView $view,
+        $buttonProvider = null,
+        array $options = []
+    ) {
+        $this->configuration = $configuration;
+        $this->view = $view;
+        $this->cache = $cache;
+        $this->options = array_merge(
+            [
+            'limit' => 5,
+            'lifeTime' => 43200
+            ],
+            $options
+        );
+        $this->buttonProvider = $buttonProvider;
+    }
+
+    public function renderWidgetContent(): string
+    {
+        $this->view->setTemplate('Widget/RssWidget');
+        $this->view->assignMultiple([
+            'items' => $this->getRssItems(),
+            'options' => $this->options,
+            'button' => $this->getButton(),
+            'configuration' => $this->configuration,
+        ]);
+        return $this->view->render();
+    }
+
+    protected function getRssItems(): array
+    {
+        $cacheHash = md5($this->options['feedUrl']);
+        if ($items = $this->cache->get($cacheHash)) {
+            return $items;
+        }
+
+        $rssContent = GeneralUtility::getUrl($this->options['feedUrl']);
+        if ($rssContent === false) {
+            throw new \RuntimeException('RSS URL could not be fetched', 1573385431);
+        }
+        $rssFeed = simplexml_load_string($rssContent);
+        $itemCount = 0;
+        $items = [];
+        foreach ($rssFeed->channel->item as $item) {
+            if ($itemCount >= $this->options['limit']) {
+                break;
+            }
+
+            $items[] = [
+                'title' => (string)$item->title,
+                'link' => trim((string)$item->link),
+                'pubDate' => (string)$item->pubDate,
+                'description' => (string)$item->description,
+            ];
+            $itemCount++;
+        }
+        $this->cache->set($cacheHash, $items, ['dashboard_rss'], $this->options['lifeTime']);
+
+        return $items;
+    }
+
+    private function getButton(): array
+    {
+        $button = [];
+
+        if ($this->buttonProvider instanceof ButtonProviderInterface && $this->buttonProvider->getTitle() && $this->buttonProvider->getLink()) {
+            $button = [
+                'text' => $this->buttonProvider->getTitle(),
+                'link' => $this->buttonProvider->getLink(),
+                'target' => $this->buttonProvider->getTarget(),
+            ];
+        }
+
+        return $button;
+    }
+}
diff --git a/typo3/sysext/dashboard/Classes/Widgets/T3GeneralInformation.php b/typo3/sysext/dashboard/Classes/Widgets/T3GeneralInformationWidget.php
similarity index 54%
rename from typo3/sysext/dashboard/Classes/Widgets/T3GeneralInformation.php
rename to typo3/sysext/dashboard/Classes/Widgets/T3GeneralInformationWidget.php
index 2dd249edf553..6d31f3eb0e45 100644
--- a/typo3/sysext/dashboard/Classes/Widgets/T3GeneralInformation.php
+++ b/typo3/sysext/dashboard/Classes/Widgets/T3GeneralInformationWidget.php
@@ -17,53 +17,58 @@ namespace TYPO3\CMS\Dashboard\Widgets;
 
 use TYPO3\CMS\Core\Information\Typo3Information;
 use TYPO3\CMS\Core\Information\Typo3Version;
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\WidgetConfigurationInterface;
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\WidgetInterface;
+use TYPO3\CMS\Fluid\View\StandaloneView;
 
 /**
- * This widget will show general information regarding TYPO3
+ * Concrete TYPO3 information widget
+ *
+ * This widget will give some general information about TYPO3 version and the version installed.
+ *
+ * There are no options available for this widget
  */
-class T3GeneralInformation extends AbstractWidget
+class T3GeneralInformationWidget implements WidgetInterface
 {
     /**
-     * @var string
+     * @var WidgetConfigurationInterface
      */
-    protected $title = 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3information.title';
+    private $configuration;
 
     /**
-     * @var string
+     * @var StandaloneView
      */
-    protected $description = 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3information.description';
+    private $view;
 
     /**
-     * @var string
+     * @var array
      */
-    protected $templateName = 'T3GeneralInformation';
+    private $options;
 
-    /**
-     * @var string
-     */
-    protected $iconIdentifier = 'content-widget-text';
-
-    /**
-     * @var int
-     */
-    protected $height = 4;
-
-    /**
-     * @var int
-     */
-    protected $width = 4;
+    public function __construct(
+        WidgetConfigurationInterface $configuration,
+        StandaloneView $view,
+        array $options = []
+    ) {
+        $this->configuration = $configuration;
+        $this->view = $view;
+        $this->options = $options;
+    }
 
     public function renderWidgetContent(): string
     {
         $typo3Information = new Typo3Information();
         $typo3Version = new Typo3Version();
+
+        $this->view->setTemplate('Widget/T3GeneralInformationWidget');
         $this->view->assignMultiple([
             'title' => 'TYPO3 CMS ' . $typo3Version->getVersion(),
             'copyrightYear' => $typo3Information->getCopyrightYear(),
             'currentVersion' => $typo3Version->getVersion(),
             'donationUrl' => $typo3Information::URL_DONATE,
             'copyRightNotice' => $typo3Information->getCopyrightNotice(),
-
+            'options' => $this->options,
+            'configuration' => $this->configuration
         ]);
         return $this->view->render();
     }
diff --git a/typo3/sysext/dashboard/Classes/Widgets/T3NewsWidget.php b/typo3/sysext/dashboard/Classes/Widgets/T3NewsWidget.php
deleted file mode 100644
index 1ddbb57f75cb..000000000000
--- a/typo3/sysext/dashboard/Classes/Widgets/T3NewsWidget.php
+++ /dev/null
@@ -1,53 +0,0 @@
-<?php
-declare(strict_types = 1);
-namespace TYPO3\CMS\Dashboard\Widgets;
-
-/*
- * This file is part of the TYPO3 CMS project.
- *
- * It is free software; you can redistribute it and/or modify it under
- * the terms of the GNU General Public License, either version 2
- * of the License, or any later version.
- *
- * For the full copyright and license information, please read the
- * LICENSE.txt file that was distributed with this source code.
- *
- * The TYPO3 project - inspiring people to share!
- */
-
-/**
- * This widget will show the latest news from the TYPO3 news RSS feed right
- * on the dashboard.
- */
-class T3NewsWidget extends AbstractRssWidget
-{
-    /**
-     * @var string
-     */
-    protected $rssFile = 'https://www.typo3.org/rss';
-
-    /**
-     * @var int
-     */
-    protected $lifeTime = 43200; // 12 hours cache
-
-    /**
-     * @var string
-     */
-    protected $title = 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.title';
-
-    /**
-     * @var string
-     */
-    protected $description = 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.description';
-
-    /**
-     * @var string
-     */
-    protected $moreItemsLink = 'https://typo3.org/project/news';
-
-    /**
-     * @var string
-     */
-    protected $moreItemsText = 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.moreItems';
-}
diff --git a/typo3/sysext/dashboard/Classes/Widgets/T3SecurityAdvisoriesWidget.php b/typo3/sysext/dashboard/Classes/Widgets/T3SecurityAdvisoriesWidget.php
deleted file mode 100644
index a33fa35cbb2f..000000000000
--- a/typo3/sysext/dashboard/Classes/Widgets/T3SecurityAdvisoriesWidget.php
+++ /dev/null
@@ -1,53 +0,0 @@
-<?php
-declare(strict_types = 1);
-namespace TYPO3\CMS\Dashboard\Widgets;
-
-/*
- * This file is part of the TYPO3 CMS project.
- *
- * It is free software; you can redistribute it and/or modify it under
- * the terms of the GNU General Public License, either version 2
- * of the License, or any later version.
- *
- * For the full copyright and license information, please read the
- * LICENSE.txt file that was distributed with this source code.
- *
- * The TYPO3 project - inspiring people to share!
- */
-
-/**
- * This widget will show the latest security advisories from the TYPO3 news RSS feed
- * right on the dashboard.
- */
-class T3SecurityAdvisoriesWidget extends AbstractRssWidget
-{
-    /**
-     * @var string
-     */
-    protected $rssFile = 'https://typo3.org/?type=101';
-
-    /**
-     * @var int
-     */
-    protected $lifeTime = 43200; // 12 hours cache
-
-    /**
-     * @var string
-     */
-    protected $title = 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3securityAdvisories.title';
-
-    /**
-     * @var string
-     */
-    protected $description = 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3securityAdvisories.description';
-
-    /**
-     * @var string
-     */
-    protected $moreItemsLink = 'https://typo3.org/help/security-advisories';
-
-    /**
-     * @var string
-     */
-    protected $moreItemsText = 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3securityAdvisories.moreItems';
-}
diff --git a/typo3/sysext/dashboard/Classes/Widgets/WidgetConfiguration.php b/typo3/sysext/dashboard/Classes/Widgets/WidgetConfiguration.php
new file mode 100644
index 000000000000..008ca099cc77
--- /dev/null
+++ b/typo3/sysext/dashboard/Classes/Widgets/WidgetConfiguration.php
@@ -0,0 +1,144 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Dashboard\Widgets;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\WidgetConfigurationInterface;
+
+class WidgetConfiguration implements WidgetConfigurationInterface
+{
+    /**
+     * @var string
+     */
+    private $identifier;
+
+    /**
+     * @var string
+     */
+    private $serviceName;
+
+    /**
+     * @var array
+     */
+    private $groupNames;
+
+    /**
+     * @var string
+     */
+    private $title;
+
+    /**
+     * @var string
+     */
+    private $description;
+
+    /**
+     * @var string
+     */
+    private $iconIdentifier;
+
+    /**
+     * @var string
+     */
+    private $height;
+
+    /**
+     * @var string
+     */
+    private $width;
+
+    /**
+     * @var array
+     */
+    private $additionalCssClasses;
+
+    /**
+     * @throws \InvalidArgumentException If non valid parameters were provided.
+     */
+    public function __construct(
+        string $identifier,
+        string $serviceName,
+        array $groupNames,
+        string $title,
+        string $description,
+        string $iconIdentifier,
+        string $height,
+        string $width,
+        array $additionalCssClasses
+    ) {
+        $allowedSizes = ['small', 'medium', 'large'];
+        if (!in_array($height, $allowedSizes, true)) {
+            throw new \InvalidArgumentException('Height of widgets has to be small, medium or large', 1584778196);
+        }
+        if (!in_array($height, $allowedSizes, true)) {
+            throw new \InvalidArgumentException('Width of widgets has to be small, medium or large', 1585249769);
+        }
+
+        $this->identifier = $identifier;
+        $this->serviceName = $serviceName;
+        $this->groupNames = $groupNames;
+        $this->title = $title;
+        $this->description = $description;
+        $this->iconIdentifier = $iconIdentifier;
+        $this->height = $height;
+        $this->width = $width;
+        $this->additionalCssClasses = $additionalCssClasses;
+    }
+
+    public function getIdentifier(): string
+    {
+        return $this->identifier;
+    }
+
+    public function getServiceName(): string
+    {
+        return $this->serviceName;
+    }
+
+    public function getGroupNames(): array
+    {
+        return $this->groupNames;
+    }
+
+    public function getTitle(): string
+    {
+        return $this->title;
+    }
+
+    public function getDescription(): string
+    {
+        return $this->description;
+    }
+
+    public function getIconIdentifier(): string
+    {
+        return $this->iconIdentifier;
+    }
+
+    public function getHeight(): string
+    {
+        return $this->height;
+    }
+
+    public function getWidth(): string
+    {
+        return $this->width;
+    }
+
+    public function getAdditionalCssClasses(): string
+    {
+        return implode(' ', $this->additionalCssClasses);
+    }
+}
diff --git a/typo3/sysext/dashboard/Configuration/Backend/DashboardWidgets.yaml b/typo3/sysext/dashboard/Configuration/Backend/DashboardWidgets.yaml
new file mode 100644
index 000000000000..5744d3be9948
--- /dev/null
+++ b/typo3/sysext/dashboard/Configuration/Backend/DashboardWidgets.yaml
@@ -0,0 +1,208 @@
+services:
+  _defaults:
+    autowire: true
+    autoconfigure: true
+    public: false
+
+  TYPO3\CMS\Dashboard\Widgets\:
+    resource: '../Classes/Widgets/*'
+
+  TYPO3\CMS\Dashboard\Widgets\WidgetConfiguration:
+    autowire: false
+
+  TYPO3\CMS\Dashboard\Widgets\Provider\SysLogButtonProvider:
+    arguments:
+      $title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.sysLogErrors.buttonText'
+
+  cache.dashboard.rss:
+    class: 'TYPO3\CMS\Core\Cache\Frontend\FrontendInterface'
+    factory: ['@TYPO3\CMS\Core\Cache\CacheManager', 'getCache']
+    arguments:
+      $identifier: 'dashboard_rss'
+
+  dashboard.buttons.t3news:
+    class: 'TYPO3\CMS\Dashboard\Widgets\Provider\ButtonProvider'
+    arguments:
+      $title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.moreItems'
+      $link: 'https://typo3.org/project/news'
+      $target: '_blank'
+
+  dashboard.buttons.t3securityAdvisories:
+    class: 'TYPO3\CMS\Dashboard\Widgets\Provider\ButtonProvider'
+    arguments:
+      $title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3securityAdvisories.moreItems'
+      $link: 'https://typo3.org/help/security-advisories'
+      $target: '_blank'
+
+  dashboard.buttons.docGettingStarted:
+    class: 'TYPO3\CMS\Dashboard\Widgets\Provider\ButtonProvider'
+    arguments:
+      $title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.gettingStarted.content.label'
+      $link: 'https://docs.typo3.org/m/typo3/tutorial-getting-started/master/en-us/Index.html'
+      $target: '_blank'
+
+  dashboard.buttons.docTypoScriptReference:
+    class: 'TYPO3\CMS\Dashboard\Widgets\Provider\ButtonProvider'
+    arguments:
+      $title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.typoscriptReference.content.label'
+      $link: 'https://docs.typo3.org/m/typo3/reference-typoscript/master/en-us/Index.html'
+      $target: '_blank'
+
+  dashboard.buttons.docTSconfig:
+    class: 'TYPO3\CMS\Dashboard\Widgets\Provider\ButtonProvider'
+    arguments:
+      $title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.TSconfigReference.content.label'
+      $link: 'https://docs.typo3.org/m/typo3/reference-tsconfig/master/en-us/Index.html'
+      $target: '_blank'
+
+  dashboard.views.widget:
+    class: 'TYPO3\CMS\Fluid\View\StandaloneView'
+    public: true
+    factory: ['TYPO3\CMS\Dashboard\Views\Factory', 'widgetTemplate']
+
+  dashboard.widget.t3news:
+    class: 'TYPO3\CMS\Dashboard\Widgets\RssWidget'
+    arguments:
+      $view: '@dashboard.views.widget'
+      $cache: '@cache.dashboard.rss'
+      $buttonProvider: '@dashboard.buttons.t3news'
+      $options:
+        feedUrl: 'https://www.typo3.org/rss'
+    tags:
+      - name: dashboard.widget
+        identifier: 't3news'
+        groupNames: 'typo3'
+        title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.title'
+        description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.description'
+        iconIdentifier: 'content-widget-rss'
+        height: 'large'
+        width: 'medium'
+
+  dashboard.widget.sysLogErrors:
+    class: 'TYPO3\CMS\Dashboard\Widgets\BarChartWidget'
+    arguments:
+      $dataProvider: '@TYPO3\CMS\Dashboard\Widgets\Provider\SysLogErrorsDataProvider'
+      $view: '@dashboard.views.widget'
+      $buttonProvider: '@TYPO3\CMS\Dashboard\Widgets\Provider\SysLogButtonProvider'
+    tags:
+      - name: dashboard.widget
+        identifier: 'sysLogErrors'
+        groupNames: 'systemInfo'
+        title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.sysLogErrors.title'
+        description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.sysLogErrors.description'
+        iconIdentifier: 'content-widget-chart-bar'
+        additionalCssClasses: 'dashboard-item--chart'
+        height: 'medium'
+        width: 'medium'
+
+  dashboard.widget.docGettingStarted:
+    class: 'TYPO3\CMS\Dashboard\Widgets\CtaWidget'
+    arguments:
+      $view: '@dashboard.views.widget'
+      $buttonProvider: '@dashboard.buttons.docGettingStarted'
+      $options:
+        text: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.gettingStarted.text'
+    tags:
+      - name: dashboard.widget
+        identifier: 'docGettingStarted'
+        groupNames: 'documentation'
+        title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.gettingStarted.title'
+        description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.gettingStarted.description'
+        iconIdentifier: 'content-widget-text'
+        height: 'small'
+
+  dashboard.widget.docTypoScriptReference:
+    class: 'TYPO3\CMS\Dashboard\Widgets\CtaWidget'
+    arguments:
+      $view: '@dashboard.views.widget'
+      $buttonProvider: '@dashboard.buttons.docTypoScriptReference'
+      $options:
+        text: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.typoscriptReference.text'
+    tags:
+      - name: dashboard.widget
+        identifier: 'docTypoScriptReference'
+        groupNames: 'documentation'
+        title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.typoscriptReference.title'
+        description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.typoscriptReference.description'
+        iconIdentifier: 'content-widget-text'
+        height: 'small'
+
+  dashboard.widget.docTSconfig:
+    class: 'TYPO3\CMS\Dashboard\Widgets\CtaWidget'
+    arguments:
+      $view: '@dashboard.views.widget'
+      $buttonProvider: '@dashboard.buttons.docTSconfig'
+      $options:
+        text: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.TSconfigReference.text'
+    tags:
+      - name: dashboard.widget
+        identifier: 'docTSconfig'
+        groupNames: 'documentation'
+        title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.TSconfigReference.title'
+        description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.TSconfigReference.description'
+        iconIdentifier: 'content-widget-text'
+        height: 'small'
+
+  dashboard.widget.t3information:
+    class: 'TYPO3\CMS\Dashboard\Widgets\T3GeneralInformationWidget'
+    arguments:
+      $view: '@dashboard.views.widget'
+    tags:
+      - name: dashboard.widget
+        identifier: 't3information'
+        groupNames: 'general'
+        title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3information.title'
+        description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3information.description'
+        iconIdentifier: 'content-widget-text'
+        height: 'medium'
+        width: 'medium'
+
+  dashboard.widget.typeOfUsers:
+    class: 'TYPO3\CMS\Dashboard\Widgets\DoughnutChartWidget'
+    arguments:
+      $view: '@dashboard.views.widget'
+      $dataProvider: '@TYPO3\CMS\Dashboard\Widgets\Provider\TypeOfUsersChartDataProvider'
+    tags:
+      - name: dashboard.widget
+        identifier: 'typeOfUsers'
+        groupNames: 'systemInfo'
+        title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.typeOfUsers.title'
+        description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.typeOfUsers.description'
+        iconIdentifier: 'content-widget-chart-pie'
+        additionalCssClasses: 'dashboard-item--chart'
+        height: 'large'
+
+  dashboard.widget.t3securityAdvisories:
+    class: 'TYPO3\CMS\Dashboard\Widgets\RssWidget'
+    arguments:
+      $view: '@dashboard.views.widget'
+      $cache: '@cache.dashboard.rss'
+      $buttonProvider: '@dashboard.buttons.t3securityAdvisories'
+      $options:
+        feedUrl: 'https://typo3.org/?type=101'
+    tags:
+      - name: dashboard.widget
+        identifier: 't3securityAdvisories'
+        groupNames: 'typo3'
+        title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3securityAdvisories.title'
+        description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3securityAdvisories.description'
+        iconIdentifier: 'content-widget-rss'
+        height: 'large'
+        width: 'medium'
+
+  dashboard.widget.failedLogins:
+    class: 'TYPO3\CMS\Dashboard\Widgets\NumberWithIconWidget'
+    arguments:
+      $dataProvider: '@TYPO3\CMS\Dashboard\Widgets\Provider\NumberOfFailedLoginsDataProvider'
+      $view: '@dashboard.views.widget'
+      $options:
+        title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.failedLogins.title'
+        subtitle: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.failedLogins.subtitle'
+        icon: 'content-elements-login'
+    tags:
+      - name: dashboard.widget
+        identifier: 'failedLogins'
+        groupNames: 'systemInfo'
+        title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.failedLogins.title'
+        description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.failedLogins.description'
+        iconIdentifier: 'content-widget-number'
diff --git a/typo3/sysext/dashboard/Configuration/Services.yaml b/typo3/sysext/dashboard/Configuration/Services.yaml
index 711d9111cb5d..cebad0f0d6f9 100644
--- a/typo3/sysext/dashboard/Configuration/Services.yaml
+++ b/typo3/sysext/dashboard/Configuration/Services.yaml
@@ -1,3 +1,6 @@
+imports:
+  - { resource: Backend/DashboardWidgets.yaml }
+
 services:
   _defaults:
     autowire: true
@@ -6,6 +9,7 @@ services:
 
   TYPO3\CMS\Dashboard\:
     resource: '../Classes/*'
+    exclude: '../Classes/Widgets/*'
 
   TYPO3\CMS\Dashboard\Controller\DashboardController:
     public: true
@@ -15,66 +19,3 @@ services:
 
   TYPO3\CMS\Dashboard\DasboardRegistry:
     public: true
-
-  TYPO3\CMS\Dashboard\Widgets\DocumentationGettingStartedWidget:
-    arguments: ['docGettingStarted']
-    tags:
-      - name: dashboard.widget
-        identifier: docGettingStarted
-        widgetGroups: 'documentation'
-
-  TYPO3\CMS\Dashboard\Widgets\DocumentationTypoScriptReference:
-    arguments: ['docTypoScriptReference']
-    tags:
-      - name: dashboard.widget
-        identifier: docTypoScriptReference
-        widgetGroups: 'documentation'
-
-  TYPO3\CMS\Dashboard\Widgets\DocumentationTSconfigReferenceWidget:
-    arguments: ['docTSconfig']
-    tags:
-      - name: dashboard.widget
-        identifier: docTSconfig
-        widgetGroups: 'documentation'
-
-  TYPO3\CMS\Dashboard\Widgets\T3GeneralInformation:
-    arguments: ['t3information']
-    tags:
-      - name: dashboard.widget
-        identifier: t3information
-        widgetGroups: 'general'
-
-  TYPO3\CMS\Dashboard\Widgets\SysLogErrorsWidget:
-    arguments: ['sysLogErrors']
-    tags:
-      - name: dashboard.widget
-        identifier: sysLogErrors
-        widgetGroups: 'systemInfo'
-
-  TYPO3\CMS\Dashboard\Widgets\TypeOfUsersWidget:
-    arguments: ['typeOfUsers']
-    tags:
-      - name: dashboard.widget
-        identifier: typeOfUsers
-        widgetGroups: 'systemInfo'
-
-  TYPO3\CMS\Dashboard\Widgets\FailedLoginsWidget:
-    arguments: ['failedLogins']
-    tags:
-      - name: dashboard.widget
-        identifier: failedLogins
-        widgetGroups: 'general'
-
-  TYPO3\CMS\Dashboard\Widgets\T3NewsWidget:
-    arguments: ['t3news']
-    tags:
-      - name: dashboard.widget
-        identifier: t3news
-        widgetGroups: 'typo3'
-
-  TYPO3\CMS\Dashboard\Widgets\T3SecurityAdvisoriesWidget:
-    arguments: ['t3securityAdvisories']
-    tags:
-      - name: dashboard.widget
-        identifier: t3securityAdvisories
-        widgetGroups: 'typo3'
diff --git a/typo3/sysext/dashboard/Resources/Private/Layouts/Widget/Widget.html b/typo3/sysext/dashboard/Resources/Private/Layouts/Widget/Widget.html
index d18e1f94d012..4af5719f7e74 100644
--- a/typo3/sysext/dashboard/Resources/Private/Layouts/Widget/Widget.html
+++ b/typo3/sysext/dashboard/Resources/Private/Layouts/Widget/Widget.html
@@ -1,5 +1,5 @@
 <html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
-    <div class="widget-content-title"><span>{title}</span></div>
+    <div class="widget-content-title"><span>{f:translate(id: configuration.title, default: configuration.title)}</span></div>
     <div class="widget-content-main">
         <f:render section="main" optional="true" />
     </div>
diff --git a/typo3/sysext/dashboard/Resources/Private/Templates/Dashboard/Main.html b/typo3/sysext/dashboard/Resources/Private/Templates/Dashboard/Main.html
index 27c80a43bcac..b5f2ed8e133f 100644
--- a/typo3/sysext/dashboard/Resources/Private/Templates/Dashboard/Main.html
+++ b/typo3/sysext/dashboard/Resources/Private/Templates/Dashboard/Main.html
@@ -27,7 +27,7 @@
     <f:then>
         <div class="dashboard-grid">
             <f:for each="{currentDashboard.widgets}" as="widget" iteration="widgetIterator" key="widgetHash">
-                <div class="dashboard-item dashboard-item--w{widget.width} dashboard-item--h{widget.height} {widget.additionalClasses} dashboard-item--enableSelect" data-widget-hash="{widgetHash}" data-widget-key="{widget.identifier}">
+                <div class="dashboard-item dashboard-item--w-{widget.width} dashboard-item--h-{widget.height} {widget.additionalCssClasses} dashboard-item--enableSelect" data-widget-hash="{widgetHash}" data-widget-key="{widget.identifier}">
                     <div class="dashboard-item-content">
                         <div class="widget widget-identifier-{widget.identifier}">
                             <div class="widget-waiting">
diff --git a/typo3/sysext/dashboard/Resources/Private/Templates/Widget/ChartWidget.html b/typo3/sysext/dashboard/Resources/Private/Templates/Widget/ChartWidget.html
index 4e753d334fe8..45b2fbe4d322 100644
--- a/typo3/sysext/dashboard/Resources/Private/Templates/Widget/ChartWidget.html
+++ b/typo3/sysext/dashboard/Resources/Private/Templates/Widget/ChartWidget.html
@@ -10,7 +10,7 @@
 <f:section name="footer">
 
     <f:if condition="{button}">
-        <a href="{button.link}" target="{button.target}" class="widget-cta">{button.text}</a>
+        <a href="{button.link}" target="{button.target}" class="widget-cta">{f:translate(id: button.text, default: button.text)}</a>
     </f:if>
 
 </f:section>
diff --git a/typo3/sysext/dashboard/Resources/Private/Templates/Widget/CtaWidget.html b/typo3/sysext/dashboard/Resources/Private/Templates/Widget/CtaWidget.html
index 5f86147458f4..97f590fa0c05 100644
--- a/typo3/sysext/dashboard/Resources/Private/Templates/Widget/CtaWidget.html
+++ b/typo3/sysext/dashboard/Resources/Private/Templates/Widget/CtaWidget.html
@@ -3,24 +3,13 @@
 <f:section name="main">
 
     <f:if condition="{text}">
-        {text}
+        <f:translate id='{text}' default='{text}' />
     </f:if>
 
 </f:section>
 <f:section name="footer">
-
-    <f:if condition="{link}">
-        <f:link.external uri="{link}" target="_blank" class="widget-cta">
-            <f:if condition="{icon}">
-                <div class="widget-cta-icon">
-                    <core:icon identifier="{icon}" size="large" alternativeMarkupIdentifier="inline" />
-                </div>
-            </f:if>
-            <f:if condition="{label}">
-                <f:translate key="{label}" default="{label}"/>
-            </f:if>
-        </f:link.external>
+    <f:if condition="{button}">
+        <a href="{button.link}" target="{button.target}" class="widget-cta">{f:translate(id: button.text, default: button.text)}</a>
     </f:if>
-
 </f:section>
 </html>
diff --git a/typo3/sysext/dashboard/Resources/Private/Templates/Widget/ListWidget.html b/typo3/sysext/dashboard/Resources/Private/Templates/Widget/ListWidget.html
index bbbac6ee9667..3e04b449ada8 100644
--- a/typo3/sysext/dashboard/Resources/Private/Templates/Widget/ListWidget.html
+++ b/typo3/sysext/dashboard/Resources/Private/Templates/Widget/ListWidget.html
@@ -5,7 +5,7 @@
     <f:if condition="{items}">
         <ul>
             <f:for each="{items}" as="item">
-                <li><f:link.typolink parameter="{item.link}">{item.title}</f:link.typolink></li>
+                <li>{item}</li>
             </f:for>
         </ul>
     </f:if>
diff --git a/typo3/sysext/dashboard/Resources/Private/Templates/Widget/NumberWithIconWidget.html b/typo3/sysext/dashboard/Resources/Private/Templates/Widget/NumberWithIconWidget.html
index 99c210653e29..361eb0ba58ca 100644
--- a/typo3/sysext/dashboard/Resources/Private/Templates/Widget/NumberWithIconWidget.html
+++ b/typo3/sysext/dashboard/Resources/Private/Templates/Widget/NumberWithIconWidget.html
@@ -9,9 +9,9 @@
             </div>
         </f:if>
         <div class="widget-number-content">
-            <div class="widget-number-title">{title}</div>
+            <div class="widget-number-title"><f:translate key="{title}" default="{title}" /></div>
             <f:if condition="{subtitle}">
-                <div class="widget-number-subtitle"><small>{subtitle}</small></div>
+                <div class="widget-number-subtitle"><small><f:translate key="{subtitle}" default="{subtitle}" /></small></div>
             </f:if>
             <div class="widget-number-number">{number}</div>
         </div>
diff --git a/typo3/sysext/dashboard/Resources/Private/Templates/Widget/RssWidget.html b/typo3/sysext/dashboard/Resources/Private/Templates/Widget/RssWidget.html
index 5dd79fb931a1..2a261352455e 100644
--- a/typo3/sysext/dashboard/Resources/Private/Templates/Widget/RssWidget.html
+++ b/typo3/sysext/dashboard/Resources/Private/Templates/Widget/RssWidget.html
@@ -18,12 +18,8 @@
 
 </f:section>
 <f:section name="footer">
-
-    <f:if condition="{moreItemsLink}">
-        <f:link.typolink parameter="{moreItemsLink}" target="_blank" class="widget-cta">
-            <f:translate key="{moreItemsText}" default="{moreItemsText}"/>
-        </f:link.typolink>
+    <f:if condition="{button}">
+        <a href="{button.link}" target="{button.target}" class="widget-cta">{f:translate(id: button.text, default: button.text)}</a>
     </f:if>
-
 </f:section>
 </html>
diff --git a/typo3/sysext/dashboard/Resources/Private/Templates/Widget/T3GeneralInformation.html b/typo3/sysext/dashboard/Resources/Private/Templates/Widget/T3GeneralInformationWidget.html
similarity index 100%
rename from typo3/sysext/dashboard/Resources/Private/Templates/Widget/T3GeneralInformation.html
rename to typo3/sysext/dashboard/Resources/Private/Templates/Widget/T3GeneralInformationWidget.html
diff --git a/typo3/sysext/dashboard/Resources/Public/Css/dashboard.css b/typo3/sysext/dashboard/Resources/Public/Css/dashboard.css
index 8d79f154f43c..df55e386901b 100644
--- a/typo3/sysext/dashboard/Resources/Public/Css/dashboard.css
+++ b/typo3/sysext/dashboard/Resources/Public/Css/dashboard.css
@@ -10,4 +10,4 @@
  * 
  * The TYPO3 project - inspiring people to share!
  */
-.module.module{background-color:#eaeaea}.module.module h1{line-height:calc(48 / 32);margin-bottom:20px;font-weight:900;font-size:32px}.dashboard-header{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;margin:-24px -24px 24px;padding:24px 24px 0;background:#dadada;border-bottom:1px solid #cdcdcd}.dashboard-tabs{display:flex;flex-wrap:wrap;align-items:center}.dashboard-tab{border-radius:5px 5px 0 0;display:inline-block;padding:12px;margin-right:2px;background:#bababa;color:#000}.dashboard-tab:focus,.dashboard-tab:hover{text-decoration:none;background:#adadad;color:#000}.dashboard-tab--active{background:#ff8700;color:#fff}.dashboard-tab--active:focus,.dashboard-tab--active:hover{text-decoration:none;background:#e67a00;color:#f2f2f2}.dashboard-button-tab-add{margin:5px}.dashboard-configuration{padding:10px 0}.dashboard-configuration-button{margin-left:10px;color:#737373;text-decoration:none}.dashboard-configuration-button:focus,.dashboard-configuration-button:hover{color:#ff8700;text-decoration:none}.dashboard-configuration-button:active{color:#000;text-decoration:none}.dashboard-empty{position:relative}.dashboard-empty-content{background-color:rgba(0,0,0,.05);border:2px dashed rgba(0,0,0,.15);padding:2.5em;text-align:center}.dashboard-empty-content h3{font-size:1.5em;margin-bottom:.5em}.dashboard-empty-content p{font-size:1.25em;margin-bottom:1em}.dashboard-empty-content>:first-child{margin-top:0}.dashboard-empty-content>:last-child{margin-bottom:0}.dashboard-grid{position:relative;margin-right:-10px;margin-left:-10px}.dashboard-item{position:absolute;z-index:1;padding:10px;width:100%;height:auto}@media screen and (min-width:750px){.dashboard-item{width:50%;height:200px}}@media screen and (min-width:1285px){.dashboard-item{width:25%}}.dashboard-item.muuri-item-positioning{z-index:2}.dashboard-item.muuri-item-positioning .widget-remove{display:none}.dashboard-item.muuri-item-placeholder{z-index:2;margin:0;opacity:.5}.dashboard-item.muuri-item-placeholder .widget{border:1px dashed #737373}.dashboard-item.muuri-item-placeholder .widget-remove{display:none}.dashboard-item.muuri-item-dragging,.dashboard-item.muuri-item-releasing{z-index:9999}.dashboard-item.muuri-item-releasing .widget-remove{display:none}.dashboard-item.muuri-item-dragging{cursor:move}.dashboard-item.muuri-item-hidden{z-index:0}.dashboard-item.widget-waiting{line-height:200px}.dashboard-item--enableSelect{-webkit-user-select:auto!important;-moz-user-select:auto!important;-ms-user-select:auto!important;user-select:auto!important}@media screen and (min-width:750px){.dashboard-item--h4{height:400px}}@media screen and (min-width:750px){.dashboard-item--h6{height:600px}}.dashboard-item--w4{width:100%}@media screen and (min-width:1285px){.dashboard-item--w4{width:50%}}.dashboard-item-content{position:relative;width:100%;height:100%}.dashboard-button{display:inline-flex;align-items:center;border-radius:3px;background:#313131;color:#fff;padding:8px;text-decoration:none}.dashboard-button:focus,.dashboard-button:hover{text-decoration:none;background:#ff8700;color:#fff}.dashboard-button .dashboard-button-icon .icon{display:block}.dashboard-button .dashboard-button-icon+.dashboard-button-text{margin-left:.25em;margin-right:.25em}.dashboard-button-add{position:fixed;padding:16px;right:24px;bottom:24px;z-index:2}.widget{height:100%;border-radius:2px;overflow:hidden;background-color:#fff;box-shadow:0 2px 2px 0 rgba(0,0,0,.15);color:#000}.widget:hover .widget-actions{opacity:1}.widget-content{display:flex;flex-direction:column;height:100%}.widget-content-title{padding:10px 20px;padding-right:76px;border-bottom:1px solid #d7d7d7;font-family:"Source Sans Pro",sans-serif;font-size:16px;font-weight:700;line-height:1.25}.widget-content-title span{overflow:hidden;display:block;white-space:nowrap;text-overflow:ellipsis}.widget-content-main{flex-grow:1;overflow-y:auto;padding:20px}.widget-content-footer{padding:20px;padding-top:0}.widget-actions{position:absolute;display:flex;top:calc(((16px * 1.25)/ 2) + (20px / 2));right:10px;transform:translate(0,-50%);opacity:0;transition:opacity .2s ease-in-out}.widget-action{width:28px;height:28px;position:relative;color:#737373;text-align:center}.widget-action:focus,.widget-action:hover{color:#ff8700}.widget-action .icon{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.widget-action-move{cursor:-webkit-grab;cursor:grab}.widget-waiting{position:absolute;top:50%;left:50%;line-height:300px;margin-right:-50%;transform:translate(-50%,-50%)}.widget-error{padding:20px;position:absolute;top:50%;text-align:center;transform:translateY(-50%);color:#c83c3c}.widget-chart{width:100%;height:100%}.widget-edit{width:45px;text-align:center}.widget-editIcon{color:#000}.widget-editIcon:focus,.widget-editIcon:hover{color:#ff8700}.widget-table{width:100%;color:#000}.widget-table thead tr{background-color:transparent}.widget-table tr:nth-child(odd){background-color:transparent}.widget-table tr:nth-child(even){background-color:#f2f2f2}.widget-table tbody td,.widget-table tbody th{border-top:1px solid #e0e0e0}.widget-table tbody:first-child tr:first-child td,.widget-table tbody:first-child tr:first-child th{border-top:none}.widget-table td,.widget-table th{padding:10px}.widget-table td>:first-child,.widget-table th>:first-child{margin-top:0}.widget-table td>:last-child,.widget-table th>:last-child{margin-bottom:0}.widget-table th{font-weight:700}.widget-content-main .widget-table-wrapper{margin-top:-10px;margin-left:-20px;margin-right:-20px}.widget-content-main .widget-table-wrapper td:first-child,.widget-content-main .widget-table-wrapper th:first-child{padding-left:20px}.widget-content-main .widget-table-wrapper td:last-child,.widget-content-main .widget-table-wrapper th:last-child{padding-right:20px}.widget-cta{display:flex;justify-content:center;align-items:center;background-color:#313131;color:#fff;border-radius:3px;padding:8px}.widget-cta:focus,.widget-cta:hover{text-decoration:none;background:#ff8700;color:#fff}.widget-cta-icon{display:flex;justify-content:center;align-items:center;width:18px;height:18px;margin-right:12px;color:#fff}.widget-doughnut--value{line-height:1.3;font-weight:900;font-size:36px;text-align:center}.widget-doughnut--meta{margin-top:10px;font-style:italic;color:#737373;text-align:center}.widget-number{height:100%;display:flex;align-items:center}.widget-number-icon{display:flex;align-items:center;width:42px;margin-right:20px;color:#000}.widget-number-content{display:flex;flex-direction:column;justify-content:center}.widget-number-title{line-height:1.3;margin-bottom:5px;font-size:16px;color:#000}.widget-number-number{line-height:1.3;font-weight:900;font-size:24px}
\ No newline at end of file
+.module.module{background-color:#eaeaea}.module.module h1{line-height:calc(48 / 32);margin-bottom:20px;font-weight:900;font-size:32px}.dashboard-header{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;margin:-24px -24px 24px;padding:24px 24px 0;background:#dadada;border-bottom:1px solid #cdcdcd}.dashboard-tabs{display:flex;flex-wrap:wrap;align-items:center}.dashboard-tab{border-radius:5px 5px 0 0;display:inline-block;padding:12px;margin-right:2px;background:#bababa;color:#000}.dashboard-tab:focus,.dashboard-tab:hover{text-decoration:none;background:#adadad;color:#000}.dashboard-tab--active{background:#ff8700;color:#fff}.dashboard-tab--active:focus,.dashboard-tab--active:hover{text-decoration:none;background:#e67a00;color:#f2f2f2}.dashboard-button-tab-add{margin:5px}.dashboard-configuration{padding:10px 0}.dashboard-configuration-button{margin-left:10px;color:#737373;text-decoration:none}.dashboard-configuration-button:focus,.dashboard-configuration-button:hover{color:#ff8700;text-decoration:none}.dashboard-configuration-button:active{color:#000;text-decoration:none}.dashboard-empty{position:relative}.dashboard-empty-content{background-color:rgba(0,0,0,.05);border:2px dashed rgba(0,0,0,.15);padding:2.5em;text-align:center}.dashboard-empty-content h3{font-size:1.5em;margin-bottom:.5em}.dashboard-empty-content p{font-size:1.25em;margin-bottom:1em}.dashboard-empty-content>:first-child{margin-top:0}.dashboard-empty-content>:last-child{margin-bottom:0}.dashboard-grid{position:relative;margin-right:-10px;margin-left:-10px}.dashboard-item{position:absolute;z-index:1;padding:10px;width:100%;height:auto}@media screen and (min-width:750px){.dashboard-item{width:50%;height:200px}}@media screen and (min-width:1285px){.dashboard-item{width:25%}}.dashboard-item.muuri-item-positioning{z-index:2}.dashboard-item.muuri-item-positioning .widget-remove{display:none}.dashboard-item.muuri-item-placeholder{z-index:2;margin:0;opacity:.5}.dashboard-item.muuri-item-placeholder .widget{border:1px dashed #737373}.dashboard-item.muuri-item-placeholder .widget-remove{display:none}.dashboard-item.muuri-item-dragging,.dashboard-item.muuri-item-releasing{z-index:9999}.dashboard-item.muuri-item-releasing .widget-remove{display:none}.dashboard-item.muuri-item-dragging{cursor:move}.dashboard-item.muuri-item-hidden{z-index:0}.dashboard-item.widget-waiting{line-height:200px}.dashboard-item--enableSelect{-webkit-user-select:auto!important;-moz-user-select:auto!important;-ms-user-select:auto!important;user-select:auto!important}@media screen and (min-width:750px){.dashboard-item--h-medium{height:400px}}@media screen and (min-width:750px){.dashboard-item--h-large{height:600px}}.dashboard-item--w-medium{width:100%}@media screen and (min-width:1285px){.dashboard-item--w-medium{width:50%}}.dashboard-item--w-large{width:100%}.dashboard-item-content{position:relative;width:100%;height:100%}.dashboard-button{display:inline-flex;align-items:center;border-radius:3px;background:#313131;color:#fff;padding:8px;text-decoration:none}.dashboard-button:focus,.dashboard-button:hover{text-decoration:none;background:#ff8700;color:#fff}.dashboard-button .dashboard-button-icon .icon{display:block}.dashboard-button .dashboard-button-icon+.dashboard-button-text{margin-left:.25em;margin-right:.25em}.dashboard-button-add{position:fixed;padding:16px;right:24px;bottom:24px;z-index:2}.widget{height:100%;border-radius:2px;overflow:hidden;background-color:#fff;box-shadow:0 2px 2px 0 rgba(0,0,0,.15);color:#000}.widget:hover .widget-actions{opacity:1}.widget-content{display:flex;flex-direction:column;height:100%}.widget-content-title{padding:10px 20px;padding-right:76px;border-bottom:1px solid #d7d7d7;font-family:"Source Sans Pro",sans-serif;font-size:16px;font-weight:700;line-height:1.25}.widget-content-title span{overflow:hidden;display:block;white-space:nowrap;text-overflow:ellipsis}.widget-content-main{flex-grow:1;overflow-y:auto;padding:20px}.widget-content-footer{padding:20px;padding-top:0}.widget-actions{position:absolute;display:flex;top:calc(((16px * 1.25)/ 2) + (20px / 2));right:10px;transform:translate(0,-50%);opacity:0;transition:opacity .2s ease-in-out}.widget-action{width:28px;height:28px;position:relative;color:#737373;text-align:center}.widget-action:focus,.widget-action:hover{color:#ff8700}.widget-action .icon{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.widget-action-move{cursor:-webkit-grab;cursor:grab}.widget-waiting{position:absolute;top:50%;left:50%;line-height:300px;margin-right:-50%;transform:translate(-50%,-50%)}.widget-error{padding:20px;position:absolute;top:50%;text-align:center;transform:translateY(-50%);color:#c83c3c}.widget-chart{width:100%;height:100%}.widget-edit{width:45px;text-align:center}.widget-editIcon{color:#000}.widget-editIcon:focus,.widget-editIcon:hover{color:#ff8700}.widget-table{width:100%;color:#000}.widget-table thead tr{background-color:transparent}.widget-table tr:nth-child(odd){background-color:transparent}.widget-table tr:nth-child(even){background-color:#f2f2f2}.widget-table tbody td,.widget-table tbody th{border-top:1px solid #e0e0e0}.widget-table tbody:first-child tr:first-child td,.widget-table tbody:first-child tr:first-child th{border-top:none}.widget-table td,.widget-table th{padding:10px}.widget-table td>:first-child,.widget-table th>:first-child{margin-top:0}.widget-table td>:last-child,.widget-table th>:last-child{margin-bottom:0}.widget-table th{font-weight:700}.widget-content-main .widget-table-wrapper{margin-top:-10px;margin-left:-20px;margin-right:-20px}.widget-content-main .widget-table-wrapper td:first-child,.widget-content-main .widget-table-wrapper th:first-child{padding-left:20px}.widget-content-main .widget-table-wrapper td:last-child,.widget-content-main .widget-table-wrapper th:last-child{padding-right:20px}.widget-cta{display:flex;justify-content:center;align-items:center;background-color:#313131;color:#fff;border-radius:3px;padding:8px}.widget-cta:focus,.widget-cta:hover{text-decoration:none;background:#ff8700;color:#fff}.widget-cta-icon{display:flex;justify-content:center;align-items:center;width:18px;height:18px;margin-right:12px;color:#fff}.widget-doughnut--value{line-height:1.3;font-weight:900;font-size:36px;text-align:center}.widget-doughnut--meta{margin-top:10px;font-style:italic;color:#737373;text-align:center}.widget-number{height:100%;display:flex;align-items:center}.widget-number-icon{display:flex;align-items:center;width:42px;margin-right:20px;color:#000}.widget-number-content{display:flex;flex-direction:column;justify-content:center}.widget-number-title{line-height:1.3;margin-bottom:5px;font-size:16px;color:#000}.widget-number-number{line-height:1.3;font-weight:900;font-size:24px}
\ No newline at end of file
diff --git a/typo3/sysext/dashboard/Tests/Unit/DependencyInjection/DashboardWidgetPassTest.php b/typo3/sysext/dashboard/Tests/Unit/DependencyInjection/DashboardWidgetPassTest.php
new file mode 100644
index 000000000000..13b4176a900d
--- /dev/null
+++ b/typo3/sysext/dashboard/Tests/Unit/DependencyInjection/DashboardWidgetPassTest.php
@@ -0,0 +1,226 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Dashboard\Tests\Unit;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Prophecy\Argument;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Definition;
+use TYPO3\CMS\Dashboard\DependencyInjection\DashboardWidgetPass;
+use TYPO3\CMS\Dashboard\WidgetRegistry;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+class DashboardWidgetPassTest extends UnitTestCase
+{
+    /**
+     * @var DashboardWidgetPass
+     */
+    protected $subject;
+
+    /**
+     * @var ContainerBuilder
+     */
+    protected $container;
+
+    /**
+     * @var Definition
+     */
+    protected $widgetRegistryDefinition;
+
+    public function setUp(): void
+    {
+        parent::setUp();
+
+        $this->subject = new DashboardWidgetPass('dashboard.widget');
+        $this->container = $this->prophesize(ContainerBuilder::class);
+        $this->widgetRegistryDefinition = $this->prophesize(Definition::class);
+    }
+
+    /**
+     * @test
+     */
+    public function doesNothingIfWidgetRegistryIsUnkown(): void
+    {
+        $this->container->findDefinition(WidgetRegistry::class)->willReturn(false);
+        $this->container->findTaggedServiceIds('dashboard.widget')->shouldNotBeCalled();
+
+        $this->subject->process($this->container->reveal());
+    }
+
+    /**
+     * @test
+     */
+    public function doesNothingIfNoWidgetsAreTagged(): void
+    {
+        $this->container->findDefinition(WidgetRegistry::class)->willReturn($this->widgetRegistryDefinition->reveal());
+        $this->container->findTaggedServiceIds('dashboard.widget')->willReturn([])->shouldBeCalled();
+        $this->widgetRegistryDefinition->addMethodCall()->shouldNotBeCalled();
+
+        $this->subject->process($this->container->reveal());
+    }
+
+    /**
+     * @test
+     */
+    public function makesWidgetPublic(): void
+    {
+        $this->container->findDefinition(WidgetRegistry::class)->willReturn($this->widgetRegistryDefinition->reveal());
+        $this->container->findTaggedServiceIds('dashboard.widget')->willReturn(['NewsWidget' => []]);
+        $definition = $this->prophesize(Definition::class);
+        $this->container->findDefinition('NewsWidget')->willReturn($definition->reveal());
+        $definition->setPublic(true)->shouldBeCalled();
+
+        $this->subject->process($this->container->reveal());
+    }
+
+    /**
+     * @test
+     */
+    public function registersTaggedWidgetWithMiniumConfigurationInRegistry(): void
+    {
+        $this->container->findDefinition(WidgetRegistry::class)->willReturn($this->widgetRegistryDefinition->reveal());
+        $definition = $this->prophesize(Definition::class);
+        $this->container->findDefinition('dashboard.widget.t3news')->willReturn($definition->reveal());
+        $definition->setPublic(true);
+        $definition->setArgument('$configuration', Argument::that(function ($argument) {
+            return $argument instanceof Reference && (string)$argument === 't3newsWidgetConfiguration';
+        }));
+
+        $this->container->findTaggedServiceIds('dashboard.widget')->willReturn([
+            'dashboard.widget.t3news' => [
+                [
+                    'identifier' => 't3news',
+                    'groupNames' => 'typo3',
+                    'title' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.title',
+                    'description' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.description',
+                ]
+            ]
+        ]);
+        $this->container->addDefinitions(Argument::that(function (array $widgetConfigurationDefinitions) {
+            $definition = $widgetConfigurationDefinitions['t3newsWidgetConfiguration'];
+            /* @var Definition $definition */
+            return $definition instanceof Definition
+                && $definition->getClass(WidgetConfiguration::class)
+                && $definition->getArgument('$identifier') === 't3news'
+                && $definition->getArgument('$groupNames') === ['typo3']
+                && $definition->getArgument('$title') ===  'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.title'
+                && $definition->getArgument('$description') ===  'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.description'
+                && $definition->getArgument('$iconIdentifier') ===  'content-dashboard'
+                && $definition->getArgument('$height') === 'small'
+                && $definition->getArgument('$width') === 'small'
+                ;
+        }))->shouldBeCalled();
+        $this->widgetRegistryDefinition->addMethodCall(
+            'registerWidget',
+            [
+                't3newsWidgetConfiguration',
+            ]
+        )->shouldBeCalled();
+
+        $this->subject->process($this->container->reveal());
+    }
+
+    /**
+     * @test
+     */
+    public function registersWidgetToMultipleGroupsByComma(): void
+    {
+        $this->container->findDefinition(WidgetRegistry::class)->willReturn($this->widgetRegistryDefinition->reveal());
+        $definition = $this->prophesize(Definition::class);
+        $this->container->findDefinition('dashboard.widget.t3news')->willReturn($definition->reveal());
+        $definition->setPublic(true);
+        $definition->setArgument('$configuration', Argument::that(function ($argument) {
+            return $argument instanceof Reference && (string)$argument === 't3newsWidgetConfiguration';
+        }));
+
+        $this->container->findTaggedServiceIds('dashboard.widget')->willReturn([
+            'dashboard.widget.t3news' => [
+                [
+                    'identifier' => 't3news',
+                    'groupNames' => 'typo3, general',
+                    'title' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.title',
+                    'description' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.description',
+                ]
+            ]
+        ]);
+        $this->container->addDefinitions(Argument::that(function (array $widgetConfigurationDefinitions) {
+            $definition = $widgetConfigurationDefinitions['t3newsWidgetConfiguration'];
+            /* @var Definition $definition */
+            return $definition instanceof Definition
+                && $definition->getClass(WidgetConfiguration::class)
+                && $definition->getArgument('$groupNames') === ['typo3', 'general']
+                ;
+        }))->shouldBeCalled();
+        $this->widgetRegistryDefinition->addMethodCall(
+            'registerWidget',
+            [
+                't3newsWidgetConfiguration',
+            ]
+        )->shouldBeCalled();
+
+        $this->subject->process($this->container->reveal());
+    }
+
+    /**
+     * @test
+     */
+    public function registersTaggedWidgetWithMaximumConfigurationInRegistry(): void
+    {
+        $this->container->findDefinition(WidgetRegistry::class)->willReturn($this->widgetRegistryDefinition->reveal());
+        $definition = $this->prophesize(Definition::class);
+        $this->container->findDefinition('dashboard.widget.t3news')->willReturn($definition->reveal());
+        $definition->setPublic(true);
+        $definition->setArgument('$configuration', Argument::that(function ($argument) {
+            return $argument instanceof Reference && (string)$argument === 't3newsWidgetConfiguration';
+        }));
+
+        $this->container->findTaggedServiceIds('dashboard.widget')->willReturn([
+            'dashboard.widget.t3news' => [
+                [
+                    'identifier' => 't3news',
+                    'groupNames' => 'typo3',
+                    'title' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.title',
+                    'description' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.description',
+                    'iconIdentifier' => 'some-icon',
+                    'height' => 'large',
+                    'width' => 'medium',
+                ]
+            ]
+        ]);
+        $this->container->addDefinitions(Argument::that(function (array $widgetConfigurationDefinitions) {
+            $definition = $widgetConfigurationDefinitions['t3newsWidgetConfiguration'];
+            /* @var Definition $definition */
+            return $definition instanceof Definition
+                && $definition->getClass(WidgetConfiguration::class)
+                && $definition->getArgument('$identifier') === 't3news'
+                && $definition->getArgument('$groupNames') === ['typo3']
+                && $definition->getArgument('$title') ===  'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.title'
+                && $definition->getArgument('$description') ===  'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.description'
+                && $definition->getArgument('$iconIdentifier') ===  'some-icon'
+                && $definition->getArgument('$height') === 'large'
+                && $definition->getArgument('$width') === 'medium'
+                ;
+        }))->shouldBeCalled();
+        $this->widgetRegistryDefinition->addMethodCall(
+            'registerWidget',
+            [
+                't3newsWidgetConfiguration',
+            ]
+        )->shouldBeCalled();
+
+        $this->subject->process($this->container->reveal());
+    }
+}
diff --git a/typo3/sysext/dashboard/Tests/Unit/WidgetRegistryTest.php b/typo3/sysext/dashboard/Tests/Unit/WidgetRegistryTest.php
index ae2514e29f6c..7b245d443add 100644
--- a/typo3/sysext/dashboard/Tests/Unit/WidgetRegistryTest.php
+++ b/typo3/sysext/dashboard/Tests/Unit/WidgetRegistryTest.php
@@ -16,10 +16,12 @@ namespace TYPO3\CMS\Dashboard\Tests\Unit;
  * The TYPO3 project - inspiring people to share!
  */
 
+use Prophecy\Argument;
 use Prophecy\Prophecy\ObjectProphecy;
 use Psr\Container\ContainerInterface;
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
 use TYPO3\CMS\Dashboard\WidgetRegistry;
+use TYPO3\CMS\Dashboard\Widgets\Interfaces\WidgetConfigurationInterface;
 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 
 class WidgetRegistryTest extends UnitTestCase
@@ -29,7 +31,9 @@ class WidgetRegistryTest extends UnitTestCase
      */
     protected $resetSingletonInstances = true;
 
-    /** @var WidgetRegistry  */
+    /**
+     * @var WidgetRegistry
+     */
     protected $subject;
 
     /**
@@ -37,13 +41,18 @@ class WidgetRegistryTest extends UnitTestCase
      */
     protected $beUserProphecy;
 
+    /**
+     * @var ContainerInterface|ObjectProphecy
+     */
+    protected $containerProphecy;
+
     public function setUp(): void
     {
         $this->beUserProphecy = $this->prophesize(BackendUserAuthentication::class);
-        $containerProphecy = $this->prophesize(ContainerInterface::class);
+        $this->containerProphecy = $this->prophesize(ContainerInterface::class);
 
         $GLOBALS['BE_USER'] = $this->beUserProphecy->reveal();
-        $this->subject = new WidgetRegistry($containerProphecy->reveal());
+        $this->subject = new WidgetRegistry($this->containerProphecy->reveal());
     }
 
     /**
@@ -63,13 +72,31 @@ class WidgetRegistryTest extends UnitTestCase
      */
     public function getAllWidgetReturnsAllRegisteredWidgets(array $expectedValues, array $widgetsToRegister): void
     {
-        foreach ($widgetsToRegister as $widget) {
-            $this->subject->registerWidget($widget['identifier'], $widget['className'], $widget['groups']);
-        }
+        $this->registerWidgets($widgetsToRegister);
 
         self::assertCount((int)$expectedValues['count'], $this->subject->getAllWidgets());
     }
 
+    /**
+     * @param array $expectedValues
+     * @param array $widgetsToRegister
+     *
+     * @test
+     * @dataProvider widgetsToRegister
+     */
+    public function returnsWidgetsForGroup(
+        array $expectedValues,
+        array $widgetsToRegister
+    ): void {
+        $this->registerWidgets($widgetsToRegister);
+        $this->beUserProphecy->check('available_widgets', Argument::any())->willReturn(true);
+
+        self::assertCount(
+            (int)$expectedValues['group1Count'],
+            $this->subject->getAvailableWidgetsForWidgetGroup('group1')
+        );
+    }
+
     /**
      * @param array $expectedValues
      * @param array $widgetsToRegister
@@ -82,7 +109,7 @@ class WidgetRegistryTest extends UnitTestCase
         array $widgetsToRegister
     ): void {
         foreach ($widgetsToRegister as $widget) {
-            $this->subject->registerWidget($widget['identifier'], $widget['className'], $widget['groups']);
+            $this->registerWidget($widget);
 
             $this->beUserProphecy
                 ->check(
@@ -107,7 +134,7 @@ class WidgetRegistryTest extends UnitTestCase
         array $widgetsToRegister
     ): void {
         foreach ($widgetsToRegister as $widget) {
-            $this->subject->registerWidget($widget['identifier'], $widget['className'], $widget['groups']);
+            $this->registerWidget($widget);
 
             $this->beUserProphecy
                 ->check(
@@ -121,89 +148,242 @@ class WidgetRegistryTest extends UnitTestCase
         self::assertCount((int)$expectedValues['userCount'], $this->subject->getAvailableWidgets());
     }
 
+    /**
+     * @test
+     */
+    public function addWidgetsInItemsProcFunc(): void
+    {
+        $this->registerWidgets([
+            [
+                'serviceName' => 'dashboard.widget.t3news',
+                'identifier' => 't3orgnews',
+                'groups' => ['typo3'],
+                'iconIdentifier' => 'content-widget-rss',
+                'title' => 'LLL:EXT:dashboard/Resources/Private/Language/Widgets.xlf:T3OrgNews.title',
+                'description' => 'LLL:EXT:dashboard/Resources/Private/Language/Widgets.xlf:T3OrgNews.description',
+                'height' => 4,
+                'width' => 4,
+                'additionalCssClasses' => [],
+            ],
+            [
+                'serviceName' => 'dashboard.widget.t3comnews',
+                'identifier' => '2ndWidget',
+                'groups' => ['typo3'],
+                'iconIdentifier' => 'content-widget-2nd',
+                'title' => 'LLL:EXT:dashboard/Resources/Private/Language/Widgets.xlf:2ndWidget.title',
+                'description' => 'LLL:EXT:dashboard/Resources/Private/Language/Widgets.xlf:2ndWidget.description',
+                'height' => 4,
+                'width' => 4,
+                'additionalCssClasses' => [],
+            ],
+        ]);
+
+        $parameters = [];
+        $this->subject->widgetItemsProcFunc($parameters);
+
+        self::assertEquals(
+            [
+                'items' => [
+                    [
+                        'LLL:EXT:dashboard/Resources/Private/Language/Widgets.xlf:T3OrgNews.title',
+                        't3orgnews',
+                        'content-widget-rss',
+                        'LLL:EXT:dashboard/Resources/Private/Language/Widgets.xlf:T3OrgNews.description',
+                    ],
+                    [
+                        'LLL:EXT:dashboard/Resources/Private/Language/Widgets.xlf:2ndWidget.title',
+                        '2ndWidget',
+                        'content-widget-2nd',
+                        'LLL:EXT:dashboard/Resources/Private/Language/Widgets.xlf:2ndWidget.description',
+                    ],
+                ]
+            ],
+            $parameters
+        );
+    }
+
+    private function registerWidgets(array $widgetsToRegister): void
+    {
+        foreach ($widgetsToRegister as $widget) {
+            $this->registerWidget($widget);
+        }
+    }
+
+    private function registerWidget(array $widget)
+    {
+        $widgetConfiguration = $this->prophesize(WidgetConfigurationInterface::class);
+        $widgetConfiguration->getTitle()->willReturn($widget['title']);
+        $widgetConfiguration->getIdentifier()->willReturn($widget['identifier']);
+        $widgetConfiguration->getIconIdentifier()->willReturn($widget['iconIdentifier']);
+        $widgetConfiguration->getDescription()->willReturn($widget['description']);
+        $widgetConfiguration->getGroupNames()->willReturn($widget['groups']);
+        $widgetConfiguration->getHeight()->willReturn($widget['height']);
+        $widgetConfiguration->getWidth()->willReturn($widget['width']);
+        $widgetConfiguration->getAdditionalCssClasses()->willReturn($widget['additionalCssClasses']);
+
+        $this->containerProphecy->get($widget['serviceName'])->willReturn($widgetConfiguration->reveal());
+        $this->subject->registerWidget($widget['serviceName']);
+    }
+
     public function widgetsToRegister(): array
     {
         return [
-            [
+            'Single widget' => [
                 [
                     'count' => 1,
                     'adminCount' => 1,
-                    'userCount' => 1
+                    'userCount' => 1,
+                    'group1Count' => 1,
                 ],
                 [
                     [
                         'identifier' => 'test-widget1',
-                        'className' => 'TYPO3\CMS\Dashboard\Widgets\T3NewsWidget',
+                        'serviceName' => 'dashboard.widget.t3news',
                         'groups' => ['group1'],
+                        'title' => 'LLL:EXT:dashboard/Resources/Private/Language/Widgets.xlf:T3OrgNews.title',
+                        'description' => 'LLL:EXT:dashboard/Resources/Private/Language/Widgets.xlf:T3OrgNews.description',
+                        'iconIdentifier' => 'content-widget-rss',
+                        'height' => 2,
+                        'width' => 4,
+                        'additionalCssClasses' => [
+                            'custom-widget',
+                            'rss-condensed',
+                        ],
                         'availableForUser' => true
                     ]
                 ]
             ],
-            [
+            'Two widgets' => [
                 [
                     'count' => 2,
                     'adminCount' => 2,
-                    'userCount' => 1
+                    'userCount' => 1,
+                    'group1Count' => 2,
                 ],
                 [
                     [
                         'identifier' => 'test-widget1',
-                        'className' => 'TYPO3\CMS\Dashboard\Widgets\T3NewsWidget',
+                        'serviceName' => 'dashboard.widget.t3news',
                         'groups' => ['group1'],
-                        'availableForUser' => true
+                        'title' => 'LLL:EXT:dashboard/Resources/Private/Language/Widgets.xlf:T3OrgNews.title',
+                        'description' => 'LLL:EXT:dashboard/Resources/Private/Language/Widgets.xlf:T3OrgNews.description',
+                        'iconIdentifier' => 'content-widget-rss',
+                        'height' => 2,
+                        'width' => 4,
+                        'additionalCssClasses' => [
+                            'custom-widget',
+                            'rss-condensed',
+                        ],
+                        'availableForUser' => true,
                     ],
                     [
                         'identifier' => 'test-widget2',
-                        'className' => 'TYPO3\CMS\Dashboard\Widgets\T3NewsWidget',
+                        'serviceName' => 'dashboard.widget.t3comnews',
                         'groups' => ['group1'],
+                        'title' => 'LLL:EXT:dashboard/Resources/Private/Language/Widgets.xlf:T3OrgNews.title',
+                        'description' => 'LLL:EXT:dashboard/Resources/Private/Language/Widgets.xlf:T3OrgNews.description',
+                        'iconIdentifier' => 'content-widget-rss',
+                        'height' => 2,
+                        'width' => 4,
+                        'additionalCssClasses' => [
+                            'custom-widget',
+                            'rss-condensed',
+                        ],
                         'availableForUser' => false
                     ],
                 ]
             ],
-            [
+            'Three widgets, two having same identifier' => [
                 [
                     'count' => 2,
                     'adminCount' => 2,
-                    'userCount' => 2
+                    'userCount' => 2,
+                    'group1Count' => 1,
                 ],
                 [
                     [
                         'identifier' => 'test-widget1',
-                        'className' => 'TYPO3\CMS\Dashboard\Widgets\T3NewsWidget',
+                        'serviceName' => 'dashboard.widget.t3news',
                         'groups' => ['group1'],
+                        'title' => 'LLL:EXT:dashboard/Resources/Private/Language/Widgets.xlf:T3OrgNews.title',
+                        'description' => 'LLL:EXT:dashboard/Resources/Private/Language/Widgets.xlf:T3OrgNews.description',
+                        'iconIdentifier' => 'content-widget-rss',
+                        'height' => 2,
+                        'width' => 4,
+                        'additionalCssClasses' => [
+                            'custom-widget',
+                            'rss-condensed',
+                        ],
                         'availableForUser' => true
                     ],
                     [
                         'identifier' => 'test-widget1',
-                        'className' => 'TYPO3\CMS\Dashboard\Widgets\T3NewsWidget',
+                        'serviceName' => 'dashboard.widget.t3news',
                         'groups' => ['group1'],
+                        'title' => 'LLL:EXT:dashboard/Resources/Private/Language/Widgets.xlf:T3OrgNews.title',
+                        'description' => 'LLL:EXT:dashboard/Resources/Private/Language/Widgets.xlf:T3OrgNews.description',
+                        'iconIdentifier' => 'content-widget-rss',
+                        'height' => 2,
+                        'width' => 4,
+                        'additionalCssClasses' => [
+                            'custom-widget',
+                            'rss-condensed',
+                        ],
                         'availableForUser' => true
                     ],
                     [
                         'identifier' => 'test-widget2',
-                        'className' => 'TYPO3\CMS\Dashboard\Widgets\T3NewsWidget',
+                        'serviceName' => 'dashboard.widget.t3orgnews',
                         'groups' => ['group2'],
+                        'title' => 'LLL:EXT:dashboard/Resources/Private/Language/Widgets.xlf:T3OrgNews.title',
+                        'description' => 'LLL:EXT:dashboard/Resources/Private/Language/Widgets.xlf:T3OrgNews.description',
+                        'iconIdentifier' => 'content-widget-rss',
+                        'height' => 2,
+                        'width' => 4,
+                        'additionalCssClasses' => [
+                            'custom-widget',
+                            'rss-condensed',
+                        ],
                         'availableForUser' => true
                     ],
                 ]
             ],
-            [
+            'Two widgets, one not available for user' => [
                 [
                     'count' => 2,
                     'adminCount' => 2,
-                    'userCount' => 1
+                    'userCount' => 1,
+                    'group1Count' => 1,
                 ],
                 [
                     [
                         'identifier' => 'test-widget1',
-                        'className' => 'TYPO3\CMS\Dashboard\Widgets\T3NewsWidget',
+                        'serviceName' => 'dashboard.widget.t3news',
                         'groups' => ['group1'],
+                        'title' => 'LLL:EXT:dashboard/Resources/Private/Language/Widgets.xlf:T3OrgNews.title',
+                        'description' => 'LLL:EXT:dashboard/Resources/Private/Language/Widgets.xlf:T3OrgNews.description',
+                        'iconIdentifier' => 'content-widget-rss',
+                        'height' => 2,
+                        'width' => 4,
+                        'additionalCssClasses' => [
+                            'custom-widget',
+                            'rss-condensed',
+                        ],
                         'availableForUser' => true
                     ],
                     [
                         'identifier' => 'test-widget2',
-                        'className' => 'TYPO3\CMS\Dashboard\Widgets\T3NewsWidget',
-                        'groups' => ['group1', 'group2'],
+                        'serviceName' => 'dashboard.widget.t3comnews',
+                        'groups' => ['group2'],
+                        'title' => 'LLL:EXT:dashboard/Resources/Private/Language/Widgets.xlf:T3OrgNews.title',
+                        'description' => 'LLL:EXT:dashboard/Resources/Private/Language/Widgets.xlf:T3OrgNews.description',
+                        'iconIdentifier' => 'content-widget-rss',
+                        'height' => 2,
+                        'width' => 4,
+                        'additionalCssClasses' => [
+                            'custom-widget',
+                            'rss-condensed',
+                        ],
                         'availableForUser' => false
                     ],
                 ]
diff --git a/typo3/sysext/install/Configuration/ExtensionScanner/Php/ClassNameMatcher.php b/typo3/sysext/install/Configuration/ExtensionScanner/Php/ClassNameMatcher.php
index 8afba7fb77ee..42d542fafbfa 100644
--- a/typo3/sysext/install/Configuration/ExtensionScanner/Php/ClassNameMatcher.php
+++ b/typo3/sysext/install/Configuration/ExtensionScanner/Php/ClassNameMatcher.php
@@ -1412,4 +1412,69 @@ return [
             'Deprecation-90937-VariousHooksInContentObjectRenderer.rst',
         ],
     ],
+    'TYPO3\CMS\Dashboard\Widgets\AbstractBarChartWidget' => [
+        'restFiles' => [
+            'Breaking-90660-RegistrationOfWidgetsChanged.rst',
+        ],
+    ],
+    'TYPO3\CMS\Dashboard\Widgets\AbstractChartWidget' => [
+        'restFiles' => [
+            'Breaking-90660-RegistrationOfWidgetsChanged.rst',
+        ],
+    ],
+    'TYPO3\CMS\Dashboard\Widgets\AbstractCtaButtonWidget' => [
+        'restFiles' => [
+            'Breaking-90660-RegistrationOfWidgetsChanged.rst',
+        ],
+    ],
+    'TYPO3\CMS\Dashboard\Widgets\AbstractDoughnutChartWidget' => [
+        'restFiles' => [
+            'Breaking-90660-RegistrationOfWidgetsChanged.rst',
+        ],
+    ],
+    'TYPO3\CMS\Dashboard\Widgets\AbstractListWidget' => [
+        'restFiles' => [
+            'Breaking-90660-RegistrationOfWidgetsChanged.rst',
+        ],
+    ],
+    'TYPO3\CMS\Dashboard\Widgets\AbstractNumberWithIconWidget' => [
+        'restFiles' => [
+            'Breaking-90660-RegistrationOfWidgetsChanged.rst',
+        ],
+    ],
+    'TYPO3\CMS\Dashboard\Widgets\AbstractRssWidget' => [
+        'restFiles' => [
+            'Breaking-90660-RegistrationOfWidgetsChanged.rst',
+        ],
+    ],
+    'TYPO3\CMS\Dashboard\Widgets\AbstractWidget' => [
+        'restFiles' => [
+            'Breaking-90660-RegistrationOfWidgetsChanged.rst',
+        ],
+    ],
+    'TYPO3\CMS\Dashboard\Widgets\DocumentationGettingStartedWidget' => [
+        'restFiles' => [
+            'Breaking-90660-RegistrationOfWidgetsChanged.rst',
+        ],
+    ],
+    'TYPO3\CMS\Dashboard\Widgets\DocumentationTSconfigReferenceWidget' => [
+        'restFiles' => [
+            'Breaking-90660-RegistrationOfWidgetsChanged.rst',
+        ],
+    ],
+    'TYPO3\CMS\Dashboard\Widgets\T3GeneralInformation' => [
+        'restFiles' => [
+            'Breaking-90660-RegistrationOfWidgetsChanged.rst',
+        ],
+    ],
+    'TYPO3\CMS\Dashboard\Widgets\T3NewsWidget' => [
+        'restFiles' => [
+            'Breaking-90660-RegistrationOfWidgetsChanged.rst',
+        ],
+    ],
+    'TYPO3\CMS\Dashboard\Widgets\T3SecurityAdvisoriesWidget' => [
+        'restFiles' => [
+            'Breaking-90660-RegistrationOfWidgetsChanged.rst',
+        ],
+    ],
 ];
-- 
GitLab