From b60afbea113b91d3f3838bbe00f87d5d6c17ba4f Mon Sep 17 00:00:00 2001 From: SagarRajput-7 Date: Wed, 13 Nov 2024 05:13:38 +0530 Subject: [PATCH 1/4] feat: added table column and row logic for the new api response structure for prodcure overview --- .../MQDetails/MQTables/MQTableUtils.tsx | 92 +++++++++++++++++++ .../MQDetails/MQTables/MQTables.tsx | 44 ++++++++- .../MQTables/getTopicThroughputOverview.ts | 9 +- .../MQDetails/MessagingQueueOverview.tsx | 4 - 4 files changed, 141 insertions(+), 8 deletions(-) create mode 100644 frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTableUtils.tsx diff --git a/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTableUtils.tsx b/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTableUtils.tsx new file mode 100644 index 0000000000..032656f1ad --- /dev/null +++ b/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTableUtils.tsx @@ -0,0 +1,92 @@ +import { Typography } from 'antd'; +import dayjs from 'dayjs'; +import { History } from 'history'; +import { + convertToTitleCase, + RowData, +} from 'pages/MessagingQueues/MessagingQueuesUtils'; + +interface ProducerLatencyOverviewColumn { + timestamp: string; + data: { + [key: string]: number | string; + }; +} + +export interface TopicThroughputProducerOverviewResponse { + status: string; + payload: { + resultType: string; + result: { + queryName: string; + list: ProducerLatencyOverviewColumn[]; + }[]; + }; +} + +export const getColumnsForProduderLatencyOverview = ( + list: ProducerLatencyOverviewColumn[], + history: History, +): RowData[] => { + if (list?.length === 0) { + return []; + } + + const columns: { + title: string; + dataIndex: string; + key: string; + }[] = Object.keys(list[0].data)?.map((column) => ({ + title: convertToTitleCase(column), + dataIndex: column, + key: column, + render: (data: string | number): JSX.Element => { + if (column === 'service_name') { + return ( + { + e.preventDefault(); + e.stopPropagation(); + history.push(`/services/${encodeURIComponent(data as string)}`); + }} + > + {data} + + ); + } + + if (column === 'ts') { + const date = + typeof data === 'string' + ? dayjs(data).format('YYYY-MM-DD HH:mm:ss.SSS') + : dayjs(data / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS'); + return {date}; + } + + if (typeof data === 'number') { + return {data.toFixed(3)}; + } + + return {data}; + }, + })); + + return columns; +}; + +export const getTableDataForProducerLatencyOverview = ( + list: ProducerLatencyOverviewColumn[], +): RowData[] => { + if (list?.length === 0) { + return []; + } + + const tableData: RowData[] = list?.map( + (row, index: number): RowData => ({ + ...row.data, + key: index, + }), + ); + + return tableData; +}; diff --git a/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTables.tsx b/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTables.tsx index 73fd1b2f41..b181e17ebc 100644 --- a/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTables.tsx +++ b/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTables.tsx @@ -32,6 +32,11 @@ import { MessagingQueueServicePayload, MessagingQueuesPayloadProps, } from './getConsumerLagDetails'; +import { + getColumnsForProduderLatencyOverview, + getTableDataForProducerLatencyOverview, + TopicThroughputProducerOverviewResponse, +} from './MQTableUtils'; const INITIAL_PAGE_SIZE = 10; @@ -126,7 +131,13 @@ function MessagingQueuesTable({ tableApi: ( props: MessagingQueueServicePayload, ) => Promise< - SuccessResponse | ErrorResponse + | SuccessResponse< + ( + | MessagingQueuesPayloadProps + | TopicThroughputProducerOverviewResponse + )['payload'] + > + | ErrorResponse >; validConfigPresent?: boolean; type?: 'Detail' | 'Overview'; @@ -177,8 +188,35 @@ function MessagingQueuesTable({ { onSuccess: (data) => { if (data.payload) { - setColumns(getColumns(data?.payload, history)); - setTableData(getTableData(data?.payload)); + if ( + type === 'Overview' && + selectedView === MessagingQueuesViewType.producerLatency.value && + tableApiPayload?.detailType === 'producer' + ) { + setColumns( + getColumnsForProduderLatencyOverview( + (data?.payload as TopicThroughputProducerOverviewResponse['payload']) + .result[0].list, + history, + ), + ); + setTableData( + getTableDataForProducerLatencyOverview( + (data?.payload as TopicThroughputProducerOverviewResponse['payload']) + .result[0].list, + ), + ); + } else { + setColumns( + getColumns( + data?.payload as MessagingQueuesPayloadProps['payload'], + history, + ), + ); + setTableData( + getTableData(data?.payload as MessagingQueuesPayloadProps['payload']), + ); + } } }, onError: handleConsumerDetailsOnError, diff --git a/frontend/src/pages/MessagingQueues/MQDetails/MQTables/getTopicThroughputOverview.ts b/frontend/src/pages/MessagingQueues/MQDetails/MQTables/getTopicThroughputOverview.ts index ac955e8405..0edcf54413 100644 --- a/frontend/src/pages/MessagingQueues/MQDetails/MQTables/getTopicThroughputOverview.ts +++ b/frontend/src/pages/MessagingQueues/MQDetails/MQTables/getTopicThroughputOverview.ts @@ -5,11 +5,18 @@ import { MessagingQueueServicePayload, MessagingQueuesPayloadProps, } from './getConsumerLagDetails'; +import { TopicThroughputProducerOverviewResponse } from './MQTableUtils'; export const getTopicThroughputOverview = async ( props: Omit, ): Promise< - SuccessResponse | ErrorResponse + | SuccessResponse< + ( + | MessagingQueuesPayloadProps + | TopicThroughputProducerOverviewResponse + )['payload'] + > + | ErrorResponse > => { const { detailType, start, end } = props; const response = await axios.post( diff --git a/frontend/src/pages/MessagingQueues/MQDetails/MessagingQueueOverview.tsx b/frontend/src/pages/MessagingQueues/MQDetails/MessagingQueueOverview.tsx index ea55384ac3..5e3b752a33 100644 --- a/frontend/src/pages/MessagingQueues/MQDetails/MessagingQueueOverview.tsx +++ b/frontend/src/pages/MessagingQueues/MQDetails/MessagingQueueOverview.tsx @@ -82,10 +82,6 @@ function MessagingQueueOverview({ ? 'producer' : 'consumer' : undefined, - evalTime: - selectedView === MessagingQueuesViewType.dropRate.value - ? 2363404 - : undefined, }; return ( From aadff3dede5df556ec2f3837364ed707c591d144 Mon Sep 17 00:00:00 2001 From: SagarRajput-7 Date: Wed, 13 Nov 2024 05:19:36 +0530 Subject: [PATCH 2/4] feat: fixed typo in function name --- .../pages/MessagingQueues/MQDetails/MQTables/MQTableUtils.tsx | 2 +- .../src/pages/MessagingQueues/MQDetails/MQTables/MQTables.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTableUtils.tsx b/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTableUtils.tsx index 032656f1ad..99818fb5cf 100644 --- a/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTableUtils.tsx +++ b/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTableUtils.tsx @@ -24,7 +24,7 @@ export interface TopicThroughputProducerOverviewResponse { }; } -export const getColumnsForProduderLatencyOverview = ( +export const getColumnsForProducerLatencyOverview = ( list: ProducerLatencyOverviewColumn[], history: History, ): RowData[] => { diff --git a/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTables.tsx b/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTables.tsx index b181e17ebc..34c945190f 100644 --- a/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTables.tsx +++ b/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTables.tsx @@ -33,7 +33,7 @@ import { MessagingQueuesPayloadProps, } from './getConsumerLagDetails'; import { - getColumnsForProduderLatencyOverview, + getColumnsForProducerLatencyOverview, getTableDataForProducerLatencyOverview, TopicThroughputProducerOverviewResponse, } from './MQTableUtils'; @@ -194,7 +194,7 @@ function MessagingQueuesTable({ tableApiPayload?.detailType === 'producer' ) { setColumns( - getColumnsForProduderLatencyOverview( + getColumnsForProducerLatencyOverview( (data?.payload as TopicThroughputProducerOverviewResponse['payload']) .result[0].list, history, From 3d7b6c63c19c728cbb72550dc211206130fb71cb Mon Sep 17 00:00:00 2001 From: SagarRajput-7 Date: Tue, 10 Dec 2024 10:46:22 +0530 Subject: [PATCH 3/4] feat: consumed new 2 table merging logic for producer latency overview --- .../getTopicThroughputOverview.ts | 14 +-- .../MessagingQueues/MQDetails/MQDetails.tsx | 7 -- .../MQDetails/MQTables/MQTableUtils.tsx | 111 +++++------------- .../MQDetails/MQTables/MQTables.tsx | 91 +++++++------- .../MQDetails/MessagingQueueOverview.tsx | 47 +++++--- 5 files changed, 104 insertions(+), 166 deletions(-) rename frontend/src/{pages/MessagingQueues/MQDetails/MQTables => api/messagingQueues}/getTopicThroughputOverview.ts (69%) diff --git a/frontend/src/pages/MessagingQueues/MQDetails/MQTables/getTopicThroughputOverview.ts b/frontend/src/api/messagingQueues/getTopicThroughputOverview.ts similarity index 69% rename from frontend/src/pages/MessagingQueues/MQDetails/MQTables/getTopicThroughputOverview.ts rename to frontend/src/api/messagingQueues/getTopicThroughputOverview.ts index 0edcf54413..072f5eccf2 100644 --- a/frontend/src/pages/MessagingQueues/MQDetails/MQTables/getTopicThroughputOverview.ts +++ b/frontend/src/api/messagingQueues/getTopicThroughputOverview.ts @@ -1,22 +1,14 @@ import axios from 'api'; -import { ErrorResponse, SuccessResponse } from 'types/api'; - import { MessagingQueueServicePayload, MessagingQueuesPayloadProps, -} from './getConsumerLagDetails'; -import { TopicThroughputProducerOverviewResponse } from './MQTableUtils'; +} from 'pages/MessagingQueues/MQDetails/MQTables/getConsumerLagDetails'; +import { ErrorResponse, SuccessResponse } from 'types/api'; export const getTopicThroughputOverview = async ( props: Omit, ): Promise< - | SuccessResponse< - ( - | MessagingQueuesPayloadProps - | TopicThroughputProducerOverviewResponse - )['payload'] - > - | ErrorResponse + SuccessResponse | ErrorResponse > => { const { detailType, start, end } = props; const response = await axios.post( diff --git a/frontend/src/pages/MessagingQueues/MQDetails/MQDetails.tsx b/frontend/src/pages/MessagingQueues/MQDetails/MQDetails.tsx index 9f30fbbd00..7eab107e59 100644 --- a/frontend/src/pages/MessagingQueues/MQDetails/MQDetails.tsx +++ b/frontend/src/pages/MessagingQueues/MQDetails/MQDetails.tsx @@ -108,13 +108,6 @@ const checkValidityOfDetailConfigs = ( return false; } - if (currentTab === MessagingQueueServiceDetailType.ProducerDetails) { - return Boolean( - configDetails?.topic && - configDetails?.partition && - configDetails?.service_name, - ); - } return Boolean(configDetails?.topic && configDetails?.service_name); } diff --git a/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTableUtils.tsx b/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTableUtils.tsx index 99818fb5cf..1671baf7f9 100644 --- a/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTableUtils.tsx +++ b/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTableUtils.tsx @@ -1,92 +1,35 @@ -import { Typography } from 'antd'; -import dayjs from 'dayjs'; -import { History } from 'history'; -import { - convertToTitleCase, - RowData, -} from 'pages/MessagingQueues/MessagingQueuesUtils'; +import { RowData } from 'pages/MessagingQueues/MessagingQueuesUtils'; -interface ProducerLatencyOverviewColumn { - timestamp: string; - data: { - [key: string]: number | string; - }; -} - -export interface TopicThroughputProducerOverviewResponse { - status: string; - payload: { - resultType: string; - result: { - queryName: string; - list: ProducerLatencyOverviewColumn[]; - }[]; - }; -} +import { MessagingQueuesPayloadProps } from './getConsumerLagDetails'; -export const getColumnsForProducerLatencyOverview = ( - list: ProducerLatencyOverviewColumn[], - history: History, -): RowData[] => { - if (list?.length === 0) { +export function getTableDataForProducerLatencyOverview( + data: MessagingQueuesPayloadProps['payload'], +): RowData[] { + if (data?.result?.length === 0) { return []; } - const columns: { - title: string; - dataIndex: string; - key: string; - }[] = Object.keys(list[0].data)?.map((column) => ({ - title: convertToTitleCase(column), - dataIndex: column, - key: column, - render: (data: string | number): JSX.Element => { - if (column === 'service_name') { - return ( - { - e.preventDefault(); - e.stopPropagation(); - history.push(`/services/${encodeURIComponent(data as string)}`); - }} - > - {data} - - ); - } - - if (column === 'ts') { - const date = - typeof data === 'string' - ? dayjs(data).format('YYYY-MM-DD HH:mm:ss.SSS') - : dayjs(data / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS'); - return {date}; - } - - if (typeof data === 'number') { - return {data.toFixed(3)}; - } - - return {data}; - }, - })); + const firstTableData = data.result[0].table.rows || []; + const secondTableData = data.result[1]?.table.rows || []; - return columns; -}; - -export const getTableDataForProducerLatencyOverview = ( - list: ProducerLatencyOverviewColumn[], -): RowData[] => { - if (list?.length === 0) { - return []; - } - - const tableData: RowData[] = list?.map( - (row, index: number): RowData => ({ - ...row.data, - key: index, - }), + // Create a map for quick lookup of byte_rate using service_name and topic + const byteRateMap = new Map( + secondTableData.map((row) => [ + `${row.data.service_name}--${row.data.topic}`, + row.data.byte_rate, + ]), ); - return tableData; -}; + // Merge the data from both tables + const mergedTableData: RowData[] = + firstTableData.map( + (row, index): RowData => ({ + ...row.data, + byte_rate: + byteRateMap.get(`${row.data.service_name}--${row.data.topic}`) || 0, + key: index, + }), + ) || []; + + return mergedTableData; +} diff --git a/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTables.tsx b/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTables.tsx index 34c945190f..d839db1068 100644 --- a/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTables.tsx +++ b/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTables.tsx @@ -32,11 +32,7 @@ import { MessagingQueueServicePayload, MessagingQueuesPayloadProps, } from './getConsumerLagDetails'; -import { - getColumnsForProducerLatencyOverview, - getTableDataForProducerLatencyOverview, - TopicThroughputProducerOverviewResponse, -} from './MQTableUtils'; +import { getTableDataForProducerLatencyOverview } from './MQTableUtils'; const INITIAL_PAGE_SIZE = 10; @@ -44,16 +40,24 @@ const INITIAL_PAGE_SIZE = 10; export function getColumns( data: MessagingQueuesPayloadProps['payload'], history: History, + isProducerOverview?: boolean, ): RowData[] { if (data?.result?.length === 0) { return []; } + const mergedColumns = isProducerOverview + ? [ + ...(data?.result?.[0]?.table?.columns || []), + { name: 'byte_rate', queryName: 'byte_rate' }, + ] + : data?.result?.[0]?.table?.columns; + const columns: { title: string; dataIndex: string; key: string; - }[] = data?.result?.[0]?.table?.columns.map((column) => ({ + }[] = mergedColumns.map((column) => ({ title: convertToTitleCase(column.name), dataIndex: column.name, key: column.name, @@ -131,13 +135,7 @@ function MessagingQueuesTable({ tableApi: ( props: MessagingQueueServicePayload, ) => Promise< - | SuccessResponse< - ( - | MessagingQueuesPayloadProps - | TopicThroughputProducerOverviewResponse - )['payload'] - > - | ErrorResponse + SuccessResponse | ErrorResponse >; validConfigPresent?: boolean; type?: 'Detail' | 'Overview'; @@ -183,40 +181,25 @@ function MessagingQueuesTable({ }); }; + const isProducerOverview = useMemo( + () => + type === 'Overview' && + selectedView === MessagingQueuesViewType.producerLatency.value && + tableApiPayload?.detailType === 'producer', + [type, selectedView, tableApiPayload], + ); + const { mutate: getViewDetails, isLoading, error, isError } = useMutation( tableApi, { onSuccess: (data) => { if (data.payload) { - if ( - type === 'Overview' && - selectedView === MessagingQueuesViewType.producerLatency.value && - tableApiPayload?.detailType === 'producer' - ) { - setColumns( - getColumnsForProducerLatencyOverview( - (data?.payload as TopicThroughputProducerOverviewResponse['payload']) - .result[0].list, - history, - ), - ); - setTableData( - getTableDataForProducerLatencyOverview( - (data?.payload as TopicThroughputProducerOverviewResponse['payload']) - .result[0].list, - ), - ); - } else { - setColumns( - getColumns( - data?.payload as MessagingQueuesPayloadProps['payload'], - history, - ), - ); - setTableData( - getTableData(data?.payload as MessagingQueuesPayloadProps['payload']), - ); - } + setColumns(getColumns(data?.payload, history, isProducerOverview)); + setTableData( + isProducerOverview + ? getTableDataForProducerLatencyOverview(data?.payload) + : getTableData(data?.payload), + ); } }, onError: handleConsumerDetailsOnError, @@ -237,15 +220,29 @@ function MessagingQueuesTable({ const [, setSelectedRows] = useState(); const location = useLocation(); - const onRowClick = (record: { [key: string]: string }): void => { - const selectedKey = record.key; + const selectedRowKeyGenerator = (record: { + [key: string]: string; + }): React.Key => { + if (!isEmpty(tableApiPayload?.detailType)) { + return `${record.key}_${selectedView}_${tableApiPayload?.detailType}`; + } + return `${record.key}_${selectedView}`; + }; - if (`${selectedKey}_${selectedView}` === selectedRowKey) { + useEffect(() => { + if (isEmpty(configDetailQueryData)) { + setSelectedRowKey(undefined); + setSelectedRows({}); + } + }, [configDetailQueryData]); + + const onRowClick = (record: { [key: string]: string }): void => { + if (selectedRowKeyGenerator(record) === selectedRowKey) { setSelectedRowKey(undefined); setSelectedRows({}); setConfigDetail(urlQuery, location, history, {}); } else { - setSelectedRowKey(`${selectedKey}_${selectedView}`); + setSelectedRowKey(selectedRowKeyGenerator(record)); setSelectedRows(record); if (!isEmpty(record)) { @@ -305,7 +302,7 @@ function MessagingQueuesTable({ : {} } rowClassName={(record): any => - `${record.key}_${selectedView}` === selectedRowKey + selectedRowKeyGenerator(record) === selectedRowKey ? 'ant-table-row-selected' : '' } diff --git a/frontend/src/pages/MessagingQueues/MQDetails/MessagingQueueOverview.tsx b/frontend/src/pages/MessagingQueues/MQDetails/MessagingQueueOverview.tsx index 5e3b752a33..c4c9cfb253 100644 --- a/frontend/src/pages/MessagingQueues/MQDetails/MessagingQueueOverview.tsx +++ b/frontend/src/pages/MessagingQueues/MQDetails/MessagingQueueOverview.tsx @@ -1,8 +1,11 @@ import './MQDetails.style.scss'; import { Radio } from 'antd'; -import { Dispatch, SetStateAction } from 'react'; +import { getTopicThroughputOverview } from 'api/messagingQueues/getTopicThroughputOverview'; +import useUrlQuery from 'hooks/useUrlQuery'; +import { Dispatch, SetStateAction, useMemo } from 'react'; import { useSelector } from 'react-redux'; +import { useHistory, useLocation } from 'react-router-dom'; import { AppState } from 'store/reducers'; import { GlobalReducer } from 'types/reducer/globalTime'; @@ -10,25 +13,32 @@ import { MessagingQueuesViewType, MessagingQueuesViewTypeOptions, ProducerLatencyOptions, + setConfigDetail, } from '../MessagingQueuesUtils'; import { MessagingQueueServicePayload } from './MQTables/getConsumerLagDetails'; import { getKafkaSpanEval } from './MQTables/getKafkaSpanEval'; import { getPartitionLatencyOverview } from './MQTables/getPartitionLatencyOverview'; -import { getTopicThroughputOverview } from './MQTables/getTopicThroughputOverview'; import MessagingQueuesTable from './MQTables/MQTables'; type SelectedViewType = keyof typeof MessagingQueuesViewType; -function PartitionLatencyTabs({ +function ProducerLatencyTabs({ option, setOption, }: { option: ProducerLatencyOptions; setOption: Dispatch>; }): JSX.Element { + const urlQuery = useUrlQuery(); + const location = useLocation(); + const history = useHistory(); + return ( setOption(e.target.value)} + onChange={(e): void => { + setConfigDetail(urlQuery, location, history, {}); + setOption(e.target.value); + }} value={option} className="mq-details-options" > @@ -71,23 +81,26 @@ function MessagingQueueOverview({ (state) => state.globalTime, ); - const tableApiPayload: MessagingQueueServicePayload = { - variables: {}, - start: minTime, - end: maxTime, - detailType: - // eslint-disable-next-line no-nested-ternary - selectedView === MessagingQueuesViewType.producerLatency.value - ? option === ProducerLatencyOptions.Producers - ? 'producer' - : 'consumer' - : undefined, - }; + const tableApiPayload: MessagingQueueServicePayload = useMemo( + () => ({ + variables: {}, + start: minTime, + end: maxTime, + detailType: + // eslint-disable-next-line no-nested-ternary + selectedView === MessagingQueuesViewType.producerLatency.value + ? option === ProducerLatencyOptions.Producers + ? 'producer' + : 'consumer' + : undefined, + }), + [minTime, maxTime, selectedView, option], + ); return (
{selectedView === MessagingQueuesViewType.producerLatency.value ? ( - + ) : (
{MessagingQueuesViewType[selectedView as SelectedViewType].label} From 3bfd93de7948d8cad2378b8401fbf4ef6e914da2 Mon Sep 17 00:00:00 2001 From: SagarRajput-7 Date: Thu, 12 Dec 2024 09:30:27 +0530 Subject: [PATCH 4/4] feat: added 3 digit precision to 'ingestion_rate' and 'byte_rate'in consumer overview --- .../src/pages/MessagingQueues/MQDetails/MQTables/MQTables.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTables.tsx b/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTables.tsx index d839db1068..d4ee82b8b7 100644 --- a/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTables.tsx +++ b/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTables.tsx @@ -67,6 +67,8 @@ export function getColumns( 'throughput', 'avg_msg_size', 'error_percentage', + 'ingestion_rate', + 'byte_rate', ].includes(column.name) ? (value: number | string): string => { if (!isNumber(value)) return value.toString();