From 6982597b6a6d319dacfbe3bee2edd2a39b3d6d68 Mon Sep 17 00:00:00 2001 From: Richard Steinmetz Date: Thu, 17 Aug 2023 15:09:30 +0200 Subject: [PATCH] feat(dashboard): implement widget item api v2 This API enables the dashboard to render all widgets from the API data alone without having apps to provide their own bundles. This saves a lot of traffic and execution time as a lot less javascript has to be parsed on the frontend. Signed-off-by: Richard Steinmetz --- apps/dashboard/appinfo/routes.php | 2 + .../lib/Controller/DashboardApiController.php | 72 +++++++- apps/dashboard/lib/ResponseDefinitions.php | 10 + apps/dashboard/openapi.json | 134 +++++++++++++- apps/dashboard/src/DashboardApp.vue | 106 +++++++++-- .../src/components/ApiDashboardWidget.vue | 140 ++++++++++++++ .../lib/Dashboard/UserStatusWidget.php | 27 +-- apps/user_status/src/dashboard.js | 44 ----- apps/user_status/src/views/Dashboard.vue | 121 ------------- .../Unit/Dashboard/UserStatusWidgetTest.php | 171 +----------------- lib/composer/composer/autoload_classmap.php | 3 + lib/composer/composer/autoload_static.php | 3 + lib/public/Dashboard/IAPIWidgetV2.php | 43 +++++ lib/public/Dashboard/IReloadableWidget.php | 41 +++++ lib/public/Dashboard/Model/WidgetItem.php | 25 ++- lib/public/Dashboard/Model/WidgetItems.php | 100 ++++++++++ webpack.modules.js | 1 - 17 files changed, 667 insertions(+), 376 deletions(-) create mode 100644 apps/dashboard/src/components/ApiDashboardWidget.vue delete mode 100644 apps/user_status/src/dashboard.js delete mode 100644 apps/user_status/src/views/Dashboard.vue create mode 100644 lib/public/Dashboard/IAPIWidgetV2.php create mode 100644 lib/public/Dashboard/IReloadableWidget.php create mode 100644 lib/public/Dashboard/Model/WidgetItems.php diff --git a/apps/dashboard/appinfo/routes.php b/apps/dashboard/appinfo/routes.php index c6891837384..e872c47084b 100644 --- a/apps/dashboard/appinfo/routes.php +++ b/apps/dashboard/appinfo/routes.php @@ -7,6 +7,7 @@ declare(strict_types=1); * * @author Julien Veyssier * @author Julius Härtl + * @author Richard Steinmetz * * @license GNU AGPL version 3 or any later version * @@ -33,5 +34,6 @@ return [ 'ocs' => [ ['name' => 'dashboardApi#getWidgets', 'url' => '/api/v1/widgets', 'verb' => 'GET'], ['name' => 'dashboardApi#getWidgetItems', 'url' => '/api/v1/widget-items', 'verb' => 'GET'], + ['name' => 'dashboardApi#getWidgetItemsV2', 'url' => '/api/v2/widget-items', 'verb' => 'GET'], ] ]; diff --git a/apps/dashboard/lib/Controller/DashboardApiController.php b/apps/dashboard/lib/Controller/DashboardApiController.php index df1c75e4b68..8855bf71700 100644 --- a/apps/dashboard/lib/Controller/DashboardApiController.php +++ b/apps/dashboard/lib/Controller/DashboardApiController.php @@ -7,6 +7,7 @@ declare(strict_types=1); * * @author Julien Veyssier * @author Kate Döen + * @author Richard Steinmetz * * @license GNU AGPL version 3 or any later version * @@ -35,6 +36,7 @@ use OCP\Dashboard\IButtonWidget; use OCP\Dashboard\IIconWidget; use OCP\Dashboard\IOptionWidget; use OCP\Dashboard\IManager; +use OCP\Dashboard\IReloadableWidget; use OCP\Dashboard\IWidget; use OCP\Dashboard\Model\WidgetButton; use OCP\Dashboard\Model\WidgetOptions; @@ -42,11 +44,14 @@ use OCP\IConfig; use OCP\IRequest; use OCP\Dashboard\IAPIWidget; +use OCP\Dashboard\IAPIWidgetV2; use OCP\Dashboard\Model\WidgetItem; +use OCP\Dashboard\Model\WidgetItems; /** * @psalm-import-type DashboardWidget from ResponseDefinitions * @psalm-import-type DashboardWidgetItem from ResponseDefinitions + * @psalm-import-type DashboardWidgetItems from ResponseDefinitions */ class DashboardApiController extends OCSController { @@ -71,6 +76,24 @@ class DashboardApiController extends OCSController { $this->userId = $userId; } + /** + * @param string[] $widgetIds Limit widgets to given ids + * @return IWidget[] + */ + private function getShownWidgets(array $widgetIds): array { + if (empty($widgetIds)) { + $systemDefault = $this->config->getAppValue('dashboard', 'layout', 'recommendations,spreed,mail,calendar'); + $widgetIds = explode(',', $this->config->getUserValue($this->userId, 'dashboard', 'layout', $systemDefault)); + } + + return array_filter( + $this->dashboardManager->getWidgets(), + static function (IWidget $widget) use ($widgetIds) { + return in_array($widget->getId(), $widgetIds); + }, + ); + } + /** * @NoAdminRequired * @NoCSRFRequired @@ -83,18 +106,11 @@ class DashboardApiController extends OCSController { * @return DataResponse, array{}> */ public function getWidgetItems(array $sinceIds = [], int $limit = 7, array $widgets = []): DataResponse { - $showWidgets = $widgets; $items = []; - - if (empty($showWidgets)) { - $systemDefault = $this->config->getAppValue('dashboard', 'layout', 'recommendations,spreed,mail,calendar'); - $showWidgets = explode(',', $this->config->getUserValue($this->userId, 'dashboard', 'layout', $systemDefault)); - } - - $widgets = $this->dashboardManager->getWidgets(); + $widgets = $this->getShownWidgets($widgets); foreach ($widgets as $widget) { - if ($widget instanceof IAPIWidget && in_array($widget->getId(), $showWidgets)) { - $items[$widget->getId()] = array_map(function (WidgetItem $item) { + if ($widget instanceof IAPIWidget) { + $items[$widget->getId()] = array_map(static function (WidgetItem $item) { return $item->jsonSerialize(); }, $widget->getItems($this->userId, $sinceIds[$widget->getId()] ?? null, $limit)); } @@ -103,6 +119,31 @@ class DashboardApiController extends OCSController { return new DataResponse($items); } + /** + * @NoAdminRequired + * @NoCSRFRequired + * + * Get the items for the widgets + * + * @param array $sinceIds Array indexed by widget Ids, contains date/id from which we want the new items + * @param int $limit Limit number of result items per widget + * @param string[] $widgets Limit results to specific widgets + * @return DataResponse, array{}> + */ + public function getWidgetItemsV2(array $sinceIds = [], int $limit = 7, array $widgets = []): DataResponse { + $items = []; + $widgets = $this->getShownWidgets($widgets); + foreach ($widgets as $widget) { + if ($widget instanceof IAPIWidgetV2) { + $items[$widget->getId()] = $widget + ->getItemsV2($this->userId, $sinceIds[$widget->getId()] ?? null, $limit) + ->jsonSerialize(); + } + } + + return new DataResponse($items); + } + /** * Get the widgets * @@ -124,6 +165,8 @@ class DashboardApiController extends OCSController { 'icon_url' => ($widget instanceof IIconWidget) ? $widget->getIconUrl() : '', 'widget_url' => $widget->getUrl(), 'item_icons_round' => $options->withRoundItemIcons(), + 'item_api_versions' => [], + 'reload_interval' => 0, ]; if ($widget instanceof IButtonWidget) { $data += [ @@ -136,6 +179,15 @@ class DashboardApiController extends OCSController { }, $widget->getWidgetButtons($this->userId)), ]; } + if ($widget instanceof IReloadableWidget) { + $data['reload_interval'] = $widget->getReloadInterval(); + } + if ($widget instanceof IAPIWidget) { + $data['item_api_versions'][] = 1; + } + if ($widget instanceof IAPIWidgetV2) { + $data['item_api_versions'][] = 2; + } return $data; }, $widgets); diff --git a/apps/dashboard/lib/ResponseDefinitions.php b/apps/dashboard/lib/ResponseDefinitions.php index 1c40f251f2a..b35531be2a7 100644 --- a/apps/dashboard/lib/ResponseDefinitions.php +++ b/apps/dashboard/lib/ResponseDefinitions.php @@ -5,6 +5,7 @@ declare(strict_types=1); * @copyright Copyright (c) 2023 Kate Döen * * @author Kate Döen + * @author Richard Steinmetz * * @license GNU AGPL version 3 or any later version * @@ -34,6 +35,8 @@ namespace OCA\Dashboard; * icon_url: string, * widget_url: ?string, * item_icons_round: bool, + * item_api_versions: int[], + * reload_interval: int, * buttons?: array{ * type: string, * text: string, @@ -46,8 +49,15 @@ namespace OCA\Dashboard; * title: string, * link: string, * iconUrl: string, + * overlayIconUrl: string, * sinceId: string, * } + * + * @psalm-type DashboardWidgetItems = array{ + * items: DashboardWidgetItem[], + * emptyContentMessage: string, + * halfEmptyContentMessage: string, + * } */ class ResponseDefinitions { } diff --git a/apps/dashboard/openapi.json b/apps/dashboard/openapi.json index 594aed76793..739ba2c4afc 100644 --- a/apps/dashboard/openapi.json +++ b/apps/dashboard/openapi.json @@ -53,7 +53,9 @@ "icon_class", "icon_url", "widget_url", - "item_icons_round" + "item_icons_round", + "item_api_versions", + "reload_interval" ], "properties": { "id": { @@ -79,6 +81,17 @@ "item_icons_round": { "type": "boolean" }, + "item_api_versions": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + }, + "reload_interval": { + "type": "integer", + "format": "int64" + }, "buttons": { "type": "array", "items": { @@ -110,6 +123,7 @@ "title", "link", "iconUrl", + "overlayIconUrl", "sinceId" ], "properties": { @@ -125,10 +139,35 @@ "iconUrl": { "type": "string" }, + "overlayIconUrl": { + "type": "string" + }, "sinceId": { "type": "string" } } + }, + "WidgetItems": { + "type": "object", + "required": [ + "items", + "emptyContentMessage", + "halfEmptyContentMessage" + ], + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WidgetItem" + } + }, + "emptyContentMessage": { + "type": "string" + }, + "halfEmptyContentMessage": { + "type": "string" + } + } } } }, @@ -291,6 +330,99 @@ } } } + }, + "/ocs/v2.php/apps/dashboard/api/v2/widget-items": { + "get": { + "operationId": "dashboard_api-get-widget-items-v2", + "summary": "Get the items for the widgets", + "tags": [ + "dashboard_api" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "sinceIds", + "in": "query", + "description": "Array indexed by widget Ids, contains date/id from which we want the new items", + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "Limit number of result items per widget", + "schema": { + "type": "integer", + "format": "int64", + "default": 7 + } + }, + { + "name": "widgets[]", + "in": "query", + "description": "Limit results to specific widgets", + "schema": { + "type": "array", + "default": [], + "items": { + "type": "string" + } + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "required": true, + "schema": { + "type": "string", + "default": "true" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/WidgetItems" + } + } + } + } + } + } + } + } + } + } + } } }, "tags": [] diff --git a/apps/dashboard/src/DashboardApp.vue b/apps/dashboard/src/DashboardApp.vue index 993340dae36..7a7b56da266 100644 --- a/apps/dashboard/src/DashboardApp.vue +++ b/apps/dashboard/src/DashboardApp.vue @@ -14,21 +14,44 @@ v-bind="{swapThreshold: 0.30, delay: 500, delayOnTouchOnly: true, touchStartThreshold: 3}" handle=".panel--header" @end="saveLayout"> -
-
-

-

- {{ t('dashboard', '"{title} icon"', { title: panels[panelId].title }) }} +