Chapter 2: Build a dashboard¶
本教程的第一部分向您介绍了大部分Owl的概念。现在是时候全面了解Odoo JavaScript框架了,因为它是Web客户端所使用的。
To get started, you need a running Odoo server and a development environment setup. Before getting
into the exercises, make sure you have followed all the steps described in this
tutorial introduction. For this chapter, we will start
from the empty dashboard provided by the awesome_dashboard
addon. We will progressively add
features to it, using the Odoo JavaScript framework.
目标
The solutions for each exercise of the chapter are hosted on the official Odoo tutorials repository.
1. 新布局¶
Most screens in the Odoo web client uses a common layout: a control panel on top, with some buttons,
and a main content zone just below. This is done using the Layout component, available in @web/search/layout
.
Update the
AwesomeDashboard
component located inawesome_dashboard/static/src/
to use theLayout
component. You can use{controlPanel: {} }
for thedisplay
props of theLayout
component.Add a
className
prop toLayout
:className="'o_dashboard h-100'"
Add a
dashboard.scss
file in which you set the background-color of.o_dashboard
to gray (or your favorite color)
Open http://localhost:8069/web, then open the Awesome Dashboard app, and see the result.
Theory: Services¶
In practice, every component (except the root component) may be destroyed at any time and replaced (or not) with another component. This means that each component internal state is not persistent. This is fine in many cases, but there certainly are situations where we want to keep some data around. For example, all Discuss messages should not be reloaded every time we display a channel.
Also, it may happen that we need to write some code that is not a component. Maybe something that process all barcodes, or that manages the user configuration (context, etc.).
The Odoo framework defines the idea of a service, which is a persistent piece of code that exports state and/or functions. Each service can depend on other services, and components can import a service.
下面的示例注册了一个简单的服务,每5秒显示一次通知:
import { registry } from "@web/core/registry";
const myService = {
dependencies: ["notification"],
start(env, { notification }) {
let counter = 1;
setInterval(() => {
notification.add(`Tick Tock ${counter++}`);
}, 5000);
},
};
registry.category("services").add("myService", myService);
Services can be accessed by any component. Imagine that we have a service to maintain some shared state:
import { registry } from "@web/core/registry";
const sharedStateService = {
start(env) {
let state = {};
return {
getValue(key) {
return state[key];
},
setValue(key, value) {
state[key] = value;
},
};
},
};
registry.category("services").add("shared_state", sharedStateService);
Then, any component can do this:
import { useService } from "@web/core/utils/hooks";
setup() {
this.sharedState = useService("shared_state");
const value = this.sharedState.getValue("somekey");
// do something with value
}
3. Add a dashboard item¶
Let us now improve our content.
Create a generic
DashboardItem
component that display its default slot in a nice card layout. It should take an optionalsize
number props, that default to1
. The width should be hardcoded to(18*size)rem
.Add two cards to the dashboard. One with no size, and the other with a size of 2.
4. Call the server, add some statistics¶
Let’s improve the dashboard by adding a few dashboard items to display real business data.
The awesome_dashboard
addon provides a /awesome_dashboard/statistics
route that is meant
to return some interesting information.
To call a specific controller, we need to use the rpc service.
It only exports a single function that perform the request: rpc(route, params, settings)
.
A basic request could look like this:
setup() {
this.rpc = useService("rpc");
onWillStart(async () => {
const result = await this.rpc("/my/controller", {a: 1, b: 2});
// ...
});
}
Update
Dashboard
so that it uses therpc
service.Call the statistics route
/awesome_dashboard/statistics
in theonWillStart
hook.在仪表板中显示几张包含以下内容的卡片:
本月新订单数量
本月新订单的总金额
本月每个订单的T恤平均数量
本月取消订单数量
从“新建”到“已发送”或“已取消”的订单平均处理时间
5. Cache network calls, create a service¶
If you open the Network tab of your browser’s dev tools, you will see that the call to
/awesome_dashboard/statistics
is done every time the client action is displayed. This is because the
onWillStart
hook is called each time the Dashboard
component is mounted. But in this case, we
would prefer to do it only the first time, so we actually need to maintain some state outside of the
Dashboard
component. This is a nice use case for a service!
Register and import a new
awesome_dashboard.statistics
service.它应该提供一个名为
loadStatistics
的函数,一旦调用,就执行实际的rpc,并始终返回相同的信息。Use the memoize utility function from
@web/core/utils/functions
that allows caching the statistics.在
Dashboard
组件中使用此服务。Check that it works as expected.
6. Display a pie chart¶
每个人都喜欢图表(!),所以让我们在我们的仪表板中添加一个饼图。它将显示每个尺码(S/M/L/XL/XXL)销售的T恤比例。
对于这个练习,我们将使用 Chart.js。它是图表视图使用的图表库。然而,默认情况下它不会被加载,所以我们需要将它添加到我们的资源包中,或者进行懒加载。懒加载通常更好,因为我们的用户如果不需要它,就不必每次都加载 chartjs 代码。
Create a
PieChart
component.In its
onWillStart
method, load chartjs, you can use the loadJs function to load/web/static/lib/Chart/Chart.js
.Use the
PieChart
component in aDashboardItem
to display a pie chart that shows the quantity for each sold t-shirts in each size (that information is available in the/statistics
route). Note that you can use thesize
property to make it look larger.The
PieChart
component will need to render a canvas, and draw on it usingchart.js
.让它工作起来!
另请参阅
7. Real life update¶
Since we moved the data loading in a cache, it never updates. But let us say that we are looking at fast moving data, so we want to periodically (for example, every 10min) reload fresh data.
This is quite simple to implement, with a setTimeout
or setInterval
in the statistics service.
However, here is the tricky part: if the dashboard is currently being displayed, it should be
updated immediately.
To do that, one can use a reactive
object: it is just like the proxy returned by useState
,
but not linked to any component. A component can then do a useState
on it to subscribe to its
changes.
Update the statistics service to reload data every 10 minutes (to test it, use 10s instead!)
Modify it to return a reactive object. Reloading data should update the reactive object in place.
The
Dashboard
component can now use it with auseState
8. Lazy loading the dashboard¶
Let us imagine that our dashboard is getting quite big, and is only of interest to some of our users. In that case, it could make sense to lazy load our dashboard, and all related assets, so we only pay the cost of loading the code when we actually want to look at it.
One way to do this is to use LazyComponent
(from @web/core/assets
) as an intermediate
that will load an asset bundle before displaying our component.
Example
example_action.js
:
export class ExampleComponentLoader extends Component {
static components = { LazyComponent };
static template = xml`
<LazyComponent bundle="'example_module.example_assets'" Component="'ExampleComponent'" />
`;
}
registry.category("actions").add("example_module.example_action", ExampleComponentLoader);
Move all dashboard assets into a sub folder
/dashboard
to make it easier to add to a bundle.Create a
awesome_dashboard.dashboard
assets bundle containing all content of the/dashboard
folder.Modify
dashboard.js
to register itself to thelazy_components
registry instead ofactions
.In
src/dashboard_action.js
, create an intermediate component that usesLazyComponent
and register it to theactions
registry.
9. Making our dashboard generic¶
So far, we have a nice working dashboard. But it is currently hardcoded in the dashboard template. What if we want to customize our dashboard? Maybe some users have different needs and want to see other data.
So, the next step is to make our dashboard generic: instead of hard-coding its content in the template, it can just iterate over a list of dashboard items. But then, many questions come up: how to represent a dashboard item, how to register it, what data should it receive, and so on. There are many different ways to design such a system, with different trade-offs.
For this tutorial, we will say that a dashboard item is an object with the following structure:
const item = {
id: "average_quantity",
description: "Average amount of t-shirt",
Component: StandardItem,
// size and props are optionals
size: 3,
props: (data) => ({
title: "Average amount of t-shirt by order this month",
value: data.average_quantity
}),
};
The description
value will be useful in a later exercise to show the name of items that the
user can add to their dashboard. The size
number is optional, and simply describes
the size of the dashboard item that will be displayed. Finally, the props
function is optional.
If not given, we will simply give the statistics
object as data. But if it is defined, it will
be used to compute specific props for the component.
The goal is to replace the content of the dashboard with the following snippet:
<t t-foreach="items" t-as="item" t-key="item.id">
<DashboardItem size="item.size || 1">
<t t-set="itemProp" t-value="item.props ? item.props(statistics) : {'data': statistics}"/>
<t t-component="item.Component" t-props="itemProp" />
</DashboardItem>
</t>
Note that the above example features two advanced features of Owl: dynamic components and dynamic props.
We currently have two kinds of item components: number cards with a title and a number, and pie cards with some label and a pie chart.
Create and implement two components:
NumberCard
andPieChartCard
, with the corresponding props.Create a file
dashboard_items.js
in which you define and export a list of items, usingNumberCard
andPieChartCard
to recreate our current dashboard.Import that list of items in our
Dashboard
component, add it to the component, and update the template to use at-foreach
like shown above.setup() { this.items = items; }
And now, our dashboard template is generic!
10. Making our dashboard extensible¶
However, the content of our item list is still hardcoded. Let us fix that by using a registry:
Instead of exporting a list, register all dashboard items in a
awesome_dashboard
registryImport all the items of the
awesome_dashboard
registry in theDashboard
component
The dashboard is now easily extensible. Any other Odoo addon that wants to register a new item to the dashboard can just add it to the registry.
11. Add and remove dashboard items¶
Let us see how we can make our dashboard customizable. To make it simple, we will save the user dashboard configuration in the local storage so that it is persistent, but we don’t have to deal with the server for now.
The dashboard configuration will be saved as a list of removed item ids.
Add a button in the control panel with a gear icon to indicate that it is a settings button.
Clicking on that button should open a dialog.
In that dialog, we want to see a list of all existing dashboard items, each with a checkbox.
There should be a
Apply
button in the footer. Clicking on it will build a list of all item ids that are unchecked.We want to store that value in the local storage.
And modify the
Dashboard
component to filter the current items by removing the ids of items from the configuration.
12. Going further¶
以下是一些小的改进建议,如果您有时间可以尝试一下:
确保你的应用程序可以通过
env._t
进行 翻译。Clicking on a section of the pie chart should open a list view of all orders that have the corresponding size.
Save the content of the dashboard in a user setting on the server!
Make it responsive: in mobile mode, each card should take 100% of the width.