diff --git a/client/src/components/LiveLogs/LiveLogs.tsx b/client/src/components/LiveLogs/LiveLogs.tsx index 0e9316f3..245696da 100644 --- a/client/src/components/LiveLogs/LiveLogs.tsx +++ b/client/src/components/LiveLogs/LiveLogs.tsx @@ -53,6 +53,19 @@ const LiveLogs = React.forwardRef( .emitWithAck(SsmEvents.Logs.GET_LOGS, { containerId: id, from }) .then((response) => { if (response.status === 'OK') { + terminalRef?.current?.onDataIn( + '---\n' + + '# ,;;:;,\n' + + '# ;;;;;\n' + + "# ,:;;:; ,'=.\n" + + "# ;:;:;' .=\" ,'_\\\n" + + "# ':;:;,/ ,__:=@\n" + + "# ';;:; =./)_\n" + + '# `"=\\_ )_"`\n' + + '# ``\'"`\n' + + '# Squirrel Servers Manager Container Live Logs\n' + + '---\n', + ); socket.on(SsmEvents.Logs.NEW_LOGS, onNewLogs); } else { handleConnectionError(`(${response.status} - ${response.error})`); diff --git a/client/src/components/PlaybookExecutionModal/PlaybookExecutionTerminalModal.tsx b/client/src/components/PlaybookExecutionModal/PlaybookExecutionTerminalModal.tsx index 7595a769..e3cff19c 100644 --- a/client/src/components/PlaybookExecutionModal/PlaybookExecutionTerminalModal.tsx +++ b/client/src/components/PlaybookExecutionModal/PlaybookExecutionTerminalModal.tsx @@ -145,6 +145,19 @@ const PlaybookExecutionTerminalModal = React.forwardRef< const pollingCallback = () => terminalHandler.pollingCallback(execId); const startPolling = () => { + terminalRef?.current?.onDataIn( + '---\n' + + '# ,;;:;,\n' + + '# ;;;;;\n' + + "# ,:;;:; ,'=.\n" + + "# ;:;:;' .=\" ,'_\\\n" + + "# ':;:;,/ ,__:=@\n" + + "# ';;:; =./)_\n" + + '# `"=\\_ )_"`\n' + + '# ``\'"`\n' + + '# Squirrel Servers Manager Playbook Executor\n' + + '---\n', + ); // pollingCallback(); // To immediately start fetching data // Polling every 30 seconds // @ts-ignore diff --git a/client/src/pages/Admin/Logs/TaskLogsColumns.tsx b/client/src/pages/Admin/Logs/TaskLogsColumns.tsx index f6dfce19..02a43886 100644 --- a/client/src/pages/Admin/Logs/TaskLogsColumns.tsx +++ b/client/src/pages/Admin/Logs/TaskLogsColumns.tsx @@ -1,8 +1,25 @@ +import DeviceQuickActionDropDown from '@/components/DeviceComponents/DeviceQuickAction/DeviceQuickActionDropDown'; +import TaskLogsTerminalModal from '@/pages/Admin/Logs/TaskLogsTerminalModal'; import { ProColumns } from '@ant-design/pro-components'; -import { Tag } from 'antd'; +import { Tag, Typography } from 'antd'; import React from 'react'; import { API, SsmAnsible } from 'ssm-shared-lib'; +const { Text } = Typography; + +const EllipsisMiddle: React.FC<{ suffixCount: number; children: string }> = ({ + suffixCount, + children, +}) => { + const start = children.slice(0, children.length - suffixCount); + const suffix = children.slice(-suffixCount).trim(); + return ( + + {start} + + ); +}; + const TaskLogsColumns: ProColumns[] = [ { title: 'Created At', @@ -48,7 +65,17 @@ const TaskLogsColumns: ProColumns[] = [ title: 'Command', dataIndex: 'cmd', key: 'cmd', - valueType: 'code', + render: (_, entity) => { + return {entity.cmd}; + }, + }, + { + dataIndex: 'option', + valueType: 'option', + hideInSearch: true, + render: (_, record) => [ + , + ], }, ]; diff --git a/client/src/pages/Admin/Logs/TaskLogsTerminalModal.tsx b/client/src/pages/Admin/Logs/TaskLogsTerminalModal.tsx new file mode 100644 index 00000000..3882c52a --- /dev/null +++ b/client/src/pages/Admin/Logs/TaskLogsTerminalModal.tsx @@ -0,0 +1,90 @@ +import TerminalCoreModal, { + PlaybookExecutionTerminalModalHandles, +} from '@/components/PlaybookExecutionModal/PlaybookExecutionTerminalModal'; +import TerminalCore, { + TerminalCoreHandles, +} from '@/components/Terminal/TerminalCore'; +import { getTaskEventsLogs } from '@/services/rest/logs'; +import { Modal } from 'antd'; +import React, { useEffect, useRef, useState } from 'react'; +import { API } from 'ssm-shared-lib'; + +type TaskLogsTerminalModalProps = { + task: API.Task; +}; + +const TaskLogsTerminalModal: React.FC = ({ + task, +}) => { + const terminalRef = useRef(null); + const [isOpen, setIsOpen] = useState(false); + + useEffect(() => { + if (isOpen) { + terminalRef?.current?.resetTerminalContent(); + getTaskEventsLogs(task.ident).then((res) => { + if (!res.data || res.data.length === 0) { + terminalRef?.current?.onDataIn( + 'No logs to show.\nSelected a higher retention period for "Ansible tasks & statuses retention in seconds" in Settings > General Settings', + true, + ); + return; + } + terminalRef?.current?.onDataIn( + '---\n' + + '# ,;;:;,\n' + + '# ;;;;;\n' + + "# ,:;;:; ,'=.\n" + + "# ;:;:;' .=\" ,'_\\\n" + + "# ':;:;,/ ,__:=@\n" + + "# ';;:; =./)_\n" + + '# `"=\\_ )_"`\n' + + '# ``\'"`\n' + + '# Squirrel Servers Manager Playbook Executor\n' + + '---\n', + ); + for (const line of res.data) { + if (line.stdout) { + terminalRef?.current?.onDataIn(line.stdout, true); + } + } + }); + } + }, [isOpen, task.ident]); + + return ( + <> + { + setIsOpen(true); + }} + > + Show logs + + { + setIsOpen(false); + }} + onCancel={() => { + setIsOpen(false); + }} + width={1000} + > +
+ +
+
+ + ); +}; + +export default TaskLogsTerminalModal; diff --git a/client/src/pages/Admin/Logs/index.tsx b/client/src/pages/Admin/Logs/index.tsx index 686d6d42..d5dadb32 100644 --- a/client/src/pages/Admin/Logs/index.tsx +++ b/client/src/pages/Admin/Logs/index.tsx @@ -19,6 +19,7 @@ const Index: React.FC = () => { const [form] = ProForm.useForm(); const [searchParams] = useSearchParams(); const location = useLocation(); + const [currentRow, setCurrentRow] = useState(); const [columnsStateMap, setColumnsStateMap] = useState< Record @@ -95,20 +96,22 @@ const Index: React.FC = () => { }, [location.hash]); return ( - } - /> - ), - }} - tabList={logsTabItems} - onTabChange={handleTabChange} - tabActiveKey={location.hash.replace('#', '') || logsTabItems[0].key} - /> + <> + } + /> + ), + }} + tabList={logsTabItems} + onTabChange={handleTabChange} + tabActiveKey={location.hash.replace('#', '') || logsTabItems[0].key} + /> + ); }; diff --git a/client/src/pages/Devices/DeviceSSHTerminal.tsx b/client/src/pages/Devices/DeviceSSHTerminal.tsx index 47a2c10b..5c60bc67 100644 --- a/client/src/pages/Devices/DeviceSSHTerminal.tsx +++ b/client/src/pages/Devices/DeviceSSHTerminal.tsx @@ -32,7 +32,22 @@ const DeviceSSHTerminal = () => { const setupSocket = ( onDataIn: (value: string, newLine?: boolean) => void, ) => { + terminalRef?.current?.resetTerminalContent(); socket.connect(); + terminalRef?.current?.onDataIn('---', true); + terminalRef?.current?.onDataIn('# ,;;:;,', true); + terminalRef?.current?.onDataIn('# ;;;;;', true); + terminalRef?.current?.onDataIn("# ,:;;:; ,'=.", true); + terminalRef?.current?.onDataIn("# ;:;:;' .=\" ,'_\\", true); + terminalRef?.current?.onDataIn("# ':;:;,/ ,__:=@", true); + terminalRef?.current?.onDataIn("# ';;:; =./)_", true); + terminalRef?.current?.onDataIn('# `"=\\_ )_"`', true); + terminalRef?.current?.onDataIn('# ``\'"`', true); + terminalRef?.current?.onDataIn( + '# Squirrel Servers Manager Remote SSH Terminal', + true, + ); + terminalRef?.current?.onDataIn('---', true); onDataIn('🛜 Connecting...', true); socket .emitWithAck(SsmEvents.SSH.START_SESSION, { deviceUuid: id, rows, cols }) diff --git a/client/src/services/rest/logs.ts b/client/src/services/rest/logs.ts index 951a6921..7ab66be8 100644 --- a/client/src/services/rest/logs.ts +++ b/client/src/services/rest/logs.ts @@ -26,3 +26,17 @@ export async function getServerLogs( ...(options || {}), }); } + +export async function getTaskEventsLogs( + id: string, + params?: API.PageParams, + options?: { [key: string]: any }, +) { + return request(`/api/logs/tasks/${id}/events`, { + method: 'GET', + params: { + ...params, + }, + ...(options || {}), + }); +} diff --git a/server/src/controllers/rest/logs/task.ts b/server/src/controllers/rest/logs/task.ts index 5cb91e6f..d1c03c40 100644 --- a/server/src/controllers/rest/logs/task.ts +++ b/server/src/controllers/rest/logs/task.ts @@ -1,10 +1,12 @@ import { parse } from 'url'; import { API } from 'ssm-shared-lib'; +import AnsibleLogsRepo from '../../../data/database/repository/AnsibleLogsRepo'; import AnsibleTaskRepo from '../../../data/database/repository/AnsibleTaskRepo'; import { filterByFields, filterByQueryParams } from '../../../helpers/query/FilterHelper'; import { paginate } from '../../../helpers/query/PaginationHelper'; import { sortByFields } from '../../../helpers/query/SorterHelper'; import logger from '../../../logger'; +import { NotFoundError } from '../../../middlewares/api/ApiError'; import { SuccessResponse } from '../../../middlewares/api/ApiResponse'; export const getTaskLogs = async (req, res) => { @@ -40,3 +42,13 @@ export const getTaskLogs = async (req, res) => { current: parseInt(`${params.current}`, 10) || 1, }).send(res); }; + +export const getTaskEvents = async (req, res) => { + const { id } = req.params; + if (!id) { + throw new NotFoundError('No id'); + } + const events = await AnsibleLogsRepo.findAllByIdent(id); + + new SuccessResponse('Get task logs successful', events).send(res); +}; diff --git a/server/src/controllers/rest/logs/task.validator.ts b/server/src/controllers/rest/logs/task.validator.ts new file mode 100644 index 00000000..b7ba9fac --- /dev/null +++ b/server/src/controllers/rest/logs/task.validator.ts @@ -0,0 +1,4 @@ +import { param } from 'express-validator'; +import validator from '../../../middlewares/Validator'; + +export const getTaskEventsValidator = [param('id').exists().notEmpty().isUUID(), validator]; diff --git a/server/src/routes/logs.ts b/server/src/routes/logs.ts index 38bd85ef..1d508b48 100644 --- a/server/src/routes/logs.ts +++ b/server/src/routes/logs.ts @@ -1,7 +1,8 @@ import express from 'express'; import passport from 'passport'; import { getServerLogs } from '../controllers/rest/logs/server'; -import { getTaskLogs } from '../controllers/rest/logs/task'; +import { getTaskEvents, getTaskLogs } from '../controllers/rest/logs/task'; +import { getTaskEventsValidator } from '../controllers/rest/logs/task.validator'; const router = express.Router(); @@ -9,5 +10,6 @@ router.use(passport.authenticate('jwt', { session: false })); router.get(`/server`, getServerLogs); router.get(`/tasks`, getTaskLogs); +router.get(`/tasks/:id/events`, getTaskEventsValidator, getTaskEvents); export default router;