Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 7 additions & 128 deletions packages/components/src/components/Log/Log.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,15 @@ limitations under the License.
/* istanbul ignore file */

import { Component, createRef } from 'react';
import { Button, PrefixContext, SkeletonText } from '@carbon/react';
import { PrefixContext, SkeletonText } from '@carbon/react';
import { FixedSizeList as List } from 'react-window';
import { injectIntl, useIntl } from 'react-intl';
import { getStepStatusReason, isRunning } from '@tektoncd/dashboard-utils';
import { DownToBottom, Information, UpToTop } from '@carbon/react/icons';
import { Information } from '@carbon/react/icons';

import {
hasElementPositiveVerticalScrollBottom,
hasElementPositiveVerticalScrollTop,
isElementEndBelowViewBottom,
isElementStartAboveViewTop
isElementEndBelowViewBottom
} from './domUtils';
import DotSpinner from '../DotSpinner';
import LogFormat from '../LogFormat';
Expand Down Expand Up @@ -89,7 +87,7 @@ export class LogContainer extends Component {

componentDidMount() {
this.loadLog();
if (this.props.enableLogAutoScroll || this.props.enableLogScrollButtons) {
if (this.props.enableLogAutoScroll) {
this.wasRunningAfterMounting();
window.addEventListener('scroll', this.handleLogScroll, true);
this.handleLogScroll();
Expand All @@ -98,7 +96,7 @@ export class LogContainer extends Component {

componentDidUpdate(prevProps, prevState) {
if (
(this.props.enableLogAutoScroll || this.props.enableLogScrollButtons) &&
this.props.enableLogAutoScroll &&
(prevState.logs?.length !== this.state.logs?.length ||
prevProps.isLogsMaximized !== this.props.isLogsMaximized)
) {
Expand Down Expand Up @@ -128,18 +126,9 @@ export class LogContainer extends Component {
handleLogScroll = () => {
if (!this.state.loading) {
const isLogBottomUnseen = this.isLogBottomUnseen();
const isLogTopUnseen =
this.props.enableLogScrollButtons && this.isLogTopUnseen();
this.updateScrollButtonCoordinates();

if (
isLogBottomUnseen !== this.state.isLogBottomUnseen ||
(this.props.enableLogScrollButtons &&
isLogTopUnseen !== this.state.isLogTopUnseen)
) {
if (isLogBottomUnseen !== this.state.isLogBottomUnseen) {
this.setState({
isLogBottomUnseen,
isLogTopUnseen
isLogBottomUnseen
});
}
}
Expand All @@ -163,15 +152,6 @@ export class LogContainer extends Component {
);
};

isLogTopUnseen = () => {
return (
isElementStartAboveViewTop(this.logRef?.current) ||
hasElementPositiveVerticalScrollTop(
this.textRef?.current?.firstElementChild
)
);
};

scrollToBottomLog = () => {
const longTextElement = this.textRef?.current?.firstElementChild;
if (longTextElement) {
Expand All @@ -182,14 +162,6 @@ export class LogContainer extends Component {
rootElement.scrollTop = rootElement.scrollHeight - rootElement.clientHeight;
};

scrollToTopLog = () => {
const longTextElement = this.textRef?.current?.firstElementChild;
if (longTextElement) {
longTextElement.scrollTop = 0;
}
document.documentElement.scrollTop = 0;
};

wasRunningAfterMounting = () => {
if (this.alreadyWasRunningAfterMounting) {
return true;
Expand All @@ -203,98 +175,6 @@ export class LogContainer extends Component {
return false;
};

updateScrollButtonCoordinates = () => {
if (this.props.enableLogScrollButtons) {
const logRectangle = this.logRef.current?.getBoundingClientRect();
const logElementRight =
document.documentElement.clientWidth - logRectangle.right;

const scrollButtonTop = Math.max(0, logRectangle.top);

const scrollButtonBottom = Math.max(
0,
document.documentElement.clientHeight - logRectangle.bottom
);

this.updateCssStyleProperty(logElementRight, '--tkn-log-element-right');
this.updateCssStyleProperty(scrollButtonTop, '--tkn-scroll-button-top');
this.updateCssStyleProperty(
scrollButtonBottom,
'--tkn-scroll-button-bottom'
);
}
};

updateCssStyleProperty = (computedVariable, variableName) => {
// instead of using a state variable + inline styling for the button vertical position,
// a class variable + css custom property are used (avoiding unnecessary re-rendering of entire component)
if (
!Number.isNaN(computedVariable) &&
this[variableName] !== computedVariable
) {
this[variableName] = computedVariable;
document.documentElement.style.setProperty(
variableName,
`${computedVariable.toString()}px`
);
}
};

getScrollButtons = () => {
const carbonPrefix = this.context;
const { enableLogScrollButtons, intl } = this.props;
const { isLogBottomUnseen, isLogTopUnseen, loading } = this.state;

if (!enableLogScrollButtons || loading) {
return null;
}
const scrollButtonTopMessage = intl.formatMessage({
id: 'dashboard.logs.scrollToTop',
defaultMessage: 'Scroll to start of logs'
});
const scrollButtonBottomMessage = intl.formatMessage({
id: 'dashboard.logs.scrollToBottom',
defaultMessage: 'Scroll to end of logs'
});

return (
<div className="button-container">
{isLogTopUnseen ? (
<Button
className={`${carbonPrefix}--copy-btn`}
hasIconOnly
iconDescription={scrollButtonTopMessage}
id="log-scroll-to-start-btn"
onClick={this.scrollToTopLog}
renderIcon={() => (
<UpToTop>
<title>{scrollButtonTopMessage}</title>
</UpToTop>
)}
size="sm"
tooltipPosition="right"
/>
) : null}
{isLogBottomUnseen ? (
<Button
className={`${carbonPrefix}--copy-btn`}
iconDescription={scrollButtonBottomMessage}
hasIconOnly
id="log-scroll-to-end-btn"
onClick={this.scrollToBottomLog}
renderIcon={() => (
<DownToBottom>
<title>{scrollButtonBottomMessage}</title>
</DownToBottom>
)}
size="sm"
tooltipPosition="right"
/>
) : null}
</div>
);
};

getLogList = () => {
const {
intl,
Expand Down Expand Up @@ -610,7 +490,6 @@ export class LogContainer extends Component {
{this.getLogList()}
</div>
{this.logTrailer()}
{this.getScrollButtons()}
</>
)}
</pre>
Expand Down
66 changes: 1 addition & 65 deletions packages/components/src/components/Log/Log.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import { fireEvent, waitFor } from '@testing-library/react';
import { waitFor } from '@testing-library/react';
import Log, { logFormatRegex } from './Log';
import { render } from '../../utils/test';

Expand Down Expand Up @@ -193,70 +193,6 @@ describe('Log', () => {
);
await waitFor(() => expect(spiedFn).not.toHaveBeenCalled());
});

it('displays the scroll to top button when the virtualized list scroll is all the way down', async () => {
const long = Array.from(
{ length: 20000 },
(_v, i) => `Line ${i + 1}\n`
).join('');
vi.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue({
bottom: 0,
top: 0,
right: 0
});
vi.spyOn(Element.prototype, 'scrollTop', 'get').mockReturnValue(1);
const spiedFn = vi.spyOn(Element.prototype, 'scrollTop', 'set'); // the scrollTop value is changed in scrollToBottomLog

const { container } = render(
<Log
stepStatus={{ terminated: { reason: 'Completed' } }}
enableLogScrollButtons
fetchLogs={() => long}
/>
);
await waitFor(() => {
expect(
container.querySelector('#log-scroll-to-start-btn')
).not.toBeNull();
});
expect(container.querySelector('#log-scroll-to-end-btn')).toBeNull();
fireEvent.click(container.querySelector('#log-scroll-to-start-btn'));

await waitFor(() => expect(spiedFn).toHaveBeenCalled());
});

it('displays both scroll buttons when the virtualized list scrolled is neither all the way up nor down', async () => {
const long = Array.from(
{ length: 20000 },
(_v, i) => `Line ${i + 1}\n`
).join('');
vi.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue({
bottom: 0,
top: 0,
right: 0
});
vi.spyOn(Element.prototype, 'scrollTop', 'get').mockReturnValue(1);
vi.spyOn(Element.prototype, 'scrollHeight', 'get').mockReturnValue(2);
const spiedFn = vi.spyOn(Element.prototype, 'scrollTop', 'set'); // the scrollTop value is changed in scrollToBottomLog

const { container } = render(
<Log
stepStatus={{ terminated: { reason: 'Completed' } }}
enableLogScrollButtons
fetchLogs={() => long}
/>
);
await waitFor(() => {
expect(
container.querySelector('#log-scroll-to-start-btn')
).not.toBeNull();
});
expect(container.querySelector('#log-scroll-to-end-btn')).not.toBeNull();
fireEvent.click(container.querySelector('#log-scroll-to-end-btn'));

await waitFor(() => expect(spiedFn).toHaveBeenCalled());
});

it('renders the provided content when streaming logs', async () => {
const { getByText } = render(
<Log
Expand Down
31 changes: 14 additions & 17 deletions packages/components/src/components/Log/_Log.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,20 @@ limitations under the License.
@use '@carbon/react/scss/theme' as *;
@use '@carbon/react/scss/type' as *;


.tkn--scroll-buttons {
position: fixed;
inset-inline-end: .75rem;
inset-block-end: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
z-index: 10000;
}
.tkn--scroll-buttons--maximized{
inset-inline-end: 2rem;
}

pre.tkn--log {
--tkn-log-inline-padding: 1.6rem;
position: relative;
Expand Down Expand Up @@ -71,23 +85,6 @@ pre.tkn--log {
inline-size: var(--tkn-log-inline-padding);
}

#log-scroll-to-start-btn, #log-scroll-to-end-btn {
position: fixed;
inset-inline-end: var(--tkn-log-element-right);

&::before { // the tooltip is not shown because it is outside of its parent .button-container (which has clip-path inset) But fin still appears.
content: none; // To remove fin of truncated tooltip. To obtain higher specificity than Carbon css, id selector is needed.
}
}

#log-scroll-to-start-btn {
inset-block-start: calc(var(--tkn-scroll-button-top) + 3.125rem); //3.125 rem is the maximum between padding-block-start of pre.tkn--log and between the page header height
}

#log-scroll-to-end-btn {
inset-block-end: var(--tkn-scroll-button-bottom);
}

.#{$prefix}--btn--ghost,
.#{$prefix}--copy-btn {
inline-size: 2rem;
Expand Down
Loading