Initial version of server extension cards

TODO: Extension DataTables mapping
This commit is contained in:
Aurora Lahtela 2022-08-23 22:01:22 +03:00
parent ce81179388
commit ea16adb13d
8 changed files with 195 additions and 30 deletions

View File

@ -23,6 +23,7 @@ import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.exception.BadRequestException;
import com.djrapitops.plan.delivery.web.resolver.exception.NotFoundException;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
import com.djrapitops.plan.delivery.webserver.cache.AsyncJSONResolverService;
import com.djrapitops.plan.delivery.webserver.cache.DataID;
import com.djrapitops.plan.delivery.webserver.cache.JSONStorage;
@ -66,7 +67,8 @@ public class ExtensionJSONResolver implements Resolver {
@Override
public boolean canAccess(Request request) {
return false;
WebUser permissions = request.getUser().orElse(new WebUser(""));
return permissions.hasPermission("page.server") || permissions.hasPermission("page.network");
}
@GET

View File

@ -30,6 +30,8 @@ import AllPlayers from "./views/players/AllPlayers";
import ServerGeolocations from "./views/server/ServerGeolocations";
import LoginPage from "./views/layout/LoginPage";
import ServerPerformance from "./views/server/ServerPerformance";
import ServerPluginData from "./views/server/ServerPluginData";
import ServerWidePluginData from "./views/server/ServerWidePluginData";
const SwaggerView = React.lazy(() => import("./views/SwaggerView"));
@ -89,7 +91,8 @@ function App() {
<Route path="players" element={<ServerPlayers/>}/>
<Route path="geolocations" element={<ServerGeolocations/>}/>
<Route path="performance" element={<ServerPerformance/>}/>
<Route path="plugins-overview" element={<></>}/>
<Route path="plugins-overview" element={<ServerPluginData/>}/>
<Route path="plugins/:plugin" element={<ServerWidePluginData/>}/>
<Route path="*" element={<ErrorView error={{
message: 'Unknown tab address, please correct the address',
title: 'No such tab',

View File

@ -4,6 +4,7 @@ import ExtensionIcon from "./ExtensionIcon";
import Datapoint from "../Datapoint";
import Masonry from 'masonry-layout'
import {useTheme} from "../../hooks/themeHook";
import {useTranslation} from "react-i18next";
export const ExtensionCardWrapper = ({extension, children}) => {
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
@ -51,17 +52,29 @@ const ExtensionValue = ({data}) => {
/>);
}
const ExtensionValues = ({tab}) => (
<Card.Body>
{tab.values.map((data, i) => {
return (<ExtensionValue key={i} data={data}/>);
}
)}
</Card.Body>
)
const ExtensionValues = ({tab}) => {
return (
<>
{Boolean(tab.values.length) && <Card.Body>
{tab.values.map((data, i) => {
return (<ExtensionValue key={i} data={data}/>);
}
)}
</Card.Body>}
</>
)
}
const ExtensionTable = ({table}) => {
const {nightModeEnabled} = useTheme();
const {t} = useTranslation();
const columns = table.table.rows.length ? table.table.rows.map((row, i) => <tr key={i}>{row.map((value, j) => <td
key={i + '' + j}>{value}</td>)}</tr>) :
<tr>{table.table.columns.map((column, i) =>
<td key={i}>{i === 0 ? t('generic.noData') : '-'}</td>)}
</tr>
return (
<table className={"table table-striped" + (nightModeEnabled ? " table-dark" : '')}>
<thead className={table.tableColorClass}>
@ -72,11 +85,10 @@ const ExtensionTable = ({table}) => {
</tr>
</thead>
<tbody>
{table.table.rows.map((row, i) => <tr key={i}>{row.map((value, j) => <td
key={i + '' + j}>{value}</td>)}</tr>)}
{columns}
</tbody>
</table>
);
)
}
const ExtensionTables = ({tab}) => {
@ -113,7 +125,7 @@ const ExtensionCard = ({extension}) => {
{extension.onlyGenericTab ? '' :
extension.tabs.map((tab, i) => <li key={i} role="presentation" className="nav-item col-black">
<button className={"nav-link col-black"
+ (openTabIndex === i ? ' active' : '')} onClick={() => toggleTabIndex(i)}>
+ (openTabIndex === i ? ' active' : '')} onClick={() => toggleTabIndex(i)}>
<ExtensionIcon icon={tab.tabInformation.icon}/> {tab.tabInformation.tabName}
</button>
</li>)

View File

@ -42,6 +42,12 @@ export const fetchPerformanceOverview = async (identifier) => {
return doGetRequest(url);
}
export const fetchExtensionData = async (identifier) => {
const timestamp = Date.now();
const url = `/v1/extensionData?server=${identifier}&timestamp=${timestamp}`;
return doGetRequest(url);
}
export const fetchSessions = async (identifier) => {
const timestamp = Date.now();
const url = `/v1/sessions?server=${identifier}&timestamp=${timestamp}`;

View File

@ -1,6 +1,6 @@
import React, {useEffect, useState} from "react";
import {useTranslation} from "react-i18next";
import {Outlet, useParams} from "react-router-dom";
import {Outlet, useOutletContext, useParams} from "react-router-dom";
import {useNavigation} from "../../hooks/navigationHook";
import {
faCampground,
@ -25,14 +25,22 @@ import ErrorPage from "./ErrorPage";
import {SwitchTransition} from "react-transition-group";
import MainPageRedirect from "../../components/navigation/MainPageRedirect";
import {useDataRequest} from "../../hooks/dataFetchHook";
import {fetchServerIdentity} from "../../service/serverService";
import {fetchExtensionData, fetchServerIdentity} from "../../service/serverService";
import ExtensionIcon from "../../components/extensions/ExtensionIcon";
const ServerPage = () => {
const {t, i18n} = useTranslation();
const {identifier} = useParams();
const {isProxy, serverName} = useMetadata();
const {data: serverIdentity, loadingError: identityLoadingError} = useDataRequest(fetchServerIdentity, [identifier])
const {
data: serverIdentity,
loadingError: identityLoadingError
} = useDataRequest(fetchServerIdentity, [identifier]);
const {
data: extensionData,
loadingError: extensionDataLoadingError
} = useDataRequest(fetchExtensionData, [identifier]);
const [error] = useState(undefined);
const [sidebarItems, setSidebarItems] = useState([]);
@ -76,17 +84,30 @@ const ServerPage = () => {
{name: 'html.label.performance', icon: faCogs, href: "performance"},
{},
{name: 'html.label.plugins'},
{name: 'html.label.pluginsOverview', icon: faCubes, href: "plugins-overview"},
{},
{name: 'html.label.links'},
{name: 'html.label.query', icon: faSearch, href: "/query"},
]
// TODO Extensions
if (extensionData) {
items.push({name: 'html.label.pluginsOverview', icon: faCubes, href: "plugins-overview"})
extensionData.extensions.filter(extension => extension.wide)
.map(extension => extension.extensionInformation)
.map(info => {
return {
name: info.pluginName,
icon: <ExtensionIcon icon={info.icon}/>,
href: `plugins/${encodeURIComponent(info.pluginName)}`
}
}).forEach(item => items.push(item))
}
items.push(
{},
{name: 'html.label.links'},
{name: 'html.label.query', icon: faSearch, href: "/query"}
);
setSidebarItems(items);
window.document.title = `Plan | Server Analysis`;
}, [t, i18n])
}, [t, i18n, extensionData])
const {authRequired, loggedIn, user} = useAuth();
if (authRequired && !loggedIn) return <MainPageRedirect/>
@ -113,6 +134,10 @@ const ServerPage = () => {
error={{title: t('html.error.404NotFound'), message: t('html.error.serverNotSeen')}}/>
return <ErrorPage error={identityLoadingError}/>
}
if (extensionDataLoadingError) {
return <ErrorPage error={extensionDataLoadingError}/>
}
if (!extensionData) return <></>
return (
<>
@ -123,7 +148,7 @@ const ServerPage = () => {
<div id="content" style={{display: 'flex'}}>
<main className="container-fluid mt-4">
<SwitchTransition>
<Outlet context={{}}/>
<Outlet context={extensionData}/>
</SwitchTransition>
</main>
<aside>
@ -135,4 +160,8 @@ const ServerPage = () => {
)
}
export const useServer = () => {
return useOutletContext();
}
export default ServerPage;

View File

@ -1,6 +1,6 @@
import React, {useEffect} from "react";
import ExtensionCard, {ExtensionCardWrapper} from "../../components/extensions/ExtensionCard";
import {Card, Row} from "react-bootstrap-v5";
import {Card, Col, Row} from "react-bootstrap-v5";
import {useParams} from "react-router-dom";
import Masonry from "masonry-layout";
import {usePlayer} from "../layout/PlayerPage";
@ -14,6 +14,7 @@ const PlayerPluginData = () => {
useEffect(() => {
const masonryRow = document.getElementById('extension-masonry-row');
if (!masonryRow) return;
let masonry = Masonry.data(masonryRow);
if (!masonry) {
masonry = new Masonry(masonryRow, {"percentPosition": true, "itemSelector": ".extension-wrapper"});
@ -28,11 +29,13 @@ const PlayerPluginData = () => {
<LoadIn>
<section className="player_plugin_data">
<Row style={{overflowY: 'hidden'}}>
<Card>
<Card.Body>
<p>No Extension data for {serverName}</p>
</Card.Body>
</Card>
<Col md={12}>
<Card>
<Card.Body>
<p>No Extension data for {serverName}</p>
</Card.Body>
</Card>
</Col>
</Row>
</section>
</LoadIn>

View File

@ -0,0 +1,61 @@
import React, {useEffect} from 'react';
import {useServer} from "../layout/ServerPage";
import Masonry from "masonry-layout";
import LoadIn from "../../components/animation/LoadIn";
import {Card, Col, Row} from "react-bootstrap-v5";
import ExtensionCard, {ExtensionCardWrapper} from "../../components/extensions/ExtensionCard";
const ServerPluginData = () => {
const extensionData = useServer();
console.log(extensionData);
const extensions = extensionData ? extensionData.extensions.filter(extension => !extension.wide) : [];
useEffect(() => {
const masonryRow = document.getElementById('extension-masonry-row');
if (!masonryRow) return;
let masonry = Masonry.data(masonryRow);
if (!masonry) {
masonry = new Masonry(masonryRow, {"percentPosition": true, "itemSelector": ".extension-wrapper"});
}
return () => {
if (masonry.element) masonry.destroy();
}
}, [])
if (!extensions || !extensions.length) {
return (
<LoadIn>
<section className="server_plugin_data">
<Row style={{overflowY: 'hidden'}}>
<Col md={12}>
<Card>
<Card.Body>
<p>No Extension data</p>
</Card.Body>
</Card>
</Col>
</Row>
</section>
</LoadIn>
)
}
return (
<LoadIn>
<section className="server_plugin_data">
<Row id="extension-masonry-row"
data-masonry='{"percentPosition": true, "itemSelector": ".extension-wrapper"}'
style={{overflowY: 'hidden'}}>
{extensions.map((extension, i) =>
<ExtensionCardWrapper key={'ext-' + i} extension={extension}>
<ExtensionCard extension={extension}/>
</ExtensionCardWrapper>
)}
</Row>
</section>
</LoadIn>
)
};
export default ServerPluginData

View File

@ -0,0 +1,49 @@
import React from 'react';
import {useServer} from "../layout/ServerPage";
import ErrorView from "../ErrorView";
import LoadIn from "../../components/animation/LoadIn";
import {Card, Col, Row} from "react-bootstrap-v5";
import ExtensionCard from "../../components/extensions/ExtensionCard";
import {useParams} from "react-router-dom";
const ServerWidePluginData = () => {
const {plugin} = useParams();
const {extensionData, extensionDataLoadingError} = useServer();
if (extensionDataLoadingError) return <ErrorView error={extensionDataLoadingError}/>;
const extension = extensionData?.find(extension => extension.extensionInformation.pluginName === plugin)
if (!extension) {
return (
<LoadIn>
<section className="server_plugin_data">
<Row style={{overflowY: 'hidden'}}>
<Col md={12}>
<Card>
<Card.Body>
<p>No Extension data</p>
</Card.Body>
</Card>
</Col>
</Row>
</section>
</LoadIn>
)
}
return (
<LoadIn>
<section className="server_plugin_data">
<Row id={"extension-" + extension.extensionInformation.pluginName}
style={{overflowY: 'hidden'}}>
<Col md={12}>
<ExtensionCard extension={extension}/>
</Col>
</Row>
</section>
</LoadIn>
)
};
export default ServerWidePluginData