Repository: provectus/kafka-ui
Issue: Topic analysis improvements
Labels: type/enhancement
good first issue
scope/frontend
status/accepted
status/design-review

<img width="351" alt="image" src="https://user-images.githubusercontent.com/1494347/193237442-2e91ab1c-cc49-40e1-b9de-935d381f08a0.png">

- Display percentage in %

- Display time passed since the start
- Split "scanned messages" to two lines: "scanned messages" and "scanned bytes"

Issue comments:
@Haarolean it looks almost according the way you suggested. However the overall progress percentage and progress bar look a bit strange now:
![image](https://user-images.githubusercontent.com/112083452/200502621-64bee7cb-3947-4afa-9753-b741b14716e3.png)

Suggestion: Show the percentage above the progress bar, using a bigger font (most important info in the block, while the rest is additional info).
And the progress bar width to be matched to block width and it's alignment to the center. So it would be more consistent.

Should it be created as a separate improvement task or fixed here while we are at it anyway?

CC @armenuikafka 
![pct_statistics](https://github.com/provectus/kafka-ui/assets/894746/059ec5eb-c6ca-4d87-9f9e-79e95d1f3b8e)
Were you looking for something like this? @BulatKha 
I can submit a PR, can you assign this to me? @Haarolean 
@pradyumnad that's better, yes
@Haarolean PR submitted https://github.com/provectus/kafka-ui/pull/4123

@Haarolean @pradyumnad just a minor comment to fix the alignment of the progress bar, as it looks a bit sideways. Centered and stretched from the left edge of the text to the right border of the second column would look better.
Looks fine otherwise!
@BulatKha yes, I was just fiddling with Pixelmator to kind of get the idea. The code was done to fix it.

Pull request review comments:
> Display time passed since the start

I see that you show time passed since component mount not since analyzing start
pls remove it
pls add tests for `calculateTimer` 
what are you testing here?
should be wrapped to `it`

Patch that resolved the issue:
diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Statistics/Metrics.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Statistics/Metrics.tsx
index 34feb01b72e..40abb14d705 100644
--- a/kafka-ui-react-app/src/components/Topics/Topic/Statistics/Metrics.tsx
+++ b/kafka-ui-react-app/src/components/Topics/Topic/Statistics/Metrics.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useEffect, useState } from 'react';
 import {
   useAnalyzeTopic,
   useCancelTopicAnalysis,
@@ -15,6 +15,7 @@ import {
 } from 'components/common/PropertiesList/PropertiesList.styled';
 import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';
 import { useTimeFormat } from 'lib/hooks/useTimeFormat';
+import { calculateTimer } from 'lib/dateTimeHelpers';
 
 import * as S from './Statistics.styles';
 import Total from './Indicators/Total';
@@ -25,13 +26,14 @@ const Metrics: React.FC = () => {
   const formatTimestamp = useTimeFormat();
 
   const params = useAppParams<RouteParamsClusterTopic>();
-  const [isAnalyzing, setIsAnalyzing] = React.useState(true);
+
+  const [isAnalyzing, setIsAnalyzing] = useState(true);
   const analyzeTopic = useAnalyzeTopic(params);
   const cancelTopicAnalysis = useCancelTopicAnalysis(params);
 
   const { data } = useTopicAnalysis(params, isAnalyzing);
 
-  React.useEffect(() => {
+  useEffect(() => {
     if (data && !data.progress) {
       setIsAnalyzing(false);
     }
@@ -44,7 +46,10 @@ const Metrics: React.FC = () => {
   if (data.progress) {
     return (
       <S.ProgressContainer>
-        <ProgressBar completed={data.progress.completenessPercent || 0} />
+        <S.ProgressBarWrapper>
+          <ProgressBar completed={data.progress.completenessPercent || 0} />
+          <span> {Math.floor(data.progress.completenessPercent || 0)} %</span>
+        </S.ProgressBarWrapper>
         <Button
           onClick={async () => {
             await cancelTopicAnalysis.mutateAsync();
@@ -58,9 +63,12 @@ const Metrics: React.FC = () => {
         <List>
           <Label>Started at</Label>
           <span>{formatTimestamp(data.progress.startedAt, 'hh:mm:ss a')}</span>
+          <Label>Passed since start</Label>
+          <span>{calculateTimer(data.progress.startedAt as number)}</span>
           <Label>Scanned messages</Label>
+          <span>{data.progress.msgsScanned}</span>
+          <Label>Scanned size</Label>
           <span>
-            {data.progress.msgsScanned} /{' '}
             <BytesFormatted value={data.progress.bytesScanned} />
           </span>
         </List>
@@ -90,7 +98,6 @@ const Metrics: React.FC = () => {
           Restart Analysis
         </Button>
       </S.ActionsBar>
-
       <Informers.Wrapper>
         <Total {...totalStats} />
         {totalStats.keySize && (
diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Statistics/Statistics.styles.ts b/kafka-ui-react-app/src/components/Topics/Topic/Statistics/Statistics.styles.ts
index 472562eef9d..06a4ae9c9d8 100644
--- a/kafka-ui-react-app/src/components/Topics/Topic/Statistics/Statistics.styles.ts
+++ b/kafka-ui-react-app/src/components/Topics/Topic/Statistics/Statistics.styles.ts
@@ -1,7 +1,4 @@
-import {
-  Label,
-  List,
-} from 'components/common/PropertiesList/PropertiesList.styled';
+import { List } from 'components/common/PropertiesList/PropertiesList.styled';
 import styled from 'styled-components';
 
 export const ProgressContainer = styled.div`
@@ -16,10 +13,6 @@ export const ProgressContainer = styled.div`
 
   ${List} {
     opacity: 0.5;
-
-    ${Label} {
-      text-align: right;
-    }
   }
 `;
 
@@ -42,3 +35,10 @@ export const PartitionInfo = styled.div`
   grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
   column-gap: 24px;
 `;
+
+export const ProgressBarWrapper = styled.div`
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  width: 280px;
+`;
diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Statistics/__test__/Metrics.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Statistics/__test__/Metrics.spec.tsx
index 2f27802a002..299a53f4a7b 100644
--- a/kafka-ui-react-app/src/components/Topics/Topic/Statistics/__test__/Metrics.spec.tsx
+++ b/kafka-ui-react-app/src/components/Topics/Topic/Statistics/__test__/Metrics.spec.tsx
@@ -34,6 +34,7 @@ describe('Metrics', () => {
 
   describe('when analysis is in progress', () => {
     const cancelMock = jest.fn();
+
     beforeEach(() => {
       (useCancelTopicAnalysis as jest.Mock).mockImplementation(() => ({
         mutateAsync: cancelMock,
@@ -50,10 +51,10 @@ describe('Metrics', () => {
       renderComponent();
     });
 
-    it('renders Stop Analysis button', () => {
+    it('renders Stop Analysis button', async () => {
       const btn = screen.getByRole('button', { name: 'Stop Analysis' });
       expect(btn).toBeInTheDocument();
-      userEvent.click(btn);
+      await userEvent.click(btn);
       expect(cancelMock).toHaveBeenCalled();
     });
 
@@ -62,6 +63,10 @@ describe('Metrics', () => {
       expect(progressbar).toBeInTheDocument();
       expect(progressbar).toHaveStyleRule('width', '0%');
     });
+
+    it('calculate Timer ', () => {
+      expect(screen.getByText('Passed since start')).toBeInTheDocument();
+    });
   });
 
   describe('when analysis is completed', () => {
diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Statistics/__test__/Statistics.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Statistics/__test__/Statistics.spec.tsx
index d69c68b8edb..ef034b4d83a 100644
--- a/kafka-ui-react-app/src/components/Topics/Topic/Statistics/__test__/Statistics.spec.tsx
+++ b/kafka-ui-react-app/src/components/Topics/Topic/Statistics/__test__/Statistics.spec.tsx
@@ -1,9 +1,10 @@
 import React from 'react';
-import { screen } from '@testing-library/react';
+import { screen, waitFor } from '@testing-library/react';
 import { render, WithRoute } from 'lib/testHelpers';
 import Statistics from 'components/Topics/Topic/Statistics/Statistics';
 import { clusterTopicStatisticsPath } from 'lib/paths';
-import { useTopicAnalysis } from 'lib/hooks/api/topics';
+import { useTopicAnalysis, useAnalyzeTopic } from 'lib/hooks/api/topics';
+import userEvent from '@testing-library/user-event';
 
 const clusterName = 'local';
 const topicName = 'topic';
@@ -11,6 +12,7 @@ const topicName = 'topic';
 jest.mock('lib/hooks/api/topics', () => ({
   ...jest.requireActual('lib/hooks/api/topics'),
   useTopicAnalysis: jest.fn(),
+  useAnalyzeTopic: jest.fn(),
 }));
 
 describe('Statistics', () => {
@@ -23,13 +25,24 @@ describe('Statistics', () => {
       { initialEntries: [path] }
     );
   };
-
-  it('renders Metricks component', () => {
+  const startMock = jest.fn();
+  it('renders Metricks component', async () => {
     (useTopicAnalysis as jest.Mock).mockImplementation(() => ({
       data: { result: 1 },
     }));
 
     renderComponent();
-    expect(screen.getByText('Restart Analysis')).toBeInTheDocument();
+    await expect(screen.getByText('Restart Analysis')).toBeInTheDocument();
+    expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
+  });
+  it('renders Start Analysis button', async () => {
+    (useAnalyzeTopic as jest.Mock).mockImplementation(() => ({
+      mutateAsync: startMock,
+    }));
+    renderComponent();
+    const btn = screen.getByRole('button', { name: 'Start Analysis' });
+    expect(btn).toBeInTheDocument();
+    await waitFor(() => userEvent.click(btn));
+    expect(startMock).toHaveBeenCalled();
   });
 });
diff --git a/kafka-ui-react-app/src/lib/__test__/dateTimeHelpers.spec.ts b/kafka-ui-react-app/src/lib/__test__/dateTimeHelpers.spec.ts
new file mode 100644
index 00000000000..c34371452b6
--- /dev/null
+++ b/kafka-ui-react-app/src/lib/__test__/dateTimeHelpers.spec.ts
@@ -0,0 +1,51 @@
+import {
+  passedTime,
+  calculateTimer,
+  formatMilliseconds,
+} from 'lib/dateTimeHelpers';
+
+const startedAt = 1664891890889;
+
+describe('format Milliseconds', () => {
+  it('hours > 0', () => {
+    const result = formatMilliseconds(10000000);
+
+    expect(result).toEqual('2h 46m');
+  });
+  it('minutes > 0', () => {
+    const result = formatMilliseconds(1000000);
+
+    expect(result).toEqual('16m 40s');
+  });
+
+  it('seconds > 0', () => {
+    const result = formatMilliseconds(10000);
+
+    expect(result).toEqual('10s');
+  });
+
+  it('milliseconds > 0', () => {
+    const result = formatMilliseconds(100);
+
+    expect(result).toEqual('100ms' || '0ms');
+    expect(formatMilliseconds()).toEqual('0ms');
+  });
+});
+
+describe('calculate timer', () => {
+  it('time value < 10', () => {
+    expect(passedTime(5)).toBeTruthy();
+  });
+
+  it('time value > 9', () => {
+    expect(passedTime(10)).toBeTruthy();
+  });
+
+  it('run calculate time', () => {
+    expect(calculateTimer(startedAt));
+  });
+
+  it('return when startedAt > new Date()', () => {
+    expect(calculateTimer(1664891890889199)).toBe('00:00');
+  });
+});
diff --git a/kafka-ui-react-app/src/lib/dateTimeHelpers.ts b/kafka-ui-react-app/src/lib/dateTimeHelpers.ts
index 57a8834ad3a..5c72af7bc47 100644
--- a/kafka-ui-react-app/src/lib/dateTimeHelpers.ts
+++ b/kafka-ui-react-app/src/lib/dateTimeHelpers.ts
@@ -32,3 +32,14 @@ export const formatMilliseconds = (input = 0) => {
 
   return `${milliseconds}ms`;
 };
+
+export const passedTime = (value: number) => (value < 10 ? `0${value}` : value);
+
+export const calculateTimer = (startedAt: number) => {
+  const now = new Date().getTime();
+  const newDate = now - startedAt;
+  const minutes = dayjs(newDate).minute();
+  const second = dayjs(newDate).second();
+
+  return newDate > 0 ? `${passedTime(minutes)}:${passedTime(second)}` : '00:00';
+};


Test patch: