diff --git a/frontend/src/components/Connect/Details/DetailsPage.tsx b/frontend/src/components/Connect/Details/DetailsPage.tsx index 31046a401f..13b6e15dfa 100644 --- a/frontend/src/components/Connect/Details/DetailsPage.tsx +++ b/frontend/src/components/Connect/Details/DetailsPage.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import { NavLink, Route, Routes } from 'react-router-dom'; +import { NavLink, Route, Routes, useLocation } from 'react-router-dom'; import useAppParams from 'lib/hooks/useAppParams'; +import { getConnectDetailsPageTitle } from 'lib/pageTitles'; import { clusterConnectConnectorConfigPath, clusterConnectConnectorConfigRelativePath, @@ -25,6 +26,7 @@ import Topics from './Topics/Topics'; const DetailsPage: React.FC = () => { const { clusterName, connectName, connectorName } = useAppParams(); + const { pathname } = useLocation(); const connector = useConnector({ clusterName, connectName, connectorName }); const tasks = useConnectorTasks({ clusterName, connectName, connectorName }); @@ -43,6 +45,12 @@ const DetailsPage: React.FC = () => { text={connectorName} backTo={clusterConnectorsPath(clusterName)} backText="Connectors" + documentTitle={getConnectDetailsPageTitle( + pathname, + clusterName, + connectName, + connectorName + )} > diff --git a/frontend/src/components/Connect/Header/Header.tsx b/frontend/src/components/Connect/Header/Header.tsx index 24e0e78f97..ec12cb37a8 100644 --- a/frontend/src/components/Connect/Header/Header.tsx +++ b/frontend/src/components/Connect/Header/Header.tsx @@ -5,6 +5,7 @@ import ClusterContext from 'components/contexts/ClusterContext'; import { ResourceType, Action } from 'generated-sources'; import { useConnects } from 'lib/hooks/api/kafkaConnect'; import useAppParams from 'lib/hooks/useAppParams'; +import { getKafkaConnectPageTitle } from 'lib/pageTitles'; import { clusterConnectorNewPath, clusterConnectorsRelativePath, @@ -48,7 +49,10 @@ const Header = () => { }; return ( - + {!isReadOnly && ( { text={consumerGroupID} backTo={clusterConsumerGroupsPath(routerParams.clusterName)} backText="Consumers" + documentTitle={buildPageTitle( + 'Reset Offsets', + consumerGroupID, + routerParams.clusterName + )} />
{ const { clusterName } = useAppParams(); + const { pathname } = useLocation(); const tables = useKsqlTables(clusterName); const streams = useKsqlStreams(clusterName); @@ -38,7 +46,10 @@ const KsqlDb: React.FC = () => { return ( <> - + { const { messageData, setMessage, clearMessage } = useProduceMessage(); const { clusterName, topicName } = useAppParams(); + const { pathname } = useLocation(); const openSidebarWithMessage = (message: TopicMessage) => { setMessage(message); @@ -103,6 +111,7 @@ const Topic: React.FC = () => { text={topicName} backText="Topics" backTo={clusterTopicsPath(clusterName)} + documentTitle={getTopicPageTitle(pathname, clusterName, topicName)} > > = ({ @@ -16,8 +18,12 @@ const PageHeading: React.FC> = ({ backText, children, title, + documentTitle, }) => { const isBackButtonVisible = backTo && backText; + React.useEffect(() => { + document.title = documentTitle || buildPageTitle(text, title); + }, [documentTitle, text, title]); return ( diff --git a/frontend/src/components/common/PageHeading/__tests__/PageHeading.spec.tsx b/frontend/src/components/common/PageHeading/__tests__/PageHeading.spec.tsx new file mode 100644 index 0000000000..62c99358f5 --- /dev/null +++ b/frontend/src/components/common/PageHeading/__tests__/PageHeading.spec.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { render } from 'lib/testHelpers'; +import PageHeading from 'components/common/PageHeading/PageHeading'; + +const originalTitle = document.title; + +describe('PageHeading', () => { + afterEach(() => { + document.title = originalTitle; + }); + + it('sets the browser title from heading content by default', () => { + render(); + + expect(document.title).toBe('Topics | local | Kafbat UI'); + }); + + it('uses an explicit browser title override when provided', () => { + render( + + ); + + expect(document.title).toBe('Messages | orders | local | Kafbat UI'); + }); +}); diff --git a/frontend/src/lib/__test__/pageTitles.spec.ts b/frontend/src/lib/__test__/pageTitles.spec.ts new file mode 100644 index 0000000000..1fc4a9d3b8 --- /dev/null +++ b/frontend/src/lib/__test__/pageTitles.spec.ts @@ -0,0 +1,54 @@ +import { + clusterConnectConnectorConfigPath, + clusterConnectorsRelativePath, + clusterKsqlDbQueryPath, + clusterTopicMessagesPath, +} from 'lib/paths'; +import { + buildPageTitle, + getConnectDetailsPageTitle, + getKafkaConnectPageTitle, + getKsqlDbPageTitle, + getTopicPageTitle, +} from 'lib/pageTitles'; + +describe('pageTitles', () => { + it('builds titles from non-empty parts', () => { + expect(buildPageTitle('Messages', '', 'orders')).toBe( + 'Messages | orders | Kafbat UI' + ); + }); + + it('maps topic detail routes to section titles', () => { + expect( + getTopicPageTitle( + clusterTopicMessagesPath('local', 'orders'), + 'local', + 'orders' + ) + ).toBe('Messages | orders | local | Kafbat UI'); + }); + + it('maps connector detail routes to section titles', () => { + expect( + getConnectDetailsPageTitle( + clusterConnectConnectorConfigPath('local', 'main-connect', 'sink-a'), + 'local', + 'main-connect', + 'sink-a' + ) + ).toBe('Config | sink-a | local | Kafbat UI'); + }); + + it('maps kafka connect list routes to page titles', () => { + expect( + getKafkaConnectPageTitle(clusterConnectorsRelativePath, 'local') + ).toBe('Connectors | Kafka Connect | local | Kafbat UI'); + }); + + it('maps ksqldb routes to page titles', () => { + expect(getKsqlDbPageTitle(clusterKsqlDbQueryPath('local'), 'local')).toBe( + 'Query | KSQL DB | local | Kafbat UI' + ); + }); +}); diff --git a/frontend/src/lib/pageTitles.ts b/frontend/src/lib/pageTitles.ts new file mode 100644 index 0000000000..c064ab3ad2 --- /dev/null +++ b/frontend/src/lib/pageTitles.ts @@ -0,0 +1,134 @@ +import { + clusterConnectConnectorConfigPath, + clusterConnectConnectorPath, + clusterConnectConnectorTopicsPath, + clusterConnectConnectorTasksPath, + clusterKsqlDbPath, + clusterKsqlDbQueryPath, + clusterKsqlDbStreamsPath, + clusterKsqlDbTablesPath, + clusterTopicAclsRelativePath, + clusterTopicConnectorsRelativePath, + clusterTopicConsumerGroupsPath, + clusterTopicEditPath, + clusterTopicMessagesPath, + clusterTopicPath, + clusterTopicSettingsPath, + clusterTopicStatisticsPath, + kafkaConnectClustersRelativePath, +} from 'lib/paths'; + +const APP_NAME = 'Kafbat UI'; + +export const buildPageTitle = (...parts: Array) => + [...parts.filter((part): part is string => !!part?.trim()), APP_NAME].join( + ' | ' + ); + +export const getKafkaConnectPageTitle = ( + currentPath: string | undefined, + clusterName: string +) => { + if (currentPath === kafkaConnectClustersRelativePath) { + return buildPageTitle('Clusters', 'Kafka Connect', clusterName); + } + + return buildPageTitle('Connectors', 'Kafka Connect', clusterName); +}; + +export const getKsqlDbPageTitle = (pathname: string, clusterName: string) => { + if (pathname === clusterKsqlDbQueryPath(clusterName)) { + return buildPageTitle('Query', 'KSQL DB', clusterName); + } + + if (pathname === clusterKsqlDbStreamsPath(clusterName)) { + return buildPageTitle('Streams', 'KSQL DB', clusterName); + } + + if ( + pathname === clusterKsqlDbTablesPath(clusterName) || + pathname === clusterKsqlDbPath(clusterName) + ) { + return buildPageTitle('Tables', 'KSQL DB', clusterName); + } + + return buildPageTitle('Tables', 'KSQL DB', clusterName); +}; + +export const getConnectDetailsPageTitle = ( + pathname: string, + clusterName: string, + connectName: string, + connectorName: string +) => { + if ( + pathname === + clusterConnectConnectorConfigPath(clusterName, connectName, connectorName) + ) { + return buildPageTitle('Config', connectorName, clusterName); + } + + if ( + pathname === + clusterConnectConnectorTopicsPath(clusterName, connectName, connectorName) + ) { + return buildPageTitle('Topics', connectorName, clusterName); + } + + if ( + pathname === + clusterConnectConnectorTasksPath( + clusterName, + connectName, + connectorName + ) || + pathname === + clusterConnectConnectorPath(clusterName, connectName, connectorName) + ) { + return buildPageTitle('Tasks', connectorName, clusterName); + } + + return buildPageTitle(connectorName, clusterName); +}; + +export const getTopicPageTitle = ( + pathname: string, + clusterName: string, + topicName: string +) => { + if (pathname === clusterTopicMessagesPath(clusterName, topicName)) { + return buildPageTitle('Messages', topicName, clusterName); + } + + if (pathname === clusterTopicSettingsPath(clusterName, topicName)) { + return buildPageTitle('Settings', topicName, clusterName); + } + + if (pathname === clusterTopicConsumerGroupsPath(clusterName, topicName)) { + return buildPageTitle('Consumers', topicName, clusterName); + } + + if (pathname === clusterTopicStatisticsPath(clusterName, topicName)) { + return buildPageTitle('Statistics', topicName, clusterName); + } + + if ( + pathname === + `${clusterTopicPath(clusterName, topicName)}/${clusterTopicAclsRelativePath}` + ) { + return buildPageTitle('ACLs', topicName, clusterName); + } + + if ( + pathname === + `${clusterTopicPath(clusterName, topicName)}/${clusterTopicConnectorsRelativePath}` + ) { + return buildPageTitle('Connectors', topicName, clusterName); + } + + if (pathname === clusterTopicEditPath(clusterName, topicName)) { + return buildPageTitle('Edit', topicName, clusterName); + } + + return buildPageTitle(topicName, clusterName); +};