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
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import java.util.Set;
import java.util.stream.Collectors;

import org.jboss.logging.Logger;

import io.apicurio.registry.resolver.ParsedSchema;
import io.apicurio.registry.resolver.SchemaParser;
import io.apicurio.registry.resolver.data.Record;
Expand All @@ -13,6 +15,7 @@
*/
public class MultiformatSchemaParser<D> implements SchemaParser<Object, D> {

private static final Logger LOGGER = Logger.getLogger(MultiformatSchemaParser.class);
private final Map<String, SchemaParser<Object, ?>> delegates;

public MultiformatSchemaParser(Set<SchemaParser<Object, ?>> delegates) {
Expand Down Expand Up @@ -41,6 +44,8 @@ public Object parseSchema(byte[] rawSchema, Map<String, ParsedSchema<Object>> re
return delegate.parseSchema(rawSchema, resolvedReferences);
} catch (Exception e) {
// Schema is not valid for the delegate parser
LOGGER.debugf("Schema could not be parsed as artifact type %s: %s",
delegate.artifactType(), e.getLocalizedMessage());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,9 +205,20 @@ SchemaLookupResult<Object> resolveSchema(String topic, Headers headers, RecordDa
try {
schema = getSchemaResolver().resolveSchemaByArtifactReference(reference);
} catch (Exception e) {
LOGGER.warnf("Exception retrieving schema: %s", RootCause.of(e)
.map(Throwable::getMessage)
.orElseGet(() -> String.valueOf(e)));
Throwable cause = RootCause.of(e).orElse(e);

if (cause instanceof io.apicurio.registry.rest.client.v2.models.Error clientError) {
LOGGER.infof("Schema could not be resolved: %s. Message: %s", reference, clientError.getMessageEscaped());
} else if (LOGGER.isDebugEnabled()) {
/*
* Only log the stack trace at debug level. Schema resolution will be attempted
* for every message consumed and will lead to excessive logging in case of a
* problem.
*/
LOGGER.debugf(e, "Exception resolving schema reference: %s", reference);
} else {
LOGGER.warnf("Exception resolving schema reference: %s ; %s", reference, e.getMessage());
}
schema = EMPTY_RESULT;
}
}
Expand Down
23 changes: 23 additions & 0 deletions api/src/main/webui/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,29 @@ class ApiClient {
return data;
}

/**
* GET request returning plain text (e.g. schema content)
*/
async getText(path: string, options?: RequestInit): Promise<string> {
const url = `${this.baseUrl}${path}`;
const response = await fetch(url, {
...options,
method: 'GET',
headers: {
'X-Requested-With': 'JavaScript',
...options?.headers,
},
});
if (response.status === 499 && response.headers.get('WWW-Authenticate') === 'OIDC') {
this.login();
return '';
}
if (!response.ok) {
throw new ApiError(response.status, response.statusText);
}
return response.text();
}

/**
* GET request
*/
Expand Down
19 changes: 19 additions & 0 deletions api/src/main/webui/src/api/hooks/useSchemaContent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Hook for fetching schema content from a schema registry link.
*
* The `links.content` field on a RelatedSchema is an absolute path from origin
* (e.g. `/api/registries/{registryId}/schemas/{schemaId}`).
* The response is plain text (the raw schema definition).
*/

import { useQuery } from '@tanstack/react-query';
import { apiClient } from '../client';

export function useSchemaContent(contentUrl: string | null | undefined) {
return useQuery({
queryKey: ['schemaContent', contentUrl],
queryFn: () => apiClient.getText(contentUrl!),
enabled: !!contentUrl,
staleTime: 5 * 60 * 1000,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function KafkaClusterSidebar() {
{
id: 'connect',
label: t('kafka.connect.title'),
path: `/kafka/${kafkaId}/connect/connectors`,
path: `/kafka/${kafkaId}/connect`,
},
{
id: 'users',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,17 @@ import {
TabTitleText,
ClipboardCopy,
Title,
Tooltip,
CodeBlock,
CodeBlockCode,
} from '@patternfly/react-core';
import { HelpIcon } from '@patternfly/react-icons';
import { allExpanded, darkStyles, defaultStyles, JsonView } from 'react-json-view-lite';
import 'react-json-view-lite/dist/index.css';
import { KafkaRecord } from '@/api/types';
import { formatDateTime } from '@/utils/dateTime';
import { useTheme } from '@/components/app/ThemeProvider';
import { useSchemaContent } from '@/api/hooks/useSchemaContent';

interface MessageDetailsProps {
message: KafkaRecord;
Expand Down Expand Up @@ -78,6 +83,11 @@ export function MessageDetails({ message }: MessageDetailsProps) {
const valueJson = maybeJson(message.attributes.value);
const headers = Object.entries(message.attributes.headers ?? {});

const keySchemaContentUrl = message.relationships.keySchema?.links?.content;
const valueSchemaContentUrl = message.relationships.valueSchema?.links?.content;
const { data: keySchemaContent } = useSchemaContent(keySchemaContentUrl);
const { data: valueSchemaContent } = useSchemaContent(valueSchemaContentUrl);

return (
<DrawerPanelBody>
<DescriptionList isHorizontal isCompact>
Expand All @@ -96,7 +106,12 @@ export function MessageDetails({ message }: MessageDetailsProps) {
</DescriptionListGroup>

<DescriptionListGroup>
<DescriptionListTerm>{t('topics.messages.field.size')}</DescriptionListTerm>
<DescriptionListTerm>
{t('topics.messages.field.size')}{' '}
<Tooltip content={t('topics.messages.tooltip.size')}>
<HelpIcon />
</Tooltip>
</DescriptionListTerm>
<DescriptionListDescription>
{formatBytes(message.attributes.size)}
</DescriptionListDescription>
Expand Down Expand Up @@ -167,6 +182,11 @@ export function MessageDetails({ message }: MessageDetailsProps) {
{t('topics.messages.schema')}
</Title>
<p>{message.relationships.valueSchema.meta.name}</p>
{valueSchemaContent && (
<CodeBlock style={{ marginTop: '0.5rem' }}>
<CodeBlockCode>{valueSchemaContent}</CodeBlockCode>
</CodeBlock>
)}
</div>
)}
</div>
Expand Down Expand Up @@ -196,6 +216,11 @@ export function MessageDetails({ message }: MessageDetailsProps) {
{t('topics.messages.schema')}
</Title>
<p>{message.relationships.keySchema.meta.name}</p>
{keySchemaContent && (
<CodeBlock style={{ marginTop: '0.5rem' }}>
<CodeBlockCode>{keySchemaContent}</CodeBlockCode>
</CodeBlock>
)}
</div>
)}
</div>
Expand Down
46 changes: 42 additions & 4 deletions api/src/main/webui/src/components/kafka/topics/MessagesTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import {
Button,
Title,
Content,
Tooltip,
} from '@patternfly/react-core';
import { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table';
import { SearchIcon } from '@patternfly/react-icons';
import { ExclamationTriangleIcon, HelpIcon, SearchIcon } from '@patternfly/react-icons';
import { KafkaRecord } from '@/api/types';
import { Column, useColumnLabels } from './ColumnsModal';
import { formatDateTime } from '@/utils/dateTime';
Expand Down Expand Up @@ -129,11 +130,39 @@ export function MessagesTable({
case 'timestamp':
return formatTimestampLocal(message.attributes.timestamp);
case 'key':
return truncate(message.attributes.key);
return (
<>
{truncate(message.attributes.key)}
{message.relationships.keySchema && (
<Content>
<Content component="small">
{message.relationships.keySchema.meta?.name}
{message.relationships.keySchema.meta?.errors && (
<> <ExclamationTriangleIcon /> {message.relationships.keySchema.meta.errors[0].detail}</>
)}
</Content>
</Content>
)}
</>
);
case 'headers':
return renderHeaders(message.attributes.headers);
case 'value':
return truncate(message.attributes.value);
return (
<>
{truncate(message.attributes.value)}
{message.relationships.valueSchema && (
<Content>
<Content component="small">
{message.relationships.valueSchema.meta?.name}
{message.relationships.valueSchema.meta?.errors && (
<> <ExclamationTriangleIcon /> {message.relationships.valueSchema.meta.errors[0].detail}</>
)}
</Content>
</Content>
)}
</>
);
case 'size':
return formatBytes(message.attributes.size);
default:
Expand Down Expand Up @@ -167,7 +196,16 @@ export function MessagesTable({

return (
<Th key={column} modifier={modifier} width={width}>
{columnLabels[column]}
{column === 'size' ? (
<>
{columnLabels[column]}{' '}
<Tooltip content={t('topics.messages.tooltip.size')}>
<HelpIcon />
</Tooltip>
</>
) : (
columnLabels[column]
)}
</Th>
);
})}
Expand Down
3 changes: 2 additions & 1 deletion api/src/main/webui/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
* Custom React Hooks
*/

export { useTableState } from './useTableState';
export { useTableState } from './useTableState';
export { usePageTitle } from './usePageTitle';
37 changes: 37 additions & 0 deletions api/src/main/webui/src/hooks/usePageTitle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* usePageTitle Hook
*
* Sets the browser tab/window title in the format:
* "Page Name | StreamsHub Console"
*
* When called with no argument (from KafkaLayout and HomePage), the title is
* derived from the `handle.title` of the deepest matched route that declares
* one. Detail pages that have a dynamically-fetched name (e.g. topic name,
* connector name) call usePageTitle(fetchedName) to override the title once
* the data is available.
*
* When no page title can be determined the fallback is just "StreamsHub Console".
*/

import { useEffect } from 'react';
import { useMatches } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import type { RouteHandle } from '@/routes';

export function usePageTitle(override?: string) {
const { t } = useTranslation();
const matches = useMatches();

// Walk matches from deepest to shallowest to find the most specific title.
const routeTitle = [...matches]
.reverse()
.map((m) => (m.handle as RouteHandle | undefined)?.title)
.find((titleFn) => typeof titleFn === 'function')?.(t);

const page = override ?? routeTitle;
const appTitle = t('common.title');

useEffect(() => {
document.title = page ? `${page} | ${appTitle}` : appTitle;
}, [page, appTitle]);
}
1 change: 1 addition & 0 deletions api/src/main/webui/src/i18n/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@
"title": "Kafka Connect",
"connectors": "Connectors",
"connectClusters": "Clusters",
"connectClustersTitle": "Connect Clusters",
"searchConnectors": "Search connectors",
"searchClusters": "Search connect clusters",
"noConnectors": "No connectors found",
Expand Down
2 changes: 2 additions & 0 deletions api/src/main/webui/src/pages/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ import { ExternalLinkAltIcon } from '@patternfly/react-icons';
import { useKafkaClusters } from '../api/hooks/useKafkaClusters';
import { useMetadata } from '../api/hooks/useMetadata';
import { useShowLearning } from '../hooks/useShowLearning';
import { usePageTitle } from '@/hooks';
import { AppLayout } from '@/components/app/AppLayout';
import { ClustersDataView } from '@/components/home/ClustersDataView';
import { ResourceListParams } from '@/api/hooks/useResourceList';

export function HomePage() {
const { t } = useTranslation();
usePageTitle();
const [dataParams, setDataParams] = useState<ResourceListParams>({});
const clusterResult = useKafkaClusters(dataParams);
const { data: metadata } = useMetadata();
Expand Down
5 changes: 5 additions & 0 deletions api/src/main/webui/src/pages/kafka/KafkaLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import { Outlet, useParams, useLocation, Link } from 'react-router';
import { useTranslation } from 'react-i18next';
import { usePageTitle } from '@/hooks';
import {
Page,
PageSection,
Expand Down Expand Up @@ -45,6 +46,10 @@ export function KafkaLayout() {
nodeId?: string;
}>();

// Must be called unconditionally before any early returns.
// Static routes supply handle.title; detail pages call usePageTitle themselves.
usePageTitle();

const location = useLocation();
const { data, isLoading, error } = useKafkaCluster(kafkaId, {
fields: 'name,namespace,status,kafkaVersion,creationTimestamp,listeners,conditions',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import { useState } from 'react';
import { useParams, Link } from 'react-router';
import { usePageTitle } from '@/hooks';
import {
PageSection,
Tabs,
Expand Down Expand Up @@ -53,6 +54,9 @@ export function ConnectClusterDetailPage() {

const { data, isLoading, error } = useConnectCluster(connectClusterId);

const clusterName = data?.data?.attributes?.name || connectClusterId || '';
usePageTitle(clusterName || undefined);

const handleTabClick = (
_event: React.MouseEvent<HTMLElement, MouseEvent>,
tabIndex: string | number
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import { useState } from 'react';
import { useParams } from 'react-router';
import { usePageTitle } from '@/hooks';
import {
PageSection,
Title,
Expand Down Expand Up @@ -53,6 +54,9 @@ export function ConnectorDetailPage() {

const { data, isLoading, error } = useConnector(connectorId);

const connectorName = data?.data?.attributes?.name || connectorId || '';
usePageTitle(connectorName || undefined);

const handleTabClick = (
_event: React.MouseEvent<HTMLElement, MouseEvent>,
tabIndex: string | number
Expand Down Expand Up @@ -84,7 +88,6 @@ export function ConnectorDetailPage() {
const tasks = data.included?.filter((item) => item.type === 'connectorTasks') || [];
const config = connector.attributes.config || {};
const configEntries = Object.entries(config);
const connectorName = connector.attributes.name || connectorId || '';

return (
<>
Expand Down
Loading
Loading