Skip to content

Commit

Permalink
feat: dont use ink when size in greater than 10k (#32)
Browse files Browse the repository at this point in the history
* feat: dont use ink when size in greater than 10k

* feat: use plain table in CI

* test: bypass CI check
  • Loading branch information
mdonnalley authored Oct 29, 2024
1 parent 6853528 commit 8c1371e
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 72 deletions.
121 changes: 49 additions & 72 deletions src/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import cliTruncate from 'cli-truncate'
import {Box, Text, render} from 'ink'
import {EventEmitter} from 'node:events'
import {env} from 'node:process'
import {sha1} from 'object-hash'
import React from 'react'
import stripAnsi from 'strip-ansi'
Expand All @@ -29,6 +30,7 @@ import {
getHeadings,
intersperse,
maybeStripAnsi,
shouldUsePlainTable,
sortData,
} from './utils.js'

Expand Down Expand Up @@ -436,7 +438,7 @@ export function Skeleton(props: React.PropsWithChildren & {readonly height?: num
}

type FakeStdout = {
lastFrame: () => string | undefined
lastFrame: () => string
} & NodeJS.WriteStream

/**
Expand Down Expand Up @@ -482,99 +484,74 @@ class Output {
this.stream = createStdout()
}

public maybePrintLastFrame() {
public printLastFrame() {
process.stdout.write(`${this.stream.lastFrame()}\n`)
}
}

function chunk<T>(array: T[], size: number): T[][] {
return array.reduce((acc, _, i) => {
if (i % size === 0) acc.push(array.slice(i, i + size))
return acc
}, [] as T[][])
}
function renderPlainTable<T extends Record<string, unknown>>(props: TableOptions<T>): void {
const {columns, headings, processedData, title} = setup(props)

if (title) console.log(title)
const headerString = columns.reduce((acc, column) => {
const {horizontalAlignment, overflow, padding, width} = column
const {marginLeft, marginRight, text} = formatTextWithMargins({
horizontalAlignment,
overflow,
padding,
value: column.column,
width,
})
const columnHeader = headings[text] ?? text
return `${acc}${' '.repeat(marginLeft)}${columnHeader}${' '.repeat(marginRight)}`
}, '')
console.log(headerString)
console.log('-'.repeat(headerString.length))

function renderTableInChunks<T extends Record<string, unknown>>(props: TableOptions<T>): void {
const {
columns,
config,
dataComponent,
footerComponent,
headerComponent,
headerFooterComponent,
headingComponent,
headings,
processedData,
separatorComponent,
title,
titleOptions,
} = setup(props)
for (const row of processedData) {
const stringToPrint = columns.reduce((acc, column) => {
const {horizontalAlignment, overflow, padding, width} = column
const value = row[column.column]

const headerOutput = new Output()
const headerInstance = render(
<Box flexDirection="column" width={determineWidthToUse(columns, config.maxWidth)}>
{title && <Text {...titleOptions}>{title}</Text>}
{headerComponent({columns, data: {}, key: 'header'})}
{headingComponent({columns, data: headings, key: 'heading'})}
{headerFooterComponent({columns, data: {}, key: 'footer'})}
</Box>,
{stdout: headerOutput.stream},
)
headerInstance.unmount()
headerOutput.maybePrintLastFrame()

const chunks = chunk(processedData, Math.max(1, Math.floor(process.stdout.rows / 2)))
for (const chunk of chunks) {
const chunkOutput = new Output()
const instance = render(
<Box flexDirection="column" width={determineWidthToUse(columns, config.maxWidth)}>
{chunk.map((row, index) => {
// Calculate the hash of the row based on its value and position
const key = `row-${sha1(row)}-${index}`
// Construct a row.
return (
<Box key={key} flexDirection="column">
{separatorComponent({columns, data: {}, key: `separator-${key}`})}
{dataComponent({columns, data: row, key: `data-${key}`})}
</Box>
)
})}
</Box>,
{stdout: chunkOutput.stream},
)
instance.unmount()
chunkOutput.maybePrintLastFrame()
if (value === undefined || value === null) {
return `${acc}${' '.repeat(width)}`
}

const {marginLeft, marginRight, text} = formatTextWithMargins({
horizontalAlignment,
overflow,
padding,
value,
width,
})

return `${acc}${' '.repeat(marginLeft)}${text}${' '.repeat(marginRight)}`
}, '')
console.log(stringToPrint)
}

const footerOutput = new Output()
const footerInstance = render(
<Box flexDirection="column" width={determineWidthToUse(columns, config.maxWidth)}>
{footerComponent({columns, data: {}, key: 'footer'})}
</Box>,
{stdout: footerOutput.stream},
)
footerInstance.unmount()
footerOutput.maybePrintLastFrame()
console.log()
}

/**
* Prints a table based on the provided options. If the data length exceeds 50,000 entries,
* the table is rendered in chunks to handle large datasets efficiently.
* Prints a table based on the provided options. If the data length exceeds 10,000 entries,
* the table is rendered in a non-styled format to avoid memory issues.
*
* @template T - A generic type that extends a record with string keys and unknown values.
* @param {TableOptions<T>} options - The options for rendering the table, including data and other configurations.
* @returns {void}
*/
export function printTable<T extends Record<string, unknown>>(options: TableOptions<T>): void {
if (options.data.length > 50_000) {
renderTableInChunks(options)
const limit = Number.parseInt(env.OCLIF_TABLE_LIMIT ?? env.SF_TABLE_LIMIT ?? '10000', 10) ?? 10_000
if (options.data.length >= limit || shouldUsePlainTable()) {
renderPlainTable(options)
return
}

const output = new Output()
const instance = render(<Table {...options} />, {stdout: output.stream})
instance.unmount()
output.maybePrintLastFrame()
output.printLastFrame()
}

/**
Expand Down Expand Up @@ -635,5 +612,5 @@ export function printTables<T extends Record<string, unknown>[]>(
{stdout: output.stream},
)
instance.unmount()
output.maybePrintLastFrame()
output.printLastFrame()
}
25 changes: 25 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {camelCase, capitalCase, constantCase, kebabCase, pascalCase, sentenceCase, snakeCase} from 'change-case'
import {orderBy} from 'natural-orderby'
import {env} from 'node:process'
import stripAnsi from 'strip-ansi'

import {Column, ColumnProps, Config, Sort} from './types.js'
Expand Down Expand Up @@ -179,3 +180,27 @@ export function maybeStripAnsi<T extends Record<string, unknown>[]>(data: T, noS

return newData as T
}

function isTruthy(value: string | undefined): boolean {
return value !== '0' && value !== 'false'
}

/**
* Determines whether the plain text table should be used.
*
* If the OCLIF_TABLE_SKIP_CI_CHECK environment variable is set to a truthy value, the CI check will be skipped.
*
* If the CI environment variable is set, the plain text table will be used.
*
* @returns {boolean} True if the plain text table should be used, false otherwise.
*/
export function shouldUsePlainTable(): boolean {
if (env.OCLIF_TABLE_SKIP_CI_CHECK && isTruthy(env.OCLIF_TABLE_SKIP_CI_CHECK)) return false
// Inspired by https://github.com/sindresorhus/is-in-ci
if (
isTruthy(env.CI) &&
('CI' in env || 'CONTINUOUS_INTEGRATION' in env || Object.keys(env).some((key) => key.startsWith('CI_')))
)
return true
return false
}
2 changes: 2 additions & 0 deletions test/table.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {Cell, Header, Skeleton, Table, formatTextWithMargins, printTable} from '

config.truncateThreshold = 0

process.env.OCLIF_TABLE_SKIP_CI_CHECK = 'true'

// Helpers -------------------------------------------------------------------

const skeleton = (v: string) => <Skeleton>{v}</Skeleton>
Expand Down

0 comments on commit 8c1371e

Please sign in to comment.