298 lines
9.4 KiB
JavaScript
298 lines
9.4 KiB
JavaScript
// Copyright 2018 Red Hat, Inc
|
|
//
|
|
// Licensed 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, { useEffect, useState } from 'react'
|
|
import PropTypes from 'prop-types'
|
|
import {
|
|
Breadcrumb,
|
|
BreadcrumbItem,
|
|
Divider,
|
|
EmptyState,
|
|
EmptyStateVariant,
|
|
EmptyStateIcon,
|
|
Title,
|
|
ToggleGroup,
|
|
ToggleGroupItem,
|
|
} from '@patternfly/react-core'
|
|
import { FileCodeIcon } from '@patternfly/react-icons'
|
|
|
|
import { Fetching } from '../Fetching'
|
|
|
|
// Helper function to sort list of numbers in ascending order
|
|
const sortNumeric = (a, b) => a - b
|
|
|
|
// When scrolling down to a highlighted section, we don't want to want to keep a little bit of
|
|
// context.
|
|
const SCROLL_OFFSET = -50
|
|
|
|
export default function LogFile({
|
|
logfileName,
|
|
logfileContent,
|
|
isFetching,
|
|
handleBreadcrumbItemClick,
|
|
location,
|
|
history,
|
|
...props
|
|
}) {
|
|
const [severity, setSeverity] = useState(props.severity)
|
|
const [highlightStart, setHighlightStart] = useState(0)
|
|
const [highlightEnd, setHighlightEnd] = useState(0)
|
|
// We only want to scroll down to the highlighted section if the highlight
|
|
// fields we're populated from the URL parameters. Here we assume that we
|
|
// always want to scroll when the page is loaded and therefore disable the
|
|
// initialScroll parameter in the onClick handler that is called when a line
|
|
// or section is marked.
|
|
const [scrollOnPageLoad, setScrollOnPageLoad] = useState(true)
|
|
|
|
useEffect(() => {
|
|
// Only highlight the lines if the log is present (otherwise it doesn't make
|
|
// sense). Although, scrolling to the selected section only works once the
|
|
// necessary log lines are part of the DOM tree.
|
|
// Additionally note that if we set highlightStart before the page content
|
|
// is available then the window scrolling won't match any lines and we won't
|
|
// scroll. Then when we try to set highlightStart after page content is loaded
|
|
// the value isn't different than what is set previously preventing the
|
|
// scroll event from firing.
|
|
if (!isFetching) {
|
|
// Get the line numbers to highlight from the URL and directly cast them to
|
|
// a number. The substring(1) removes the '#' character.
|
|
const lines = location.hash
|
|
.substring(1)
|
|
.split('-')
|
|
.map(Number)
|
|
.sort(sortNumeric)
|
|
if (lines.length > 1) {
|
|
setHighlightStart(lines[0])
|
|
setHighlightEnd(lines[1])
|
|
} else if (lines.length === 1) {
|
|
setHighlightStart(lines[0])
|
|
setHighlightEnd(lines[0])
|
|
}
|
|
}
|
|
}, [location.hash, isFetching])
|
|
|
|
useEffect(() => {
|
|
const scrollToHighlightedLine = () => {
|
|
const elements = document.getElementsByClassName('ln-' + highlightStart)
|
|
if (elements.length > 0) {
|
|
// When scrolling down to the highlighted section keep some vertical
|
|
// offset so we can see some contextual log lines.
|
|
const y =
|
|
elements[0].getBoundingClientRect().top +
|
|
window.pageYOffset +
|
|
SCROLL_OFFSET
|
|
window.scrollTo({ top: y, behavior: 'smooth' })
|
|
}
|
|
}
|
|
|
|
if (scrollOnPageLoad) {
|
|
scrollToHighlightedLine()
|
|
}
|
|
}, [scrollOnPageLoad, highlightStart])
|
|
|
|
function handleItemClick(isSelected, event) {
|
|
const id = parseInt(event.currentTarget.id)
|
|
setSeverity(id)
|
|
writeSeverityToUrl(id)
|
|
// TODO (felix): Should we add a state for the toggling "progress", so we
|
|
// can show a spinner (like fetching) when the new log level lines are
|
|
// "calculated". As this might take some time, the UI is often unresponsive
|
|
// when clicking on a log level button.
|
|
}
|
|
|
|
function writeSeverityToUrl(severity) {
|
|
const urlParams = new URLSearchParams('')
|
|
urlParams.append('severity', severity)
|
|
history.push({
|
|
pathname: location.pathname,
|
|
search: urlParams.toString(),
|
|
})
|
|
}
|
|
|
|
function updateSelection(event) {
|
|
const lines = window.location.hash.substring(1).split('-').map(Number)
|
|
const lineClicked = Number(event.currentTarget.innerText)
|
|
if (!event.shiftKey || lines.length === 0) {
|
|
// First line clicked
|
|
lines[0] = [lineClicked]
|
|
lines.splice(1, 1)
|
|
} else {
|
|
// Second line shift-clicked
|
|
const distances = lines.map((pos) => Math.abs(lineClicked - pos))
|
|
// Adjust the range based on the edge distance
|
|
if (distances[0] < distances[1]) {
|
|
lines[0] = lineClicked
|
|
} else {
|
|
lines[1] = lineClicked
|
|
}
|
|
}
|
|
window.location.hash = '#' + lines.sort(sortNumeric).join('-')
|
|
// We don't want to scroll to that section if we just highlighted the lines
|
|
setScrollOnPageLoad(false)
|
|
}
|
|
|
|
function renderLogfile(logfileContent, severity) {
|
|
return (
|
|
<>
|
|
<ToggleGroup aria-label="Log line severity filter">
|
|
<ToggleGroupItem
|
|
buttonId='0'
|
|
text='All'
|
|
isSelected={severity === 0}
|
|
onChange={handleItemClick}
|
|
/>
|
|
<ToggleGroupItem
|
|
buttonId='1'
|
|
text='Debug'
|
|
isSelected={severity === 1}
|
|
onChange={handleItemClick}
|
|
/>
|
|
<ToggleGroupItem
|
|
buttonId='2'
|
|
text='Info'
|
|
isSelected={severity === 2}
|
|
onChange={handleItemClick}
|
|
/>
|
|
<ToggleGroupItem
|
|
buttonId='3'
|
|
text='Warning'
|
|
isSelected={severity === 3}
|
|
onChange={handleItemClick}
|
|
/>
|
|
<ToggleGroupItem
|
|
buttonId='4'
|
|
text='Error'
|
|
isSelected={severity === 4}
|
|
onChange={handleItemClick}
|
|
/>
|
|
<ToggleGroupItem
|
|
buttonId='5'
|
|
text='Trace'
|
|
isSelected={severity === 5}
|
|
onChange={handleItemClick}
|
|
/>
|
|
<ToggleGroupItem
|
|
buttonId='6'
|
|
text='Audit'
|
|
isSelected={severity === 6}
|
|
onChange={handleItemClick}
|
|
/>
|
|
<ToggleGroupItem
|
|
buttonId='7'
|
|
text='Critical'
|
|
isSelected={severity === 7}
|
|
onChange={handleItemClick}
|
|
/>
|
|
</ToggleGroup>
|
|
<Divider />
|
|
<pre className="zuul-log-output">
|
|
<table>
|
|
<tbody>
|
|
{logfileContent.map((line) => {
|
|
// Highlight the line if it's part of the selected range
|
|
const highlightLine =
|
|
line.index >= highlightStart && line.index <= highlightEnd
|
|
return (
|
|
line.severity >= severity && (
|
|
<tr
|
|
key={line.index}
|
|
className={`ln-${line.index} ${
|
|
highlightLine ? 'highlight' : ''
|
|
}`}
|
|
>
|
|
<td className="line-number" onClick={updateSelection}>
|
|
{line.index}
|
|
</td>
|
|
<td>
|
|
<span
|
|
className={`log-message zuul-log-sev-${
|
|
line.severity || 0
|
|
}`}
|
|
>
|
|
{line.text + '\n'}
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
)
|
|
)
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</pre>
|
|
</>
|
|
)
|
|
}
|
|
|
|
// Split the logfile's name to show some breadcrumbs
|
|
const logfilePath = logfileName.split('/')
|
|
|
|
const content =
|
|
!logfileContent && isFetching ? (
|
|
<Fetching />
|
|
) : logfileContent ? (
|
|
renderLogfile(logfileContent, severity)
|
|
) : (
|
|
<EmptyState variant={EmptyStateVariant.small}>
|
|
<EmptyStateIcon icon={FileCodeIcon} />
|
|
<Title headingLevel="h4" size="lg">
|
|
This logfile could not be found
|
|
</Title>
|
|
</EmptyState>
|
|
)
|
|
|
|
return (
|
|
<>
|
|
<div style={{ padding: '1rem' }}>
|
|
<Breadcrumb>
|
|
<BreadcrumbItem
|
|
key={-1}
|
|
// Fake a link via the "to" property to get an appropriate CSS
|
|
// styling for the breadcrumb. The link itself is handled via a
|
|
// custom onClick handler to allow client-side routing with
|
|
// react-router. The BreadcrumbItem only allows us to specify a
|
|
// <a href=""> as link which would post-back to the server.
|
|
to="#"
|
|
onClick={() => handleBreadcrumbItemClick()}
|
|
>
|
|
Logs
|
|
</BreadcrumbItem>
|
|
{logfilePath.map((part, index) => (
|
|
<BreadcrumbItem
|
|
key={index}
|
|
isActive={index === logfilePath.length - 1}
|
|
>
|
|
{part}
|
|
</BreadcrumbItem>
|
|
))}
|
|
</Breadcrumb>
|
|
</div>
|
|
{content}
|
|
</>
|
|
)
|
|
}
|
|
|
|
LogFile.propTypes = {
|
|
logfileName: PropTypes.string.isRequired,
|
|
logfileContent: PropTypes.array,
|
|
severity: PropTypes.number,
|
|
isFetching: PropTypes.bool.isRequired,
|
|
handleBreadcrumbItemClick: PropTypes.func.isRequired,
|
|
location: PropTypes.object.isRequired,
|
|
history: PropTypes.object.isRequired,
|
|
}
|
|
|
|
LogFile.defaultProps = {
|
|
severity: 0,
|
|
}
|