Skip to content

Commit b665127

Browse files
RomanLamsal1337LukasHanke3006romanlamsalIngoStrauch2020FabianBesner2020
authored
feat(ui): add toggle "time ago" and timestamp with interactive tooltip (#1043)
close #938 Co-authored-by: Lukas Hanke <[email protected]> Co-authored-by: Roman Lamsal <[email protected]> Co-authored-by: Ingo Strauch <[email protected]> Co-authored-by: Fabian Besner <[email protected]> Co-authored-by: Timon Back <[email protected]> Co-authored-by: Martin Schuenemann <[email protected]> Co-authored-by: Lukas Hanke <[email protected]>
1 parent ab9a444 commit b665127

File tree

13 files changed

+214
-44
lines changed

13 files changed

+214
-44
lines changed

application.example.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ akhq:
105105
show-all-consumer-groups: true # Expand list of consumer groups instead of showing one. Overrides default.
106106
topic-data:
107107
sort: NEWEST # default sort order (OLDEST, NEWEST) (default: OLDEST). Overrides default
108+
date-time-format: ISO # format of message timestamps (RELATIVE, ISO) (default: RELATIVE)
108109

109110
my-cluster-ssl:
110111
properties:
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React, { Component } from 'react';
2+
import PropTypes from 'prop-types';
3+
4+
import TimeAgo from 'react-timeago';
5+
import { Tooltip } from '@material-ui/core';
6+
7+
import { SETTINGS_VALUES } from '../../utils/constants';
8+
9+
class DateTime extends Component {
10+
11+
render() {
12+
const isoDate = this.props.isoDateTimeString;
13+
const TimeAgoComp = <TimeAgo date={Date.parse(isoDate)} title={''}/>
14+
return (
15+
<Tooltip arrow title={
16+
this.props.dateTimeFormat === SETTINGS_VALUES.TOPIC_DATA.DATE_TIME_FORMAT.ISO ?
17+
TimeAgoComp :
18+
isoDate
19+
} interactive>
20+
<span>{
21+
this.props.dateTimeFormat === SETTINGS_VALUES.TOPIC_DATA.DATE_TIME_FORMAT.ISO ?
22+
isoDate :
23+
TimeAgoComp
24+
}</span>
25+
</Tooltip>
26+
);
27+
}
28+
29+
}
30+
31+
DateTime.propTypes = {
32+
isoDateTimeString: PropTypes.string.isRequired,
33+
dateTimeFormat: PropTypes.string.isRequired,
34+
};
35+
36+
export default DateTime;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import DateTime from './DateTime';
2+
3+
export default DateTime;

client/src/components/SearchBar/SearchBar.jsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from 'react';
22
import Joi from 'joi-browser';
33
import PropTypes from 'prop-types';
44
import Form from '../Form/Form';
5+
import { SETTINGS_VALUES } from '../../utils/constants';
56
import './styles.scss';
67

78
class SearchBar extends Form {
@@ -17,19 +18,19 @@ class SearchBar extends Form {
1718
errors: {},
1819
topicListViewOptions: [
1920
{
20-
_id: 'ALL',
21+
_id: SETTINGS_VALUES.TOPIC.TOPIC_DEFAULT_VIEW.ALL,
2122
name: 'Show all topics'
2223
},
2324
{
24-
_id: 'HIDE_INTERNAL',
25+
_id: SETTINGS_VALUES.TOPIC.TOPIC_DEFAULT_VIEW.HIDE_INTERNAL,
2526
name: 'Hide internal topics'
2627
},
2728
{
28-
_id: 'HIDE_INTERNAL_STREAM',
29+
_id: SETTINGS_VALUES.TOPIC.TOPIC_DEFAULT_VIEW.HIDE_INTERNAL_STREAM,
2930
name: 'Hide internal & stream topics'
3031
},
3132
{
32-
_id: 'HIDE_STREAM',
33+
_id: SETTINGS_VALUES.TOPIC.TOPIC_DEFAULT_VIEW.HIDE_STREAM,
3334
name: 'Hide stream topics'
3435
}
3536
]

client/src/components/Table/Table.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,7 @@ class Table extends Component {
443443
colspan() {
444444
const { actions, columns } = this.props;
445445

446-
return columns.length + (actions && actions.length ? actions.length : 0)
446+
return columns.filter(column => !column.extraRow).length + (actions && actions.length ? actions.length : 0)
447447
}
448448

449449
render() {

client/src/containers/Settings/Settings.jsx

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,36 @@ import React from 'react';
22
import Joi from 'joi-browser';
33
import Form from '../../components/Form/Form';
44
import Header from '../Header';
5-
import {getUIOptions, setUIOptions} from "../../utils/localstorage";
5+
import { SETTINGS_VALUES } from '../../utils/constants';
6+
import {getUIOptions, setUIOptions} from '../../utils/localstorage';
67
import './styles.scss';
7-
import {toast} from "react-toastify";
8+
import {toast} from 'react-toastify';
89

910
class Settings extends Form {
1011
state = {
1112
clusterId: '',
1213
formData: {
1314
topicDefaultView: '',
1415
topicDataSort: '',
16+
topicDataDateTimeFormat: '',
1517
skipConsumerGroups: false,
1618
skipLastRecord: false,
1719
showAllConsumerGroups: true
1820
},
1921
errors: {}
2022
};
2123

22-
topicDefaultView = [ { _id: 'ALL', name: 'ALL' }, { _id: 'HIDE_INTERNAL', name: 'HIDE_INTERNAL' },
23-
{ _id: 'HIDE_INTERNAL_STREAM', name: 'HIDE_INTERNAL_STREAM' }, { _id: 'HIDE_STREAM', name: 'HIDE_STREAM' } ];
24-
topicDataSort = [ { _id: 'OLDEST', name: 'OLDEST' }, { _id: 'NEWEST', name: 'NEWEST' } ];
24+
topicDefaultView = Object.entries(SETTINGS_VALUES.TOPIC.TOPIC_DEFAULT_VIEW)
25+
.map(([value]) => ({_id: value, name: value}));
26+
topicDataSort = Object.entries(SETTINGS_VALUES.TOPIC_DATA.SORT)
27+
.map(([value]) => ({_id: value, name: value}));
28+
topicDataDateTimeFormat = Object.entries(SETTINGS_VALUES.TOPIC_DATA.DATE_TIME_FORMAT)
29+
.map(([value]) => ({_id: value, name: value}));
2530

2631
schema = {
2732
topicDefaultView: Joi.string().optional(),
2833
topicDataSort: Joi.string().optional(),
34+
topicDataDateTimeFormat: Joi.string().required(),
2935
skipConsumerGroups: Joi.boolean().optional(),
3036
skipLastRecord: Joi.boolean().optional(),
3137
showAllConsumerGroups: Joi.boolean().optional()
@@ -39,6 +45,7 @@ class Settings extends Form {
3945
this.setState({ clusterId, formData: {
4046
topicDefaultView: (uiOptions && uiOptions.topic)? uiOptions.topic.defaultView : '',
4147
topicDataSort: (uiOptions && uiOptions.topicData)? uiOptions.topicData.sort : '',
48+
topicDataDateTimeFormat: (uiOptions && uiOptions.topicData)? uiOptions.topicData.dateTimeFormat : '',
4249
skipConsumerGroups: (uiOptions && uiOptions.topic)? uiOptions.topic.skipConsumerGroups : false,
4350
skipLastRecord: (uiOptions && uiOptions.topic)? uiOptions.topic.skipLastRecord : false,
4451
showAllConsumerGroups: (uiOptions && uiOptions.topic)? uiOptions.topic.showAllConsumerGroups : false
@@ -64,7 +71,8 @@ class Settings extends Form {
6471
showAllConsumerGroups: formData.showAllConsumerGroups
6572
},
6673
topicData: {
67-
sort: formData.topicDataSort
74+
sort: formData.topicDataSort,
75+
dateTimeFormat: formData.topicDataDateTimeFormat
6876
}
6977
});
7078
toast.success(`Settings for cluster '${clusterId}' updated successfully.`);
@@ -151,6 +159,20 @@ class Settings extends Form {
151159
true,
152160
{ className: 'form-control' }
153161
)}
162+
{this.renderSelect(
163+
'topicDataDateTimeFormat',
164+
'Time Format',
165+
this.topicDataDateTimeFormat,
166+
({ currentTarget: input }) => {
167+
const { formData } = this.state;
168+
formData.topicDataDateTimeFormat = input.value;
169+
this.setState({formData});
170+
},
171+
'col-sm-10',
172+
'select-wrapper settings-wrapper',
173+
true,
174+
{ className: 'form-control' }
175+
)}
154176
</fieldset>
155177

156178
{this.renderButton(

client/src/containers/Tail/Tail.jsx

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@ import Dropdown from 'react-bootstrap/Dropdown';
33
import _ from 'lodash';
44
import Input from '../../components/Form/Input';
55
import Header from '../Header';
6-
import {uriLiveTail, uriTopicsName} from '../../utils/endpoints';
6+
import { SETTINGS_VALUES } from '../../utils/constants';
7+
import { getClusterUIOptions } from '../../utils/functions';
8+
import { uriLiveTail, uriTopicsName } from '../../utils/endpoints';
79
import Table from '../../components/Table';
810
import AceEditor from 'react-ace';
911
import 'ace-builds/webpack-resolver';
1012
import 'ace-builds/src-noconflict/mode-json';
1113
import 'ace-builds/src-noconflict/theme-merbivore_soft';
1214
import Root from '../../components/Root';
13-
import TimeAgo from 'react-timeago';
15+
import DateTime from '../../components/DateTime';
1416

1517
const STATUS = {
1618
STOPPED: 'STOPPED',
@@ -30,7 +32,8 @@ class Tail extends Root {
3032
selectedStatus: 'STOPPED',
3133
maxRecords: 50,
3234
data: [],
33-
showFilters: ''
35+
showFilters: '',
36+
dateTimeFormat: SETTINGS_VALUES.TOPIC_DATA.DATE_TIME_FORMAT.RELATIVE
3437
};
3538
eventSource;
3639

@@ -60,6 +63,18 @@ class Tail extends Root {
6063
}
6164
});
6265
}
66+
67+
this.initDateTimeFormat();
68+
}
69+
70+
initDateTimeFormat = async () => {
71+
const { clusterId } = this.props.match.params;
72+
const uiOptions = await getClusterUIOptions(clusterId)
73+
if(uiOptions.topicData && uiOptions.topicData.dateTimeFormat) {
74+
this.setState(({
75+
dateTimeFormat: uiOptions.topicData.dateTimeFormat
76+
}));
77+
}
6378
}
6479

6580
componentWillUnmount = () => {
@@ -405,7 +420,14 @@ class Tail extends Root {
405420
colName: 'Date',
406421
type: 'text',
407422
cell: obj => {
408-
return (<div className="tail-headers"><TimeAgo date={Date.parse(obj.timestamp)} title={obj.timestamp}/></div>);
423+
return (
424+
<div className="tail-headers">
425+
<DateTime
426+
isoDateTimeString={obj.timestamp}
427+
dateTimeFormat={this.state.dateTimeFormat}
428+
/>
429+
</div>
430+
);
409431
}
410432
},
411433
{

client/src/containers/Topic/Topic/TopicData/TopicData.jsx

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import moment from 'moment';
1717
import DatePicker from '../../../../components/DatePicker';
1818
import _ from 'lodash';
1919
import constants from '../../../../utils/constants';
20-
import { setProduceToTopicValues } from '../../../../utils/localstorage';
2120
import AceEditor from 'react-ace';
2221
import ConfirmModal from '../../../../components/Modal/ConfirmModal';
2322

@@ -27,9 +26,10 @@ import 'ace-builds/src-noconflict/theme-dracula';
2726
import { toast } from 'react-toastify';
2827
import 'react-toastify/dist/ReactToastify.css';
2928
import Root from '../../../../components/Root';
29+
import DateTime from '../../../../components/DateTime';
3030
import { capitalizeTxt, getClusterUIOptions } from '../../../../utils/functions';
31+
import { setProduceToTopicValues, setUIOptions} from '../../../../utils/localstorage';
3132
import Select from '../../../../components/Form/Select';
32-
import TimeAgo from 'react-timeago'
3333
import JSONbig from 'json-bigint';
3434

3535
class TopicData extends Root {
@@ -66,7 +66,8 @@ class TopicData extends Root {
6666
canDeleteRecords: false,
6767
percent: 0,
6868
loading: true,
69-
canDownload: false
69+
canDownload: false,
70+
dateTimeFormat: constants.SETTINGS_VALUES.TOPIC_DATA.DATE_TIME_FORMAT.RELATIVE
7071
};
7172

7273
searchFilterTypes = [
@@ -99,8 +100,8 @@ class TopicData extends Root {
99100
const query = new URLSearchParams(this.props.location.search);
100101
const uiOptions = await getClusterUIOptions(clusterId);
101102

102-
this.setState(
103-
{
103+
this.setState((prevState) =>
104+
({
104105
selectedCluster: clusterId,
105106
selectedTopic: topicId,
106107
sortBy: (query.get('sort'))? query.get('sort') : (uiOptions && uiOptions.topicData && uiOptions.topicData.sort)?
@@ -111,7 +112,9 @@ class TopicData extends Root {
111112
search: this._buildSearchFromQueryString(query),
112113
offsets: (query.get('offset'))? this._getOffsetsByOffset(query.get('partition'), query.get('offset')) :
113114
((query.get('after'))? this._getOffsetsByAfterString(query.get('after')): this.state.offsets),
114-
},
115+
dateTimeFormat: (uiOptions && uiOptions.topicData && uiOptions.topicData.dateTimeFormat)?
116+
uiOptions.topicData.dateTimeFormat : prevState.dateTimeFormat
117+
}),
115118
() => {
116119
if(query.get('single') !== null) {
117120
this._getSingleMessage(query.get('partition'), query.get('offset'));
@@ -366,6 +369,22 @@ class TopicData extends Root {
366369
a.remove();
367370
}
368371

372+
async _handleOnDateTimeFormatChanged(newDateTimeFormat) {
373+
const { clusterId } = this.props.match.params;
374+
this.setState(({
375+
dateTimeFormat: newDateTimeFormat
376+
}));
377+
const currentUiOptions = await getClusterUIOptions(clusterId);
378+
const newUiOptions = {
379+
...currentUiOptions,
380+
topicData: {
381+
...(currentUiOptions.topicData),
382+
dateTimeFormat: newDateTimeFormat
383+
}
384+
}
385+
setUIOptions(clusterId, newUiOptions);
386+
}
387+
369388
_handleCopy(row) {
370389
const data = {
371390
partition: row.partition,
@@ -852,6 +871,29 @@ class TopicData extends Root {
852871
)}
853872
</Dropdown>
854873
</li>
874+
<li>
875+
<Dropdown>
876+
<Dropdown.Toggle className="nav-link dropdown-toggle">
877+
<strong>Time Format:</strong> ({this.state.dateTimeFormat})
878+
</Dropdown.Toggle>
879+
<Dropdown.Menu>
880+
<Dropdown.Item onClick={() =>
881+
this._handleOnDateTimeFormatChanged(
882+
constants.SETTINGS_VALUES.TOPIC_DATA.DATE_TIME_FORMAT.RELATIVE
883+
)
884+
}>
885+
Show relative time
886+
</Dropdown.Item>
887+
<Dropdown.Item onClick={() =>
888+
this._handleOnDateTimeFormatChanged(
889+
constants.SETTINGS_VALUES.TOPIC_DATA.DATE_TIME_FORMAT.ISO
890+
)
891+
}>
892+
Show ISO timestamp
893+
</Dropdown.Item>
894+
</Dropdown.Menu>
895+
</Dropdown>
896+
</li>
855897
</ul>
856898
</div>
857899
</nav>
@@ -927,7 +969,10 @@ class TopicData extends Root {
927969
colName: 'Date',
928970
type: 'text',
929971
cell: (obj, col) => {
930-
return (<TimeAgo date={Date.parse(obj[col.accessor])} title={obj[col.accessor]}/>);
972+
return <DateTime
973+
isoDateTimeString={obj[col.accessor]}
974+
dateTimeFormat={this.state.dateTimeFormat}
975+
/>;
931976
}
932977
},
933978
{

0 commit comments

Comments
 (0)