Add ApiContextDebug component for API server debugging and testing functionality; removed PrintServerContextDebug component.
This commit is contained in:
parent
fbdb451659
commit
f0cb4b3b83
842
src/components/Dashboard/Developer/ApiContextDebug.jsx
Normal file
842
src/components/Dashboard/Developer/ApiContextDebug.jsx
Normal file
@ -0,0 +1,842 @@
|
||||
import { useContext, useState, useEffect } from 'react'
|
||||
import {
|
||||
Descriptions,
|
||||
Button,
|
||||
Typography,
|
||||
Flex,
|
||||
Space,
|
||||
Dropdown,
|
||||
message,
|
||||
Tag,
|
||||
Input,
|
||||
InputNumber,
|
||||
Select
|
||||
} from 'antd'
|
||||
import ReloadIcon from '../../Icons/ReloadIcon.jsx'
|
||||
import CloudIcon from '../../Icons/CloudIcon.jsx'
|
||||
import { ApiServerContext } from '../context/ApiServerContext.jsx'
|
||||
import BoolDisplay from '../common/BoolDisplay.jsx'
|
||||
import InfoCollapse from '../common/InfoCollapse.jsx'
|
||||
|
||||
const { Text, Paragraph } = Typography
|
||||
|
||||
const ApiContextDebug = () => {
|
||||
const {
|
||||
apiServer,
|
||||
error,
|
||||
connecting,
|
||||
connected,
|
||||
fetchLoading,
|
||||
lockObject,
|
||||
unlockObject,
|
||||
fetchObjectLock,
|
||||
updateObject,
|
||||
createObject,
|
||||
deleteObject,
|
||||
subscribeToObjectUpdates,
|
||||
subscribeToObjectEvent,
|
||||
subscribeToObjectTypeUpdates,
|
||||
subscribeToObjectLock,
|
||||
fetchObject,
|
||||
fetchObjects,
|
||||
fetchObjectsByProperty,
|
||||
fetchSpotlightData,
|
||||
fetchObjectContent,
|
||||
fetchTemplatePreview,
|
||||
fetchNotes,
|
||||
fetchHostOTP,
|
||||
sendObjectAction
|
||||
} = useContext(ApiServerContext)
|
||||
|
||||
const [msgApi, contextHolder] = message.useMessage()
|
||||
const [connectionStatus, setConnectionStatus] = useState('disconnected')
|
||||
const [socketId, setSocketId] = useState(null)
|
||||
|
||||
// Test input states
|
||||
const [testInputs, setTestInputs] = useState({
|
||||
objectId: 'test-id',
|
||||
objectType: 'user',
|
||||
hostId: 'test-host-id',
|
||||
parentId: 'test-parent-id',
|
||||
fileName: 'test.gcode',
|
||||
query: 'test query',
|
||||
action: 'start',
|
||||
eventType: 'status',
|
||||
properties: ['name', 'email'],
|
||||
filter: { active: true },
|
||||
scale: 1.0,
|
||||
templateContent: 'Test template content',
|
||||
testObject: { name: 'Test Object' }
|
||||
})
|
||||
|
||||
// Collapse states
|
||||
const [collapseStates, setCollapseStates] = useState({
|
||||
fetchTests: false,
|
||||
crudTests: false,
|
||||
subscriptionTests: false,
|
||||
specialTests: false
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (apiServer) {
|
||||
setConnectionStatus(apiServer.connected ? 'connected' : 'disconnected')
|
||||
setSocketId(apiServer.id)
|
||||
|
||||
// Listen for connection status changes
|
||||
const handleConnect = () => {
|
||||
setConnectionStatus('connected')
|
||||
setSocketId(apiServer.id)
|
||||
}
|
||||
|
||||
const handleDisconnect = () => {
|
||||
setConnectionStatus('disconnected')
|
||||
setSocketId(null)
|
||||
}
|
||||
|
||||
apiServer.on('connect', handleConnect)
|
||||
apiServer.on('disconnect', handleDisconnect)
|
||||
|
||||
return () => {
|
||||
apiServer.off('connect', handleConnect)
|
||||
apiServer.off('disconnect', handleDisconnect)
|
||||
}
|
||||
} else {
|
||||
setConnectionStatus('disconnected')
|
||||
setSocketId(null)
|
||||
}
|
||||
}, [apiServer])
|
||||
|
||||
// Helper functions
|
||||
const updateTestInput = (key, value) => {
|
||||
setTestInputs((prev) => ({ ...prev, [key]: value }))
|
||||
}
|
||||
|
||||
const toggleCollapse = (key) => {
|
||||
setCollapseStates((prev) => ({ ...prev, [key]: !prev[key] }))
|
||||
}
|
||||
|
||||
const handleReconnect = () => {
|
||||
if (apiServer) {
|
||||
apiServer.connect()
|
||||
msgApi.info('Attempting to reconnect...')
|
||||
} else {
|
||||
msgApi.warning('No API server instance available')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDisconnect = () => {
|
||||
if (apiServer) {
|
||||
apiServer.disconnect()
|
||||
msgApi.info('Disconnected from API server')
|
||||
}
|
||||
}
|
||||
|
||||
const testFetchObject = async () => {
|
||||
try {
|
||||
msgApi.loading('Testing fetchObject...', 0)
|
||||
const result = await fetchObject(
|
||||
testInputs.objectId,
|
||||
testInputs.objectType
|
||||
)
|
||||
msgApi.destroy()
|
||||
msgApi.success('fetchObject test completed')
|
||||
console.log('fetchObject result:', result)
|
||||
} catch (err) {
|
||||
msgApi.destroy()
|
||||
msgApi.error('fetchObject test failed')
|
||||
console.error('fetchObject error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const testFetchObjects = async () => {
|
||||
try {
|
||||
msgApi.loading('Testing fetchObjects...', 0)
|
||||
const result = await fetchObjects(testInputs.objectType, {
|
||||
page: 1,
|
||||
limit: 5
|
||||
})
|
||||
msgApi.destroy()
|
||||
msgApi.success('fetchObjects test completed')
|
||||
console.log('fetchObjects result:', result)
|
||||
} catch (err) {
|
||||
msgApi.destroy()
|
||||
msgApi.error('fetchObjects test failed')
|
||||
console.error('fetchObjects error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const testLockObject = () => {
|
||||
try {
|
||||
lockObject(testInputs.objectId, testInputs.objectType)
|
||||
msgApi.success('Lock command sent')
|
||||
} catch (err) {
|
||||
msgApi.error('Lock command failed')
|
||||
console.error('Lock error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const testUnlockObject = () => {
|
||||
try {
|
||||
unlockObject(testInputs.objectId, testInputs.objectType)
|
||||
msgApi.success('Unlock command sent')
|
||||
} catch (err) {
|
||||
msgApi.error('Unlock command failed')
|
||||
console.error('Unlock error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const testFetchObjectLock = async () => {
|
||||
try {
|
||||
msgApi.loading('Testing fetchObjectLock...', 0)
|
||||
const result = await fetchObjectLock(
|
||||
testInputs.objectId,
|
||||
testInputs.objectType
|
||||
)
|
||||
msgApi.destroy()
|
||||
msgApi.success('fetchObjectLock test completed')
|
||||
console.log('fetchObjectLock result:', result)
|
||||
} catch (err) {
|
||||
msgApi.destroy()
|
||||
msgApi.error('fetchObjectLock test failed')
|
||||
console.error('fetchObjectLock error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const testUpdateObject = async () => {
|
||||
try {
|
||||
msgApi.loading('Testing updateObject...', 0)
|
||||
const testData = {
|
||||
name: 'Test Update',
|
||||
updated: new Date().toISOString()
|
||||
}
|
||||
const result = await updateObject(
|
||||
testInputs.objectId,
|
||||
testInputs.objectType,
|
||||
testData
|
||||
)
|
||||
msgApi.destroy()
|
||||
msgApi.success('updateObject test completed')
|
||||
console.log('updateObject result:', result)
|
||||
} catch (err) {
|
||||
msgApi.destroy()
|
||||
msgApi.error('updateObject test failed')
|
||||
console.error('updateObject error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const testCreateObject = async () => {
|
||||
try {
|
||||
msgApi.loading('Testing createObject...', 0)
|
||||
const testData = {
|
||||
name: 'Test Create',
|
||||
created: new Date().toISOString()
|
||||
}
|
||||
const result = await createObject(testInputs.objectType, testData)
|
||||
msgApi.destroy()
|
||||
msgApi.success('createObject test completed')
|
||||
console.log('createObject result:', result)
|
||||
} catch (err) {
|
||||
msgApi.destroy()
|
||||
msgApi.error('createObject test failed')
|
||||
console.error('createObject error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const testDeleteObject = async () => {
|
||||
try {
|
||||
msgApi.loading('Testing deleteObject...', 0)
|
||||
const result = await deleteObject(
|
||||
testInputs.objectId,
|
||||
testInputs.objectType
|
||||
)
|
||||
msgApi.destroy()
|
||||
msgApi.success('deleteObject test completed')
|
||||
console.log('deleteObject result:', result)
|
||||
} catch (err) {
|
||||
msgApi.destroy()
|
||||
msgApi.error('deleteObject test failed')
|
||||
console.error('deleteObject error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const testFetchObjectsByProperty = async () => {
|
||||
try {
|
||||
msgApi.loading('Testing fetchObjectsByProperty...', 0)
|
||||
const params = {
|
||||
properties: testInputs.properties,
|
||||
filter: testInputs.filter,
|
||||
masterFilter: {}
|
||||
}
|
||||
const result = await fetchObjectsByProperty(testInputs.objectType, params)
|
||||
msgApi.destroy()
|
||||
msgApi.success('fetchObjectsByProperty test completed')
|
||||
console.log('fetchObjectsByProperty result:', result)
|
||||
} catch (err) {
|
||||
msgApi.destroy()
|
||||
msgApi.error('fetchObjectsByProperty test failed')
|
||||
console.error('fetchObjectsByProperty error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const testFetchSpotlightData = async () => {
|
||||
try {
|
||||
msgApi.loading('Testing fetchSpotlightData...', 0)
|
||||
const result = await fetchSpotlightData(testInputs.query)
|
||||
msgApi.destroy()
|
||||
msgApi.success('fetchSpotlightData test completed')
|
||||
console.log('fetchSpotlightData result:', result)
|
||||
} catch (err) {
|
||||
msgApi.destroy()
|
||||
msgApi.error('fetchSpotlightData test failed')
|
||||
console.error('fetchSpotlightData error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const testFetchObjectContent = async () => {
|
||||
try {
|
||||
msgApi.loading('Testing fetchObjectContent...', 0)
|
||||
await fetchObjectContent(
|
||||
testInputs.objectId,
|
||||
'gcodefile',
|
||||
testInputs.fileName
|
||||
)
|
||||
msgApi.destroy()
|
||||
msgApi.success('fetchObjectContent test completed')
|
||||
} catch (err) {
|
||||
msgApi.destroy()
|
||||
msgApi.error('fetchObjectContent test failed')
|
||||
console.error('fetchObjectContent error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const testFetchTemplatePreview = async () => {
|
||||
try {
|
||||
msgApi.loading('Testing fetchTemplatePreview...', 0)
|
||||
|
||||
fetchTemplatePreview(
|
||||
testInputs.objectId,
|
||||
testInputs.templateContent,
|
||||
testInputs.testObject,
|
||||
testInputs.scale,
|
||||
(result) => {
|
||||
msgApi.destroy()
|
||||
if (result.success) {
|
||||
msgApi.success('fetchTemplatePreview test completed')
|
||||
} else {
|
||||
msgApi.error('fetchTemplatePreview test failed')
|
||||
}
|
||||
console.log('fetchTemplatePreview result:', result)
|
||||
}
|
||||
)
|
||||
} catch (err) {
|
||||
msgApi.destroy()
|
||||
msgApi.error('fetchTemplatePreview test failed')
|
||||
console.error('fetchTemplatePreview error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const testFetchNotes = async () => {
|
||||
try {
|
||||
msgApi.loading('Testing fetchNotes...', 0)
|
||||
const result = await fetchNotes(testInputs.parentId)
|
||||
msgApi.destroy()
|
||||
msgApi.success('fetchNotes test completed')
|
||||
console.log('fetchNotes result:', result)
|
||||
} catch (err) {
|
||||
msgApi.destroy()
|
||||
msgApi.error('fetchNotes test failed')
|
||||
console.error('fetchNotes error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const testFetchHostOTP = async () => {
|
||||
try {
|
||||
msgApi.loading('Testing fetchHostOTP...', 0)
|
||||
|
||||
fetchHostOTP(testInputs.hostId, (result) => {
|
||||
msgApi.destroy()
|
||||
if (result.otp) {
|
||||
msgApi.success('fetchHostOTP test completed: ' + result.otp)
|
||||
} else {
|
||||
msgApi.error('fetchHostOTP test failed')
|
||||
}
|
||||
console.log('fetchHostOTP result:', result)
|
||||
})
|
||||
} catch (err) {
|
||||
msgApi.destroy()
|
||||
msgApi.error('fetchHostOTP test failed')
|
||||
console.error('fetchHostOTP error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const testSendObjectAction = async () => {
|
||||
try {
|
||||
msgApi.loading('Testing sendObjectAction...', 0)
|
||||
|
||||
sendObjectAction(
|
||||
testInputs.objectId,
|
||||
'printer',
|
||||
testInputs.action,
|
||||
(result) => {
|
||||
msgApi.destroy()
|
||||
if (result.success) {
|
||||
msgApi.success('sendObjectAction test completed')
|
||||
} else {
|
||||
msgApi.error('sendObjectAction test failed')
|
||||
}
|
||||
console.log('sendObjectAction result:', result)
|
||||
}
|
||||
)
|
||||
} catch (err) {
|
||||
msgApi.destroy()
|
||||
msgApi.error('sendObjectAction test failed')
|
||||
console.error('sendObjectAction error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const testSubscribeToObjectUpdates = () => {
|
||||
try {
|
||||
const callback = (data) => {
|
||||
console.log('Object update received:', data)
|
||||
}
|
||||
const unsubscribe = subscribeToObjectUpdates(
|
||||
testInputs.objectId,
|
||||
testInputs.objectType,
|
||||
callback
|
||||
)
|
||||
msgApi.success('Subscribed to object updates')
|
||||
console.log('Subscribed to object updates for test-id')
|
||||
|
||||
// Store unsubscribe function for cleanup
|
||||
setTimeout(() => {
|
||||
if (unsubscribe) {
|
||||
unsubscribe()
|
||||
console.log('Unsubscribed from object updates')
|
||||
}
|
||||
}, 10000) // Auto-unsubscribe after 10 seconds
|
||||
} catch (err) {
|
||||
msgApi.error('Subscribe to object updates failed')
|
||||
console.error('Subscribe error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const testSubscribeToObjectEvent = () => {
|
||||
try {
|
||||
const callback = (event) => {
|
||||
console.log('Object event received:', event)
|
||||
}
|
||||
const unsubscribe = subscribeToObjectEvent(
|
||||
testInputs.objectId,
|
||||
'printer',
|
||||
testInputs.eventType,
|
||||
callback
|
||||
)
|
||||
msgApi.success('Subscribed to object events')
|
||||
console.log('Subscribed to object events for test-id')
|
||||
|
||||
// Store unsubscribe function for cleanup
|
||||
setTimeout(() => {
|
||||
if (unsubscribe) {
|
||||
unsubscribe()
|
||||
console.log('Unsubscribed from object events')
|
||||
}
|
||||
}, 10000) // Auto-unsubscribe after 10 seconds
|
||||
} catch (err) {
|
||||
msgApi.error('Subscribe to object events failed')
|
||||
console.error('Subscribe error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const testSubscribeToObjectTypeUpdates = () => {
|
||||
try {
|
||||
const callback = (data) => {
|
||||
console.log('Object type update received:', data)
|
||||
}
|
||||
const unsubscribe = subscribeToObjectTypeUpdates(
|
||||
testInputs.objectType,
|
||||
callback
|
||||
)
|
||||
msgApi.success('Subscribed to object type updates')
|
||||
console.log('Subscribed to object type updates for user')
|
||||
|
||||
// Store unsubscribe function for cleanup
|
||||
setTimeout(() => {
|
||||
if (unsubscribe) {
|
||||
unsubscribe()
|
||||
console.log('Unsubscribed from object type updates')
|
||||
}
|
||||
}, 10000) // Auto-unsubscribe after 10 seconds
|
||||
} catch (err) {
|
||||
msgApi.error('Subscribe to object type updates failed')
|
||||
console.error('Subscribe error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const testSubscribeToObjectLock = () => {
|
||||
try {
|
||||
const callback = (lockData) => {
|
||||
console.log('Object lock update received:', lockData)
|
||||
}
|
||||
const unsubscribe = subscribeToObjectLock(
|
||||
testInputs.objectId,
|
||||
testInputs.objectType,
|
||||
callback
|
||||
)
|
||||
msgApi.success('Subscribed to object lock updates')
|
||||
console.log('Subscribed to object lock updates for test-id')
|
||||
|
||||
// Store unsubscribe function for cleanup
|
||||
setTimeout(() => {
|
||||
if (unsubscribe) {
|
||||
unsubscribe()
|
||||
console.log('Unsubscribed from object lock updates')
|
||||
}
|
||||
}, 10000) // Auto-unsubscribe after 10 seconds
|
||||
} catch (err) {
|
||||
msgApi.error('Subscribe to object lock updates failed')
|
||||
console.error('Subscribe error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const actionItems = {
|
||||
items: [
|
||||
{
|
||||
label: 'Reconnect',
|
||||
key: 'reconnect',
|
||||
disabled: connected
|
||||
},
|
||||
{
|
||||
label: 'Disconnect',
|
||||
key: 'disconnect',
|
||||
disabled: !connected
|
||||
},
|
||||
{
|
||||
type: 'divider'
|
||||
},
|
||||
{
|
||||
label: 'Reload',
|
||||
key: 'reload',
|
||||
icon: <ReloadIcon />
|
||||
}
|
||||
],
|
||||
onClick: ({ key }) => {
|
||||
switch (key) {
|
||||
case 'reconnect':
|
||||
handleReconnect()
|
||||
break
|
||||
case 'disconnect':
|
||||
handleDisconnect()
|
||||
break
|
||||
case 'reload':
|
||||
msgApi.info('Reloading API State...')
|
||||
window.location.reload()
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getConnectionStatusColor = () => {
|
||||
switch (connectionStatus) {
|
||||
case 'connected':
|
||||
return 'success'
|
||||
case 'connecting':
|
||||
return 'processing'
|
||||
case 'disconnected':
|
||||
return 'error'
|
||||
default:
|
||||
return 'default'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex vertical gap='large' style={{ height: '100%', minHeight: 0 }}>
|
||||
{contextHolder}
|
||||
|
||||
{/* Header with Actions */}
|
||||
<Flex justify={'space-between'} align={'center'}>
|
||||
<Space>
|
||||
<Dropdown menu={actionItems}>
|
||||
<Button>Actions</Button>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
</Flex>
|
||||
|
||||
<Descriptions bordered>
|
||||
<Descriptions.Item label='Status'>
|
||||
<Space>
|
||||
<Tag color={getConnectionStatusColor()} icon={<CloudIcon />}>
|
||||
{connectionStatus.charAt(0).toUpperCase() +
|
||||
connectionStatus.slice(1)}
|
||||
</Tag>
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label='Connecting'>
|
||||
<BoolDisplay value={connecting} />
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label='Connected'>
|
||||
<BoolDisplay value={connected} />
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label='Socket ID'>
|
||||
<Text code>{socketId || 'None'}</Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label='Fetch Loading'>
|
||||
<BoolDisplay value={fetchLoading} />
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label='Error'>
|
||||
<Text type={error ? 'danger' : 'secondary'}>{error || 'None'}</Text>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<InfoCollapse
|
||||
title='Params'
|
||||
icon={<span>🔎</span>}
|
||||
active={collapseStates.params}
|
||||
onToggle={() => toggleCollapse('params')}
|
||||
collapseKey='params'
|
||||
>
|
||||
<Descriptions bordered column={2}>
|
||||
<Descriptions.Item label='Object ID'>
|
||||
<Input
|
||||
value={testInputs.objectId}
|
||||
onChange={(e) => updateTestInput('objectId', e.target.value)}
|
||||
placeholder='Enter object ID'
|
||||
size='small'
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label='Object Type'>
|
||||
<Select
|
||||
value={testInputs.objectType}
|
||||
onChange={(value) => updateTestInput('objectType', value)}
|
||||
style={{ width: '100%' }}
|
||||
options={[
|
||||
{ value: 'user', label: 'User' },
|
||||
{ value: 'printer', label: 'Printer' },
|
||||
{ value: 'job', label: 'Job' },
|
||||
{ value: 'filament', label: 'Filament' },
|
||||
{ value: 'gcodefile', label: 'GCode File' }
|
||||
]}
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label='Host ID'>
|
||||
<Input
|
||||
value={testInputs.hostId}
|
||||
onChange={(e) => updateTestInput('hostId', e.target.value)}
|
||||
placeholder='Enter host ID'
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label='Parent ID'>
|
||||
<Input
|
||||
value={testInputs.parentId}
|
||||
onChange={(e) => updateTestInput('parentId', e.target.value)}
|
||||
placeholder='Enter parent ID'
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label='File Name'>
|
||||
<Input
|
||||
value={testInputs.fileName}
|
||||
onChange={(e) => updateTestInput('fileName', e.target.value)}
|
||||
placeholder='Enter file name'
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label='Query'>
|
||||
<Input
|
||||
value={testInputs.query}
|
||||
onChange={(e) => updateTestInput('query', e.target.value)}
|
||||
placeholder='Enter search query'
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label='Action'>
|
||||
<Select
|
||||
value={testInputs.action}
|
||||
onChange={(value) => updateTestInput('action', value)}
|
||||
style={{ width: '100%' }}
|
||||
options={[
|
||||
{ value: 'start', label: 'Start' },
|
||||
{ value: 'stop', label: 'Stop' },
|
||||
{ value: 'pause', label: 'Pause' },
|
||||
{ value: 'resume', label: 'Resume' }
|
||||
]}
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label='Event Type'>
|
||||
<Select
|
||||
value={testInputs.eventType}
|
||||
onChange={(value) => updateTestInput('eventType', value)}
|
||||
style={{ width: '100%' }}
|
||||
options={[
|
||||
{ value: 'status', label: 'Status' },
|
||||
{ value: 'progress', label: 'Progress' },
|
||||
{ value: 'error', label: 'Error' },
|
||||
{ value: 'complete', label: 'Complete' }
|
||||
]}
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label='Scale'>
|
||||
<InputNumber
|
||||
value={testInputs.scale}
|
||||
onChange={(value) => updateTestInput('scale', value)}
|
||||
min={0.1}
|
||||
max={10}
|
||||
step={0.1}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label='Template Content' span={1}>
|
||||
<Input.TextArea
|
||||
value={testInputs.templateContent}
|
||||
onChange={(e) =>
|
||||
updateTestInput('templateContent', e.target.value)
|
||||
}
|
||||
placeholder='Enter template content'
|
||||
rows={2}
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</InfoCollapse>
|
||||
|
||||
{/* Test Sections */}
|
||||
<div style={{ height: '100%', overflow: 'auto' }}>
|
||||
<Flex vertical gap={'large'}>
|
||||
{/* Fetch Tests */}
|
||||
<InfoCollapse
|
||||
title='Fetch Tests'
|
||||
icon={<span>📥</span>}
|
||||
active={collapseStates.fetchTests}
|
||||
onToggle={() => toggleCollapse('fetchTests')}
|
||||
collapseKey='fetchTests'
|
||||
>
|
||||
<Space direction='vertical' style={{ width: '100%' }}>
|
||||
<Button onClick={testFetchObject} block>
|
||||
Test fetchObject
|
||||
</Button>
|
||||
<Button onClick={testFetchObjects} block>
|
||||
Test fetchObjects
|
||||
</Button>
|
||||
<Button onClick={testFetchObjectLock} block>
|
||||
Test fetchObjectLock
|
||||
</Button>
|
||||
<Button onClick={testFetchObjectsByProperty} block>
|
||||
Test fetchObjectsByProperty
|
||||
</Button>
|
||||
<Button onClick={testFetchSpotlightData} block>
|
||||
Test fetchSpotlightData
|
||||
</Button>
|
||||
<Button onClick={testFetchObjectContent} block>
|
||||
Test fetchObjectContent
|
||||
</Button>
|
||||
<Button onClick={testFetchNotes} block>
|
||||
Test fetchNotes
|
||||
</Button>
|
||||
</Space>
|
||||
</InfoCollapse>
|
||||
|
||||
{/* CRUD Tests */}
|
||||
<InfoCollapse
|
||||
title='CRUD Tests'
|
||||
icon={<span>✏️</span>}
|
||||
active={collapseStates.crudTests}
|
||||
onToggle={() => toggleCollapse('crudTests')}
|
||||
collapseKey='crudTests'
|
||||
>
|
||||
<Space direction='vertical' style={{ width: '100%' }}>
|
||||
<Button onClick={testLockObject} block>
|
||||
Test Lock Object
|
||||
</Button>
|
||||
<Button onClick={testUnlockObject} block>
|
||||
Test Unlock Object
|
||||
</Button>
|
||||
<Button onClick={testUpdateObject} block>
|
||||
Test updateObject
|
||||
</Button>
|
||||
<Button onClick={testCreateObject} block>
|
||||
Test createObject
|
||||
</Button>
|
||||
<Button onClick={testDeleteObject} block>
|
||||
Test deleteObject
|
||||
</Button>
|
||||
</Space>
|
||||
</InfoCollapse>
|
||||
|
||||
{/* Special Tests */}
|
||||
<InfoCollapse
|
||||
title='Special Tests'
|
||||
icon={<span>🔧</span>}
|
||||
active={collapseStates.specialTests}
|
||||
onToggle={() => toggleCollapse('specialTests')}
|
||||
collapseKey='specialTests'
|
||||
>
|
||||
<Space direction='vertical' style={{ width: '100%' }}>
|
||||
<Button onClick={testFetchTemplatePreview} block>
|
||||
Test fetchTemplatePreview
|
||||
</Button>
|
||||
<Button onClick={testFetchHostOTP} block>
|
||||
Test fetchHostOTP
|
||||
</Button>
|
||||
<Button onClick={testSendObjectAction} block>
|
||||
Test sendObjectAction
|
||||
</Button>
|
||||
</Space>
|
||||
</InfoCollapse>
|
||||
|
||||
{/* Subscription Tests */}
|
||||
<InfoCollapse
|
||||
title='Subscription Tests'
|
||||
icon={<span>📡</span>}
|
||||
active={collapseStates.subscriptionTests}
|
||||
onToggle={() => toggleCollapse('subscriptionTests')}
|
||||
collapseKey='subscriptionTests'
|
||||
>
|
||||
<Space direction='vertical' style={{ width: '100%' }}>
|
||||
<Button onClick={testSubscribeToObjectUpdates} block>
|
||||
Test Subscribe Object Updates
|
||||
</Button>
|
||||
<Button onClick={testSubscribeToObjectEvent} block>
|
||||
Test Subscribe Object Events
|
||||
</Button>
|
||||
<Button onClick={testSubscribeToObjectTypeUpdates} block>
|
||||
Test Subscribe Object Type Updates
|
||||
</Button>
|
||||
<Button onClick={testSubscribeToObjectLock} block>
|
||||
Test Subscribe Object Lock
|
||||
</Button>
|
||||
</Space>
|
||||
</InfoCollapse>
|
||||
|
||||
{/* API Server Instance Info */}
|
||||
<InfoCollapse
|
||||
title='API Server Instance'
|
||||
icon={<span>🖥️</span>}
|
||||
active={false}
|
||||
onToggle={() => {}}
|
||||
collapseKey='apiServer'
|
||||
>
|
||||
<pre style={{ margin: 0, fontSize: 12 }}>
|
||||
{apiServer ? (
|
||||
<Paragraph>
|
||||
<pre>
|
||||
{JSON.stringify(
|
||||
{
|
||||
connected: apiServer.connected,
|
||||
id: apiServer.id,
|
||||
transport: apiServer.io.engine.transport.name,
|
||||
readyState: apiServer.io.engine.readyState
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}
|
||||
</pre>
|
||||
</Paragraph>
|
||||
) : (
|
||||
<Text type='secondary'>No API server instance</Text>
|
||||
)}
|
||||
</pre>
|
||||
</InfoCollapse>
|
||||
</Flex>
|
||||
</div>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
export default ApiContextDebug
|
||||
@ -1,77 +0,0 @@
|
||||
import { useContext } from 'react'
|
||||
import {
|
||||
Descriptions,
|
||||
Button,
|
||||
Typography,
|
||||
Flex,
|
||||
Space,
|
||||
Dropdown,
|
||||
message
|
||||
} from 'antd'
|
||||
import ReloadIcon from '../../Icons/ReloadIcon.jsx'
|
||||
import { PrintServerContext } from '../context/PrintServerContext.jsx'
|
||||
import BoolDisplay from '../common/BoolDisplay.jsx'
|
||||
|
||||
const { Text, Paragraph } = Typography
|
||||
|
||||
const PrintServerContextDebug = () => {
|
||||
const { printServer, error, connecting } = useContext(PrintServerContext)
|
||||
const [msgApi, contextHolder] = message.useMessage()
|
||||
|
||||
const actionItems = {
|
||||
items: [
|
||||
{
|
||||
label: 'Reload',
|
||||
key: 'reload',
|
||||
icon: <ReloadIcon />
|
||||
}
|
||||
],
|
||||
onClick: ({ key }) => {
|
||||
if (key === 'reload') {
|
||||
msgApi.info('Reloading Page...')
|
||||
window.location.reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to display socket info safely
|
||||
const getSocketInfo = () => {
|
||||
if (!printServer) return 'n/a'
|
||||
// Only show safe properties
|
||||
const { id, connected, disconnected, nsp } = printServer
|
||||
return JSON.stringify({ id, connected, disconnected, nsp }, null, 2)
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex vertical gap='large' style={{ height: '100%', minHeight: 0 }}>
|
||||
{contextHolder}
|
||||
<Flex justify={'space-between'} align={'center'}>
|
||||
<Space>
|
||||
<Dropdown menu={actionItems}>
|
||||
<Button>Actions</Button>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
</Flex>
|
||||
<div style={{ height: '100%', overflow: 'auto' }}>
|
||||
<Descriptions bordered column={1}>
|
||||
<Descriptions.Item label='Connected'>
|
||||
<BoolDisplay value={printServer?.connected || false} />
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label='Connecting'>
|
||||
<BoolDisplay value={connecting} />
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label='Error'>
|
||||
{error ? <Text type='danger'>{error}</Text> : <Text>n/a</Text>}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label='Socket'>
|
||||
<Paragraph>
|
||||
<pre>{getSocketInfo()}</pre>
|
||||
</Paragraph>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</div>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
export default PrintServerContextDebug
|
||||
@ -24,6 +24,16 @@ import config from '../../../config'
|
||||
import loglevel from 'loglevel'
|
||||
import { ElectronContext } from './ElectronContext'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import {
|
||||
getAuthCookies,
|
||||
setAuthCookies,
|
||||
clearAuthCookies,
|
||||
areCookiesEnabled,
|
||||
validateAuthCookies,
|
||||
setupCookieSync,
|
||||
checkAuthCookiesExpiry
|
||||
} from '../../../utils/cookies'
|
||||
|
||||
const logger = loglevel.getLogger('ApiServerContext')
|
||||
logger.setLevel(config.logLevel)
|
||||
|
||||
@ -37,7 +47,7 @@ const AuthProvider = ({ children }) => {
|
||||
notification.useNotification()
|
||||
const [authenticated, setAuthenticated] = useState(false)
|
||||
const [initialized, setInitialized] = useState(false)
|
||||
const [retreivedTokenFromSession, setRetreivedTokenFromSession] =
|
||||
const [retreivedTokenFromCookies, setRetreivedTokenFromCookies] =
|
||||
useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [token, setToken] = useState(null)
|
||||
@ -60,13 +70,32 @@ const AuthProvider = ({ children }) => {
|
||||
if (isElectron == true && import.meta.env.MODE != 'development') {
|
||||
redirectType = 'app-scheme'
|
||||
}
|
||||
// Read token from session storage if present
|
||||
|
||||
// Check if cookies are enabled and show warning if not
|
||||
useEffect(() => {
|
||||
const storedToken = sessionStorage.getItem('authToken')
|
||||
const storedUser = JSON.parse(sessionStorage.getItem('user'))
|
||||
const storedExpiresAt = sessionStorage.getItem('authExpiresAt')
|
||||
console.log('stored user', storedUser, storedToken)
|
||||
if (storedToken && storedExpiresAt && storedUser) {
|
||||
if (!areCookiesEnabled()) {
|
||||
messageApi.warning(
|
||||
'Cookies are disabled. Login state may not persist between tabs.'
|
||||
)
|
||||
}
|
||||
}, [messageApi])
|
||||
|
||||
// Read token from cookies if present
|
||||
useEffect(() => {
|
||||
try {
|
||||
// First validate existing cookies to clean up expired ones
|
||||
if (validateAuthCookies()) {
|
||||
const {
|
||||
token: storedToken,
|
||||
expiresAt: storedExpiresAt,
|
||||
user: storedUser
|
||||
} = getAuthCookies()
|
||||
console.log('Retrieved from cookies:', {
|
||||
storedUser,
|
||||
storedToken,
|
||||
storedExpiresAt
|
||||
})
|
||||
|
||||
setToken(storedToken)
|
||||
setUserProfile(storedUser)
|
||||
setExpiresAt(storedExpiresAt)
|
||||
@ -76,17 +105,63 @@ const AuthProvider = ({ children }) => {
|
||||
setUserProfile(null)
|
||||
setShowUnauthorizedModal(true)
|
||||
}
|
||||
setRetreivedTokenFromSession(true)
|
||||
} catch (error) {
|
||||
console.error('Error reading auth cookies:', error)
|
||||
clearAuthCookies()
|
||||
setAuthenticated(false)
|
||||
setUserProfile(null)
|
||||
setShowUnauthorizedModal(true)
|
||||
}
|
||||
setRetreivedTokenFromCookies(true)
|
||||
}, [])
|
||||
|
||||
// Set up cookie synchronization between tabs
|
||||
useEffect(() => {
|
||||
const cleanupCookieSync = setupCookieSync(() => {
|
||||
// When cookies change in another tab, re-validate and update state
|
||||
try {
|
||||
if (validateAuthCookies()) {
|
||||
const {
|
||||
token: newToken,
|
||||
expiresAt: newExpiresAt,
|
||||
user: newUser
|
||||
} = getAuthCookies()
|
||||
if (
|
||||
newToken !== token ||
|
||||
newExpiresAt !== expiresAt ||
|
||||
JSON.stringify(newUser) !== JSON.stringify(userProfile)
|
||||
) {
|
||||
setToken(newToken)
|
||||
setExpiresAt(newExpiresAt)
|
||||
setUserProfile(newUser)
|
||||
setAuthenticated(true)
|
||||
console.log('Auth state synchronized from another tab')
|
||||
}
|
||||
} else {
|
||||
// Cookies are invalid, clear state
|
||||
setToken(null)
|
||||
setExpiresAt(null)
|
||||
setUserProfile(null)
|
||||
setAuthenticated(false)
|
||||
setShowUnauthorizedModal(true)
|
||||
console.log(
|
||||
'Auth state cleared due to invalid cookies from another tab'
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error syncing auth state:', error)
|
||||
}
|
||||
})
|
||||
|
||||
return cleanupCookieSync
|
||||
}, [token, expiresAt, userProfile])
|
||||
|
||||
const logout = useCallback((redirectUri = '/login') => {
|
||||
setAuthenticated(false)
|
||||
setToken(null)
|
||||
setExpiresAt(null)
|
||||
setUserProfile(null)
|
||||
sessionStorage.removeItem('authToken')
|
||||
sessionStorage.removeItem('authExpiresAt')
|
||||
sessionStorage.removeItem('user')
|
||||
clearAuthCookies()
|
||||
window.location.href = `${config.backendUrl}/auth/logout?redirect_uri=${encodeURIComponent(redirectUri)}`
|
||||
}, [])
|
||||
|
||||
@ -128,18 +203,20 @@ const AuthProvider = ({ children }) => {
|
||||
|
||||
if (response.status === 200 && response.data) {
|
||||
logger.debug('Got auth token!')
|
||||
setToken(response.data.access_token)
|
||||
setExpiresAt(response.data.expires_at)
|
||||
setUserProfile(response.data)
|
||||
sessionStorage.setItem('authToken', response.data.access_token)
|
||||
sessionStorage.setItem('authExpiresAt', response.data.expires_at)
|
||||
const userObject = {
|
||||
...response.data,
|
||||
access_token: undefined,
|
||||
refresh_token: undefined,
|
||||
id_token: undefined
|
||||
const authData = response.data
|
||||
|
||||
setToken(authData.access_token)
|
||||
setExpiresAt(authData.expires_at)
|
||||
setUserProfile(authData)
|
||||
|
||||
// Store in cookies for persistence between tabs
|
||||
const cookieSuccess = setAuthCookies(authData)
|
||||
if (!cookieSuccess) {
|
||||
messageApi.warning(
|
||||
'Authentication successful but failed to save login state. You may need to log in again if you close this tab.'
|
||||
)
|
||||
}
|
||||
sessionStorage.setItem('user', JSON.stringify(userObject))
|
||||
|
||||
const searchParams = new URLSearchParams(location.search)
|
||||
searchParams.delete('authCode')
|
||||
const newSearch = searchParams.toString()
|
||||
@ -167,8 +244,9 @@ const AuthProvider = ({ children }) => {
|
||||
setLoading(false)
|
||||
}
|
||||
},
|
||||
[isElectron]
|
||||
[isElectron, navigate, location.search, location.pathname, messageApi]
|
||||
)
|
||||
|
||||
// Function to check if the user is logged in
|
||||
const checkAuthStatus = useCallback(async () => {
|
||||
setLoading(true)
|
||||
@ -183,12 +261,19 @@ const AuthProvider = ({ children }) => {
|
||||
|
||||
if (response.status === 200 && response.data) {
|
||||
logger.debug('Got auth token!')
|
||||
setToken(response.data.access_token)
|
||||
setExpiresAt(response.data.expires_at)
|
||||
setUserProfile(response.data)
|
||||
sessionStorage.setItem('authToken', response.data.access_token)
|
||||
sessionStorage.setItem('authExpiresAt', response.data.expires_at)
|
||||
sessionStorage.setItem('user', response.data)
|
||||
const authData = response.data
|
||||
|
||||
setToken(authData.access_token)
|
||||
setExpiresAt(authData.expires_at)
|
||||
setUserProfile(authData)
|
||||
|
||||
// Update cookies with fresh data
|
||||
const cookieSuccess = setAuthCookies(authData)
|
||||
if (!cookieSuccess) {
|
||||
messageApi.warning(
|
||||
'Failed to update login state. You may need to log in again if you close this tab.'
|
||||
)
|
||||
}
|
||||
} else {
|
||||
setAuthenticated(false)
|
||||
setAuthError('Failed to authenticate user.')
|
||||
@ -206,15 +291,13 @@ const AuthProvider = ({ children }) => {
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [token])
|
||||
}, [token, messageApi])
|
||||
|
||||
const setUnauthenticated = () => {
|
||||
setToken(null)
|
||||
setExpiresAt(null)
|
||||
setUserProfile(null)
|
||||
sessionStorage.removeItem('authToken')
|
||||
sessionStorage.removeItem('authExpiresAt')
|
||||
sessionStorage.removeItem('user')
|
||||
clearAuthCookies()
|
||||
setShowUnauthorizedModal(true)
|
||||
}
|
||||
|
||||
@ -227,15 +310,23 @@ const AuthProvider = ({ children }) => {
|
||||
}
|
||||
})
|
||||
if (response.status === 200 && response.data) {
|
||||
setToken(response.data.access_token)
|
||||
setExpiresAt(response.data.expires_at)
|
||||
sessionStorage.setItem('authToken', response.data.access_token)
|
||||
sessionStorage.setItem('authExpiresAt', response.data.expires_at)
|
||||
const authData = response.data
|
||||
|
||||
setToken(authData.access_token)
|
||||
setExpiresAt(authData.expires_at)
|
||||
|
||||
// Update cookies with fresh token data
|
||||
const cookieSuccess = setAuthCookies(authData)
|
||||
if (!cookieSuccess) {
|
||||
messageApi.warning(
|
||||
'Failed to update login state. You may need to log in again if you close this tab.'
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Token refresh failed', error)
|
||||
}
|
||||
}, [token])
|
||||
}, [token, messageApi])
|
||||
|
||||
const handleSessionExpiredModalOk = () => {
|
||||
setShowSessionExpiredModal(false)
|
||||
@ -315,6 +406,55 @@ const AuthProvider = ({ children }) => {
|
||||
notificationApi.destroy('token-expiration')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Check cookies directly if expiresAt is not set in state
|
||||
const expiryInfo = checkAuthCookiesExpiry(5) // Check if expiring within 5 minutes
|
||||
if (expiryInfo.isExpiringSoon && expiryInfo.minutesRemaining <= 1) {
|
||||
// Show notification for cookies expiring soon
|
||||
const seconds = Math.floor((expiryInfo.timeRemaining % 60000) / 1000)
|
||||
const totalSeconds = 60
|
||||
const remainingSeconds = totalSeconds - seconds
|
||||
const progress = (remainingSeconds / totalSeconds) * 100
|
||||
|
||||
notificationApi.info({
|
||||
message: 'Session Expiring Soon',
|
||||
description: (
|
||||
<div>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
Your session will expire in {seconds} seconds
|
||||
</div>
|
||||
<Progress
|
||||
percent={progress}
|
||||
size='small'
|
||||
status='active'
|
||||
showInfo={false}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
duration: 0,
|
||||
key: 'token-expiration',
|
||||
icon: null,
|
||||
placement: 'bottomRight',
|
||||
style: {
|
||||
width: 360
|
||||
},
|
||||
className: 'token-expiration-notification',
|
||||
closeIcon: null,
|
||||
onClose: () => {},
|
||||
btn: (
|
||||
<Button
|
||||
type='primary'
|
||||
size='small'
|
||||
onClick={() => {
|
||||
notificationApi.destroy('token-expiration')
|
||||
refreshToken()
|
||||
}}
|
||||
>
|
||||
Reload Session
|
||||
</Button>
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -335,12 +475,12 @@ const AuthProvider = ({ children }) => {
|
||||
getLoginToken(authCode)
|
||||
} else if (
|
||||
token == null &&
|
||||
retreivedTokenFromSession == true &&
|
||||
retreivedTokenFromCookies == true &&
|
||||
initialized == false &&
|
||||
authCode == null
|
||||
) {
|
||||
setInitialized(true)
|
||||
console.log('Showing unauth')
|
||||
console.log('Showing unauth', token, authCode)
|
||||
setShowUnauthorizedModal(true)
|
||||
setAuthenticated(false)
|
||||
}
|
||||
@ -352,7 +492,7 @@ const AuthProvider = ({ children }) => {
|
||||
location.pathname,
|
||||
navigate,
|
||||
token,
|
||||
retreivedTokenFromSession
|
||||
retreivedTokenFromCookies
|
||||
])
|
||||
|
||||
return (
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Route } from 'react-router-dom'
|
||||
import SessionStorage from '../components/Dashboard/Developer/SessionStorage.jsx'
|
||||
import AuthContextDebug from '../components/Dashboard/Developer/AuthContextDebug.jsx'
|
||||
import PrintServerContextDebug from '../components/Dashboard/Developer/PrintServerContextDebug.jsx'
|
||||
import ApiContextDebug from '../components/Dashboard/Developer/ApiContextDebug.jsx'
|
||||
|
||||
const DeveloperRoutes = [
|
||||
<Route
|
||||
@ -15,9 +15,9 @@ const DeveloperRoutes = [
|
||||
element={<AuthContextDebug />}
|
||||
/>,
|
||||
<Route
|
||||
key='printservercontextdebug'
|
||||
path='developer/printservercontextdebug'
|
||||
element={<PrintServerContextDebug />}
|
||||
key='apicontextdebug'
|
||||
path='developer/apicontextdebug'
|
||||
element={<ApiContextDebug />}
|
||||
/>
|
||||
]
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user