mirror of
https://github.com/Team4388/ScoutingApp2022.git
synced 2026-06-09 08:48:05 -06:00
we ball
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+21
-21
@@ -10,23 +10,23 @@ services:
|
|||||||
context: ./webserver
|
context: ./webserver
|
||||||
dockerfile: ./Dockerfile.prod
|
dockerfile: ./Dockerfile.prod
|
||||||
expose:
|
expose:
|
||||||
- 8080
|
- 80
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080/tcp"
|
- "80:80/tcp"
|
||||||
# - "80:80/tcp"
|
# - "80:80/tcp"
|
||||||
# - "443:443/tcp"
|
# - "443:443/tcp"
|
||||||
volumes:
|
volumes:
|
||||||
- ./webserver/nginx:/etc/nginx/:ro
|
- ./webserver/nginx:/etc/nginx/:ro
|
||||||
ssl-proxy:
|
# ssl-proxy:
|
||||||
image: fsouza/docker-ssl-proxy
|
# image: fsouza/docker-ssl-proxy
|
||||||
ports:
|
# ports:
|
||||||
- "80:80"
|
# - "80:80"
|
||||||
- "443:443"
|
# - "443:443"
|
||||||
environment:
|
# environment:
|
||||||
- DOMAIN=10.43.88.1
|
# - DOMAIN=10.43.88.1
|
||||||
- TARGET_PORT=8080
|
# - TARGET_PORT=8080
|
||||||
- TARGET_HOST=scouting-webserver-prod
|
# - TARGET_HOST=scouting-webserver-prod
|
||||||
- SSL_PORT:443
|
# - SSL_PORT:443
|
||||||
couchdb:
|
couchdb:
|
||||||
container_name: "scouting-couchdb"
|
container_name: "scouting-couchdb"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -47,12 +47,12 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./couchdb/db.local.ini:/opt/couchdb/etc/local.ini
|
- ./couchdb/db.local.ini:/opt/couchdb/etc/local.ini
|
||||||
- ./couchdb/data:/opt/couchdb/data
|
- ./couchdb/data:/opt/couchdb/data
|
||||||
couch-ssl-proxy:
|
# couch-ssl-proxy:
|
||||||
image: fsouza/docker-ssl-proxy
|
# image: fsouza/docker-ssl-proxy
|
||||||
ports:
|
# ports:
|
||||||
- "5985:5985"
|
# - "5985:5985"
|
||||||
environment:
|
# environment:
|
||||||
- DOMAIN=10.43.88.1
|
# - DOMAIN=10.43.88.1
|
||||||
- TARGET_PORT=5984
|
# - TARGET_PORT=5984
|
||||||
- TARGET_HOST=scouting-couchdb
|
# - TARGET_HOST=scouting-couchdb
|
||||||
- SSL_PORT:5985
|
# - SSL_PORT:5985
|
||||||
|
|||||||
@@ -16,6 +16,6 @@ FROM nginx:1-alpine
|
|||||||
COPY --from=build-step /app/build /usr/share/nginx/html
|
COPY --from=build-step /app/build /usr/share/nginx/html
|
||||||
RUN rm /etc/nginx/conf.d/default.conf
|
RUN rm /etc/nginx/conf.d/default.conf
|
||||||
# COPY nginx/nginx.conf /etc/nginx/conf.d
|
# COPY nginx/nginx.conf /etc/nginx/conf.d
|
||||||
EXPOSE 8080
|
EXPOSE 80
|
||||||
# EXPOSE 443
|
# EXPOSE 443
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
|
||||||
|
types {
|
||||||
|
text/html html htm shtml;
|
||||||
|
text/css css;
|
||||||
|
text/xml xml;
|
||||||
|
image/gif gif;
|
||||||
|
image/jpeg jpeg jpg;
|
||||||
|
application/javascript js;
|
||||||
|
application/atom+xml atom;
|
||||||
|
application/rss+xml rss;
|
||||||
|
|
||||||
|
text/mathml mml;
|
||||||
|
text/plain txt;
|
||||||
|
text/vnd.sun.j2me.app-descriptor jad;
|
||||||
|
text/vnd.wap.wml wml;
|
||||||
|
text/x-component htc;
|
||||||
|
|
||||||
|
image/png png;
|
||||||
|
image/svg+xml svg svgz;
|
||||||
|
image/tiff tif tiff;
|
||||||
|
image/vnd.wap.wbmp wbmp;
|
||||||
|
image/webp webp;
|
||||||
|
image/x-icon ico;
|
||||||
|
image/x-jng jng;
|
||||||
|
image/x-ms-bmp bmp;
|
||||||
|
|
||||||
|
application/font-woff woff;
|
||||||
|
application/java-archive jar war ear;
|
||||||
|
application/json json;
|
||||||
|
application/mac-binhex40 hqx;
|
||||||
|
application/msword doc;
|
||||||
|
application/pdf pdf;
|
||||||
|
application/postscript ps eps ai;
|
||||||
|
application/rtf rtf;
|
||||||
|
application/vnd.apple.mpegurl m3u8;
|
||||||
|
application/vnd.google-earth.kml+xml kml;
|
||||||
|
application/vnd.google-earth.kmz kmz;
|
||||||
|
application/vnd.ms-excel xls;
|
||||||
|
application/vnd.ms-fontobject eot;
|
||||||
|
application/vnd.ms-powerpoint ppt;
|
||||||
|
application/vnd.oasis.opendocument.graphics odg;
|
||||||
|
application/vnd.oasis.opendocument.presentation odp;
|
||||||
|
application/vnd.oasis.opendocument.spreadsheet ods;
|
||||||
|
application/vnd.oasis.opendocument.text odt;
|
||||||
|
application/vnd.openxmlformats-officedocument.presentationml.presentation
|
||||||
|
pptx;
|
||||||
|
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
|
||||||
|
xlsx;
|
||||||
|
application/vnd.openxmlformats-officedocument.wordprocessingml.document
|
||||||
|
docx;
|
||||||
|
application/vnd.wap.wmlc wmlc;
|
||||||
|
application/x-7z-compressed 7z;
|
||||||
|
application/x-cocoa cco;
|
||||||
|
application/x-java-archive-diff jardiff;
|
||||||
|
application/x-java-jnlp-file jnlp;
|
||||||
|
application/x-makeself run;
|
||||||
|
application/x-perl pl pm;
|
||||||
|
application/x-pilot prc pdb;
|
||||||
|
application/x-rar-compressed rar;
|
||||||
|
application/x-redhat-package-manager rpm;
|
||||||
|
application/x-sea sea;
|
||||||
|
application/x-shockwave-flash swf;
|
||||||
|
application/x-stuffit sit;
|
||||||
|
application/x-tcl tcl tk;
|
||||||
|
application/x-x509-ca-cert der pem crt;
|
||||||
|
application/x-xpinstall xpi;
|
||||||
|
application/xhtml+xml xhtml;
|
||||||
|
application/xspf+xml xspf;
|
||||||
|
application/zip zip;
|
||||||
|
|
||||||
|
application/octet-stream bin exe dll;
|
||||||
|
application/octet-stream deb;
|
||||||
|
application/octet-stream dmg;
|
||||||
|
application/octet-stream iso img;
|
||||||
|
application/octet-stream msi msp msm;
|
||||||
|
|
||||||
|
audio/midi mid midi kar;
|
||||||
|
audio/mpeg mp3;
|
||||||
|
audio/ogg ogg;
|
||||||
|
audio/x-m4a m4a;
|
||||||
|
audio/x-realaudio ra;
|
||||||
|
|
||||||
|
video/3gpp 3gpp 3gp;
|
||||||
|
video/mp2t ts;
|
||||||
|
video/mp4 mp4;
|
||||||
|
video/mpeg mpeg mpg;
|
||||||
|
video/quicktime mov;
|
||||||
|
video/webm webm;
|
||||||
|
video/x-flv flv;
|
||||||
|
video/x-m4v m4v;
|
||||||
|
video/x-mng mng;
|
||||||
|
video/x-ms-asf asx asf;
|
||||||
|
video/x-ms-wmv wmv;
|
||||||
|
video/x-msvideo avi;
|
||||||
|
}
|
||||||
@@ -2,9 +2,10 @@ events {
|
|||||||
worker_connections 4096; ## Default: 1024
|
worker_connections 4096; ## Default: 1024
|
||||||
}
|
}
|
||||||
http {
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
server {
|
server {
|
||||||
listen 8080;
|
listen 80;
|
||||||
listen [::]:8080;
|
listen [::]:80;
|
||||||
location / {
|
location / {
|
||||||
root /usr/share/nginx/html/;
|
root /usr/share/nginx/html/;
|
||||||
index index.html index.htm;
|
index index.html index.htm;
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
"@testing-library/react": "^12.1.2",
|
"@testing-library/react": "^12.1.2",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"react-apexcharts": "^1.3.9",
|
"react-apexcharts": "^1.3.9",
|
||||||
|
"ag-grid-community": "^26.2.1",
|
||||||
|
"ag-grid-react": "^26.2.0",
|
||||||
"formik": "^2.2.9",
|
"formik": "^2.2.9",
|
||||||
"pouchdb": "^7.2.2",
|
"pouchdb": "^7.2.2",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#00a65a" />
|
<meta name="theme-color" content="#00a65a" />
|
||||||
<meta name="description" content="Ridgebotics Scouting App 2022" />
|
<meta name="description" content="Ridgebotics Scouting App 2022" />
|
||||||
|
<!-- disable user zooming/scaling on mobile -->
|
||||||
|
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
<!--
|
<!--
|
||||||
manifest.json provides metadata used when your web app is installed on a
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
|
|||||||
@@ -19,11 +19,11 @@ function App() {
|
|||||||
palette: {
|
palette: {
|
||||||
mode: "dark",
|
mode: "dark",
|
||||||
background: {
|
background: {
|
||||||
paper: "#101515",
|
paper: "#203030",
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
// primary: "#edf8f3",
|
primary: "#edf8f3",
|
||||||
// secondary: "#acd3bf",
|
secondary: "#acd3bf",
|
||||||
},
|
},
|
||||||
red_alliance: "#ec2e63",
|
red_alliance: "#ec2e63",
|
||||||
blue_alliance: "#2d74eb",
|
blue_alliance: "#2d74eb",
|
||||||
|
|||||||
+69
-23
@@ -1,6 +1,6 @@
|
|||||||
import PouchDB from "pouchdb";
|
import PouchDB from "pouchdb";
|
||||||
import React, { useContext, useEffect, useState } from "react";
|
import React, { useContext, useEffect, useState } from "react";
|
||||||
import { ProcessedDataBucketContext, useProcessedDataBucket } from "./ProcessedDataBucketContext";
|
import { useProcessedDataBucket } from "./ProcessedDataBucketContext";
|
||||||
import { getProcessedDataBucket, updateProcessedDataBucket } from "./ProcessedDataBucket";
|
import { getProcessedDataBucket, updateProcessedDataBucket } from "./ProcessedDataBucket";
|
||||||
|
|
||||||
const LocalDbContext = React.createContext();
|
const LocalDbContext = React.createContext();
|
||||||
@@ -16,12 +16,11 @@ export function useRemoteDb() {
|
|||||||
|
|
||||||
export function DbProvider({ children }) {
|
export function DbProvider({ children }) {
|
||||||
// const pdbCtx = useProcessedDataBucket();
|
// const pdbCtx = useProcessedDataBucket();
|
||||||
const { processedDataBucket, setProcessedDataBucket } = useContext(ProcessedDataBucketContext);
|
const { processedDataBucket, setProcessedDataBucket } = useProcessedDataBucket();
|
||||||
// console.log(pdb);
|
// console.log(pdb);
|
||||||
const [localdb, setLocaldb] = useState(new PouchDB("testdata"));
|
const [localdb, setLocaldb] = useState(new PouchDB("denver_fr"));
|
||||||
//used in development server
|
|
||||||
const [remotedb, setRemotedb] = useState(
|
const [remotedb, setRemotedb] = useState(
|
||||||
new PouchDB("http://" + window.location.hostname + ":5984/testdata", {
|
new PouchDB("http://" + window.location.hostname + ":5984/denver_fr", {
|
||||||
skip_setup: true,
|
skip_setup: true,
|
||||||
auth: {
|
auth: {
|
||||||
username: "scouting",
|
username: "scouting",
|
||||||
@@ -29,25 +28,50 @@ export function DbProvider({ children }) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
localdb.replicate
|
||||||
|
.from(remotedb, {
|
||||||
|
live: true,
|
||||||
|
retry: true,
|
||||||
|
})
|
||||||
|
.on("change", (change) => {
|
||||||
|
updateProcessedDataBucket(localdb, setProcessedDataBucket);
|
||||||
|
});
|
||||||
|
// updateProcessedDataBucket(localdb, setProcessedDataBucket);
|
||||||
|
// useEffect(() => {
|
||||||
|
// setDatabaseName("denver_fr", setLocaldb, setRemotedb, setProcessedDataBucket);
|
||||||
|
// });
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
console.log("TEST");
|
// updateProcessedDataBucket(localdb, setProcessedDataBucket);
|
||||||
updateProcessedDataBucket(localdb, setProcessedDataBucket);
|
// localdb
|
||||||
localdb
|
// .sync(remotedb, {
|
||||||
.sync(remotedb, {
|
// live: true,
|
||||||
live: true,
|
// retry: true,
|
||||||
retry: true,
|
// })
|
||||||
})
|
// .on("change", function (change) {
|
||||||
.on("change", function (change) {
|
// console.log("DB CHANGED");
|
||||||
console.log("DB CHANGED");
|
// updateProcessedDataBucket(localdb, setProcessedDataBucket);
|
||||||
updateProcessedDataBucket(localdb, setProcessedDataBucket);
|
// })
|
||||||
})
|
// .on("paused", function (info) {
|
||||||
.on("paused", function (info) {})
|
// console.log("sync paused");
|
||||||
.on("active", function (info) {})
|
// // console.log(info);
|
||||||
.on("error", function (err) {
|
// })
|
||||||
console.error(err);
|
// .on("active", function (info) {
|
||||||
});
|
// console.log("sync active");
|
||||||
}, [localdb, setProcessedDataBucket]);
|
// // console.log(info);
|
||||||
|
// })
|
||||||
|
// .on("denied", function (info) {
|
||||||
|
// console.log("sync denied");
|
||||||
|
// // console.log(info);
|
||||||
|
// })
|
||||||
|
// .on("complete", function (info) {
|
||||||
|
// console.log("sync complete");
|
||||||
|
// // console.log(info);
|
||||||
|
// })
|
||||||
|
// .on("error", function (err) {
|
||||||
|
// console.error(err);
|
||||||
|
// });
|
||||||
|
// }, [localdb, setProcessedDataBucket]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LocalDbContext.Provider value={{ localdb, setLocaldb }}>
|
<LocalDbContext.Provider value={{ localdb, setLocaldb }}>
|
||||||
@@ -55,3 +79,25 @@ export function DbProvider({ children }) {
|
|||||||
</LocalDbContext.Provider>
|
</LocalDbContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setDatabaseName(name, setLocaldb, setRemotedb, setProcessedDataBucket) {
|
||||||
|
let localdb = new PouchDB(name);
|
||||||
|
let remotedb = new PouchDB("http://" + window.location.hostname + ":5984/" + name, {
|
||||||
|
skip_setup: true,
|
||||||
|
auth: {
|
||||||
|
username: "scouting",
|
||||||
|
password: "Ridgebotics",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
localdb.replicate
|
||||||
|
.from(remotedb, {
|
||||||
|
live: true,
|
||||||
|
retry: true,
|
||||||
|
})
|
||||||
|
.on("change", (change) => {
|
||||||
|
updateProcessedDataBucket(localdb, setProcessedDataBucket);
|
||||||
|
});
|
||||||
|
updateProcessedDataBucket(localdb, setProcessedDataBucket);
|
||||||
|
setLocaldb(localdb);
|
||||||
|
setRemotedb(remotedb);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ComparisonPanel from "./ComparisonPanel";
|
||||||
|
|
||||||
|
const AnalyticsPanel = (props) => {
|
||||||
|
if (props.selectedTeams.length > 0) return <ComparisonPanel selectedTeams={props.selectedTeams} />;
|
||||||
|
return <div />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AnalyticsPanel;
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { ProcessedDataBucketContext, useProcessedDataBucket } from "../../ProcessedDataBucketContext";
|
||||||
|
import Chart from "react-apexcharts";
|
||||||
|
import { Box } from "@mui/material";
|
||||||
|
|
||||||
|
const ComparativeBoxPlot = (props) => {
|
||||||
|
let { processedDataBucket, setProcessedDataBucket } = useProcessedDataBucket();
|
||||||
|
const getPercentile = (sorted_set, percentile) => {
|
||||||
|
let idx = percentile * sorted_set.length;
|
||||||
|
if (Math.floor(idx) == idx) return sorted_set[idx - 1] / 2 + sorted_set[idx] / 2;
|
||||||
|
else return sorted_set[Math.floor(idx)];
|
||||||
|
};
|
||||||
|
const getFiveNumberSummary = (set) => {
|
||||||
|
set.sort();
|
||||||
|
return [set[0], getPercentile(set, 0.25), getPercentile(set, 0.5), getPercentile(set, 0.75), set[set.length - 1]];
|
||||||
|
};
|
||||||
|
const generateBoxPlotData = (pdb, setName, selectedTeams) => {
|
||||||
|
let data = [];
|
||||||
|
for (const teamNumber of selectedTeams) {
|
||||||
|
//console.log(pdb.teamData[teamNumber].data_sets[setName]);
|
||||||
|
data.push({
|
||||||
|
x: teamNumber,
|
||||||
|
y: getFiveNumberSummary(pdb.teamData[teamNumber].data_sets[setName]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: "boxPlot",
|
||||||
|
data: data,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ width: "350px", height: "220px" }}>
|
||||||
|
<Chart
|
||||||
|
type="boxPlot"
|
||||||
|
options={{
|
||||||
|
chart: {
|
||||||
|
type: "boxPlot",
|
||||||
|
width: 350,
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
mode: "dark",
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
text: props.title,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
series={generateBoxPlotData(processedDataBucket, props.setName, props.selectedTeams)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
/* <Chart
|
||||||
|
options={{
|
||||||
|
chart: {
|
||||||
|
width: 384,
|
||||||
|
type: "pie",
|
||||||
|
},
|
||||||
|
labels: ["None", "Low", "Mid", "High", "Transversal"],
|
||||||
|
}}
|
||||||
|
series={processedDataBucket.teamData[4388].climb_counts}
|
||||||
|
type="pie"
|
||||||
|
width={380}
|
||||||
|
/> */
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default ComparativeBoxPlot;
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { ProcessedDataBucketContext, useProcessedDataBucket } from "../../ProcessedDataBucketContext";
|
||||||
|
import Chart from "react-apexcharts";
|
||||||
|
import { Box } from "@mui/material";
|
||||||
|
import ComparativeBoxPlot from "./ComparativeBoxPlot";
|
||||||
|
|
||||||
|
const ComparisonPanel = (props) => {
|
||||||
|
let { processedDataBucket, setProcessedDataBucket } = useProcessedDataBucket();
|
||||||
|
if (processedDataBucket == null) return <div />;
|
||||||
|
|
||||||
|
//gets the given percentile of a sorted set
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
height: "400px",
|
||||||
|
m: 1,
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "center",
|
||||||
|
p: 1,
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ComparativeBoxPlot setName="total_match_points" title="Total Match Points" selectedTeams={props.selectedTeams} />
|
||||||
|
<ComparativeBoxPlot setName="auto_points" title="Auto Points" selectedTeams={props.selectedTeams} />
|
||||||
|
<ComparativeBoxPlot setName="teleop_hub_points" title="Teleop Hub Points" selectedTeams={props.selectedTeams} />
|
||||||
|
<ComparativeBoxPlot setName="climb_points" title="Climb Points" selectedTeams={props.selectedTeams} />
|
||||||
|
<ComparativeBoxPlot setName="upper_hub_auto" title="Upper Hub Auto" selectedTeams={props.selectedTeams} />
|
||||||
|
<ComparativeBoxPlot setName="lower_hub_auto" title="Lower Hub Auto" selectedTeams={props.selectedTeams} />
|
||||||
|
<ComparativeBoxPlot setName="upper_hub_teleop" title="Upper Hub Teleop" selectedTeams={props.selectedTeams} />
|
||||||
|
<ComparativeBoxPlot setName="lower_hub_teleop" title="Lower Hub Teleop" selectedTeams={props.selectedTeams} />
|
||||||
|
{/* <Box sx={{ width: "300px", height: "200px" }}>
|
||||||
|
<Chart
|
||||||
|
type="boxPlot"
|
||||||
|
options={{
|
||||||
|
chart: {
|
||||||
|
type: "boxPlot",
|
||||||
|
width: 350,
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
mode: "dark",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
series={generateBoxPlotData(processedDataBucket, "upper_hub_auto", props.selectedTeams)}
|
||||||
|
/>
|
||||||
|
</Box> */}
|
||||||
|
{/* <Chart
|
||||||
|
options={{
|
||||||
|
chart: {
|
||||||
|
width: 384,
|
||||||
|
type: "pie",
|
||||||
|
},
|
||||||
|
labels: ["None", "Low", "Mid", "High", "Transversal"],
|
||||||
|
}}
|
||||||
|
series={processedDataBucket.teamData[4388].climb_counts}
|
||||||
|
type="pie"
|
||||||
|
width={380}
|
||||||
|
/> */}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ComparisonPanel;
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
import React from "react";
|
import React, { useState, useCallback, useRef } from "react";
|
||||||
import { useLocalDb } from "../../DbContext";
|
import { useLocalDb } from "../../DbContext";
|
||||||
import { ProcessedDataBucketContext, useProcessedDataBucket } from "../../ProcessedDataBucketContext";
|
import { ProcessedDataBucketContext, useProcessedDataBucket } from "../../ProcessedDataBucketContext";
|
||||||
import Chart from "react-apexcharts";
|
|
||||||
import { DataGrid } from "@mui/x-data-grid";
|
import { DataGrid } from "@mui/x-data-grid";
|
||||||
import { Box } from "@mui/material";
|
import { Box } from "@mui/material";
|
||||||
|
import AnalyticsPanel from "./AnalyticsPanel";
|
||||||
|
|
||||||
|
//https://ag-grid.com/react-data-grid/
|
||||||
|
import { AgGridReact } from "ag-grid-react";
|
||||||
|
|
||||||
|
import "ag-grid-community/dist/styles/ag-grid.css";
|
||||||
|
import "ag-grid-community/dist/styles/ag-theme-alpine-dark.css";
|
||||||
|
|
||||||
const DashboardPage = () => {
|
const DashboardPage = () => {
|
||||||
// <ProcessedDataBucketContext.Consumer>
|
// <ProcessedDataBucketContext.Consumer>
|
||||||
@@ -26,72 +32,90 @@ const DashboardPage = () => {
|
|||||||
// };
|
// };
|
||||||
// };
|
// };
|
||||||
let { processedDataBucket, setProcessedDataBucket } = useProcessedDataBucket();
|
let { processedDataBucket, setProcessedDataBucket } = useProcessedDataBucket();
|
||||||
console.log(processedDataBucket);
|
|
||||||
if (processedDataBucket == null) {
|
let rowData = [];
|
||||||
return <div />;
|
if (processedDataBucket != null) {
|
||||||
|
//turns the values of the key value pairs in the list into an array
|
||||||
|
let team_data_array = Object.values(processedDataBucket.teamData);
|
||||||
|
// let team_data_array = Array.from(processedDataBucket.teamData);
|
||||||
|
const roundPlaces = (n, d) => Math.round(n * Math.pow(10, d)) / Math.pow(10, d);
|
||||||
|
// team_data_array.forEach((value, index, array) => {
|
||||||
|
for (const property in processedDataBucket.teamData) {
|
||||||
|
let value = processedDataBucket.teamData[property];
|
||||||
|
rowData.push({
|
||||||
|
id: value.team_number,
|
||||||
|
average_auto_points: roundPlaces(value.average_auto_points, 2),
|
||||||
|
average_teleop_hub_points: roundPlaces(value.average_teleop_hub_points, 2),
|
||||||
|
average_climb_points: roundPlaces(value.average_climb_points, 2),
|
||||||
|
average_total_match_points: roundPlaces(value.average_total_match_points, 2),
|
||||||
|
matches_played: value.matches_played,
|
||||||
|
num_disables: value.num_disables,
|
||||||
|
num_flips: value.num_flips,
|
||||||
|
fouls: value.fouls,
|
||||||
|
fouls_tech: value.fouls_tech,
|
||||||
|
red_cards: value.red_cards,
|
||||||
|
yellow_cards: value.yellow_cards,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [columnDefs] = useState([
|
||||||
|
{ field: "id", headerName: "Team", width: 100, checkboxSelection: true, pinned: "left", sortable: true },
|
||||||
|
{ field: "average_total_match_points", headerName: "Avg Total Pts", width: 120, sortable: true },
|
||||||
|
{ field: "average_auto_points", headerName: "Avg Auto Pts", width: 120, sortable: true },
|
||||||
|
{ field: "average_teleop_hub_points", headerName: "Avg Teleop Hub Pts", width: 160, sortable: true },
|
||||||
|
{ field: "average_climb_points", headerName: "Avg Climb Pts", width: 120, sortable: true },
|
||||||
|
{ field: "yellow_cards", headerName: "Yellow Cards", width: 120, sortable: true },
|
||||||
|
{ field: "red_cards", headerName: "Red Cards", width: 100, sortable: true },
|
||||||
|
{ field: "fouls", headerName: "Fouls", width: 70, sortable: true },
|
||||||
|
{ field: "fouls_tech", headerName: "Tech Fouls", width: 100, sortable: true },
|
||||||
|
{ field: "num_disables", headerName: "Disables", width: 100, sortable: true },
|
||||||
|
{ field: "num_flips", headerName: "Flips", width: 80, sortable: true },
|
||||||
|
{ field: "matches_played", headerName: "Matches", width: 100, sortable: true },
|
||||||
|
{ field: "", headerName: "", width: 150, sortable: true },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [selectedTeams, setSelectedTeams] = useState([]);
|
||||||
|
|
||||||
|
const gridRef = useRef();
|
||||||
|
|
||||||
|
const onSelectionChanged = useCallback(() => {
|
||||||
|
let selectedRows = gridRef.current.api.getSelectedRows();
|
||||||
|
// var selectedRowsString = "";
|
||||||
|
// var maxToShow = 5;
|
||||||
|
let selectedTeams = [];
|
||||||
|
selectedRows.forEach(function (selectedRow, index) {
|
||||||
|
selectedTeams.push(selectedRow.id);
|
||||||
|
});
|
||||||
|
// if (selectedRows.length > maxToShow) {
|
||||||
|
// var othersCount = selectedRows.length - maxToShow;
|
||||||
|
// selectedRowsString += " and " + othersCount + " other" + (othersCount !== 1 ? "s" : "");
|
||||||
|
// }
|
||||||
|
// document.querySelector("#selectedRows").innerHTML = selectedRowsString;
|
||||||
|
setSelectedTeams(selectedTeams);
|
||||||
|
}, [selectedTeams]);
|
||||||
|
|
||||||
// const { processedDataBucket, setProcessedDataBucket } = pdbCtx;
|
// const { processedDataBucket, setProcessedDataBucket } = pdbCtx;
|
||||||
// console.log(pdbCtx);
|
// console.log(pdbCtx);
|
||||||
|
|
||||||
//format data for the data grid
|
//format data for the data grid
|
||||||
let grid_data = [];
|
|
||||||
//turns the values of the key value pairs in the list into an array
|
|
||||||
console.log(processedDataBucket.teamData);
|
|
||||||
let team_data_array = Object.values(processedDataBucket.teamData);
|
|
||||||
// let team_data_array = Array.from(processedDataBucket.teamData);
|
|
||||||
const roundPlaces = (n, d) => Math.round(n * Math.pow(10, d)) / Math.pow(10, d);
|
|
||||||
// team_data_array.forEach((value, index, array) => {
|
|
||||||
for (const property in processedDataBucket.teamData) {
|
|
||||||
console.log(property);
|
|
||||||
let value = processedDataBucket.teamData[property];
|
|
||||||
console.log(value);
|
|
||||||
grid_data.push({
|
|
||||||
id: value.team_number,
|
|
||||||
average_auto_points: roundPlaces(value.average_auto_points, 2),
|
|
||||||
average_teleop_hub_points: roundPlaces(value.average_teleop_hub_points, 2),
|
|
||||||
average_climb_points: roundPlaces(value.average_climb_points, 2),
|
|
||||||
average_total_match_points: roundPlaces(value.average_total_match_points, 2),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// });
|
// });
|
||||||
console.log(grid_data);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{/* <Pie data={makePieChartData(pdb, 4388)} /> */}
|
{/* <Pie data={makePieChartData(pdb, 4388)} /> */}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
height: "600px",
|
height: "400px",
|
||||||
m: 2,
|
m: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DataGrid
|
{/* <h2>{JSON.stringify(selectedTeams)}</h2> */}
|
||||||
rows={grid_data}
|
<div className="ag-theme-alpine-dark" style={{ height: 400, width: "100%" }}>
|
||||||
columns={[
|
<AgGridReact ref={gridRef} rowData={rowData} columnDefs={columnDefs} rowSelection={"multiple"} rowMultiSelectWithClick={true} onSelectionChanged={onSelectionChanged}></AgGridReact>
|
||||||
{ field: "id", headerName: "Team", width: 100 },
|
</div>
|
||||||
{ field: "average_total_match_points", headerName: "Avg Total Pts", width: 150 },
|
|
||||||
{ field: "average_auto_points", headerName: "Avg Auto Pts", width: 150 },
|
|
||||||
{ field: "average_teleop_hub_points", headerName: "Avg Teleop Hub Pts", width: 190 },
|
|
||||||
{ field: "average_climb_points", headerName: "Avg Climb Pts", width: 150 },
|
|
||||||
// { field: "matched_played", headerName: "Matches", width: 100 },
|
|
||||||
]}
|
|
||||||
checkboxSelection
|
|
||||||
pageSize={15}
|
|
||||||
rowsPerPageOptions={[15]}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
{/* <Chart
|
<AnalyticsPanel selectedTeams={selectedTeams} />
|
||||||
options={{
|
|
||||||
chart: {
|
|
||||||
width: 384,
|
|
||||||
type: "pie",
|
|
||||||
},
|
|
||||||
labels: ["None", "Low", "Mid", "High", "Transversal"],
|
|
||||||
}}
|
|
||||||
series={pdb.teamData[4388].climb_counts}
|
|
||||||
type="pie"
|
|
||||||
width={380}
|
|
||||||
/> */}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
//}}
|
//}}
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import React from "react";
|
import React, { useCallback } from "react";
|
||||||
import { useLocalDb } from "../DbContext";
|
import { useLocalDb, useRemoteDb } from "../DbContext";
|
||||||
import "./InputPage.css";
|
import "./InputPage.css";
|
||||||
import { Formik, FastField, Form } from "formik";
|
import { Formik, FastField, Form } from "formik";
|
||||||
import InputNumberField from "../components/InputNumberField.jsx";
|
import InputNumberField from "../components/InputNumberField.jsx";
|
||||||
import { TextField, Button, Grid, FormRow, Divider, Checkbox, Radio, FormControlLabel, FormControl, FormLabel, RadioGroup, IconButton, InputAdornment, Box } from "@mui/material";
|
import { TextField, Button, Grid, FormRow, Divider, Checkbox, Radio, FormControlLabel, FormControl, FormLabel, RadioGroup, IconButton, InputAdornment, Box } from "@mui/material";
|
||||||
|
import { useProcessedDataBucket } from "../ProcessedDataBucketContext";
|
||||||
|
import { getProcessedDataBucket, updateProcessedDataBucket } from "../ProcessedDataBucket";
|
||||||
|
|
||||||
const InputPage = () => {
|
const InputPage = () => {
|
||||||
const localdb = useLocalDb();
|
let { localdb, setLocaldb } = useLocalDb();
|
||||||
|
let { remotedb, setRemotedb } = useRemoteDb();
|
||||||
|
const { processedDataBucket, setProcessedDataBucket } = useProcessedDataBucket();
|
||||||
|
|
||||||
let panel_sx = {
|
let panel_sx = {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: { xs: "column", sm: "row" },
|
flexDirection: { xs: "column", sm: "row" },
|
||||||
@@ -20,6 +25,36 @@ const InputPage = () => {
|
|||||||
borderRadius: "10px",
|
borderRadius: "10px",
|
||||||
boxShadow: 7,
|
boxShadow: 7,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onSubmit = useCallback(
|
||||||
|
(values, { setSubmitting, resetForm }) => {
|
||||||
|
// setTimeout(() => {
|
||||||
|
localdb
|
||||||
|
.put({
|
||||||
|
// _id: new Date().toISOString(),
|
||||||
|
_id: "match_" + values.match_number + "_team_" + values.team_number,
|
||||||
|
...values,
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
alert("Input Saved Successfully!");
|
||||||
|
console.log(result);
|
||||||
|
console.log(localdb);
|
||||||
|
localdb.replicate.to(remotedb, {
|
||||||
|
live: true,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log("Failed To Save Input!");
|
||||||
|
alert(err);
|
||||||
|
});
|
||||||
|
// alert(JSON.stringify(values, null, 2));
|
||||||
|
// resetForm(); //Hah tobad
|
||||||
|
setSubmitting(false);
|
||||||
|
// }, 400);
|
||||||
|
updateProcessedDataBucket(localdb, setProcessedDataBucket);
|
||||||
|
},
|
||||||
|
[localdb, remotedb, setProcessedDataBucket, updateProcessedDataBucket]
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<br />
|
<br />
|
||||||
@@ -47,27 +82,7 @@ const InputPage = () => {
|
|||||||
disabled: false,
|
disabled: false,
|
||||||
}}
|
}}
|
||||||
validateOnChange="false"
|
validateOnChange="false"
|
||||||
onSubmit={(values, { setSubmitting, resetForm }) => {
|
onSubmit={onSubmit}
|
||||||
setTimeout(() => {
|
|
||||||
localdb
|
|
||||||
.put({
|
|
||||||
_id: new Date().toISOString(),
|
|
||||||
...values,
|
|
||||||
})
|
|
||||||
.then((result) => {
|
|
||||||
alert("Input Saved Successfully!");
|
|
||||||
console.log(result);
|
|
||||||
console.log(localdb);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log("Failed To Save Input!");
|
|
||||||
alert(err);
|
|
||||||
});
|
|
||||||
// alert(JSON.stringify(values, null, 2));
|
|
||||||
// resetForm(); //Hah tobad
|
|
||||||
setSubmitting(false);
|
|
||||||
}, 400);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{({ values, setValues, errors, touched, handleChange, handleBlur, handleSubmit, isSubmitting }) => (
|
{({ values, setValues, errors, touched, handleChange, handleBlur, handleSubmit, isSubmitting }) => (
|
||||||
<Form>
|
<Form>
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import "./WelcomePage.css";
|
import "./WelcomePage.css";
|
||||||
import "../App.css";
|
import "../App.css";
|
||||||
|
import DbChooser from "../components/DbChooser";
|
||||||
|
import { Box } from "@mui/material";
|
||||||
|
|
||||||
const WelcomePage = () => {
|
const WelcomePage = () => {
|
||||||
return (
|
return (
|
||||||
<div className="welcome">
|
<div className="welcome">
|
||||||
<h1>Welcome to Ridgebotics Scouting Web Application 2022</h1>
|
<Box sx={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
|
||||||
{/* <img src="/WelcomePageImage.webp" /> */}
|
<h1>Welcome to Ridgebotics Scouting Web Application 2022</h1>
|
||||||
<img src="/picgoeshard.jpg" />
|
{/* <img src="/WelcomePageImage.webp" /> */}
|
||||||
|
{/* <img src="/picgoeshard.jpg" /> */}
|
||||||
|
<DbChooser />
|
||||||
|
</Box>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,16 +8,14 @@ export function updateProcessedDataBucket(db, setProcessedDataBucket) {
|
|||||||
let teamData = {};
|
let teamData = {};
|
||||||
let matchData = {};
|
let matchData = {};
|
||||||
|
|
||||||
// console.log(result);
|
|
||||||
result.rows.forEach((dbentry) => {
|
result.rows.forEach((dbentry) => {
|
||||||
let doc = dbentry.doc;
|
let doc = dbentry.doc;
|
||||||
// console.log(doc);
|
|
||||||
|
|
||||||
//if there's no processed data on a team yet, create a default data entry
|
//if there's no processed data on a team yet, create a default data entry
|
||||||
if (typeof teamData[doc.team_number] === "undefined") {
|
if (typeof teamData[doc.team_number] === "undefined") {
|
||||||
teamData[doc.team_number] = {
|
teamData[doc.team_number] = {
|
||||||
team_number: doc.team_number,
|
team_number: doc.team_number,
|
||||||
matched_played: 0,
|
matches_played: 0,
|
||||||
data_sets: {
|
data_sets: {
|
||||||
upper_hub_auto: [],
|
upper_hub_auto: [],
|
||||||
lower_hub_auto: [],
|
lower_hub_auto: [],
|
||||||
@@ -44,7 +42,7 @@ export function updateProcessedDataBucket(db, setProcessedDataBucket) {
|
|||||||
|
|
||||||
//add this game's data to the respective team data:
|
//add this game's data to the respective team data:
|
||||||
let thisTeamData = teamData[doc.team_number];
|
let thisTeamData = teamData[doc.team_number];
|
||||||
thisTeamData.matched_played++;
|
thisTeamData.matches_played++;
|
||||||
|
|
||||||
let auto_points = (parseInt(doc.taxi_auto) ? 2 : 0) + parseInt(doc.upper_hub_auto) * 4 + parseInt(doc.lower_hub_auto) * 2;
|
let auto_points = (parseInt(doc.taxi_auto) ? 2 : 0) + parseInt(doc.upper_hub_auto) * 4 + parseInt(doc.lower_hub_auto) * 2;
|
||||||
let teleop_hub_points = parseInt(doc.upper_hub_teleop) * 2 + parseInt(doc.lower_hub_teleop) * 1;
|
let teleop_hub_points = parseInt(doc.upper_hub_teleop) * 2 + parseInt(doc.lower_hub_teleop) * 1;
|
||||||
@@ -74,10 +72,10 @@ export function updateProcessedDataBucket(db, setProcessedDataBucket) {
|
|||||||
//sum of all points in the match points data set for this team
|
//sum of all points in the match points data set for this team
|
||||||
//function for getting the sum of an array, use in reduce function of array
|
//function for getting the sum of an array, use in reduce function of array
|
||||||
const sum = (accum, current) => accum + current;
|
const sum = (accum, current) => accum + current;
|
||||||
thisTeamData.average_auto_points = thisTeamData.data_sets.auto_points.reduce(sum, 0) / thisTeamData.matched_played;
|
thisTeamData.average_auto_points = thisTeamData.data_sets.auto_points.reduce(sum, 0) / thisTeamData.matches_played;
|
||||||
thisTeamData.average_teleop_hub_points = thisTeamData.data_sets.teleop_hub_points.reduce(sum, 0) / thisTeamData.matched_played;
|
thisTeamData.average_teleop_hub_points = thisTeamData.data_sets.teleop_hub_points.reduce(sum, 0) / thisTeamData.matches_played;
|
||||||
thisTeamData.average_climb_points = thisTeamData.data_sets.climb_points.reduce(sum, 0) / thisTeamData.matched_played;
|
thisTeamData.average_climb_points = thisTeamData.data_sets.climb_points.reduce(sum, 0) / thisTeamData.matches_played;
|
||||||
thisTeamData.average_total_match_points = thisTeamData.data_sets.total_match_points.reduce(sum, 0) / thisTeamData.matched_played;
|
thisTeamData.average_total_match_points = thisTeamData.data_sets.total_match_points.reduce(sum, 0) / thisTeamData.matches_played;
|
||||||
});
|
});
|
||||||
setProcessedDataBucket({ teamData: teamData, matchData: matchData });
|
setProcessedDataBucket({ teamData: teamData, matchData: matchData });
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import React, { useCallback } from "react";
|
||||||
|
import { Box, InputLabel, MenuItem, FormControl, Select } from "@mui/material";
|
||||||
|
import { setDatabaseName, useLocalDb, useRemoteDb } from "../DbContext";
|
||||||
|
import { useProcessedDataBucket } from "../ProcessedDataBucketContext";
|
||||||
|
|
||||||
|
const DbChooser = (props) => {
|
||||||
|
const { localdb, setLocaldb } = useLocalDb();
|
||||||
|
const { remotedb, setRemotedb } = useRemoteDb();
|
||||||
|
const { processedDataBucket, setProcessedDataBucket } = useProcessedDataBucket();
|
||||||
|
|
||||||
|
const [dbname, setDbName] = React.useState(localdb.name);
|
||||||
|
|
||||||
|
const handleChange = useCallback((event) => {
|
||||||
|
console.log(event.target.value);
|
||||||
|
// setAge(event.target.value);
|
||||||
|
setDbName(event.target.value);
|
||||||
|
setDatabaseName(event.target.value, setLocaldb, setRemotedb, setProcessedDataBucket);
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Box sx={{ width: 400 }}>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel>Database</InputLabel>
|
||||||
|
<Select value={dbname} label="Database Name" onChange={handleChange}>
|
||||||
|
<MenuItem value={"denver_practice"}>Denver Practice Matches</MenuItem>
|
||||||
|
<MenuItem value={"denver_fr"}>Denver For Real</MenuItem>
|
||||||
|
{/* <MenuItem value={"utah_practice"}>Utah Practice Matches</MenuItem>
|
||||||
|
<MenuItem value={"utah_fr"}>Utah For Real</MenuItem> */}
|
||||||
|
<MenuItem value={"testdata"}>Test Data</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DbChooser;
|
||||||
@@ -4,5 +4,5 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: 2;
|
z-index: 50;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
.toolbar_logo a {
|
.toolbar_logo a {
|
||||||
color: white;
|
color: white;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 2rem;
|
font-size: 1.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar_spacer {
|
.toolbar_spacer {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const Toolbar = props => {
|
|||||||
<header className="toolbar">
|
<header className="toolbar">
|
||||||
<nav className="toolbar_navigation">
|
<nav className="toolbar_navigation">
|
||||||
<DrawerButton click={props.drawerClickHandler} />
|
<DrawerButton click={props.drawerClickHandler} />
|
||||||
<div className = "toolbar_logo"><Link to="/">Ridgebotics Scouting <3</Link></div>
|
<div className = "toolbar_logo"><Link to="/">Ridgebotics Scouting 💙</Link></div>
|
||||||
<div className = "toolbar_spacer" />
|
<div className = "toolbar_spacer" />
|
||||||
<div className = "toolbar_items">
|
<div className = "toolbar_items">
|
||||||
<PagesList />
|
<PagesList />
|
||||||
|
|||||||
Reference in New Issue
Block a user