This commit is contained in:
parent
392bfcccaa
commit
e81b7abd1a
12
Dockerfile
12
Dockerfile
@ -1,4 +1,4 @@
|
|||||||
FROM node:alpine
|
FROM node:alpine as builder
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@ -9,9 +9,13 @@ COPY tsconfig.json .
|
|||||||
|
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
COPY --chmod=111 startup.sh .
|
|
||||||
|
|
||||||
COPY public public
|
COPY public public
|
||||||
COPY src src
|
COPY src src
|
||||||
|
|
||||||
ENTRYPOINT [ "/usr/bin/env", "./startup.sh" ]
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM node:alpine
|
||||||
|
COPY --from=builder /app/build /opt/server
|
||||||
|
WORKDIR /opt/server
|
||||||
|
ENTRYPOINT [ "npx", "-y" , "serve", "-s", "/opt/server" ]
|
||||||
|
|
||||||
|
803
package-lock.json
generated
803
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
1
public/config.js
Normal file
1
public/config.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
var API_URL = "http://localhost"
|
@ -3,6 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.svg"/>
|
<link rel="icon" href="%PUBLIC_URL%/favicon.svg"/>
|
||||||
|
<script type="text/javascript" src="%PUBLIC_URL%/config.js"></script>
|
||||||
<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="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta
|
<meta
|
||||||
|
18
src/App.tsx
18
src/App.tsx
@ -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 } 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, 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';
|
||||||
@ -118,10 +118,10 @@ export default function App() {
|
|||||||
return (
|
return (
|
||||||
<ColorModeContext.Provider value={colorMode}>
|
<ColorModeContext.Provider value={colorMode}>
|
||||||
<Box height={'100vh'} width={'100vw'} overflow='clip' maxHeight='-webkit-fill-available'>
|
<Box height={'100vh'} width={'100vw'} overflow='clip' maxHeight='-webkit-fill-available'>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<ApiWrapper>
|
<ApiWrapper>
|
||||||
<GlobalUserInfo>
|
<GlobalUserInfo>
|
||||||
<Menu setMode={setMode}>
|
<Menu setMode={setMode}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path='login' element={<LoginPage />} />
|
<Route path='login' element={<LoginPage />} />
|
||||||
@ -133,10 +133,10 @@ export default function App() {
|
|||||||
</Routes>
|
</Routes>
|
||||||
</Menu>
|
</Menu>
|
||||||
</GlobalUserInfo>
|
</GlobalUserInfo>
|
||||||
</ApiWrapper>
|
</ApiWrapper>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</Box>
|
</Box>
|
||||||
</ColorModeContext.Provider>
|
</ColorModeContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
648
src/common.tsx
648
src/common.tsx
@ -1,7 +1,7 @@
|
|||||||
import axios, { AxiosInstance, AxiosResponse } from "axios";
|
import axios, { AxiosInstance, AxiosResponse } from "axios";
|
||||||
import React, { Context, Dispatch, ReactNode, createContext, useContext, useState } from "react";
|
import React, { Context, Dispatch, ReactNode, createContext, useContext, useState } from "react";
|
||||||
import { useLocation, Navigate } from "react-router-dom";
|
import { useLocation, Navigate } from "react-router-dom";
|
||||||
import Cookies from 'js-cookie'
|
import Cookies from 'js-cookie';
|
||||||
import { Box, Button, ButtonGroup, ButtonOwnProps, Modal, PaletteMode, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField } from "@mui/material";
|
import { Box, Button, ButtonGroup, ButtonOwnProps, Modal, PaletteMode, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField } from "@mui/material";
|
||||||
import { Form } from "@rjsf/mui";
|
import { Form } from "@rjsf/mui";
|
||||||
import validator from '@rjsf/validator-ajv8';
|
import validator from '@rjsf/validator-ajv8';
|
||||||
@ -19,255 +19,255 @@ import { Permission } from "./actions";
|
|||||||
import TerminalComponent from "./terminal";
|
import TerminalComponent from "./terminal";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
|
|
||||||
export const apiAuthenticatedContext: Context<[boolean, Dispatch<boolean>]> = createContext([false, (value: boolean) => {}] as [boolean, Dispatch<boolean>])
|
export const apiAuthenticatedContext: Context<[boolean, Dispatch<boolean>]> = createContext([false, (value: boolean) => { }] as [boolean, Dispatch<boolean>])
|
||||||
|
|
||||||
|
|
||||||
export const formModalStyle = {
|
export const formModalStyle = {
|
||||||
position: 'absolute' as 'absolute',
|
position: 'absolute' as 'absolute',
|
||||||
maxHeight: "90%",
|
maxHeight: "90%",
|
||||||
top: '50%',
|
top: '50%',
|
||||||
left: '50%',
|
left: '50%',
|
||||||
transform: 'translate(-50%, -50%)',
|
transform: 'translate(-50%, -50%)',
|
||||||
width: '70%',
|
width: '70%',
|
||||||
bgcolor: 'background.paper',
|
bgcolor: 'background.paper',
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
p: 4,
|
p: 4,
|
||||||
borderRadius: 3,
|
borderRadius: 3,
|
||||||
}
|
}
|
||||||
export const terminalModalStyle = {
|
export const terminalModalStyle = {
|
||||||
position: 'absolute' as 'absolute',
|
position: 'absolute' as 'absolute',
|
||||||
top: '50%',
|
top: '50%',
|
||||||
left: '50%',
|
left: '50%',
|
||||||
transform: 'translate(-50%, -50%)',
|
transform: 'translate(-50%, -50%)',
|
||||||
width: '70%',
|
width: '70%',
|
||||||
bgcolor: 'background.paper',
|
bgcolor: 'background.paper',
|
||||||
p: 1,
|
p: 1,
|
||||||
borderRadius: 3,
|
borderRadius: 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getDesignTokens = (mode: PaletteMode) => ({
|
export const getDesignTokens = (mode: PaletteMode) => ({
|
||||||
palette: {
|
palette: {
|
||||||
mode,
|
mode,
|
||||||
...(mode === 'light'
|
...(mode === 'light'
|
||||||
? {
|
? {
|
||||||
// palette values for light mode
|
// palette values for light mode
|
||||||
primary: blue,
|
primary: blue,
|
||||||
divider: blue[200],
|
divider: blue[200],
|
||||||
background: {
|
background: {
|
||||||
default: grey[200],
|
default: grey[200],
|
||||||
light: grey[100],
|
light: grey[100],
|
||||||
},
|
|
||||||
text: {
|
|
||||||
primary: grey[900],
|
|
||||||
secondary: grey[800],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
// palette values for dark mode
|
|
||||||
primary: grey,
|
|
||||||
divider: grey[700],
|
|
||||||
background: {
|
|
||||||
default: grey[900],
|
|
||||||
paper: grey[900],
|
|
||||||
light: grey[700],
|
|
||||||
},
|
|
||||||
text: {
|
|
||||||
primary: '#fff',
|
|
||||||
secondary: grey[500],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
MuiTypography: {
|
|
||||||
defaultProps: {
|
|
||||||
color: 'text.primary'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
text: {
|
||||||
|
primary: grey[900],
|
||||||
|
secondary: grey[800],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
// palette values for dark mode
|
||||||
|
primary: grey,
|
||||||
|
divider: grey[700],
|
||||||
|
background: {
|
||||||
|
default: grey[900],
|
||||||
|
paper: grey[900],
|
||||||
|
light: grey[700],
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
primary: '#fff',
|
||||||
|
secondary: grey[500],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
MuiTypography: {
|
||||||
|
defaultProps: {
|
||||||
|
color: 'text.primary'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const API_URL = `${process.env.REACT_APP_API_SCHEME}://${process.env.REACT_APP_API_URL}`
|
const API_URL = (window as any).API_URL
|
||||||
|
|
||||||
axios.defaults.withCredentials = true
|
axios.defaults.withCredentials = true
|
||||||
|
|
||||||
export const api: AxiosInstance = axios.create({
|
export const api: AxiosInstance = axios.create({
|
||||||
baseURL: API_URL,
|
baseURL: API_URL,
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ApiWrapper(p: { children: ReactNode}) {
|
export function ApiWrapper(p: { children: ReactNode }) {
|
||||||
const {children} = p
|
const { children } = p
|
||||||
const token = Cookies.get('auth')
|
const token = Cookies.get('auth')
|
||||||
|
|
||||||
const [apiAuthenticated, setApiAuthenticated] = useState(Boolean(token))
|
const [apiAuthenticated, setApiAuthenticated] = useState(Boolean(token))
|
||||||
if (!apiAuthenticated) {
|
if (!apiAuthenticated) {
|
||||||
Cookies.remove('auth')
|
Cookies.remove('auth')
|
||||||
}
|
}
|
||||||
const path = useLocation()
|
const path = useLocation()
|
||||||
return (<apiAuthenticatedContext.Provider value={[apiAuthenticated, setApiAuthenticated]}>
|
return (<apiAuthenticatedContext.Provider value={[apiAuthenticated, setApiAuthenticated]}>
|
||||||
{children}
|
{children}
|
||||||
{!apiAuthenticated && (path.pathname !== '/login' && path.pathname !== '/signup') && <Navigate to='/login' />}
|
{!apiAuthenticated && (path.pathname !== '/login' && path.pathname !== '/signup') && <Navigate to='/login' />}
|
||||||
</apiAuthenticatedContext.Provider>)
|
</apiAuthenticatedContext.Provider>)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ActionInfo {
|
export interface ActionInfo {
|
||||||
name: string
|
name: string
|
||||||
requestType: 'post' | 'get' | 'delete' | 'patch'
|
requestType: 'post' | 'get' | 'delete' | 'patch'
|
||||||
endpoint: string
|
endpoint: string
|
||||||
args: {}
|
args: {}
|
||||||
permissions?: number
|
permissions?: number
|
||||||
response_action?: 'Ignore' | 'Browse' | 'Terminal'
|
response_action?: 'Ignore' | 'Browse' | 'Terminal'
|
||||||
ServerState?: 'on' | 'off'
|
ServerState?: 'on' | 'off'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
interface Options{
|
interface Options {
|
||||||
label: string
|
label: string
|
||||||
const: string
|
const: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const actionIdentifierContext: Context<string> = createContext('')
|
export const actionIdentifierContext: Context<string> = createContext('')
|
||||||
|
|
||||||
|
|
||||||
function convertNumber(permissions: number): number[]{
|
function convertNumber(permissions: number): number[] {
|
||||||
var arr: number[] = []
|
var arr: number[] = []
|
||||||
|
|
||||||
Object.entries(Permission).forEach(
|
Object.entries(Permission).forEach(
|
||||||
([key, value]) => {
|
([key, value]) => {
|
||||||
if (permissions&value){
|
if (permissions & value) {
|
||||||
arr.push(value)
|
arr.push(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return arr
|
return arr
|
||||||
}
|
}
|
||||||
|
|
||||||
function CustomField(props: WidgetProps){
|
function CustomField(props: WidgetProps) {
|
||||||
const jp = require('jsonpath')
|
const jp = require('jsonpath')
|
||||||
const [options2, setOptions]: [Options[]|null, Dispatch<Options[]>] = useState(null as Options[]|null)
|
const [options2, setOptions]: [Options[] | null, Dispatch<Options[]>] = useState(null as Options[] | null)
|
||||||
const {schema, registry, options, ...newProps} = props
|
const { schema, registry, options, ...newProps } = props
|
||||||
const {SelectWidget, CheckboxesWidget} = registry.widgets
|
const { SelectWidget, CheckboxesWidget } = registry.widgets
|
||||||
|
|
||||||
if (!schema.fetch_url){
|
if (!schema.fetch_url) {
|
||||||
if (!schema.permissions){
|
if (!schema.permissions) {
|
||||||
return <TextField onChange={(event)=>(props.onChange(event.target.value))} value={props.value} label={props.label}/>
|
return <TextField onChange={(event) => (props.onChange(event.target.value))} value={props.value} label={props.label} />
|
||||||
}
|
|
||||||
|
|
||||||
return <CheckboxesWidget
|
|
||||||
{...newProps}
|
|
||||||
onChange={(event)=>{
|
|
||||||
props.onChange(event.reduce((partialSum: number, a: number) => (partialSum + a), 0))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
schema={{}}
|
|
||||||
options={{
|
return <CheckboxesWidget
|
||||||
|
{...newProps}
|
||||||
|
onChange={(event) => {
|
||||||
|
props.onChange(event.reduce((partialSum: number, a: number) => (partialSum + a), 0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
schema={{}}
|
||||||
|
options={{
|
||||||
enumOptions: [
|
enumOptions: [
|
||||||
{label: 'Start', value: Permission.Start},
|
{ label: 'Start', value: Permission.Start },
|
||||||
{label: 'Stop', value: Permission.Stop},
|
{ label: 'Stop', value: Permission.Stop },
|
||||||
{label: 'Browse', value: Permission.Browse},
|
{ label: 'Browse', value: Permission.Browse },
|
||||||
{label: 'Delete', value: Permission.Delete},
|
{ label: 'Delete', value: Permission.Delete },
|
||||||
{label: 'Run Command', value: Permission.RunCommand},
|
{ label: 'Run Command', value: Permission.RunCommand },
|
||||||
{label: 'Create', value: Permission.Create},
|
{ label: 'Create', value: Permission.Create },
|
||||||
{label: 'Admin', value: Permission.Admin},
|
{ label: 'Admin', value: Permission.Admin },
|
||||||
{label: 'Cloud', value: Permission.Cloud},
|
{ label: 'Cloud', value: Permission.Cloud },
|
||||||
]}} registry={registry} value={convertNumber(props.value)} />
|
]
|
||||||
}
|
}} registry={registry} value={convertNumber(props.value)} />
|
||||||
|
}
|
||||||
|
|
||||||
if (options2 === null){
|
if (options2 === null) {
|
||||||
api.get(schema.fetch_url as string).then((event)=>{
|
api.get(schema.fetch_url as string).then((event) => {
|
||||||
let newOptions: Options[] = []
|
let newOptions: Options[] = []
|
||||||
for (let response of event.data){
|
for (let response of event.data) {
|
||||||
newOptions.push({
|
newOptions.push({
|
||||||
const: jp.query(response, `$.${schema.fetch_key_path}`).join(' '),
|
const: jp.query(response, `$.${schema.fetch_key_path}`).join(' '),
|
||||||
label: jp.query(response, `$.${schema.fetch_display_path}`).join(' '),
|
label: jp.query(response, `$.${schema.fetch_display_path}`).join(' '),
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
setOptions(newOptions)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return <SelectWidget {...newProps} schema={{oneOf: options2?options2:[]}} registry={registry} options={{enumOptions: (options2?options2:[]).map((value: Options)=>({label: value.label, value: value.const}))}}/>
|
|
||||||
|
setOptions(newOptions)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return <SelectWidget {...newProps} schema={{ oneOf: options2 ? options2 : [] }} registry={registry} options={{ enumOptions: (options2 ? options2 : []).map((value: Options) => ({ label: value.label, value: value.const })) }} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function isUserAllowed(user: User|null, action: ActionInfo): boolean{
|
function isUserAllowed(user: User | null, action: ActionInfo): boolean {
|
||||||
if (user === null){
|
if (user === null) {
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAdmin = (user.Permissions & Permission.Admin) === Permission.Admin
|
|
||||||
if (isAdmin){
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!action.permissions){
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((action.permissions & user.Permissions) == action.permissions){
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAdmin = (user.Permissions & Permission.Admin) === Permission.Admin
|
||||||
|
if (isAdmin) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!action.permissions) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((action.permissions & user.Permissions) == action.permissions) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function ActionItem(p: { action: ActionInfo, identifierSubstring?: string, sx?: ButtonOwnProps, variant?: any, onClick?: Function }) {
|
export function ActionItem(p: { action: ActionInfo, identifierSubstring?: string, sx?: ButtonOwnProps, variant?: any, onClick?: Function }) {
|
||||||
const actionIdentifier: string = useContext(actionIdentifierContext)
|
const actionIdentifier: string = useContext(actionIdentifierContext)
|
||||||
const identifierSubstring = (typeof p.identifierSubstring !== 'undefined') ? p.identifierSubstring : ''
|
const identifierSubstring = (typeof p.identifierSubstring !== 'undefined') ? p.identifierSubstring : ''
|
||||||
const user = useContext(UserInfoContext)
|
const user = useContext(UserInfoContext)
|
||||||
|
|
||||||
const [form, setForm] = useState(false);
|
const [form, setForm] = useState(false);
|
||||||
const [terminal, setTerminal] = useState(null as string|null);
|
const [terminal, setTerminal] = useState(null as string | null);
|
||||||
const [formData, setFormData]: [RJSFSchema, Dispatch<RJSFSchema>] = useState({})
|
const [formData, setFormData]: [RJSFSchema, Dispatch<RJSFSchema>] = useState({})
|
||||||
const url = p.action.endpoint.replaceAll(`{${identifierSubstring}}`, actionIdentifier)
|
const url = p.action.endpoint.replaceAll(`{${identifierSubstring}}`, actionIdentifier)
|
||||||
|
|
||||||
|
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
let promise: Promise<AxiosResponse<any, any>>|null = null
|
let promise: Promise<AxiosResponse<any, any>> | null = null
|
||||||
switch (p.action.requestType) {
|
switch (p.action.requestType) {
|
||||||
case 'post': {
|
case 'post': {
|
||||||
promise = api.post(url, formData)
|
promise = api.post(url, formData)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'patch':{
|
case 'patch': {
|
||||||
promise = api.patch(url, formData)
|
promise = api.patch(url, formData)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'get': {
|
case 'get': {
|
||||||
if (formData){
|
if (formData) {
|
||||||
console.warn('get can get no arguments, dropping')
|
console.warn('get can get no arguments, dropping')
|
||||||
}
|
|
||||||
promise = api.get(url)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'delete': {
|
|
||||||
if (formData){
|
|
||||||
console.warn('delete can get no arguments, dropping')
|
|
||||||
}
|
|
||||||
promise = api.delete(url)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
switch (p.action.response_action){
|
promise = api.get(url)
|
||||||
case 'Browse':{
|
break
|
||||||
if (promise !== null){
|
}
|
||||||
promise.then((event)=>{window.open(`https://${event.data}`)})
|
case 'delete': {
|
||||||
}
|
if (formData) {
|
||||||
}
|
console.warn('delete can get no arguments, dropping')
|
||||||
}
|
}
|
||||||
setForm(false)
|
promise = api.delete(url)
|
||||||
setFormData({})
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
switch (p.action.response_action) {
|
||||||
function onFormChange(args: IChangeEvent<any, RJSFSchema, any>) {
|
case 'Browse': {
|
||||||
setFormData(args.formData)
|
if (promise !== null) {
|
||||||
|
promise.then((event) => { window.open(`https://${event.data}`) })
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
setForm(false)
|
||||||
|
setFormData({})
|
||||||
|
}
|
||||||
|
|
||||||
function createTerminal(websocket: string){
|
function onFormChange(args: IChangeEvent<any, RJSFSchema, any>) {
|
||||||
const NewWindow = window.open('', '', 'width=800 height=600')!
|
setFormData(args.formData)
|
||||||
NewWindow.document.write(`<!doctype html>
|
}
|
||||||
|
|
||||||
|
function createTerminal(websocket: string) {
|
||||||
|
const NewWindow = window.open('', '', 'width=800 height=600')!
|
||||||
|
NewWindow.document.write(`<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<link rel="stylesheet" href="xterm/css/xterm.css" />
|
<link rel="stylesheet" href="xterm/css/xterm.css" />
|
||||||
@ -329,71 +329,71 @@ export function ActionItem(p: { action: ActionInfo, identifierSubstring?: string
|
|||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`)
|
`)
|
||||||
NewWindow.onload = () => {
|
NewWindow.onload = () => {
|
||||||
const root = ReactDOM.createRoot(NewWindow.document.getElementById('root') as HTMLElement);
|
const root = ReactDOM.createRoot(NewWindow.document.getElementById('root') as HTMLElement);
|
||||||
root.render(<TerminalComponent websocket={websocket}/>);
|
root.render(<TerminalComponent websocket={websocket} />);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return (<>
|
return (<>
|
||||||
<Button variant={p.variant} disabled={!isUserAllowed(user, p.action)} onClick={() => { if (p.onClick) { p.onClick() } p.action.response_action == 'Terminal'?createTerminal(`ws${API_URL.slice("http".length)}${url}`):setForm(true) }} sx={p.sx}>{p.action.name}</Button >
|
<Button variant={p.variant} disabled={!isUserAllowed(user, p.action)} onClick={() => { if (p.onClick) { p.onClick() } p.action.response_action == 'Terminal' ? createTerminal(`ws${API_URL.slice("http".length)}${url}`) : setForm(true) }} sx={p.sx}>{p.action.name}</Button >
|
||||||
<Modal
|
<Modal
|
||||||
onClose={() => { setForm(false); setFormData({}); }}
|
onClose={() => { setForm(false); setFormData({}); }}
|
||||||
open={form}
|
open={form}
|
||||||
>
|
>
|
||||||
<Box sx={{ ...formModalStyle}}>
|
<Box sx={{ ...formModalStyle }}>
|
||||||
<Form validator={validator} widgets={{TextWidget: CustomField}} schema={p.action.args} onChange={onFormChange} formData={formData} onSubmit={handleSubmit} />
|
<Form validator={validator} widgets={{ TextWidget: CustomField }} schema={p.action.args} onChange={onFormChange} formData={formData} onSubmit={handleSubmit} />
|
||||||
</Box>
|
</Box>
|
||||||
</Modal>
|
</Modal>
|
||||||
<Modal
|
<Modal
|
||||||
onClose={() => { setTerminal(null); }}
|
onClose={() => { setTerminal(null); }}
|
||||||
open={terminal != null}
|
open={terminal != null}
|
||||||
>
|
>
|
||||||
<Box sx={terminalModalStyle}>
|
<Box sx={terminalModalStyle}>
|
||||||
{/* <TerminalComponent websocket={terminal} /> */}
|
{/* <TerminalComponent websocket={terminal} /> */}
|
||||||
</Box>
|
</Box>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>)
|
</>)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ActionGroup(p: { actions: ActionInfo[], identifierSubstring?: string, children?: ReactNode}) {
|
export function ActionGroup(p: { actions: ActionInfo[], identifierSubstring?: string, children?: ReactNode }) {
|
||||||
const actionItems: any[] = p.actions.map((action, index, array) => (<ActionItem action={action} identifierSubstring={p.identifierSubstring} /> ))
|
const actionItems: any[] = p.actions.map((action, index, array) => (<ActionItem action={action} identifierSubstring={p.identifierSubstring} />))
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const anchorRef = React.useRef<HTMLDivElement>(null);
|
const anchorRef = React.useRef<HTMLDivElement>(null);
|
||||||
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
||||||
const user = useContext(UserInfoContext)
|
const user = useContext(UserInfoContext)
|
||||||
|
|
||||||
for (let child of React.Children.toArray(p.children)){
|
for (let child of React.Children.toArray(p.children)) {
|
||||||
actionItems.push(child)
|
actionItems.push(child)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMenuItemClick = (
|
||||||
|
event: React.MouseEvent<HTMLLIElement, MouseEvent>,
|
||||||
|
index: number,
|
||||||
|
) => {
|
||||||
|
setSelectedIndex(index);
|
||||||
|
setOpen(false)
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggle = () => {
|
||||||
|
setOpen((prevOpen) => !prevOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = (event: Event) => {
|
||||||
|
if (
|
||||||
|
anchorRef.current &&
|
||||||
|
anchorRef.current.contains(event.target as HTMLElement)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMenuItemClick = (
|
setOpen(false);
|
||||||
event: React.MouseEvent<HTMLLIElement, MouseEvent>,
|
}
|
||||||
index: number,
|
return (
|
||||||
) => {
|
<React.Fragment>
|
||||||
setSelectedIndex(index);
|
<ButtonGroup variant="outlined" ref={anchorRef} aria-label="split button">
|
||||||
setOpen(false)
|
{actionItems[selectedIndex]}
|
||||||
};
|
<Button
|
||||||
|
|
||||||
const handleToggle = () => {
|
|
||||||
setOpen((prevOpen) => !prevOpen);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = (event: Event) => {
|
|
||||||
if (
|
|
||||||
anchorRef.current &&
|
|
||||||
anchorRef.current.contains(event.target as HTMLElement)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setOpen(false);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<ButtonGroup variant="outlined" ref={anchorRef} aria-label="split button">
|
|
||||||
{actionItems[selectedIndex]}
|
|
||||||
<Button
|
|
||||||
size="small"
|
size="small"
|
||||||
aria-controls={open ? 'split-button-menu' : undefined}
|
aria-controls={open ? 'split-button-menu' : undefined}
|
||||||
aria-expanded={open ? 'true' : undefined}
|
aria-expanded={open ? 'true' : undefined}
|
||||||
@ -403,87 +403,87 @@ export function ActionGroup(p: { actions: ActionInfo[], identifierSubstring?: st
|
|||||||
>
|
>
|
||||||
<ArrowDropDownIcon />
|
<ArrowDropDownIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
<Popper
|
<Popper
|
||||||
sx={{
|
sx={{
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
|
}}
|
||||||
|
open={open}
|
||||||
|
anchorEl={anchorRef.current}
|
||||||
|
role={undefined}
|
||||||
|
transition
|
||||||
|
disablePortal
|
||||||
|
>
|
||||||
|
{({ TransitionProps, placement }) => (
|
||||||
|
<Grow
|
||||||
|
{...TransitionProps}
|
||||||
|
style={{
|
||||||
|
transformOrigin:
|
||||||
|
placement === 'bottom' ? 'center top' : 'center bottom',
|
||||||
}}
|
}}
|
||||||
open={open}
|
|
||||||
anchorEl={anchorRef.current}
|
|
||||||
role={undefined}
|
|
||||||
transition
|
|
||||||
disablePortal
|
|
||||||
>
|
>
|
||||||
{({ TransitionProps, placement }) => (
|
<Paper>
|
||||||
<Grow
|
<ClickAwayListener onClickAway={handleClose}>
|
||||||
{...TransitionProps}
|
<MenuList id="split-button-menu" autoFocusItem>
|
||||||
style={{
|
{actionItems.map((option, index) => {
|
||||||
transformOrigin:
|
return <MenuItem
|
||||||
placement === 'bottom' ? 'center top' : 'center bottom',
|
key={option.props.action.name}
|
||||||
}}
|
selected={index === selectedIndex}
|
||||||
>
|
onClick={(event) => handleMenuItemClick(event, index)}
|
||||||
<Paper>
|
disabled={!isUserAllowed(user, option.props.action)}
|
||||||
<ClickAwayListener onClickAway={handleClose}>
|
>
|
||||||
<MenuList id="split-button-menu" autoFocusItem>
|
{option.props.action.name}
|
||||||
{actionItems.map((option, index) => {
|
</MenuItem>
|
||||||
return <MenuItem
|
}
|
||||||
key={option.props.action.name}
|
)
|
||||||
selected={index === selectedIndex}
|
}
|
||||||
onClick={(event) => handleMenuItemClick(event, index)}
|
</MenuList>
|
||||||
disabled={!isUserAllowed(user, option.props.action)}
|
</ClickAwayListener>
|
||||||
>
|
</Paper>
|
||||||
{option.props.action.name}
|
</Grow>
|
||||||
</MenuItem>
|
)}
|
||||||
}
|
</Popper>
|
||||||
)
|
</React.Fragment>
|
||||||
}
|
);
|
||||||
</MenuList>
|
|
||||||
</ClickAwayListener>
|
|
||||||
</Paper>
|
|
||||||
</Grow>
|
|
||||||
)}
|
|
||||||
</Popper>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function DataTable(props: { headers: string[], children: ReactNode, actionInfo?: ActionInfo, actionHook?: Function }) {
|
export function DataTable(props: { headers: string[], children: ReactNode, actionInfo?: ActionInfo, actionHook?: Function }) {
|
||||||
const { children, headers, actionInfo, actionHook } = props
|
const { children, headers, actionInfo, actionHook } = props
|
||||||
return <Box padding={4} overflow='clip'>
|
return <Box padding={4} overflow='clip'>
|
||||||
<TableContainer component={Paper} sx={{maxHeight: '80svh'}}>
|
<TableContainer component={Paper} sx={{ maxHeight: '80svh' }}>
|
||||||
<Table stickyHeader>
|
<Table stickyHeader>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow sx={{ backgroundColor: 'background.light', fontWeight: 'bold' }}>
|
<TableRow sx={{ backgroundColor: 'background.light', fontWeight: 'bold' }}>
|
||||||
{headers.map((value, index, array) => (<TableCell sx={{ backgroundColor: 'background.light', fontWeight: 'bold' }}>{value}</TableCell>))}
|
{headers.map((value, index, array) => (<TableCell sx={{ backgroundColor: 'background.light', fontWeight: 'bold' }}>{value}</TableCell>))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{children}
|
{children}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
{(actionInfo && <Box marginTop={2} overflow='clip'>
|
{(actionInfo && <Box marginTop={2} overflow='clip'>
|
||||||
<ActionItem variant="contained" action={actionInfo} onClick={actionHook} />
|
<ActionItem variant="contained" action={actionInfo} onClick={actionHook} />
|
||||||
</Box>)}
|
</Box>)}
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const UserInfoContext: Context<User|null> = createContext(null as User|null)
|
export const UserInfoContext: Context<User | null> = createContext(null as User | null)
|
||||||
|
|
||||||
|
|
||||||
export function GlobalUserInfo(props: {children: any}){
|
export function GlobalUserInfo(props: { children: any }) {
|
||||||
const [user, setUser]: [User|null, Dispatch<User|null>] = useState(null as User|null)
|
const [user, setUser]: [User | null, Dispatch<User | null>] = useState(null as User | null)
|
||||||
const [apiAuthenticated, _] = useContext(apiAuthenticatedContext)
|
const [apiAuthenticated, _] = useContext(apiAuthenticatedContext)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (user === null && apiAuthenticated){
|
if (user === null && apiAuthenticated) {
|
||||||
api.get('/users/@me').then((event)=>{setUser(event.data)}).catch(()=>{setUser(null)})
|
api.get('/users/@me').then((event) => { setUser(event.data) }).catch(() => { setUser(null) })
|
||||||
}
|
}
|
||||||
|
|
||||||
return <UserInfoContext.Provider value={user}>
|
return <UserInfoContext.Provider value={user}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</UserInfoContext.Provider>
|
</UserInfoContext.Provider>
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user