Skip to content

Commit

Permalink
Feature redpanda-data#52: adding column fields config, timestamps set…
Browse files Browse the repository at this point in the history
…tings and popup on no key
  • Loading branch information
rjmasikome committed Jul 12, 2020
1 parent b307e50 commit 6aeba69
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 10 deletions.
140 changes: 130 additions & 10 deletions frontend/src/components/pages/topics/Tab.Messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { TopicDetail, TopicConfigEntry, TopicMessage } from "../../../state/rest
import { Table, Tooltip, Row, Statistic, Tabs, Descriptions, Popover, Skeleton, Radio, Checkbox, Button, Select, Input, Form, Divider, Typography, message, Tag, Alert, Empty, ConfigProvider, Modal, AutoComplete, Space, Dropdown, Menu, Spin, Progress, Switch, notification } from "antd";
import { observer } from "mobx-react";
import { api } from "../../../state/backendApi";
import { uiSettings, PreviewTag, TopicOffsetOrigin, FilterEntry } from "../../../state/ui";
import { uiSettings, PreviewTag, TopicOffsetOrigin, FilterEntry, ColumnList } from "../../../state/ui";
import ReactJson, { CollapsedFieldProps } from 'react-json-view'
import { PageComponent, PageInitHelper } from "../Page";
import prettyMilliseconds from 'pretty-ms';
Expand All @@ -23,16 +23,16 @@ import { appGlobal } from "../../../state/appGlobal";
import qs from 'query-string';
import url, { URL, parse as parseUrl, format as formatUrl } from "url";
import { editQuery } from "../../../utils/queryHelper";
import { numberToThousandsString, ZeroSizeWrapper, Label, OptionGroup, StatusIndicator, QuickTable, LayoutBypass } from "../../../utils/tsxUtils";
import { numberToThousandsString, renderTimestamp, ZeroSizeWrapper, Label, OptionGroup, StatusIndicator, QuickTable, LayoutBypass } from "../../../utils/tsxUtils";

import Octicon, { Skip, Sync, ChevronDown, Play, ChevronRight } from '@primer/octicons-react';
import { SyncIcon, XCircleIcon, PlayIcon, ChevronRightIcon, ArrowRightIcon, HorizontalRuleIcon, DashIcon, CircleIcon, PlusIcon } from '@primer/octicons-v2-react'
import { ReactComponent as SvgCircleStop } from '../../../assets/circle-stop.svg';

import queryString, { ParseOptions, StringifyOptions, ParsedQuery } from 'query-string';
import Icon, { SettingOutlined, FilterOutlined, DeleteOutlined, PlusOutlined, CopyOutlined, LinkOutlined, ReloadOutlined, UserOutlined, PlayCircleFilled, DoubleRightOutlined, PlayCircleOutlined, VerticalAlignTopOutlined, LoadingOutlined, QuestionCircleTwoTone } from '@ant-design/icons';
import Icon, { SettingOutlined, FilterOutlined, DeleteOutlined, PlusOutlined, CopyOutlined, LinkOutlined, ReloadOutlined, UserOutlined, PlayCircleFilled, DoubleRightOutlined, PlayCircleOutlined, VerticalAlignTopOutlined, LoadingOutlined, QuestionCircleTwoTone, FilterFilled } from '@ant-design/icons';
import { ErrorBoundary } from "../../misc/ErrorBoundary";
import { SortOrder } from "antd/lib/table/interface";
import { SortOrder, FilterDropdownProps } from "antd/lib/table/interface";
import TextArea from "antd/lib/input/TextArea";
import { IsDev } from "../../../utils/env";

