Skip to content

Chore/logs #152

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 24, 2024
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
127 changes: 70 additions & 57 deletions src/components/LogList/LogActionBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import React from 'react';
import styled from 'styled-components';
import { useTranslation } from 'react-i18next';
import Button from '../Button';
import { NotificationType, useNotifications } from '../Notifications';
import { NotificationType, useNotifications, Notification } from '../Notifications';
import copy from 'copy-to-clipboard';
import Icon from '../Icon';
import { LocalSearchType, LogItem } from '../../hooks/useLogData';
import FilterInput from '../FilterInput';
import { TFunction } from 'i18next';

//
// Typedef
Expand All @@ -20,6 +21,28 @@ type LogActionBarProps = {
spaceAround?: boolean;
};

const handleFilterChange = (search: LocalSearchType) => (key: string) => {
search.search(key);
};

const handleFilterSubmit = (search: LocalSearchType) => () => {
search.nextResult();
};

const handleCopyButtonClick =
(
addNotification: (...notification: Notification[]) => void,
data: LogItem[],
t: TFunction<'translation', undefined, 'translation'>,
) =>
() => {
copy(data.map((item) => (typeof item === 'object' ? item.line : item)).join('\n'));
addNotification({
type: NotificationType.Info,
message: t('task.all-logs-copied'),
});
};

