Skip to content

Commit

Permalink
feat(logs): add live-tail component
Browse files Browse the repository at this point in the history
ref: MANAGER-15918

Signed-off-by: Romain Jamet <[email protected]>
  • Loading branch information
Romain Jamet committed Dec 18, 2024
1 parent a77538a commit f91ee90
Show file tree
Hide file tree
Showing 36 changed files with 1,086 additions and 56 deletions.
4 changes: 3 additions & 1 deletion packages/manager/modules/logs-to-customer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
"test": "vitest run",
"test:coverage": "vitest run --coverage"
},
"dependencies": {},
"dependencies": {
"@tanstack/react-virtual": "^3.10.9"
},
"devDependencies": {
"@ovh-ux/manager-core-api": "^0.9.0",
"@ovh-ux/manager-react-components": "^1.41.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export default function LogsToCustomerModule({
if (error)
return (
<ApiError
testId="logKinds-error"
error={error}
onRetry={() =>
queryClient.refetchQueries({
Expand All @@ -79,6 +80,9 @@ export default function LogsToCustomerModule({
if (logKinds.length === 0)
return <Description>{t('log_kind_empty_state_description')}</Description>;

if (!currentLogKind)
return <Description>{t('log_kind_no_kind_selected')}</Description>;

return (
<div className="flex flex-col gap-8">
{logKinds.length > 1 && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ describe('LogsToCustomer module', () => {
it('should display an error if /log/kind api is KO', async () => {
await renderTest({ isLogKindsKO: true });

await waitFor(() => expect(screen.getByText('error_title')).toBeDefined(), {
timeout: 10_000,
});
await waitFor(
() => expect(screen.getByTestId('logKinds-error')).toBeVisible(),
{
timeout: 10_000,
},
);
});

it('should render a loading state when the api request is pending', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,16 @@ import React from 'react';
import { useTranslation } from 'react-i18next';

export interface IError {
testId: string;
error: Error;
onRetry: () => void;
}

export default function ApiError({ error, onRetry }: IError) {
export default function ApiError({ error, onRetry, testId }: Readonly<IError>) {
const { t } = useTranslation('error');

return (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-4" data-testid={testId}>
<OsdsMessage
color={ODS_THEME_COLOR_INTENT.error}
type={ODS_MESSAGE_TYPE.error}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React, { useContext } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { OsdsSpinner } from '@ovhcloud/ods-components/react';
import { LogsContext } from '../../LogsToCustomer.context';
import ApiError from '../apiError/ApiError.component';
import {
getLogTailUrlQueryKey,
useLogTailUrl,
} from '../../data/hooks/useLogTailUrl';
import { LogMessages } from './logMessages/LogMessages.component';
import './logTail.css';

export default function LogTail() {
const { currentLogKind, logApiUrls, logApiVersion } = useContext(LogsContext);
const queryClient = useQueryClient();

const { data, error, isPending } = useLogTailUrl({
logTailUrl: logApiUrls.logUrl,
logKind: currentLogKind?.name,
apiVersion: logApiVersion,
});

if (isPending || error) {
return (
<div
className={`h-[--tail-height] bg-slate-800 text-gray-200 flex items-center justify-center`}
>
{isPending && (
<OsdsSpinner inline contrasted data-testid="logTail-spinner" />
)}
{error && (
<ApiError
testId="logTail-error"
error={error}
onRetry={() =>
queryClient.refetchQueries({
queryKey: getLogTailUrlQueryKey({
logKind: currentLogKind.name,
logTailUrl: logApiUrls.logUrl,
}),
})
}
/>
)}
</div>
);
}

return (
<div className={`h-[--tail-height] bg-slate-800 text-slate-300`}>
{<LogMessages logTailMessageUrl={data.url} />}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, it, vi } from 'vitest';
import { renderTest } from '../../test-utils';
import { logMessagesMock } from '../../data/mocks/logMessage.mock';

const IntersectionObserverMock = vi.fn(() => ({
disconnect: vi.fn(),
observe: vi.fn(),
takeRecords: vi.fn(),
unobserve: vi.fn(),
}));

vi.stubGlobal('IntersectionObserver', IntersectionObserverMock);

const getDOMRect = (width: number, height: number) => ({
width,
height,
top: 0,
left: 0,
bottom: 0,
right: 0,
x: 0,
y: 0,
toJSON: () => {},
});

beforeEach(() => {
Element.prototype.getBoundingClientRect = vi.fn(function test() {
if (this.getAttribute('data-testid') === 'logTail-listContainer') {
return getDOMRect(1000, 800);
}
return getDOMRect(500, 20);
});
});

afterEach(() => {
Element.prototype.getBoundingClientRect = vi.fn(function test() {
return getDOMRect(0, 0);
});
});

describe('LogTail test suite', () => {
it('should display an error if /log/url api is KO', async () => {
await renderTest({ isLogTailUrlKO: true });

await waitFor(
() => expect(screen.getByTestId('logTail-error')).toBeVisible(),
{
timeout: 10_000,
},
);
});

it('should render a loading state when the api request is pending', async () => {
await renderTest();

await waitFor(
() => expect(screen.getByTestId('logTail-spinner')).toBeVisible(),
{
timeout: 10_000,
},
);

await waitFor(
() => expect(screen.getByTestId('logTail-searchInput')).toBeVisible(),
{
timeout: 10_000,
},
);
});

it('should render LogMessage component', async () => {
await renderTest();

await waitFor(
() => expect(screen.getByTestId('logTail-searchInput')).toBeVisible(),
{
timeout: 10_000,
},
);
expect(screen.getByTestId('logTail-togglePolling')).toBeVisible();
expect(screen.getByTestId('logTail-clearSession')).toBeVisible();
});

it('should display messages', async () => {
await renderTest();

await waitFor(
() => expect(screen.getByText(logMessagesMock[0].message)).toBeVisible(),
{
timeout: 10_000,
},
);

expect(screen.queryAllByTestId('logTail-item')).toHaveLength(
logMessagesMock.length,
);
});

it('should display waiting message on polling', async () => {
const user = userEvent.setup();
await renderTest();

await waitFor(
() => expect(screen.getByText(logMessagesMock[0].message)).toBeVisible(),
{
timeout: 10_000,
},
);

expect(screen.getByTestId('logTail-polling')).toBeVisible();

await user.click(screen.getByTestId('logTail-togglePolling'));

expect(screen.queryAllByTestId('logTail-polling')).toHaveLength(0);
});

it('should display error message on API error', async () => {
await renderTest({ isLogMessagesKO: true });

await waitFor(
() => expect(screen.getByTestId('logTail-message-error')).toBeVisible(),
{
timeout: 10_000,
},
);
});

it('should clear the list on click on clear session', async () => {
const user = userEvent.setup();
await renderTest();

await waitFor(
() => expect(screen.getByText(logMessagesMock[0].message)).toBeVisible(),
{
timeout: 10_000,
},
);

await user.click(screen.getByTestId('logTail-togglePolling'));
await user.click(screen.getByTestId('logTail-clearSession'));

await waitFor(
() => expect(screen.queryAllByTestId('logTail-item')).toHaveLength(0),
{
timeout: 10_000,
},
);
});
});

This file was deleted.

Loading

0 comments on commit f91ee90

Please sign in to comment.