Merge branch 'master' into issues/2752
This commit is contained in:
commit
af9bd198f1
26 changed files with 331 additions and 128 deletions
|
@ -12,7 +12,7 @@
|
||||||
<artifactId>kafka-ui-api</artifactId>
|
<artifactId>kafka-ui-api</artifactId>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<jacoco.version>0.8.8</jacoco.version>
|
<jacoco.version>0.8.10</jacoco.version>
|
||||||
<sonar.java.coveragePlugin>jacoco</sonar.java.coveragePlugin>
|
<sonar.java.coveragePlugin>jacoco</sonar.java.coveragePlugin>
|
||||||
<sonar.dynamicAnalysis>reuseReports</sonar.dynamicAnalysis>
|
<sonar.dynamicAnalysis>reuseReports</sonar.dynamicAnalysis>
|
||||||
<sonar.jacoco.reportPath>${project.basedir}/target/jacoco.exec</sonar.jacoco.reportPath>
|
<sonar.jacoco.reportPath>${project.basedir}/target/jacoco.exec</sonar.jacoco.reportPath>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package com.provectus.kafka.ui.model;
|
package com.provectus.kafka.ui.model;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.MathContext;
|
import java.math.RoundingMode;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
@ -21,8 +21,6 @@ public class PartitionDistributionStats {
|
||||||
// avg skew will show unuseful results on low number of partitions
|
// avg skew will show unuseful results on low number of partitions
|
||||||
private static final int MIN_PARTITIONS_FOR_SKEW_CALCULATION = 50;
|
private static final int MIN_PARTITIONS_FOR_SKEW_CALCULATION = 50;
|
||||||
|
|
||||||
private static final MathContext ROUNDING_MATH_CTX = new MathContext(3);
|
|
||||||
|
|
||||||
private final Map<Node, Integer> partitionLeaders;
|
private final Map<Node, Integer> partitionLeaders;
|
||||||
private final Map<Node, Integer> partitionsCount;
|
private final Map<Node, Integer> partitionsCount;
|
||||||
private final Map<Node, Integer> inSyncPartitions;
|
private final Map<Node, Integer> inSyncPartitions;
|
||||||
|
@ -88,6 +86,7 @@ public class PartitionDistributionStats {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
value = value == null ? 0 : value;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
<httpcomponents.version>5.2.1</httpcomponents.version>
|
<httpcomponents.version>5.2.1</httpcomponents.version>
|
||||||
<selenium.version>4.8.1</selenium.version>
|
<selenium.version>4.8.1</selenium.version>
|
||||||
<selenide.version>6.12.3</selenide.version>
|
<selenide.version>6.12.3</selenide.version>
|
||||||
<testng.version>7.7.0</testng.version>
|
<testng.version>7.7.1</testng.version>
|
||||||
<allure.version>2.21.0</allure.version>
|
<allure.version>2.21.0</allure.version>
|
||||||
<qase.io.version>3.0.4</qase.io.version>
|
<qase.io.version>3.0.4</qase.io.version>
|
||||||
<aspectj.version>1.9.9.1</aspectj.version>
|
<aspectj.version>1.9.9.1</aspectj.version>
|
||||||
|
|
|
@ -48,7 +48,8 @@ public class BrokersList extends BasePage {
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<SelenideElement> getEnabledColumnHeaders() {
|
private List<SelenideElement> 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)))
|
.map(name -> $x(String.format(columnHeaderLocator, name)))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,11 +17,12 @@ import java.util.List;
|
||||||
public class KsqlQueryForm extends BasePage {
|
public class KsqlQueryForm extends BasePage {
|
||||||
protected SelenideElement clearBtn = $x("//div/button[text()='Clear']");
|
protected SelenideElement clearBtn = $x("//div/button[text()='Clear']");
|
||||||
protected SelenideElement executeBtn = $x("//div/button[text()='Execute']");
|
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 clearResultsBtn = $x("//div/button[text()='Clear results']");
|
||||||
protected SelenideElement addStreamPropertyBtn = $x("//button[text()='Add Stream Property']");
|
protected SelenideElement addStreamPropertyBtn = $x("//button[text()='Add Stream Property']");
|
||||||
protected SelenideElement queryAreaValue = $x("//div[@class='ace_content']");
|
protected SelenideElement queryAreaValue = $x("//div[@class='ace_content']");
|
||||||
protected SelenideElement queryArea = $x("//div[@id='ksql']/textarea[@class='ace_text-input']");
|
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 ksqlGridItems = $$x("//tbody//tr");
|
||||||
protected ElementsCollection keyField = $$x("//input[@aria-label='key']");
|
protected ElementsCollection keyField = $$x("//input[@aria-label='key']");
|
||||||
protected ElementsCollection valueField = $$x("//input[@aria-label='value']");
|
protected ElementsCollection valueField = $$x("//input[@aria-label='value']");
|
||||||
|
@ -48,7 +49,7 @@ public class KsqlQueryForm extends BasePage {
|
||||||
public KsqlQueryForm clickExecuteBtn(String query) {
|
public KsqlQueryForm clickExecuteBtn(String query) {
|
||||||
clickByActions(executeBtn);
|
clickByActions(executeBtn);
|
||||||
if (query.contains("EMIT CHANGES")) {
|
if (query.contains("EMIT CHANGES")) {
|
||||||
loadingSpinner.shouldBe(Condition.visible);
|
abortButton.shouldBe(Condition.visible);
|
||||||
} else {
|
} else {
|
||||||
waitUntilSpinnerDisappear();
|
waitUntilSpinnerDisappear();
|
||||||
}
|
}
|
||||||
|
@ -56,12 +57,21 @@ public class KsqlQueryForm extends BasePage {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Step
|
@Step
|
||||||
public KsqlQueryForm clickStopQueryBtn() {
|
public boolean isAbortBtnVisible() {
|
||||||
clickByActions(stopQueryBtn);
|
return isVisible(abortButton);
|
||||||
waitUntilSpinnerDisappear();
|
}
|
||||||
|
|
||||||
|
@Step
|
||||||
|
public KsqlQueryForm clickAbortBtn() {
|
||||||
|
clickByActions(abortButton);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Step
|
||||||
|
public boolean isCancelledAlertVisible() {
|
||||||
|
return isVisible(cancelledAlert);
|
||||||
|
}
|
||||||
|
|
||||||
@Step
|
@Step
|
||||||
public KsqlQueryForm clickClearResultsBtn() {
|
public KsqlQueryForm clickClearResultsBtn() {
|
||||||
clickByActions(clearResultsBtn);
|
clickByActions(clearResultsBtn);
|
||||||
|
|
|
@ -22,72 +22,65 @@ public class SmokeBacklog extends BaseManualTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Automation(state = TO_BE_AUTOMATED)
|
@Automation(state = TO_BE_AUTOMATED)
|
||||||
@Suite(id = KSQL_DB_SUITE_ID)
|
@Suite(id = BROKERS_SUITE_ID)
|
||||||
@QaseId(277)
|
@QaseId(331)
|
||||||
@Test
|
@Test
|
||||||
public void testCaseB() {
|
public void testCaseB() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Automation(state = TO_BE_AUTOMATED)
|
|
||||||
@Suite(id = BROKERS_SUITE_ID)
|
|
||||||
@QaseId(331)
|
|
||||||
@Test
|
|
||||||
public void testCaseC() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Automation(state = TO_BE_AUTOMATED)
|
@Automation(state = TO_BE_AUTOMATED)
|
||||||
@Suite(id = BROKERS_SUITE_ID)
|
@Suite(id = BROKERS_SUITE_ID)
|
||||||
@QaseId(332)
|
@QaseId(332)
|
||||||
@Test
|
@Test
|
||||||
public void testCaseD() {
|
public void testCaseC() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Automation(state = TO_BE_AUTOMATED)
|
@Automation(state = TO_BE_AUTOMATED)
|
||||||
@Suite(id = TOPICS_PROFILE_SUITE_ID)
|
@Suite(id = TOPICS_PROFILE_SUITE_ID)
|
||||||
@QaseId(335)
|
@QaseId(335)
|
||||||
@Test
|
@Test
|
||||||
public void testCaseE() {
|
public void testCaseD() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Automation(state = TO_BE_AUTOMATED)
|
@Automation(state = TO_BE_AUTOMATED)
|
||||||
@Suite(id = TOPICS_PROFILE_SUITE_ID)
|
@Suite(id = TOPICS_PROFILE_SUITE_ID)
|
||||||
@QaseId(336)
|
@QaseId(336)
|
||||||
@Test
|
@Test
|
||||||
public void testCaseF() {
|
public void testCaseE() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Automation(state = TO_BE_AUTOMATED)
|
@Automation(state = TO_BE_AUTOMATED)
|
||||||
@Suite(id = TOPICS_PROFILE_SUITE_ID)
|
@Suite(id = TOPICS_PROFILE_SUITE_ID)
|
||||||
@QaseId(343)
|
@QaseId(343)
|
||||||
@Test
|
@Test
|
||||||
public void testCaseG() {
|
public void testCaseF() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Automation(state = TO_BE_AUTOMATED)
|
@Automation(state = TO_BE_AUTOMATED)
|
||||||
@Suite(id = KSQL_DB_SUITE_ID)
|
@Suite(id = KSQL_DB_SUITE_ID)
|
||||||
@QaseId(344)
|
@QaseId(344)
|
||||||
@Test
|
@Test
|
||||||
public void testCaseH() {
|
public void testCaseG() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Automation(state = TO_BE_AUTOMATED)
|
@Automation(state = TO_BE_AUTOMATED)
|
||||||
@Suite(id = SCHEMAS_SUITE_ID)
|
@Suite(id = SCHEMAS_SUITE_ID)
|
||||||
@QaseId(345)
|
@QaseId(345)
|
||||||
@Test
|
@Test
|
||||||
public void testCaseI() {
|
public void testCaseH() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Automation(state = TO_BE_AUTOMATED)
|
@Automation(state = TO_BE_AUTOMATED)
|
||||||
@Suite(id = SCHEMAS_SUITE_ID)
|
@Suite(id = SCHEMAS_SUITE_ID)
|
||||||
@QaseId(346)
|
@QaseId(346)
|
||||||
@Test
|
@Test
|
||||||
public void testCaseJ() {
|
public void testCaseI() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Automation(state = TO_BE_AUTOMATED)
|
@Automation(state = TO_BE_AUTOMATED)
|
||||||
@Suite(id = TOPICS_PROFILE_SUITE_ID)
|
@Suite(id = TOPICS_PROFILE_SUITE_ID)
|
||||||
@QaseId(347)
|
@QaseId(347)
|
||||||
@Test
|
@Test
|
||||||
public void testCaseK() {
|
public void testCaseJ() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.provectus.kafka.ui.smokesuite.ksqldb;
|
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.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_STREAMS;
|
||||||
import static com.provectus.kafka.ui.pages.ksqldb.enums.KsqlQueryConfig.SHOW_TABLES;
|
import static com.provectus.kafka.ui.pages.ksqldb.enums.KsqlQueryConfig.SHOW_TABLES;
|
||||||
import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.KSQL_DB;
|
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());
|
navigateToKsqlDbAndExecuteRequest(SHOW_STREAMS.getQuery());
|
||||||
SoftAssert softly = new SoftAssert();
|
SoftAssert softly = new SoftAssert();
|
||||||
softly.assertTrue(ksqlQueryForm.areResultsVisible(), "areResultsVisible()");
|
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();
|
softly.assertAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,6 +106,16 @@ public class KsqlDbTest extends BaseTest {
|
||||||
softly.assertAll();
|
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)
|
@AfterClass(alwaysRun = true)
|
||||||
public void afterClass() {
|
public void afterClass() {
|
||||||
TOPIC_NAMES_LIST.forEach(topicName -> apiService.deleteTopic(topicName));
|
TOPIC_NAMES_LIST.forEach(topicName -> apiService.deleteTopic(topicName));
|
||||||
|
|
|
@ -11,7 +11,9 @@ import CheckMarkRoundIcon from 'components/common/Icons/CheckMarkRoundIcon';
|
||||||
import { ColumnDef } from '@tanstack/react-table';
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
import { clusterBrokerPath } from 'lib/paths';
|
import { clusterBrokerPath } from 'lib/paths';
|
||||||
import Tooltip from 'components/common/Tooltip/Tooltip';
|
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';
|
import * as S from './BrokersList.styled';
|
||||||
|
|
||||||
const NA = 'N/A';
|
const NA = 'N/A';
|
||||||
|
@ -57,11 +59,15 @@ const BrokersList: React.FC = () => {
|
||||||
count: segmentCount || NA,
|
count: segmentCount || NA,
|
||||||
port: broker?.port,
|
port: broker?.port,
|
||||||
host: broker?.host,
|
host: broker?.host,
|
||||||
|
partitionsLeader: broker?.partitionsLeader,
|
||||||
|
partitionsSkew: broker?.partitionsSkew,
|
||||||
|
leadersSkew: broker?.leadersSkew,
|
||||||
|
inSyncPartitions: broker?.inSyncPartitions,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, [diskUsage, brokers]);
|
}, [diskUsage, brokers]);
|
||||||
|
|
||||||
const columns = React.useMemo<ColumnDef<typeof rows>[]>(
|
const columns = React.useMemo<ColumnDef<(typeof rows)[number]>[]>(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
header: 'Broker ID',
|
header: 'Broker ID',
|
||||||
|
@ -84,7 +90,7 @@ const BrokersList: React.FC = () => {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Segment Size',
|
header: 'Disk usage',
|
||||||
accessorKey: 'size',
|
accessorKey: 'size',
|
||||||
// eslint-disable-next-line react/no-unstable-nested-components
|
// eslint-disable-next-line react/no-unstable-nested-components
|
||||||
cell: ({ getValue, table, cell, column, renderValue, row }) =>
|
cell: ({ getValue, table, cell, column, renderValue, row }) =>
|
||||||
|
@ -98,10 +104,56 @@ const BrokersList: React.FC = () => {
|
||||||
cell={cell}
|
cell={cell}
|
||||||
getValue={getValue}
|
getValue={getValue}
|
||||||
renderValue={renderValue}
|
renderValue={renderValue}
|
||||||
|
renderSegments
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{ header: 'Segment Count', accessorKey: 'count' },
|
{
|
||||||
|
// eslint-disable-next-line react/no-unstable-nested-components
|
||||||
|
header: () => <SkewHeader />,
|
||||||
|
accessorKey: 'partitionsSkew',
|
||||||
|
// eslint-disable-next-line react/no-unstable-nested-components
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const value = getValue<number>();
|
||||||
|
return (
|
||||||
|
<ColoredCell
|
||||||
|
value={value ? `${value.toFixed(2)}%` : '-'}
|
||||||
|
warn={value >= 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<number>();
|
||||||
|
return (
|
||||||
|
<ColoredCell
|
||||||
|
value={value ? `${value.toFixed(2)}%` : '-'}
|
||||||
|
warn={value >= 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<number>();
|
||||||
|
return (
|
||||||
|
<ColoredCell
|
||||||
|
value={value}
|
||||||
|
attention={value !== row.original.count}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
{ header: 'Port', accessorKey: 'port' },
|
{ header: 'Port', accessorKey: 'port' },
|
||||||
{
|
{
|
||||||
header: 'Host',
|
header: 'Host',
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
`;
|
|
@ -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 = () => (
|
||||||
|
<S.CellWrapper>
|
||||||
|
Partitions skew
|
||||||
|
<Tooltip
|
||||||
|
value={<InfoIcon />}
|
||||||
|
content="The divergence from the average brokers' value"
|
||||||
|
/>
|
||||||
|
</S.CellWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default SkewHeader;
|
|
@ -37,7 +37,7 @@ const Config: React.FC = () => {
|
||||||
formState: { isDirty, isSubmitting, isValid, errors },
|
formState: { isDirty, isSubmitting, isValid, errors },
|
||||||
setValue,
|
setValue,
|
||||||
} = useForm<FormValues>({
|
} = useForm<FormValues>({
|
||||||
mode: 'onTouched',
|
mode: 'onChange',
|
||||||
resolver: yupResolver(validationSchema),
|
resolver: yupResolver(validationSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
config: JSON.stringify(config, null, '\t'),
|
config: JSON.stringify(config, null, '\t'),
|
||||||
|
|
|
@ -1,17 +1,31 @@
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { Cluster } from 'generated-sources';
|
import { Cluster, ResourceType } from 'generated-sources';
|
||||||
import { CellContext } from '@tanstack/react-table';
|
import { CellContext } from '@tanstack/react-table';
|
||||||
import { Button } from 'components/common/Button/Button';
|
|
||||||
import { clusterConfigPath } from 'lib/paths';
|
import { clusterConfigPath } from 'lib/paths';
|
||||||
|
import { useGetUserInfo } from 'lib/hooks/api/roles';
|
||||||
|
import { ActionCanButton } from 'components/common/ActionComponent';
|
||||||
|
|
||||||
type Props = CellContext<Cluster, unknown>;
|
type Props = CellContext<Cluster, unknown>;
|
||||||
|
|
||||||
const ClusterTableActionsCell: React.FC<Props> = ({ row }) => {
|
const ClusterTableActionsCell: React.FC<Props> = ({ row }) => {
|
||||||
const { name } = row.original;
|
const { name } = row.original;
|
||||||
|
const { data } = useGetUserInfo();
|
||||||
|
|
||||||
|
const isApplicationConfig = useMemo(() => {
|
||||||
|
return !!data?.userInfo?.permissions.some(
|
||||||
|
(permission) => permission.resource === ResourceType.APPLICATIONCONFIG
|
||||||
|
);
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button buttonType="secondary" buttonSize="S" to={clusterConfigPath(name)}>
|
<ActionCanButton
|
||||||
|
buttonType="secondary"
|
||||||
|
buttonSize="S"
|
||||||
|
to={clusterConfigPath(name)}
|
||||||
|
canDoAction={isApplicationConfig}
|
||||||
|
>
|
||||||
Configure
|
Configure
|
||||||
</Button>
|
</ActionCanButton>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,23 +1,25 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, useMemo } from 'react';
|
||||||
import PageHeading from 'components/common/PageHeading/PageHeading';
|
import PageHeading from 'components/common/PageHeading/PageHeading';
|
||||||
import * as Metrics from 'components/common/Metrics';
|
import * as Metrics from 'components/common/Metrics';
|
||||||
import { Tag } from 'components/common/Tag/Tag.styled';
|
import { Tag } from 'components/common/Tag/Tag.styled';
|
||||||
import Switch from 'components/common/Switch/Switch';
|
import Switch from 'components/common/Switch/Switch';
|
||||||
import { useClusters } from 'lib/hooks/api/clusters';
|
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 { ColumnDef } from '@tanstack/react-table';
|
||||||
import Table, { SizeCell } from 'components/common/NewTable';
|
import Table, { SizeCell } from 'components/common/NewTable';
|
||||||
import useBoolean from 'lib/hooks/useBoolean';
|
import useBoolean from 'lib/hooks/useBoolean';
|
||||||
import { Button } from 'components/common/Button/Button';
|
|
||||||
import { clusterNewConfigPath } from 'lib/paths';
|
import { clusterNewConfigPath } from 'lib/paths';
|
||||||
import { GlobalSettingsContext } from 'components/contexts/GlobalSettingsContext';
|
import { GlobalSettingsContext } from 'components/contexts/GlobalSettingsContext';
|
||||||
import { useNavigate } from 'react-router-dom';
|
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 * as S from './Dashboard.styled';
|
||||||
import ClusterName from './ClusterName';
|
import ClusterName from './ClusterName';
|
||||||
import ClusterTableActionsCell from './ClusterTableActionsCell';
|
import ClusterTableActionsCell from './ClusterTableActionsCell';
|
||||||
|
|
||||||
const Dashboard: React.FC = () => {
|
const Dashboard: React.FC = () => {
|
||||||
|
const { data } = useGetUserInfo();
|
||||||
const clusters = useClusters();
|
const clusters = useClusters();
|
||||||
const { value: showOfflineOnly, toggle } = useBoolean(false);
|
const { value: showOfflineOnly, toggle } = useBoolean(false);
|
||||||
const appInfo = React.useContext(GlobalSettingsContext);
|
const appInfo = React.useContext(GlobalSettingsContext);
|
||||||
|
@ -62,6 +64,11 @@ const Dashboard: React.FC = () => {
|
||||||
}
|
}
|
||||||
}, [clusters, appInfo.hasDynamicConfig]);
|
}, [clusters, appInfo.hasDynamicConfig]);
|
||||||
|
|
||||||
|
const isApplicationConfig = useMemo(() => {
|
||||||
|
return !!data?.userInfo?.permissions.some(
|
||||||
|
(permission) => permission.resource === ResourceType.APPLICATIONCONFIG
|
||||||
|
);
|
||||||
|
}, [data]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeading text="Dashboard" />
|
<PageHeading text="Dashboard" />
|
||||||
|
@ -87,9 +94,14 @@ const Dashboard: React.FC = () => {
|
||||||
<label>Only offline clusters</label>
|
<label>Only offline clusters</label>
|
||||||
</div>
|
</div>
|
||||||
{appInfo.hasDynamicConfig && (
|
{appInfo.hasDynamicConfig && (
|
||||||
<Button buttonType="primary" buttonSize="M" to={clusterNewConfigPath}>
|
<ActionCanButton
|
||||||
|
buttonType="primary"
|
||||||
|
buttonSize="M"
|
||||||
|
to={clusterNewConfigPath}
|
||||||
|
canDoAction={isApplicationConfig}
|
||||||
|
>
|
||||||
Configure new cluster
|
Configure new cluster
|
||||||
</Button>
|
</ActionCanButton>
|
||||||
)}
|
)}
|
||||||
</S.Toolbar>
|
</S.Toolbar>
|
||||||
<Table
|
<Table
|
||||||
|
|
|
@ -55,7 +55,7 @@ const Form: React.FC = () => {
|
||||||
yup.object().shape({
|
yup.object().shape({
|
||||||
newSchema:
|
newSchema:
|
||||||
schema?.schemaType === SchemaType.PROTOBUF
|
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'),
|
: yup.string().required().isJsonObject('Schema syntax is not valid'),
|
||||||
});
|
});
|
||||||
const methods = useForm<NewSchemaSubjectRaw>({
|
const methods = useForm<NewSchemaSubjectRaw>({
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { formatTimestamp } from 'lib/dateTimeHelpers';
|
||||||
import { JSONPath } from 'jsonpath-plus';
|
import { JSONPath } from 'jsonpath-plus';
|
||||||
import Ellipsis from 'components/common/Ellipsis/Ellipsis';
|
import Ellipsis from 'components/common/Ellipsis/Ellipsis';
|
||||||
import WarningRedIcon from 'components/common/Icons/WarningRedIcon';
|
import WarningRedIcon from 'components/common/Icons/WarningRedIcon';
|
||||||
|
import Tooltip from 'components/common/Tooltip/Tooltip';
|
||||||
|
|
||||||
import MessageContent from './MessageContent/MessageContent';
|
import MessageContent from './MessageContent/MessageContent';
|
||||||
import * as S from './MessageContent/MessageContent.styled';
|
import * as S from './MessageContent/MessageContent.styled';
|
||||||
|
@ -110,14 +111,26 @@ const Message: React.FC<Props> = ({
|
||||||
</td>
|
</td>
|
||||||
<S.DataCell title={key}>
|
<S.DataCell title={key}>
|
||||||
<Ellipsis text={renderFilteredJson(key, keyFilters)}>
|
<Ellipsis text={renderFilteredJson(key, keyFilters)}>
|
||||||
{keySerde === 'Fallback' && <WarningRedIcon />}
|
{keySerde === 'Fallback' && (
|
||||||
|
<Tooltip
|
||||||
|
value={<WarningRedIcon />}
|
||||||
|
content="Fallback serde was used"
|
||||||
|
placement="left"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Ellipsis>
|
</Ellipsis>
|
||||||
</S.DataCell>
|
</S.DataCell>
|
||||||
<S.DataCell title={content}>
|
<S.DataCell title={content}>
|
||||||
<S.Metadata>
|
<S.Metadata>
|
||||||
<S.MetadataValue>
|
<S.MetadataValue>
|
||||||
<Ellipsis text={renderFilteredJson(content, contentFilters)}>
|
<Ellipsis text={renderFilteredJson(content, contentFilters)}>
|
||||||
{valueSerde === 'Fallback' && <WarningRedIcon />}
|
{valueSerde === 'Fallback' && (
|
||||||
|
<Tooltip
|
||||||
|
value={<WarningRedIcon />}
|
||||||
|
content="Fallback serde was used"
|
||||||
|
placement="left"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Ellipsis>
|
</Ellipsis>
|
||||||
</S.MetadataValue>
|
</S.MetadataValue>
|
||||||
</S.Metadata>
|
</S.Metadata>
|
||||||
|
|
|
@ -210,6 +210,7 @@ const SendMessage: React.FC<{ closeSidebar: () => void }> = ({
|
||||||
name={name}
|
name={name}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
value={value}
|
value={value}
|
||||||
|
height="40px"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@ -225,6 +226,7 @@ const SendMessage: React.FC<{ closeSidebar: () => void }> = ({
|
||||||
name={name}
|
name={name}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
value={value}
|
value={value}
|
||||||
|
height="280px"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@ -242,7 +244,7 @@ const SendMessage: React.FC<{ closeSidebar: () => void }> = ({
|
||||||
defaultValue="{}"
|
defaultValue="{}"
|
||||||
name={name}
|
name={name}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
height="200px"
|
height="40px"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -29,6 +29,16 @@ export const Wrapper = styled.div`
|
||||||
width: 16px;
|
width: 16px;
|
||||||
fill: ${({ theme }) => theme.input.icon.color};
|
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<InputProps>(
|
export const Input = styled.input<InputProps>(
|
||||||
|
|
|
@ -16,6 +16,7 @@ export interface InputProps
|
||||||
withError?: boolean;
|
withError?: boolean;
|
||||||
label?: React.ReactNode;
|
label?: React.ReactNode;
|
||||||
hint?: React.ReactNode;
|
hint?: React.ReactNode;
|
||||||
|
clearIcon?: React.ReactNode;
|
||||||
|
|
||||||
// Some may only accept integer, like `Number of Partitions`
|
// Some may only accept integer, like `Number of Partitions`
|
||||||
// some may accept decimal
|
// some may accept decimal
|
||||||
|
@ -99,7 +100,8 @@ function pasteNumberCheck(
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Input: React.FC<InputProps> = ({
|
const Input = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => {
|
||||||
|
const {
|
||||||
name,
|
name,
|
||||||
hookFormOptions,
|
hookFormOptions,
|
||||||
search,
|
search,
|
||||||
|
@ -110,8 +112,10 @@ const Input: React.FC<InputProps> = ({
|
||||||
withError = false,
|
withError = false,
|
||||||
label,
|
label,
|
||||||
hint,
|
hint,
|
||||||
|
clearIcon,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
} = props;
|
||||||
|
|
||||||
const methods = useFormContext();
|
const methods = useFormContext();
|
||||||
|
|
||||||
const fieldId = React.useId();
|
const fieldId = React.useId();
|
||||||
|
@ -168,7 +172,6 @@ const Input: React.FC<InputProps> = ({
|
||||||
// if the field is a part of react-hook-form form
|
// if the field is a part of react-hook-form form
|
||||||
inputOptions = { ...rest, ...methods.register(name, hookFormOptions) };
|
inputOptions = { ...rest, ...methods.register(name, hookFormOptions) };
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{label && <InputLabel htmlFor={rest.id || fieldId}>{label}</InputLabel>}
|
{label && <InputLabel htmlFor={rest.id || fieldId}>{label}</InputLabel>}
|
||||||
|
@ -181,8 +184,11 @@ const Input: React.FC<InputProps> = ({
|
||||||
type={type}
|
type={type}
|
||||||
onKeyPress={keyPressEventHandler}
|
onKeyPress={keyPressEventHandler}
|
||||||
onPaste={pasteEventHandler}
|
onPaste={pasteEventHandler}
|
||||||
|
ref={ref}
|
||||||
{...inputOptions}
|
{...inputOptions}
|
||||||
/>
|
/>
|
||||||
|
{clearIcon}
|
||||||
|
|
||||||
{withError && isHookFormField && (
|
{withError && isHookFormField && (
|
||||||
<S.FormError>
|
<S.FormError>
|
||||||
<ErrorMessage name={name} />
|
<ErrorMessage name={name} />
|
||||||
|
@ -192,6 +198,6 @@ const Input: React.FC<InputProps> = ({
|
||||||
</S.Wrapper>
|
</S.Wrapper>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
export default Input;
|
export default Input;
|
||||||
|
|
|
@ -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<CellProps>`
|
||||||
|
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<ColoredCellProps> = ({
|
||||||
|
value,
|
||||||
|
warn,
|
||||||
|
attention,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Cell isWarning={warn} isAttention={attention}>
|
||||||
|
{value}
|
||||||
|
</Cell>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ColoredCell;
|
|
@ -3,8 +3,15 @@ import { CellContext } from '@tanstack/react-table';
|
||||||
import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';
|
import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const SizeCell: React.FC<CellContext<any, unknown>> = ({ getValue }) => (
|
type AsAny = any;
|
||||||
|
|
||||||
|
const SizeCell: React.FC<
|
||||||
|
CellContext<AsAny, unknown> & { renderSegments?: boolean }
|
||||||
|
> = ({ getValue, row, renderSegments = false }) => (
|
||||||
|
<>
|
||||||
<BytesFormatted value={getValue<string | number>()} />
|
<BytesFormatted value={getValue<string | number>()} />
|
||||||
|
{renderSegments ? `, ${row?.original.count} segment(s)` : null}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default SizeCell;
|
export default SizeCell;
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import React from 'react';
|
import React, { useRef } from 'react';
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
import Input from 'components/common/Input/Input';
|
import Input from 'components/common/Input/Input';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import CloseIcon from 'components/common/Icons/CloseIcon';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
interface SearchProps {
|
interface SearchProps {
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
@ -10,6 +12,16 @@ interface SearchProps {
|
||||||
value?: string;
|
value?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const IconButtonWrapper = styled.span.attrs(() => ({
|
||||||
|
role: 'button',
|
||||||
|
tabIndex: '0',
|
||||||
|
}))`
|
||||||
|
height: 16px !important;
|
||||||
|
display: inline-block;
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
`;
|
||||||
const Search: React.FC<SearchProps> = ({
|
const Search: React.FC<SearchProps> = ({
|
||||||
placeholder = 'Search',
|
placeholder = 'Search',
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
@ -17,7 +29,11 @@ const Search: React.FC<SearchProps> = ({
|
||||||
onChange,
|
onChange,
|
||||||
}) => {
|
}) => {
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const ref = useRef<HTMLInputElement>(null);
|
||||||
const handleChange = useDebouncedCallback((e) => {
|
const handleChange = useDebouncedCallback((e) => {
|
||||||
|
if (ref.current != null) {
|
||||||
|
ref.current.value = e.target.value;
|
||||||
|
}
|
||||||
if (onChange) {
|
if (onChange) {
|
||||||
onChange(e.target.value);
|
onChange(e.target.value);
|
||||||
} else {
|
} else {
|
||||||
|
@ -28,6 +44,15 @@ const Search: React.FC<SearchProps> = ({
|
||||||
setSearchParams(searchParams);
|
setSearchParams(searchParams);
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
|
const clearSearchValue = () => {
|
||||||
|
if (searchParams.get('q')) {
|
||||||
|
searchParams.set('q', '');
|
||||||
|
setSearchParams(searchParams);
|
||||||
|
}
|
||||||
|
if (ref.current != null) {
|
||||||
|
ref.current.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
|
@ -37,7 +62,13 @@ const Search: React.FC<SearchProps> = ({
|
||||||
defaultValue={value || searchParams.get('q') || ''}
|
defaultValue={value || searchParams.get('q') || ''}
|
||||||
inputSize="M"
|
inputSize="M"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
ref={ref}
|
||||||
search
|
search
|
||||||
|
clearIcon={
|
||||||
|
<IconButtonWrapper onClick={clearSearchValue}>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButtonWrapper>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -41,4 +41,24 @@ describe('Search', () => {
|
||||||
render(<Search />);
|
render(<Search />);
|
||||||
expect(screen.queryByPlaceholderText('Search')).toBeInTheDocument();
|
expect(screen.queryByPlaceholderText('Search')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Clear button is visible', () => {
|
||||||
|
render(<Search placeholder={placeholder} />);
|
||||||
|
|
||||||
|
const clearButton = screen.getByRole('button');
|
||||||
|
expect(clearButton).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Clear button should clear text from input', async () => {
|
||||||
|
render(<Search placeholder={placeholder} />);
|
||||||
|
|
||||||
|
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('');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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('yup extended', () => {
|
||||||
describe('isValidJsonObject', () => {
|
describe('isValidJsonObject', () => {
|
||||||
it('returns false for no value', () => {
|
it('returns false for no value', () => {
|
||||||
|
@ -35,21 +21,4 @@ describe('yup extended', () => {
|
||||||
expect(isValidJsonObject('{ "foo": "bar" }')).toBeTruthy();
|
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
export const formatTimestamp = (
|
export const formatTimestamp = (
|
||||||
timestamp?: number | string | Date,
|
timestamp?: number | string | Date,
|
||||||
format: Intl.DateTimeFormatOptions = { hour12: false }
|
format: Intl.DateTimeFormatOptions = { hourCycle: 'h23' }
|
||||||
): string => {
|
): string => {
|
||||||
if (!timestamp) {
|
if (!timestamp) {
|
||||||
return '';
|
return '';
|
||||||
|
@ -8,7 +8,6 @@ export const formatTimestamp = (
|
||||||
|
|
||||||
// empty array gets the default one from the browser
|
// empty array gets the default one from the browser
|
||||||
const date = new Date(timestamp);
|
const date = new Date(timestamp);
|
||||||
|
|
||||||
// invalid date
|
// invalid date
|
||||||
if (Number.isNaN(date.getTime())) {
|
if (Number.isNaN(date.getTime())) {
|
||||||
return '';
|
return '';
|
||||||
|
|
|
@ -10,7 +10,6 @@ declare module 'yup' {
|
||||||
TFlags extends yup.Flags = ''
|
TFlags extends yup.Flags = ''
|
||||||
> extends yup.Schema<TType, TContext, TDefault, TFlags> {
|
> extends yup.Schema<TType, TContext, TDefault, TFlags> {
|
||||||
isJsonObject(message?: string): StringSchema<TType, TContext>;
|
isJsonObject(message?: string): StringSchema<TType, TContext>;
|
||||||
isEnum(message?: string): StringSchema<TType, TContext>;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,32 +39,6 @@ const isJsonObject = (message?: string) => {
|
||||||
isValidJsonObject
|
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,
|
* due to yup rerunning all the object validiation during any render,
|
||||||
* it makes sense to cache the async results
|
* 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, 'isJsonObject', isJsonObject);
|
||||||
yup.addMethod(yup.StringSchema, 'isEnum', isEnum);
|
|
||||||
|
|
||||||
export const topicFormValidationSchema = yup.object().shape({
|
export const topicFormValidationSchema = yup.object().shape({
|
||||||
name: yup
|
name: yup
|
||||||
|
|
|
@ -533,6 +533,12 @@ export const theme = {
|
||||||
active: Colors.neutral[90],
|
active: Colors.neutral[90],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
colored: {
|
||||||
|
color: {
|
||||||
|
attention: Colors.red[50],
|
||||||
|
warning: Colors.yellow[20],
|
||||||
|
},
|
||||||
|
},
|
||||||
expander: {
|
expander: {
|
||||||
normal: Colors.brand[30],
|
normal: Colors.brand[30],
|
||||||
hover: Colors.brand[40],
|
hover: Colors.brand[40],
|
||||||
|
@ -928,6 +934,12 @@ export const darkTheme: ThemeType = {
|
||||||
active: Colors.neutral[0],
|
active: Colors.neutral[0],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
colored: {
|
||||||
|
color: {
|
||||||
|
attention: Colors.red[50],
|
||||||
|
warning: Colors.yellow[20],
|
||||||
|
},
|
||||||
|
},
|
||||||
expander: {
|
expander: {
|
||||||
normal: Colors.brand[30],
|
normal: Colors.brand[30],
|
||||||
hover: Colors.brand[40],
|
hover: Colors.brand[40],
|
||||||
|
|
Loading…
Add table
Reference in a new issue