带有 Material 的 Angular 仪表板🅰️
使用 Cube.js 的分析后端
前端应用程序
具有多个图表的交互式仪表板
带数据表的多页仪表板
Angular🅰️ 是许多专业开发者的首选 Web 框架。根据2020 年 Stack Overflow 开发者调查,只有约 10% 的开发者更喜欢 React 而不是 Angular。
Material是 Angular 的Material Design组件的参考实现。它提供了许多现成的组件,可以快速轻松地构建 Web 应用程序(包括仪表板)。
在本指南中,我们将学习如何构建包含 KPI、图表和数据表的全栈式仪表板。我们将从数据库中的数据讲解到可交互、可筛选、可搜索的仪表板。
我们将使用 Cube.js 作为我们的分析 API。它省去了构建 API 层、生成 SQL 和查询数据库的所有麻烦。它还提供了许多生产级功能,例如用于优化性能的多级缓存、多租户、安全性等等。
下面您可以看到我们将要构建的应用程序的动画图。此外,您还可以查看GitHub 上的现场演示和完整源代码。
使用 Cube.js 的分析后端
我们将为一家希望追踪其整体业绩和订单状态的电商公司构建仪表板。假设该公司将数据保存在 SQL 数据库中。因此,为了在仪表板上显示这些数据,我们将创建一个分析后端。
首先,我们需要安装 Cube.js 命令行工具(CLI)。为了方便起见,我们先在机器上全局安装它。
$ npm install -g cubejs-cli
然后,安装 CLI 后,我们可以通过运行单个命令来创建一个基本的后端。Cube.js 支持所有流行的数据库,并且后端将预先配置为与特定的数据库类型一起使用:
$ cubejs create <project name> -d <database type>
我们将使用PostgreSQL数据库。请确保您已安装 PostgreSQL。
要创建后端,我们运行以下命令:
$ cubejs create angular-dashboard -d postgres
现在我们可以下载并导入 PostgreSQL 的示例电子商务数据集:
$ curl http://cube.dev/downloads/ecom-dump.sql > ecom-dump.sql
$ createdb ecom
$ psql --dbname ecom -f ecom-dump.sql
数据库准备就绪后,即可配置后端连接到数据库。为此,我们通过.env
Cube.js 项目文件夹根目录中的文件 ( angular-dashboard
) 提供了一些选项:
CUBEJS_DB_NAME=ecom
CUBEJS_DB_TYPE=postgres
CUBEJS_API_SECRET=secret
现在我们可以运行后端了!
在开发模式下,后端还将运行 Cube.js Playground。这是一个节省时间的 Web 应用程序,可帮助您创建数据模式、测试图表等。在 Cube.js 项目文件夹中运行以下命令:
$ node index.js
接下来,在浏览器中打开http://localhost:4000 。
我们将使用 Cube.js Playground 创建数据模式。它本质上是一段 JavaScript 代码,以声明式的方式描述数据,定义度量和维度等分析实体,并将它们映射到 SQL 查询。以下是可用于描述用户数据的模式示例。
cube('Users', {
sql: 'SELECT * FROM users',
measures: {
count: {
sql: `id`,
type: `count`
},
},
dimensions: {
city: {
sql: `city`,
type: `string`
},
signedUp: {
sql: `created_at`,
type: `time`
},
companyName: {
sql: `company_name`,
type: `string`
},
},
});
Cube.js 可以根据数据库表生成简单的数据模式。如果您的数据库中已经有一组重要的表,可以考虑使用数据模式生成功能,因为它可以节省时间。
对于我们的后端,我们选择line_items
、、和表orders
,然后单击“生成架构”。结果,我们将在文件夹中生成 4 个文件 - 每个表一个架构文件。products
users
schema
生成架构后,我们可以通过 Web UI 构建示例图表。具体操作如下:导航至“构建”选项卡,并从架构中选择一些度量和维度。
在“构建”选项卡中,您可以使用不同的可视化库构建示例图表,并检查图表创建的各个方面,从生成的 SQL 一直到用于渲染图表的 JavaScript 代码。您还可以检查发送到 Cube.js 后端的 JSON 编码的 Cube.js 查询。
前端应用程序
从头开始创建复杂的仪表板通常需要时间和精力。幸运的是,Angular 提供了一个工具,只需几个命令即可帮助您创建应用程序样板代码。添加 Material 库和 Cube.js 作为分析 API 也非常容易。
安装库
因此,让我们使用 Angular CLI 并在文件夹内创建前端应用程序angular-dashboard
:
npm install -g @angular/cli # Install Angular CLI
ng new dashboard-app # Create an app
cd dashboard-app # Change the folder
ng serve # Run the app
恭喜!现在我们的项目中有了dashboard-app
文件夹。此文件夹包含我们将要修改和改进以构建分析仪表板的前端代码。
现在是时候添加 Material 库了。要将 Material 库安装到我们的应用程序中,请运行:
ng add @angular/material
选择自定义主题和以下选项:
- 设置全局 Angular Material 排版样式?-是的
- 为 Angular Material 设置浏览器动画?-是的
太棒了!我们还需要一个图表库来将图表添加到仪表板。Chart.js是最流行的图表库,它稳定且功能丰富。所以……
现在该添加 Chart.js 库了。要安装它,请运行:
npm install ng2-charts
npm install chart.js
ng2-charts
另外,为了能够在 Angular 应用中使用 指令,我们需要 import ChartsModule
。为此,我们在app.module.ts
文件中添加以下 import 语句:
+ import { ChartsModule } from 'ng2-charts';
第二步是将其添加 ChartsModule
到装饰器的 imports 数组中 @NgModule
:
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
BrowserAnimationsModule,
+ ChartsModule
],
providers: [],
bootstrap: [AppComponent]
})
最后,是时候添加 Cube.js 了。这是让我们的应用程序通过分析 API 访问数据库中数据的最后一步,即为 Angular 安装 Cube.js 客户端库。运行:
npm install --save @cubejs-client/ngx
npm install --save @cubejs-client/core
现在我们可以添加CubejsClientModule
到您的app.module.ts
文件中:
...
+ import { CubejsClientModule } from '@cubejs-client/ngx';
+ const cubejsOptions = {
+ token: 'YOUR-CUBEJS-API-TOKEN',
+ options: { apiUrl: 'http://localhost:4200/cubejs-api/v1' }
+ };
@NgModule({
...
imports: [
...
+ CubejsClientModule.forRoot(cubejsOptions)
],
...
})
export class AppModule { }
CubejsClientModule
提供 CubejsClient
您可以注入到组件或服务中以进行 API 调用和检索数据的功能:
import { CubejsClient } from '@cubejs-client/ngx';
export class AppComponent {
constructor(private cubejs:CubejsClient){}
ngOnInit(){
this.cubejs.load({
measures: ["some_measure"]
}).subscribe(
resultSet => {
this.data = resultSet.chartPivot();
},
err => console.log('HTTP Error', err)
);
}
}
一切顺利!让我们开始吧。
创建第一个图表
让我们bar-chart
使用 Angular CLI 创建一个通用组件。运行:
$ ng g c bar-chart # Oh these single-letter commands!
此命令将向我们的应用程序添加四个新文件,因为这是 Angular 用于其组件的文件:
src/app/bar-chart/bar-chart.component.html
src/app/bar-chart/bar-chart.component.ts
src/app/bar-chart/bar-chart.component.scss
src/app/bar-chart/bar-chart.component.spec.ts
打开 bar-chart.component.html
该文件并将其内容替换为以下代码:
<div>
<div style="display: block">
<canvas baseChart
height="320"
[datasets]="barChartData"
[labels]="barChartLabels"
[options]="barChartOptions"
[legend]="barChartLegend"
[chartType]="barChartType">
</canvas>
</div>
</div>
这里我们使用了 baseChart
添加到 canvas 元素的 指令。此外,datasets
、 labels
、 options
、 legend
和 chartType
属性绑定到了类成员,这些成员被添加到BarChartComponent
类的实现中 bar-chart-component.ts
:
import { Component, OnInit, Input } from "@angular/core";
import { CubejsClient } from '@cubejs-client/ngx';
import {formatDate, registerLocaleData} from "@angular/common"
import localeEn from '@angular/common/locales/en';
registerLocaleData(localeEn);
@Component({
selector: "app-bar-chart",
templateUrl: "./bar-chart.component.html",
styleUrls: ["./bar-chart.component.scss"]
})
export class BarChartComponent implements OnInit {
@Input() query: Object;
constructor(private cubejs:CubejsClient){}
public barChartOptions = {
responsive: true,
maintainAspectRatio: false,
legend: { display: false },
cornerRadius: 50,
tooltips: {
enabled: true,
mode: 'index',
intersect: false,
borderWidth: 1,
borderColor: "#eeeeee",
backgroundColor: "#ffffff",
titleFontColor: "#43436B",
bodyFontColor: "#A1A1B5",
footerFontColor: "#A1A1B5",
},
layout: { padding: 0 },
scales: {
xAxes: [
{
barThickness: 12,
maxBarThickness: 10,
barPercentage: 0.5,
categoryPercentage: 0.5,
ticks: {
fontColor: "#A1A1B5",
},
gridLines: {
display: false,
drawBorder: false,
},
},
],
yAxes: [
{
ticks: {
fontColor: "#A1A1B5",
beginAtZero: true,
min: 0,
},
gridLines: {
borderDash: [2],
borderDashOffset: [2],
color: "#eeeeee",
drawBorder: false,
zeroLineBorderDash: [2],
zeroLineBorderDashOffset: [2],
zeroLineColor: "#eeeeee",
},
},
],
},
};
public barChartLabels = [];
public barChartType = "bar";
public barChartLegend = true;
public barChartData = [];
ngOnInit() {
this.cubejs.load(this.query).subscribe(
resultSet => {
const COLORS_SERIES = ['#FF6492', '#F3F3FB', '#FFA2BE'];
this.barChartLabels = resultSet.chartPivot().map((c) => formatDate(c.category, 'longDate', 'en'));
this.barChartData = resultSet.series().map((s, index) => ({
label: s.title,
data: s.series.map((r) => r.value),
backgroundColor: COLORS_SERIES[index],
fill: false,
}));
},
err => console.log('HTTP Error', err)
);
}
}
好的,我们已经有了图表的代码,让我们在应用中展示它。我们可以使用 Angular 命令来生成基础网格。运行:
ng generate @angular/material:dashboard dashboard-page
现在我们有了一个包含dashboard-page
组件的文件夹。打开app.component.html
并插入以下代码:
<app-dashboard-page></app-dashboard-page>
现在是时候打开dashboard-page/dashobard-page.component.html
并添加我们的组件了,如下所示:
<div class="grid-container">
<h1 class="mat-h1">Dashboard</h1>
+ <mat-grid-list cols="2" rowHeight="450px">
- <mat-grid-tile *ngFor="let card of cards | async" [colspan]="card.cols" [rowspan]="card.rows">
+ <mat-grid-tile *ngFor="let card of cards" [colspan]="card.cols" [rowspan]="card.rows">
<mat-card class="dashboard-card">
<mat-card-header>
<mat-card-title>
<button mat-icon-button class="more-button" [matMenuTriggerFor]="menu" aria-label="Toggle menu">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menu="matMenu" xPosition="before">
<button mat-menu-item>Expand</button>
<button mat-menu-item>Remove</button>
</mat-menu>
</mat-card-title>
</mat-card-header>
<mat-card-content class="dashboard-card-content">
<div>
+ <app-bar-chart [query]="card.query" *ngIf="card.chart === 'bar'"></app-bar-chart>
</div>
</mat-card-content>
</mat-card>
</mat-grid-tile>
</mat-grid-list>
</div>
最后一次编辑如下dashboard-page.component.ts
:
import { Component, OnInit } from "@angular/core";
import { BehaviorSubject } from "rxjs";
@Component({
selector: "app-dashboard-page",
templateUrl: "./dashboard-page.component.html",
styleUrls: ["./dashboard-page.component.scss"]
})
export class DashboardPageComponent implements OnInit {
private query = new BehaviorSubject({
measures: ["Orders.count"],
timeDimensions: [{ dimension: "Orders.createdAt", granularity: "month", dateRange: "This year" }],
dimensions: ["Orders.status"],
filters: [{ dimension: "Orders.status", operator: "notEquals", values: ["completed"] }]
});
cards = [];
ngOnInit() {
this.query.subscribe(data => {
this.cards[0] = {
chart: "bar", cols: 2, rows: 1,
query: data
};
});
}
}
干得好!🎉 这就是我们所需的全部内容,我们可以通过 Cube.js 从 Postgres 加载数据,显示第一个图表。
在下一部分中,我们将让用户将日期范围从“今年”更改为其他预定义值,从而使此图表具有交互性。
具有多个图表的交互式仪表板
在上一部分中,我们创建了一个分析后端和一个包含第一个图表的基本仪表板。现在,我们将扩展仪表板,使其能够显示我们电商公司的关键绩效指标 (KPI)。
自定义日期范围
作为第一步,我们将允许用户更改现有图表的日期范围。
为此,我们需要对dashboard-page.component.ts
文件进行更改:
// ...
export class DashboardPageComponent implements OnInit {
private query = new BehaviorSubject({
measures: ["Orders.count"],
timeDimensions: [{ dimension: "Orders.createdAt", granularity: "month", dateRange: "This year" }],
dimensions: ["Orders.status"],
filters: [{ dimension: "Orders.status", operator: "notEquals", values: ["completed"] }]
});
+ changeDateRange = (value) => {
+ this.query.next({
+ ...this.query.value,
+ timeDimensions: [{ dimension: "Orders.createdAt", granularity: "month", dateRange: value }]
+ });
+ };
cards = [];
ngOnInit() {
this.query.subscribe(data => {
this.cards[0] = {
chart: "bar", cols: 2, rows: 1,
query: data
};
});
}
}
文件中还有另一个dashobard-page.component.html
:
<div class="grid-container">
<h1 class="mat-h1">Dashboard</h1>
<mat-grid-list cols="3" rowHeight="450px">
<mat-grid-tile *ngFor="let card of cards" [colspan]="card.cols" [rowspan]="card.rows">
<mat-card class="dashboard-card">
<mat-card-header>
<mat-card-title>
<button mat-icon-button class="more-button" [matMenuTriggerFor]="menu" aria-label="Toggle menu">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menu="matMenu" xPosition="before">
+ <button mat-menu-item (click)="changeDateRange('This year')">This year</button>
+ <button mat-menu-item (click)="changeDateRange('Last year')">Last year</button>
</mat-menu>
</mat-card-title>
</mat-card-header>
<mat-card-content class="dashboard-card-content">
<div>
<app-bar-chart [query]="card.query" *ngIf="card.chart === 'bar'"></app-bar-chart>
</div>
</mat-card-content>
</mat-card>
</mat-grid-tile>
</mat-grid-list>
</div>
做得好!🎉 我们的仪表板应用程序如下所示:
关键绩效指标图表
KPI 图表可用于显示业务指标,提供有关我们电商公司当前绩效的信息。该图表由网格图块组成,每个图块将显示特定类别的单个 KPI 数字值。
首先,让我们添加countUp
包,以便将计数动画添加到 KPI 图表上的值中。在 dashboard-app 文件夹中运行以下命令:
npm i ngx-countup @angular/material/progress-bar
我们需要导入这些模块:
+ import { CountUpModule } from 'ngx-countup';
+ import { MatProgressBarModule } from '@angular/material/progress-bar'
@NgModule({
imports: [
// ...
+ CountUpModule,
+ MatProgressBarModule
],
...
})
其次,让我们向文件中添加要显示的卡片数组dashboard-page.component.ts
:
export class DashboardPageComponent implements OnInit {
// ...
+ public KPICards = [
+ {
+ title: 'ORDERS',
+ query: { measures: ['Orders.count'] },
+ difference: 'Orders',
+ duration: 1.25,
+ },
+ {
+ title: 'TOTAL USERS',
+ query: { measures: ['Users.count'] },
+ difference: 'Users',
+ duration: 1.5,
+ },
+ {
+ title: 'COMPLETED ORDERS',
+ query: { measures: ['Orders.percentOfCompletedOrders'] },
+ progress: true,
+ duration: 1.75,
+ },
+ {
+ title: 'TOTAL PROFIT',
+ query: { measures: ['LineItems.price'] },
+ duration: 2.25,
+ },
+ ];
// ...
}
下一步是创建 KPI 卡组件。运行:
ng generate component kpi-card
编辑该组件的代码:
import { Component, Input, OnInit } from "@angular/core";
import { CubejsClient } from "@cubejs-client/ngx";
@Component({
selector: 'app-kpi-card',
templateUrl: './kpi-card.component.html',
styleUrls: ['./kpi-card.component.scss']
})
export class KpiCardComponent implements OnInit {
@Input() query: object;
@Input() title: string;
@Input() duration: number;
@Input() progress: boolean;
constructor(private cubejs:CubejsClient){}
public result = 0;
public postfix = null;
public prefix = null;
ngOnInit(): void {
this.cubejs.load(this.query).subscribe(
resultSet => {
resultSet.series().map((s) => {
this.result = s['series'][0]['value'].toFixed(1);
const measureKey = resultSet.seriesNames()[0].key;
const annotations = resultSet.tableColumns().find((tableColumn) => tableColumn.key === measureKey);
const format = annotations.format || (annotations.meta && annotations.meta.format);
if (format === 'percent') {
this.postfix = '%';
} else if (format === 'currency') {
this.prefix = '$';
}
})
},
err => console.log('HTTP Error', err)
);
}
}
以及组件的模板:
<mat-card class="dashboard-card">
<mat-card-header class="dashboard-card__header">
<mat-card-title>
<h3 class="kpi-title">{{title}}</h3>
</mat-card-title>
</mat-card-header>
<mat-card-content class="dashboard-card-content kpi-result">
<span>{{prefix}}</span>
<span [countUp]="result" [options]="{duration: duration}">0</span>
<span>{{postfix}}</span>
<mat-progress-bar [color]="'primary'" class="kpi-progress" *ngIf="progress" value="{{result}}"></mat-progress-bar>
</mat-card-content>
</mat-card>
最后一步是将此组件添加到我们的仪表板页面。为此,请打开dashboard-page.component.html
并替换代码:
<div class="grid-container">
<div class="kpi-wrap">
<mat-grid-list cols="4" rowHeight="131px">
<mat-grid-tile *ngFor="let card of KPICards" [colspan]="1" [rowspan]="1">
<app-kpi-card class="kpi-card"
[query]="card.query"
[title]="card.title"
[duration]="card.duration"
[progress]="card.progress"
></app-kpi-card>
</mat-grid-tile>
</mat-grid-list>
</div>
<div>
<mat-grid-list cols="5" rowHeight="510px">
<mat-grid-tile *ngFor="let card of cards" [colspan]="card.cols" [rowspan]="card.rows">
<mat-card class="dashboard-card">
<mat-card-header class="dashboard-card__header">
<mat-card-title>
<h3>Last sales</h3>
<button mat-icon-button class="more-button" [matMenuTriggerFor]="menu" aria-label="Toggle menu">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menu="matMenu" xPosition="before">
<button mat-menu-item (click)="changeDateRange('This year')">This year</button>
<button mat-menu-item (click)="changeDateRange('Last year')">Last year</button>
</mat-menu>
</mat-card-title>
</mat-card-header>
<mat-card-content class="dashboard-card-content">
<div>
<app-bar-chart [query]="card.query" *ngIf="card.chart === 'bar'"></app-bar-chart>
</div>
</mat-card-content>
</mat-card>
</mat-grid-tile>
</mat-grid-list>
</div>
</div>
剩下的就是调整 Cube.js 的 schema。在调整过程中,我们将学习 Cube.js 的一个重要方面……
让我们学习如何在数据架构中创建自定义度量并显示其值。在电子商务业务中,了解已完成订单的份额至关重要。为了让用户能够监控此指标,我们希望将其显示在 KPI 图表上。因此,我们将修改数据架构,添加一个自定义度量( percentOfCompletedOrders
),该度量将根据另一个度量 ( ) 计算份额completedCount
。
让我们自定义“Orders”模式。打开schema/Orders.js
Cube.js项目根文件夹中的文件并进行以下更改:
- 添加
completedCount
度量 - 添加
percentOfCompletedOrders
度量
cube(`Orders`, {
sql: `SELECT * FROM public.orders`,
// ...
measures: {
count: {
type: `count`,
drillMembers: [id, createdAt]
},
number: {
sql: `number`,
type: `sum`
},
+ completedCount: {
+ sql: `id`,
+ type: `count`,
+ filters: [
+ { sql: `${CUBE}.status = 'completed'` }
+ ]
+ },
+ percentOfCompletedOrders: {
+ sql: `${completedCount} * 100.0 / ${count}`,
+ type: `number`,
+ format: `percent`
+ }
},
// ...
太棒了!🎉现在我们的仪表盘上有一排美观且信息丰富的 KPI 指标:
甜甜圈图
现在,使用 KPI 图表,我们的用户能够监控已完成订单的占比。此外,订单还分为两种:“已处理”订单(已确认但尚未发货)和“已发货”订单(实质上是指已接收发货但尚未完成的订单)。
为了让用户能够监控所有这些类型的订单,我们需要在仪表盘中添加一个图表。最好使用圆环图,因为它非常有助于直观地展示某个指标在多个状态(例如,所有类型的订单)之间的分布情况。
首先,让我们创建DoughnutChart
组件。运行:
ng generate component doughnut-chart
然后编辑doughnut-chart.component.ts
文件:
import { Component, Input, OnInit } from "@angular/core";
import { CubejsClient } from "@cubejs-client/ngx";
@Component({
selector: "app-doughnut-chart",
templateUrl: "./doughnut-chart.component.html",
styleUrls: ["./doughnut-chart.component.scss"]
})
export class DoughnutChartComponent implements OnInit {
@Input() query: Object;
public barChartOptions = {
legend: {
display: false
},
responsive: true,
maintainAspectRatio: false,
cutoutPercentage: 80,
layout: { padding: 0 },
tooltips: {
enabled: true,
mode: "index",
intersect: false,
borderWidth: 1,
borderColor: "#eeeeee",
backgroundColor: "#ffffff",
titleFontColor: "#43436B",
bodyFontColor: "#A1A1B5",
footerFontColor: "#A1A1B5"
}
};
public barChartLabels = [];
public barChartType = "doughnut";
public barChartLegend = true;
public barChartData = [];
public value = 0;
public labels = [];
constructor(private cubejs: CubejsClient) {
}
ngOnInit() {
this.cubejs.load(this.query).subscribe(
resultSet => {
const COLORS_SERIES = ["#FF6492", "#F3F3FB", "#FFA2BE"];
this.barChartLabels = resultSet.chartPivot().map((c) => c.category);
this.barChartData = resultSet.series().map((s) => ({
label: s.title,
data: s.series.map((r) => r.value),
backgroundColor: COLORS_SERIES,
hoverBackgroundColor: COLORS_SERIES
}));
resultSet.series().map(s => {
this.labels = s.series;
this.value = s.series.reduce((sum, current) => {
return sum.value ? sum.value + current.value : sum + current.value
});
});
},
err => console.log("HTTP Error", err)
);
}
}
文件中的模板如下doughnut-chart.component.html
:
<div>
<canvas baseChart
height="215"
[datasets]="barChartData"
[labels]="barChartLabels"
[options]="barChartOptions"
[legend]="barChartLegend"
[chartType]="barChartType">
</canvas>
<mat-grid-list cols="3">
<mat-grid-tile *ngFor="let card of labels" [colspan]="1" [rowspan]="1">
<div>
<h3 class="doughnut-label">{{card.category}}</h3>
<h2 class="doughnut-number">{{((card.value/value) * 100).toFixed(1)}}%</h2>
</div>
</mat-grid-tile>
</mat-grid-list>
</div>
下一步是将此卡片添加到dashboard-page.component.ts
文件中:
export class DashboardPageComponent implements OnInit {
// ...
+ private doughnutQuery = new BehaviorSubject({
+ measures: ['Orders.count'],
+ timeDimensions: [
+ {
+ dimension: 'Orders.createdAt',
+ },
+ ],
+ filters: [],
+ dimensions: ['Orders.status'],
+ });
ngOnInit() {
...
+ this.doughnutQuery.subscribe(data => {
+ this.cards[1] = {
+ hasDatePick: false,
+ title: 'Users by Device',
+ chart: "doughnut", cols: 2, rows: 1,
+ query: data
+ };
+ });
}
}
最后一步是在dashboard-page.component.html
文件中使用此模板:
<div class="grid-container">
// ...
<mat-grid-list cols="5" rowHeight="510px">
<mat-grid-tile *ngFor="let card of cards" [colspan]="card.cols" [rowspan]="card.rows">
<mat-card class="dashboard-card">
<mat-card-header class="dashboard-card__header">
<mat-card-title>
<h3>{{card.title}}</h3>
+ <div *ngIf="card.hasDatePick">
<button mat-icon-button class="more-button" [matMenuTriggerFor]="menu" aria-label="Toggle menu">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menu="matMenu" xPosition="before">
<button mat-menu-item (click)="changeDateRange('This year')">This year</button>
<button mat-menu-item (click)="changeDateRange('Last year')">Last year</button>
</mat-menu>
+ </div>
</mat-card-title>
</mat-card-header>
<mat-card-content class="dashboard-card-content">
<div>
<app-bar-chart [query]="card.query" *ngIf="card.chart === 'bar'"></app-bar-chart>
+ <app-doughnut-chart [query]="card.query" *ngIf="card.chart === 'doughnut'"></app-doughnut-chart>
</div>
</mat-card-content>
</mat-card>
</mat-grid-tile>
</mat-grid-list>
</div>
</div>
太棒了!🎉 现在我们的仪表盘第一页已经完成了:
带数据表的多页仪表板
现在,我们有一个单页仪表盘,可以显示汇总的业务指标,并提供多个 KPI 的概览。但是,我们无法获取特定订单或一系列订单的信息。
我们将通过在仪表板上添加第二个页面来解决这个问题,该页面包含所有订单的信息。但是,我们需要一种在两个页面之间导航的方法。因此,让我们添加一个导航侧栏。
导航侧栏
现在我们需要一个路由器,所以让我们为其添加一个模块。运行:
ng generate module app-routing --flat --module=app
然后编辑app-routing.module.ts
文件:
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DashboardPageComponent } from './dashboard-page/dashboard-page.component';
import { TablePageComponent } from './table-page/table-page.component';
const routes: Routes = [
{ path: '', component: DashboardPageComponent },
{ path: 'table', component: TablePageComponent },
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
现在我们需要向文件中添加新模块app.module.ts
:
// ...
import { CountUpModule } from 'ngx-countup';
import { DoughnutChartComponent } from './doughnut-chart/doughnut-chart.component';
+ import { AppRoutingModule } from './app-routing.module';
+ import { MatListModule } from '@angular/material/list';
// ...
CountUpModule,
MatProgressBarModule,
+ AppRoutingModule,
+ MatListModule,
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
最后一步是将app.component.html
文件设置为此代码:
<style>
* {
box-sizing: border-box;
}
.toolbar {
position: relative;
top: 0;
left: 0;
right: 0;
height: 60px;
display: flex;
align-items: center;
background-color: #43436B;
color: #D5D5E2;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 26px;
letter-spacing: 0.02em;
text-align: left;
padding: 0 1rem;
}
.spacer {
flex: 1;
}
.toolbar img {
margin: 0 16px;
}
.root {
width: 100%;
display: flex;
position: relative;
}
.component {
width: 82.2%;
min-height: 100vh;
padding-top: 1rem;
background: #F3F3FB;
}
.divider {
width: 17.8%;
background: #fff;
padding: 1rem;
}
.nav-link {
text-decoration: none;
color: #A1A1B5;
}
.nav-link:hover .mat-list-item {
background-color: rgba(67, 67, 107, 0.04);
}
.nav-link .mat-list-item {
color: #A1A1B5;
}
.nav-link.active-link .mat-list-item {
color: #7A77FF;
}
</style>
<!-- Toolbar -->
<div class="toolbar" role="banner">
<span>Angular Dashboard with Material</span>
<div class="spacer"></div>
<div class="links">
<a
aria-label="Cube.js on github"
target="_blank"
rel="noopener"
href="https://github.com/cube-js/cube.js/tree/master/examples/angular-dashboard-with-material-ui"
title="Cube.js on GitHub"
>GitHub</a>
<a
aria-label="Cube.js on Slack"
target="_blank"
rel="noopener"
href="https://slack.cube.dev/"
title="Cube.js on Slack"
>Slack</a>
</div>
</div>
<div class="root">
<div class="divider">
<mat-list>
<a class="nav-link"
routerLinkActive="active-link"
[routerLinkActiveOptions]="{exact: true}"
*ngFor="let link of links" [routerLink]="[link.href]"
>
<mat-list-item>
<mat-icon mat-list-icon>{{link.icon}}</mat-icon>
<div mat-line>{{link.name}}</div>
</mat-list-item>
</a>
</mat-list>
</div>
<div class="component">
<router-outlet class="content"></router-outlet>
</div>
</div>
为了使一切最终正常工作,让我们将链接添加到我们的app.component.ts
:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
+ public links = [
+ {name: 'Dashboard', href: '/', icon: 'dashboard'},
+ {name: 'Orders', href: '/table', icon: 'assignment'}
+ ];
title = 'dashboard-app';
}
哇!🎉 这是我们的导航侧栏,可用于在仪表板的不同页面之间切换:
订单数据表
为了获取数据表的数据,我们需要自定义数据模式并定义一些新指标:订单中的商品数量(其大小)、订单的价格和用户的全名。
首先,让我们在文件中的“用户”模式中添加全名schema/Users.js
:
cube(`Users`, {
sql: `SELECT * FROM public.users`,
// ...
dimensions: {
// ...
firstName: {
sql: `first_name`,
type: `string`
},
lastName: {
sql: `last_name`,
type: `string`
},
+ fullName: {
+ sql: `CONCAT(${firstName}, ' ', ${lastName})`,
+ type: `string`
+ },
age: {
sql: `age`,
type: `number`
},
createdAt: {
sql: `created_at`,
type: `time`
}
}
});
然后,让我们向文件中的“订单”模式添加其他措施schema/Orders.js
。
对于这些度量,我们将使用Cube.js 的子查询功能。您可以使用子查询维度来引用维度内其他多维数据集的度量。定义此类维度的方法如下:
cube(`Orders`, {
sql: `SELECT * FROM public.orders`,
dimensions: {
id: {
sql: `id`,
type: `number`,
primaryKey: true,
+ shown: true
},
status: {
sql: `status`,
type: `string`
},
createdAt: {
sql: `created_at`,
type: `time`
},
completedAt: {
sql: `completed_at`,
type: `time`
},
+ size: {
+ sql: `${LineItems.count}`,
+ subQuery: true,
+ type: 'number'
+ },
+
+ price: {
+ sql: `${LineItems.price}`,
+ subQuery: true,
+ type: 'number'
+ }
}
});
现在我们准备添加一个新页面。让我们创建table-page
组件。运行:
ng generate component table-page
编辑table-page.module.ts
文件:
import { Component, OnInit } from '@angular/core';
import { BehaviorSubject } from "rxjs";
@Component({
selector: 'app-table-page',
templateUrl: './table-page.component.html',
styleUrls: ['./table-page.component.scss']
})
export class TablePageComponent implements OnInit {
public _query = new BehaviorSubject({
"limit": 500,
"timeDimensions": [
{
"dimension": "Orders.createdAt",
"granularity": "day"
}
],
"dimensions": [
"Users.id",
"Orders.id",
"Orders.size",
"Users.fullName",
"Users.city",
"Orders.price",
"Orders.status",
"Orders.createdAt"
]
});
public query = {};
constructor() { }
ngOnInit(): void {
this._query.subscribe(query => {
this.query = query;
});
}
}
并将模板设置为以下内容:
<div class="table-warp">
<app-material-table [query]="query"></app-material-table>
</div>
请注意,此组件包含一个 Cube.js 查询。稍后我们将修改此查询以启用数据过滤功能。
另外,让我们创建material-table
组件。运行:
ng generate component material-table
将其添加到app.module.ts
文件中:
+ import { MatTableModule } from '@angular/material/table'
imports: [
// ...
+ MatTableModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
并编辑material-table.module.ts
文件:
import { Component, OnInit, Input } from "@angular/core";
import { CubejsClient } from "@cubejs-client/ngx";
@Component({
selector: "app-material-table",
templateUrl: "./material-table.component.html",
styleUrls: ["./material-table.component.scss"]
})
export class MaterialTableComponent implements OnInit {
@Input() query: object;
constructor(private cubejs: CubejsClient) {
}
public dataSource = [];
displayedColumns = ['id', 'size', 'name', 'city', 'price', 'status', 'date'];
ngOnInit(): void {
this.cubejs.load(this.query).subscribe(
resultSet => {
this.dataSource = resultSet.tablePivot();
},
err => console.log("HTTP Error", err)
);
}
}
然后将其模板设置为这些内容:
<table style="width: 100%; box-shadow: none"
mat-table
matSort
[dataSource]="dataSource"
class="table mat-elevation-z8"
>
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Order ID</th>
<td mat-cell *matCellDef="let element"> {{element['Orders.id']}} </td>
</ng-container>
<ng-container matColumnDef="size">
<th mat-header-cell *matHeaderCellDef> Orders size</th>
<td mat-cell *matCellDef="let element"> {{element['Orders.size']}} </td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> Full Name</th>
<td mat-cell *matCellDef="let element"> {{element['Users.fullName']}} </td>
</ng-container>
<ng-container matColumnDef="city">
<th mat-header-cell *matHeaderCellDef> User city</th>
<td mat-cell *matCellDef="let element"> {{element['Users.city']}} </td>
</ng-container>
<ng-container matColumnDef="price">
<th mat-header-cell *matHeaderCellDef> Order price</th>
<td mat-cell *matCellDef="let element"> {{element['Orders.price']}} </td>
</ng-container>
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef> Status</th>
<td mat-cell *matCellDef="let element"> {{element['Orders.status']}} </td>
</ng-container>
<ng-container matColumnDef="date">
<th mat-header-cell *matHeaderCellDef> Created at</th>
<td mat-cell *matCellDef="let element"> {{element['Orders.createdAt'] | date}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
<!--<mat-paginator [pageSizeOptions]="[5, 10, 25, 100]"></mat-paginator>-->
</table>
是时候添加分页了!
再次,让我们添加模块到app.module.ts
:
+ import {MatPaginatorModule} from "@angular/material/paginator";
+ import {MatProgressSpinnerModule} from "@angular/material/progress-spinner";
@NgModule({
...
imports: [
+ MatPaginatorModule,
+ MatProgressSpinnerModule
],
...
})
export class AppModule { }
然后,我们来编辑模板:
+ <div class="example-loading-shade"
+ *ngIf="loading">
+ <mat-spinner></mat-spinner>
+ </div>
+ <div class="example-table-container">
<table style="width: 100%; box-shadow: none"
mat-table
matSort
[dataSource]="dataSource"
class="table mat-elevation-z8"
>
// ...
</table>
+ </div>
+ <mat-paginator [length]="length"
+ [pageSize]="pageSize"
+ [pageSizeOptions]="pageSizeOptions"
+ (page)="pageEvent.emit($event)"
+ ></mat-paginator>
风格...
/* Structure */
.example-container {
position: relative;
min-height: 200px;
}
.example-table-container {
position: relative;
max-height: 75vh;
overflow: auto;
}
table {
width: 100%;
}
.example-loading-shade {
position: absolute;
top: 0;
left: 0;
bottom: 56px;
right: 0;
background: rgba(0, 0, 0, 0.15);
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
}
.example-rate-limit-reached {
color: #980000;
max-width: 360px;
text-align: center;
}
/* Column Widths */
.mat-column-number,
.mat-column-state {
max-width: 64px;
}
.mat-column-created {
max-width: 124px;
}
.table th {
background: #F8F8FC;
color: #43436B;
font-weight: 500;
line-height: 1.5rem;
border-bottom: 1px solid #eeeeee;
&:hover {
color: #7A77FF;
cursor: pointer;
}
}
.table thead {
background: #F8F8FC;
}
以及组件:
import { Component, Input, Output } from "@angular/core";
import { CubejsClient } from "@cubejs-client/ngx";
import { EventEmitter } from '@angular/core';
@Component({
selector: "app-material-table",
templateUrl: "./material-table.component.html",
styleUrls: ["./material-table.component.scss"]
})
export class MaterialTableComponent {
constructor(private cubejs: CubejsClient) {}
@Input() set query(query: object) {
this.loading = true;
this.cubejs.load(query).subscribe(
resultSet => {
this.dataSource = resultSet.tablePivot();
this.loading = false;
},
err => console.log("HTTP Error", err)
);
this.cubejs.load({...query, limit: 50000, offset: 0}).subscribe(
resultSet => {
this.length = resultSet.tablePivot().length;
},
err => console.log("HTTP Error", err)
);
};
@Input() limit: number;
@Output() pageEvent = new EventEmitter();
loading = true;
length = 0;
pageSize = 10;
pageSizeOptions: number[] = [5, 10, 25, 100];
dataSource = [];
displayedColumns = ['id', 'size', 'name', 'city', 'price', 'status', 'date'];
}
最后的编辑将针对table-page-component.ts
文件:
import { Component, OnInit } from '@angular/core';
import { BehaviorSubject } from "rxjs";
@Component({
selector: 'app-table-page',
templateUrl: './table-page.component.html',
styleUrls: ['./table-page.component.scss']
})
export class TablePageComponent implements OnInit {
public limit = 50;
public page = 0;
public _query = new BehaviorSubject({
"limit": this.limit,
"offset": this.page * this.limit,
"timeDimensions": [
{
"dimension": "Orders.createdAt",
"granularity": "day"
}
],
"dimensions": [
"Users.id",
"Orders.id",
"Orders.size",
"Users.fullName",
"Users.city",
"Orders.price",
"Orders.status",
"Orders.createdAt"
],
filters: []
});
public query = null;
public changePage = (obj) => {
this._query.next({
...this._query.value,
"limit": obj.pageSize,
"offset": obj.pageIndex * obj.pageSize,
});
};
public statusChanged(value) {
this._query.next({...this._query.value,
"filters": this.getFilters(value)});
};
private getFilters = (value) => {
return [
{
"dimension": "Orders.status",
"operator": value === 'all' ? "set" : "equals",
"values": [
value
]
}
]
};
constructor() { }
ngOnInit(): void {
this._query.subscribe(query => {
this.query = query;
});
}
}
以及相关模板:
<div class="table-warp">
<app-material-table [query]="query" [limit]="limit" (pageEvent)="changePage($event)"></app-material-table>
</div>
瞧!🎉现在我们有了一个显示所有订单信息的表格:
然而,仅使用提供的控件很难探索这些订单。为了解决这个问题,我们将添加一个带有过滤器的综合工具栏,并使表格具有交互性。
为此,让我们创建table-filters
组件。运行:
ng generate component table-filters
设置模块内容:
import { Component, EventEmitter, OnInit, Output } from "@angular/core";
@Component({
selector: 'app-table-filters',
templateUrl: './table-filters.component.html',
styleUrls: ['./table-filters.component.scss']
})
export class TableFiltersComponent implements OnInit {
@Output() statusChanged = new EventEmitter();
statusChangedFunc = (obj) => {
this.statusChanged.emit(obj.value);
};
constructor() { }
ngOnInit(): void {
}
}
还有模板...
<mat-button-toggle-group class="table-filters"
(change)="statusChangedFunc($event)">
<mat-button-toggle value="all">All</mat-button-toggle>
<mat-button-toggle value="shipped">Shipped</mat-button-toggle>
<mat-button-toggle value="processing">Processing</mat-button-toggle>
<mat-button-toggle value="completed">Completed</mat-button-toggle>
</mat-button-toggle-group>
具有各种风格...
.table-filters {
margin-bottom: 2rem;
.mat-button-toggle-appearance-standard {
background: transparent;
color: #43436b;
}
}
.mat-button-toggle-standalone.mat-button-toggle-appearance-standard, .mat-button-toggle-group-appearance-standard.table-filters {
border: none;
-webkit-border-radius: 0;
-moz-border-radius: 0;
border-radius: 0;
border-bottom: 1px solid #7A77FF;
}
.mat-button-toggle-checked {
border-bottom: 2px solid #7A77FF;
}
.mat-button-toggle-group-appearance-standard .mat-button-toggle + .mat-button-toggle {
border-left: none;
}
最后一步是将其添加到table-page.component.html
文件中:
<div class="table-warp">
+ <app-table-filters (statusChanged)="statusChanged($event)"></app-table-filters>
<app-material-table [query]="query" [limit]="limit" (pageEvent)="changePage($event)"></app-material-table>
</div>
完美!🎉 现在数据表新增了一个过滤器,可以在不同类型的订单之间切换:
但是,订单还有其他参数,例如价格和日期。让我们为这些参数创建过滤器,并在表格中启用排序。
编辑table-filters
组件:
import { Component, EventEmitter, OnInit, Output } from "@angular/core";
@Component({
selector: 'app-table-filters',
templateUrl: './table-filters.component.html',
styleUrls: ['./table-filters.component.scss']
})
export class TableFiltersComponent implements OnInit {
@Output() statusChanged = new EventEmitter();
@Output() dateChange = new EventEmitter();
@Output() sliderChanged = new EventEmitter();
statusChangedFunc = (obj) => {
this.statusChanged.emit(obj.value);
};
changeDate(number, date) {
this.dateChange.emit({number, date});
};
formatLabel(value: number) {
if (value >= 1000) {
return Math.round(value / 1000) + 'k';
}
return value;
}
sliderChange(value) {
this.sliderChanged.emit(value);
}
constructor() { }
ngOnInit(): void {
}
}
及其模板:
<mat-grid-list cols="4" rowHeight="131px">
<mat-grid-tile>
<mat-button-toggle-group class="table-filters"
(change)="statusChangedFunc($event)">
<mat-button-toggle value="all">All</mat-button-toggle>
<mat-button-toggle value="shipped">Shipped</mat-button-toggle>
<mat-button-toggle value="processing">Processing</mat-button-toggle>
<mat-button-toggle value="completed">Completed</mat-button-toggle>
</mat-button-toggle-group>
</mat-grid-tile>
<mat-grid-tile>
<mat-form-field class="table-filters__date-form" color="primary" (change)="changeDate(0, $event)">
<mat-label>Start date</mat-label>
<input #ref matInput [matDatepicker]="picker1" (dateChange)="changeDate(0, ref.value)">
<mat-datepicker-toggle matSuffix [for]="picker1"></mat-datepicker-toggle>
<mat-datepicker #picker1></mat-datepicker>
</mat-form-field>
</mat-grid-tile>
<mat-grid-tile>
<mat-form-field class="table-filters__date-form" color="primary" (change)="changeDate(1, $event)">
<mat-label>Finish date</mat-label>
<input #ref1 matInput [matDatepicker]="picker2" (dateChange)="changeDate(1, ref1.value)">
<mat-datepicker-toggle matSuffix [for]="picker2"></mat-datepicker-toggle>
<mat-datepicker #picker2></mat-datepicker>
</mat-form-field>
</mat-grid-tile>
<mat-grid-tile>
<div>
<mat-label class="price-label">Price range</mat-label>
<mat-slider
color="primary"
thumbLabel
(change)="sliderChange($event)"
[displayWith]="formatLabel"
tickInterval="10"
min="1"
max="1200"></mat-slider>
</div>
</mat-grid-tile>
</mat-grid-list>
再次向文件中添加大量模块app.module.ts
:
// ...
import { TableFiltersComponent } from "./table-filters/table-filters.component";
import { MatButtonToggleModule } from "@angular/material/button-toggle";
+ import { MatDatepickerModule } from "@angular/material/datepicker";
+ import { MatFormFieldModule } from "@angular/material/form-field";
+ import { MatNativeDateModule } from "@angular/material/core";
+ import { MatInputModule } from "@angular/material/input";
+ import {MatSliderModule} from "@angular/material/slider";
// ...
MatProgressSpinnerModule,
MatButtonToggleModule,
+ MatDatepickerModule,
+ MatFormFieldModule,
+ MatNativeDateModule,
+ MatInputModule,
+ MatSliderModule
],
+ providers: [MatDatepickerModule],
bootstrap: [AppComponent]
})
export class AppModule {
}
编辑table-page.component.html
文件:
<div class="table-warp">
<app-table-filters (statusChanged)="statusChanged($event)"
(dateChange)="dateChanged($event)"
(sliderChanged)="sliderChanged($event)"
></app-table-filters>
<app-material-table [query]="query"
[limit]="limit"
(pageEvent)="changePage($event)"
+ (sortingChanged)="sortingChanged($event)"></app-material-table>
</div>
以及table-page
组件:
import { Component, OnInit } from "@angular/core";
import { BehaviorSubject } from "rxjs";
@Component({
selector: "app-table-page",
templateUrl: "./table-page.component.html",
styleUrls: ["./table-page.component.scss"]
})
export class TablePageComponent implements OnInit {
...
+ public limit = 50;
+ public page = 0;
+ public sorting = ['Orders.createdAt', 'desc'];
+ public startDate = "01/1/2019";
+ public finishDate = "01/1/2022";
+ private minPrice = 0;
public _query = new BehaviorSubject({
+ "limit": this.limit,
+ "offset": this.page * this.limit,
+ order: {
+ [`${this.sorting[0]}`]: this.sorting[1],
+ },
+ "timeDimensions": [
+ {
+ "dimension": "Orders.createdAt",
+ "dateRange" : [this.startDate, this.finishDate],
+ "granularity": "day"
+ }
+ ],
"dimensions": [
"Users.id",
"Orders.id",
"Orders.size",
"Users.fullName",
"Users.city",
"Orders.price",
"Orders.status",
"Orders.createdAt"
],
+ filters: []
});
+ public changePage = (obj) => {
+ this._query.next({
+ ...this._query.value,
+ "limit": obj.pageSize,
+ "offset": obj.pageIndex * obj.pageSize
+ });
+ };
+ public sortingChanged(value) {
+ if (value === this.sorting[0] && this.sorting[1] === 'desc') {
+ this.sorting[0] = value;
+ this.sorting[1] = 'asc'
+ } else if (value === this.sorting[0] && this.sorting[1] === 'asc') {
+ this.sorting[0] = value;
+ this.sorting[1] = 'desc'
+ } else {
+ this.sorting[0] = value;
+ }
+ this.sorting[0] = value;
+ this._query.next({
+ ...this._query.value,
+ order: {
+ [`${this.sorting[0]}`]: this.sorting[1],
+ },
+ });
+ }
+ public dateChanged(value) {
+ if (value.number === 0) {
+ this.startDate = value.date
+ }
+ if (value.number === 1) {
+ this.finishDate = value.date
+ }
+ this._query.next({
+ ...this._query.value,
+ timeDimensions: [
+ {
+ dimension: "Orders.createdAt",
+ dateRange: [this.startDate, this.finishDate],
+ granularity: null
+ }
+ ]
+ });
+ }
+ public statusChanged(value) {
+ this.status = value;
+ this._query.next({
+ ...this._query.value,
+ "filters": this.getFilters(this.status, this.minPrice)
+ });
+ };
+ public sliderChanged(obj) {
+ this.minPrice = obj.value;
+ this._query.next({
+ ...this._query.value,
+ "filters": this.getFilters(this.status, this.minPrice)
+ });
+ };
+ private getFilters = (status, price) => {
+ let filters = [];
+ if (status) {
+ filters.push(
+ {
+ "dimension": "Orders.status",
+ "operator": status === "all" ? "set" : "equals",
+ "values": [
+ status
+ ]
+ }
+ );
+ }
+ if (price) {
+ filters.push(
+ {
+ dimension: 'Orders.price',
+ operator: 'gt',
+ values: [`${price}`],
+ },
+ );
+ }
+ return filters;
+ };
...
}
现在我们需要将更改传播到material-table
组件:
// ...
export class MaterialTableComponent {
// ...
+ @Output() sortingChanged = new EventEmitter();
// ...
+ changeSorting(value) {
+ this.sortingChanged.emit(value)
+ }
}
及其模板:
// ...
<ng-container matColumnDef="id">
<th matSort mat-header-cell *matHeaderCellDef mat-sort-header
+. (click)="changeSorting('Orders.id')"> Order ID</th>
<td mat-cell *matCellDef="let element"> {{element['Orders.id']}} </td>
</ng-container>
<ng-container matColumnDef="size">
<th mat-header-cell *matHeaderCellDef mat-sort-header
+ (click)="changeSorting('Orders.size')"> Orders size</th>
<td mat-cell *matCellDef="let element"> {{element['Orders.size']}} </td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header
+ (click)="changeSorting('Users.fullName')"> Full Name</th>
<td mat-cell *matCellDef="let element"> {{element['Users.fullName']}} </td>
</ng-container>
<ng-container matColumnDef="city">
<th mat-header-cell *matHeaderCellDef mat-sort-header
+ (click)="changeSorting('Users.city')"> User city</th>
<td mat-cell *matCellDef="let element"> {{element['Users.city']}} </td>
</ng-container>
<ng-container matColumnDef="price">
<th mat-header-cell *matHeaderCellDef mat-sort-header
+ (click)="changeSorting('Orders.price')"> Order price</th>
<td mat-cell *matCellDef="let element"> {{element['Orders.price']}} </td>
</ng-container>
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef mat-sort-header
+ (click)="changeSorting('Orders.status')"> Status</th>
<td mat-cell *matCellDef="let element"> {{element['Orders.status']}} </td>
</ng-container>
<ng-container matColumnDef="date">
<th mat-header-cell *matHeaderCellDef mat-sort-header
+ (click)="changeSorting('Orders.createdAt')"> Created at</th>
<td mat-cell *matCellDef="let element"> {{element['Orders.createdAt'] | date}} </td>
</ng-container>
// ...
太棒了!🎉 现在我们有了完全支持过滤和排序的数据表:
就这些!😇 恭喜你完成本指南!🎉
现在,您应该能够使用 Angular 和 Material 创建由 Cube.js 提供支持的综合分析仪表板来显示汇总指标和详细信息。
欢迎随意探索使用 Cube.js 可以实现的其他示例,例如实时仪表板指南和开源 Web 分析平台指南。
鏂囩珷鏉ユ簮锛�https://dev.to/cubejs/angular-dashboard-with-material-1aj