diff --git a/.storybook/mocks/meteor.js b/.storybook/mocks/meteor.js index d85c752a2b2b..383f6fcf8cac 100644 --- a/.storybook/mocks/meteor.js +++ b/.storybook/mocks/meteor.js @@ -4,7 +4,10 @@ export const Meteor = { _localStorage: window.localStorage, absoluteUrl: () => {}, userId: () => {}, - Streamer: () => {}, + Streamer: () => ({ + on: () => {}, + removeListener: () => {}, + }), startup: () => {}, methods: () => {}, call: () => {}, diff --git a/app/logger/client/ansispan.js b/app/logger/client/ansispan.js deleted file mode 100644 index 1ccf35296f78..000000000000 --- a/app/logger/client/ansispan.js +++ /dev/null @@ -1,34 +0,0 @@ -const foregroundColors = { - 30: 'gray', - 31: 'red', - 32: 'lime', - 33: 'yellow', - 34: '#6B98FF', - 35: '#FF00FF', - 36: 'cyan', - 37: 'white', -}; - -export const ansispan = (str: string) => { - str = str - .replace(/\s/g, ' ') - .replace(/(\\n|\n)/g, '') - .replace(/>/g, '>') - .replace(/$1') - .replace(/\033\[1m/g, '') - .replace(/\033\[22m/g, '') - .replace(/\033\[3m/g, '') - .replace(/\033\[23m/g, '') - .replace(/\033\[m/g, '') - .replace(/\033\[0m/g, '') - .replace(/\033\[39m/g, ''); - return Object.entries(foregroundColors).reduce((str, [ansiCode, color]) => { - const span = ``; - return ( - str - .replace(new RegExp(`\\033\\[${ ansiCode }m`, 'g'), span) - .replace(new RegExp(`\\033\\[0;${ ansiCode }m`, 'g'), span) - ); - }, str); -}; diff --git a/app/logger/client/components/ViewLogs.js b/app/logger/client/components/ViewLogs.js new file mode 100644 index 000000000000..28824144afab --- /dev/null +++ b/app/logger/client/components/ViewLogs.js @@ -0,0 +1,245 @@ +import { Meteor } from 'meteor/meteor'; +import React, { useEffect, useRef, useState, useCallback } from 'react'; +import { Box, Scrollable } from '@rocket.chat/fuselage'; + +import { useToastMessageDispatch } from '../../../../client/contexts/ToastMessagesContext'; +import { useEndpoint } from '../../../../client/contexts/ServerContext'; +import { useTranslation } from '../../../../client/contexts/TranslationContext'; +import { Page } from '../../../../client/components/basic/Page'; + +const foregroundColors = { + 30: 'gray', + 31: 'red', + 32: 'lime', + 33: 'yellow', + 34: '#6B98FF', + 35: '#FF00FF', + 36: 'cyan', + 37: 'white', +}; + +const ansispan = (str) => { + str = str + .replace(/\s/g, ' ') + .replace(/(\\n|\n)/g, '') + .replace(/>/g, '>') + .replace(/$1') + .replace(/\033\[1m/g, '') + .replace(/\033\[22m/g, '') + .replace(/\033\[3m/g, '') + .replace(/\033\[23m/g, '') + .replace(/\033\[m/g, '') + .replace(/\033\[0m/g, '') + .replace(/\033\[39m/g, ''); + return Object.entries(foregroundColors).reduce((str, [ansiCode, color]) => { + const span = ``; + return ( + str + .replace(new RegExp(`\\033\\[${ ansiCode }m`, 'g'), span) + .replace(new RegExp(`\\033\\[0;${ ansiCode }m`, 'g'), span) + ); + }, str); +}; + +function ViewLogs() { + const [lines, setLines] = useState([]); + window.setLines = setLines; + + const dispatchToastMessage = useToastMessageDispatch(); + + const getStdoutQueue = useEndpoint('GET', 'stdout.queue'); + + useEffect(() => { + const fetchLines = async () => { + try { + const { queue } = await getStdoutQueue(); + setLines(queue); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } + }; + + fetchLines(); + }, []); + + useEffect(() => { + const stdoutStreamer = new Meteor.Streamer('stdout'); + + const handleStdout = (line) => { + setLines((lines) => [...lines, line]); + }; + + stdoutStreamer.on('stdout', handleStdout); + + return () => { + stdoutStreamer.removeListener('stdout'); + }; + }, []); + + const t = useTranslation(); + + const wrapperRef = useRef(); + const atBottomRef = useRef(); + + const [newLogsVisible, setNewLogsVisible] = useState(false); + + const isAtBottom = useCallback((scrollThreshold) => { + const wrapper = wrapperRef.current; + + if (scrollThreshold == null) { + scrollThreshold = 0; + } + if (wrapper.scrollTop + scrollThreshold >= wrapper.scrollHeight - wrapper.clientHeight) { + setNewLogsVisible(false); + return true; + } + return false; + }, []); + + const sendToBottom = useCallback(() => { + const wrapper = wrapperRef.current; + + wrapper.scrollTop = wrapper.scrollHeight - wrapper.clientHeight; + setNewLogsVisible(false); + }, []); + + const checkIfScrollIsAtBottom = useCallback(() => { + atBottomRef.current = isAtBottom(100); + }, [isAtBottom]); + + const sendToBottomIfNecessary = useCallback(() => { + if (atBottomRef.current === true && isAtBottom() !== true) { + sendToBottom(); + } else if (atBottomRef.current === false) { + setNewLogsVisible(true); + } + }, [isAtBottom, sendToBottom]); + + useEffect(() => { + if (window.MutationObserver) { + const observer = new MutationObserver((mutations) => { + mutations.forEach(() => { + sendToBottomIfNecessary(); + }); + }); + observer.observe(wrapperRef.current, { childList: true }); + + return () => { + observer.disconnect(); + }; + } + + const handleSubtreeModified = () => { + sendToBottomIfNecessary(); + }; + wrapperRef.current.addEventListener('DOMSubtreeModified', handleSubtreeModified); + + return () => { + wrapperRef.current.removeEventListener('DOMSubtreeModified', handleSubtreeModified); + }; + }, [sendToBottomIfNecessary]); + + useEffect(() => { + const handleWindowResize = () => { + setTimeout(() => { + sendToBottomIfNecessary(); + }, 100); + }; + + window.addEventListener('resize', handleWindowResize); + + return () => { + window.removeEventListener('resize', handleWindowResize); + }; + }, [sendToBottomIfNecessary]); + + const handleWheel = useCallback(() => { + atBottomRef.current = false; + setTimeout(() => { + checkIfScrollIsAtBottom(); + }, 100); + }, [checkIfScrollIsAtBottom]); + + const handleTouchStart = () => { + atBottomRef.current = false; + }; + + const handleTouchEnd = useCallback(() => { + setTimeout(() => { + checkIfScrollIsAtBottom(); + }, 100); + }, [checkIfScrollIsAtBottom]); + + const handleScroll = useCallback(() => { + atBottomRef.current = false; + setTimeout(() => { + checkIfScrollIsAtBottom(); + }, 100); + }, [checkIfScrollIsAtBottom]); + + const handleClick = useCallback(() => { + atBottomRef.current = true; + sendToBottomIfNecessary(); + }, [sendToBottomIfNecessary]); + + return + + + + + + {lines.sort((a, b) => a.ts - b.ts).map(({ string }, i) => + )} + + + + + {t('New_logs')} + + + + ; +} + +export default ViewLogs; diff --git a/app/logger/client/components/ViewLogs.stories.js b/app/logger/client/components/ViewLogs.stories.js new file mode 100644 index 000000000000..3ca844e97266 --- /dev/null +++ b/app/logger/client/components/ViewLogs.stories.js @@ -0,0 +1,16 @@ +import React from 'react'; + +import ViewLogs from './ViewLogs'; + +export default { + title: 'admin/pages/ViewLogs', + component: ViewLogs, + decorators: [ + (storyFn) => + {storyFn()} + , + ], +}; + +export const _default = () => + ; diff --git a/app/logger/client/components/ViewLogsRoute.js b/app/logger/client/components/ViewLogsRoute.js new file mode 100644 index 000000000000..ee94869ae9df --- /dev/null +++ b/app/logger/client/components/ViewLogsRoute.js @@ -0,0 +1,15 @@ +import React from 'react'; + +import { NotAuthorizedPage } from '../../../ui-admin/client/components/NotAuthorizedPage'; +import { usePermission } from '../../../../client/contexts/AuthorizationContext'; +import ViewLogs from './ViewLogs'; + +export default function ViewLogsRoute() { + const canViewLogs = usePermission('view-logs'); + + if (!canViewLogs) { + return ; + } + + return ; +} diff --git a/app/logger/client/index.js b/app/logger/client/index.js index 689a3ac95d83..3f4a6f231aae 100644 --- a/app/logger/client/index.js +++ b/app/logger/client/index.js @@ -1,2 +1,16 @@ import './logger'; -import './viewLogs'; + +import { hasAllPermission } from '../../authorization'; +import { registerAdminRoute, registerAdminSidebarItem } from '../../ui-admin/client'; + +registerAdminSidebarItem({ + href: 'admin-view-logs', + i18nLabel: 'View_Logs', + icon: 'post', + permissionGranted: () => hasAllPermission('view-logs'), +}); + +registerAdminRoute('/view-logs', { + name: 'admin-view-logs', + lazyRouteComponent: () => import('./components/ViewLogsRoute'), +}); diff --git a/app/logger/client/viewLogs.js b/app/logger/client/viewLogs.js deleted file mode 100644 index 7683f9d1d292..000000000000 --- a/app/logger/client/viewLogs.js +++ /dev/null @@ -1,33 +0,0 @@ -import { BlazeLayout } from 'meteor/kadira:blaze-layout'; -import { Meteor } from 'meteor/meteor'; -import { Mongo } from 'meteor/mongo'; - -import { hasAllPermission } from '../../authorization'; -import { registerAdminRoute, registerAdminSidebarItem } from '../../ui-admin/client'; -import { t } from '../../utils'; - -export const stdout = new Mongo.Collection(null); - -Meteor.startup(function() { - registerAdminSidebarItem({ - href: 'admin-view-logs', - i18nLabel: 'View_Logs', - icon: 'post', - permissionGranted() { - return hasAllPermission('view-logs'); - }, - }); -}); - -registerAdminRoute('/view-logs', { - name: 'admin-view-logs', - async action() { - await import('./views/viewLogs'); - return BlazeLayout.render('main', { - center: 'pageSettingsContainer', - pageTitle: t('View_Logs'), - pageTemplate: 'viewLogs', - noScroll: true, - }); - }, -}); diff --git a/app/logger/client/views/viewLogs.css b/app/logger/client/views/viewLogs.css deleted file mode 100644 index e7619344c083..000000000000 --- a/app/logger/client/views/viewLogs.css +++ /dev/null @@ -1,29 +0,0 @@ -.view-logs { - &__terminal { - overflow-y: scroll; - flex: 1; - - margin: 0; - margin-bottom: 0 !important; - padding: 8px 10px !important; - - color: var(--color-white); - border: none !important; - background-color: #444444 !important; - - font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; - font-weight: 500; - - &-line { - word-break: break-all; - } - - &-time { - color: #7f7f7f; - } - - .rtl & { - direction: ltr; - } - } -} diff --git a/app/logger/client/views/viewLogs.html b/app/logger/client/views/viewLogs.html deleted file mode 100644 index 9c664aac2f8c..000000000000 --- a/app/logger/client/views/viewLogs.html +++ /dev/null @@ -1,17 +0,0 @@ - - {{#if hasPermission}} - - {{#each logs}} - - {{{ansispan string}}} - - {{/each}} - - - - {{_ "New_logs"}} - - {{else}} - {{_ "Not_authorized"}} - {{/if}} - diff --git a/app/logger/client/views/viewLogs.js b/app/logger/client/views/viewLogs.js deleted file mode 100644 index a10fbdef10ce..000000000000 --- a/app/logger/client/views/viewLogs.js +++ /dev/null @@ -1,121 +0,0 @@ -import _ from 'underscore'; -import { Meteor } from 'meteor/meteor'; -import { Template } from 'meteor/templating'; -import { Tracker } from 'meteor/tracker'; - -import { ansispan } from '../ansispan'; -import { stdout } from '../viewLogs'; -import { hasAllPermission } from '../../../authorization'; -import { SideNav } from '../../../ui-utils/client'; -import './viewLogs.html'; -import './viewLogs.css'; -import { APIClient } from '../../../utils/client'; - -const stdoutStreamer = new Meteor.Streamer('stdout'); - -Template.viewLogs.onCreated(async function() { - const { queue } = await APIClient.v1.get('stdout.queue'); - (queue || []).forEach((item) => stdout.insert(item)); - stdoutStreamer.on('stdout', (item) => stdout.insert(item)); - this.atBottom = true; -}); - -Template.viewLogs.onDestroyed(() => { - stdout.remove({}); - stdoutStreamer.removeListener('stdout'); -}); - -Template.viewLogs.helpers({ - hasPermission() { - return hasAllPermission('view-logs'); - }, - logs() { - return stdout.find({}, { sort: { ts: 1 } }); - }, - ansispan, -}); - -Template.viewLogs.events({ - 'click .new-logs'(event, instance) { - instance.atBottom = true; - instance.sendToBottomIfNecessary(); - }, -}); - -Template.viewLogs.onRendered(function() { - Tracker.afterFlush(() => { - SideNav.setFlex('adminFlex'); - SideNav.openFlex(); - }); - - const wrapper = this.find('.js-terminal'); - const newLogs = this.find('.js-new-logs'); - - this.isAtBottom = (scrollThreshold) => { - if (scrollThreshold == null) { - scrollThreshold = 0; - } - if (wrapper.scrollTop + scrollThreshold >= wrapper.scrollHeight - wrapper.clientHeight) { - newLogs.className = 'new-logs not'; - return true; - } - return false; - }; - - this.sendToBottom = () => { - wrapper.scrollTop = wrapper.scrollHeight - wrapper.clientHeight; - newLogs.className = 'new-logs not'; - }; - - this.checkIfScrollIsAtBottom = () => { - this.atBottom = this.isAtBottom(100); - }; - - this.sendToBottomIfNecessary = () => { - if (this.atBottom === true && this.isAtBottom() !== true) { - this.sendToBottom(); - } else if (this.atBottom === false) { - newLogs.className = 'new-logs'; - } - }; - - this.sendToBottomIfNecessaryDebounced = _.debounce(this.sendToBottomIfNecessary, 10); - this.sendToBottomIfNecessary(); - - if (window.MutationObserver) { - const observer = new MutationObserver((mutations) => { - mutations.forEach(() => this.sendToBottomIfNecessaryDebounced()); - }); - observer.observe(wrapper, { childList: true }); - } else { - wrapper.addEventListener('DOMSubtreeModified', () => this.sendToBottomIfNecessaryDebounced()); - } - - this.onWindowResize = () => { - Meteor.defer(() => this.sendToBottomIfNecessaryDebounced()); - }; - window.addEventListener('resize', this.onWindowResize); - - wrapper.addEventListener('mousewheel', () => { - this.atBottom = false; - Meteor.defer(() => this.checkIfScrollIsAtBottom()); - }); - - wrapper.addEventListener('wheel', () => { - this.atBottom = false; - Meteor.defer(() => this.checkIfScrollIsAtBottom()); - }); - - wrapper.addEventListener('touchstart', () => { - this.atBottom = false; - }); - - wrapper.addEventListener('touchend', () => { - Meteor.defer(() => this.checkIfScrollIsAtBottom()); - }); - - wrapper.addEventListener('scroll', () => { - this.atBottom = false; - Meteor.defer(() => this.checkIfScrollIsAtBottom()); - }); -}); diff --git a/app/theme/client/imports/general/base_old.css b/app/theme/client/imports/general/base_old.css index dc786964fff5..7be16e07ff66 100644 --- a/app/theme/client/imports/general/base_old.css +++ b/app/theme/client/imports/general/base_old.css @@ -4704,30 +4704,6 @@ rc-old select, background-color: #f8f8f8; } -.rc-old .new-logs { - position: absolute; - bottom: 8px; - left: 50%; - - width: 130px; - height: 30px; - margin: 0 -65px; - - cursor: pointer; - transition: transform 0.3s ease-out; - transform: translateY(0); - text-align: center; - - border-radius: 20px; - - font-size: 0.8em; - line-height: 30px; - - &.not { - transform: translateY(150%); - } -} - .rc-old .powered-by { margin-top: 1em; } diff --git a/client/components/basic/Page.js b/client/components/basic/Page.js index 49111df7bd08..280321a83dc9 100644 --- a/client/components/basic/Page.js +++ b/client/components/basic/Page.js @@ -7,9 +7,15 @@ const PageContext = createContext(); export function Page(props) { const [border, setBorder] = useState(false); return - - ({ overflow: 'hidden', flex: '1 1 auto', height: '100%' }), [])} {...props} /> - + ({ overflow: 'hidden' }), [])} {...props} + /> ; } @@ -39,7 +45,7 @@ export function PageContentShadowScroll({ onScrollContent, ...props }) { export function PageContent({ onScrollContent, ...props }) { return - ({ padding: '1rem' }), [])} {...props} /> + ; } diff --git a/package-lock.json b/package-lock.json index 653443192d1f..fe7c2aa8bbcc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15689,7 +15689,7 @@ }, "minimist": { "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "resolved": false, "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "dev": true, "optional": true @@ -15717,7 +15717,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "resolved": false, "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, "optional": true, @@ -15772,7 +15772,7 @@ }, "nopt": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "resolved": false, "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "dev": true, "optional": true, @@ -15890,7 +15890,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": false, "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true, "optional": true @@ -15899,7 +15899,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": false, "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "optional": true, diff --git a/private/server/colors.less b/private/server/colors.less index 476b75909183..638946d86c82 100755 --- a/private/server/colors.less +++ b/private/server/colors.less @@ -480,10 +480,6 @@ input:-webkit-autofill { } } -.new-logs { - background: @primary-action-contrast; -} - .avatar-suggestion-item { .question-mark::before { color: @secondary-font-color;