diff --git a/kafka-ui-api/pom.xml b/kafka-ui-api/pom.xml index 7f2d4c16be..5ebefe31df 100644 --- a/kafka-ui-api/pom.xml +++ b/kafka-ui-api/pom.xml @@ -12,7 +12,7 @@ kafka-ui-api - 0.8.8 + 0.8.10 jacoco reuseReports ${project.basedir}/target/jacoco.exec diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/PartitionDistributionStats.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/PartitionDistributionStats.java index b625533d1d..46efc67000 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/PartitionDistributionStats.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/PartitionDistributionStats.java @@ -1,7 +1,7 @@ package com.provectus.kafka.ui.model; import java.math.BigDecimal; -import java.math.MathContext; +import java.math.RoundingMode; import java.util.HashMap; import java.util.Map; import javax.annotation.Nullable; @@ -21,8 +21,6 @@ public class PartitionDistributionStats { // avg skew will show unuseful results on low number of partitions private static final int MIN_PARTITIONS_FOR_SKEW_CALCULATION = 50; - private static final MathContext ROUNDING_MATH_CTX = new MathContext(3); - private final Map partitionLeaders; private final Map partitionsCount; private final Map inSyncPartitions; @@ -88,6 +86,7 @@ public class PartitionDistributionStats { return null; } value = value == null ? 0 : value; - return new BigDecimal((value - avgValue) / avgValue * 100.0).round(ROUNDING_MATH_CTX); + return new BigDecimal((value - avgValue) / avgValue * 100.0) + .setScale(1, RoundingMode.HALF_UP); } } diff --git a/kafka-ui-e2e-checks/pom.xml b/kafka-ui-e2e-checks/pom.xml index c93f6bcabb..cfd1414fd4 100644 --- a/kafka-ui-e2e-checks/pom.xml +++ b/kafka-ui-e2e-checks/pom.xml @@ -18,7 +18,7 @@ 5.2.1 4.8.1 6.12.3 - 7.7.0 + 7.7.1 2.21.0 3.0.4 1.9.9.1 diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersList.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersList.java index 50ecdff359..9e81a0795c 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersList.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersList.java @@ -48,7 +48,8 @@ public class BrokersList extends BasePage { } private List getEnabledColumnHeaders() { - return Stream.of("Broker ID", "Segment Size", "Segment Count", "Port", "Host") + return Stream.of("Broker ID", "Disk usage", "Partitions skew", + "Leaders", "Leader skew", "Online partitions", "Port", "Host") .map(name -> $x(String.format(columnHeaderLocator, name))) .collect(Collectors.toList()); } diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/KsqlQueryForm.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/KsqlQueryForm.java index 4ce282b6cc..ff57de39b2 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/KsqlQueryForm.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/KsqlQueryForm.java @@ -17,11 +17,12 @@ import java.util.List; public class KsqlQueryForm extends BasePage { protected SelenideElement clearBtn = $x("//div/button[text()='Clear']"); protected SelenideElement executeBtn = $x("//div/button[text()='Execute']"); - protected SelenideElement stopQueryBtn = $x("//div/button[text()='Stop query']"); protected SelenideElement clearResultsBtn = $x("//div/button[text()='Clear results']"); protected SelenideElement addStreamPropertyBtn = $x("//button[text()='Add Stream Property']"); protected SelenideElement queryAreaValue = $x("//div[@class='ace_content']"); protected SelenideElement queryArea = $x("//div[@id='ksql']/textarea[@class='ace_text-input']"); + protected SelenideElement abortButton = $x("//div[@role='status']/div[text()='Abort']"); + protected SelenideElement cancelledAlert = $x("//div[@role='status'][text()='Cancelled']"); protected ElementsCollection ksqlGridItems = $$x("//tbody//tr"); protected ElementsCollection keyField = $$x("//input[@aria-label='key']"); protected ElementsCollection valueField = $$x("//input[@aria-label='value']"); @@ -48,7 +49,7 @@ public class KsqlQueryForm extends BasePage { public KsqlQueryForm clickExecuteBtn(String query) { clickByActions(executeBtn); if (query.contains("EMIT CHANGES")) { - loadingSpinner.shouldBe(Condition.visible); + abortButton.shouldBe(Condition.visible); } else { waitUntilSpinnerDisappear(); } @@ -56,12 +57,21 @@ public class KsqlQueryForm extends BasePage { } @Step - public KsqlQueryForm clickStopQueryBtn() { - clickByActions(stopQueryBtn); - waitUntilSpinnerDisappear(); + public boolean isAbortBtnVisible() { + return isVisible(abortButton); + } + + @Step + public KsqlQueryForm clickAbortBtn() { + clickByActions(abortButton); return this; } + @Step + public boolean isCancelledAlertVisible() { + return isVisible(cancelledAlert); + } + @Step public KsqlQueryForm clickClearResultsBtn() { clickByActions(clearResultsBtn); diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/backlog/SmokeBacklog.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/backlog/SmokeBacklog.java index 25b9538882..7e663f5893 100644 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/backlog/SmokeBacklog.java +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/backlog/SmokeBacklog.java @@ -22,72 +22,65 @@ public class SmokeBacklog extends BaseManualTest { } @Automation(state = TO_BE_AUTOMATED) - @Suite(id = KSQL_DB_SUITE_ID) - @QaseId(277) + @Suite(id = BROKERS_SUITE_ID) + @QaseId(331) @Test public void testCaseB() { } - @Automation(state = TO_BE_AUTOMATED) - @Suite(id = BROKERS_SUITE_ID) - @QaseId(331) - @Test - public void testCaseC() { - } - @Automation(state = TO_BE_AUTOMATED) @Suite(id = BROKERS_SUITE_ID) @QaseId(332) @Test - public void testCaseD() { + public void testCaseC() { } @Automation(state = TO_BE_AUTOMATED) @Suite(id = TOPICS_PROFILE_SUITE_ID) @QaseId(335) @Test - public void testCaseE() { + public void testCaseD() { } @Automation(state = TO_BE_AUTOMATED) @Suite(id = TOPICS_PROFILE_SUITE_ID) @QaseId(336) @Test - public void testCaseF() { + public void testCaseE() { } @Automation(state = TO_BE_AUTOMATED) @Suite(id = TOPICS_PROFILE_SUITE_ID) @QaseId(343) @Test - public void testCaseG() { + public void testCaseF() { } @Automation(state = TO_BE_AUTOMATED) @Suite(id = KSQL_DB_SUITE_ID) @QaseId(344) @Test - public void testCaseH() { + public void testCaseG() { } @Automation(state = TO_BE_AUTOMATED) @Suite(id = SCHEMAS_SUITE_ID) @QaseId(345) @Test - public void testCaseI() { + public void testCaseH() { } @Automation(state = TO_BE_AUTOMATED) @Suite(id = SCHEMAS_SUITE_ID) @QaseId(346) @Test - public void testCaseJ() { + public void testCaseI() { } @Automation(state = TO_BE_AUTOMATED) @Suite(id = TOPICS_PROFILE_SUITE_ID) @QaseId(347) @Test - public void testCaseK() { + public void testCaseJ() { } } diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/ksqldb/KsqlDbTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/ksqldb/KsqlDbTest.java index 00460da08d..0504a8a31a 100644 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/ksqldb/KsqlDbTest.java +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/ksqldb/KsqlDbTest.java @@ -1,6 +1,7 @@ package com.provectus.kafka.ui.smokesuite.ksqldb; import static com.provectus.kafka.ui.pages.ksqldb.enums.KsqlMenuTabs.STREAMS; +import static com.provectus.kafka.ui.pages.ksqldb.enums.KsqlQueryConfig.SELECT_ALL_FROM; import static com.provectus.kafka.ui.pages.ksqldb.enums.KsqlQueryConfig.SHOW_STREAMS; import static com.provectus.kafka.ui.pages.ksqldb.enums.KsqlQueryConfig.SHOW_TABLES; import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.KSQL_DB; @@ -87,7 +88,8 @@ public class KsqlDbTest extends BaseTest { navigateToKsqlDbAndExecuteRequest(SHOW_STREAMS.getQuery()); SoftAssert softly = new SoftAssert(); softly.assertTrue(ksqlQueryForm.areResultsVisible(), "areResultsVisible()"); - softly.assertTrue(ksqlQueryForm.getItemByName(DEFAULT_STREAM.getName()).isVisible(), "getItemByName()"); + softly.assertTrue(ksqlQueryForm.getItemByName(DEFAULT_STREAM.getName()).isVisible(), + String.format("getItemByName(%s)", FIRST_TABLE.getName())); softly.assertAll(); } @@ -104,6 +106,16 @@ public class KsqlDbTest extends BaseTest { softly.assertAll(); } + @QaseId(277) + @Test(priority = 6) + public void stopQueryFunctionalCheck() { + navigateToKsqlDbAndExecuteRequest(String.format(SELECT_ALL_FROM.getQuery(), FIRST_TABLE.getName())); + Assert.assertTrue(ksqlQueryForm.isAbortBtnVisible(), "isAbortBtnVisible()"); + ksqlQueryForm + .clickAbortBtn(); + Assert.assertTrue(ksqlQueryForm.isCancelledAlertVisible(), "isCancelledAlertVisible()"); + } + @AfterClass(alwaysRun = true) public void afterClass() { TOPIC_NAMES_LIST.forEach(topicName -> apiService.deleteTopic(topicName)); diff --git a/kafka-ui-react-app/src/components/Brokers/BrokersList/BrokersList.tsx b/kafka-ui-react-app/src/components/Brokers/BrokersList/BrokersList.tsx index 966edecf1f..d8cd0a2f76 100644 --- a/kafka-ui-react-app/src/components/Brokers/BrokersList/BrokersList.tsx +++ b/kafka-ui-react-app/src/components/Brokers/BrokersList/BrokersList.tsx @@ -11,7 +11,9 @@ import CheckMarkRoundIcon from 'components/common/Icons/CheckMarkRoundIcon'; import { ColumnDef } from '@tanstack/react-table'; import { clusterBrokerPath } from 'lib/paths'; import Tooltip from 'components/common/Tooltip/Tooltip'; +import ColoredCell from 'components/common/NewTable/ColoredCell'; +import SkewHeader from './SkewHeader/SkewHeader'; import * as S from './BrokersList.styled'; const NA = 'N/A'; @@ -57,11 +59,15 @@ const BrokersList: React.FC = () => { count: segmentCount || NA, port: broker?.port, host: broker?.host, + partitionsLeader: broker?.partitionsLeader, + partitionsSkew: broker?.partitionsSkew, + leadersSkew: broker?.leadersSkew, + inSyncPartitions: broker?.inSyncPartitions, }; }); }, [diskUsage, brokers]); - const columns = React.useMemo[]>( + const columns = React.useMemo[]>( () => [ { header: 'Broker ID', @@ -84,7 +90,7 @@ const BrokersList: React.FC = () => { ), }, { - header: 'Segment Size', + header: 'Disk usage', accessorKey: 'size', // eslint-disable-next-line react/no-unstable-nested-components cell: ({ getValue, table, cell, column, renderValue, row }) => @@ -98,10 +104,56 @@ const BrokersList: React.FC = () => { cell={cell} getValue={getValue} renderValue={renderValue} + renderSegments /> ), }, - { header: 'Segment Count', accessorKey: 'count' }, + { + // eslint-disable-next-line react/no-unstable-nested-components + header: () => , + accessorKey: 'partitionsSkew', + // eslint-disable-next-line react/no-unstable-nested-components + cell: ({ getValue }) => { + const value = getValue(); + return ( + = 10 && value < 20} + attention={value >= 20} + /> + ); + }, + }, + { header: 'Leaders', accessorKey: 'partitionsLeader' }, + { + header: 'Leader skew', + accessorKey: 'leadersSkew', + // eslint-disable-next-line react/no-unstable-nested-components + cell: ({ getValue }) => { + const value = getValue(); + return ( + = 10 && value < 20} + attention={value >= 20} + /> + ); + }, + }, + { + header: 'Online partitions', + accessorKey: 'inSyncPartitions', + // eslint-disable-next-line react/no-unstable-nested-components + cell: ({ getValue, row }) => { + const value = getValue(); + return ( + + ); + }, + }, { header: 'Port', accessorKey: 'port' }, { header: 'Host', diff --git a/kafka-ui-react-app/src/components/Brokers/BrokersList/SkewHeader/SkewHeader.styled.ts b/kafka-ui-react-app/src/components/Brokers/BrokersList/SkewHeader/SkewHeader.styled.ts new file mode 100644 index 0000000000..eea2fa3cd9 --- /dev/null +++ b/kafka-ui-react-app/src/components/Brokers/BrokersList/SkewHeader/SkewHeader.styled.ts @@ -0,0 +1,11 @@ +import styled from 'styled-components'; +import { MessageTooltip } from 'components/common/Tooltip/Tooltip.styled'; + +export const CellWrapper = styled.div` + display: flex; + gap: 10px; + + ${MessageTooltip} { + max-height: unset; + } +`; diff --git a/kafka-ui-react-app/src/components/Brokers/BrokersList/SkewHeader/SkewHeader.tsx b/kafka-ui-react-app/src/components/Brokers/BrokersList/SkewHeader/SkewHeader.tsx new file mode 100644 index 0000000000..978d1768dd --- /dev/null +++ b/kafka-ui-react-app/src/components/Brokers/BrokersList/SkewHeader/SkewHeader.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import Tooltip from 'components/common/Tooltip/Tooltip'; +import InfoIcon from 'components/common/Icons/InfoIcon'; + +import * as S from './SkewHeader.styled'; + +const SkewHeader: React.FC = () => ( + + Partitions skew + } + content="The divergence from the average brokers' value" + /> + +); + +export default SkewHeader; diff --git a/kafka-ui-react-app/src/components/Connect/Details/Config/Config.tsx b/kafka-ui-react-app/src/components/Connect/Details/Config/Config.tsx index 0e86d48940..8a372e9d12 100644 --- a/kafka-ui-react-app/src/components/Connect/Details/Config/Config.tsx +++ b/kafka-ui-react-app/src/components/Connect/Details/Config/Config.tsx @@ -37,7 +37,7 @@ const Config: React.FC = () => { formState: { isDirty, isSubmitting, isValid, errors }, setValue, } = useForm({ - mode: 'onTouched', + mode: 'onChange', resolver: yupResolver(validationSchema), defaultValues: { config: JSON.stringify(config, null, '\t'), diff --git a/kafka-ui-react-app/src/components/Dashboard/ClusterTableActionsCell.tsx b/kafka-ui-react-app/src/components/Dashboard/ClusterTableActionsCell.tsx index cb41ab06a8..19fefd784c 100644 --- a/kafka-ui-react-app/src/components/Dashboard/ClusterTableActionsCell.tsx +++ b/kafka-ui-react-app/src/components/Dashboard/ClusterTableActionsCell.tsx @@ -1,17 +1,31 @@ -import React from 'react'; -import { Cluster } from 'generated-sources'; +import React, { useMemo } from 'react'; +import { Cluster, ResourceType } from 'generated-sources'; import { CellContext } from '@tanstack/react-table'; -import { Button } from 'components/common/Button/Button'; import { clusterConfigPath } from 'lib/paths'; +import { useGetUserInfo } from 'lib/hooks/api/roles'; +import { ActionCanButton } from 'components/common/ActionComponent'; type Props = CellContext; const ClusterTableActionsCell: React.FC = ({ row }) => { const { name } = row.original; + const { data } = useGetUserInfo(); + + const isApplicationConfig = useMemo(() => { + return !!data?.userInfo?.permissions.some( + (permission) => permission.resource === ResourceType.APPLICATIONCONFIG + ); + }, [data]); + return ( - + ); }; diff --git a/kafka-ui-react-app/src/components/Dashboard/Dashboard.tsx b/kafka-ui-react-app/src/components/Dashboard/Dashboard.tsx index 7eab4c1d2f..c7b64aef1c 100644 --- a/kafka-ui-react-app/src/components/Dashboard/Dashboard.tsx +++ b/kafka-ui-react-app/src/components/Dashboard/Dashboard.tsx @@ -1,23 +1,25 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useMemo } from 'react'; import PageHeading from 'components/common/PageHeading/PageHeading'; import * as Metrics from 'components/common/Metrics'; import { Tag } from 'components/common/Tag/Tag.styled'; import Switch from 'components/common/Switch/Switch'; import { useClusters } from 'lib/hooks/api/clusters'; -import { Cluster, ServerStatus } from 'generated-sources'; +import { Cluster, ResourceType, ServerStatus } from 'generated-sources'; import { ColumnDef } from '@tanstack/react-table'; import Table, { SizeCell } from 'components/common/NewTable'; import useBoolean from 'lib/hooks/useBoolean'; -import { Button } from 'components/common/Button/Button'; import { clusterNewConfigPath } from 'lib/paths'; import { GlobalSettingsContext } from 'components/contexts/GlobalSettingsContext'; import { useNavigate } from 'react-router-dom'; +import { ActionCanButton } from 'components/common/ActionComponent'; +import { useGetUserInfo } from 'lib/hooks/api/roles'; import * as S from './Dashboard.styled'; import ClusterName from './ClusterName'; import ClusterTableActionsCell from './ClusterTableActionsCell'; const Dashboard: React.FC = () => { + const { data } = useGetUserInfo(); const clusters = useClusters(); const { value: showOfflineOnly, toggle } = useBoolean(false); const appInfo = React.useContext(GlobalSettingsContext); @@ -62,6 +64,11 @@ const Dashboard: React.FC = () => { } }, [clusters, appInfo.hasDynamicConfig]); + const isApplicationConfig = useMemo(() => { + return !!data?.userInfo?.permissions.some( + (permission) => permission.resource === ResourceType.APPLICATIONCONFIG + ); + }, [data]); return ( <> @@ -87,9 +94,14 @@ const Dashboard: React.FC = () => { {appInfo.hasDynamicConfig && ( - + )} { yup.object().shape({ newSchema: schema?.schemaType === SchemaType.PROTOBUF - ? yup.string().required().isEnum('Schema syntax is not valid') + ? yup.string().required() : yup.string().required().isJsonObject('Schema syntax is not valid'), }); const methods = useForm({ diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Messages/Message.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Messages/Message.tsx index dd5cfae748..af76db6739 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Messages/Message.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Messages/Message.tsx @@ -8,6 +8,7 @@ import { formatTimestamp } from 'lib/dateTimeHelpers'; import { JSONPath } from 'jsonpath-plus'; import Ellipsis from 'components/common/Ellipsis/Ellipsis'; import WarningRedIcon from 'components/common/Icons/WarningRedIcon'; +import Tooltip from 'components/common/Tooltip/Tooltip'; import MessageContent from './MessageContent/MessageContent'; import * as S from './MessageContent/MessageContent.styled'; @@ -110,14 +111,26 @@ const Message: React.FC = ({ - {keySerde === 'Fallback' && } + {keySerde === 'Fallback' && ( + } + content="Fallback serde was used" + placement="left" + /> + )} - {valueSerde === 'Fallback' && } + {valueSerde === 'Fallback' && ( + } + content="Fallback serde was used" + placement="left" + /> + )} diff --git a/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx b/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx index bacfa76c93..b7f31a230b 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx @@ -210,6 +210,7 @@ const SendMessage: React.FC<{ closeSidebar: () => void }> = ({ name={name} onChange={onChange} value={value} + height="40px" /> )} /> @@ -225,6 +226,7 @@ const SendMessage: React.FC<{ closeSidebar: () => void }> = ({ name={name} onChange={onChange} value={value} + height="280px" /> )} /> @@ -242,7 +244,7 @@ const SendMessage: React.FC<{ closeSidebar: () => void }> = ({ defaultValue="{}" name={name} onChange={onChange} - height="200px" + height="40px" /> )} /> diff --git a/kafka-ui-react-app/src/components/common/Input/Input.styled.ts b/kafka-ui-react-app/src/components/common/Input/Input.styled.ts index 9495aaecbe..f21962fe6b 100644 --- a/kafka-ui-react-app/src/components/common/Input/Input.styled.ts +++ b/kafka-ui-react-app/src/components/common/Input/Input.styled.ts @@ -29,6 +29,16 @@ export const Wrapper = styled.div` width: 16px; fill: ${({ theme }) => theme.input.icon.color}; } + svg:last-child { + position: absolute; + top: 8px; + line-height: 0; + z-index: 1; + left: unset; + right: 12px; + height: 16px; + width: 16px; + } `; export const Input = styled.input( diff --git a/kafka-ui-react-app/src/components/common/Input/Input.tsx b/kafka-ui-react-app/src/components/common/Input/Input.tsx index ae76bc4717..4d04b730e5 100644 --- a/kafka-ui-react-app/src/components/common/Input/Input.tsx +++ b/kafka-ui-react-app/src/components/common/Input/Input.tsx @@ -16,6 +16,7 @@ export interface InputProps withError?: boolean; label?: React.ReactNode; hint?: React.ReactNode; + clearIcon?: React.ReactNode; // Some may only accept integer, like `Number of Partitions` // some may accept decimal @@ -99,19 +100,22 @@ function pasteNumberCheck( return value; } -const Input: React.FC = ({ - name, - hookFormOptions, - search, - inputSize = 'L', - type, - positiveOnly, - integerOnly, - withError = false, - label, - hint, - ...rest -}) => { +const Input = React.forwardRef((props, ref) => { + const { + name, + hookFormOptions, + search, + inputSize = 'L', + type, + positiveOnly, + integerOnly, + withError = false, + label, + hint, + clearIcon, + ...rest + } = props; + const methods = useFormContext(); const fieldId = React.useId(); @@ -168,7 +172,6 @@ const Input: React.FC = ({ // if the field is a part of react-hook-form form inputOptions = { ...rest, ...methods.register(name, hookFormOptions) }; } - return (
{label && {label}} @@ -181,8 +184,11 @@ const Input: React.FC = ({ type={type} onKeyPress={keyPressEventHandler} onPaste={pasteEventHandler} + ref={ref} {...inputOptions} /> + {clearIcon} + {withError && isHookFormField && ( @@ -192,6 +198,6 @@ const Input: React.FC = ({
); -}; +}); export default Input; diff --git a/kafka-ui-react-app/src/components/common/NewTable/ColoredCell.tsx b/kafka-ui-react-app/src/components/common/NewTable/ColoredCell.tsx new file mode 100644 index 0000000000..df8ab2d6a8 --- /dev/null +++ b/kafka-ui-react-app/src/components/common/NewTable/ColoredCell.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import styled from 'styled-components'; + +interface CellProps { + isWarning?: boolean; + isAttention?: boolean; +} + +interface ColoredCellProps { + value: number | string; + warn?: boolean; + attention?: boolean; +} + +const Cell = styled.div` + color: ${(props) => { + if (props.isAttention) { + return props.theme.table.colored.color.attention; + } + + if (props.isWarning) { + return props.theme.table.colored.color.warning; + } + + return 'inherit'; + }}; +`; + +const ColoredCell: React.FC = ({ + value, + warn, + attention, +}) => { + return ( + + {value} + + ); +}; + +export default ColoredCell; diff --git a/kafka-ui-react-app/src/components/common/NewTable/SizeCell.tsx b/kafka-ui-react-app/src/components/common/NewTable/SizeCell.tsx index 00a60086d9..24485342aa 100644 --- a/kafka-ui-react-app/src/components/common/NewTable/SizeCell.tsx +++ b/kafka-ui-react-app/src/components/common/NewTable/SizeCell.tsx @@ -3,8 +3,15 @@ import { CellContext } from '@tanstack/react-table'; import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted'; // eslint-disable-next-line @typescript-eslint/no-explicit-any -const SizeCell: React.FC> = ({ getValue }) => ( - ()} /> +type AsAny = any; + +const SizeCell: React.FC< + CellContext & { renderSegments?: boolean } +> = ({ getValue, row, renderSegments = false }) => ( + <> + ()} /> + {renderSegments ? `, ${row?.original.count} segment(s)` : null} + ); export default SizeCell; diff --git a/kafka-ui-react-app/src/components/common/Search/Search.tsx b/kafka-ui-react-app/src/components/common/Search/Search.tsx index 66c0e95030..65116d645a 100644 --- a/kafka-ui-react-app/src/components/common/Search/Search.tsx +++ b/kafka-ui-react-app/src/components/common/Search/Search.tsx @@ -1,7 +1,9 @@ -import React from 'react'; +import React, { useRef } from 'react'; import { useDebouncedCallback } from 'use-debounce'; import Input from 'components/common/Input/Input'; import { useSearchParams } from 'react-router-dom'; +import CloseIcon from 'components/common/Icons/CloseIcon'; +import styled from 'styled-components'; interface SearchProps { placeholder?: string; @@ -10,6 +12,16 @@ interface SearchProps { value?: string; } +const IconButtonWrapper = styled.span.attrs(() => ({ + role: 'button', + tabIndex: '0', +}))` + height: 16px !important; + display: inline-block; + &:hover { + cursor: pointer; + } +`; const Search: React.FC = ({ placeholder = 'Search', disabled = false, @@ -17,7 +29,11 @@ const Search: React.FC = ({ onChange, }) => { const [searchParams, setSearchParams] = useSearchParams(); + const ref = useRef(null); const handleChange = useDebouncedCallback((e) => { + if (ref.current != null) { + ref.current.value = e.target.value; + } if (onChange) { onChange(e.target.value); } else { @@ -28,6 +44,15 @@ const Search: React.FC = ({ setSearchParams(searchParams); } }, 500); + const clearSearchValue = () => { + if (searchParams.get('q')) { + searchParams.set('q', ''); + setSearchParams(searchParams); + } + if (ref.current != null) { + ref.current.value = ''; + } + }; return ( = ({ defaultValue={value || searchParams.get('q') || ''} inputSize="M" disabled={disabled} + ref={ref} search + clearIcon={ + + + + } /> ); }; diff --git a/kafka-ui-react-app/src/components/common/Search/__tests__/Search.spec.tsx b/kafka-ui-react-app/src/components/common/Search/__tests__/Search.spec.tsx index 808f229317..2103d22336 100644 --- a/kafka-ui-react-app/src/components/common/Search/__tests__/Search.spec.tsx +++ b/kafka-ui-react-app/src/components/common/Search/__tests__/Search.spec.tsx @@ -41,4 +41,24 @@ describe('Search', () => { render(); expect(screen.queryByPlaceholderText('Search')).toBeInTheDocument(); }); + + it('Clear button is visible', () => { + render(); + + const clearButton = screen.getByRole('button'); + expect(clearButton).toBeInTheDocument(); + }); + + it('Clear button should clear text from input', async () => { + render(); + + const searchField = screen.getAllByRole('textbox')[0]; + await userEvent.type(searchField, 'some text'); + expect(searchField).toHaveValue('some text'); + + const clearButton = screen.getByRole('button'); + await userEvent.click(clearButton); + + expect(searchField).toHaveValue(''); + }); }); diff --git a/kafka-ui-react-app/src/lib/__test__/yupExtended.spec.ts b/kafka-ui-react-app/src/lib/__test__/yupExtended.spec.ts index 8100b9a326..bd43dd3f72 100644 --- a/kafka-ui-react-app/src/lib/__test__/yupExtended.spec.ts +++ b/kafka-ui-react-app/src/lib/__test__/yupExtended.spec.ts @@ -1,19 +1,5 @@ -import { isValidEnum, isValidJsonObject } from 'lib/yupExtended'; +import { isValidJsonObject } from 'lib/yupExtended'; -const invalidEnum = ` -ennum SchemType { - AVRO = 0; - JSON = 1; - PROTOBUF = 3; -} -`; -const validEnum = ` -enum SchemType { - AVRO = 0; - JSON = 1; - PROTOBUF = 3; -} -`; describe('yup extended', () => { describe('isValidJsonObject', () => { it('returns false for no value', () => { @@ -35,21 +21,4 @@ describe('yup extended', () => { expect(isValidJsonObject('{ "foo": "bar" }')).toBeTruthy(); }); }); - - describe('isValidEnum', () => { - it('returns false for invalid enum', () => { - expect(isValidEnum(invalidEnum)).toBeFalsy(); - }); - it('returns false for no value', () => { - expect(isValidEnum()).toBeFalsy(); - }); - it('returns true should trim value', () => { - expect( - isValidEnum(` enum SchemType {AVRO = 0; PROTOBUF = 3;} `) - ).toBeTruthy(); - }); - it('returns true for valid enum', () => { - expect(isValidEnum(validEnum)).toBeTruthy(); - }); - }); }); diff --git a/kafka-ui-react-app/src/lib/dateTimeHelpers.ts b/kafka-ui-react-app/src/lib/dateTimeHelpers.ts index 3dce0edd78..148a70d2a3 100644 --- a/kafka-ui-react-app/src/lib/dateTimeHelpers.ts +++ b/kafka-ui-react-app/src/lib/dateTimeHelpers.ts @@ -1,6 +1,6 @@ export const formatTimestamp = ( timestamp?: number | string | Date, - format: Intl.DateTimeFormatOptions = { hour12: false } + format: Intl.DateTimeFormatOptions = { hourCycle: 'h23' } ): string => { if (!timestamp) { return ''; @@ -8,7 +8,6 @@ export const formatTimestamp = ( // empty array gets the default one from the browser const date = new Date(timestamp); - // invalid date if (Number.isNaN(date.getTime())) { return ''; diff --git a/kafka-ui-react-app/src/lib/yupExtended.ts b/kafka-ui-react-app/src/lib/yupExtended.ts index 4c662ca822..241dac9770 100644 --- a/kafka-ui-react-app/src/lib/yupExtended.ts +++ b/kafka-ui-react-app/src/lib/yupExtended.ts @@ -10,7 +10,6 @@ declare module 'yup' { TFlags extends yup.Flags = '' > extends yup.Schema { isJsonObject(message?: string): StringSchema; - isEnum(message?: string): StringSchema; } } @@ -40,32 +39,6 @@ const isJsonObject = (message?: string) => { isValidJsonObject ); }; - -export const isValidEnum = (value?: string) => { - try { - if (!value) return false; - const trimmedValue = value.trim(); - if ( - trimmedValue.indexOf('enum') === 0 && - trimmedValue.lastIndexOf('}') === trimmedValue.length - 1 - ) { - return true; - } - } catch { - // do nothing - } - return false; -}; - -const isEnum = (message?: string) => { - return yup.string().test( - 'isEnum', - // eslint-disable-next-line no-template-curly-in-string - message || '${path} is not Enum object', - isValidEnum - ); -}; - /** * due to yup rerunning all the object validiation during any render, * it makes sense to cache the async results @@ -88,7 +61,6 @@ export function cacheTest( } yup.addMethod(yup.StringSchema, 'isJsonObject', isJsonObject); -yup.addMethod(yup.StringSchema, 'isEnum', isEnum); export const topicFormValidationSchema = yup.object().shape({ name: yup diff --git a/kafka-ui-react-app/src/theme/theme.ts b/kafka-ui-react-app/src/theme/theme.ts index 33dbf1c619..80cc58991c 100644 --- a/kafka-ui-react-app/src/theme/theme.ts +++ b/kafka-ui-react-app/src/theme/theme.ts @@ -533,6 +533,12 @@ export const theme = { active: Colors.neutral[90], }, }, + colored: { + color: { + attention: Colors.red[50], + warning: Colors.yellow[20], + }, + }, expander: { normal: Colors.brand[30], hover: Colors.brand[40], @@ -928,6 +934,12 @@ export const darkTheme: ThemeType = { active: Colors.neutral[0], }, }, + colored: { + color: { + attention: Colors.red[50], + warning: Colors.yellow[20], + }, + }, expander: { normal: Colors.brand[30], hover: Colors.brand[40],