Expand Down Expand Up @@ -62,6 +62,7 @@ export class TopicMessageView extends Component<{ topic: TopicDetail }> {
@observable previewDisplay: string[] = [];
@observable allCurrentKeys: string[] = [];
@observable showPreviewSettings = false;
@observable showColumnSettings = false;

@observable fetchError = null as Error | null;

Expand Down Expand Up @@ -241,8 +242,16 @@ export class TopicMessageView extends Component<{ topic: TopicDetail }> {
</div>
</Label>

{/* Quick Search */}
{/* Field Settings */}
<div style={{ marginTop: spaceStyle.marginTop, marginLeft: 'auto' }}>
<Button shape='round' onClick={() => this.showPreviewSettings = true} style={{ color: '#1890ff', borderColor: '#1890ff', padding: '0 0.5em', background: 'transparent' }}>
<SettingOutlined style={{ fontSize: '1rem', transform: 'translateY(1px)' }} />
<span style={{ marginLeft: '.3em', fontSize: '85%' }}>Fields</span>
</Button>
</div>

{/* Quick Search */}
<div style={{ marginTop: spaceStyle.marginTop, marginLeft: '4px' }}>
<Input placeholder='Quick Search' allowClear={true} size='middle'
style={{ width: '200px', padding: '2px 8px', whiteSpace: 'nowrap' }}
value={uiState.topicSettings.quickSearch}
Expand All @@ -252,7 +261,7 @@ export class TopicMessageView extends Component<{ topic: TopicDetail }> {
</div>

{/* Search Progress Indicator: "Consuming Messages 30/30" */}
{api.MessageSearchPhase &&
{api.MessageSearchPhase && searchParams.filtersEnabled &&
<StatusIndicator
identityKey='messageSearch'
fillFactor={(api.Messages?.length ?? 0) / searchParams.maxResults}
Expand Down Expand Up @@ -385,10 +394,11 @@ export class TopicMessageView extends Component<{ topic: TopicDetail }> {
</span>
</>

const tsFormat = uiState.topicSettings.previewTimestamps;
const columns: ColumnProps<TopicMessage>[] = [
{ width: 1, title: 'Offset', dataIndex: 'offset', sorter: sortField('offset'), defaultSortOrder: 'descend', render: (t: number) => numberToThousandsString(t) },
{ width: 1, title: 'Partition', dataIndex: 'partitionID', sorter: sortField('partitionID'), },
{ width: 1, title: 'Timestamp', dataIndex: 'timestamp', sorter: sortField('timestamp'), render: (t: number) => new Date(t * 1000).toLocaleString() },
{ width: 1, title: 'Timestamp', dataIndex: 'timestamp', sorter: sortField('timestamp'), render: (t: number) => renderTimestamp(t, tsFormat) },
{ width: 3, title: 'Key', dataIndex: 'key', render: renderKey, sorter: this.keySorter },
{
width: 'auto',
Expand All @@ -404,6 +414,14 @@ export class TopicMessageView extends Component<{ topic: TopicDetail }> {
},
{
width: 1, title: ' ', key: 'action', className: 'msgTableActionColumn',
filters: [],
filterDropdownVisible: false,
onFilterDropdownVisibleChange: (_) => this.showColumnSettings = true,
filterIcon: (_) => {
return <Tooltip title='Column Settings' mouseEnterDelay={0.1}>
<span style={{ opacity: 0.66, marginLeft: '5px', width: '15px' }}><FilterFilled /></span>
</Tooltip>
},
render: (text, record) => !record.isValueNull && (
<span>
<ZeroSizeWrapper width={32} height={0}>
Expand All @@ -418,6 +436,17 @@ export class TopicMessageView extends Component<{ topic: TopicDetail }> {
},
];

// If the previewColumnFields is empty then use the default columns, otherwise filter it based on it
const filteredColumns: (ColumnProps<TopicMessage>)[] =
uiState.topicSettings.previewColumnFields.length === 0 ?
columns : uiState.topicSettings.previewColumnFields
.map(columnList =>
columns.find(c => c.dataIndex === columnList.dataIndex)
)
.filter(column => !!column)
// Add the action tab at the end
.concat(columns[columns.length -1]) as (ColumnProps<TopicMessage>)[];

return <>
<ConfigProvider renderEmpty={this.empty}>
<Table
Expand All @@ -438,18 +467,24 @@ export class TopicMessageView extends Component<{ topic: TopicDetail }> {

expandable={{
expandRowByClick: false,
expandIconColumnIndex: filteredColumns.findIndex(c => c.dataIndex === 'value'),
rowExpandable: _ => filteredColumns.findIndex(c => c.dataIndex === 'value') === -1 ? false : true,
expandedRowRender: record => RenderExpandedMessage(record),
expandIconColumnIndex: columns.findIndex(c => c.dataIndex === 'value')
}}

columns={columns}
columns={filteredColumns}
/>

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

{
(this.messageSource?.data?.length > 0) &&
<ColumnSettings allCurrentKeys={this.allCurrentKeys} getShowDialog={() => this.showColumnSettings} setShowDialog={s => this.showColumnSettings = s} />
}

</ConfigProvider>
</>
})
Expand Down Expand Up @@ -589,7 +624,9 @@ const renderKey = (key: any | null | undefined) => {
const text = typeof key === 'string' ? key : ToJson(key);

if (key == undefined || key == null || text.length == 0 || text == '{}')
return <span style={{ opacity: 0.66, marginLeft: '2px' }}><Octicon icon={Skip} /></span>
return <Tooltip title="Empty Key" mouseEnterDelay={0.1}>
<span style={{ opacity: 0.66, marginLeft: '2px' }}><Octicon icon={Skip} /></span>
</Tooltip>

if (text.length > 45) {

Expand Down Expand Up @@ -812,6 +849,89 @@ class PreviewSettings extends Component<{ allCurrentKeys: string[], getShowDialo
}
}

@observer
class ColumnSettings extends Component<{ allCurrentKeys: string[], getShowDialog: () => boolean, setShowDialog: (show: boolean) => void }> {

render() {

const content = <>
<Paragraph>
<Text>
Click on the column field on the text field and/or <b>x</b> on to remove it.<br />
</Text>
</Paragraph>
<div style={{ padding: '1.5em 1em', background: 'rgba(200, 205, 210, 0.16)', borderRadius: '4px' }}>
<ColumnOptions tags={uiState.topicSettings.previewColumnFields} />
</div>
<div style={{ marginTop: '1em' }}>
<h3 style={{ marginBottom: '0.5em' }}>More Settings</h3>
<Space size='large'>
<OptionGroup label='Timestamp' options={{ 'Default': 'default', 'Only Date': 'onlyDate', 'Only Time': 'onlyTime', 'Unix Seconds' : 'unixSeconds', 'Relative': 'relative'}}
value={uiState.topicSettings.previewTimestamps}
onChange={e => uiState.topicSettings.previewTimestamps = e}
/>
</Space>
</div>
</>

return <Modal
title={<span><FilterOutlined style={{ fontSize: '22px', verticalAlign: 'bottom', marginRight: '16px', color: 'hsla(209, 20%, 35%, 1)' }} />Column Settings</span>}
visible={this.props.getShowDialog()}
onOk={() => this.props.setShowDialog(false)}
onCancel={() => this.props.setShowDialog(false)}
width={750}
okText='Close'
cancelButtonProps={{ style: { display: 'none' } }}
closable={false}
maskClosable={true}
>
{content}
</Modal>;
}
}

@observer
class ColumnOptions extends Component<{ tags: ColumnList[]}> {

defaultColumnList: ColumnList[] = [
{ title: 'Offset', dataIndex: 'offset' },
{ title: 'Partition', dataIndex: 'partitionID' },
{ title: 'Timestamp', dataIndex: 'timestamp' },
{ title: 'Key', dataIndex: 'key' },
{ title: 'Value', dataIndex: 'value' },
{ title: 'Size', dataIndex: 'size' },
];

render() {
const defaultValues = uiState.topicSettings.previewColumnFields.map(column => column.title);
const children = this.defaultColumnList.map((column: ColumnList) =>
<Option value={column.dataIndex} key={column.dataIndex}>{column.title}</Option>
);

return <>
<Select
mode="multiple"
style={{ width: '100%' }}
placeholder="Currently on default View, please select"
defaultValue={defaultValues}
onChange={this.handleColumnListChange}
>
{children}
</Select>
</>
}

handleColumnListChange = (values: string[]) => {
if (!values.length) {
uiState.topicSettings.previewColumnFields = [];
} else {
const columnsSelected = values
.map(value => this.defaultColumnList.find(columnList => columnList.dataIndex === value))
.filter(columnList => !!columnList) as ColumnList[];
uiState.topicSettings.previewColumnFields = columnsSelected;
}
}
}

@observer
class CustomTagList extends Component<{ tags: PreviewTag[], allCurrentKeys: string[] }> {
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/state/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ export interface PreviewTag {
active: boolean;
}

export interface ColumnList {
title: string;
dataIndex: string;
}

export type FilterType = 'simple' | 'code'
export const FilterOperators = [
{
Expand Down Expand Up @@ -76,9 +81,13 @@ export class TopicDetailsSettings {
// @observable previewResultLimit: 3; // todo
@observable previewShowEmptyMessages = true; // todo: filter out messages that don't match

@observable previewTimestamps = 'default' as 'default' | 'onlyDate' | 'onlyTime' | 'unixSeconds' | 'relative';
@observable previewColumnFields = [] as ColumnList[];

@observable consumerPageSize = 20;
@observable partitionPageSize = 20;


@observable quickSearch = '';

}
Expand Down
23 changes: 23 additions & 0 deletions frontend/src/utils/tsxUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { Children, useState, Component, CSSProperties } from "react";
import { simpleUniqueId } from "./utils";
import { Radio, message, Progress } from 'antd';
import { MessageType } from "antd/lib/message";
import prettyMilliseconds from 'pretty-ms';



Expand All @@ -25,6 +26,28 @@ export function numberToThousandsString(n: number): JSX.Element {
return <>{result}</>
}

export function renderTimestamp(unixEpochSecond: number, format?: string): string {
let timestamp = "";
switch(format) {
case 'onlyDate':
timestamp = new Date(unixEpochSecond * 1000).toDateString()
break;
case 'onlyTime':
timestamp = new Date(unixEpochSecond * 1000).toLocaleTimeString()
break;
case 'unixSeconds':
timestamp = unixEpochSecond.toString();
break;
case 'relative':
timestamp = prettyMilliseconds(Date.now() - unixEpochSecond * 1000, { compact: true }) + ' ago';
break;
default:
timestamp = new Date(unixEpochSecond * 1000).toLocaleString();
}

return timestamp;
}

export const ZeroSizeWrapper = (p: { width: number, height: number, children?: React.ReactNode }) => {
return <span style={{
width: p.width, height: p.height,
Expand Down

0 comments on commit 6aeba69

Please sign in to comment.