Add OTP functionality and related components for host management
- Introduced HostOTP component to handle one-time passcode generation and display. - Added OTPIcon for visual representation of OTP functionality. - Updated HostInfo component to integrate OTP modal and manage state for OTP actions. - Refactored Host model to include a new 'connect' action for OTP access. - Created new SVG and design assets for OTP icon representation. - Enhanced user experience with loading states and progress indicators for OTP validity.
This commit is contained in:
parent
ec2d656b6e
commit
ed322436e6
BIN
src/assets/icons/otpicon.afdesign
Normal file
BIN
src/assets/icons/otpicon.afdesign
Normal file
Binary file not shown.
1
src/assets/icons/otpicon.min.svg
Normal file
1
src/assets/icons/otpicon.min.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2" viewBox="0 0 64 64"><path d="M39.064 13.281a16 16 0 0 0-1.552 2.283 16 16 0 0 0-1.102 2.424H11.319c-2.038 0-3.189 1.143-3.189 3.185V42.78c0 2.014 1.151 3.185 3.189 3.185h17.908c-.335 1.012-.407 2.18-.02 3.274l.532 1.47H10.622C5.731 50.709 3 48.064 3 43.165V20.784c0-4.878 2.731-7.503 7.622-7.503zm21.781 21.78v8.104c0 4.899-2.731 7.544-7.644 7.544h-5.695a4.8 4.8 0 0 0-.008-2.542l-.362-1.277 3.085-.865q.1-.029.197-.06h2.108c2.037 0 3.189-1.171 3.189-3.185v-5.062c1.92-.572 3.643-1.468 5.13-2.657" style="fill-rule:nonzero" transform="translate(-1.5)"/><path d="M18.888 77.914c1.551 1.303 3.64 1.459 5.099.032l9.646-9.657c1.437-1.448 1.375-3.661-.031-5.099l-4.446-4.435 6.674-6.653c1.408-1.406 1.385-3.65-.021-5.099l-6.024-6.046c8.385-4.254 13.079-11.304 13.079-19.577C42.864 9.551 33.292 0 21.443 0 9.51 0 0 9.498 0 21.38c0 8.455 4.793 16.002 12.406 19.504v29.212c0 1.161.372 2.517 1.378 3.399zm2.555-5.961-3.36-3.328V36.464C11.107 34.977 5.994 28.773 5.994 21.38c0-8.491 6.895-15.355 15.449-15.355 8.533 0 15.387 6.864 15.387 15.355 0 7.34-5.135 13.607-12.9 15.254v7.047l5.867 5.918-6.228 6.134v5.982l4.05 3.978zm0-51.321c2.771 0 5.042-2.282 5.042-5.064s-2.271-5.022-5.042-5.022c-2.814 0-5.043 2.231-5.043 5.022 0 2.782 2.25 5.064 5.043 5.064" style="fill-rule:nonzero" transform="rotate(29.234 11.74 88.091)scale(.60562)"/><path d="M34.933 27.241a4.697 4.697 0 0 0 4.694-4.694 4.697 4.697 0 0 0-4.694-4.694 4.7 4.7 0 0 0-4.695 4.694 4.7 4.7 0 0 0 4.695 4.694m-14.642 0c2.565 0 4.669-2.104 4.669-4.694s-2.13-4.694-4.669-4.694a4.697 4.697 0 0 0-4.694 4.694 4.697 4.697 0 0 0 4.694 4.694" style="fill-rule:nonzero" transform="translate(1.5 13.281)scale(.8287)"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
16
src/assets/icons/otpicon.svg
Normal file
16
src/assets/icons/otpicon.svg
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g transform="matrix(1,0,0,1,-1.5,7.10543e-15)">
|
||||||
|
<path d="M39.064,13.281C38.491,13.982 37.971,14.744 37.512,15.564C37.071,16.351 36.704,17.161 36.41,17.988L11.319,17.988C9.281,17.988 8.13,19.131 8.13,21.173L8.13,42.78C8.13,44.794 9.281,45.965 11.319,45.965L29.227,45.965C28.892,46.977 28.82,48.145 29.207,49.239L29.739,50.709L10.622,50.709C5.731,50.709 3,48.064 3,43.165L3,20.784C3,15.906 5.731,13.281 10.622,13.281L39.064,13.281ZM60.845,35.061L60.845,43.165C60.845,48.064 58.114,50.709 53.201,50.709L47.506,50.709C47.725,49.911 47.734,49.038 47.498,48.167L47.136,46.89L50.221,46.025C50.287,46.006 50.353,45.986 50.418,45.965L52.526,45.965C54.563,45.965 55.715,44.794 55.715,42.78L55.715,37.718C57.635,37.146 59.358,36.25 60.845,35.061Z" style="fill-rule:nonzero;"/>
|
||||||
|
<g transform="matrix(0.528487,0.29577,-0.29577,0.528487,46.0168,5.48596)">
|
||||||
|
<path d="M18.888,77.914C20.439,79.217 22.528,79.373 23.987,77.946L33.633,68.289C35.07,66.841 35.008,64.628 33.602,63.19L29.156,58.755L35.83,52.102C37.238,50.696 37.215,48.452 35.809,47.003L29.785,40.957C38.17,36.703 42.864,29.653 42.864,21.38C42.864,9.551 33.292,0 21.443,0C9.51,0 0,9.498 0,21.38C0,29.835 4.793,37.382 12.406,40.884L12.406,70.096C12.406,71.257 12.778,72.613 13.784,73.495L18.888,77.914ZM21.443,71.953L18.083,68.625L18.083,36.464C11.107,34.977 5.994,28.773 5.994,21.38C5.994,12.889 12.889,6.025 21.443,6.025C29.976,6.025 36.83,12.889 36.83,21.38C36.83,28.72 31.695,34.987 23.93,36.634L23.93,43.681L29.797,49.599L23.569,55.733L23.569,61.715L27.619,65.693L21.443,71.953ZM21.443,20.632C24.214,20.632 26.485,18.35 26.485,15.568C26.485,12.787 24.214,10.546 21.443,10.546C18.629,10.546 16.4,12.777 16.4,15.568C16.4,18.35 18.65,20.632 21.443,20.632Z" style="fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.828691,0,0,0.828691,3,13.2814)">
|
||||||
|
<path d="M34.933,27.241C37.523,27.241 39.627,25.137 39.627,22.547C39.627,19.957 37.523,17.853 34.933,17.853C32.343,17.853 30.238,19.957 30.238,22.547C30.238,25.137 32.343,27.241 34.933,27.241Z" style="fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.828691,0,0,0.828691,3,13.2814)">
|
||||||
|
<path d="M20.291,27.241C22.856,27.241 24.96,25.137 24.96,22.547C24.96,19.957 22.83,17.853 20.291,17.853C17.701,17.853 15.597,19.957 15.597,22.547C15.597,25.137 17.701,27.241 20.291,27.241Z" style="fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.8 KiB |
@ -1,6 +1,6 @@
|
|||||||
import React from 'react'
|
import React, { useRef, useState } from 'react'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
import { Space, Flex, Card } from 'antd'
|
import { Space, Flex, Card, Modal } from 'antd'
|
||||||
import { LoadingOutlined } from '@ant-design/icons'
|
import { LoadingOutlined } from '@ant-design/icons'
|
||||||
import loglevel from 'loglevel'
|
import loglevel from 'loglevel'
|
||||||
import config from '../../../../config.js'
|
import config from '../../../../config.js'
|
||||||
@ -19,12 +19,15 @@ import ActionHandler from '../../common/ActionHandler.jsx'
|
|||||||
import ObjectActions from '../../common/ObjectActions.jsx'
|
import ObjectActions from '../../common/ObjectActions.jsx'
|
||||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||||
|
import HostOTP from './HostOtp.jsx'
|
||||||
|
|
||||||
const log = loglevel.getLogger('HostInfo')
|
const log = loglevel.getLogger('HostInfo')
|
||||||
log.setLevel(config.logLevel)
|
log.setLevel(config.logLevel)
|
||||||
|
|
||||||
const HostInfo = () => {
|
const HostInfo = () => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
const editFormRef = useRef(null)
|
||||||
|
const actionHandlerRef = useRef(null)
|
||||||
const hostId = new URLSearchParams(location.search).get('hostId')
|
const hostId = new URLSearchParams(location.search).get('hostId')
|
||||||
const [collapseState, updateCollapseState] = useCollapseState('HostInfo', {
|
const [collapseState, updateCollapseState] = useCollapseState('HostInfo', {
|
||||||
info: true,
|
info: true,
|
||||||
@ -33,103 +36,115 @@ const HostInfo = () => {
|
|||||||
auditLogs: true
|
auditLogs: true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const [hostOTPOpen, setHostOTPOpen] = useState(false)
|
||||||
|
const [editFormState, setEditFormState] = useState({
|
||||||
|
isEditing: false,
|
||||||
|
editLoading: false,
|
||||||
|
formValid: false,
|
||||||
|
locked: false,
|
||||||
|
loading: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
reload: () => {
|
||||||
|
editFormRef?.current.handleFetchObject()
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
hostOTP: () => {
|
||||||
|
setHostOTPOpen(true)
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
edit: () => {
|
||||||
|
editFormRef?.current.startEditing()
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
cancelEdit: () => {
|
||||||
|
editFormRef?.current.cancelEditing()
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
finishEdit: () => {
|
||||||
|
editFormRef?.current.handleUpdate()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditObjectForm id={hostId} type='host' style={{ height: '100%' }}>
|
<>
|
||||||
{({
|
<Flex
|
||||||
loading,
|
gap='large'
|
||||||
isEditing,
|
vertical='true'
|
||||||
startEditing,
|
style={{
|
||||||
cancelEditing,
|
height: 'calc(var(--unit-100vh) - 155px)',
|
||||||
handleUpdate,
|
minHeight: 0
|
||||||
formValid,
|
}}
|
||||||
objectData,
|
>
|
||||||
editLoading,
|
<Flex justify={'space-between'}>
|
||||||
lock,
|
<Space size='middle'>
|
||||||
fetchObject
|
<Space size='small'>
|
||||||
}) => {
|
<ObjectActions
|
||||||
// Define actions for ActionHandler
|
type='host'
|
||||||
const actions = {
|
id={hostId}
|
||||||
reload: () => {
|
disabled={editFormState.loading}
|
||||||
fetchObject()
|
/>
|
||||||
return true
|
<ViewButton
|
||||||
},
|
disabled={editFormState.loading}
|
||||||
edit: () => {
|
items={[
|
||||||
startEditing()
|
{ key: 'info', label: 'Host Information' },
|
||||||
return false
|
{ key: 'notes', label: 'Notes' },
|
||||||
},
|
{ key: 'auditLogs', label: 'Audit Logs' }
|
||||||
cancelEdit: () => {
|
]}
|
||||||
cancelEditing()
|
visibleState={collapseState}
|
||||||
return true
|
updateVisibleState={updateCollapseState}
|
||||||
},
|
/>
|
||||||
finishEdit: () => {
|
</Space>
|
||||||
handleUpdate()
|
<LockIndicator lock={editFormState.lock} />
|
||||||
return true
|
</Space>
|
||||||
}
|
<Space>
|
||||||
}
|
<EditButtons
|
||||||
|
isEditing={editFormState.isEditing}
|
||||||
|
handleUpdate={() => {
|
||||||
|
actionHandlerRef.current.callAction('finishEdit')
|
||||||
|
}}
|
||||||
|
cancelEditing={() => {
|
||||||
|
actionHandlerRef.current.callAction('cancelEdit')
|
||||||
|
}}
|
||||||
|
startEditing={() => {
|
||||||
|
actionHandlerRef.current.callAction('edit')
|
||||||
|
}}
|
||||||
|
editLoading={editFormState.editLoading}
|
||||||
|
formValid={editFormState.formValid}
|
||||||
|
disabled={editFormState.lock?.locked || editFormState.loading}
|
||||||
|
loading={editFormState.editLoading}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
return (
|
<div style={{ height: '100%', overflowY: 'scroll' }}>
|
||||||
<ActionHandler actions={actions} loading={loading}>
|
<Flex vertical gap={'large'}>
|
||||||
{({ callAction }) => (
|
<ActionHandler
|
||||||
<Flex
|
actions={actions}
|
||||||
gap='large'
|
loading={editFormState.loading}
|
||||||
vertical='true'
|
ref={actionHandlerRef}
|
||||||
style={{
|
>
|
||||||
height: 'calc(var(--unit-100vh) - 155px)',
|
<InfoCollapse
|
||||||
minHeight: 0
|
title='Host Information'
|
||||||
}}
|
icon={<InfoCircleIcon />}
|
||||||
|
active={collapseState.info}
|
||||||
|
onToggle={(expanded) => updateCollapseState('info', expanded)}
|
||||||
|
collapseKey='info'
|
||||||
>
|
>
|
||||||
<Flex justify={'space-between'}>
|
<EditObjectForm
|
||||||
<Space size='middle'>
|
id={hostId}
|
||||||
<Space size='small'>
|
type='host'
|
||||||
<ObjectActions
|
style={{ height: '100%' }}
|
||||||
type='host'
|
ref={editFormRef}
|
||||||
id={hostId}
|
onStateChange={(state) => {
|
||||||
disabled={loading}
|
console.log('Got edit form state change', state)
|
||||||
/>
|
setEditFormState((prev) => ({ ...prev, ...state }))
|
||||||
<ViewButton
|
}}
|
||||||
disabled={loading}
|
>
|
||||||
items={[
|
{({ loading, isEditing, objectData }) => {
|
||||||
{ key: 'info', label: 'Host Information' },
|
return (
|
||||||
{ key: 'notes', label: 'Notes' },
|
|
||||||
{ key: 'auditLogs', label: 'Audit Logs' }
|
|
||||||
]}
|
|
||||||
visibleState={collapseState}
|
|
||||||
updateVisibleState={updateCollapseState}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
<LockIndicator lock={lock} />
|
|
||||||
</Space>
|
|
||||||
<Space>
|
|
||||||
<EditButtons
|
|
||||||
isEditing={isEditing}
|
|
||||||
handleUpdate={() => {
|
|
||||||
callAction('finishEdit')
|
|
||||||
}}
|
|
||||||
cancelEditing={() => {
|
|
||||||
callAction('cancelEdit')
|
|
||||||
}}
|
|
||||||
startEditing={() => {
|
|
||||||
callAction('edit')
|
|
||||||
}}
|
|
||||||
editLoading={editLoading}
|
|
||||||
formValid={formValid}
|
|
||||||
disabled={lock?.locked || loading}
|
|
||||||
loading={editLoading}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<div style={{ height: '100%', overflowY: 'scroll' }}>
|
|
||||||
<Flex vertical gap={'large'}>
|
|
||||||
<InfoCollapse
|
|
||||||
title='Host Information'
|
|
||||||
icon={<InfoCircleIcon />}
|
|
||||||
active={collapseState.info}
|
|
||||||
onToggle={(expanded) =>
|
|
||||||
updateCollapseState('info', expanded)
|
|
||||||
}
|
|
||||||
collapseKey='info'
|
|
||||||
>
|
|
||||||
<ObjectInfo
|
<ObjectInfo
|
||||||
loading={loading}
|
loading={loading}
|
||||||
indicator={<LoadingOutlined />}
|
indicator={<LoadingOutlined />}
|
||||||
@ -137,49 +152,59 @@ const HostInfo = () => {
|
|||||||
type='host'
|
type='host'
|
||||||
objectData={objectData}
|
objectData={objectData}
|
||||||
/>
|
/>
|
||||||
</InfoCollapse>
|
)
|
||||||
|
}}
|
||||||
|
</EditObjectForm>
|
||||||
|
</InfoCollapse>
|
||||||
|
</ActionHandler>
|
||||||
|
|
||||||
<InfoCollapse
|
<InfoCollapse
|
||||||
title='Notes'
|
title='Notes'
|
||||||
icon={<NoteIcon />}
|
icon={<NoteIcon />}
|
||||||
active={collapseState.notes}
|
active={collapseState.notes}
|
||||||
onToggle={(expanded) =>
|
onToggle={(expanded) => updateCollapseState('notes', expanded)}
|
||||||
updateCollapseState('notes', expanded)
|
collapseKey='notes'
|
||||||
}
|
>
|
||||||
collapseKey='notes'
|
<Card>
|
||||||
>
|
<NotesPanel _id={hostId} type='host' />
|
||||||
<Card>
|
</Card>
|
||||||
<NotesPanel _id={hostId} type='host' />
|
</InfoCollapse>
|
||||||
</Card>
|
|
||||||
</InfoCollapse>
|
|
||||||
|
|
||||||
<InfoCollapse
|
<InfoCollapse
|
||||||
title='Audit Logs'
|
title='Audit Logs'
|
||||||
icon={<AuditLogIcon />}
|
icon={<AuditLogIcon />}
|
||||||
active={collapseState.auditLogs}
|
active={collapseState.auditLogs}
|
||||||
onToggle={(expanded) =>
|
onToggle={(expanded) =>
|
||||||
updateCollapseState('auditLogs', expanded)
|
updateCollapseState('auditLogs', expanded)
|
||||||
}
|
}
|
||||||
collapseKey='auditLogs'
|
collapseKey='auditLogs'
|
||||||
>
|
>
|
||||||
{loading ? (
|
{editFormState.loading ? (
|
||||||
<InfoCollapsePlaceholder />
|
<InfoCollapsePlaceholder />
|
||||||
) : (
|
) : (
|
||||||
<ObjectTable
|
<ObjectTable
|
||||||
type='auditLog'
|
type='auditLog'
|
||||||
masterFilter={{ 'parent._id': hostId }}
|
masterFilter={{ 'parent._id': hostId }}
|
||||||
visibleColumns={{ _id: false, 'parent._id': false }}
|
visibleColumns={{ _id: false, 'parent._id': false }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</InfoCollapse>
|
</InfoCollapse>
|
||||||
</Flex>
|
</Flex>
|
||||||
</div>
|
</div>
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
|
||||||
</ActionHandler>
|
<Modal
|
||||||
)
|
open={hostOTPOpen}
|
||||||
}}
|
destroyOnHidden={true}
|
||||||
</EditObjectForm>
|
width={650}
|
||||||
|
onCancel={() => {
|
||||||
|
setHostOTPOpen(false)
|
||||||
|
}}
|
||||||
|
footer={false}
|
||||||
|
>
|
||||||
|
<HostOTP id={hostId} />
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
138
src/components/Dashboard/Management/Hosts/HostOtp.jsx
Normal file
138
src/components/Dashboard/Management/Hosts/HostOtp.jsx
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import React, { useContext, useEffect, useState, useRef } from 'react'
|
||||||
|
import { Input, Result, Typography, Flex, Progress, Button } from 'antd'
|
||||||
|
import { LoadingOutlined } from '@ant-design/icons'
|
||||||
|
import { ApiServerContext } from '../../context/ApiServerContext'
|
||||||
|
import CopyButton from '../../common/CopyButton'
|
||||||
|
import OTPIcon from '../../../Icons/OTPIcon'
|
||||||
|
|
||||||
|
const { Text } = Typography
|
||||||
|
|
||||||
|
const HostOTP = ({ id }) => {
|
||||||
|
const { fetchHostOTP, sendObjectAction } = useContext(ApiServerContext)
|
||||||
|
const [hostObject, setHostObject] = useState(null)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [timeRemaining, setTimeRemaining] = useState(0)
|
||||||
|
const [totalTime, setTotalTime] = useState(0)
|
||||||
|
const [initialized, setInitialized] = useState(false)
|
||||||
|
const intervalRef = useRef(null)
|
||||||
|
|
||||||
|
const fetchNewOTP = () => {
|
||||||
|
setLoading(true)
|
||||||
|
setHostObject(null) // Reset to show loading
|
||||||
|
fetchHostOTP(id, (hostOTPObject) => {
|
||||||
|
setHostObject(hostOTPObject)
|
||||||
|
setLoading(false)
|
||||||
|
|
||||||
|
if (hostOTPObject?.otpExpiresAt) {
|
||||||
|
const now = Date.now()
|
||||||
|
const expiresAt = new Date(hostOTPObject.otpExpiresAt).getTime()
|
||||||
|
const remaining = Math.max(0, expiresAt - now)
|
||||||
|
|
||||||
|
setTimeRemaining(remaining)
|
||||||
|
setTotalTime(remaining)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendTestAction = () => {
|
||||||
|
console.log('Sending test action...')
|
||||||
|
sendObjectAction(id, 'host', { method: 'testMethod' }, (result) => {
|
||||||
|
console.log('Got callback', result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hostObject === null && initialized == false) {
|
||||||
|
setInitialized(true)
|
||||||
|
fetchNewOTP()
|
||||||
|
}
|
||||||
|
}, [id])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hostObject && timeRemaining > 0) {
|
||||||
|
intervalRef.current = setInterval(() => {
|
||||||
|
setTimeRemaining((prev) => {
|
||||||
|
const newTime = prev - 1000
|
||||||
|
if (newTime <= 0) {
|
||||||
|
// OTP expired, fetch a new one
|
||||||
|
fetchNewOTP()
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return newTime
|
||||||
|
})
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [hostObject, timeRemaining])
|
||||||
|
|
||||||
|
// Clean up interval on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const progressPercent =
|
||||||
|
totalTime > 0 ? Math.max(0, (timeRemaining / totalTime) * 100) : 0
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex vertical align='center'>
|
||||||
|
<Button onClick={sendTestAction}>Test Action</Button>
|
||||||
|
<Result
|
||||||
|
title={'Connect a Host.'}
|
||||||
|
subTitle={<Text>Enter the following one time passcode.</Text>}
|
||||||
|
icon={<OTPIcon />}
|
||||||
|
>
|
||||||
|
<Flex justify='center'>
|
||||||
|
<Flex gap={'small'} align='center' justify='center'>
|
||||||
|
<CopyButton
|
||||||
|
size='default'
|
||||||
|
text={hostObject?.otp}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Input.OTP
|
||||||
|
disabled={loading}
|
||||||
|
size='large'
|
||||||
|
value={hostObject?.otp}
|
||||||
|
onChange={(e) => e.preventDefault()} // prevent typing
|
||||||
|
onKeyDown={(e) => e.preventDefault()} // prevent key input
|
||||||
|
onPaste={(e) => e.preventDefault()} // prevent pasting
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ margin: '0 6px 0 8px', paddingBottom: '5px' }}>
|
||||||
|
{loading ? (
|
||||||
|
<Text>
|
||||||
|
<LoadingOutlined />
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Progress
|
||||||
|
type='circle'
|
||||||
|
showInfo={false}
|
||||||
|
strokeColor='#32D74B'
|
||||||
|
size={14}
|
||||||
|
strokeWidth={14}
|
||||||
|
percent={progressPercent}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Result>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
HostOTP.propTypes = {
|
||||||
|
id: PropTypes.string.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HostOTP
|
||||||
7
src/components/Icons/OTPIcon.jsx
Normal file
7
src/components/Icons/OTPIcon.jsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import Icon from '@ant-design/icons'
|
||||||
|
import { ReactComponent as CustomIconSvg } from '../../assets/icons/otpicon.min.svg'
|
||||||
|
|
||||||
|
const OTPIcon = (props) => <Icon component={CustomIconSvg} {...props} />
|
||||||
|
|
||||||
|
export default OTPIcon
|
||||||
@ -2,6 +2,7 @@ import HostIcon from '../../components/Icons/HostIcon'
|
|||||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||||
import EditIcon from '../../components/Icons/EditIcon'
|
import EditIcon from '../../components/Icons/EditIcon'
|
||||||
|
import OTPIcon from '../../components/Icons/OTPIcon'
|
||||||
|
|
||||||
export const Host = {
|
export const Host = {
|
||||||
name: 'host',
|
name: 'host',
|
||||||
@ -15,7 +16,7 @@ export const Host = {
|
|||||||
default: true,
|
default: true,
|
||||||
row: true,
|
row: true,
|
||||||
icon: InfoCircleIcon,
|
icon: InfoCircleIcon,
|
||||||
url: (_id) => `/dashboard/production/hosts/info?hostId=${_id}`
|
url: (_id) => `/dashboard/management/hosts/info?hostId=${_id}`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -23,14 +24,21 @@ export const Host = {
|
|||||||
label: 'Reload',
|
label: 'Reload',
|
||||||
icon: ReloadIcon,
|
icon: ReloadIcon,
|
||||||
url: (_id) =>
|
url: (_id) =>
|
||||||
`/dashboard/production/hosts/info?hostId=${_id}&action=reload`
|
`/dashboard/management/hosts/info?hostId=${_id}&action=reload`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'connect',
|
||||||
|
label: 'Connect',
|
||||||
|
icon: OTPIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/management/hosts/info?hostId=${_id}&action=hostOTP`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'edit',
|
name: 'edit',
|
||||||
label: 'Edit',
|
label: 'Edit',
|
||||||
row: true,
|
row: true,
|
||||||
icon: EditIcon,
|
icon: EditIcon,
|
||||||
url: (_id) => `/dashboard/production/hosts/info?hostId=${_id}&action=edit`
|
url: (_id) => `/dashboard/management/hosts/info?hostId=${_id}&action=edit`
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
columns: ['name', '_id', 'state', 'tags', 'connectedAt'],
|
columns: ['name', '_id', 'state', 'tags', 'connectedAt'],
|
||||||
@ -61,30 +69,95 @@ export const Host = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'state',
|
name: 'state',
|
||||||
label: 'Status',
|
label: 'State',
|
||||||
type: 'state',
|
type: 'state',
|
||||||
objectType: 'host',
|
objectType: 'host',
|
||||||
showName: false,
|
showName: false,
|
||||||
readOnly: true
|
readOnly: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'host',
|
name: 'active',
|
||||||
label: 'Host',
|
label: 'Active',
|
||||||
type: 'text',
|
type: 'bool',
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'online',
|
||||||
|
label: 'Online',
|
||||||
|
type: 'bool',
|
||||||
|
readOnly: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'deviceInfo.os',
|
||||||
|
label: 'Operating System',
|
||||||
|
type: 'text',
|
||||||
|
required: false,
|
||||||
|
readOnly: true,
|
||||||
|
value: (objectData) => {
|
||||||
|
if (
|
||||||
|
objectData.deviceInfo?.os?.type &&
|
||||||
|
objectData.deviceInfo?.os?.release &&
|
||||||
|
objectData.deviceInfo?.os?.arch
|
||||||
|
) {
|
||||||
|
return `${objectData.deviceInfo.os.type} ${objectData.deviceInfo.os.release} (${objectData.deviceInfo.os.arch})`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'deviceInfo.os.hostname',
|
||||||
|
label: 'Hostname',
|
||||||
|
type: 'text',
|
||||||
|
required: false,
|
||||||
|
readOnly: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'deviceInfo.cpu.model',
|
||||||
|
label: 'CPU Model',
|
||||||
|
type: 'text',
|
||||||
|
required: false,
|
||||||
|
readOnly: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'deviceInfo.cpu',
|
||||||
|
label: 'CPU Info',
|
||||||
|
type: 'text',
|
||||||
|
required: false,
|
||||||
|
readOnly: true,
|
||||||
|
value: (objectData) => {
|
||||||
|
if (
|
||||||
|
objectData.deviceInfo?.cpu?.cores &&
|
||||||
|
objectData.deviceInfo?.cpu?.speedMHz
|
||||||
|
) {
|
||||||
|
return `Cores: ${objectData.deviceInfo.cpu.cores}, Speed: ${objectData.deviceInfo.cpu.speedMHz} MHz`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'deviceInfo.user.username',
|
||||||
|
label: 'User',
|
||||||
|
type: 'text',
|
||||||
|
required: false,
|
||||||
|
readOnly: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'deviceInfo.user.homedir',
|
||||||
|
label: 'User Home',
|
||||||
|
type: 'text',
|
||||||
|
required: false,
|
||||||
|
readOnly: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'deviceInfo.process.nodeVersion',
|
||||||
|
label: 'NodeJS Version',
|
||||||
|
type: 'text',
|
||||||
|
required: false,
|
||||||
|
readOnly: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'tags',
|
name: 'tags',
|
||||||
label: 'Tags',
|
label: 'Tags',
|
||||||
type: 'tags',
|
type: 'tags',
|
||||||
required: false
|
required: false
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'operatingSystem',
|
|
||||||
label: 'Operating System',
|
|
||||||
type: 'text',
|
|
||||||
required: false,
|
|
||||||
readOnly: true
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user