Merge branch 'master' into ISSUE_754_acl

This commit is contained in:
Ilya Kuramshin 2023-04-28 10:28:07 +04:00 committed by GitHub
commit 27fede1d15
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 170 additions and 293 deletions

View file

@ -123,11 +123,11 @@ public class ConsumerRecordDeserializer {
} }
private static Long getKeySize(ConsumerRecord<Bytes, Bytes> consumerRecord) { private static Long getKeySize(ConsumerRecord<Bytes, Bytes> consumerRecord) {
return consumerRecord.key() != null ? (long) consumerRecord.key().get().length : null; return consumerRecord.key() != null ? (long) consumerRecord.serializedKeySize() : null;
} }
private static Long getValueSize(ConsumerRecord<Bytes, Bytes> consumerRecord) { private static Long getValueSize(ConsumerRecord<Bytes, Bytes> consumerRecord) {
return consumerRecord.value() != null ? (long) consumerRecord.value().get().length : null; return consumerRecord.value() != null ? (long) consumerRecord.serializedValueSize() : null;
} }
private static int headerSize(Header header) { private static int headerSize(Header header) {

View file

@ -122,8 +122,6 @@ public class SerdesInitializer {
registeredSerdes, registeredSerdes,
Optional.ofNullable(clusterProperties.getDefaultKeySerde()) Optional.ofNullable(clusterProperties.getDefaultKeySerde())
.map(name -> Preconditions.checkNotNull(registeredSerdes.get(name), "Default key serde not found")) .map(name -> Preconditions.checkNotNull(registeredSerdes.get(name), "Default key serde not found"))
.or(() -> Optional.ofNullable(registeredSerdes.get(SchemaRegistrySerde.name())))
.or(() -> Optional.ofNullable(registeredSerdes.get(ProtobufFileSerde.name())))
.orElse(null), .orElse(null),
Optional.ofNullable(clusterProperties.getDefaultValueSerde()) Optional.ofNullable(clusterProperties.getDefaultValueSerde())
.map(name -> Preconditions.checkNotNull(registeredSerdes.get(name), "Default value serde not found")) .map(name -> Preconditions.checkNotNull(registeredSerdes.get(name), "Default value serde not found"))

View file

@ -109,6 +109,7 @@ public class KafkaConnectService {
private Stream<String> getStringsForSearch(FullConnectorInfoDTO fullConnectorInfo) { private Stream<String> getStringsForSearch(FullConnectorInfoDTO fullConnectorInfo) {
return Stream.of( return Stream.of(
fullConnectorInfo.getName(), fullConnectorInfo.getName(),
fullConnectorInfo.getConnect(),
fullConnectorInfo.getStatus().getState().getValue(), fullConnectorInfo.getStatus().getState().getValue(),
fullConnectorInfo.getType().getValue()); fullConnectorInfo.getType().getValue());
} }

View file

@ -43,8 +43,7 @@ class TopicAnalysisStats {
Long max; Long max;
final UpdateDoublesSketch sizeSketch = DoublesSketch.builder().build(); final UpdateDoublesSketch sizeSketch = DoublesSketch.builder().build();
void apply(byte[] bytes) { void apply(int len) {
int len = bytes.length;
sum += len; sum += len;
min = minNullable(min, len); min = minNullable(min, len);
max = maxNullable(max, len); max = maxNullable(max, len);
@ -98,7 +97,7 @@ class TopicAnalysisStats {
if (rec.key() != null) { if (rec.key() != null) {
byte[] keyBytes = rec.key().get(); byte[] keyBytes = rec.key().get();
keysSize.apply(keyBytes); keysSize.apply(rec.serializedKeySize());
uniqKeys.update(keyBytes); uniqKeys.update(keyBytes);
} else { } else {
nullKeys++; nullKeys++;
@ -106,7 +105,7 @@ class TopicAnalysisStats {
if (rec.value() != null) { if (rec.value() != null) {
byte[] valueBytes = rec.value().get(); byte[] valueBytes = rec.value().get();
valuesSize.apply(valueBytes); valuesSize.apply(rec.serializedValueSize());
uniqValues.update(valueBytes); uniqValues.update(valueBytes);
} else { } else {
nullValues++; nullValues++;

View file

@ -142,6 +142,8 @@ const Message: React.FC<Props> = ({
timestampType={timestampType} timestampType={timestampType}
keySize={keySize} keySize={keySize}
contentSize={valueSize} contentSize={valueSize}
keySerde={keySerde}
valueSerde={valueSerde}
/> />
)} )}
</> </>

View file

@ -3,7 +3,6 @@ import EditorViewer from 'components/common/EditorViewer/EditorViewer';
import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted'; import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';
import { SchemaType, TopicMessageTimestampTypeEnum } from 'generated-sources'; import { SchemaType, TopicMessageTimestampTypeEnum } from 'generated-sources';
import { formatTimestamp } from 'lib/dateTimeHelpers'; import { formatTimestamp } from 'lib/dateTimeHelpers';
import { useSearchParams } from 'react-router-dom';
import * as S from './MessageContent.styled'; import * as S from './MessageContent.styled';
@ -17,6 +16,8 @@ export interface MessageContentProps {
timestampType?: TopicMessageTimestampTypeEnum; timestampType?: TopicMessageTimestampTypeEnum;
keySize?: number; keySize?: number;
contentSize?: number; contentSize?: number;
keySerde?: string;
valueSerde?: string;
} }
const MessageContent: React.FC<MessageContentProps> = ({ const MessageContent: React.FC<MessageContentProps> = ({
@ -27,12 +28,10 @@ const MessageContent: React.FC<MessageContentProps> = ({
timestampType, timestampType,
keySize, keySize,
contentSize, contentSize,
keySerde,
valueSerde,
}) => { }) => {
const [activeTab, setActiveTab] = React.useState<Tab>('content'); const [activeTab, setActiveTab] = React.useState<Tab>('content');
const [searchParams] = useSearchParams();
const keyFormat = searchParams.get('keySerde') || '';
const valueFormat = searchParams.get('valueSerde') || '';
const activeTabContent = () => { const activeTabContent = () => {
switch (activeTab) { switch (activeTab) {
case 'content': case 'content':
@ -110,7 +109,7 @@ const MessageContent: React.FC<MessageContentProps> = ({
<S.Metadata> <S.Metadata>
<S.MetadataLabel>Key Serde</S.MetadataLabel> <S.MetadataLabel>Key Serde</S.MetadataLabel>
<span> <span>
<S.MetadataValue>{keyFormat}</S.MetadataValue> <S.MetadataValue>{keySerde}</S.MetadataValue>
<S.MetadataMeta> <S.MetadataMeta>
Size: <BytesFormatted value={keySize} /> Size: <BytesFormatted value={keySize} />
</S.MetadataMeta> </S.MetadataMeta>
@ -120,7 +119,7 @@ const MessageContent: React.FC<MessageContentProps> = ({
<S.Metadata> <S.Metadata>
<S.MetadataLabel>Value Serde</S.MetadataLabel> <S.MetadataLabel>Value Serde</S.MetadataLabel>
<span> <span>
<S.MetadataValue>{valueFormat}</S.MetadataValue> <S.MetadataValue>{valueSerde}</S.MetadataValue>
<S.MetadataMeta> <S.MetadataMeta>
Size: <BytesFormatted value={contentSize} /> Size: <BytesFormatted value={contentSize} />
</S.MetadataMeta> </S.MetadataMeta>

View file

@ -20,6 +20,8 @@ const setupWrapper = (props?: Partial<MessageContentProps>) => {
headers={{ header: 'test' }} headers={{ header: 'test' }}
timestamp={new Date(0)} timestamp={new Date(0)}
timestampType={TopicMessageTimestampTypeEnum.CREATE_TIME} timestampType={TopicMessageTimestampTypeEnum.CREATE_TIME}
keySerde="SchemaRegistry"
valueSerde="Avro"
{...props} {...props}
/> />
</tbody> </tbody>
@ -27,42 +29,20 @@ const setupWrapper = (props?: Partial<MessageContentProps>) => {
); );
}; };
const proto =
'syntax = "proto3";\npackage com.provectus;\n\nmessage TestProtoRecord {\n string f1 = 1;\n int32 f2 = 2;\n}\n';
global.TextEncoder = TextEncoder; global.TextEncoder = TextEncoder;
const searchParamsContentAVRO = new URLSearchParams({
keySerde: 'SchemaRegistry',
valueSerde: 'AVRO',
limit: '100',
});
const searchParamsContentJSON = new URLSearchParams({
keySerde: 'SchemaRegistry',
valueSerde: 'JSON',
limit: '100',
});
const searchParamsContentPROTOBUF = new URLSearchParams({
keySerde: 'SchemaRegistry',
valueSerde: 'PROTOBUF',
limit: '100',
});
describe('MessageContent screen', () => { describe('MessageContent screen', () => {
beforeEach(() => { beforeEach(() => {
render(setupWrapper(), { render(setupWrapper());
initialEntries: [`/messages?${searchParamsContentAVRO}`],
});
}); });
describe('renders', () => { describe('Checking keySerde and valueSerde', () => {
it('key format in document', () => { it('keySerde in document', () => {
expect(screen.getByText('SchemaRegistry')).toBeInTheDocument(); expect(screen.getByText('SchemaRegistry')).toBeInTheDocument();
}); });
it('content format in document', () => { it('valueSerde in document', () => {
expect(screen.getByText('AVRO')).toBeInTheDocument(); expect(screen.getByText('Avro')).toBeInTheDocument();
}); });
}); });
@ -98,42 +78,3 @@ describe('MessageContent screen', () => {
}); });
}); });
}); });
describe('checking content type depend on message type', () => {
it('renders component with message having JSON type', () => {
render(
setupWrapper({
messageContent: '{"data": "test"}',
}),
{ initialEntries: [`/messages?${searchParamsContentJSON}`] }
);
expect(screen.getByText('JSON')).toBeInTheDocument();
});
it('renders component with message having AVRO type', () => {
render(
setupWrapper({
messageContent: '{"data": "test"}',
}),
{ initialEntries: [`/messages?${searchParamsContentAVRO}`] }
);
expect(screen.getByText('AVRO')).toBeInTheDocument();
});
it('renders component with message having PROTOBUF type', () => {
render(
setupWrapper({
messageContent: proto,
}),
{ initialEntries: [`/messages?${searchParamsContentPROTOBUF}`] }
);
expect(screen.getByText('PROTOBUF')).toBeInTheDocument();
});
it('renders component with message having no type which is equal to having PROTOBUF type', () => {
render(
setupWrapper({
messageContent: '',
}),
{ initialEntries: [`/messages?${searchParamsContentPROTOBUF}`] }
);
expect(screen.getByText('PROTOBUF')).toBeInTheDocument();
});
});

View file

@ -8,15 +8,29 @@ export const Wrapper = styled.div`
export const Columns = styled.div` export const Columns = styled.div`
margin: -0.75rem; margin: -0.75rem;
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
display: flex;
flex-direction: column;
padding: 0.75rem;
gap: 8px;
@media screen and (min-width: 769px) { @media screen and (min-width: 769px) {
display: flex; display: flex;
} }
`; `;
export const Flex = styled.div`
export const Column = styled.div` display: flex;
flex-basis: 0; flex-direction: row;
flex-grow: 1; gap: 8px;
flex-shrink: 1; @media screen and (max-width: 1200px) {
padding: 0.75rem; flex-direction: column;
}
`;
export const FlexItem = styled.div`
width: 18rem;
@media screen and (max-width: 1450px) {
width: 50%;
}
@media screen and (max-width: 1200px) {
width: 100%;
}
`; `;

View file

@ -4,6 +4,7 @@ import { RouteParamsClusterTopic } from 'lib/paths';
import { Button } from 'components/common/Button/Button'; import { Button } from 'components/common/Button/Button';
import Editor from 'components/common/Editor/Editor'; import Editor from 'components/common/Editor/Editor';
import Select, { SelectOption } from 'components/common/Select/Select'; import Select, { SelectOption } from 'components/common/Select/Select';
import Switch from 'components/common/Switch/Switch';
import useAppParams from 'lib/hooks/useAppParams'; import useAppParams from 'lib/hooks/useAppParams';
import { showAlert } from 'lib/errorHandling'; import { showAlert } from 'lib/errorHandling';
import { useSendMessage, useTopicDetails } from 'lib/hooks/api/topics'; import { useSendMessage, useTopicDetails } from 'lib/hooks/api/topics';
@ -26,9 +27,12 @@ interface FormType {
partition: number; partition: number;
keySerde: string; keySerde: string;
valueSerde: string; valueSerde: string;
keepContents: boolean;
} }
const SendMessage: React.FC<{ onSubmit: () => void }> = ({ onSubmit }) => { const SendMessage: React.FC<{ closeSidebar: () => void }> = ({
closeSidebar,
}) => {
const { clusterName, topicName } = useAppParams<RouteParamsClusterTopic>(); const { clusterName, topicName } = useAppParams<RouteParamsClusterTopic>();
const { data: topic } = useTopicDetails({ clusterName, topicName }); const { data: topic } = useTopicDetails({ clusterName, topicName });
const { data: serdes = {} } = useSerdes({ const { data: serdes = {} } = useSerdes({
@ -47,11 +51,13 @@ const SendMessage: React.FC<{ onSubmit: () => void }> = ({ onSubmit }) => {
handleSubmit, handleSubmit,
formState: { isSubmitting }, formState: { isSubmitting },
control, control,
setValue,
} = useForm<FormType>({ } = useForm<FormType>({
mode: 'onChange', mode: 'onChange',
defaultValues: { defaultValues: {
...defaultValues, ...defaultValues,
partition: Number(partitionOptions[0].value), partition: Number(partitionOptions[0].value),
keepContents: false,
}, },
}); });
@ -62,6 +68,7 @@ const SendMessage: React.FC<{ onSubmit: () => void }> = ({ onSubmit }) => {
content, content,
headers, headers,
partition, partition,
keepContents,
}: FormType) => { }: FormType) => {
let errors: string[] = []; let errors: string[] = [];
@ -110,7 +117,11 @@ const SendMessage: React.FC<{ onSubmit: () => void }> = ({ onSubmit }) => {
keySerde, keySerde,
valueSerde, valueSerde,
}); });
onSubmit(); if (!keepContents) {
setValue('key', '');
setValue('content', '');
closeSidebar();
}
} catch (e) { } catch (e) {
// do nothing // do nothing
} }
@ -120,7 +131,7 @@ const SendMessage: React.FC<{ onSubmit: () => void }> = ({ onSubmit }) => {
<S.Wrapper> <S.Wrapper>
<form onSubmit={handleSubmit(submit)}> <form onSubmit={handleSubmit(submit)}>
<S.Columns> <S.Columns>
<S.Column> <S.FlexItem>
<InputLabel>Partition</InputLabel> <InputLabel>Partition</InputLabel>
<Controller <Controller
control={control} control={control}
@ -137,47 +148,58 @@ const SendMessage: React.FC<{ onSubmit: () => void }> = ({ onSubmit }) => {
/> />
)} )}
/> />
</S.Column> </S.FlexItem>
<S.Column> <S.Flex>
<InputLabel>Key Serde</InputLabel> <S.FlexItem>
<InputLabel>Key Serde</InputLabel>
<Controller
control={control}
name="keySerde"
render={({ field: { name, onChange, value } }) => (
<Select
id="selectKeySerdeOptions"
aria-labelledby="selectKeySerdeOptions"
name={name}
onChange={onChange}
minWidth="100%"
options={getSerdeOptions(serdes.key || [])}
value={value}
/>
)}
/>
</S.FlexItem>
<S.FlexItem>
<InputLabel>Value Serde</InputLabel>
<Controller
control={control}
name="valueSerde"
render={({ field: { name, onChange, value } }) => (
<Select
id="selectValueSerdeOptions"
aria-labelledby="selectValueSerdeOptions"
name={name}
onChange={onChange}
minWidth="100%"
options={getSerdeOptions(serdes.value || [])}
value={value}
/>
)}
/>
</S.FlexItem>
</S.Flex>
<div>
<Controller <Controller
control={control} control={control}
name="keySerde" name="keepContents"
render={({ field: { name, onChange, value } }) => ( render={({ field: { name, onChange, value } }) => (
<Select <Switch name={name} onChange={onChange} checked={value} />
id="selectKeySerdeOptions"
aria-labelledby="selectKeySerdeOptions"
name={name}
onChange={onChange}
minWidth="100%"
options={getSerdeOptions(serdes.key || [])}
value={value}
/>
)} )}
/> />
</S.Column> <InputLabel>Keep contents</InputLabel>
<S.Column> </div>
<InputLabel>Value Serde</InputLabel>
<Controller
control={control}
name="valueSerde"
render={({ field: { name, onChange, value } }) => (
<Select
id="selectValueSerdeOptions"
aria-labelledby="selectValueSerdeOptions"
name={name}
onChange={onChange}
minWidth="100%"
options={getSerdeOptions(serdes.value || [])}
value={value}
/>
)}
/>
</S.Column>
</S.Columns> </S.Columns>
<S.Columns> <S.Columns>
<S.Column> <div>
<InputLabel>Key</InputLabel> <InputLabel>Key</InputLabel>
<Controller <Controller
control={control} control={control}
@ -191,8 +213,8 @@ const SendMessage: React.FC<{ onSubmit: () => void }> = ({ onSubmit }) => {
/> />
)} )}
/> />
</S.Column> </div>
<S.Column> <div>
<InputLabel>Value</InputLabel> <InputLabel>Value</InputLabel>
<Controller <Controller
control={control} control={control}
@ -206,10 +228,10 @@ const SendMessage: React.FC<{ onSubmit: () => void }> = ({ onSubmit }) => {
/> />
)} )}
/> />
</S.Column> </div>
</S.Columns> </S.Columns>
<S.Columns> <S.Columns>
<S.Column> <div>
<InputLabel>Headers</InputLabel> <InputLabel>Headers</InputLabel>
<Controller <Controller
control={control} control={control}
@ -224,7 +246,7 @@ const SendMessage: React.FC<{ onSubmit: () => void }> = ({ onSubmit }) => {
/> />
)} )}
/> />
</S.Column> </div>
</S.Columns> </S.Columns>
<Button <Button
buttonSize="M" buttonSize="M"

View file

@ -49,7 +49,7 @@ const renderComponent = async () => {
const path = clusterTopicPath(clusterName, topicName); const path = clusterTopicPath(clusterName, topicName);
await render( await render(
<WithRoute path={clusterTopicPath()}> <WithRoute path={clusterTopicPath()}>
<SendMessage onSubmit={mockOnSubmit} /> <SendMessage closeSidebar={mockOnSubmit} />
</WithRoute>, </WithRoute>,
{ initialEntries: [path] } { initialEntries: [path] }
); );

View file

@ -236,7 +236,7 @@ const Topic: React.FC = () => {
title="Produce Message" title="Produce Message"
> >
<Suspense fallback={<PageLoader />}> <Suspense fallback={<PageLoader />}>
<SendMessage onSubmit={closeSidebar} /> <SendMessage closeSidebar={closeSidebar} />
</Suspense> </Suspense>
</SlidingSidebar> </SlidingSidebar>
</> </>

View file

@ -1,52 +1,38 @@
import React from 'react'; import React from 'react';
import WarningIcon from 'components/common/Icons/WarningIcon'; import WarningIcon from 'components/common/Icons/WarningIcon';
import { gitCommitPath } from 'lib/paths'; import { gitCommitPath } from 'lib/paths';
import { useActuatorInfo } from 'lib/hooks/api/actuatorInfo';
import { BUILD_VERSION_PATTERN } from 'lib/constants';
import { useLatestVersion } from 'lib/hooks/api/latestVersion'; import { useLatestVersion } from 'lib/hooks/api/latestVersion';
import { formatTimestamp } from 'lib/dateTimeHelpers'; import { formatTimestamp } from 'lib/dateTimeHelpers';
import * as S from './Version.styled'; import * as S from './Version.styled';
import compareVersions from './compareVersions';
const Version: React.FC = () => { const Version: React.FC = () => {
const { data: actuatorInfo = {} } = useActuatorInfo();
const { data: latestVersionInfo = {} } = useLatestVersion(); const { data: latestVersionInfo = {} } = useLatestVersion();
const { buildTime, commitId, isLatestRelease } = latestVersionInfo.build;
const tag = actuatorInfo?.build?.version; const { versionTag } = latestVersionInfo?.latestRelease || '';
const commit = actuatorInfo?.git?.commit.id;
const { tag_name: latestTag } = latestVersionInfo;
const outdated = compareVersions(tag, latestTag);
const currentVersion = tag?.match(BUILD_VERSION_PATTERN)
? tag
: formatTimestamp(actuatorInfo?.build?.time);
if (!tag) return null;
return ( return (
<S.Wrapper> <S.Wrapper>
{!!outdated && ( {!isLatestRelease && (
<S.OutdatedWarning <S.OutdatedWarning
title={`Your app version is outdated. Current latest version is ${latestTag}`} title={`Your app version is outdated. Current latest version is ${versionTag}`}
> >
<WarningIcon /> <WarningIcon />
</S.OutdatedWarning> </S.OutdatedWarning>
)} )}
{commit && ( {commitId && (
<div> <div>
<S.CurrentCommitLink <S.CurrentCommitLink
title="Current commit" title="Current commit"
target="__blank" target="__blank"
href={gitCommitPath(commit)} href={gitCommitPath(commitId)}
> >
{commit} {commitId}
</S.CurrentCommitLink> </S.CurrentCommitLink>
</div> </div>
)} )}
<S.CurrentVersion>{currentVersion}</S.CurrentVersion> <S.CurrentVersion>{formatTimestamp(buildTime)}</S.CurrentVersion>
</S.Wrapper> </S.Wrapper>
); );
}; };

View file

@ -2,87 +2,40 @@ import React from 'react';
import { screen } from '@testing-library/dom'; import { screen } from '@testing-library/dom';
import Version from 'components/Version/Version'; import Version from 'components/Version/Version';
import { render } from 'lib/testHelpers'; import { render } from 'lib/testHelpers';
import { formatTimestamp } from 'lib/dateTimeHelpers';
import { useActuatorInfo } from 'lib/hooks/api/actuatorInfo';
import { useLatestVersion } from 'lib/hooks/api/latestVersion'; import { useLatestVersion } from 'lib/hooks/api/latestVersion';
import { actuatorInfoPayload } from 'lib/fixtures/actuatorInfo'; import {
import { latestVersionPayload } from 'lib/fixtures/latestVersion'; deprecatedVersionPayload,
latestVersionPayload,
} from 'lib/fixtures/latestVersion';
jest.mock('lib/hooks/api/actuatorInfo', () => ({
useActuatorInfo: jest.fn(),
}));
jest.mock('lib/hooks/api/latestVersion', () => ({ jest.mock('lib/hooks/api/latestVersion', () => ({
useLatestVersion: jest.fn(), useLatestVersion: jest.fn(),
})); }));
describe('Version Component', () => { describe('Version Component', () => {
const versionTag = 'v0.5.0'; const commitId = '96a577a';
const snapshotTag = 'test-SNAPSHOT';
const commitTag = 'befd3b328e2c9c7df57b0c5746561b2f7fee8813';
const actuatorVersionPayload = actuatorInfoPayload(versionTag); describe('render latest version', () => {
const formattedTimestamp = formatTimestamp(actuatorVersionPayload.build.time); beforeEach(() => {
(useLatestVersion as jest.Mock).mockImplementation(() => ({
data: latestVersionPayload,
}));
});
it('renders latest release version as current version', async () => {
render(<Version />);
expect(screen.getByText(commitId)).toBeInTheDocument();
});
beforeEach(() => { it('should not show warning icon if it is last release', async () => {
(useActuatorInfo as jest.Mock).mockImplementation(() => ({ render(<Version />);
data: actuatorVersionPayload, expect(screen.queryByRole('img')).not.toBeInTheDocument();
})); });
});
it('show warning icon if it is not last release', async () => {
(useLatestVersion as jest.Mock).mockImplementation(() => ({ (useLatestVersion as jest.Mock).mockImplementation(() => ({
data: latestVersionPayload, data: deprecatedVersionPayload,
})); }));
}); render(<Version />);
expect(screen.getByRole('img')).toBeInTheDocument();
describe('tag does not exist', () => {
it('does not render component', async () => {
(useActuatorInfo as jest.Mock).mockImplementation(() => ({
data: null,
}));
const { container } = render(<Version />);
expect(container.firstChild).toBeEmptyDOMElement();
});
});
describe('renders current version', () => {
it('renders release build version as current version', async () => {
render(<Version />);
expect(screen.getByText(versionTag)).toBeInTheDocument();
});
it('renders formatted timestamp as current version when version is commit', async () => {
(useActuatorInfo as jest.Mock).mockImplementation(() => ({
data: actuatorInfoPayload(commitTag),
}));
render(<Version />);
expect(screen.getByText(formattedTimestamp)).toBeInTheDocument();
});
it('renders formatted timestamp as current version when version contains -SNAPSHOT', async () => {
(useActuatorInfo as jest.Mock).mockImplementation(() => ({
data: actuatorInfoPayload(snapshotTag),
}));
render(<Version />);
expect(screen.getByText(formattedTimestamp)).toBeInTheDocument();
});
});
describe('outdated build version', () => {
it('renders warning message', async () => {
(useActuatorInfo as jest.Mock).mockImplementation(() => ({
data: actuatorInfoPayload('v0.3.0'),
}));
render(<Version />);
expect(
screen.getByTitle(
`Your app version is outdated. Current latest version is ${latestVersionPayload.tag_name}`
)
).toBeInTheDocument();
});
});
describe('current commit id with link', () => {
it('renders', async () => {
render(<Version />);
expect(
screen.getByText(actuatorVersionPayload.git.commit.id)
).toBeInTheDocument();
});
}); });
}); });

View file

@ -13,6 +13,7 @@ const WarningIcon: React.FC = () => {
return ( return (
<WarningIconContainer> <WarningIconContainer>
<svg <svg
role="img"
width="14" width="14"
height="13" height="13"
viewBox="0 0 14 13" viewBox="0 0 14 13"

View file

@ -6,7 +6,7 @@ export const Wrapper = styled.div<{ $open?: boolean }>(
position: fixed; position: fixed;
top: ${theme.layout.navBarHeight}; top: ${theme.layout.navBarHeight};
bottom: 0; bottom: 0;
width: 60vw; width: 37vw;
right: calc(${$open ? '0px' : theme.layout.rightSidebarWidth} * -1); right: calc(${$open ? '0px' : theme.layout.rightSidebarWidth} * -1);
box-shadow: -1px 0px 10px 0px rgba(0, 0, 0, 0.2); box-shadow: -1px 0px 10px 0px rgba(0, 0, 0, 0.2);
transition: right 0.3s linear; transition: right 0.3s linear;

View file

@ -1,12 +0,0 @@
export const actuatorInfoPayload = (
version = 'befd3b328e2c9c7df57b0c5746561b2f7fee8813'
) => ({
git: { commit: { id: 'befd3b3' } },
build: {
artifact: 'kafka-ui-api',
name: 'kafka-ui-api',
time: '2022-09-15T09:52:21.753Z',
version,
group: 'com.provectus',
},
});

View file

@ -1,3 +1,16 @@
export const latestVersionPayload = { export const deprecatedVersionPayload = {
tag_name: 'v0.4.0', build: {
buildTime: '2023-04-14T09:47:35.463Z',
commitId: '96a577a',
isLatestRelease: false,
version: '96a577a98c6069376c5d22ed49cffd3739f1bbdc',
},
};
export const latestVersionPayload = {
build: {
buildTime: '2023-04-14T09:47:35.463Z',
commitId: '96a577a',
isLatestRelease: true,
version: '96a577a98c6069376c5d22ed49cffd3739f1bbdc',
},
}; };

View file

@ -1,17 +0,0 @@
import fetchMock from 'fetch-mock';
import * as hooks from 'lib/hooks/api/actuatorInfo';
import { expectQueryWorks, renderQueryHook } from 'lib/testHelpers';
import { actuatorInfoPayload } from 'lib/fixtures/actuatorInfo';
const actuatorInfoPath = '/actuator/info';
describe('Actuator info hooks', () => {
beforeEach(() => fetchMock.restore());
describe('useActuatorInfo', () => {
it('returns the correct data', async () => {
const mock = fetchMock.getOnce(actuatorInfoPath, actuatorInfoPayload());
const { result } = renderQueryHook(() => hooks.useActuatorInfo());
await expectQueryWorks(mock, result);
});
});
});

View file

@ -1,18 +1,16 @@
import fetchMock from 'fetch-mock'; import fetchMock from 'fetch-mock';
import { expectQueryWorks, renderQueryHook } from 'lib/testHelpers'; import { expectQueryWorks, renderQueryHook } from 'lib/testHelpers';
import * as hooks from 'lib/hooks/api/latestVersion';
import { GIT_REPO_LATEST_RELEASE_LINK } from 'lib/constants';
import { latestVersionPayload } from 'lib/fixtures/latestVersion'; import { latestVersionPayload } from 'lib/fixtures/latestVersion';
import { useLatestVersion } from 'lib/hooks/api/latestVersion';
const latestVersionPath = '/api/info';
describe('Latest version hooks', () => { describe('Latest version hooks', () => {
beforeEach(() => fetchMock.restore()); beforeEach(() => fetchMock.restore());
describe('useLatestVersion', () => { describe('useLatestVersion', () => {
it('returns the correct data', async () => { it('returns the correct data', async () => {
const mock = fetchMock.getOnce( const mock = fetchMock.getOnce(latestVersionPath, latestVersionPayload);
GIT_REPO_LATEST_RELEASE_LINK, const { result } = renderQueryHook(() => useLatestVersion());
latestVersionPayload
);
const { result } = renderQueryHook(() => hooks.useLatestVersion());
await expectQueryWorks(mock, result); await expectQueryWorks(mock, result);
}); });
}); });

View file

@ -1,19 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import { BASE_PARAMS, QUERY_REFETCH_OFF_OPTIONS } from 'lib/constants';
const fetchActuatorInfo = async () => {
const data = await fetch(
`${BASE_PARAMS.basePath}/actuator/info`,
BASE_PARAMS
).then((res) => res.json());
return data;
};
export function useActuatorInfo() {
return useQuery(
['actuatorInfo'],
fetchActuatorInfo,
QUERY_REFETCH_OFF_OPTIONS
);
}

View file

@ -1,21 +1,19 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { import { BASE_PARAMS, QUERY_REFETCH_OFF_OPTIONS } from 'lib/constants';
QUERY_REFETCH_OFF_OPTIONS,
GIT_REPO_LATEST_RELEASE_LINK,
} from 'lib/constants';
const fetchLatestVersion = async () => { const fetchLatestVersionInfo = async () => {
const data = await fetch(GIT_REPO_LATEST_RELEASE_LINK).then((res) => const data = await fetch(
res.json() `${BASE_PARAMS.basePath}/api/info`,
); BASE_PARAMS
).then((res) => res.json());
return data; return data;
}; };
export function useLatestVersion() { export function useLatestVersion() {
return useQuery( return useQuery(
['latestVersion'], ['versionInfo'],
fetchLatestVersion, fetchLatestVersionInfo,
QUERY_REFETCH_OFF_OPTIONS QUERY_REFETCH_OFF_OPTIONS
); );
} }