Skip to content

Commit ec1dfaf

Browse files
committed
Add support for client-rendered widgets
1 parent 69e3e75 commit ec1dfaf

15 files changed

Lines changed: 545 additions & 84 deletions

File tree

core/Widget/WidgetConfig.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ class WidgetConfig
2727
protected $action = '';
2828
protected $parameters = array();
2929
protected $middlewareParameters = array();
30+
protected $clientSideComponent = array();
31+
protected $clientSideProps = array();
3032
protected $name = '';
3133
protected $order = 99;
3234
protected $isEnabled = true;
@@ -352,6 +354,52 @@ public function getMiddlewareParameters()
352354
return $this->middlewareParameters;
353355
}
354356

357+
/**
358+
* Marks this widget as client-rendered by a Vue component exported by the given plugin bundle.
359+
*
360+
* @param string $plugin eg 'Transitions'
361+
* @param string $component eg 'TransitionsPage'
362+
* @return static
363+
*/
364+
public function setClientSideComponent(string $plugin, string $component)
365+
{
366+
$this->clientSideComponent = array(
367+
'plugin' => $plugin,
368+
'name' => $component,
369+
);
370+
371+
return $this;
372+
}
373+
374+
/**
375+
* Returns the configured client-rendered component definition.
376+
*/
377+
public function getClientSideComponent(): array
378+
{
379+
return $this->clientSideComponent;
380+
}
381+
382+
/**
383+
* Sets props that should be passed to the client-rendered Vue widget.
384+
*
385+
* @param array $props
386+
* @return static
387+
*/
388+
public function setClientSideProps(array $props)
389+
{
390+
$this->clientSideProps = $props;
391+
392+
return $this;
393+
}
394+
395+
/**
396+
* Returns props configured for the client-rendered Vue widget.
397+
*/
398+
public function getClientSideProps(): array
399+
{
400+
return $this->clientSideProps;
401+
}
402+
355403
/**
356404
* Marks this widget as a "wide" widget that requires the full width.
357405
*

plugins/API/WidgetMetadata.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,17 @@ public function buildWidgetMetadata(WidgetConfig $widget, $categoryList = null,
116116
$item['middlewareParameters'] = $middleware;
117117
}
118118

119+
$clientComponent = $widget->getClientSideComponent();
120+
121+
if (!empty($clientComponent)) {
122+
$item['clientComponent'] = $clientComponent;
123+
124+
$clientProps = $widget->getClientSideProps();
125+
if (!empty($clientProps)) {
126+
$item['clientComponent']['props'] = $clientProps;
127+
}
128+
}
129+
119130
if ($widget instanceof ReportWidgetConfig) {
120131
$item['viewDataTable'] = $widget->getViewDataTable();
121132
$item['isReport'] = true;

plugins/API/tests/Unit/WidgetMetadataTest.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,20 @@ public function testBuildWidgetMetadataShouldAddOptionalMiddlewareParameters()
114114
$this->assertSame(array('module' => 'Goals', 'action' => 'hasAnyConversions'), $metadata['middlewareParameters']);
115115
}
116116

117+
public function testBuildWidgetMetadataShouldAddOptionalClientComponent()
118+
{
119+
$config = $this->createWidgetConfig('Test', 'CategoryId', 'SubcategoryId');
120+
$config->setClientSideComponent('Transitions', 'TransitionsPage');
121+
$config->setClientSideProps(array('foo' => 'bar'));
122+
$metadata = $this->metadata->buildWidgetMetadata($config);
123+
124+
$this->assertSame(array(
125+
'plugin' => 'Transitions',
126+
'name' => 'TransitionsPage',
127+
'props' => array('foo' => 'bar'),
128+
), $metadata['clientComponent']);
129+
}
130+
117131
public function testBuildWidgetMetadataShouldAddReportInformtionIfReportWidgetConfigGiven()
118132
{
119133
$config = new ReportWidgetConfig();

plugins/CoreHome/vue/dist/CoreHome.umd.js

Lines changed: 125 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

plugins/CoreHome/vue/dist/CoreHome.umd.min.js

Lines changed: 50 additions & 50 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
<!--
2+
Matomo - free/libre analytics platform
3+
4+
@link https://matomo.org
5+
@license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
6+
-->
7+
8+
<template>
9+
<ActivityIndicator
10+
v-if="loading"
11+
:loading="true"
12+
:loading-message="translate('General_LoadingData')"
13+
/>
14+
<Alert
15+
v-else-if="loadingFailed"
16+
severity="danger"
17+
>
18+
{{ translate('General_ErrorRequest', '', '') }}
19+
</Alert>
20+
<component
21+
v-else-if="componentToRender"
22+
:is="componentToRender"
23+
v-bind="componentProps"
24+
/>
25+
</template>
26+
27+
<script lang="ts">
28+
import {
29+
Component,
30+
defineComponent,
31+
markRaw,
32+
} from 'vue';
33+
import ActivityIndicator from '../ActivityIndicator/ActivityIndicator.vue';
34+
import Alert from '../Alert/Alert.vue';
35+
import importPluginUmd from '../importPluginUmd';
36+
import { Widget as WidgetData } from './types';
37+
38+
interface ClientWidgetRendererState {
39+
componentToRender: Component|null;
40+
loading: boolean;
41+
loadingFailed: boolean;
42+
}
43+
44+
export default defineComponent({
45+
props: {
46+
widget: {
47+
type: Object,
48+
required: true,
49+
},
50+
widgetized: Boolean,
51+
},
52+
components: {
53+
ActivityIndicator,
54+
Alert,
55+
},
56+
data(): ClientWidgetRendererState {
57+
return {
58+
componentToRender: null,
59+
loading: false,
60+
loadingFailed: false,
61+
};
62+
},
63+
watch: {
64+
widget: {
65+
handler() {
66+
this.loadComponent();
67+
},
68+
immediate: true,
69+
},
70+
},
71+
computed: {
72+
componentProps() {
73+
const widget = this.widget as WidgetData;
74+
return {
75+
...(widget.clientComponent?.props || {}),
76+
uniqueId: widget.uniqueId,
77+
widgetName: widget.name,
78+
widgetized: this.widgetized,
79+
isWidget: this.widgetized,
80+
isWide: widget.isWide,
81+
};
82+
},
83+
},
84+
methods: {
85+
async loadComponent() {
86+
const widget = this.widget as WidgetData;
87+
const { clientComponent } = widget;
88+
this.loading = true;
89+
this.loadingFailed = false;
90+
this.componentToRender = null;
91+
92+
try {
93+
if (!clientComponent) {
94+
throw new Error('Missing client-rendered widget metadata');
95+
}
96+
97+
const pluginModule = await importPluginUmd(
98+
clientComponent.plugin,
99+
) as Record<string, unknown>|undefined;
100+
const component = pluginModule?.[clientComponent.name];
101+
102+
if (!component) {
103+
throw new Error(
104+
`Unknown widget component ${clientComponent.plugin}.${clientComponent.name}`,
105+
);
106+
}
107+
108+
this.componentToRender = markRaw(component as Component);
109+
} catch (e) {
110+
this.loadingFailed = true;
111+
} finally {
112+
this.loading = false;
113+
}
114+
},
115+
},
116+
});
117+
</script>

plugins/CoreHome/vue/src/Widget/Widget.vue

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,15 @@
1414
v-tooltips="{ content: tooltipContent }"
1515
>
1616
<WidgetLoader
17-
v-if="!actualWidget.isContainer && actualWidget.parameters"
17+
v-if="!actualWidget.isContainer && actualWidget.parameters && !actualWidget.clientComponent"
1818
:widget-params="actualWidget.parameters"
1919
:widget-name="actualWidget.name"
2020
/>
21+
<ClientWidgetRenderer
22+
v-if="!actualWidget.isContainer && actualWidget.clientComponent"
23+
:widget="actualWidget"
24+
:widgetized="widgetized"
25+
/>
2126
<div v-if="actualWidget.isContainer
2227
&& actualWidget.layout !== 'ByDimension'
2328
&& !this.preventRecursion"
@@ -39,6 +44,7 @@ import { DeepReadonly, defineComponent } from 'vue';
3944
import WidgetLoader from '../WidgetLoader/WidgetLoader.vue';
4045
import WidgetContainer from '../WidgetContainer/WidgetContainer.vue';
4146
import WidgetByDimensionContainer from '../WidgetByDimensionContainer/WidgetByDimensionContainer.vue';
47+
import ClientWidgetRenderer from './ClientWidgetRenderer.vue';
4248
import WidgetsStoreInstance, { getWidgetChildren } from './Widgets.store';
4349
import {
4450
Widget as WidgetData,
@@ -97,6 +103,7 @@ export default defineComponent({
97103
WidgetLoader,
98104
WidgetContainer,
99105
WidgetByDimensionContainer,
106+
ClientWidgetRenderer,
100107
},
101108
directives: {
102109
Tooltips,

plugins/CoreHome/vue/src/Widget/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,15 @@
88
import { Orderable } from '../Orderable';
99
import { Subcategory } from '../ReportingMenu/Subcategory';
1010

11+
export interface ClientComponent {
12+
plugin: string;
13+
name: string;
14+
props?: Record<string, unknown>;
15+
}
16+
1117
export interface Widget extends Orderable {
1218
uniqueId?: string;
19+
name?: string;
1320
module?: string;
1421
action?: string;
1522
viewDataTable?: string;
@@ -18,6 +25,7 @@ export interface Widget extends Orderable {
1825
isContainer?: boolean;
1926
isReport?: boolean;
2027
middlewareParameters?: Record<string, unknown>;
28+
clientComponent?: ClientComponent;
2129
documentation?: string;
2230
layout?: string;
2331
isWide?: boolean;

plugins/CoreHome/vue/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export { default as ReportingPagesStore } from './ReportingPages/ReportingPages.
7272
export { default as ReportMetadataStore } from './ReportMetadata/ReportMetadata.store';
7373
export { default as WidgetsStore } from './Widget/Widgets.store';
7474
export { default as WidgetLoader } from './WidgetLoader/WidgetLoader.vue';
75+
export { default as ClientWidgetRenderer } from './Widget/ClientWidgetRenderer.vue';
7576
export { default as WidgetContainer } from './WidgetContainer/WidgetContainer.vue';
7677
export {
7778
default as WidgetByDimensionContainer,

plugins/Dashboard/javascripts/widgetMenu.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,42 @@ widgetsHelper.loadWidgetAjax = function (widgetUniqueId, widgetParameters, onWid
174174

175175
widgetParameters['widget'] = 1;
176176

177+
var clientWidgetRequest = {
178+
abort: function () {}
179+
};
180+
181+
var clientWidget = null;
182+
if (widgetsHelper.availableWidgets) {
183+
for (var widgetCategory in widgetsHelper.availableWidgets) {
184+
if (!widgetsHelper.availableWidgets.hasOwnProperty(widgetCategory)) {
185+
continue;
186+
}
187+
188+
var widgets = widgetsHelper.availableWidgets[widgetCategory];
189+
for (var index in widgets) {
190+
if (widgets.hasOwnProperty(index) && widgets[index]["uniqueId"] == widgetUniqueId) {
191+
clientWidget = widgets[index];
192+
break;
193+
}
194+
}
195+
196+
if (clientWidget) {
197+
break;
198+
}
199+
}
200+
}
201+
202+
if (clientWidget && clientWidget.clientComponent) {
203+
clientWidget = $.extend(true, {}, clientWidget);
204+
clientWidget.parameters = $.extend({}, clientWidget.parameters, widgetParameters);
205+
206+
var html = '<div vue-entry="CoreHome.Widget"'
207+
+ ' widget="' + piwikHelper.htmlEntities(JSON.stringify(clientWidget)) + '"'
208+
+ ' widgetized="true"></div>';
209+
onWidgetLoadedCallback(html);
210+
return clientWidgetRequest;
211+
}
212+
177213
var ajaxRequest = new ajaxHelper();
178214
ajaxRequest.addParams(widgetParameters, 'get');
179215
ajaxRequest.setCallback(onWidgetLoadedCallback);

0 commit comments

Comments
 (0)