From 78fed1c8b32c5ba128d6d7d5b75813e04f3c5242 Mon Sep 17 00:00:00 2001 From: Jan Vorcak Date: Mon, 19 Feb 2024 12:19:24 +0100 Subject: [PATCH 1/4] Better handling of long names in the tables & breadcrumbs --- frontend/src/components/layout/Header.tsx | 93 ++++++++++--------- .../reassign-partitions/Step1.Partitions.tsx | 8 +- .../components/pages/schemas/Schema.List.tsx | 10 +- .../components/pages/topics/Topic.List.tsx | 9 +- frontend/src/index-cloud-integration.scss | 4 - frontend/src/index.scss | 4 - 6 files changed, 65 insertions(+), 63 deletions(-) diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx index 9d3378677..7ca3e6642 100644 --- a/frontend/src/components/layout/Header.tsx +++ b/frontend/src/components/layout/Header.tsx @@ -14,62 +14,67 @@ import { observer } from 'mobx-react'; import { Link, useRouteMatch } from 'react-router-dom'; import { isEmbedded } from '../../config'; import { uiState } from '../../state/uiState'; -import { MotionDiv } from '../../utils/animationProps'; -import { ZeroSizeWrapper } from '../../utils/tsxUtils'; import { UserPreferencesButton } from '../misc/UserPreferences'; import DataRefreshButton from '../misc/buttons/data-refresh/Component'; import { IsDev } from '../../utils/env'; -import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbLinkProps, ColorModeSwitch, Flex } from '@redpanda-data/ui'; +import { Box, Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbLinkProps, ColorModeSwitch, Flex } from '@redpanda-data/ui'; const AppPageHeader = observer(() => { const showRefresh = useShouldShowRefresh(); - return - }> - {!isEmbedded() && uiState.selectedClusterName && - - - Cluster - - - } - {uiState.pageBreadcrumbs.filter((_,i,arr) => { - const isCurrentPage = arr.length - 1 === i - return !isEmbedded() || isCurrentPage - }).map((entry, i, arr) => { + return {/* we need to refactor out #mainLayout > div rule, for now I've added this box as a workaround */} + + }> + {!isEmbedded() && uiState.selectedClusterName && + + + Cluster + + + } + {uiState.pageBreadcrumbs.filter((_, i, arr) => { const isCurrentPage = arr.length - 1 === i; - const currentBreadcrumbProps: BreadcrumbLinkProps = isCurrentPage ? { - as: 'span', - fontWeight: 700, - fontSize: 'xl', - } : {}; + return !isEmbedded() || isCurrentPage; + }).map((entry, i, arr) => { + const isCurrentPage = arr.length - 1 === i; + const currentBreadcrumbProps: BreadcrumbLinkProps = isCurrentPage ? { + as: 'span', + fontWeight: 700, + fontSize: 'xl', + wordBreak: 'break-all', + whiteSpace: 'break-spaces', + } : { + whiteSpace: 'nowrap' + }; - return ( - - - {entry.title} - + return ( + + + {entry.title} + - {isCurrentPage && showRefresh && ( - - - - )} - - ); - } - )} - + {isCurrentPage && showRefresh && ( + + + + )} + + ); + } + )} + - - - {(IsDev && !isEmbedded()) && } + + + {(IsDev && !isEmbedded()) && } + - ; + ; }); export default AppPageHeader; diff --git a/frontend/src/components/pages/reassign-partitions/Step1.Partitions.tsx b/frontend/src/components/pages/reassign-partitions/Step1.Partitions.tsx index be236cc8a..91b5b5f13 100644 --- a/frontend/src/components/pages/reassign-partitions/Step1.Partitions.tsx +++ b/frontend/src/components/pages/reassign-partitions/Step1.Partitions.tsx @@ -24,7 +24,7 @@ import Highlighter from 'react-highlight-words'; import { uiSettings } from '../../../state/ui'; import { WarningTwoTone } from '@ant-design/icons'; import { SearchTitle } from '../../misc/KowlTable'; -import { Checkbox, DataTable, Popover } from '@redpanda-data/ui' +import { Box, Checkbox, DataTable, Popover } from '@redpanda-data/ui' import { Row } from '@tanstack/react-table'; export type TopicWithPartitions = Topic & { partitions: Partition[], activeReassignments: PartitionReassignmentsPartition[] }; @@ -108,13 +108,13 @@ export class StepSelectPartitions extends Component<{ partitionSelection: Partit : record.topicName; if (this.props.throttledTopics.includes(record.topicName)) { - return
+ return {content} -
+ } - return content; + return {content}; }, size: Infinity, }, diff --git a/frontend/src/components/pages/schemas/Schema.List.tsx b/frontend/src/components/pages/schemas/Schema.List.tsx index 00069f93f..375285007 100644 --- a/frontend/src/components/pages/schemas/Schema.List.tsx +++ b/frontend/src/components/pages/schemas/Schema.List.tsx @@ -27,7 +27,7 @@ import { SmallStat } from '../../misc/SmallStat'; import { TrashIcon } from '@heroicons/react/outline'; import { openDeleteModal, openPermanentDeleteModal } from './modals'; -import { createStandaloneToast } from '@chakra-ui/react'; +import { Box, createStandaloneToast } from '@chakra-ui/react'; import { SchemaRegistrySubject } from '../../../state/restInterfaces'; import { Link } from 'react-router-dom'; import { encodeURIComponentPercents } from './Schema.Details'; @@ -167,8 +167,10 @@ class SchemaList extends PageComponent<{}> { sorting columns={[ { - header: 'Name', accessorKey: 'name', size: 400, cell: ({ row: { original: { name } } }) => - {name} + header: 'Name', accessorKey: 'name', size: Infinity, cell: ({ row: { original: { name } } }) => + + {name} + }, { header: 'Type', cell: ({row: {original: r}}) => , size: 100 }, { header: 'Compatibility', cell: ({row: {original: r}}) => , size: 100 }, @@ -178,7 +180,7 @@ class SchemaList extends PageComponent<{}> { id: 'actions', cell: ({row: {original: r}}) => ); }, diff --git a/frontend/src/components/pages/acls/CreateServiceAccountModal.tsx b/frontend/src/components/pages/acls/CreateServiceAccountModal.tsx index 58c0ca1b5..43f7c173d 100644 --- a/frontend/src/components/pages/acls/CreateServiceAccountModal.tsx +++ b/frontend/src/components/pages/acls/CreateServiceAccountModal.tsx @@ -208,7 +208,7 @@ const CreateUserConfirmationModal = observer((p: { state: CreateUserModalState; - + {p.state.username} diff --git a/frontend/src/components/pages/connect/Overview.tsx b/frontend/src/components/pages/connect/Overview.tsx index 16c135e30..4614fe74b 100644 --- a/frontend/src/components/pages/connect/Overview.tsx +++ b/frontend/src/components/pages/connect/Overview.tsx @@ -21,7 +21,7 @@ import { PageComponent, PageInitHelper } from '../Page'; import { ConnectorClass, ConnectorsColumn, errIcon, mr05, NotConfigured, OverviewStatisticsCard, TasksColumn, TaskState } from './helper'; import Section from '../../misc/Section'; import PageContent from '../../misc/PageContent'; -import { DataTable, Tooltip } from '@redpanda-data/ui'; +import { DataTable, Tooltip, Text } from '@redpanda-data/ui'; @observer class KafkaConnectOverview extends PageComponent { @@ -212,9 +212,9 @@ class TabTasks extends Component { header: 'Connector', accessorKey: 'name', // Assuming 'name' is correct based on your initial dataIndex cell: ({ row: { original } }) => ( - appGlobal.history.push(`/connect-clusters/${encodeURIComponent(original.cluster.clusterName)}/${encodeURIComponent(original.connectorName)}`)}> + appGlobal.history.push(`/connect-clusters/${encodeURIComponent(original.cluster.clusterName)}/${encodeURIComponent(original.connectorName)}`)}> {original.connectorName} - + ), size: 300 }, diff --git a/frontend/src/components/pages/schemas/EditCompatibility.tsx b/frontend/src/components/pages/schemas/EditCompatibility.tsx index 4738d0e22..c9edb11eb 100644 --- a/frontend/src/components/pages/schemas/EditCompatibility.tsx +++ b/frontend/src/components/pages/schemas/EditCompatibility.tsx @@ -226,7 +226,7 @@ function EditSchemaCompatibility(p: { {(subjectName && schema) && <> - {subjectName} + {subjectName} Schema diff --git a/frontend/src/components/pages/schemas/Schema.Details.tsx b/frontend/src/components/pages/schemas/Schema.Details.tsx index ded1f9bc4..f057f6d80 100644 --- a/frontend/src/components/pages/schemas/Schema.Details.tsx +++ b/frontend/src/components/pages/schemas/Schema.Details.tsx @@ -89,7 +89,10 @@ class SchemaDetailsView extends PageComponent<{ subjectName: string }> { uiState.pageTitle = subjectNameRaw; uiState.pageBreadcrumbs = []; uiState.pageBreadcrumbs.push({ title: 'Schema Registry', linkTo: '/schema-registry' }); - uiState.pageBreadcrumbs.push({ title: subjectNameRaw, linkTo: `/schema-registry/${encodeURIComponent(subjectNameRaw)}?version=${version}` }); + uiState.pageBreadcrumbs.push({ title: subjectNameRaw, linkTo: `/schema-registry/${encodeURIComponent(subjectNameRaw)}?version=${version}`, options: { + canBeTruncated: true, + canBeCopied: true, + }}); } refreshData(force?: boolean) { diff --git a/frontend/src/components/pages/schemas/Schema.List.tsx b/frontend/src/components/pages/schemas/Schema.List.tsx index 375285007..6b2e46cd6 100644 --- a/frontend/src/components/pages/schemas/Schema.List.tsx +++ b/frontend/src/components/pages/schemas/Schema.List.tsx @@ -168,7 +168,7 @@ class SchemaList extends PageComponent<{}> { columns={[ { header: 'Name', accessorKey: 'name', size: Infinity, cell: ({ row: { original: { name } } }) => - + {name} }, diff --git a/frontend/src/components/pages/topics/Topic.Details.tsx b/frontend/src/components/pages/topics/Topic.Details.tsx index b83d43878..91ae1efe5 100644 --- a/frontend/src/components/pages/topics/Topic.Details.tsx +++ b/frontend/src/components/pages/topics/Topic.Details.tsx @@ -158,7 +158,10 @@ class TopicDetails extends PageComponent<{ topicName: string }> { p.title = topicName; p.addBreadcrumb('Topics', '/topics'); - p.addBreadcrumb(topicName, '/topics/' + topicName); + p.addBreadcrumb(topicName, '/topics/' + topicName, { + canBeCopied: true, + canBeTruncated: true, + }); // clear messages from different topic if we have some if (api.messagesFor != '' && api.messagesFor != topicName) { diff --git a/frontend/src/components/pages/topics/Topic.List.tsx b/frontend/src/components/pages/topics/Topic.List.tsx index 4148ac725..f3129dfc2 100644 --- a/frontend/src/components/pages/topics/Topic.List.tsx +++ b/frontend/src/components/pages/topics/Topic.List.tsx @@ -42,8 +42,10 @@ import { Box, Button, Checkbox, + CopyButton, DataTable, Flex, + Grid, Icon, Popover, Text, @@ -214,7 +216,7 @@ const TopicsTable: FC<{ topics: Topic[], onDelete: (record: Topic) => void }> = { header: 'Name', accessorKey: 'topicName', - cell: ({row: {original: topic}}) => {renderName(topic)}, + cell: ({row: {original: topic}}) => {renderName(topic)}, size: Infinity, }, { @@ -479,7 +481,7 @@ function makeCreateTopicModal(parent: TopicList) { return createAutoModal({ modalProps: { title: 'Create Topic', - style: { width: '80%', minWidth: '600px', maxWidth: '1000px', top: '50px' }, + style: { width: '80%', minWidth: '600px', maxWidth: '1000px', top: '50px', paddingTop: '10px', paddingBottom: '10px' }, okText: 'Create', successTitle: 'Topic created!', @@ -551,18 +553,31 @@ function makeCreateTopicModal(parent: TopicList) { configs: config.filter(x => x.name.length > 0), }); - return
- Name:{result.topicName} - Partitions:{String(result.partitionCount).replace('-1', '(Default)')} - Replication Factor:{String(result.replicationFactor).replace('-1', '(Default)')} -
+ return ( + + Name: + + {result.topicName} + + + Partitions: + + {String(result.partitionCount).replace('-1', '(Default)')} + + Replication Factor: + + {String(result.replicationFactor).replace('-1', '(Default)')} + + + ) }, onSuccess: (_state, _result) => { parent.refreshData(true); diff --git a/frontend/src/components/pages/topics/Topic.Produce.tsx b/frontend/src/components/pages/topics/Topic.Produce.tsx index 32e75ef6e..2b7e35776 100644 --- a/frontend/src/components/pages/topics/Topic.Produce.tsx +++ b/frontend/src/components/pages/topics/Topic.Produce.tsx @@ -15,7 +15,7 @@ import { Link as ReactRouterLink } from 'react-router-dom' import { PublishMessagePayloadOptions, PublishMessageRequest } from '../../../protogen/redpanda/api/console/v1alpha1/publish_messages_pb'; import { uiSettings } from '../../../state/ui'; import { appGlobal } from '../../../state/appGlobal'; -import { base64ToUInt8Array, isValidBase64 } from '../../../utils/utils'; +import { base64ToUInt8Array, isValidBase64, substringWithEllipsis } from '../../../utils/utils'; import { isEmbedded } from '../../../config'; type EncodingOption = { @@ -552,7 +552,8 @@ export class TopicProducePage extends PageComponent<{ topicName: string }> { const topicName = this.props.topicName; p.title = 'Produce' p.addBreadcrumb('Topics', '/topics'); - p.addBreadcrumb(topicName, '/topics/' + topicName); + p.addBreadcrumb(substringWithEllipsis(topicName, 50), '/topics/' + topicName); + p.addBreadcrumb('Produce record', '/produce-record') this.refreshData(true); appGlobal.onRefresh = () => this.refreshData(true); @@ -566,7 +567,9 @@ export class TopicProducePage extends PageComponent<{ topicName: string }> { return ( Produce Kafka record - This will produce a single record to the {this.props.topicName} topic. + + This will produce a single record to the topic. + diff --git a/frontend/src/state/uiState.ts b/frontend/src/state/uiState.ts index 65d372b05..097db6028 100644 --- a/frontend/src/state/uiState.ts +++ b/frontend/src/state/uiState.ts @@ -14,10 +14,15 @@ import { PageDefinition } from '../components/routes'; import { api } from './backendApi'; import { uiSettings, TopicDetailsSettings as TopicSettings } from './ui'; +export interface BreadcrumbOptions { + canBeTruncated?: boolean; + canBeCopied?: boolean; +} export interface BreadcrumbEntry { title: string; linkTo: string; + options?: BreadcrumbOptions; } diff --git a/frontend/src/utils/utils.test.ts b/frontend/src/utils/utils.test.ts new file mode 100644 index 000000000..d8fbcfa95 --- /dev/null +++ b/frontend/src/utils/utils.test.ts @@ -0,0 +1,31 @@ +import { substringWithEllipsis } from './utils'; // Adjust the import path as needed + +describe('substringWithEllipsis', () => { + it('returns the original string if its length is less than maxLength', () => { + expect(substringWithEllipsis('Hello', 10)).toBe('Hello'); + }); + + it('returns the original string if its length is equal to maxLength', () => { + expect(substringWithEllipsis('Hello', 5)).toBe('Hello'); + }); + + it('handles cases where maxLength is less than 3', () => { + // Since effectiveLength is calculated as Math.max(maxLength - 3, 1), + // a maxLength of 2 would lead to an effectiveLength of 1, and thus the output should be "H..." + // However, given the logic, it's adjusted to ensure there's at least 1 character before the ellipsis + expect(substringWithEllipsis('Hello, world!', 2)).toBe('H...'); + expect(substringWithEllipsis('Hello, world!', 1)).toBe('H...'); + }); + + it('returns an empty string with ellipsis if maxLength is 0', () => { + // This scenario is interesting because the logic dictates a minimum effective length of 1 character. + // However, a maxLength of 0 logically suggests no characters should be shown. + // The function's logic needs to be clear on this behavior; assuming we follow the implementation, it would be: + expect(substringWithEllipsis('Hello, world!', 0)).toBe('H...'); + // But if considering maxLength of 0 as a request for no output, the implementation might need adjusting. + }); + + it('correctly handles an empty input string', () => { + expect(substringWithEllipsis('', 5)).toBe(''); + }); +}); diff --git a/frontend/src/utils/utils.ts b/frontend/src/utils/utils.ts index 8575ca9b2..5fdc9352c 100644 --- a/frontend/src/utils/utils.ts +++ b/frontend/src/utils/utils.ts @@ -806,3 +806,23 @@ export function retrier(operation: () => Promise, { attempts = Infinity, d * performing indexing */ export type ElementOf = T extends (infer E)[] ? E : T extends readonly (infer F)[] ? F : never; + +/** + * Truncates a string to a specified length and adds an ellipsis (...) if the truncation occurs. + * + * @param {string} input The string to truncate. + * @param {number} maxLength The maximum length of the string, including the ellipsis. + * @returns {string} The truncated string with ellipsis if truncation was necessary, otherwise the original string. + */ +export function substringWithEllipsis(input: string, maxLength: number): string { + // Check if the input length is greater than the maxLength + // Note: We account for the length of the ellipsis in the comparison + if (input.length > maxLength) { + // Subtract 3 from maxLength to accommodate the ellipsis + // Ensure maxLength is at least 4 to avoid negative substring lengths + const effectiveLength = Math.max(maxLength - 3, 1); + return input.substring(0, effectiveLength) + '...'; + } else { + return input; + } +} From 358500964aeff3d9f07ee09a2710f9560339770a Mon Sep 17 00:00:00 2001 From: Jan Vorcak Date: Wed, 28 Feb 2024 10:31:40 +0100 Subject: [PATCH 3/4] Long names fixes in Connectors --- frontend/src/components/pages/connect/Cluster.Details.tsx | 4 ++-- .../src/components/pages/connect/Connector.Details.tsx | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/pages/connect/Cluster.Details.tsx b/frontend/src/components/pages/connect/Cluster.Details.tsx index 248f94e57..780307ef9 100644 --- a/frontend/src/components/pages/connect/Cluster.Details.tsx +++ b/frontend/src/components/pages/connect/Cluster.Details.tsx @@ -20,7 +20,7 @@ import { PageComponent, PageInitHelper } from '../Page'; import { ClusterStatisticsCard, ConnectorClass, NotConfigured, TasksColumn, TaskState } from './helper'; import { isEmbedded } from '../../../config'; import { Link } from 'react-router-dom'; -import { Box, Button, DataTable } from '@redpanda-data/ui'; +import { Box, Button, DataTable, Text } from '@redpanda-data/ui'; import { ClusterAdditionalInfo, ClusterConnectorInfo } from '../../../state/restInterfaces'; import SearchBar from '../../misc/SearchBar'; import { uiSettings } from '../../../state/ui'; @@ -116,7 +116,7 @@ class KafkaClusterDetails extends PageComponent<{ clusterName: string }> { accessorKey: 'name', cell: ({row: {original}}) => ( - {original.name} + {original.name} ), size: Infinity diff --git a/frontend/src/components/pages/connect/Connector.Details.tsx b/frontend/src/components/pages/connect/Connector.Details.tsx index 72f9d19d1..ba0e40d25 100644 --- a/frontend/src/components/pages/connect/Connector.Details.tsx +++ b/frontend/src/components/pages/connect/Connector.Details.tsx @@ -348,7 +348,7 @@ const ConnectorErrorModal = observer((p: { error: ConnectorError }) => { return <> - {p.error.title} + {p.error.title} @@ -380,7 +380,10 @@ class KafkaConnectorDetails extends PageComponent<{ clusterName: string; connect p.title = connector; p.addBreadcrumb('Connectors', '/connect-clusters'); p.addBreadcrumb(clusterName, `/connect-clusters/${encodeURIComponent(clusterName)}`); - p.addBreadcrumb(connector, `/connect-clusters/${encodeURIComponent(clusterName)}/${encodeURIComponent(connector)}`); + p.addBreadcrumb(connector, `/connect-clusters/${encodeURIComponent(clusterName)}/${encodeURIComponent(connector)}`, { + canBeTruncated: true, + canBeCopied: true + }); this.refreshData(true); appGlobal.onRefresh = () => this.refreshData(true); } From d42925c610204cc971b9d58933837a9ec578dceb Mon Sep 17 00:00:00 2001 From: Jan Vorcak Date: Wed, 28 Feb 2024 10:52:10 +0100 Subject: [PATCH 4/4] Long names in groups --- .../src/components/pages/consumers/Group.Details.tsx | 5 ++++- frontend/src/components/pages/consumers/Group.List.tsx | 10 +++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/pages/consumers/Group.Details.tsx b/frontend/src/components/pages/consumers/Group.Details.tsx index bac203e87..e497044e3 100644 --- a/frontend/src/components/pages/consumers/Group.Details.tsx +++ b/frontend/src/components/pages/consumers/Group.Details.tsx @@ -50,7 +50,10 @@ class GroupDetails extends PageComponent<{ groupId: string }> { p.title = this.props.groupId; p.addBreadcrumb('Consumer Groups', '/groups'); - if (group) p.addBreadcrumb(group, '/' + group); + if (group) p.addBreadcrumb(group, '/' + group, { + canBeCopied: true, + canBeTruncated: true, + }); this.refreshData(true); appGlobal.onRefresh = () => this.refreshData(true); diff --git a/frontend/src/components/pages/consumers/Group.List.tsx b/frontend/src/components/pages/consumers/Group.List.tsx index 86cae17ee..7ef7a61b3 100644 --- a/frontend/src/components/pages/consumers/Group.List.tsx +++ b/frontend/src/components/pages/consumers/Group.List.tsx @@ -25,7 +25,7 @@ import { BrokerList } from '../../misc/BrokerList'; import { ShortNum } from '../../misc/ShortNum'; import Section from '../../misc/Section'; import PageContent from '../../misc/PageContent'; -import { DataTable, Flex, SearchField, Tag } from '@redpanda-data/ui'; +import { DataTable, Flex, SearchField, Tag, Text } from '@redpanda-data/ui'; import { Statistic } from '../../misc/Statistic'; import { Link } from 'react-router-dom'; @@ -189,11 +189,15 @@ class GroupList extends PageComponent { GroupId = (p: { group: GroupDescription }) => { const protocol = p.group.protocolType; - if (protocol == 'consumer') return <>{p.group.groupId}; + const groupIdEl = {p.group.groupId} + + if (protocol == 'consumer') { + return groupIdEl; + } return Protocol: {protocol} - {p.group.groupId} + {groupIdEl} ; } }