feat: support dashboard in homepage (#2207)

* feat: support dashboard

* feat: support dashboard
This commit is contained in:
2023-08-14 14:32:12 +08:00
committed by Yang Luo
parent 7a2a40edcc
commit 8ff0cfd6ec
7 changed files with 283 additions and 15 deletions

View File

@ -0,0 +1,33 @@
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import "github.com/casdoor/casdoor/object"
// GetDashboard
// @Title GetDashboard
// @Tag GetDashboard API
// @Description get information of dashboard
// @Success 200 {object} controllers.Response The Response object
// @router /get-dashboard [get]
func (c *ApiController) GetDashboard() {
data, err := object.GetDashboard()
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(data)
}

111
object/get-dashboard.go Normal file
View File

@ -0,0 +1,111 @@
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import "time"
type Dashboard struct {
OrganizationCounts []int `json:"organizationCounts"`
UserCounts []int `json:"userCounts"`
ProviderCounts []int `json:"providerCounts"`
ApplicationCounts []int `json:"applicationCounts"`
SubscriptionCounts []int `json:"subscriptionCounts"`
}
func GetDashboard() (*Dashboard, error) {
dashboard := &Dashboard{
OrganizationCounts: make([]int, 31),
UserCounts: make([]int, 31),
ProviderCounts: make([]int, 31),
ApplicationCounts: make([]int, 31),
SubscriptionCounts: make([]int, 31),
}
nowTime := time.Now()
organizations := make([]Organization, 0)
if err := ormer.Engine.Find(&organizations); err != nil {
return dashboard, err
}
users := make([]User, 0)
if err := ormer.Engine.Find(&users); err != nil {
return dashboard, err
}
providers := make([]Provider, 0)
if err := ormer.Engine.Find(&providers); err != nil {
return dashboard, err
}
applications := make([]Application, 0)
if err := ormer.Engine.Find(&applications); err != nil {
return dashboard, err
}
subscriptions := make([]Subscription, 0)
if err := ormer.Engine.Find(&subscriptions); err != nil {
return dashboard, err
}
for i := 30; i >= 0; i-- {
cutTime := nowTime.AddDate(0, 0, -i)
dashboard.OrganizationCounts[30-i] = countCreatedBefore(organizations, cutTime)
dashboard.UserCounts[30-i] = countCreatedBefore(users, cutTime)
dashboard.ProviderCounts[30-i] = countCreatedBefore(providers, cutTime)
dashboard.ApplicationCounts[30-i] = countCreatedBefore(applications, cutTime)
dashboard.SubscriptionCounts[30-i] = countCreatedBefore(subscriptions, cutTime)
}
return dashboard, nil
}
func countCreatedBefore(objects interface{}, before time.Time) int {
count := 0
switch obj := objects.(type) {
case []Organization:
for _, o := range obj {
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", o.CreatedTime)
if createdTime.Before(before) {
count++
}
}
case []User:
for _, u := range obj {
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", u.CreatedTime)
if createdTime.Before(before) {
count++
}
}
case []Provider:
for _, p := range obj {
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", p.CreatedTime)
if createdTime.Before(before) {
count++
}
}
case []Application:
for _, a := range obj {
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", a.CreatedTime)
if createdTime.Before(before) {
count++
}
}
case []Subscription:
for _, s := range obj {
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", s.CreatedTime)
if createdTime.Before(before) {
count++
}
}
}
return count
}

View File

@ -51,6 +51,7 @@ func initAPI() {
beego.Router("/api/signup", &controllers.ApiController{}, "POST:Signup") beego.Router("/api/signup", &controllers.ApiController{}, "POST:Signup")
beego.Router("/api/login", &controllers.ApiController{}, "POST:Login") beego.Router("/api/login", &controllers.ApiController{}, "POST:Login")
beego.Router("/api/get-app-login", &controllers.ApiController{}, "GET:GetApplicationLogin") beego.Router("/api/get-app-login", &controllers.ApiController{}, "GET:GetApplicationLogin")
beego.Router("/api/get-dashboard", &controllers.ApiController{}, "GET:GetDashboard")
beego.Router("/api/logout", &controllers.ApiController{}, "GET,POST:Logout") beego.Router("/api/logout", &controllers.ApiController{}, "GET,POST:Logout")
beego.Router("/api/get-account", &controllers.ApiController{}, "GET:GetAccount") beego.Router("/api/get-account", &controllers.ApiController{}, "GET:GetAccount")
beego.Router("/api/userinfo", &controllers.ApiController{}, "GET:GetUserinfo") beego.Router("/api/userinfo", &controllers.ApiController{}, "GET:GetUserinfo")

View File

@ -30,6 +30,7 @@
"copy-to-clipboard": "^3.3.1", "copy-to-clipboard": "^3.3.1",
"core-js": "^3.25.0", "core-js": "^3.25.0",
"craco-less": "^2.0.0", "craco-less": "^2.0.0",
"echarts": "^5.4.3",
"eslint-plugin-unused-imports": "^2.0.0", "eslint-plugin-unused-imports": "^2.0.0",
"ethers": "5.6.9", "ethers": "5.6.9",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",

View File

@ -0,0 +1,25 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as Setting from "../Setting";
export function getDashboard(owner, name) {
return fetch(`${Setting.ServerUrl}/api/get-dashboard`, {
method: "GET",
credentials: "include",
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}

View File

@ -13,8 +13,11 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {Card, Col, Row} from "antd"; import {Card, Col, Row, Spin, Statistic} from "antd";
import {ArrowUpOutlined} from "@ant-design/icons";
import * as ApplicationBackend from "../backend/ApplicationBackend"; import * as ApplicationBackend from "../backend/ApplicationBackend";
import * as DashboardBackend from "../backend/DashboardBackend";
import * as echarts from "echarts";
import * as Setting from "../Setting"; import * as Setting from "../Setting";
import SingleCard from "./SingleCard"; import SingleCard from "./SingleCard";
import i18next from "i18next"; import i18next from "i18next";
@ -25,11 +28,13 @@ class HomePage extends React.Component {
this.state = { this.state = {
classes: props, classes: props,
applications: null, applications: null,
dashboardData: null,
}; };
} }
UNSAFE_componentWillMount() { UNSAFE_componentWillMount() {
this.getApplicationsByOrganization(this.props.account.owner); this.getApplicationsByOrganization(this.props.account.owner);
this.getDashboard();
} }
getApplicationsByOrganization(organizationName) { getApplicationsByOrganization(organizationName) {
@ -41,6 +46,21 @@ class HomePage extends React.Component {
}); });
} }
getDashboard() {
DashboardBackend.getDashboard()
.then((res) => {
if (res.status === "ok") {
this.setState({
dashboardData: res.data,
}, () => {
this.renderEChart();
});
} else {
Setting.showMessage("error", res.msg);
}
});
}
getItems() { getItems() {
let items = []; let items = [];
if (Setting.isAdminUser(this.props.account)) { if (Setting.isAdminUser(this.props.account)) {
@ -75,9 +95,53 @@ class HomePage extends React.Component {
return items; return items;
} }
renderEChart() {
const data = this.state.dashboardData;
const chartDom = document.getElementById("echarts-chart");
const myChart = echarts.init(chartDom);
const currentDate = new Date();
const dateArray = [];
for (let i = 30; i >= 0; i--) {
const date = new Date(currentDate);
date.setDate(date.getDate() - i);
const month = parseInt(date.getMonth()) + 1;
const day = parseInt(date.getDate());
const formattedDate = `${month}-${day}`;
dateArray.push(formattedDate);
}
const option = {
title: {text: i18next.t("home:Past 30 Days")},
tooltip: {trigger: "axis"},
legend: {data: [
i18next.t("general:Users"),
i18next.t("general:Providers"),
i18next.t("general:Applications"),
i18next.t("general:Organizations"),
i18next.t("general:Subscriptions"),
]},
grid: {left: "3%", right: "4%", bottom: "3%", containLabel: true},
xAxis: {type: "category", boundaryGap: false, data: dateArray},
yAxis: {type: "value"},
series: [
{name: i18next.t("general:Organizations"), type: "line", data: data?.organizationCounts},
{name: i18next.t("general:Users"), type: "line", data: data?.userCounts},
{name: i18next.t("general:Providers"), type: "line", data: data?.providerCounts},
{name: i18next.t("general:Applications"), type: "line", data: data?.applicationCounts},
{name: i18next.t("general:Subscriptions"), type: "line", data: data?.subscriptionCounts},
],
};
myChart.setOption(option);
}
renderCards() { renderCards() {
if (this.state.applications === null) { const data = this.state.dashboardData;
return null; if (data === null) {
return (
<div style={{display: "flex", justifyContent: "center", alignItems: "center", marginTop: "10%"}}>
<Spin size="large" tip={i18next.t("login:Loading")} style={{paddingTop: "10%"}} />
</div>
);
} }
const items = this.getItems(); const items = this.getItems();
@ -96,24 +160,35 @@ class HomePage extends React.Component {
); );
} else { } else {
return ( return (
<div style={{marginRight: "15px", marginLeft: "15px"}}> <Row gutter={80}>
<Row style={{marginLeft: "-20px", marginRight: "-20px", marginTop: "20px"}} gutter={24}> <Col span={50}>
{ <Card bordered={false} bodyStyle={{width: "100%", height: "150px", display: "flex", alignItems: "center", justifyContent: "center"}}>
items.map(item => { <Statistic title={i18next.t("home:Total users")} fontSize="100px" value={data?.userCounts[30]} valueStyle={{fontSize: "30px"}} style={{width: "200px", paddingLeft: "10px"}} />
return ( </Card>
<SingleCard logo={item.logo} link={item.link} title={item.name} desc={item.organizer} time={item.createdTime} isSingle={items.length === 1} key={item.name} /> </Col>
); <Col span={50}>
}) <Card bordered={false} bodyStyle={{width: "100%", height: "150px", display: "flex", alignItems: "center", justifyContent: "center"}}>
} <Statistic title={i18next.t("home:New users today")} fontSize="100px" value={data?.userCounts[30] - data?.userCounts[30 - 1]} valueStyle={{fontSize: "30px"}} prefix={<ArrowUpOutlined />} style={{width: "200px", paddingLeft: "10px"}} />
</Card>
</Col>
<Col span={50}>
<Card bordered={false} bodyStyle={{width: "100%", height: "150px", display: "flex", alignItems: "center", justifyContent: "center"}}>
<Statistic title={i18next.t("home:New users past 7 days")} value={data?.userCounts[30] - data?.userCounts[30 - 7]} valueStyle={{fontSize: "30px"}} prefix={<ArrowUpOutlined />} style={{width: "200px", paddingLeft: "10px"}} />
</Card>
</Col>
<Col span={50}>
<Card bordered={false} bodyStyle={{width: "100%", height: "150px", display: "flex", alignItems: "center", justifyContent: "center"}}>
<Statistic title={i18next.t("home:New users past 30 days")} value={data?.userCounts[30] - data?.userCounts[30 - 30]} valueStyle={{fontSize: "30px"}} prefix={<ArrowUpOutlined />} style={{width: "200px", paddingLeft: "10px"}} />
</Card>
</Col>
</Row> </Row>
</div>
); );
} }
} }
render() { render() {
return ( return (
<div> <div style={{display: "flex", justifyContent: "center", flexDirection: "column", alignItems: "center"}}>
<Row style={{width: "100%"}}> <Row style={{width: "100%"}}>
<Col span={24} style={{display: "flex", justifyContent: "center"}} > <Col span={24} style={{display: "flex", justifyContent: "center"}} >
{ {
@ -121,6 +196,8 @@ class HomePage extends React.Component {
} }
</Col> </Col>
</Row> </Row>
<div id="echarts-chart"
style={{width: "80%", height: "400px", textAlign: "center", marginTop: "20px"}}></div>
</div> </div>
); );
} }

View File

@ -6682,6 +6682,14 @@ ecc-jsbn@~0.1.1:
jsbn "~0.1.0" jsbn "~0.1.0"
safer-buffer "^2.1.0" safer-buffer "^2.1.0"
echarts@^5.4.3:
version "5.4.3"
resolved "https://registry.yarnpkg.com/echarts/-/echarts-5.4.3.tgz#f5522ef24419164903eedcfd2b506c6fc91fb20c"
integrity sha512-mYKxLxhzy6zyTi/FaEbJMOZU1ULGEQHaeIeuMR5L+JnJTpz+YR03mnnpBhbR4+UYJAgiXgpyTVLffPAjOTLkZA==
dependencies:
tslib "2.3.0"
zrender "5.4.4"
ee-first@1.1.1: ee-first@1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@ -14147,6 +14155,11 @@ tsconfig-paths@^3.14.1:
minimist "^1.2.6" minimist "^1.2.6"
strip-bom "^3.0.0" strip-bom "^3.0.0"
tslib@2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
tslib@^1.8.1, tslib@^1.9.0: tslib@^1.8.1, tslib@^1.9.0:
version "1.14.1" version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
@ -15090,3 +15103,10 @@ yocto-queue@^0.1.0:
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
zrender@5.4.4:
version "5.4.4"
resolved "https://registry.yarnpkg.com/zrender/-/zrender-5.4.4.tgz#8854f1d95ecc82cf8912f5a11f86657cb8c9e261"
integrity sha512-0VxCNJ7AGOMCWeHVyTrGzUgrK4asT4ml9PEkeGirAkKNYXYzoPJCLvmyfdoOXcjTHPs10OZVMfD1Rwg16AZyYw==
dependencies:
tslib "2.3.0"