diff --git a/controllers/get-dashboard.go b/controllers/get-dashboard.go new file mode 100644 index 00000000..84766620 --- /dev/null +++ b/controllers/get-dashboard.go @@ -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) +} diff --git a/object/get-dashboard.go b/object/get-dashboard.go new file mode 100644 index 00000000..1d394a97 --- /dev/null +++ b/object/get-dashboard.go @@ -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 +} diff --git a/routers/router.go b/routers/router.go index 44d0fc5b..709b36eb 100644 --- a/routers/router.go +++ b/routers/router.go @@ -51,6 +51,7 @@ func initAPI() { beego.Router("/api/signup", &controllers.ApiController{}, "POST:Signup") beego.Router("/api/login", &controllers.ApiController{}, "POST:Login") 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/get-account", &controllers.ApiController{}, "GET:GetAccount") beego.Router("/api/userinfo", &controllers.ApiController{}, "GET:GetUserinfo") diff --git a/web/package.json b/web/package.json index f1083476..a537173e 100644 --- a/web/package.json +++ b/web/package.json @@ -30,6 +30,7 @@ "copy-to-clipboard": "^3.3.1", "core-js": "^3.25.0", "craco-less": "^2.0.0", + "echarts": "^5.4.3", "eslint-plugin-unused-imports": "^2.0.0", "ethers": "5.6.9", "file-saver": "^2.0.5", diff --git a/web/src/backend/DashboardBackend.js b/web/src/backend/DashboardBackend.js new file mode 100644 index 00000000..7636ce00 --- /dev/null +++ b/web/src/backend/DashboardBackend.js @@ -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()); +} diff --git a/web/src/basic/HomePage.js b/web/src/basic/HomePage.js index 8cb1d8e9..fcd47c37 100644 --- a/web/src/basic/HomePage.js +++ b/web/src/basic/HomePage.js @@ -13,8 +13,11 @@ // limitations under the License. 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 DashboardBackend from "../backend/DashboardBackend"; +import * as echarts from "echarts"; import * as Setting from "../Setting"; import SingleCard from "./SingleCard"; import i18next from "i18next"; @@ -25,11 +28,13 @@ class HomePage extends React.Component { this.state = { classes: props, applications: null, + dashboardData: null, }; } UNSAFE_componentWillMount() { this.getApplicationsByOrganization(this.props.account.owner); + this.getDashboard(); } 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() { let items = []; if (Setting.isAdminUser(this.props.account)) { @@ -75,9 +95,53 @@ class HomePage extends React.Component { 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() { - if (this.state.applications === null) { - return null; + const data = this.state.dashboardData; + if (data === null) { + return ( +
+ +
+ ); } const items = this.getItems(); @@ -96,24 +160,35 @@ class HomePage extends React.Component { ); } else { return ( -
- - { - items.map(item => { - return ( - - ); - }) - } - -
+ + + + + + + + + } style={{width: "200px", paddingLeft: "10px"}} /> + + + + + } style={{width: "200px", paddingLeft: "10px"}} /> + + + + + } style={{width: "200px", paddingLeft: "10px"}} /> + + + ); } } render() { return ( -
+
{ @@ -121,6 +196,8 @@ class HomePage extends React.Component { } +
); } diff --git a/web/yarn.lock b/web/yarn.lock index 768bcb5f..a71065f6 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -6682,6 +6682,14 @@ ecc-jsbn@~0.1.1: jsbn "~0.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: version "1.1.1" 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" 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: version "1.14.1" 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" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" 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"