diff --git a/desktop/core/src/desktop/js/apps/admin/ServerLogs/ServerLogsHeader.scss b/desktop/core/src/desktop/js/apps/admin/ServerLogs/ServerLogsHeader.scss new file mode 100644 index 00000000000..aa033f55b6d --- /dev/null +++ b/desktop/core/src/desktop/js/apps/admin/ServerLogs/ServerLogsHeader.scss @@ -0,0 +1,68 @@ +// Licensed to Cloudera, Inc. under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. Cloudera, Inc. licenses this file +// to you under the Apache License, Version 2.0 (the +// 'License'); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an 'AS IS' BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +@import '../../../components/styles/variables'; + +.antd.cuix { + .server-admin-header { + display: flex; + align-items: center; + justify-content: space-between; + + .server__input-filter { + margin: $font-size-sm; + width: 200px; + padding: 2px; + + input { + box-shadow: none; + -webkit-box-shadow: none; + margin-top: 3px; + } + + .server__input-filter--prefix { + margin: 5px 3px 3px 3px; + } + } + + .server__filter-arrow { + height: 16px; + width: 16px; + margin: 0; + } + + .server--right-actions { + display: flex; + align-items: center; + justify-content: space-between; + + .server__wrap-logs, + .server__download-button, + .server__host-text { + margin-right: 8px; + } + + .server__checkbox-icon { + margin-right: 2px; + } + + .server__download-button { + border: 1px solid $fluidx-gray-600; + } + } + } +} diff --git a/desktop/core/src/desktop/js/apps/admin/ServerLogs/ServerLogsHeader.tsx b/desktop/core/src/desktop/js/apps/admin/ServerLogs/ServerLogsHeader.tsx new file mode 100644 index 00000000000..f28fca37b68 --- /dev/null +++ b/desktop/core/src/desktop/js/apps/admin/ServerLogs/ServerLogsHeader.tsx @@ -0,0 +1,88 @@ +// Licensed to Cloudera, Inc. under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. Cloudera, Inc. licenses this file +// to you under the Apache License, Version 2.0 (the +// 'License'); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an 'AS IS' BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React, { useState } from 'react'; +import { Input, Checkbox } from 'antd'; +import Button from 'cuix/dist/components/Button'; +import Search from '@cloudera/cuix-core/icons/react/SearchIcon'; +import Download from '@cloudera/cuix-core/icons/react/DownloadIcon'; +import { i18nReact } from '../../../utils/i18nReact'; +import huePubSub from '../../../utils/huePubSub'; +import './ServerLogsHeader.scss'; + +interface ServerLogsHeaderProps { + onFilterChange: (value: string) => void; + onWrapLogsChange: (wrap: boolean) => void; + hostName: string; +} + +const ServerLogsHeader: React.FC = ({ + onFilterChange, + onWrapLogsChange, + hostName +}): JSX.Element => { + const { t } = i18nReact.useTranslation(); + const [filterValue, setFilterValue] = useState(''); + const [wrapLogs, setWrapLogs] = useState(true); + + const handleFilterChange = (newFilterValue: string) => { + setFilterValue(newFilterValue); + onFilterChange(newFilterValue); + }; + + const handleDownloadClick = () => { + huePubSub.publish('open.link', '/desktop/download_logs'); + }; + + return ( +
+ + + + } + value={filterValue} + onChange={e => handleFilterChange(e.target.value)} + /> + +
+ {t(`Host: ${hostName}`)} + { + setWrapLogs(e.target.checked); + onWrapLogsChange(e.target.checked); + }} + checked={wrapLogs} + className="server__checkbox-icon" + /> + Wrap logs + +
+
+ ); +}; + +export default ServerLogsHeader; diff --git a/desktop/core/src/desktop/js/apps/admin/ServerLogs/ServerLogsTab.scss b/desktop/core/src/desktop/js/apps/admin/ServerLogs/ServerLogsTab.scss new file mode 100644 index 00000000000..57d0baf9538 --- /dev/null +++ b/desktop/core/src/desktop/js/apps/admin/ServerLogs/ServerLogsTab.scss @@ -0,0 +1,50 @@ +// Licensed to Cloudera, Inc. under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. Cloudera, Inc. licenses this file +// to you under the Apache License, Version 2.0 (the +// 'License'); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an 'AS IS' BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +@import '../../../components/styles/variables'; + +.antd.cuix { + .server-logs-component { + background-color: $fluidx-gray-100; + padding: 24px; + + .server__display-logs { + overflow: auto; + background-color: $fluidx-white; + width: 100%; + padding: 10px 0 10px 0; + + .server__log-line { + background-color: $fluidx-white; + margin: 0; + padding: 2px; + } + } + + .server_nowrap { + white-space: nowrap; + } + + .server__no-logs-found { + background-color: $fluidx-white; + } + + .server--highlight-word { + background-color: $fluidx-pear-050; + color: $fluidx-black; + } + } +} diff --git a/desktop/core/src/desktop/js/apps/admin/ServerLogs/ServerLogsTab.tsx b/desktop/core/src/desktop/js/apps/admin/ServerLogs/ServerLogsTab.tsx new file mode 100644 index 00000000000..0ed44c27a95 --- /dev/null +++ b/desktop/core/src/desktop/js/apps/admin/ServerLogs/ServerLogsTab.tsx @@ -0,0 +1,131 @@ +// Licensed to Cloudera, Inc. under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. Cloudera, Inc. licenses this file +// to you under the Apache License, Version 2.0 (the +// 'License'); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an 'AS IS' BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React, { useState, useEffect } from 'react'; +import { Spin, Alert } from 'antd'; +import ServerLogsHeader from './ServerLogsHeader'; +import './ServerLogsTab.scss'; + +const ServerLogs: React.FC = (): JSX.Element => { + const [loading, setLoading] = useState(true); + const [logs, setLogs] = useState([]); + const [noLogsFound, setNoLogsFound] = useState(false); + const [error, setError] = useState(''); + const [filter, setFilter] = useState(''); + const [hostName, setHostName] = useState(''); + const [wrapLogs, setWrapLogs] = useState(true); + + useEffect(() => { + const fetchLogs = async () => { + try { + const response = await fetch('/api/v1/logs'); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + const json = await response.json(); + const fetchedLogs = json['logs']; + if (!fetchedLogs.length || fetchedLogs[0] === '') { + setNoLogsFound(true); + } else { + setLogs(fetchedLogs); + } + setHostName(json['hue_hostname']); + } catch (error) { + setError(error.message); + } finally { + setLoading(false); + } + }; + fetchLogs(); + }, []); + + useEffect(() => { + const updateSize = () => { + const newHeight = document.documentElement.clientHeight - 250; + const logsComponent = document.querySelector('.server__display-logs') as HTMLElement; + if (logsComponent) { + logsComponent.style.height = `${newHeight}px`; + } + }; + updateSize(); + window.addEventListener('resize', updateSize); + return () => window.removeEventListener('resize', updateSize); + }, []); + + const handleFilterChange = (newFilterValue: string) => { + setFilter(newFilterValue); + }; + + const handleWrapLogsChange = (wrap: boolean) => { + setWrapLogs(wrap); + }; + + const highlightText = (text: string, searchValue: string) => { + if (!searchValue) { + return text; + } + const regex = new RegExp(`(${searchValue})`, 'gi'); + const parts = text.split(regex); + return parts.map((part, index) => + regex.test(part) ? ( + + {part} + + ) : ( + part + ) + ); + }; + + if (error) { + return ( +
+ +
+ ); + } + + return ( +
+ + {!loading && ( + <> + + {noLogsFound &&
No logs found!
} + +
+ {logs.map((line, index) => ( +
+ {highlightText(line, filter)} +
+ ))} +
+ + )} +
+
+ ); +}; + +export default ServerLogs; diff --git a/desktop/core/src/desktop/js/onePageViewModel.js b/desktop/core/src/desktop/js/onePageViewModel.js index 5790083409e..eb5a3ffeb52 100644 --- a/desktop/core/src/desktop/js/onePageViewModel.js +++ b/desktop/core/src/desktop/js/onePageViewModel.js @@ -461,8 +461,8 @@ class OnePageViewModel { self.extraEmbeddableURLParams(''); const currentPath = window.location.pathname; // Retrieve the current path from the window location const basePath = currentPath.split('=')[0]; - const inlineScriptsUrls = ['oozie', 'beeswax', 'jobbrowser', 'jobsub', 'logs'].some( - segment => basePath.includes(segment) + const inlineScriptsUrls = ['oozie', 'beeswax', 'jobbrowser', 'jobsub'].some(segment => + basePath.includes(segment) ); if (inlineScriptsUrls) { self.processHeaders(response).done($rawHtml => { @@ -640,9 +640,6 @@ class OnePageViewModel { url: '/desktop/metrics', app: function () { self.loadApp('metrics'); - self.getActiveAppViewModel(viewModel => { - viewModel.fetchMetrics(); - }); } }, { diff --git a/desktop/core/src/desktop/js/reactComponents/imports.js b/desktop/core/src/desktop/js/reactComponents/imports.js index 16a9a321acf..20acc47328a 100644 --- a/desktop/core/src/desktop/js/reactComponents/imports.js +++ b/desktop/core/src/desktop/js/reactComponents/imports.js @@ -16,6 +16,9 @@ export async function loadComponent(name) { case 'Configuration': return (await import('../apps/admin/Configuration/ConfigurationTab')).default; + case 'ServerLogs': + return (await import('../apps/admin/ServerLogs/ServerLogsTab')).default; + // Application global components here case 'AppBanner': return (await import('./AppBanner/AppBanner')).default; diff --git a/desktop/core/src/desktop/log/api.py b/desktop/core/src/desktop/log/api.py index 88683f60d98..33e720a799b 100644 --- a/desktop/core/src/desktop/log/api.py +++ b/desktop/core/src/desktop/log/api.py @@ -98,7 +98,8 @@ def get_hue_logs(request): # Read the previous log file contents buffer = _read_previous_log_file(LOG_BUFFER_SIZE, previous_log_file, prev_log_file_size, log_file_size) + buffer - response = {'hue_hostname': socket.gethostname(), 'logs': ''.join(buffer)} + + response = {'hue_hostname': socket.gethostname(), 'logs': buffer[::-1]} return JsonResponse(response) diff --git a/desktop/core/src/desktop/static/desktop/js/logs-inline.js b/desktop/core/src/desktop/static/desktop/js/logs-inline.js new file mode 100644 index 00000000000..e479e26d8bf --- /dev/null +++ b/desktop/core/src/desktop/static/desktop/js/logs-inline.js @@ -0,0 +1,3 @@ +(function () { + window.createReactComponents('#ServerLogs'); +})(); \ No newline at end of file diff --git a/desktop/core/src/desktop/templates/logs.mako b/desktop/core/src/desktop/templates/logs.mako index f9394d680c5..ab6dcebdd7d 100644 --- a/desktop/core/src/desktop/templates/logs.mako +++ b/desktop/core/src/desktop/templates/logs.mako @@ -16,18 +16,9 @@ <%! import re import sys - -from desktop.lib.conf import BoundConfig -from desktop.lib.i18n import smart_str from desktop.views import commonheader, commonfooter - -if sys.version_info[0] > 2: - from django.utils.translation import gettext as _ -else: - from django.utils.translation import ugettext as _ %> -<%namespace name="actionbar" file="actionbar.mako" /> <%namespace name="layout" file="about_layout.mako" /> % if not is_embeddable: @@ -36,186 +27,12 @@ ${ commonheader(_('Server Logs'), "about", user, request) | n,unicode } ${ layout.menubar(section='log_view') } - - -
-
- <%actionbar:render> - <%def name="search()"> - - - <%def name="creation()"> -
- - - - - ${ _('Download entire log as zip') } - -
- - - - <% log.reverse() %> - -
- % for l in log: -
${ smart_str(l, errors='ignore') }
- % endfor -
- -
+ +
+
- - % if not is_embeddable: ${ commonfooter(request, messages) | n,unicode } % endif diff --git a/desktop/core/src/desktop/templates/metrics.mako b/desktop/core/src/desktop/templates/metrics.mako index e61100205e1..eebe10f54cb 100644 --- a/desktop/core/src/desktop/templates/metrics.mako +++ b/desktop/core/src/desktop/templates/metrics.mako @@ -15,25 +15,10 @@ ## limitations under the License. <%! import sys - from desktop.views import commonheader, commonfooter from desktop import conf - -if sys.version_info[0] > 2: - from django.utils.translation import gettext as _ -else: - from django.utils.translation import ugettext as _ -%> - -<% -MAIN_SCROLLABLE = is_embeddable and "'.page-content'" or "window" -if conf.CUSTOM.BANNER_TOP_HTML.get(): - TOP_SNAP = is_embeddable and "78px" or "106px" -else: - TOP_SNAP = is_embeddable and "50px" or "106px" %> -<%namespace name="actionbar" file="actionbar.mako" /> <%namespace name="layout" file="about_layout.mako" /> %if not is_embeddable: