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

View File

@ -1,25 +1,25 @@
{
short_name: "React App",
name: "Create React App Sample",
icons: [
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
src: "favicon.ico",
sizes: "64x64 32x32 24x24 16x16",
type: "image/x-icon"
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
src: "logo192.png",
type: "image/png",
sizes: "192x192"
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
src: "logo512.png",
type: "image/png",
sizes: "512x512"
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
start_url: ".",
display: "standalone",
theme_color: "#000000",
background_color: "#ffffff"
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"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 React, { Dispatch, ReactNode, useState } from "react";
import React, { Dispatch, ReactNode } from "react";
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 ServersBoard from "./servers";
import MenuIcon from '@mui/icons-material/Menu';
@ -121,7 +121,6 @@ export default function App() {
<ThemeProvider theme={theme}>
<BrowserRouter>
<ApiWrapper>
<GlobalOpenApi>
<GlobalUserInfo>
<Menu setMode={setMode}>
<Routes>
@ -134,7 +133,6 @@ export default function App() {
</Routes>
</Menu>
</GlobalUserInfo>
</GlobalOpenApi>
</ApiWrapper>
</BrowserRouter>
</ThemeProvider>

View File

@ -596,3 +596,27 @@ export const SERVER_ACTIONS: ActionInfo[] = [
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 React, { useContext, Dispatch, useState, useEffect, createContext, Context } from "react"
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 { Browser, ServerInfo } from "./interfaces";
import { loadServers } from "./servers";
@ -85,7 +85,7 @@ export function BrowsersPage({ }) {
)
}
return <DataTable headers={['Browser Owner', 'Server Owner', 'Server Game', 'Game Version', 'Actions']}>
<browserActionContext.Provider value={useActions(api, '/browsers/{browser_id}')}>
<browserActionContext.Provider value={[]}>
{browserComponents}
</browserActionContext.Provider>
</DataTable>

View File

@ -79,8 +79,6 @@ export const api: AxiosInstance = axios.create({
baseURL: API_URL,
});
console.log(process.env)
export function ApiWrapper(p: { children: ReactNode}) {
const {children} = p
const token = localStorage.getItem('token')
@ -99,34 +97,6 @@ export function ApiWrapper(p: { children: ReactNode}) {
</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 {
name: string
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{
label: string
const: string
@ -253,6 +142,7 @@ function FetcherField(props: WidgetProps){
function isUserAllowed(user: User|null, action: ActionInfo): boolean{
console.log({User: user, Action: action})
if (user === null){
return false
}
@ -468,19 +358,3 @@ export function GlobalUserInfo(props: {children: any}){
{props.children}
</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) => {
try {
const response = await api.post('/token', {
const response = await api.post('/auth/signin', {
username: username,
password: password,
remember: remember,
}, {
withCredentials: true
});
return response.data.access_token;
return response.data;
} catch (error) {
console.error('Error fetching token:', error);
throw error;
@ -41,7 +41,7 @@ export function LoginPage(props: {}) {
fetchToken(username, password, Boolean(data.get('remember'))).then(
(token) => {
api.defaults.headers.common.Authorization = `Bearer ${token}`;
localStorage.setItem('token', token)
Cookies.set('auth', token)
setApiAuthenticated(true)
},
(error) => {

View File

@ -1,10 +1,10 @@
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 { TableRow, TableCell, Chip } from "@mui/material"
import { ImageInfo, ServerInfo } from "./interfaces"
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 user = useContext(UserInfoContext)
let permissions = 0
console.log(user)
if (props.server_info.OwnerId === user?.Username){
permissions |= Permission.Admin
}else{
@ -68,23 +68,6 @@ function ServerItem(props: { server_info: ServerInfo }) {
export default function ServersBoard() {
const [servers, setServers]: [ServerInfo[], Dispatch<ServerInfo[]>] = useState([] as ServerInfo[]);
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() {
if (!apiAuthenticated) {
@ -123,23 +106,7 @@ export default function ServersBoard() {
return (
<DataTable headers={['Nickname', 'Owner', 'Server', 'Version', 'Domain', 'Ports', 'Actions']} actionInfo={useAction({path: '/servers', method: 'post'})} actionHook={() => {
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)
}
}
}
)
}}>
<DataTable headers={['Nickname', 'Owner', 'Server', 'Version', 'Domain', 'Ports', 'Actions']} actionInfo={CREATE_SERVER_ACTION}>
<serverActionsContext.Provider value={SERVER_ACTIONS}>
{
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 React, { useContext, Dispatch, useState, useEffect, Context, createContext } from "react"
import Form from "@rjsf/mui"
import { apiAuthenticatedContext, useApiDoc, api, ActionInfo, useActions, ActionGroup, actionIdentifierContext, ActionItem, DataTable, useAction } from "./common"
import validator from '@rjsf/validator-ajv8';
import { TableRow, TableCell, Chip } from "@mui/material"
import { useContext, Dispatch, useState, useEffect, Context, createContext } from "react"
import { apiAuthenticatedContext, api, ActionInfo, ActionGroup, actionIdentifierContext, DataTable } from "./common"
import { User } from './interfaces'
import { INVITE_USER_ACTION, Permission } from "./actions";
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 }) {
const user = p.user;
@ -16,7 +39,7 @@ function UserItem(p: { user: User }) {
<TableRow>
<TableCell>{user.Username}</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>
</TableRow>
)
@ -26,7 +49,7 @@ export function UsersPage({ }) {
const [apiAuthenticated, setApiAuthenticated] = useContext(apiAuthenticatedContext)
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(() => {
if (!apiAuthenticated) {
@ -73,7 +96,7 @@ export function UsersPage({ }) {
}
return <DataTable headers={['Username', 'Email', 'Permissions', 'Actions']} actionInfo={action}>
<UserActionsContext.Provider value={useActions(api, '/users/{username}')}>
<UserActionsContext.Provider value={[]}>
{userComponents}
</UserActionsContext.Provider>
</DataTable>

View File

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