//
// Component
//
Expand All @@ -33,76 +56,66 @@ const LogActionBar: React.FC<LogActionBarProps> = ({
}) => {
const { addNotification } = useNotifications();
const { t } = useTranslation();

return (
<LogActionBarContainer spaceAround={spaceAround} data-testid="log-action-bar">
{data && data.length > 0 && (
<>
<SearchContainer>
<FilterInput
sectionLabel={t('task.log-search')}
onChange={(e) => {
search.search(e);
}}
onSubmit={() => {
search.nextResult();
}}
noClear
customIcon={['search', 'sm']}
customIconElement={
search.result.active &&
search.result.result.length > 0 && (
<ResultElement>
{search.result.current + 1}/{search.result.result.length}
</ResultElement>
)
}
infoMsg={t('task.log-search-tip') ?? ''}
/>
</SearchContainer>

<Buttons data-testid="log-action-bar-buttons">
<>
<SearchContainer>
<FilterInput
sectionLabel={t('task.log-search')}
onChange={handleFilterChange(search)}
onSubmit={handleFilterSubmit(search)}
noClear
customIcon={['search', 'sm']}
customIconElement={
search.result.active &&
search.result.result.length > 0 && (
<ResultElement>
{search.result.current + 1}/{search.result.result.length}
</ResultElement>
)
}
infoMsg={t('task.log-search-tip') ?? ''}
/>
</SearchContainer>
<Buttons data-testid="log-action-bar-buttons">
{data && data.length > 0 && (
<Button
data-testid="log-action-button"
title={t('task.copy-logs-to-clipboard') ?? ''}
iconOnly
onClick={handleCopyButtonClick(addNotification, data, t)}
>
<Icon name="copy" size="sm" />
</Button>
)}

<a title={t('task.download-logs') ?? ''} href={downloadlink} download data-testid="log-action-button">
<Button
onClick={() => {
copy(data.map((item) => (typeof item === 'object' ? item.line : item)).join('\n'));
addNotification({
type: NotificationType.Info,
message: t('task.all-logs-copied'),
message: t('task.downloading-logs'),
});
}}
iconOnly
>
<Icon name="copy" size="sm" />
<Icon name="download" size="sm" />
</Button>
</a>

<a title={t('task.download-logs') ?? ''} href={downloadlink} download data-testid="log-action-button">
<Button
onClick={() => {
addNotification({
type: NotificationType.Info,
message: t('task.downloading-logs'),
});
}}
iconOnly
>
<Icon name="download" size="sm" />
</Button>
</a>

{setFullscreen && (
<Button
title={t('task.show-fullscreen') ?? ''}
onClick={() => setFullscreen()}
withIcon
data-testid="log-action-button"
>
<Icon name="maximize" size="sm" />
</Button>
)}
</Buttons>
</>
)}
{setFullscreen && (
<Button
title={t('task.show-fullscreen') ?? ''}
onClick={() => setFullscreen()}
withIcon
data-testid="log-action-button"
>
<Icon name="maximize" size="sm" />
</Button>
)}
</Buttons>
</>
</LogActionBarContainer>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ const Search = {
};

describe('LogActionBar', () => {
it('Should render empty action bar since there is no log data', () => {
it('Should render only download and fullscreen buttons since there is no log data', () => {
mount(
<TestWrapper>
<LogActionBar downloadlink="" data={[]} search={Search} />
</TestWrapper>,
);

gid('log-action-bar').children().should('have.length', 0);
gid('log-action-bar').children().should('have.length', 2);
});

it('Should render action bar with two buttons', () => {
Expand Down
8 changes: 0 additions & 8 deletions src/components/LogList/__tests__/LogList.test.cypress.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,7 @@ function generateLines(amount: number) {
const LIST_CONTAINER_CLASS = 'ReactVirtualized__List';

describe('LogActionBar', () => {
it('Should render empty wrapper in initial situation', () => {
mount(
<TestWrapper>
<LogList logdata={createLogData({})} downloadUrl="" />
</TestWrapper>,
);

gid('loglist-wrapper').children().should('have.length', 1);
});

it('Should render message about empty preload when preload was empty or error and final fetch is not started', () => {
mount(
Expand Down
27 changes: 10 additions & 17 deletions src/components/LogList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { LogData, LogItem, SearchState } from '../../hooks/useLogData';
import { useDebounce } from 'use-debounce';
import { AsyncStatus, Log } from '../../types';
import { lighten } from 'polished';
import LogActionBar from './LogActionBar';
import { getTimestampString } from '../../utils/date';
import { TimezoneContext } from '../TimezoneProvider';
import { MeasuredCellParent } from 'react-virtualized/dist/es/CellMeasurer';
Expand All @@ -29,7 +28,7 @@ type LogProps = {

const LIST_MAX_HEIGHT = 400;

const LogList: React.FC<LogProps> = ({ logdata, fixedHeight, onScroll, downloadUrl, setFullscreen }) => {
const LogList: React.FC<LogProps> = ({ logdata, fixedHeight, onScroll }) => {
const { timezone } = useContext(TimezoneContext);
const { t } = useTranslation();
const rows = logdata.logs;
Expand Down Expand Up @@ -110,16 +109,16 @@ const LogList: React.FC<LogProps> = ({ logdata, fixedHeight, onScroll, downloadU
[onScroll],
);

const handleScroll = (args: { scrollTop: number; clientHeight: number; scrollHeight: number }) => {
if (args.scrollTop + args.clientHeight >= args.scrollHeight) {
setStickBottom(true);
} else if (stickBottom) {
setStickBottom(false);
}
};

return (
<div style={{ flex: '1 1 0' }} data-testid="loglist-wrapper">
<LogActionBar
data={logdata.logs}
downloadlink={downloadUrl}
setFullscreen={setFullscreen}
search={logdata.localSearch}
spaceAround={!!fixedHeight}
/>

{rows.length === 0 && ['Ok', 'Error'].includes(logdata.preloadStatus) && logdata.status === 'NotAsked' && (
<div data-testid="loglist-preload-empty">{t('task.no-preload-logs')}</div>
)}
Expand All @@ -137,13 +136,7 @@ const LogList: React.FC<LogProps> = ({ logdata, fixedHeight, onScroll, downloadU
rowHeight={cache.rowHeight}
onRowsRendered={onRowsRendered}
deferredMeasurementCache={cache}
onScroll={(args: { scrollTop: number; clientHeight: number; scrollHeight: number }) => {
if (args.scrollTop + args.clientHeight >= args.scrollHeight) {
setStickBottom(true);
} else if (stickBottom) {
setStickBottom(false);
}
}}
onScroll={handleScroll}
rowRenderer={({
index,
style,
Expand Down
53 changes: 29 additions & 24 deletions src/hooks/useLogData/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Log, AsyncStatus, APIError } from '../../types';
import { DataModel, defaultError } from '../../hooks/useResource';
import { apiHttp } from '../../constants';
Expand Down Expand Up @@ -78,7 +78,7 @@ const useLogData = ({ preload, paused, url, pagesize }: LogDataSettings): LogDat
.then((response) => response.json())
.then((result: DataModel<Log[]> | APIError) => {
if (isOkResult(result)) {
// Check if there was any new lines. If there wasnt, lets cancel post finish polling.
// Check if there was any new lines. If there wasn't, let's cancel post finish polling.
// Or if was postpoll and we didnt get any results
if (
(result.data.length > 0 && logs.length > 0 && result.data[0].row === logs.length - 1) ||
Expand Down Expand Up @@ -147,7 +147,7 @@ const useLogData = ({ preload, paused, url, pagesize }: LogDataSettings): LogDat
}, [paused, url, status, fetchLogs]);

useEffect(() => {
// For preload to happen following rules has to be matched
// For preload to happen following rules have to be matched
// paused -> Run has to be on running state
// status -> This should always be NotAsked if paused is on. Check just in case
// preload -> Run has to be runnign
Expand All @@ -172,7 +172,7 @@ const useLogData = ({ preload, paused, url, pagesize }: LogDataSettings): LogDat

// Post finish polling
// In some cases all logs might not be there after task finishes. For this, lets poll new logs every 10sec until
// there is no new lines
// there are no new lines
useEffect(() => {
let t: number;
if (status === 'Ok' && postPoll) {
Expand Down Expand Up @@ -213,31 +213,34 @@ const useLogData = ({ preload, paused, url, pagesize }: LogDataSettings): LogDat

const [searchResult, setSearchResult] = useState<SearchState>(emptySearchResult);

function search(str: string) {
if (!str) {
return setSearchResult(emptySearchResult);
}
const query = str.toLowerCase();
const results = logs
.filter(filterbySearchTerm)
.filter((line) => line.line.toLowerCase().indexOf(query) > -1)
.map((item) => {
const index = item.line.toLowerCase().indexOf(query);
return {
line: item.row,
char: [index, index + str.length] as [number, number],
};
});
setSearchResult({ active: true, result: results, current: 0, query: str });
}
const search = useCallback(
(str: string) => {
if (!str) {
return setSearchResult(emptySearchResult);
}
const query = str.toLowerCase();
const results = logs
.filter(filterbySearchTerm)
.filter((line) => line.line.toLowerCase().indexOf(query) > -1)
.map((item) => {
const index = item.line.toLowerCase().indexOf(query);
return {
line: item.row,
char: [index, index + str.length] as [number, number],
};
});
setSearchResult({ active: true, result: results, current: 0, query: str });
},
[logs],
);

function nextResult() {
const nextResult = useCallback(() => {
if (searchResult.current === searchResult.result.length - 1) {
setSearchResult((cur) => ({ ...cur, current: 0 }));
} else {
setSearchResult((cur) => ({ ...cur, current: cur.current + 1 }));
}
}
}, [searchResult]);

// Clean up on url change
useEffect(() => {
Expand All @@ -251,7 +254,9 @@ const useLogData = ({ preload, paused, url, pagesize }: LogDataSettings): LogDat
};
}, [url]);

return { logs, status, preloadStatus, error, loadMore, localSearch: { search, nextResult, result: searchResult } };
const localSearch = useMemo(() => ({ search, nextResult, result: searchResult }), [nextResult, search, searchResult]);

return { logs, status, preloadStatus, error, loadMore, localSearch };
};

function filterbySearchTerm(item: LogItem): item is Log {
Expand Down
Loading
Loading