import axios, { AxiosInstance, AxiosResponse } from "axios"; import React, { Context, Dispatch, ReactNode, createContext, useContext, useState } from "react"; import { useLocation, Navigate } from "react-router-dom"; import Cookies from 'js-cookie' import { Box, Button, ButtonGroup, ButtonOwnProps, Modal, PaletteMode, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField } from "@mui/material"; import { Form } from "@rjsf/mui"; import validator from '@rjsf/validator-ajv8'; import { blue, grey } from "@mui/material/colors"; import { User } from "./interfaces"; import { IChangeEvent } from "@rjsf/core"; import { RJSFSchema, WidgetProps } from "@rjsf/utils"; import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; import ClickAwayListener from '@mui/material/ClickAwayListener'; import Grow from '@mui/material/Grow'; import Popper from '@mui/material/Popper'; import MenuItem from '@mui/material/MenuItem'; import MenuList from '@mui/material/MenuList'; import { Permission } from "./actions"; import TerminalComponent from "./terminal"; import ReactDOM from "react-dom/client"; export const apiAuthenticatedContext: Context<[boolean, Dispatch]> = createContext([false, (value: boolean) => {}] as [boolean, Dispatch]) export const formModalStyle = { position: 'absolute' as 'absolute', maxHeight: "90%", top: '50%', left: '50%', transform: 'translate(-50%, -50%)', width: '70%', bgcolor: 'background.paper', overflowY: 'auto', p: 4, borderRadius: 3, } export const terminalModalStyle = { position: 'absolute' as 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', width: '70%', bgcolor: 'background.paper', p: 1, borderRadius: 3, } export const getDesignTokens = (mode: PaletteMode) => ({ palette: { mode, ...(mode === 'light' ? { // palette values for light mode primary: blue, divider: blue[200], background: { default: grey[200], 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' } }, } }); const API_URL = `${process.env.REACT_APP_API_SCHEME}://${process.env.REACT_APP_API_URL}` axios.defaults.withCredentials = true export const api: AxiosInstance = axios.create({ baseURL: API_URL, withCredentials: true, }); export function ApiWrapper(p: { children: ReactNode}) { const {children} = p const token = Cookies.get('auth') const [apiAuthenticated, setApiAuthenticated] = useState(Boolean(token)) if (!apiAuthenticated) { Cookies.remove('auth') } const path = useLocation() return ( {children} {!apiAuthenticated && (path.pathname !== '/login' && path.pathname !== '/signup') && } ) } export interface ActionInfo { name: string requestType: 'post' | 'get' | 'delete' | 'patch' endpoint: string args: {} permissions?: number response_action?: 'Ignore' | 'Browse' | 'Terminal' ServerState?: 'on' | 'off' } interface Options{ label: string const: string } export const actionIdentifierContext: Context = createContext('') function convertNumber(permissions: number): number[]{ var arr: number[] = [] Object.entries(Permission).forEach( ([key, value]) => { if (permissions&value){ arr.push(value) } } ); return arr } function CustomField(props: WidgetProps){ const jp = require('jsonpath') const [options2, setOptions]: [Options[]|null, Dispatch] = useState(null as Options[]|null) const {schema, registry, options, ...newProps} = props const {SelectWidget, CheckboxesWidget} = registry.widgets if (!schema.fetch_url){ if (!schema.permissions){ return (props.onChange(event.target.value))} value={props.value} label={props.label}/> } return { props.onChange(event.reduce((partialSum: number, a: number) => (partialSum + a), 0)) } } schema={{}} options={{ enumOptions: [ {label: 'Start', value: Permission.Start}, {label: 'Stop', value: Permission.Stop}, {label: 'Browse', value: Permission.Browse}, {label: 'Delete', value: Permission.Delete}, {label: 'Run Command', value: Permission.RunCommand}, {label: 'Create', value: Permission.Create}, {label: 'Admin', value: Permission.Admin}, {label: 'Cloud', value: Permission.Cloud}, ]}} registry={registry} value={convertNumber(props.value)} /> } if (options2 === null){ api.get(schema.fetch_url as string).then((event)=>{ let newOptions: Options[] = [] for (let response of event.data){ newOptions.push({ const: jp.query(response, `$.${schema.fetch_key_path}`).join(' '), label: jp.query(response, `$.${schema.fetch_display_path}`).join(' '), }) } setOptions(newOptions) }) } return ({label: value.label, value: value.const}))}}/> } function isUserAllowed(user: User|null, action: ActionInfo): boolean{ 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 } export function ActionItem(p: { action: ActionInfo, identifierSubstring?: string, sx?: ButtonOwnProps, variant?: any, onClick?: Function }) { const actionIdentifier: string = useContext(actionIdentifierContext) const identifierSubstring = (typeof p.identifierSubstring !== 'undefined') ? p.identifierSubstring : '' const user = useContext(UserInfoContext) const [form, setForm] = useState(false); const [terminal, setTerminal] = useState(null as string|null); const [formData, setFormData]: [RJSFSchema, Dispatch] = useState({}) const url = p.action.endpoint.replaceAll(`{${identifierSubstring}}`, actionIdentifier) function handleSubmit() { let promise: Promise>|null = null switch (p.action.requestType) { case 'post': { promise = api.post(url, formData) break } case 'patch':{ promise = api.patch(url, formData) break } case 'get': { if (formData){ 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){ case 'Browse':{ if (promise !== null){ promise.then((event)=>{window.open(`https://${event.data}`)}) } } } setForm(false) setFormData({}) } function onFormChange(args: IChangeEvent) { setFormData(args.formData) } function createTerminal(websocket: string){ const window = open('', '', 'width=800 height=600')! window.document.write('
') const root = ReactDOM.createRoot( window.document.getElementById('root') as HTMLElement ); root.render() } return (<> { setForm(false); setFormData({}); }} open={form} >
{ setTerminal(null); }} open={terminal != null} > ) } export function ActionGroup(p: { actions: ActionInfo[], identifierSubstring?: string, children?: ReactNode}) { const actionItems: any[] = p.actions.map((action, index, array) => ( )) const [open, setOpen] = React.useState(false); const anchorRef = React.useRef(null); const [selectedIndex, setSelectedIndex] = React.useState(0); const user = useContext(UserInfoContext) for (let child of React.Children.toArray(p.children)){ actionItems.push(child) } const handleMenuItemClick = ( event: React.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; } setOpen(false); } return ( {actionItems[selectedIndex]} {({ TransitionProps, placement }) => ( {actionItems.map((option, index) => { return handleMenuItemClick(event, index)} disabled={!isUserAllowed(user, option.props.action)} > {option.props.action.name} } ) } )} ); } export function DataTable(props: { headers: string[], children: ReactNode, actionInfo?: ActionInfo, actionHook?: Function }) { const { children, headers, actionInfo, actionHook } = props return {headers.map((value, index, array) => ({value}))} {children}
{(actionInfo && )}
} export const UserInfoContext: Context = createContext(null as User|null) export function GlobalUserInfo(props: {children: any}){ const [user, setUser]: [User|null, Dispatch] = useState(null as User|null) const [apiAuthenticated, _] = useContext(apiAuthenticatedContext) if (user === null && apiAuthenticated){ api.get('/users/@me').then((event)=>{setUser(event.data)}).catch(()=>{setUser(null)}) } return {props.children} }