updated ui

This commit is contained in:
ACoolName 2024-05-21 23:53:23 +03:00
parent c7667ec5b5
commit 2ebcf0a8c4
11 changed files with 10609 additions and 10723 deletions

20976
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,8 @@
{ {
name: "a-cool-games-manager", "name": "a-cool-games-manager",
version: "0.1.0", "version": "0.1.0",
private: true, "private": true,
dependencies: { "dependencies": {
"@rjsf/core": "^5.14.2", "@rjsf/core": "^5.14.2",
"@rjsf/mui": "^5.14.2", "@rjsf/mui": "^5.14.2",
"@rjsf/utils": "^5.14.2", "@rjsf/utils": "^5.14.2",
@ -16,39 +16,39 @@
"@types/react": "^18.2.38", "@types/react": "^18.2.38",
"@types/react-dom": "^18.2.16", "@types/react-dom": "^18.2.16",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
axios: "^1.6.2", "axios": "^1.6.2",
dotenv: "^16.3.1", "dotenv": "^16.3.1",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
jsonpath: "^1.1.1", "jsonpath": "^1.1.1",
react: "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-jsonschema-form": "^1.8.1", "react-jsonschema-form": "^1.8.1",
"react-router-dom": "^6.19.0", "react-router-dom": "^6.19.0",
"react-routes": "^0.2.6", "react-routes": "^0.2.6",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
serve: "^14.2.1", "serve": "^14.2.1",
typescript: "^4.9.5", "typescript": "^4.9.5",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
scripts: { "scripts": {
start: "react-scripts start", "start": "react-scripts start",
build: "/usr/bin/env react-scripts build", "build": "/usr/bin/env react-scripts build",
test: "react-scripts test", "test": "react-scripts test",
eject: "react-scripts eject" "eject": "react-scripts eject"
}, },
eslintConfig: { "eslintConfig": {
extends: [ "extends": [
"react-app", "react-app",
"react-app/jest" "react-app/jest"
] ]
}, },
browserslist: { "browserslist": {
production: [ "production": [
">0.2%", ">0.2%",
"not dead", "not dead",
"not op_mini all" "not op_mini all"
], ],
development: [ "development": [
"last 1 chrome version", "last 1 chrome version",
"last 1 firefox version", "last 1 firefox version",
"last 1 safari version" "last 1 safari version"

View File

@ -1,25 +1,25 @@
{ {
short_name: "React App", "short_name": "React App",
name: "Create React App Sample", "name": "Create React App Sample",
icons: [ "icons": [
{ {
src: "favicon.ico", "src": "favicon.ico",
sizes: "64x64 32x32 24x24 16x16", "sizes": "64x64 32x32 24x24 16x16",
type: "image/x-icon" "type": "image/x-icon"
}, },
{ {
src: "logo192.png", "src": "logo192.png",
type: "image/png", "type": "image/png",
sizes: "192x192" "sizes": "192x192"
}, },
{ {
src: "logo512.png", "src": "logo512.png",
type: "image/png", "type": "image/png",
sizes: "512x512" "sizes": "512x512"
} }
], ],
start_url: ".", "start_url": ".",
display: "standalone", "display": "standalone",
theme_color: "#000000", "theme_color": "#000000",
background_color: "#ffffff" "background_color": "#ffffff"
} }

View File

@ -1,7 +1,7 @@
import { Box, Paper, ThemeProvider, List, ListItem, ListItemButton, ListItemText, SwipeableDrawer, ListItemIcon, IconButton, AppBar, Toolbar, PaletteMode, createTheme, useMediaQuery, useTheme } from "@mui/material"; import { Box, Paper, ThemeProvider, List, ListItem, ListItemButton, ListItemText, SwipeableDrawer, ListItemIcon, IconButton, AppBar, Toolbar, PaletteMode, createTheme, useMediaQuery, useTheme } from "@mui/material";
import React, { Dispatch, ReactNode, useState } from "react"; import React, { Dispatch, ReactNode } from "react";
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import { ApiWrapper, getDesignTokens, GlobalOpenApi, GlobalUserInfo } from "./common"; import { ApiWrapper, getDesignTokens, GlobalUserInfo } from "./common";
import { LoginPage } from "./login"; import { LoginPage } from "./login";
import ServersBoard from "./servers"; import ServersBoard from "./servers";
import MenuIcon from '@mui/icons-material/Menu'; import MenuIcon from '@mui/icons-material/Menu';
@ -121,7 +121,6 @@ export default function App() {
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<BrowserRouter> <BrowserRouter>
<ApiWrapper> <ApiWrapper>
<GlobalOpenApi>
<GlobalUserInfo> <GlobalUserInfo>
<Menu setMode={setMode}> <Menu setMode={setMode}>
<Routes> <Routes>
@ -134,7 +133,6 @@ export default function App() {
</Routes> </Routes>
</Menu> </Menu>
</GlobalUserInfo> </GlobalUserInfo>
</GlobalOpenApi>
</ApiWrapper> </ApiWrapper>
</BrowserRouter> </BrowserRouter>
</ThemeProvider> </ThemeProvider>

View File

@ -596,3 +596,27 @@ export const SERVER_ACTIONS: ActionInfo[] = [
response_action: "Ignore" response_action: "Ignore"
} }
] ]
export const CREATE_SERVER_ACTION: ActionInfo = {
name: "Create Server",
args: {
"$ref": "#/definitions/CreateServer",
definitions: definitions,
},
requestType: "post",
endpoint: "/servers",
permissions: Permission.Create,
response_action: "Ignore"
}
export const INVITE_USER_ACTION: ActionInfo = {
name: "Create Server",
args: {
"$ref": "#/definitions/InviteUserRequests",
definitions: definitions,
},
requestType: "post",
endpoint: "/users",
permissions: Permission.Admin,
response_action: "Ignore"
}

View File

@ -1,7 +1,7 @@
import { TableRow, TableCell, TableContainer, TableHead, Table, Button, Popover, Paper, TableBody, Chip, Link, ButtonGroup, Modal, Box } from "@mui/material" import { TableRow, TableCell, TableContainer, TableHead, Table, Button, Popover, Paper, TableBody, Chip, Link, ButtonGroup, Modal, Box } from "@mui/material"
import React, { useContext, Dispatch, useState, useEffect, createContext, Context } from "react" import React, { useContext, Dispatch, useState, useEffect, createContext, Context } from "react"
import Form from "@rjsf/mui" import Form from "@rjsf/mui"
import { apiAuthenticatedContext, useApiDoc, api, ActionItem, formModalStyle, DataTable, useActions, ActionInfo, ActionGroup, actionIdentifierContext } from "./common" import { apiAuthenticatedContext, api, DataTable, ActionInfo, ActionGroup, actionIdentifierContext } from "./common"
import validator from '@rjsf/validator-ajv8'; import validator from '@rjsf/validator-ajv8';
import { Browser, ServerInfo } from "./interfaces"; import { Browser, ServerInfo } from "./interfaces";
import { loadServers } from "./servers"; import { loadServers } from "./servers";
@ -85,7 +85,7 @@ export function BrowsersPage({ }) {
) )
} }
return <DataTable headers={['Browser Owner', 'Server Owner', 'Server Game', 'Game Version', 'Actions']}> return <DataTable headers={['Browser Owner', 'Server Owner', 'Server Game', 'Game Version', 'Actions']}>
<browserActionContext.Provider value={useActions(api, '/browsers/{browser_id}')}> <browserActionContext.Provider value={[]}>
{browserComponents} {browserComponents}
</browserActionContext.Provider> </browserActionContext.Provider>
</DataTable> </DataTable>

View File

@ -79,8 +79,6 @@ export const api: AxiosInstance = axios.create({
baseURL: API_URL, baseURL: API_URL,
}); });
console.log(process.env)
export function ApiWrapper(p: { children: ReactNode}) { export function ApiWrapper(p: { children: ReactNode}) {
const {children} = p const {children} = p
const token = localStorage.getItem('token') const token = localStorage.getItem('token')
@ -99,34 +97,6 @@ export function ApiWrapper(p: { children: ReactNode}) {
</apiAuthenticatedContext.Provider>) </apiAuthenticatedContext.Provider>)
} }
export function useApiDoc(api: AxiosInstance, path: string, method: 'get' | 'post' | 'delete') {
const [apiRecord, setApiRecord] = useState(null as OpenAPISchema|null)
api.get('/openapi.json').then(
(value) => {
setApiRecord(value.data)
}
)
if (apiRecord) {
const schema = apiRecord.paths[path][method]
if (!schema){
return
}
let formSchema: JSONSchema7 = { title: schema.summary, type: 'object', required: [], properties: {}, definitions: apiRecord.components, default: {} }
if (schema.requestBody) {
formSchema = schema.requestBody.content['application/json'].schema
formSchema.definitions = apiRecord.components
}
return JSON.parse(JSON.stringify(formSchema).replaceAll('#/components', '#/definitions'))
}
}
export interface ActionInfo { export interface ActionInfo {
name: string name: string
requestType: 'post' | 'get' | 'delete' requestType: 'post' | 'get' | 'delete'
@ -137,87 +107,6 @@ export interface ActionInfo {
} }
export function useActions(api: AxiosInstance, path_prefix: string): ActionInfo[] {
const openapi: OpenAPISchema|null = useContext(OpenApiContext)
let [actions, setActions]: [Record<string, ActionInfo[]>, Dispatch<Record<string, ActionInfo[]>>] = useState({})
console.log(actions)
if (actions[path_prefix] && actions[path_prefix].length > 0) {
return actions[path_prefix]
}
if (openapi === null) {
return []
}
let responseActions: ActionInfo[] = []
let paths = openapi.paths
for (let [path, request] of Object.entries(paths)) {
if (path.startsWith(path_prefix)) {
for (let [method, schema] of Object.entries(request)) {
let formSchema: JSONSchema7 = { title: schema.summary, type: 'object', required: [], properties: {}, definitions: openapi.components, default: {} }
if (schema.requestBody) {
formSchema = schema.requestBody.content['application/json'].schema
formSchema.definitions = openapi.components
}
responseActions.push(
{
name: schema.summary,
args: formSchema,
requestType: (method as 'get' | 'post' | 'delete'),
endpoint: path,
permissions: schema.permissions,
response_action: schema.api_response,
}
)
}
}
}
actions[path_prefix] = responseActions
setActions(actions)
if (actions[path_prefix]) {
return actions[path_prefix]
}
else {
return []
}
}
export function useAction(p: {path: string, method: 'post' | 'get' | 'delete'}): ActionInfo|undefined{
const apiDoc: OpenAPISchema|null = useContext(OpenApiContext)
const [action, setAction]: [ActionInfo|null, Dispatch<ActionInfo|null>] = useState(null as ActionInfo|null)
if (action != null){
return action
}
if (!apiDoc){
return
}
const schema = apiDoc.paths[p.path][p.method]
if (!schema){
return
}
let formSchema: JSONSchema7 = { title: schema.summary, type: 'object', required: [], properties: {}, definitions: apiDoc.components, default: {} }
if (schema.requestBody) {
formSchema = schema.requestBody.content['application/json'].schema
formSchema.definitions = apiDoc.components
}
const calculatedAction = {
name: schema.summary,
args: JSON.parse(JSON.stringify(formSchema).replaceAll('#/components', '#/definitions')),
requestType: p.method,
endpoint: p.path,
permissions: schema.permissions,
response_action: schema.api_response,
}
setAction(calculatedAction)
return calculatedAction
}
interface Options{ interface Options{
label: string label: string
const: string const: string
@ -253,6 +142,7 @@ function FetcherField(props: WidgetProps){
function isUserAllowed(user: User|null, action: ActionInfo): boolean{ function isUserAllowed(user: User|null, action: ActionInfo): boolean{
console.log({User: user, Action: action})
if (user === null){ if (user === null){
return false return false
} }
@ -468,19 +358,3 @@ export function GlobalUserInfo(props: {children: any}){
{props.children} {props.children}
</UserInfoContext.Provider> </UserInfoContext.Provider>
} }
export const OpenApiContext: Context<OpenAPISchema|null> = createContext(null as OpenAPISchema|null)
export function GlobalOpenApi(props: {children: any}){
const [openApi, setOpenApi]: [OpenAPISchema|null, Dispatch<OpenAPISchema|null>] = useState(null as OpenAPISchema|null)
if (openApi === null){
api.get('/openapi.json').then((event)=>{
setOpenApi(event.data)
}
)
}
return <OpenApiContext.Provider value={openApi}>
{props.children}
</OpenApiContext.Provider>
}

View File

@ -6,14 +6,14 @@ import Cookies from 'js-cookie'
export const fetchToken = async (username: string, password: string, remember: boolean) => { export const fetchToken = async (username: string, password: string, remember: boolean) => {
try { try {
const response = await api.post('/token', { const response = await api.post('/auth/signin', {
username: username, username: username,
password: password, password: password,
remember: remember, remember: remember,
}, { }, {
withCredentials: true withCredentials: true
}); });
return response.data.access_token; return response.data;
} catch (error) { } catch (error) {
console.error('Error fetching token:', error); console.error('Error fetching token:', error);
throw error; throw error;
@ -41,7 +41,7 @@ export function LoginPage(props: {}) {
fetchToken(username, password, Boolean(data.get('remember'))).then( fetchToken(username, password, Boolean(data.get('remember'))).then(
(token) => { (token) => {
api.defaults.headers.common.Authorization = `Bearer ${token}`; api.defaults.headers.common.Authorization = `Bearer ${token}`;
localStorage.setItem('token', token) Cookies.set('auth', token)
setApiAuthenticated(true) setApiAuthenticated(true)
}, },
(error) => { (error) => {

View File

@ -1,10 +1,10 @@
import { AxiosInstance } from "axios" import { AxiosInstance } from "axios"
import { ActionGroup, ActionInfo, DataTable, UserInfoContext, actionIdentifierContext, api, apiAuthenticatedContext, useAction, useActions } from "./common" import { ActionGroup, ActionInfo, DataTable, UserInfoContext, actionIdentifierContext, api, apiAuthenticatedContext} from "./common"
import React, { Context, Dispatch, createContext, useContext, useEffect, useState } from "react" import React, { Context, Dispatch, createContext, useContext, useEffect, useState } from "react"
import { TableRow, TableCell, Chip } from "@mui/material" import { TableRow, TableCell, Chip } from "@mui/material"
import { ImageInfo, ServerInfo } from "./interfaces" import { ImageInfo, ServerInfo } from "./interfaces"
import { JSONSchema7 } from "json-schema" import { JSONSchema7 } from "json-schema"
import { Permission, SERVER_ACTIONS } from "./actions" import { CREATE_SERVER_ACTION, Permission, SERVER_ACTIONS } from "./actions"
@ -24,7 +24,7 @@ function ServerItem(props: { server_info: ServerInfo }) {
const [serverPermissions, setServerPermissions] = useState(null as null|number) const [serverPermissions, setServerPermissions] = useState(null as null|number)
const user = useContext(UserInfoContext) const user = useContext(UserInfoContext)
let permissions = 0 let permissions = 0
console.log(user)
if (props.server_info.OwnerId === user?.Username){ if (props.server_info.OwnerId === user?.Username){
permissions |= Permission.Admin permissions |= Permission.Admin
}else{ }else{
@ -68,23 +68,6 @@ function ServerItem(props: { server_info: ServerInfo }) {
export default function ServersBoard() { export default function ServersBoard() {
const [servers, setServers]: [ServerInfo[], Dispatch<ServerInfo[]>] = useState([] as ServerInfo[]); const [servers, setServers]: [ServerInfo[], Dispatch<ServerInfo[]>] = useState([] as ServerInfo[]);
const [apiAuthenticated, setApiAuthenticated] = useContext(apiAuthenticatedContext) const [apiAuthenticated, setApiAuthenticated] = useContext(apiAuthenticatedContext)
const [images, setImages] = useState([] as ImageInfo[])
let schema: JSONSchema7 = {
properties: {
image_id: {
type: "string",
oneOf: images.map((value, index, array) => { return { const: value.Id, title: `${value.Name} ${value.Version}` } }),
title: "Image Id"
}
},
type: "object",
required: [
"image_id"
],
title: "CreateServer"
}
function handleServers() { function handleServers() {
if (!apiAuthenticated) { if (!apiAuthenticated) {
@ -123,23 +106,7 @@ export default function ServersBoard() {
return ( return (
<DataTable headers={['Nickname', 'Owner', 'Server', 'Version', 'Domain', 'Ports', 'Actions']} actionInfo={useAction({path: '/servers', method: 'post'})} actionHook={() => { <DataTable headers={['Nickname', 'Owner', 'Server', 'Version', 'Domain', 'Ports', 'Actions']} actionInfo={CREATE_SERVER_ACTION}>
api.get('/images').then((value) => {
setImages(value.data)
}).catch(
(error) => {
console.log('Failed to get images: ' + error);
if (error.response) {
if (error.response.status === 401) {
setApiAuthenticated(false)
}
else if (error.response.status === 403) {
setApiAuthenticated(false)
}
}
}
)
}}>
<serverActionsContext.Provider value={SERVER_ACTIONS}> <serverActionsContext.Provider value={SERVER_ACTIONS}>
{ {
servers.sort((s1: ServerInfo, s2: ServerInfo) => { return s1.Id < s2.Id ? 0 : 1 }).map( servers.sort((s1: ServerInfo, s2: ServerInfo) => { return s1.Id < s2.Id ? 0 : 1 }).map(

View File

@ -1,12 +1,35 @@
import { TableRow, TableCell, TableContainer, TableHead, Table, Button, Paper, TableBody, Chip, Modal, Box } from "@mui/material" import { TableRow, TableCell, Chip } from "@mui/material"
import React, { useContext, Dispatch, useState, useEffect, Context, createContext } from "react" import { useContext, Dispatch, useState, useEffect, Context, createContext } from "react"
import Form from "@rjsf/mui" import { apiAuthenticatedContext, api, ActionInfo, ActionGroup, actionIdentifierContext, DataTable } from "./common"
import { apiAuthenticatedContext, useApiDoc, api, ActionInfo, useActions, ActionGroup, actionIdentifierContext, ActionItem, DataTable, useAction } from "./common"
import validator from '@rjsf/validator-ajv8';
import { User } from './interfaces' import { User } from './interfaces'
import { INVITE_USER_ACTION, Permission } from "./actions";
const UserActionsContext: Context<ActionInfo[]> = createContext([] as ActionInfo[]) const UserActionsContext: Context<ActionInfo[]> = createContext([] as ActionInfo[])
function getPermissionStrings(permissions: number){
let strings = []
if (permissions & Permission.Admin){
strings.push("Admin")
}
if (permissions & Permission.Start){
strings.push("Start")
}
if (permissions & Permission.Stop){
strings.push("Stop")
}
if (permissions & Permission.Browse){
strings.push("Browse")
}
if (permissions & Permission.Create){
strings.push("Create")
}
if (permissions & Permission.Delete){
strings.push("Delete")
}
if (permissions & Permission.RunCommand){
strings.push("RunCommand")
}
return strings
}
function UserItem(p: { user: User }) { function UserItem(p: { user: User }) {
const user = p.user; const user = p.user;
@ -16,7 +39,7 @@ function UserItem(p: { user: User }) {
<TableRow> <TableRow>
<TableCell>{user.Username}</TableCell> <TableCell>{user.Username}</TableCell>
<TableCell>{user.Email}</TableCell> <TableCell>{user.Email}</TableCell>
<TableCell>{user.Permissions.map((value, index, array) => { return <Chip label={value} /> })}</TableCell> <TableCell>{getPermissionStrings(user.Permissions).map((value, index, array) => { return <Chip label={value} /> })}</TableCell>
<TableCell><ActionGroup actions={userActions} identifierSubstring="username" /></TableCell> <TableCell><ActionGroup actions={userActions} identifierSubstring="username" /></TableCell>
</TableRow> </TableRow>
) )
@ -26,7 +49,7 @@ export function UsersPage({ }) {
const [apiAuthenticated, setApiAuthenticated] = useContext(apiAuthenticatedContext) const [apiAuthenticated, setApiAuthenticated] = useContext(apiAuthenticatedContext)
const [users, setUsers]: [User[], Dispatch<User[]>] = useState([] as User[]) const [users, setUsers]: [User[], Dispatch<User[]>] = useState([] as User[])
const action: ActionInfo|undefined = useAction({path: '/users', method: 'post'}) const action: ActionInfo|undefined = INVITE_USER_ACTION
useEffect(() => { useEffect(() => {
if (!apiAuthenticated) { if (!apiAuthenticated) {
@ -73,7 +96,7 @@ export function UsersPage({ }) {
} }
return <DataTable headers={['Username', 'Email', 'Permissions', 'Actions']} actionInfo={action}> return <DataTable headers={['Username', 'Email', 'Permissions', 'Actions']} actionInfo={action}>
<UserActionsContext.Provider value={useActions(api, '/users/{username}')}> <UserActionsContext.Provider value={[]}>
{userComponents} {userComponents}
</UserActionsContext.Provider> </UserActionsContext.Provider>
</DataTable> </DataTable>

View File

@ -1,26 +1,26 @@
{ {
compilerOptions: { "compilerOptions": {
target: "es5", "target": "es5",
lib: [ "lib": [
"dom", "dom",
"dom.iterable", "dom.iterable",
"esnext" "esnext"
], ],
allowJs: true, "allowJs": true,
skipLibCheck: true, "skipLibCheck": true,
esModuleInterop: true, "esModuleInterop": true,
allowSyntheticDefaultImports: true, "allowSyntheticDefaultImports": true,
strict: true, "strict": true,
forceConsistentCasingInFileNames: true, "forceConsistentCasingInFileNames": true,
noFallthroughCasesInSwitch: true, "noFallthroughCasesInSwitch": true,
module: "esnext", "module": "esnext",
moduleResolution: "node", "moduleResolution": "node",
resolveJsonModule: true, "resolveJsonModule": true,
isolatedModules: true, "isolatedModules": true,
noEmit: true, "noEmit": true,
jsx: "react-jsx" "jsx": "react-jsx"
}, },
include: [ "include": [
"src" "src"
] ]
} }