Skip to content

Commit

Permalink
frontend: fix issue 1054, clean up how copy works, add new "copy mess…
Browse files Browse the repository at this point in the history
…age" context menu entry (#1057)
  • Loading branch information
rikimaru0345 authored Jan 31, 2024
1 parent cd032d7 commit 53280bd
Showing 1 changed file with 82 additions and 36 deletions.
118 changes: 82 additions & 36 deletions frontend/src/components/pages/topics/Tab.Messages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,43 @@ interface TopicMessageViewProps {
- add back summary of quick search <this.FilterSummary />
*/

const getStringValue = (value: string | TopicMessage): string => typeof value === 'string' ? value : JSON.stringify(value, null, 4)
function getMessageAsString(value: string | TopicMessage): string {
if (typeof value === 'string')
return value;

const obj = Object.assign({}, value) as Partial<TopicMessage>;
delete obj.keyBinHexPreview;
delete obj.valueBinHexPreview;
delete obj.keyJson;
delete obj.valueJson;
if (obj.key) {
delete obj.key.normalizedPayload;
if (obj.key.rawBytes)
obj.key.rawBytes = Array.from(obj.key.rawBytes) as any;
}
if (obj.value) {
delete obj.value.normalizedPayload;
if (obj.value.rawBytes)
obj.value.rawBytes = Array.from(obj.value.rawBytes) as any;
}

return JSON.stringify(obj, null, 4);
}

function getPayloadAsString(value: string | Uint8Array | object): string {

if (value == null)
return '';

if (typeof value === 'string')
return value;

if (value instanceof Uint8Array)
return JSON.stringify(Array.from(value), null, 4);

return JSON.stringify(value, null, 4);
}


const CopyDropdown: FC<{ record: TopicMessage, onSaveToFile: Function }> = ({ record, onSaveToFile }) => {
const toast = useToast()
Expand All @@ -135,8 +171,18 @@ const CopyDropdown: FC<{ record: TopicMessage, onSaveToFile: Function }> = ({ re
<KebabHorizontalIcon />
</MenuButton>
<MenuList>
<MenuItem onClick={() => {
navigator.clipboard.writeText(getMessageAsString(record)).then(() => {
toast({
status: 'success',
description: 'Message copied to clipboard'
})
}).catch(navigatorClipboardErrorHandler)
}}>
Copy Message
</MenuItem>
<MenuItem disabled={record.key.isPayloadNull} onClick={() => {
navigator.clipboard.writeText(getStringValue(record)).then(() => {
navigator.clipboard.writeText(getPayloadAsString(record.key.payload ?? record.key.rawBytes)).then(() => {
toast({
status: 'success',
description: 'Key copied to clipboard'
Expand All @@ -146,7 +192,7 @@ const CopyDropdown: FC<{ record: TopicMessage, onSaveToFile: Function }> = ({ re
Copy Key
</MenuItem>
<MenuItem disabled={record.value.isPayloadNull} onClick={() => {
navigator.clipboard.writeText(getStringValue(record)).then(() => {
navigator.clipboard.writeText(getPayloadAsString(record.value.payload ?? record.value.rawBytes)).then(() => {
toast({
status: 'success',
description: 'Value copied to clipboard'
Expand Down Expand Up @@ -337,10 +383,10 @@ export class TopicMessageView extends Component<TopicMessageViewProps> {
{!isServerless() && (
<Label text="Filter">
<Tooltip label="You don't have permissions to use search filters in this topic"
isDisabled={canUseFilters} placement="top" hasArrow>
isDisabled={canUseFilters} placement="top" hasArrow>
<Switch size="lg" isChecked={searchParams.filtersEnabled && canUseFilters}
onChange={v => (searchParams.filtersEnabled = v.target.checked)}
isDisabled={!canUseFilters}/>
onChange={v => (searchParams.filtersEnabled = v.target.checked)}
isDisabled={!canUseFilters} />
</Tooltip>
</Label>
)}
Expand All @@ -350,15 +396,15 @@ export class TopicMessageView extends Component<TopicMessageViewProps> {
{api.messageSearchPhase == null && (
<Tooltip label="Repeat current search" placement="top" hasArrow>
<Button variant="outline" onClick={() => this.searchFunc('manual')}>
<SyncIcon size={20}/>
<SyncIcon size={20} />
</Button>
</Tooltip>
)}
{api.messageSearchPhase != null && (
<Tooltip label="Stop searching" placement="top" hasArrow>
<Button variant="solid" colorScheme="red" onClick={() => api.stopMessageSearch()}
style={{padding: 0, width: '48px'}}>
<XCircleIcon size={20}/>
style={{ padding: 0, width: '48px' }}>
<XCircleIcon size={20} />
</Button>
</Tooltip>
)}
Expand Down Expand Up @@ -510,7 +556,7 @@ export class TopicMessageView extends Component<TopicMessageViewProps> {
offset: {
header: 'Offset',
accessorKey: 'offset',
cell: ({row: {original: {offset}}}) => numberToThousandsString(offset)
cell: ({ row: { original: { offset } } }) => numberToThousandsString(offset)
},
partitionID: {
header: 'Partition',
Expand All @@ -519,57 +565,57 @@ export class TopicMessageView extends Component<TopicMessageViewProps> {
timestamp: {
header: 'Timestamp',
accessorKey: 'timestamp',
cell: ({row: {original: {timestamp}}}) => <TimestampDisplay unixEpochMillisecond={timestamp} format={tsFormat}/>,
cell: ({ row: { original: { timestamp } } }) => <TimestampDisplay unixEpochMillisecond={timestamp} format={tsFormat} />,
},
key: {
header: 'Key',
size: hasKeyTags ? 300 : 1,
accessorKey: 'key',
cell: ({row: {original}}) => <MessageKeyPreview msg={original} previewFields={() => this.activePreviewTags}/>,
cell: ({ row: { original } }) => <MessageKeyPreview msg={original} previewFields={() => this.activePreviewTags} />,
},
value: {
header: () => <span>Value {previewButton}</span>,
accessorKey: 'value',
cell: ({row: {original}}) => <MessagePreview msg={original} previewFields={() => this.activePreviewTags} isCompactTopic={this.props.topic.cleanupPolicy.includes('compact')}/>
cell: ({ row: { original } }) => <MessagePreview msg={original} previewFields={() => this.activePreviewTags} isCompactTopic={this.props.topic.cleanupPolicy.includes('compact')} />
},
keySize: {
header: 'Key Size',
accessorKey: 'key.size',
cell: ({row: {original: {key: {size}}}}) => <span>{prettyBytes(size)}</span>
cell: ({ row: { original: { key: { size } } } }) => <span>{prettyBytes(size)}</span>
},
valueSize: {
header: 'Value Size',
accessorKey: 'value.size',
cell: ({row: {original: {value: {size}}}}) => <span>{prettyBytes(size)}</span>
cell: ({ row: { original: { value: { size } } } }) => <span>{prettyBytes(size)}</span>
}
}


const newColumns: ColumnDef<TopicMessage>[] = Object.values(dataTableColumns)

if(uiState.topicSettings.previewColumnFields.length > 0) {
if (uiState.topicSettings.previewColumnFields.length > 0) {
newColumns.splice(0, newColumns.length);

// let's be defensive and remove any duplicates before showing in the table
new Set(uiState.topicSettings.previewColumnFields.map(field => field.dataIndex)).forEach(dataIndex => {
if(dataTableColumns[dataIndex]) {
if (dataTableColumns[dataIndex]) {
newColumns.push(dataTableColumns[dataIndex])
}
})
}

if(newColumns.length > 0) {
if (newColumns.length > 0) {
newColumns[newColumns.length - 1].size = Infinity
}

const columns: ColumnDef<TopicMessage>[] = [...newColumns, {
header: () => <button onClick={() => {
this.showColumnSettings = true
}}><CogIcon style={{width: 20}}/>
}}><CogIcon style={{ width: 20 }} />
</button>,
id: 'action',
size: 0,
cell: ({row: {original}}) => <CopyDropdown record={original} onSaveToFile={() => this.downloadMessages = [original]}/>,
cell: ({ row: { original } }) => <CopyDropdown record={original} onSaveToFile={() => this.downloadMessages = [original]} />,
}]

const previewButton = <>
Expand Down Expand Up @@ -599,33 +645,33 @@ export class TopicMessageView extends Component<TopicMessageViewProps> {
uiState.topicSettings.searchParams.sorting = typeof sorting === 'function' ? sorting(uiState.topicSettings.searchParams.sorting) : sorting
}}
pagination={paginationParams}
onPaginationChange={onPaginationChange(paginationParams, ({ pageSize, pageIndex}) => {
onPaginationChange={onPaginationChange(paginationParams, ({ pageSize, pageIndex }) => {
uiState.topicSettings.searchParams.pageSize = pageSize
editQuery(query => {
query['page'] = String(pageIndex)
query['pageSize'] = String(pageSize)
})
})}
subComponent={({row: {original}}) => renderExpandedMessage(original)}
subComponent={({ row: { original } }) => renderExpandedMessage(original)}
/>
<Button variant="outline"
onClick={() => {
this.downloadMessages = api.messages;
}}
isDisabled={!api.messages || api.messages.length == 0}
onClick={() => {
this.downloadMessages = api.messages;
}}
isDisabled={!api.messages || api.messages.length == 0}
>
<span style={{paddingRight: '4px'}}><DownloadIcon/></span>
<span style={{ paddingRight: '4px' }}><DownloadIcon /></span>
Save Messages
</Button>

<SaveMessagesDialog messages={this.downloadMessages} onClose={() => this.downloadMessages = null} onRequireRawPayload={() => this.executeMessageSearch()} />

{
(this.messageSource?.data?.length > 0) &&
<PreviewSettings getShowDialog={() => showPreviewSettings} setShowDialog={s => setShowPreviewSettings(s)}/>
<PreviewSettings getShowDialog={() => showPreviewSettings} setShowDialog={s => setShowPreviewSettings(s)} />
}

<ColumnSettings getShowDialog={() => this.showColumnSettings} setShowDialog={s => this.showColumnSettings = s}/>
<ColumnSettings getShowDialog={() => this.showColumnSettings} setShowDialog={s => this.showColumnSettings = s} />
</>;
});

Expand Down Expand Up @@ -1277,20 +1323,20 @@ const MessageSchema = observer((p: { schemaId: number }) => {
const MessageHeaders = observer((props: { msg: TopicMessage; }) => {
return <div className="messageHeaders">
<div>
<DataTable<{key: string, value: Payload}>
<DataTable<{ key: string, value: Payload }>
data={props.msg.headers}
columns={[
{
size: 200, header: 'Key', accessorKey: 'key',
cell: ({row: {original: {key: headerKey}}}) => <span className="cellDiv" style={{ width: 'auto' }}>
cell: ({ row: { original: { key: headerKey } } }) => <span className="cellDiv" style={{ width: 'auto' }}>
{headerKey
? <Ellipsis>{toSafeString(headerKey)}</Ellipsis>
: renderEmptyIcon('Empty Key')}
</span>
},
{
size: Infinity, header: 'Value', accessorKey: 'value',
cell: ({row: {original: {value: headerValue}}}) => {
cell: ({ row: { original: { value: headerValue } } }) => {
if (typeof headerValue.payload === 'undefined') return renderEmptyIcon('"undefined"');
if (headerValue.payload === null) return renderEmptyIcon('"null"');
if (typeof headerValue.payload === 'number') return <span>{String(headerValue.payload)}</span>;
Expand All @@ -1304,11 +1350,11 @@ const MessageHeaders = observer((props: { msg: TopicMessage; }) => {
},
{
size: 120, header: 'Encoding', accessorKey: 'value',
cell: ({row: {original: {value: payload}}}) => <span className="nowrap">{payload.encoding}</span>
cell: ({ row: { original: { value: payload } } }) => <span className="nowrap">{payload.encoding}</span>
},
]}

subComponent={({row: {original: header}}) => {
subComponent={({ row: { original: header } }) => {
return typeof header.value?.payload !== 'object'
? <div className="codeBox" style={{ margin: '0', width: '100%' }}>{toSafeString(header.value.payload)}</div>
: <KowlJsonView src={header.value.payload as object} style={{ margin: '2em 0' }} />
Expand Down Expand Up @@ -1407,7 +1453,7 @@ const ColumnSettings: FC<{ getShowDialog: () => boolean; setShowDialog: (val: bo


const handleColumnListChange = action((newValue: MultiValue<{ value: string, label: string }>) => {
uiState.topicSettings.previewColumnFields = newValue.map(({label, value}) => ({
uiState.topicSettings.previewColumnFields = newValue.map(({ label, value }) => ({
title: label,
dataIndex: value
}))
Expand All @@ -1431,7 +1477,7 @@ const ColumnOptions: FC<{ tags: ColumnList[] }> = ({ tags }) => {
value: column.dataIndex
}));

return <ChakraReactSelect<{label: string; value: string}, true>
return <ChakraReactSelect<{ label: string; value: string }, true>
isMulti={true}
name=""
options={defaultColumnList.map((column: ColumnList) => ({
Expand Down

0 comments on commit 53280bd

Please sign in to comment.