diff --git a/frontend/src/js/api/api.ts b/frontend/src/js/api/api.ts index 04138e7c67..a5e779c2fb 100644 --- a/frontend/src/js/api/api.ts +++ b/frontend/src/js/api/api.ts @@ -418,7 +418,7 @@ export const useGetResult = () => { [authToken], ); return useCallback( - (queryId: string, limit = 1000) => { + (queryId: string, limit: number) => { const url = `/result/arrow/${queryId}.arrs?` + new URLSearchParams({ limit: limit.toString() }); diff --git a/frontend/src/js/preview/Table.tsx b/frontend/src/js/preview/Table.tsx index 0355c64523..8cd7f64184 100644 --- a/frontend/src/js/preview/Table.tsx +++ b/frontend/src/js/preview/Table.tsx @@ -6,8 +6,7 @@ import { Vector, } from "apache-arrow"; import RcTable from "rc-table"; -import { DefaultRecordType } from "rc-table/lib/interface"; -import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { memo, useMemo, useRef } from "react"; import { GetQueryResponseDoneT, GetQueryResponseT } from "../api/types"; import { useCustomTableRenderers } from "./tableUtils"; @@ -69,73 +68,32 @@ export default memo(function Table({ const columns = useMemo( () => - arrowReader.schema?.fields.map((field) => ({ - title: field.name.charAt(0).toUpperCase() + field.name.slice(1), - dataIndex: field.name, - key: field.name, - render: (value: string | Vector) => { - return typeof value === "string" ? ( - {value} - ) : ( - value - ); - }, - })), - [arrowReader.schema], - ); - - const parseTableRows = useCallback( - (data: Vector[]) => { - const nextRows = [] as DefaultRecordType[]; - data.forEach((dataEntry: Vector) => { - const parsedValues = Object.fromEntries( - Object.entries(dataEntry.toJSON()).map(([key, value]) => { - const parsedValue = - getRenderFunctionByFieldName(key)?.(value) ?? value; - return [key, parsedValue]; - }), - ); - nextRows.push(parsedValues); - }); - - return nextRows; - }, - [getRenderFunctionByFieldName], + arrowReader.schema?.fields.map((field) => { + const renderer = getRenderFunctionByFieldName(field.name); + + return { + title: field.name.charAt(0).toUpperCase() + field.name.slice(1), + dataIndex: field.name, + key: field.name, + render: (value: string | Vector) => { + const rendered = renderer(value); + return {rendered}; + }, + }; + }), + [arrowReader.schema, getRenderFunctionByFieldName], ); const loadedTableData = useMemo( - () => parseTableRows(new ArrowTable(initialTableData.value).toArray()), - [initialTableData, parseTableRows], + () => new ArrowTable(initialTableData.value).toArray(), + [initialTableData], ); - const [visibleTableRows, setVisibleTableRows] = useState(50); - - useEffect(() => { - const eventFunction = async () => { - const div = rootRef.current; - if (!div) { - return; - } - const maxScroll = - (div.parentElement?.scrollHeight || div.scrollHeight) - - window.innerHeight; - const thresholdTriggered = - (div.parentElement?.scrollTop || div.scrollTop) / maxScroll > 0.9; - if (thresholdTriggered) { - setVisibleTableRows((rowCount) => - Math.min(rowCount + 50, loadedTableData.length), - ); - } - }; - - window.addEventListener("scroll", eventFunction, true); - return () => window.removeEventListener("scroll", eventFunction, true); - }, [loadedTableData, visibleTableRows, arrowReader]); return ( `previewtable_row_${index}`} components={{ table: StyledTable, diff --git a/frontend/src/js/preview/actions.ts b/frontend/src/js/preview/actions.ts index cf856360c7..905013e0dc 100644 --- a/frontend/src/js/preview/actions.ts +++ b/frontend/src/js/preview/actions.ts @@ -81,7 +81,7 @@ export function useLoadPreviewData() { try { const arrowReader = await AsyncRecordBatchStreamReader.from( - getResult(queryId), + getResult(queryId, 100), ); const loadInitialData = async () => { await arrowReader.open(); diff --git a/frontend/src/js/preview/tableUtils.ts b/frontend/src/js/preview/tableUtils.ts index ea727e13e9..db3cd14db8 100644 --- a/frontend/src/js/preview/tableUtils.ts +++ b/frontend/src/js/preview/tableUtils.ts @@ -4,12 +4,7 @@ import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; import { CurrencyConfigT, GetQueryResponseDoneT } from "../api/types"; import { StateT } from "../app/reducers"; -import { - NUMBER_TYPES, - formatDate, - formatNumber, - toFullLocaleDateString, -} from "./util"; +import { NUMBER_TYPES, currencyFromSymbol } from "./util"; export type CellValue = string | Vector; @@ -20,7 +15,18 @@ export function useCustomTableRenderers(queryData: GetQueryResponseDoneT) { ); const getRenderFunction = useCallback( - (cellType: string): ((value: CellValue) => string) | undefined => { + (cellType: string): ((value: CellValue) => string) => { + const dateFormatter = new Intl.DateTimeFormat(navigator.language, { + day: "2-digit", + month: "2-digit", + year: "numeric", + }); + + const currencyFormatter = new Intl.NumberFormat(navigator.language, { + style: "currency", + currency: currencyFromSymbol(currencyConfig.unit), + }); + if (cellType.indexOf("LIST") == 0) { const listType = cellType.match(/LIST\[(?.*)\]/)?.groups?.[ "listtype" @@ -30,59 +36,65 @@ export function useCustomTableRenderers(queryData: GetQueryResponseDoneT) { return (value) => value ? (value as Vector) - .toArray() - .map((listItem: string) => - listTypeRenderFunction - ? listTypeRenderFunction(listItem) - : listItem, - ) + .toArray() // This is somewhat slow, but for-loop produces bogus values + .map(listTypeRenderFunction) .join(", ") : null; } } else if (NUMBER_TYPES.includes(cellType)) { + const numnberFormatter = new Intl.NumberFormat(navigator.language, { + maximumFractionDigits: 2, + minimumFractionDigits: cellType == "INTEGER" ? 0 : 2, + }); + return (value) => { - const num = parseFloat(value as string); - return isNaN(num) ? "" : formatNumber(num); + if (value && !isNaN(value as unknown as number)) { + return numnberFormatter.format(value as unknown as number); + } + return ""; }; } else if (cellType == "DATE") { - return (value) => - value instanceof Date - ? toFullLocaleDateString(value) - : formatDate(value as string); + return (value) => dateFormatter.format(value as unknown as Date); } else if (cellType == "DATE_RANGE") { return (value) => { - const dateRange = (value as Vector).toJSON() as unknown as { - min: Date; - max: Date; - }; - const min = toFullLocaleDateString(dateRange.min); - const max = toFullLocaleDateString(dateRange.max); - return min == max ? min : `${min} - ${max}`; + const vector = value as unknown as { min: Date; max: Date }; + + const min = dateFormatter.format(vector.min); + const max = dateFormatter.format(vector.max); + + if (min == max) { + return min; + } + + return `${min} - ${max}`; }; } else if (cellType == "MONEY") { return (value) => { - const num = parseFloat(value as string) / 100; - return isNaN(num) - ? "" - : `${formatNumber(num, { forceFractionDigits: true })} ${ - currencyConfig.unit - }`; + if (value && !isNaN(value as unknown as number)) { + return currencyFormatter.format(value as unknown as number); + } + return ""; }; } else if (cellType == "BOOLEAN") { return (value) => (value ? t("common.true") : t("common.false")); } + + return (value) => (value ? (value as string) : ""); }, [currencyConfig.unit, t], ); const getRenderFunctionByFieldName = useCallback( - (fieldName: string): ((value: CellValue) => string) | undefined => { + (fieldName: string): ((value: CellValue) => string) => { const cellType = ( queryData as GetQueryResponseDoneT ).columnDescriptions?.find((x) => x.label == fieldName)?.type; + if (cellType) { return getRenderFunction(cellType); } + + return (value) => (value ? (value as string) : ""); }, [getRenderFunction, queryData], ); diff --git a/frontend/src/js/preview/util.ts b/frontend/src/js/preview/util.ts index dad51022b4..e4e777e4c3 100644 --- a/frontend/src/js/preview/util.ts +++ b/frontend/src/js/preview/util.ts @@ -1,11 +1,16 @@ -import { t } from "i18next"; import { BarStatistics, DateStatistics, PreviewStatistics } from "../api/types"; -import { parseDate } from "../common/helpers/dateHelper"; export const NUMBER_TYPES = ["NUMERIC", "INTEGER"]; export const NUMBER_STATISTICS_TYPES = [...NUMBER_TYPES, "MONEY"]; +export function currencyFromSymbol(symbol: string): string { + // TODO: this is a workaround until the backend sends currency-codes + if (symbol === "€") return "EUR"; + + return "USD"; +} + export function formatNumber( num: number, { @@ -19,22 +24,6 @@ export function formatNumber( }).format(num); } -export function formatDate(date: string | undefined) { - if (date) { - const parsedDate = parseDate(date, "yyyy-MM-dd"); - return parsedDate ? toFullLocaleDateString(parsedDate) : date; - } - return t("preview.dateError"); -} - -export function toFullLocaleDateString(date: Date) { - return date.toLocaleDateString(navigator.language, { - day: "2-digit", - month: "2-digit", - year: "numeric", - }); -} - export function previewStatsIsBarStats( stats: PreviewStatistics, ): stats is BarStatistics {