Explorar o código

Issues/1265 (#1303)

* Make all labels Title Case in popup menus.

* Increased length of the sorting box to make the title readable like for 'Latest first' there.

* Disabled 0 and negative values in Topic Creation redesign.

* Changed consumer, connect labels status colors.

* Added test for consumer-list

* Update kafka-ui-react-app/src/components/Brokers/Brokers.tsx

Co-authored-by: Oleg Shur <workshur@gmail.com>

* Fixed warnings.

* Added test for customer group list.

* Added an interface to the object.

Co-authored-by: Oleg Shur <workshur@gmail.com>
NelyDavtyan %!s(int64=3) %!d(string=hai) anos
pai
achega
5a487e437d

+ 9 - 1
kafka-ui-react-app/src/components/Brokers/Brokers.tsx

@@ -59,7 +59,15 @@ const Brokers: React.FC = () => {
           <Metrics.Indicator label="Version">{version}</Metrics.Indicator>
         </Metrics.Section>
         <Metrics.Section title="Partitions">
-          <Metrics.Indicator label="Online" isAlert>
+          <Metrics.Indicator
+            label="Online"
+            isAlert
+            alertType={
+              offlinePartitionCount && offlinePartitionCount > 0
+                ? 'error'
+                : 'success'
+            }
+          >
             {offlinePartitionCount && offlinePartitionCount > 0 ? (
               <Metrics.RedText>{onlinePartitionCount}</Metrics.RedText>
             ) : (

+ 16 - 7
kafka-ui-react-app/src/components/Connect/Details/Overview/__tests__/__snapshots__/Overview.spec.tsx.snap

@@ -96,6 +96,17 @@ exports[`Overview view matches snapshot 1`] = `
   border-bottom-right-radius: 8px;
 }
 
+.c6 {
+  grid-area: status;
+  fill: none;
+  width: 4px;
+  height: 4px;
+}
+
+.c7 {
+  fill: #E51A1A;
+}
+
 @media screen and (max-width:1023px) {
   .c1 > .c2:first-child,
   .c1 > .c2:last-child {
@@ -199,17 +210,15 @@ exports[`Overview view matches snapshot 1`] = `
             Tasks Failed
              
             <svg
-              fill="none"
-              height="4"
+              className="c6"
               viewBox="0 0 4 4"
-              width="4"
               xmlns="http://www.w3.org/2000/svg"
             >
               <circle
-                cx="2"
-                cy="2"
-                fill="#E61A1A"
-                r="2"
+                className="c7"
+                cx={2}
+                cy={2}
+                r={2}
               />
             </svg>
           </div>

+ 18 - 2
kafka-ui-react-app/src/components/Connect/List/ListItem.tsx

@@ -1,5 +1,5 @@
 import React from 'react';
-import { FullConnectorInfo } from 'generated-sources';
+import { ConnectorState, FullConnectorInfo } from 'generated-sources';
 import { clusterConnectConnectorPath, clusterTopicPath } from 'lib/paths';
 import { ClusterName } from 'redux/interfaces';
 import { Link, NavLink } from 'react-router-dom';
@@ -56,6 +56,20 @@ const ListItem: React.FC<ListItemProps> = ({
     return tasksCount - (failedTasksCount || 0);
   }, [tasksCount, failedTasksCount]);
 
+  const stateColor = React.useMemo(() => {
+    const { state = '' } = status;
+
+    switch (state) {
+      case ConnectorState.RUNNING:
+        return 'green';
+      case ConnectorState.FAILED:
+      case ConnectorState.TASK_FAILED:
+        return 'red';
+      default:
+        return 'yellow';
+    }
+  }, [status]);
+
   return (
     <tr>
       <TableKeyLink>
@@ -78,7 +92,9 @@ const ListItem: React.FC<ListItemProps> = ({
           ))}
         </TopicTagsWrapper>
       </td>
-      <td>{status && <TagStyled color="yellow">{status.state}</TagStyled>}</td>
+      <td>
+        {status && <TagStyled color={stateColor}>{status.state}</TagStyled>}
+      </td>
       <td>
         {runningTasks && (
           <span>

+ 13 - 3
kafka-ui-react-app/src/components/Connect/List/__tests__/__snapshots__/ListItem.spec.tsx.snap

@@ -51,7 +51,7 @@ exports[`Connectors ListItem matches snapshot 1`] = `
   border-radius: 16px;
   height: 20px;
   line-height: 20px;
-  background-color: #FFEECC;
+  background-color: #D6F5E0;
   color: #171A1C;
   font-size: 12px;
   display: inline-block;
@@ -125,6 +125,14 @@ exports[`Connectors ListItem matches snapshot 1`] = `
           },
         },
       },
+      "circularAlert": Object {
+        "color": Object {
+          "error": "#E51A1A",
+          "info": "#E3E6E8",
+          "success": "#5CD685",
+          "warning": "#FFEECC",
+        },
+      },
       "layout": Object {
         "minWidth": "1200px",
         "navBarHeight": "3.25rem",
@@ -220,6 +228,8 @@ exports[`Connectors ListItem matches snapshot 1`] = `
         "backgroundColor": Object {
           "gray": "#E3E6E8",
           "green": "#D6F5E0",
+          "red": "#FAD1D1",
+          "white": "#E3E6E8",
           "yellow": "#FFEECC",
         },
         "color": "#171A1C",
@@ -378,11 +388,11 @@ exports[`Connectors ListItem matches snapshot 1`] = `
                 </td>
                 <td>
                   <Styled(Tag)
-                    color="yellow"
+                    color="green"
                   >
                     <Tag
                       className="c3"
-                      color="yellow"
+                      color="green"
                     >
                       <p
                         className="c3"

+ 17 - 2
kafka-ui-react-app/src/components/ConsumerGroups/List/ListItem.tsx

@@ -1,12 +1,27 @@
 import React from 'react';
 import { Link } from 'react-router-dom';
-import { ConsumerGroup } from 'generated-sources';
+import { ConsumerGroup, ConsumerGroupState } from 'generated-sources';
 import TagStyled from 'components/common/Tag/Tag.styled';
 import { TableKeyLink } from 'components/common/table/Table/TableKeyLink.styled';
 
 const ListItem: React.FC<{ consumerGroup: ConsumerGroup }> = ({
   consumerGroup,
 }) => {
+  const stateColor = React.useMemo(() => {
+    const { state = '' } = consumerGroup;
+
+    switch (state) {
+      case ConsumerGroupState.STABLE:
+        return 'green';
+      case ConsumerGroupState.DEAD:
+        return 'red';
+      case ConsumerGroupState.EMPTY:
+        return 'white';
+      default:
+        return 'yellow';
+    }
+  }, [consumerGroup]);
+
   return (
     <tr>
       <TableKeyLink>
@@ -19,7 +34,7 @@ const ListItem: React.FC<{ consumerGroup: ConsumerGroup }> = ({
       <td>{consumerGroup.messagesBehind}</td>
       <td>{consumerGroup.coordinator?.id}</td>
       <td>
-        <TagStyled color="yellow">{consumerGroup.state}</TagStyled>
+        <TagStyled color={stateColor}>{consumerGroup.state}</TagStyled>
       </td>
     </tr>
   );

+ 57 - 0
kafka-ui-react-app/src/components/ConsumerGroups/List/__test__/ListItem.spec.tsx

@@ -4,6 +4,7 @@ import ListItem from 'components/ConsumerGroups/List/ListItem';
 import { ThemeProvider } from 'styled-components';
 import theme from 'theme/theme';
 import { StaticRouter } from 'react-router';
+import { ConsumerGroupState, ConsumerGroup } from 'generated-sources';
 
 describe('List', () => {
   const mockConsumerGroup = {
@@ -40,7 +41,63 @@ describe('List', () => {
     </StaticRouter>
   );
 
+  const setupWrapper = (consumerGroup: ConsumerGroup) => (
+    <StaticRouter>
+      <ThemeProvider theme={theme}>
+        <table>
+          <tbody>
+            <ListItem consumerGroup={consumerGroup} />
+          </tbody>
+        </table>
+      </ThemeProvider>
+    </StaticRouter>
+  );
+
   it('render empty ListItem', () => {
     expect(component.exists('tr')).toBeTruthy();
   });
+
+  it('renders item with stable status', () => {
+    const wrapper = mount(
+      setupWrapper({
+        ...mockConsumerGroup,
+        state: ConsumerGroupState.STABLE,
+      })
+    );
+
+    expect(wrapper.find('td').at(5).text()).toBe(ConsumerGroupState.STABLE);
+  });
+
+  it('renders item with dead status', () => {
+    const wrapper = mount(
+      setupWrapper({
+        ...mockConsumerGroup,
+        state: ConsumerGroupState.DEAD,
+      })
+    );
+
+    expect(wrapper.find('td').at(5).text()).toBe(ConsumerGroupState.DEAD);
+  });
+
+  it('renders item with empty status', () => {
+    const wrapper = mount(
+      setupWrapper({
+        ...mockConsumerGroup,
+        state: ConsumerGroupState.EMPTY,
+      })
+    );
+
+    expect(wrapper.find('td').at(5).text()).toBe(ConsumerGroupState.EMPTY);
+  });
+
+  it('renders item with empty-string status', () => {
+    const wrapper = mount(
+      setupWrapper({
+        ...mockConsumerGroup,
+        state: ConsumerGroupState.UNKNOWN,
+      })
+    );
+
+    expect(wrapper.find('td').at(5).text()).toBe(ConsumerGroupState.UNKNOWN);
+  });
 });

+ 10 - 0
kafka-ui-react-app/src/components/Schemas/New/__test__/__snapshots__/New.spec.tsx.snap

@@ -364,6 +364,14 @@ exports[`New View matches snapshot 1`] = `
           },
         },
       },
+      "circularAlert": Object {
+        "color": Object {
+          "error": "#E51A1A",
+          "info": "#E3E6E8",
+          "success": "#5CD685",
+          "warning": "#FFEECC",
+        },
+      },
       "layout": Object {
         "minWidth": "1200px",
         "navBarHeight": "3.25rem",
@@ -459,6 +467,8 @@ exports[`New View matches snapshot 1`] = `
         "backgroundColor": Object {
           "gray": "#E3E6E8",
           "green": "#D6F5E0",
+          "red": "#FAD1D1",
+          "white": "#E3E6E8",
           "yellow": "#FFEECC",
         },
         "color": "#171A1C",

+ 1 - 0
kafka-ui-react-app/src/components/Topics/New/New.tsx

@@ -21,6 +21,7 @@ interface RouterParams {
 
 const New: React.FC = () => {
   const methods = useForm<TopicFormData>({
+    mode: 'onTouched',
     resolver: yupResolver(topicFormValidationSchema),
   });
   const { clusterName } = useParams<RouterParams>();

+ 5 - 1
kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/Overview.tsx

@@ -55,7 +55,11 @@ const Overview: React.FC<Props> = ({
           >
             <Metrics.RedText>{underReplicatedPartitions}</Metrics.RedText>
           </Metrics.Indicator>
-          <Metrics.Indicator label="In Sync Replicas" isAlert>
+          <Metrics.Indicator
+            label="In Sync Replicas"
+            isAlert
+            alertType={inSyncReplicas === replicas ? 'success' : 'error'}
+          >
             {inSyncReplicas && replicas && inSyncReplicas < replicas ? (
               <Metrics.RedText>{inSyncReplicas}</Metrics.RedText>
             ) : (

+ 10 - 0
kafka-ui-react-app/src/components/Topics/Topic/Details/__test__/__snapshots__/Details.spec.tsx.snap

@@ -136,6 +136,14 @@ exports[`Details when it has readonly flag does not render the Action button a T
           },
         },
       },
+      "circularAlert": Object {
+        "color": Object {
+          "error": "#E51A1A",
+          "info": "#E3E6E8",
+          "success": "#5CD685",
+          "warning": "#FFEECC",
+        },
+      },
       "layout": Object {
         "minWidth": "1200px",
         "navBarHeight": "3.25rem",
@@ -231,6 +239,8 @@ exports[`Details when it has readonly flag does not render the Action button a T
         "backgroundColor": Object {
           "gray": "#E3E6E8",
           "green": "#D6F5E0",
+          "red": "#FAD1D1",
+          "white": "#E3E6E8",
           "yellow": "#FFEECC",
         },
         "color": "#171A1C",

+ 4 - 0
kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.tsx

@@ -70,6 +70,7 @@ const TopicForm: React.FC<Props> = ({
                 <Input
                   type="number"
                   placeholder="Number of partitions"
+                  min="1"
                   defaultValue="1"
                   name="partitions"
                   inputSize="M"
@@ -89,6 +90,7 @@ const TopicForm: React.FC<Props> = ({
               <Input
                 type="number"
                 placeholder="Replication Factor"
+                min="1"
                 defaultValue="1"
                 name="replicationFactor"
                 inputSize="M"
@@ -104,6 +106,7 @@ const TopicForm: React.FC<Props> = ({
             <Input
               type="number"
               placeholder="Min In Sync Replicas"
+              min="1"
               defaultValue="1"
               name="minInsyncReplicas"
               inputSize="M"
@@ -149,6 +152,7 @@ const TopicForm: React.FC<Props> = ({
           <InputLabel>Maximum message size in bytes *</InputLabel>
           <Input
             type="number"
+            min="1"
             defaultValue="1000012"
             name="maxMessageBytes"
             inputSize="M"

+ 6 - 9
kafka-ui-react-app/src/components/common/Metrics/Indicator.tsx

@@ -1,4 +1,5 @@
 import React from 'react';
+import { AlertType } from 'redux/interfaces';
 
 import * as S from './Metrics.styled';
 
@@ -7,6 +8,7 @@ interface Props {
   isAlert?: boolean;
   label: React.ReactNode;
   title?: string;
+  alertType?: AlertType;
 }
 
 const Indicator: React.FC<Props> = ({
@@ -14,6 +16,7 @@ const Indicator: React.FC<Props> = ({
   title,
   fetching,
   isAlert,
+  alertType = 'error',
   children,
 }) => {
   return (
@@ -22,15 +25,9 @@ const Indicator: React.FC<Props> = ({
         <S.IndicatorTitle>
           {label}{' '}
           {isAlert && (
-            <svg
-              width="4"
-              height="4"
-              viewBox="0 0 4 4"
-              fill="none"
-              xmlns="http://www.w3.org/2000/svg"
-            >
-              <circle cx="2" cy="2" r="2" fill="#E61A1A" />
-            </svg>
+            <S.CircularAlertWrapper>
+              <S.CircularAlert $type={alertType} />
+            </S.CircularAlertWrapper>
           )}
         </S.IndicatorTitle>
         <span>

+ 21 - 1
kafka-ui-react-app/src/components/common/Metrics/Metrics.styled.tsx

@@ -1,4 +1,5 @@
-import styled from 'styled-components';
+import styled, { css } from 'styled-components';
+import { AlertType } from 'redux/interfaces';
 
 export const Wrapper = styled.div`
   padding: 1.5rem 1rem;
@@ -43,6 +44,7 @@ export const IndicatorsWrapper = styled.div`
       border-top-left-radius: 8px;
       border-bottom-left-radius: 8px;
     }
+
     &:last-child {
       border-top-right-radius: 8px;
       border-bottom-right-radius: 8px;
@@ -74,3 +76,21 @@ export const RedText = styled.span`
   color: ${({ theme }) => theme.metrics.indicator.warningTextColor};
   font-size: 14px;
 `;
+
+export const CircularAlertWrapper = styled.svg.attrs({
+  viewBox: '0 0 4 4',
+  xmlns: 'http://www.w3.org/2000/svg',
+})`
+  grid-area: status;
+  fill: none;
+  width: 4px;
+  height: 4px;
+`;
+
+export const CircularAlert = styled.circle.attrs({ cx: 2, cy: 2, r: 2 })<{
+  $type: AlertType;
+}>(
+  ({ theme, $type }) => css`
+    fill: ${theme.circularAlert.color[$type]};
+  `
+);

+ 1 - 1
kafka-ui-react-app/src/components/common/Tag/Tag.styled.tsx

@@ -3,7 +3,7 @@ import React from 'react';
 
 interface Props {
   className?: string;
-  color: 'green' | 'gray' | 'yellow';
+  color: 'green' | 'gray' | 'yellow' | 'red' | 'white';
 }
 
 const Tag: React.FC<Props> = ({ className, children }) => {

+ 4 - 4
kafka-ui-react-app/src/lib/yupExtended.ts

@@ -50,13 +50,13 @@ export const topicFormValidationSchema = yup.object().shape({
       TOPIC_NAME_VALIDATION_PATTERN,
       'Only alphanumeric, _, -, and . allowed'
     ),
-  partitions: yup.number().required(),
-  replicationFactor: yup.number().required(),
-  minInsyncReplicas: yup.number().required(),
+  partitions: yup.number().min(1).required(),
+  replicationFactor: yup.number().min(1).required(),
+  minInsyncReplicas: yup.number().min(1).required(),
   cleanupPolicy: yup.string().required(),
   retentionMs: yup.number().min(-1, 'Must be greater than or equal to -1'),
   retentionBytes: yup.number(),
-  maxMessageBytes: yup.number().required(),
+  maxMessageBytes: yup.number().min(1).required(),
   customParams: yup.array().of(
     yup.object().shape({
       name: yup.string().required(),

+ 10 - 0
kafka-ui-react-app/src/theme/theme.ts

@@ -50,6 +50,14 @@ const theme = {
       info: Colors.neutral[10],
     },
   },
+  circularAlert: {
+    color: {
+      error: Colors.red[50],
+      success: Colors.green[40],
+      warning: Colors.yellow[10],
+      info: Colors.neutral[10],
+    },
+  },
   buttonStyles: {
     primary: {
       backgroundColor: {
@@ -159,6 +167,8 @@ const theme = {
       green: Colors.green[10],
       gray: Colors.neutral[10],
       yellow: Colors.yellow[10],
+      white: Colors.neutral[10],
+      red: Colors.red[10],
     },
     color: Colors.neutral[90],
   },