Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docker/latest/management/management.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ spring:
filter:
order: 2147483647

server:
port: 8090

management:
zookeeper:
clusters:
Expand Down
1 change: 1 addition & 0 deletions hermes-console/src/api/app-configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export interface OwnerSourceConfiguration {
export interface TopicViewConfiguration {
messagePreviewEnabled: boolean;
offlineClientsEnabled: boolean;
schemaRegistryUrl: string;
defaults: DefaultTopicViewConfiguration;
contentTypes: TopicContentType[];
readOnlyModeEnabled: boolean;
Expand Down
4 changes: 4 additions & 0 deletions hermes-console/src/api/topic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import type { OwnerId } from '@/api/owner-id';

export interface TopicWithSchema extends Topic {
schema: string;
// for JSON topic these values are null, for AVRO they are present
schemaVersion?: number;
availableSchemaVersions?: number[];
schemaSubject?: string;
}

export interface Topic {
Expand Down
1 change: 1 addition & 0 deletions hermes-console/src/dummy/app-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const dummyAppConfig: AppConfiguration = {
topic: {
messagePreviewEnabled: true,
offlineClientsEnabled: true,
schemaRegistryUrl: 'https://schema-registry.example.com',
defaults: {
ack: 'LEADER',
contentType: 'AVRO',
Expand Down
4 changes: 4 additions & 0 deletions hermes-console/src/dummy/topic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ export const dummyTopic: TopicWithSchema = {
},
createdAt: 1634916242.877,
modifiedAt: 1636451113.517,
schemaVersion: 2,
availableSchemaVersions: [2, 1],
schemaSubject: 'pl.allegro.public.group.DummyEvent-value',
schemaRegistryUrl: 'https://schema-registry.example.com',
};

export const dummyOwner: Owner = {
Expand Down
4 changes: 4 additions & 0 deletions hermes-console/src/i18n/en-US/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -503,9 +503,13 @@ const en_US = {
title: 'Offline clients',
},
schema: {
activeVersion: 'Active version:',
allVersions: 'All versions ({count})',
copy: 'Copy to clipboard',
current: 'Current',
default: 'Default',
rawSchema: 'Raw schema',
notApplicable: 'N/A (JSON topic)',
structure: 'Structure',
showRawSchema: 'Show raw schema',
title: 'Message schema',
Expand Down
13 changes: 12 additions & 1 deletion hermes-console/src/views/topic/TopicView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,18 @@

<v-tabs-window-item :value="Tab.Schema">
<v-container class="py-0">
<schema-panel v-if="topic" :schema="topic.schema" />
<schema-panel
v-if="topic && configStore.appConfig"
:schema="topic.schema"
:content-type="topic.contentType"
:topic-name="topic.name"
:schema-version="topic.schemaVersion"
:available-schema-versions="topic.availableSchemaVersions"
:schema-subject="topic.schemaSubject"
:schema-registry-url="
configStore.appConfig.topic.schemaRegistryUrl
"
/>
</v-container>
</v-tabs-window-item>

Expand Down
98 changes: 97 additions & 1 deletion hermes-console/src/views/topic/schema-panel/SchemaPanel.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { ContentType } from '@/api/content-type';
import { describe, expect } from 'vitest';
import { dummyTopic } from '@/dummy/topic';
import { render } from '@/utils/test-utils';
import SchemaPanel from '@/views/topic/schema-panel/SchemaPanel.vue';
import userEvent from '@testing-library/user-event';

describe('SchemaPanel', () => {
const props = { schema: dummyTopic.schema };
const props = {
schema: dummyTopic.schema,
contentType: ContentType.AVRO,
topicName: dummyTopic.name,
schemaRegistryUrl: 'https://schema-registry.example.com',
schemaSubject: dummyTopic.name,
};

it('should render avro formatted schema by default', async () => {
// given
Expand Down Expand Up @@ -45,4 +52,93 @@ describe('SchemaPanel', () => {
// then
expect(codeElement).toBeVisible();
});

it('should show sorted, linked schema version history and current version', async () => {
const { getByText, getByRole } = render(SchemaPanel, {
props: {
...props,
schemaVersion: 4,
availableSchemaVersions: [4, 2, 1],
schemaRegistryUrl: 'https://schema-registry.example.com/',
topicName: 'group/topic name',
schemaSubject: 'namespace_group/topic name-value',
},
});

expect(
getByText('topicView.schema.activeVersion', { exact: false }),
).toBeVisible();
expect(getByText('4', { selector: 'strong' })).toBeVisible();
expect(
getByRole('button', {
name: 'topicView.schema.allVersions',
}),
);
await userEvent.click(
getByText('topicView.schema.allVersions', { exact: false }),
);

const links = getByRole('list').querySelectorAll('a');
expect([...links].map((link) => link.textContent?.trim())).toEqual([
'4 topicView.schema.current',
'2',
'1',
]);
expect(links[1]).toHaveAttribute(
'href',
'https://schema-registry.example.com/subjects/namespace_group%2Ftopic%20name-value/versions/2',
);
expect(links[1]).toHaveAttribute('target', '_blank');
expect(links[1]).toHaveAttribute('rel', 'noopener noreferrer');
expect(getByText('topicView.schema.current')).toBeVisible();
});

it('should show only the active version when version history is not available', () => {
const { getByText, queryByText } = render(SchemaPanel, {
props: { ...props, schemaVersion: 2 },
});

expect(
getByText('topicView.schema.activeVersion', { exact: false }),
).toBeVisible();
expect(
queryByText('topicView.schema.allVersions', { exact: false }),
).not.toBeInTheDocument();
});

it('should show JSON topics as not applicable active version', () => {
const { getByText, queryByText } = render(SchemaPanel, {
props: {
...props,
contentType: ContentType.JSON,
},
});

expect(
getByText('topicView.schema.activeVersion', { exact: false }),
).toHaveTextContent('topicView.schema.notApplicable');
expect(
queryByText('topicView.schema.allVersions', { exact: false }),
).not.toBeInTheDocument();
});

it('should cap long version histories with scrolling', async () => {
const { getByText, getByTestId } = render(SchemaPanel, {
props: {
...props,
availableSchemaVersions: Array.from(
{ length: 11 },
(_, index) => index + 1,
),
},
});

await userEvent.click(
getByText('topicView.schema.allVersions', { exact: false }),
);

expect(getByTestId('schema-version-history')).toHaveClass(
'schema-version-history',
);
});
});
86 changes: 83 additions & 3 deletions hermes-console/src/views/topic/schema-panel/SchemaPanel.vue
Original file line number Diff line number Diff line change
@@ -1,18 +1,90 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { copyToClipboard } from '@/utils/copy-utils';
import { defineProps, ref } from 'vue';
import AvroViewer from '@/views/topic/schema-panel/avro-viewer/AvroViewer.vue';
import JsonViewer from '@/components/json-viewer/JsonViewer.vue';
import type { ContentType } from '@/api/content-type';

const props = defineProps<{
schema: string;
contentType: ContentType;
topicName: string;
schemaVersion?: number;
availableSchemaVersions?: number[];
schemaSubject?: string;
schemaRegistryUrl: string;
}>();
const showRawSchema = ref(false);

const sortedSchemaVersions = computed(() =>
[...(props.availableSchemaVersions ?? [])].sort(
(first, second) => second - first,
),
);
const shouldShowVersionHistory = computed(
() =>
props.contentType === 'AVRO' &&
props.schemaSubject !== undefined &&
sortedSchemaVersions.value.length > 0,
);

function schemaRegistryVersionUrl(version: number): string {
const baseUrl = props.schemaRegistryUrl.trim().replace(/\/+$/, '');
return `${baseUrl}/subjects/${encodeURIComponent(props.schemaSubject!)}/versions/${version}`;
}
</script>

<template>
<div>
<div class="d-flex justify-space-between mb-2">
<div class="pt-6">
<div class="mb-4" data-testid="schema-version-details">
<div class="d-flex align-center ga-2">
<div>
{{ $t('topicView.schema.activeVersion') }}
<template v-if="props.contentType === 'JSON'">
{{ $t('topicView.schema.notApplicable') }}
</template>
<template v-else-if="props.schemaVersion !== undefined">
<strong>{{ props.schemaVersion }}</strong>
</template>
</div>
<v-menu v-if="shouldShowVersionHistory" location="bottom start">
<template #activator="{ props: menuProps }">
<v-btn
v-bind="menuProps"
append-icon="mdi-chevron-down"
class="text-none"
variant="outlined"
>
{{
$t('topicView.schema.allVersions', {
count: sortedSchemaVersions.length,
})
}}
</v-btn>
</template>
<v-list
data-testid="schema-version-history"
class="schema-version-history"
>
<v-list-item
v-for="version in sortedSchemaVersions"
:key="version"
:href="schemaRegistryVersionUrl(version)"
target="_blank"
rel="noopener noreferrer"
>
<v-list-item-title>
{{ version }}
<span v-if="version === props.schemaVersion" class="ml-2">
{{ $t('topicView.schema.current') }}
</span>
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</div>
</div>
<div class="d-flex justify-space-between mt-6 mb-2">
<v-btn-toggle
v-model="showRawSchema"
group
Expand Down Expand Up @@ -51,3 +123,11 @@
</div>
</div>
</template>

<style scoped>
.schema-version-history {
max-height: 320px;
overflow-y: auto;
min-width: 160px;
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import pl.allegro.tech.hermes.management.domain.owner.OwnerSources;
import pl.allegro.tech.hermes.management.domain.topic.CreatorRights;
import pl.allegro.tech.hermes.management.domain.topic.SingleMessageReaderException;
import pl.allegro.tech.hermes.management.domain.topic.TopicDetailsWithSchemaDetails;
import pl.allegro.tech.hermes.management.domain.topic.TopicManagement;

@Component
Expand Down Expand Up @@ -145,7 +146,7 @@ public Response update(
@Produces(APPLICATION_JSON)
@Path("/{topicName}")
@ApiOperation(value = "Topic details", httpMethod = HttpMethod.GET)
public TopicWithSchema get(@PathParam("topicName") String qualifiedTopicName) {
public TopicDetailsWithSchemaDetails get(@PathParam("topicName") String qualifiedTopicName) {
return topicManagement.getTopicWithSchema(TopicName.fromQualifiedName(qualifiedTopicName));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import pl.allegro.tech.hermes.management.domain.topic.schema.SchemaService;
import pl.allegro.tech.hermes.management.domain.topic.validator.TopicValidator;
import pl.allegro.tech.hermes.management.infrastructure.kafka.MultiDCAwareService;
import pl.allegro.tech.hermes.schema.SubjectNamingStrategy;

@Configuration
@EnableConfigurationProperties(CacheProperties.class)
Expand All @@ -32,6 +33,7 @@ public TopicManagement topicManagement(
GroupService groupService,
TopicProperties topicProperties,
SchemaService schemaService,
SubjectNamingStrategy subjectNamingStrategy,
TopicMetricsRepository metricRepository,
TopicValidator topicValidator,
TopicContentTypeMigrationService topicContentTypeMigrationService,
Expand All @@ -48,6 +50,7 @@ public TopicManagement topicManagement(
groupService,
topicProperties,
schemaService,
subjectNamingStrategy,
metricRepository,
topicValidator,
topicContentTypeMigrationService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
@Configuration
@EnableConfigurationProperties(ConsoleProperties.class)
public class ConsoleConfiguration {

@Bean
FilterRegistrationBean<FrontendRoutesFilter> frontendRoutesFilter() {
FilterRegistrationBean<FrontendRoutesFilter> registrationBean = new FilterRegistrationBean<>();
Expand All @@ -30,7 +29,6 @@ ConsoleConfigurationRepository consoleConfigurationRepository(
ConsoleProperties consoleProperties,
GroupProperties groupProperties,
TopicProperties topicProperties) {

// Override group settings from GroupProperties (source of truth)
// Note: console.group.nonAdminCreationEnabled is IGNORED if configured in application.yaml
// See JavaDoc on ConsoleProperties.GroupView for details
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ public void setScope(String scope) {
public static final class TopicView {
private boolean messagePreviewEnabled = true;
private boolean offlineClientsEnabled = false;
private String schemaRegistryUrl = "http://localhost:8081";
private DefaultTopicView defaults = new DefaultTopicView();
private List<TopicContentType> contentTypes =
Lists.newArrayList(
Expand All @@ -320,6 +321,14 @@ public void setOfflineClientsEnabled(boolean offlineClientsEnabled) {
this.offlineClientsEnabled = offlineClientsEnabled;
}

public String getSchemaRegistryUrl() {
return schemaRegistryUrl;
}

public void setSchemaRegistryUrl(String schemaRegistryUrl) {
this.schemaRegistryUrl = schemaRegistryUrl;
}

public DefaultTopicView getDefaults() {
return defaults;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public Topic getTopicDetails(TopicName topicName) {
}

@Override
public TopicWithSchema getTopicWithSchema(TopicName topicName) {
public TopicDetailsWithSchemaDetails getTopicWithSchema(TopicName topicName) {
return LoggingContext.withLogging(
TOPIC_NAME, topicName.qualifiedName(), () -> delegate.getTopicWithSchema(topicName));
}
Expand Down
Loading
Loading