Skip to content

Commit

Permalink
[ui-serverLogs] Convert the Server Logs page within Administrator ser…
Browse files Browse the repository at this point in the history
…ver in ReactJS (#3927)

* Making the ServerLogs component

WIP

WIP

Searchbox working

WIP

WIP

WIP

w

f

WIP

r

t

WIP

WIP

WIP

WIP

WIP test

* fix the code

WIP

* Changes after review comments

* review comments changes

* Final changes after review comments

f

* Minor changes

Wip

* Changes

* Fix reverse=true

* Fix unit test

* Tests fixed

---------

Co-authored-by: Mohammed Tabraiz <[email protected]>
  • Loading branch information
ananya-agarwal and Mohammed Tabraiz authored Feb 4, 2025
1 parent d60b253 commit c762ef0
Show file tree
Hide file tree
Showing 13 changed files with 483 additions and 249 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// 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 from 'react';
import '../ServerLogs/ServerLogsTab.scss';

interface HighlightTextProps {
text: string;
searchValue: string;
highlightClassName?: string;
}

const HighlightText: React.FC<HighlightTextProps> = ({
text,
searchValue,
highlightClassName = 'server--highlight-word'
}) => {
if (!searchValue) {
return <>{text}</>;
}
const regex = new RegExp(`(${searchValue})`, 'gi');
const parts = text.split(regex);
return (
<>
{parts.map((part, index) =>
regex.test(part) ? (
<mark key={index} className={highlightClassName}>
{part}
</mark>
) : (
part
)
)}
</>
);
};

export default HighlightText;
17 changes: 17 additions & 0 deletions desktop/core/src/desktop/js/apps/admin/Components/utils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// 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.

export const SERVER_LOGS_API_URL = '/api/v1/logs';
Original file line number Diff line number Diff line change
@@ -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 {
.hue-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;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// 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<ServerLogsHeaderProps> = ({
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 (
<div className="hue-server-admin-header admin-header">
<Input
className="server__input-filter"
placeholder={t('Search in the logs')}
prefix={
<span className="server__input-filter--prefix">
<Search />
</span>
}
value={filterValue}
onChange={e => handleFilterChange(e.target.value)}
/>

<div className="server--right-actions">
<span className="server__host-text">{t(`Host: ${hostName}`)}</span>
<Checkbox
onChange={e => {
setWrapLogs(e.target.checked);
onWrapLogsChange(e.target.checked);
}}
checked={wrapLogs}
className="server__checkbox-icon"
id="wrapLogsToggle"
/>
<label className="server__wrap-logs" htmlFor="wrapLogsToggle">
{t('Wrap logs')}
</label>

<Button
className="server__download-button"
data-event="download-button"
icon={<Download />}
onClick={handleDownloadClick}
>
{t('Download entire log as zip')}
</Button>
</div>
</div>
);
};

export default ServerLogsHeader;
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// 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 {
.hue-server-logs-component {
background-color: $fluidx-gray-100;
padding: 24px;

.server__display-logs {
overflow: auto;
height: calc(100vh - 250px);
background-color: $fluidx-white;
width: 100%;
padding: 10px 0 10px 0;

.server__log-line {
background-color: $fluidx-white;
margin: 0;
padding: 2px;
}
}

.server_wrap {
white-space: nowrap;
}

.server__no-logs-found {
background-color: $fluidx-white;
}

.server--highlight-word {
background-color: $fluidx-pear-050;
color: $fluidx-black;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// 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 from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';
import ServerLogs from './ServerLogsTab';
import { mocked } from 'jest-mock';
import useLoadData from '../../../utils/hooks/useLoadData/useLoadData';

const mockData = jest.fn().mockReturnValue({
logs: ['Log entry 1', 'Log entry 2'],
hue_hostname: 'test-hostname'
});

const emptyMockData = jest.fn().mockReturnValue({
logs: [],
hue_hostname: 'test-hostname'
});

jest.mock('../../../utils/hooks/useLoadData/useLoadData');

afterEach(() => {
jest.clearAllMocks();
});

describe('ServerLogs Component', () => {
it('should render ServerLogs component with fetched logs', () => {
mocked(useLoadData).mockImplementation(() => ({
data: mockData(),
loading: false,
reloadData: jest.fn()
}));

render(<ServerLogs />);

expect(screen.getByText('Log entry 1')).toBeInTheDocument();
expect(screen.getByText('Log entry 2')).toBeInTheDocument();
});

test('it should handle the scenario when no logs are found', () => {
mocked(useLoadData).mockImplementation(() => ({
data: emptyMockData(),
loading: false,
reloadData: jest.fn()
}));

render(<ServerLogs />);

expect(screen.getByText('No logs found!')).toBeInTheDocument();
});

test('it should find and highlights the searched value', async () => {
mocked(useLoadData).mockImplementation(() => ({
data: mockData(),
loading: false,
reloadData: jest.fn()
}));

render(<ServerLogs />);

const searchValue = 'entry 1';
const searchInput = screen.getByPlaceholderText('Search in the logs');

await userEvent.type(searchInput, searchValue);

const highlightedElements = screen.getAllByText(searchValue, { selector: 'mark' });
expect(highlightedElements.length).toBeGreaterThan(0);
highlightedElements.forEach(element => {
expect(element).toHaveClass('server--highlight-word');
});
});

test('it should wrap the logs when the user checks "Wrap logs"', async () => {
mocked(useLoadData).mockImplementation(() => ({
data: mockData(),
loading: false,
reloadData: jest.fn()
}));

render(<ServerLogs />);

expect(screen.getByText('Log entry 1')).toHaveClass('server_wrap');

fireEvent.click(screen.getByLabelText('Wrap logs'));

expect(screen.getByText('Log entry 1')).not.toHaveClass('server_wrap');
});
});
Loading

0 comments on commit c762ef0

Please sign in to comment.