From d7525b535132f12fe77dd754c0da56d052b90b12 Mon Sep 17 00:00:00 2001 From: Oliver Bartsch <bo@cedev.de> Date: Sat, 22 Feb 2020 02:58:52 +0100 Subject: [PATCH] [FEATURE] Dashboard widget: number of failed logins A new widget is added which displays the number of failed logins during the last 24 hours. Resolves: #90355 Releases: master Change-Id: I24e9e40fd7ed567f97867910d6988d8662fd7a14 Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/63364 Tested-by: Oliver Bartsch <bo@cedev.de> Tested-by: Koen Wouters <koen.wouters@maxserv.com> Tested-by: TYPO3com <noreply@typo3.com> Tested-by: Richard Haeser <richard@maxserv.com> Reviewed-by: Oliver Bartsch <bo@cedev.de> Reviewed-by: Koen Wouters <koen.wouters@maxserv.com> Reviewed-by: Richard Haeser <richard@maxserv.com> --- Build/Sources/Sass/dashboard/_widget.scss | 1 + .../Sass/dashboard/_widget_number.scss | 8 +- .../master/Feature-90333-Dashboard.rst | 4 +- .../Widgets/AbstractNumberWithIconWidget.php | 69 +++++++++++++++++ .../Classes/Widgets/FailedLoginsWidget.php | 74 +++++++++++++++++++ .../dashboard/Configuration/Services.yaml | 7 ++ .../Resources/Private/Language/locallang.xlf | 10 +++ .../Widget/NumberWithIconWidget.html | 17 +++++ .../Private/Templates/Widget/TextWidget.html | 8 -- .../Resources/Public/Css/dashboard.css | 2 +- 10 files changed, 186 insertions(+), 14 deletions(-) create mode 100644 typo3/sysext/dashboard/Classes/Widgets/AbstractNumberWithIconWidget.php create mode 100644 typo3/sysext/dashboard/Classes/Widgets/FailedLoginsWidget.php create mode 100644 typo3/sysext/dashboard/Resources/Private/Templates/Widget/NumberWithIconWidget.html delete mode 100644 typo3/sysext/dashboard/Resources/Private/Templates/Widget/TextWidget.html diff --git a/Build/Sources/Sass/dashboard/_widget.scss b/Build/Sources/Sass/dashboard/_widget.scss index 20423a586bfe..f61bf4a9b8d8 100644 --- a/Build/Sources/Sass/dashboard/_widget.scss +++ b/Build/Sources/Sass/dashboard/_widget.scss @@ -39,6 +39,7 @@ } .widget-content-main { + display: flex; flex-grow: 1; overflow-y: auto; padding: $widget-padding; diff --git a/Build/Sources/Sass/dashboard/_widget_number.scss b/Build/Sources/Sass/dashboard/_widget_number.scss index 4b6bc3448b0a..84e3324a8bbf 100644 --- a/Build/Sources/Sass/dashboard/_widget_number.scss +++ b/Build/Sources/Sass/dashboard/_widget_number.scss @@ -1,4 +1,4 @@ -.dashboard-widget-number--icon { +.widget-number-icon { display: flex; justify-content: center; align-items: center; @@ -7,20 +7,20 @@ color: $text-color; } -.dashboard-widget-number--content { +.widget-number-content { display: flex; flex-direction: column; justify-content: center; } -.dashboard-widget-number--title { +.widget-number-title { line-height: 1.3; margin-bottom: 5px; font-size: 16px; color: $text-color; } -.dashboard-widget-number--number { +.widget-number-number { line-height: 1.3; font-weight: 900; font-size: 24px; diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-90333-Dashboard.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-90333-Dashboard.rst index eabde7ccf127..884f3e92021b 100644 --- a/typo3/sysext/core/Documentation/Changelog/master/Feature-90333-Dashboard.rst +++ b/typo3/sysext/core/Documentation/Changelog/master/Feature-90333-Dashboard.rst @@ -36,7 +36,8 @@ The following widgets are shipped by core extensions now: * TypoScript Template Reference: This widget will provide a link to the TypoScript Template Reference (EXT:dashboard) * TSconfig Reference: This widget will provide a link to the TSconfig Reference (EXT:dashboard) * Number of errors in system log: Shows the number of errors in the sys_log grouped by day for the last month (EXT:dashboard) -* Type of backend users: A widget to show the different types of backend users +* Type of backend users: A widget to show the different types of backend users (EXT:dashboard) +* Failed Logins: This widget will show you the number of failed logins during the last 24 hours (EXT:dashboard) Creating your own widget ^^^^^^^^^^^^^^^^^^^^^^^^ @@ -50,6 +51,7 @@ do so, you can extend one of the WidgetAbstracts available in EXT:dashboard. * AbstractChartWidget: the base of all chart widgets * AbstractBarChartWidget: when you want to show a widget with a bar-chart you can extend this class * AbstractDoughnutChartWidget: this abstract gives you the possibility to create a doughnut-chart widget +* AbstractNumberWithIconWidget: this abstract will give you the possibility to show a title, number and an icon By extending one of those abstracts, and provide it with the right data, you are able to have a new widget quite fast. The only thing that is left is to register the widget. diff --git a/typo3/sysext/dashboard/Classes/Widgets/AbstractNumberWithIconWidget.php b/typo3/sysext/dashboard/Classes/Widgets/AbstractNumberWithIconWidget.php new file mode 100644 index 000000000000..29855ce116e2 --- /dev/null +++ b/typo3/sysext/dashboard/Classes/Widgets/AbstractNumberWithIconWidget.php @@ -0,0 +1,69 @@ +<?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 AbstractTextWidget class is the basic widget class for simple text content. + * It is possible to extend this class for own widgets. + * In your class you have to set $this->text with the text to display. + */ +abstract class AbstractNumberWithIconWidget extends AbstractWidget +{ + /** + * @inheritDoc + */ + protected $iconIdentifier = 'content-widget-number'; + + /** + * @inheritDoc + */ + protected $templateName = 'NumberWithIconWidget'; + + /** + * 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; + + 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/FailedLoginsWidget.php b/typo3/sysext/dashboard/Classes/Widgets/FailedLoginsWidget.php new file mode 100644 index 000000000000..f957aa5106cf --- /dev/null +++ b/typo3/sysext/dashboard/Classes/Widgets/FailedLoginsWidget.php @@ -0,0 +1,74 @@ +<?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\Database\Connection; +use TYPO3\CMS\Core\Database\ConnectionPool; +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; + +class FailedLoginsWidget extends AbstractNumberWithIconWidget +{ + protected $title = 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.failedLogins.title'; + protected $description = 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.failedLogins.description'; + protected $subtitle = 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.failedLogins.subtitle'; + protected $icon = 'content-elements-login'; + + protected function initializeView(): void + { + $this->number = $this->getNumberOfFailedLogins(); + parent::initializeView(); + } + + /** + * Get number of failed logins during a period + * + * @param int $secondsBack + * + * @return int + */ + public function getNumberOfFailedLogins(int $secondsBack = 86400): int + { + $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class); + $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_log'); + + return (int)$queryBuilder->count('uid') + ->from('sys_log') + ->where( + $queryBuilder->expr()->eq( + 'type', + $queryBuilder->createNamedParameter(SystemLogType::LOGIN, Connection::PARAM_INT) + ), + $queryBuilder->expr()->eq( + 'action', + $queryBuilder->createNamedParameter(SystemLogLoginAction::ATTEMPT, Connection::PARAM_INT) + ), + $queryBuilder->expr()->neq( + 'error', + $queryBuilder->createNamedParameter(SystemLogErrorClassification::MESSAGE, Connection::PARAM_INT) + ), + $queryBuilder->expr()->gt( + 'tstamp', + $queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'] - $secondsBack, Connection::PARAM_INT) + ) + ) + ->execute() + ->fetchColumn(); + } +} diff --git a/typo3/sysext/dashboard/Configuration/Services.yaml b/typo3/sysext/dashboard/Configuration/Services.yaml index 078c690555c5..1dd70867675e 100644 --- a/typo3/sysext/dashboard/Configuration/Services.yaml +++ b/typo3/sysext/dashboard/Configuration/Services.yaml @@ -58,6 +58,13 @@ services: 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: diff --git a/typo3/sysext/dashboard/Resources/Private/Language/locallang.xlf b/typo3/sysext/dashboard/Resources/Private/Language/locallang.xlf index bca37fc67bbb..4d1e13e2b42b 100644 --- a/typo3/sysext/dashboard/Resources/Private/Language/locallang.xlf +++ b/typo3/sysext/dashboard/Resources/Private/Language/locallang.xlf @@ -161,6 +161,16 @@ <source>Admin users</source> </trans-unit> + <trans-unit id="widgets.failedLogins.title" xml:space="preserve"> + <source>Failed backend logins</source> + </trans-unit> + <trans-unit id="widgets.failedLogins.subtitle" xml:space="preserve"> + <source>In last 24 hours</source> + </trans-unit> + <trans-unit id="widgets.failedLogins.description" xml:space="preserve"> + <source>Information about the number of failed logins during the last 24 hours.</source> + </trans-unit> + <trans-unit id="widget_group.general" xml:space="preserve"> <source>General</source> </trans-unit> diff --git a/typo3/sysext/dashboard/Resources/Private/Templates/Widget/NumberWithIconWidget.html b/typo3/sysext/dashboard/Resources/Private/Templates/Widget/NumberWithIconWidget.html new file mode 100644 index 000000000000..a21421906595 --- /dev/null +++ b/typo3/sysext/dashboard/Resources/Private/Templates/Widget/NumberWithIconWidget.html @@ -0,0 +1,17 @@ +<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" xmlns:core="http://typo3.org/ns/TYPO3/CMS/Core/ViewHelpers" data-namespace-typo3-fluid="true"> + +<div class="widget-content-main"> + <f:if condition="{icon}"> + <div class="widget-number-icon"> + <core:icon identifier="{icon}" size="large" alternativeMarkupIdentifier="inline" /> + </div> + </f:if> + <div class="widget-number-content"> + <div class="widget-number-title">{title}</div> + <f:if condition="{subtitle}"> + <div class="widget-number-subtitle"><small>{subtitle}</small></div> + </f:if> + <div class="widget-number-number">{number}</div> + </div> +</div> +</html> diff --git a/typo3/sysext/dashboard/Resources/Private/Templates/Widget/TextWidget.html b/typo3/sysext/dashboard/Resources/Private/Templates/Widget/TextWidget.html deleted file mode 100644 index 1fd3ed02ef83..000000000000 --- a/typo3/sysext/dashboard/Resources/Private/Templates/Widget/TextWidget.html +++ /dev/null @@ -1,8 +0,0 @@ -<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" xmlns:core="http://typo3.org/ns/TYPO3/CMS/Core/ViewHelpers" data-namespace-typo3-fluid="true"> -<f:layout name="Widget/Widget" /> -<f:section name="main"> - - <f:comment>You can put your text in this template</f:comment> - -</f:section> -</html> diff --git a/typo3/sysext/dashboard/Resources/Public/Css/dashboard.css b/typo3/sysext/dashboard/Resources/Public/Css/dashboard.css index dcdfa3211f42..d906339b077b 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}.dashboard-widget-number--icon{display:flex;justify-content:center;align-items:center;width:42px;margin-right:20px;color:#000}.dashboard-widget-number--content{display:flex;flex-direction:column;justify-content:center}.dashboard-widget-number--title{line-height:1.3;margin-bottom:5px;font-size:16px;color:#000}.dashboard-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--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{display:flex;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-icon{display:flex;justify-content:center;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 -- GitLab