Repository: provectus/kafka-ui
Issue: UI: connectors , order topics
Labels: type/enhancement
good first issue
scope/frontend
status/accepted


![Screenshot from 2022-04-06 19-15-20(1)](https://user-images.githubusercontent.com/10202690/162033378-f158a003-ec47-4120-93cf-92b010a6c644.png)
### Is your proposal related to a problem?

the current UI of connectors show the topics that consume or produce the connectors , but they are not ordered by name , that mean if I it refresh the order of the topics change

and also two connectors with the same topics can show the topics in a different order

### Describe the solution you'd like

topics by each connector sort by name in the UI

### Describe alternatives you've considered

### Additional context

Thanks :+1: 




Issue comments:
Hey, thanks for the suggestion!

Pull request review comments:
It is a work of selector
sortedTopics is array. no need to use `?`
and here we can put it in a variable and reuse it for array , in order not to call it everytime
moved to redux
fixed
```return connector.topics?.sort((a, b) => a.localeCompare(b))```
As far as I remember, localeCompare is a very slow operation and should only be used for strings containing non-latin characters. 
why we need typing here?
fixed
```diff
- return connector.topics?.length
-         ? [...connector.topics].sort()
-         : connector.topics;
+ return connector.topics?.sort((a, b) => a.localeCompare(b))
```
removed
fixed

Patch that resolved the issue:
diff --git a/kafka-ui-react-app/src/components/Connect/List/ListContainer.ts b/kafka-ui-react-app/src/components/Connect/List/ListContainer.ts
index c3e939a2db2..dbd7a717716 100644
--- a/kafka-ui-react-app/src/components/Connect/List/ListContainer.ts
+++ b/kafka-ui-react-app/src/components/Connect/List/ListContainer.ts
@@ -12,6 +12,7 @@ import {
   getAreConnectorsFetching,
   getConnectorSearch,
   getFailedConnectors,
+  getSortedTopics,
   getFailedTasks,
 } from 'redux/reducers/connect/selectors';
 import List from 'components/Connect/List/List';
@@ -21,6 +22,7 @@ const mapStateToProps = (state: RootState) => ({
   areConnectorsFetching: getAreConnectorsFetching(state),
   connects: getConnects(state),
   failedConnectors: getFailedConnectors(state),
+  sortedTopics: getSortedTopics(state),
   failedTasks: getFailedTasks(state),
   connectors: getConnectors(state),
   search: getConnectorSearch(state),
diff --git a/kafka-ui-react-app/src/components/Connect/List/__tests__/ListItem.spec.tsx b/kafka-ui-react-app/src/components/Connect/List/__tests__/ListItem.spec.tsx
index 0956b9ff697..6ecc8a5f186 100644
--- a/kafka-ui-react-app/src/components/Connect/List/__tests__/ListItem.spec.tsx
+++ b/kafka-ui-react-app/src/components/Connect/List/__tests__/ListItem.spec.tsx
@@ -52,6 +52,14 @@ describe('Connectors ListItem', () => {
     expect(screen.getAllByRole('cell')[6]).toHaveTextContent('2 of 2');
   });
 
+  it('topics tags are sorted', () => {
+    render(setupWrapper());
+    const getLink = screen.getAllByRole('link');
+    expect(getLink[1]).toHaveTextContent('a');
+    expect(getLink[2]).toHaveTextContent('b');
+    expect(getLink[3]).toHaveTextContent('c');
+  });
+
   it('renders item with failed tasks', () => {
     render(
       setupWrapper({
diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/Filters.styled.ts b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/Filters.styled.ts
index 7bb48b37686..8ca07c71a34 100644
--- a/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/Filters.styled.ts
+++ b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/Filters.styled.ts
@@ -125,6 +125,16 @@ export const InfoParagraph = styled.p`
   color: ${({ theme }) => theme.table.td.color.normal};
 `;
 
+export const InfoCodeSample = styled.pre`
+  background: #f5f5f5;
+  padding: 5px;
+  border: 1px solid #e1e1e1;
+  border-radius: 5px;
+  width: fit-content;
+  margin: 5px 20px;
+  color: #cc0f35;
+`;
+
 export const MessageFilterModal = styled.div`
   height: auto;
   width: 560px;
diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/InfoModal.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/InfoModal.tsx
index b86b8776063..91a4e5a4637 100644
--- a/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/InfoModal.tsx
+++ b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/InfoModal.tsx
@@ -26,32 +26,37 @@ const InfoModal: React.FC<InfoModalProps> = ({ toggleIsOpen }) => {
       </S.InfoParagraph>
       <ol aria-label="info-list">
         <S.ListItem>
-          `keyAsText != null && keyAsText ~&quot;([Gg])roovy&quot;` - regex for
-          key as a string
+          <code>keyAsText != null && keyAsText ~&quot;([Gg])roovy&quot;</code> -
+          regex for key as a string
         </S.ListItem>
         <S.ListItem>
-          `value.name == &quot;iS.ListItemax&quot; && value.age &gt; 30` - in
-          case value is json
+          <code>
+            value.name == &quot;iS.ListItemax&quot; && value.age &gt; 30
+          </code>{' '}
+          - in case value is json
         </S.ListItem>
         <S.ListItem>
-          `value == null && valueAsText != null` - search for values that are
-          not nulls and are not json
+          <code>value == null && valueAsText != null</code> - search for values
+          that are not nulls and are not json
         </S.ListItem>
         <S.ListItem>
-          `headers.sentBy == &quot;some system&quot; &&
-          headers[&quot;sentAt&quot;] == &quot;2020-01-01&quot;`
+          <code>
+            headers.sentBy == &quot;some system&quot; &&
+            headers[&quot;sentAt&quot;] == &quot;2020-01-01&quot;
+          </code>
         </S.ListItem>
         <S.ListItem>multiline filters are also allowed:</S.ListItem>
         <S.InfoParagraph>
-          ```
-          <br />
-          def name = value.name
-          <br />
-          def age = value.age
-          <br />
-          name == &quot;iliax&quot; && age == 30
-          <br />
-          ```
+          <S.InfoCodeSample>
+            <code>
+              def name = value.name
+              <br />
+              def age = value.age
+              <br />
+              name == &quot;iliax&quot; && age == 30
+              <br />
+            </code>
+          </S.InfoCodeSample>
         </S.InfoParagraph>
       </ol>
       <S.ButtonContainer>
diff --git a/kafka-ui-react-app/src/components/common/Metrics/Metrics.styled.tsx b/kafka-ui-react-app/src/components/common/Metrics/Metrics.styled.tsx
index 7a3bb44dbe2..9924c5ba13c 100644
--- a/kafka-ui-react-app/src/components/common/Metrics/Metrics.styled.tsx
+++ b/kafka-ui-react-app/src/components/common/Metrics/Metrics.styled.tsx
@@ -21,7 +21,6 @@ export const IndicatorWrapper = styled.div`
   align-items: flex-start;
   padding: 12px 16px;
   box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.08);
-  margin: 0 0 3px 0;
   flex-grow: 1;
 `;
 
@@ -36,10 +35,11 @@ export const IndicatorTitle = styled.div`
 
 export const IndicatorsWrapper = styled.div`
   display: flex;
-  gap: 1px;
+  gap: 2px;
   flex-wrap: wrap;
   border-radius: 8px;
   overflow: auto;
+  box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.08);
 `;
 
 export const SectionTitle = styled.h5`
diff --git a/kafka-ui-react-app/src/redux/reducers/connect/__test__/fixtures.ts b/kafka-ui-react-app/src/redux/reducers/connect/__test__/fixtures.ts
index 2ef1248acfe..5fc7cad55df 100644
--- a/kafka-ui-react-app/src/redux/reducers/connect/__test__/fixtures.ts
+++ b/kafka-ui-react-app/src/redux/reducers/connect/__test__/fixtures.ts
@@ -19,7 +19,7 @@ export const connectorsServerPayload = [
     name: 'hdfs-source-connector',
     connector_class: 'FileStreamSource',
     type: ConnectorType.SOURCE,
-    topics: ['test-topic'],
+    topics: ['a', 'b', 'c'],
     status: {
       state: ConnectorTaskStatus.RUNNING,
       workerId: 1,
@@ -48,7 +48,7 @@ export const connectors: FullConnectorInfo[] = [
     name: 'hdfs-source-connector',
     connectorClass: 'FileStreamSource',
     type: ConnectorType.SOURCE,
-    topics: ['test-topic'],
+    topics: ['a', 'b', 'c'],
     status: {
       state: ConnectorState.RUNNING,
     },
@@ -75,7 +75,7 @@ export const failedConnectors: FullConnectorInfo[] = [
     name: 'hdfs-source-connector',
     connectorClass: 'FileStreamSource',
     type: ConnectorType.SOURCE,
-    topics: ['test-topic'],
+    topics: ['a', 'b', 'c'],
     status: {
       state: ConnectorState.FAILED,
     },
@@ -87,7 +87,7 @@ export const failedConnectors: FullConnectorInfo[] = [
     name: 'hdfs2-source-connector',
     connectorClass: 'FileStreamSource',
     type: ConnectorType.SINK,
-    topics: ['test-topic'],
+    topics: ['a', 'b', 'c'],
     status: {
       state: ConnectorState.FAILED,
     },
diff --git a/kafka-ui-react-app/src/redux/reducers/connect/__test__/selectors.spec.ts b/kafka-ui-react-app/src/redux/reducers/connect/__test__/selectors.spec.ts
index 0ca9e2fdd0b..a1f71626856 100644
--- a/kafka-ui-react-app/src/redux/reducers/connect/__test__/selectors.spec.ts
+++ b/kafka-ui-react-app/src/redux/reducers/connect/__test__/selectors.spec.ts
@@ -83,6 +83,17 @@ describe('Connect selectors', () => {
       expect(selectors.getFailedTasks(store.getState())).toEqual(1);
     });
 
+    it('returns sorted topics', () => {
+      store.dispatch({
+        type: fetchConnectors.fulfilled.type,
+        payload: { connectors },
+      });
+      const sortedTopics = selectors.getSortedTopics(store.getState());
+      if (sortedTopics[0] && sortedTopics[0].length > 1) {
+        expect(sortedTopics[0]).toEqual(['a', 'b', 'c']);
+      }
+    });
+
     it('returns connector', () => {
       store.dispatch({
         type: fetchConnector.fulfilled.type,
diff --git a/kafka-ui-react-app/src/redux/reducers/connect/selectors.ts b/kafka-ui-react-app/src/redux/reducers/connect/selectors.ts
index 2e029a7f9e9..c11f7ac2e80 100644
--- a/kafka-ui-react-app/src/redux/reducers/connect/selectors.ts
+++ b/kafka-ui-react-app/src/redux/reducers/connect/selectors.ts
@@ -6,6 +6,7 @@ import {
   ConnectorState,
   FullConnectorInfo,
 } from 'generated-sources';
+import { sortBy } from 'lodash';
 
 import {
   deleteConnector,
@@ -63,6 +64,10 @@ export const getFailedTasks = createSelector(connectState, ({ connectors }) => {
     .reduce((acc: number, value: number) => acc + value, 0);
 });
 
+export const getSortedTopics = createSelector(connectState, ({ connectors }) =>
+  connectors.map(({ topics }) => sortBy(topics || []))
+);
+
 const getConnectorFetchingStatus = createFetchingSelector(
   fetchConnector.typePrefix
 );


Test patch: