@@ -1,5 +0,0 @@
|
|||||||
const genAPIKey = () => {
|
|
||||||
return [...Array(30)]
|
|
||||||
.map((e) => ((Math.random() * 36) | 0).toString(36))
|
|
||||||
.join('');
|
|
||||||
};
|
|
||||||
105
server/src/api/record.ts
Normal file
105
server/src/api/record.ts
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import { readFile, readFiles } from "../workflow-management/storage";
|
||||||
|
import { Router, Request, Response } from 'express';
|
||||||
|
import { requireAPIKey } from "../middlewares/api";
|
||||||
|
export const router = Router();
|
||||||
|
|
||||||
|
const formatRecording = (recordingData: any) => {
|
||||||
|
const recordingMeta = recordingData.recording_meta;
|
||||||
|
const workflow = recordingData.recording.workflow || [];
|
||||||
|
const firstWorkflowStep = workflow[0]?.where?.url || '';
|
||||||
|
|
||||||
|
const inputParameters = [
|
||||||
|
{
|
||||||
|
type: "string",
|
||||||
|
name: "originUrl",
|
||||||
|
label: "Origin URL",
|
||||||
|
required: true,
|
||||||
|
defaultValue: firstWorkflowStep,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: recordingMeta.id,
|
||||||
|
name: recordingMeta.name,
|
||||||
|
createdAt: new Date(recordingMeta.create_date).getTime(),
|
||||||
|
inputParameters,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
router.get("/api/robots", requireAPIKey, async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const fileContents = await readFiles('./../storage/recordings/');
|
||||||
|
|
||||||
|
const formattedRecordings = fileContents.map((fileContent: string) => {
|
||||||
|
const recordingData = JSON.parse(fileContent);
|
||||||
|
return formatRecording(recordingData);
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
statusCode: 200,
|
||||||
|
messageCode: "success",
|
||||||
|
robots: {
|
||||||
|
totalCount: formattedRecordings.length,
|
||||||
|
items: formattedRecordings,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(200).json(response);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching recordings:", error);
|
||||||
|
res.status(500).json({
|
||||||
|
statusCode: 500,
|
||||||
|
messageCode: "error",
|
||||||
|
message: "Failed to retrieve recordings",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const formatRecordingById = (recordingData: any) => {
|
||||||
|
const recordingMeta = recordingData.recording_meta;
|
||||||
|
const workflow = recordingData.recording.workflow || [];
|
||||||
|
const firstWorkflowStep = workflow[0]?.where?.url || '';
|
||||||
|
|
||||||
|
const inputParameters = [
|
||||||
|
{
|
||||||
|
type: "string",
|
||||||
|
name: "originUrl",
|
||||||
|
label: "Origin URL",
|
||||||
|
required: true,
|
||||||
|
defaultValue: firstWorkflowStep,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: recordingMeta.id,
|
||||||
|
name: recordingMeta.name,
|
||||||
|
createdAt: new Date(recordingMeta.create_date).getTime(),
|
||||||
|
inputParameters,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
router.get("/api/robots/:fileName", requireAPIKey, async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const fileContent = await readFile(`./../storage/recordings/${req.params.fileName}.waw.json`);
|
||||||
|
|
||||||
|
const recordingData = JSON.parse(fileContent);
|
||||||
|
const formattedRecording = formatRecordingById(recordingData);
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
statusCode: 200,
|
||||||
|
messageCode: "success",
|
||||||
|
robot: formattedRecording,
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(200).json(response);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching recording:", error);
|
||||||
|
res.status(404).json({
|
||||||
|
statusCode: 404,
|
||||||
|
messageCode: "not_found",
|
||||||
|
message: `Recording with name "${req.params.fileName}" not found.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
15
server/src/middlewares/api.ts
Normal file
15
server/src/middlewares/api.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import User from "../models/User";
|
||||||
|
|
||||||
|
export const requireAPIKey = async (req: Request, res: Response, next: any) => {
|
||||||
|
const apiKey = req.headers['x-api-key'];
|
||||||
|
if (!apiKey) {
|
||||||
|
return res.status(401).json({ error: "API key is missing" });
|
||||||
|
}
|
||||||
|
const user = await User.findOne({ where: { api_key: apiKey } });
|
||||||
|
if (!user) {
|
||||||
|
return res.status(403).json({ error: "Invalid API key" });
|
||||||
|
}
|
||||||
|
req.user = user;
|
||||||
|
next();
|
||||||
|
};
|
||||||
@@ -5,6 +5,7 @@ interface UserAttributes {
|
|||||||
id: number;
|
id: number;
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
api_key?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional fields for creating a new user
|
// Optional fields for creating a new user
|
||||||
@@ -14,6 +15,7 @@ class User extends Model<UserAttributes, UserCreationAttributes> implements User
|
|||||||
public id!: number;
|
public id!: number;
|
||||||
public email!: string;
|
public email!: string;
|
||||||
public password!: string;
|
public password!: string;
|
||||||
|
public api_key!: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
User.init(
|
User.init(
|
||||||
@@ -35,6 +37,10 @@ User.init(
|
|||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
},
|
},
|
||||||
|
api_key: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sequelize,
|
sequelize,
|
||||||
|
|||||||
40
server/src/routes/api.ts
Normal file
40
server/src/routes/api.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { Router, Request, Response } from 'express';
|
||||||
|
import { genAPIKey } from '../utils/api';
|
||||||
|
import User from '../models/User';
|
||||||
|
|
||||||
|
export const router = Router();
|
||||||
|
|
||||||
|
interface AuthenticatedRequest extends Request {
|
||||||
|
user?: { id: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
router.get('/generate-api-key', async (req: AuthenticatedRequest, res) => {
|
||||||
|
try {
|
||||||
|
if (!req.user) {
|
||||||
|
return res.status(401).json({ ok: false, error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
const user = await User.findByPk(req.user.id, {
|
||||||
|
attributes: { exclude: ['password'] },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({ message: 'User not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.api_key) {
|
||||||
|
return res.status(400).json({ message: 'API key already exists' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiKey = genAPIKey();
|
||||||
|
|
||||||
|
user.api_key = apiKey;
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
message: 'API key generated successfully',
|
||||||
|
api_key: apiKey,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(500).json({ message: 'Error generating API key', error });
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -3,6 +3,7 @@ import User from '../models/User';
|
|||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { hashPassword, comparePassword } from '../utils/auth';
|
import { hashPassword, comparePassword } from '../utils/auth';
|
||||||
import { requireSignIn } from '../middlewares/auth';
|
import { requireSignIn } from '../middlewares/auth';
|
||||||
|
import { genAPIKey } from '../utils/api';
|
||||||
export const router = Router();
|
export const router = Router();
|
||||||
|
|
||||||
interface AuthenticatedRequest extends Request {
|
interface AuthenticatedRequest extends Request {
|
||||||
@@ -88,4 +89,35 @@ router.get('/current-user', requireSignIn, async (req: AuthenticatedRequest, res
|
|||||||
console.error('Error in current-user route:', error);
|
console.error('Error in current-user route:', error);
|
||||||
return res.status(500).json({ ok: false, error: `Could not fetch current user: ${error.message}` });
|
return res.status(500).json({ ok: false, error: `Could not fetch current user: ${error.message}` });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.post('/generate-api-key', requireSignIn, async (req: AuthenticatedRequest, res) => {
|
||||||
|
try {
|
||||||
|
if (!req.user) {
|
||||||
|
return res.status(401).json({ ok: false, error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
const user = await User.findByPk(req.user.id, {
|
||||||
|
attributes: { exclude: ['password'] },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({ message: 'User not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.api_key) {
|
||||||
|
return res.status(400).json({ message: 'API key already exists' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiKey = genAPIKey();
|
||||||
|
|
||||||
|
user.api_key = apiKey;
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
message: 'API key generated successfully',
|
||||||
|
api_key: apiKey,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(500).json({ message: 'Error generating API key', error });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
3
server/src/utils/api.ts
Normal file
3
server/src/utils/api.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const genAPIKey = (): string => {
|
||||||
|
return [...Array(30)].map(() => ((Math.random() * 36) | 0).toString(36)).join('');
|
||||||
|
};
|
||||||
@@ -122,4 +122,3 @@ export const processGoogleSheetUpdates = async () => {
|
|||||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ interface InterpretationLogProps {
|
|||||||
setIsOpen: (isOpen: boolean) => void;
|
setIsOpen: (isOpen: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, setIsOpen }) => {
|
export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, setIsOpen }) => {
|
||||||
const [log, setLog] = useState<string>('');
|
const [log, setLog] = useState<string>('');
|
||||||
const [selectedOption, setSelectedOption] = useState<string>('10');
|
const [selectedOption, setSelectedOption] = useState<string>('10');
|
||||||
const [customValue, setCustomValue] = useState('');
|
const [customValue, setCustomValue] = useState('');
|
||||||
@@ -66,12 +66,12 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
|
|||||||
setLog((prevState) =>
|
setLog((prevState) =>
|
||||||
prevState + '\n' + '---------- Serializable output data received ----------' + '\n'
|
prevState + '\n' + '---------- Serializable output data received ----------' + '\n'
|
||||||
+ JSON.stringify(data, null, 2) + '\n' + '--------------------------------------------------');
|
+ JSON.stringify(data, null, 2) + '\n' + '--------------------------------------------------');
|
||||||
|
|
||||||
// Set table data
|
// Set table data
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
setTableData(data);
|
setTableData(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollLogToBottom();
|
scrollLogToBottom();
|
||||||
}, [log, scrollLogToBottom]);
|
}, [log, scrollLogToBottom]);
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
|
|||||||
|
|
||||||
// Extract columns dynamically from the first item of tableData
|
// Extract columns dynamically from the first item of tableData
|
||||||
const columns = tableData.length > 0 ? Object.keys(tableData[0]) : [];
|
const columns = tableData.length > 0 ? Object.keys(tableData[0]) : [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { TabPanel, TabContext } from "@mui/lab";
|
|||||||
import SettingsIcon from '@mui/icons-material/Settings';
|
import SettingsIcon from '@mui/icons-material/Settings';
|
||||||
import ImageIcon from '@mui/icons-material/Image';
|
import ImageIcon from '@mui/icons-material/Image';
|
||||||
import ArticleIcon from '@mui/icons-material/Article';
|
import ArticleIcon from '@mui/icons-material/Article';
|
||||||
import {Buffer} from 'buffer';
|
import { Buffer } from 'buffer';
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import AssignmentIcon from '@mui/icons-material/Assignment';
|
import AssignmentIcon from '@mui/icons-material/Assignment';
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ interface RunContentProps {
|
|||||||
abortRunHandler: () => void,
|
abortRunHandler: () => void,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RunContent = ({row, currentLog, interpretationInProgress, logEndRef, abortRunHandler}: RunContentProps) => {
|
export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRef, abortRunHandler }: RunContentProps) => {
|
||||||
const [tab, setTab] = React.useState<string>('log');
|
const [tab, setTab] = React.useState<string>('log');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -29,141 +29,142 @@ export const RunContent = ({row, currentLog, interpretationInProgress, logEndRef
|
|||||||
return (
|
return (
|
||||||
<Box sx={{ width: '100%' }}>
|
<Box sx={{ width: '100%' }}>
|
||||||
<TabContext value={tab}>
|
<TabContext value={tab}>
|
||||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||||
<Tabs value={tab} onChange={(e, newTab) => setTab(newTab)} aria-label="run-content-tabs">
|
<Tabs value={tab} onChange={(e, newTab) => setTab(newTab)} aria-label="run-content-tabs">
|
||||||
<Tab label="Log" value='log' />
|
<Tab label="Log" value='log' />
|
||||||
<Tab label="Input" value='input' />
|
<Tab label="Input" value='input' />
|
||||||
<Tab label="Output" value='output' />
|
<Tab label="Output" value='output' />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Box>
|
|
||||||
<TabPanel value='log'>
|
|
||||||
<Box sx={{ margin: 1,
|
|
||||||
background: '#19171c',
|
|
||||||
overflowY: 'scroll',
|
|
||||||
overflowX: 'scroll',
|
|
||||||
width: '700px',
|
|
||||||
height: 'fit-content',
|
|
||||||
maxHeight: '450px',
|
|
||||||
}}>
|
|
||||||
<div>
|
|
||||||
<Highlight className="javascript">
|
|
||||||
{interpretationInProgress ? currentLog : row.log}
|
|
||||||
</Highlight>
|
|
||||||
<div style={{ float:"left", clear: "both" }}
|
|
||||||
ref={logEndRef}/>
|
|
||||||
</div>
|
|
||||||
</Box>
|
</Box>
|
||||||
{interpretationInProgress ? <Button
|
<TabPanel value='log'>
|
||||||
color="error"
|
<Box sx={{
|
||||||
onClick={abortRunHandler}
|
margin: 1,
|
||||||
>
|
background: '#19171c',
|
||||||
Stop
|
overflowY: 'scroll',
|
||||||
</Button> : null}
|
overflowX: 'scroll',
|
||||||
</TabPanel>
|
width: '700px',
|
||||||
<TabPanel value='input' sx={{width: '700px'}}>
|
height: 'fit-content',
|
||||||
<Typography variant='h6' sx={{display:'flex', alignItems:'center'}}>
|
maxHeight: '450px',
|
||||||
<SettingsIcon sx={{marginRight: '15px'}}/>
|
}}>
|
||||||
Interpreter settings
|
<div>
|
||||||
</Typography>
|
<Highlight className="javascript">
|
||||||
{
|
{interpretationInProgress ? currentLog : row.log}
|
||||||
Object.keys(row.interpreterSettings).map((setting, index) => {
|
</Highlight>
|
||||||
if (setting === 'params') {
|
<div style={{ float: "left", clear: "both" }}
|
||||||
return (
|
ref={logEndRef} />
|
||||||
<div key={`settings-${setting}-${index}`}>
|
</div>
|
||||||
<Typography variant='h6' sx={{display:'flex', alignItems:'center'}} key={`setting-${index}`}>
|
</Box>
|
||||||
<AssignmentIcon sx={{marginRight: '15px'}}/>
|
{interpretationInProgress ? <Button
|
||||||
Recording parameters
|
color="error"
|
||||||
</Typography>
|
onClick={abortRunHandler}
|
||||||
{
|
>
|
||||||
Object.keys(row.interpreterSettings.params).map((param, index) => {
|
Stop
|
||||||
return (
|
</Button> : null}
|
||||||
<Typography key={`recording-params-item-${index}`} sx={{margin: '10px'}}>
|
</TabPanel>
|
||||||
{/*@ts-ignore*/}
|
<TabPanel value='input' sx={{ width: '700px' }}>
|
||||||
{param}: {row.interpreterSettings.params[param].toString()}
|
<Typography variant='h6' sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
</Typography>
|
<SettingsIcon sx={{ marginRight: '15px' }} />
|
||||||
)
|
Interpreter settings
|
||||||
})
|
</Typography>
|
||||||
}
|
{
|
||||||
</div>
|
Object.keys(row.interpreterSettings).map((setting, index) => {
|
||||||
)
|
if (setting === 'params') {
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Typography key={`interpreter-settings-item-${index}`} sx={{margin: '10px'}}>
|
|
||||||
{/*@ts-ignore*/}
|
|
||||||
{setting}: {row.interpreterSettings[setting].toString()}
|
|
||||||
</Typography>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel value='output' sx={{width: '700px'}}>
|
|
||||||
{ !row || !row.serializableOutput || !row.binaryOutput
|
|
||||||
|| (Object.keys(row.serializableOutput).length === 0 && Object.keys(row.binaryOutput).length === 0)
|
|
||||||
? <Typography>The output is empty.</Typography> : null }
|
|
||||||
|
|
||||||
{row.serializableOutput &&
|
|
||||||
Object.keys(row.serializableOutput).length !== 0 &&
|
|
||||||
<div>
|
|
||||||
<Typography variant='h6' sx={{display:'flex', alignItems:'center'}}>
|
|
||||||
<ArticleIcon sx={{marginRight: '15px'}}/>
|
|
||||||
Serializable output</Typography>
|
|
||||||
{ Object.keys(row.serializableOutput).map((key) => {
|
|
||||||
return (
|
|
||||||
<div key={`number-of-serializable-output-${key}`}>
|
|
||||||
<Typography>
|
|
||||||
{key}:
|
|
||||||
<a href={`data:application/json;utf8,${JSON.stringify(row.serializableOutput[key], null, 2)}`}
|
|
||||||
download={key} style={{margin:'10px'}}>Download</a>
|
|
||||||
</Typography>
|
|
||||||
<Box sx={{
|
|
||||||
width: 'fit-content',
|
|
||||||
background: 'rgba(0,0,0,0.06)',
|
|
||||||
maxHeight: '300px',
|
|
||||||
overflow: 'scroll',
|
|
||||||
}}>
|
|
||||||
<pre key={`serializable-output-${key}`}>
|
|
||||||
{row.serializableOutput[key] ? JSON.stringify(row.serializableOutput[key], null, 2)
|
|
||||||
: 'The output is empty.'}
|
|
||||||
</pre>
|
|
||||||
</Box>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
{row.binaryOutput
|
|
||||||
&& Object.keys(row.binaryOutput).length !== 0 &&
|
|
||||||
<div>
|
|
||||||
<Typography variant='h6' sx={{display:'flex', alignItems:'center'}}>
|
|
||||||
<ImageIcon sx={{marginRight:'15px'}}/>
|
|
||||||
Binary output</Typography>
|
|
||||||
{ Object.keys(row.binaryOutput).map((key) => {
|
|
||||||
try {
|
|
||||||
const binaryBuffer = JSON.parse(row.binaryOutput[key].data);
|
|
||||||
const b64 = Buffer.from(binaryBuffer.data).toString('base64');
|
|
||||||
return (
|
return (
|
||||||
<Box key={`number-of-binary-output-${key}`} sx={{
|
<div key={`settings-${setting}-${index}`}>
|
||||||
width: 'max-content',
|
<Typography variant='h6' sx={{ display: 'flex', alignItems: 'center' }} key={`setting-${index}`}>
|
||||||
}}>
|
<AssignmentIcon sx={{ marginRight: '15px' }} />
|
||||||
<Typography key={`binary-output-key-${key}`}>
|
Recording parameters
|
||||||
{key}:
|
|
||||||
<a href={`data:${row.binaryOutput[key].mimetype};base64,${b64}`}
|
|
||||||
download={key} style={{margin:'10px'}}>Download</a>
|
|
||||||
</Typography>
|
</Typography>
|
||||||
<img key={`image-${key}`} src={`data:${row.binaryOutput[key].mimetype};base64,${b64}`}
|
{
|
||||||
alt={key} height='auto' width='700px'/>
|
Object.keys(row.interpreterSettings.params).map((param, index) => {
|
||||||
</Box>
|
return (
|
||||||
|
<Typography key={`recording-params-item-${index}`} sx={{ margin: '10px' }}>
|
||||||
|
{/*@ts-ignore*/}
|
||||||
|
{param}: {row.interpreterSettings.params[param].toString()}
|
||||||
|
</Typography>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
} catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
return <Typography key={`number-of-binary-output-${key}`}>
|
|
||||||
{key}: The image failed to render
|
|
||||||
</Typography>
|
|
||||||
}
|
}
|
||||||
|
return (
|
||||||
|
<Typography key={`interpreter-settings-item-${index}`} sx={{ margin: '10px' }}>
|
||||||
|
{/*@ts-ignore*/}
|
||||||
|
{setting}: {row.interpreterSettings[setting].toString()}
|
||||||
|
</Typography>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value='output' sx={{ width: '700px' }}>
|
||||||
|
{!row || !row.serializableOutput || !row.binaryOutput
|
||||||
|
|| (Object.keys(row.serializableOutput).length === 0 && Object.keys(row.binaryOutput).length === 0)
|
||||||
|
? <Typography>The output is empty.</Typography> : null}
|
||||||
|
|
||||||
|
{row.serializableOutput &&
|
||||||
|
Object.keys(row.serializableOutput).length !== 0 &&
|
||||||
|
<div>
|
||||||
|
<Typography variant='h6' sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<ArticleIcon sx={{ marginRight: '15px' }} />
|
||||||
|
Serializable output</Typography>
|
||||||
|
{Object.keys(row.serializableOutput).map((key) => {
|
||||||
|
return (
|
||||||
|
<div key={`number-of-serializable-output-${key}`}>
|
||||||
|
<Typography>
|
||||||
|
{key}:
|
||||||
|
<a href={`data:application/json;utf8,${JSON.stringify(row.serializableOutput[key], null, 2)}`}
|
||||||
|
download={key} style={{ margin: '10px' }}>Download</a>
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{
|
||||||
|
width: 'fit-content',
|
||||||
|
background: 'rgba(0,0,0,0.06)',
|
||||||
|
maxHeight: '300px',
|
||||||
|
overflow: 'scroll',
|
||||||
|
}}>
|
||||||
|
<pre key={`serializable-output-${key}`}>
|
||||||
|
{row.serializableOutput[key] ? JSON.stringify(row.serializableOutput[key], null, 2)
|
||||||
|
: 'The output is empty.'}
|
||||||
|
</pre>
|
||||||
|
</Box>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</TabPanel>
|
{row.binaryOutput
|
||||||
|
&& Object.keys(row.binaryOutput).length !== 0 &&
|
||||||
|
<div>
|
||||||
|
<Typography variant='h6' sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<ImageIcon sx={{ marginRight: '15px' }} />
|
||||||
|
Binary output</Typography>
|
||||||
|
{Object.keys(row.binaryOutput).map((key) => {
|
||||||
|
try {
|
||||||
|
const binaryBuffer = JSON.parse(row.binaryOutput[key].data);
|
||||||
|
const b64 = Buffer.from(binaryBuffer.data).toString('base64');
|
||||||
|
return (
|
||||||
|
<Box key={`number-of-binary-output-${key}`} sx={{
|
||||||
|
width: 'max-content',
|
||||||
|
}}>
|
||||||
|
<Typography key={`binary-output-key-${key}`}>
|
||||||
|
{key}:
|
||||||
|
<a href={`data:${row.binaryOutput[key].mimetype};base64,${b64}`}
|
||||||
|
download={key} style={{ margin: '10px' }}>Download</a>
|
||||||
|
</Typography>
|
||||||
|
<img key={`image-${key}`} src={`data:${row.binaryOutput[key].mimetype};base64,${b64}`}
|
||||||
|
alt={key} height='auto' width='700px' />
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
return <Typography key={`number-of-binary-output-${key}`}>
|
||||||
|
{key}: The image failed to render
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</TabPanel>
|
||||||
</TabContext>
|
</TabContext>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const initialState = {
|
|||||||
|
|
||||||
const AuthContext = createContext<{
|
const AuthContext = createContext<{
|
||||||
state: InitialStateType;
|
state: InitialStateType;
|
||||||
dispatch: any;
|
dispatch: React.Dispatch<ActionType>;
|
||||||
}>({
|
}>({
|
||||||
state: initialState,
|
state: initialState,
|
||||||
dispatch: () => null,
|
dispatch: () => null,
|
||||||
@@ -46,38 +46,34 @@ const reducer = (state: InitialStateType, action: ActionType) => {
|
|||||||
|
|
||||||
const AuthProvider = ({ children }: AuthProviderProps) => {
|
const AuthProvider = ({ children }: AuthProviderProps) => {
|
||||||
const [state, dispatch] = useReducer(reducer, initialState);
|
const [state, dispatch] = useReducer(reducer, initialState);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
axios.defaults.withCredentials = true;
|
axios.defaults.withCredentials = true;
|
||||||
|
|
||||||
// get user info from local storage
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch({
|
const storedUser = window.localStorage.getItem('user');
|
||||||
type: 'LOGIN',
|
if (storedUser) {
|
||||||
payload: JSON.parse(window.localStorage.getItem('user') || 'null'),
|
dispatch({ type: 'LOGIN', payload: JSON.parse(storedUser) });
|
||||||
});
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
axios.interceptors.response.use(
|
axios.interceptors.response.use(
|
||||||
function (response) {
|
function (response) {
|
||||||
// any status code that lies within the range of 2XX causes this function to trigger
|
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
function (error) {
|
function (error) {
|
||||||
// any status codes that fall outside the range of 2XX cause this function to trigger
|
const res = error.response;
|
||||||
let res = error.response;
|
|
||||||
if (res.status === 401 && res.config && !res.config.__isRetryRequest) {
|
if (res.status === 401 && res.config && !res.config.__isRetryRequest) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
axios
|
axios
|
||||||
.get('http://localhost:8080/auth/logout')
|
.get('http://localhost:8080/auth/logout')
|
||||||
.then((data) => {
|
.then(() => {
|
||||||
console.log('/401 error > logout');
|
console.log('/401 error > logout');
|
||||||
dispatch({ type: 'LOGOUT' });
|
dispatch({ type: 'LOGOUT' });
|
||||||
window.localStorage.removeItem('user');
|
window.localStorage.removeItem('user');
|
||||||
navigate('/login'); // Replace router.push with navigate
|
navigate('/login');
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log('AXIOS INTERCEPTORS ERROR:', err);
|
console.error('AXIOS INTERCEPTORS ERROR:', err);
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -86,16 +82,12 @@ const AuthProvider = ({ children }: AuthProviderProps) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// csrf - include tokens in the axios header every time a request is made
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const getCsrfToken = async () => {
|
const getCsrfToken = async () => {
|
||||||
try {
|
try {
|
||||||
const { data } = await axios.get('http://localhost:8080/csrf-token');
|
const { data } = await axios.get('http://localhost:8080/csrf-token');
|
||||||
console.log('CSRF Token Response:', data);
|
if (data.csrfToken) {
|
||||||
if (data && data.csrfToken) {
|
|
||||||
(axios.defaults.headers as any)['X-CSRF-TOKEN'] = data.csrfToken;
|
(axios.defaults.headers as any)['X-CSRF-TOKEN'] = data.csrfToken;
|
||||||
} else {
|
|
||||||
console.error('CSRF token not found in the response');
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching CSRF token:', error);
|
console.error('Error fetching CSRF token:', error);
|
||||||
@@ -105,8 +97,10 @@ const AuthProvider = ({ children }: AuthProviderProps) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider value={{ state, dispatch }}>{children}</AuthContext.Provider>
|
<AuthContext.Provider value={{ state, dispatch }}>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { AuthContext, AuthProvider };
|
export { AuthContext, AuthProvider };
|
||||||
Reference in New Issue
Block a user