mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-03 04:10:20 +08:00
feat: support dashboard in homepage (#2207)
* feat: support dashboard * feat: support dashboard
This commit is contained in:
33
controllers/get-dashboard.go
Normal file
33
controllers/get-dashboard.go
Normal 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
111
object/get-dashboard.go
Normal 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
|
||||||
|
}
|
@ -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")
|
||||||
|
@ -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",
|
||||||
|
25
web/src/backend/DashboardBackend.js
Normal file
25
web/src/backend/DashboardBackend.js
Normal 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());
|
||||||
|
}
|
@ -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"}} />
|
||||||
</Row>
|
</Card>
|
||||||
</div>
|
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
Reference in New Issue
Block a user