Skip to content

Commit

Permalink
Merge pull request #21 from oclif/mdonnalley/size-issues
Browse files Browse the repository at this point in the history
fix: handle very tall and very wide tables
  • Loading branch information
iowillhoit authored Oct 17, 2024
2 parents 9584c5b + fb81cf6 commit 16ea9d8
Show file tree
Hide file tree
Showing 6 changed files with 350 additions and 30 deletions.
8 changes: 4 additions & 4 deletions examples/sf-specific/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -941,23 +941,23 @@ const deployResult = [
]

const deploy: TableOptions<(typeof deployResult)[number]> = {
borderStyle: 'vertical',
borderStyle: 'vertical-with-outline',
columns: [
'state',
'fullName',
'type',
{
key: 'filePath',
name: 'Path',
overflow: 'truncate',
overflow: 'wrap',
},
],
data: deployResult,
filter: (row) => row.state === 'Changed' && row.type.startsWith('A'),
headerOptions: {
color: 'white',
// color: 'white',
formatter: 'capitalCase',
inverse: true,
// inverse: true,
},
maxWidth: '100%',
overflow: 'truncate',
Expand Down
168 changes: 168 additions & 0 deletions examples/sf-specific/org-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import {printTable} from '../../src'
const data = [
{
alias: 'devhub',
connectedStatus: 'Connected',
defaultMarker: undefined,
instanceApiVersion: '62.0',
instanceApiVersionLastRetrieved: '10/9/2024, 12:44:28 PM',
instanceUrl: 'https://su0503.my.salesforce.com',
isDefaultDevHubUsername: false,
isDefaultUsername: false,
isDevHub: true,
lastUsed: '2024-10-09T18:44:31.494Z',
loginUrl: 'https://login.salesforce.com',
orgId: '00DB0000000Ih65MAC',
timestamp: '2022-05-10T19:26:45.436Z',
type: 'DevHub',
username: '[email protected]',
},
{
alias: 'na40devhub',
connectedStatus: 'Connected',
defaultMarker: '🌳',
instanceApiVersion: '61.0',
instanceApiVersionLastRetrieved: '10/9/2024, 12:44:29 PM',
instanceUrl: 'https://na40-dev-hub.my.salesforce.com',
isDefaultDevHubUsername: true,
isDefaultUsername: false,
isDevHub: true,
lastUsed: '2024-10-09T18:44:31.495Z',
loginUrl: 'https://login.salesforce.com',
orgId: '00D460000019MkyEAE',
privateKey: '/Users/mdonnalley/secrets/jwt/na40.key',
type: 'DevHub',
username: '[email protected]',
},
{
alias: undefined,
connectedStatus:
'Unable to refresh session due to: Error authenticating with the refresh token due to: expired access/refresh token',
defaultMarker: undefined,
instanceApiVersion: '62.0',
instanceApiVersionLastRetrieved: '10/9/2024, 12:44:28 PM',
instanceUrl: 'https://cristianalexisdominguez-devhub.my.salesforce.com',
isDefaultDevHubUsername: false,
isDefaultUsername: false,
isDevHub: true,
lastUsed: '2024-10-09T18:44:29.234Z',
loginUrl: 'https://cristianalexisdominguez-devhub.my.salesforce.com/',
orgId: '00DB00000006Mq3MAE',
type: 'DevHub',
username: '[email protected]',
},
{
alias: undefined,
connectedStatus:
'Unable to refresh session due to: Error authenticating with the refresh token due to: expired access/refresh token',
defaultMarker: undefined,
instanceApiVersion: '62.0',
instanceApiVersionLastRetrieved: '10/9/2024, 12:44:28 PM',
instanceUrl: 'https://su0503.my.salesforce.com',
isDefaultDevHubUsername: false,
isDefaultUsername: false,
isDevHub: true,
lastUsed: '2024-10-09T18:44:29.986Z',
loginUrl: 'https://login.salesforce.com',
orgId: '00DB0000000Ih65MAC',
type: 'DevHub',
username: '[email protected]',
},
{
alias: undefined,
connectedStatus: 'Connected',
defaultMarker: undefined,
instanceApiVersion: '62.0',
instanceApiVersionLastRetrieved: '10/9/2024, 12:44:28 PM',
instanceUrl: 'https://su0503--sbxgs01.sandbox.my.salesforce.com',
isDefaultDevHubUsername: false,
isDefaultUsername: false,
isDevHub: false,
isSandbox: true,
isScratch: false,
lastUsed: '2024-10-09T18:47:21.186Z',
loginUrl: 'https://test.salesforce.com',
orgId: '00D3I0000008poXUAQ',
tracksSource: false,
type: 'Sandbox',
username: '[email protected]',
},
{
alias: 'ink',
connectedStatus: 'Active',
created: '1728401061000',
createdBy: '[email protected]',
createdDate: '2024-10-08T15:24:21.000+0000',
createdOrgInstance: 'USA256S',
defaultMarker: '🍁',
devHubId: '00DB0000000Ih65MAC',
devHubOrgId: '00DB0000000Ih65MAC',
devHubUsername: '[email protected]',
edition: 'Developer',
expirationDate: '2024-10-09',
instanceApiVersion: '62.0',
instanceApiVersionLastRetrieved: '10/9/2024, 12:23:19 PM',
instanceUrl: 'https://nosoftware-platform-8292-dev-ed.scratch.my.salesforce.com',
isDefaultDevHubUsername: false,
isDefaultUsername: true,
isDevHub: false,
isExpired: false,
isSandbox: false,
isScratch: true,
lastUsed: '2024-10-09T18:44:31.494Z',
loginUrl: 'https://nosoftware-platform-8292-dev-ed.scratch.my.salesforce.com',
namespace: null,
orgId: '00DO2000004SuOLMA0',
orgName: 'Company',
signupUsername: '[email protected]',
status: 'Active',
tracksSource: true,
type: 'Scratch',
username: '[email protected]',
},
]

printTable({
borderStyle: 'vertical-with-outline',
columns: [
{
key: 'defaultMarker',
name: ' ',
},
'type',
'alias',
'username',
{
key: 'instanceUrl',
name: 'Instance URL',
},
{
key: 'orgId',
name: 'Org ID',
},
{
key: 'connectedStatus',
name: 'Status',
},
'namespace',
{
key: 'devHubId',
name: 'Devhub ID',
},
{
key: 'createdDate',
name: 'Created',
},
{
key: 'expirationDate',
name: 'Expires',
},
],
data,
headerOptions: {
formatter: 'capitalCase',
},
maxWidth: '100%',
overflow: 'wrap',
verticalAlignment: 'center',
})
15 changes: 15 additions & 0 deletions examples/very-tall.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {printTable} from '../src/index.js'

const height = 100_000
const data = Array.from({length: height}, (_, i) => ({age: i, name: `Foo ${i}`}))

printTable({
columns: ['name', 'age'],
data,
headerOptions: {
formatter: 'capitalCase',
},
horizontalAlignment: 'center',
title: 'Very Tall',
titleOptions: {bold: true},
})
128 changes: 125 additions & 3 deletions src/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ export function formatTextWithMargins({
}
}

export function Table<T extends Record<string, unknown>>(props: TableOptions<T>) {
function setup<T extends Record<string, unknown>>(props: TableOptions<T>) {
const {
data,
filter,
Expand Down Expand Up @@ -219,6 +219,12 @@ export function Table<T extends Record<string, unknown>>(props: TableOptions<T>)

const headings = getHeadings(config)
const columns = getColumns(config, headings)
// check for duplicate columns
const columnKeys = columns.map((c) => c.key)
const duplicates = columnKeys.filter((c, i) => columnKeys.indexOf(c) !== i)
if (duplicates.length > 0) {
throw new Error(`Duplicate columns found: ${duplicates.join(', ')}`)
}

const dataComponent = row<T>({
borderProps,
Expand Down Expand Up @@ -264,6 +270,38 @@ export function Table<T extends Record<string, unknown>>(props: TableOptions<T>)
skeleton: BORDER_SKELETONS[config.borderStyle].separator,
})

return {
columns,
config,
dataComponent,
footerComponent,
headerComponent,
headerFooterComponent,
headingComponent,
headings,
processedData,
separatorComponent,
title,
titleOptions,
}
}

export function Table<T extends Record<string, unknown>>(props: TableOptions<T>) {
const {
columns,
config,
dataComponent,
footerComponent,
headerComponent,
headerFooterComponent,
headingComponent,
headings,
processedData,
separatorComponent,
title,
titleOptions,
} = setup(props)

return (
<Box flexDirection="column" width={determineWidthToUse(columns, config.maxWidth)}>
{title && <Text {...titleOptions}>{title}</Text>}
Expand Down Expand Up @@ -395,7 +433,12 @@ class Stream extends WriteStream {
private frames: string[] = []

public lastFrame(): string | undefined {
return this.frames.filter((f) => stripAnsi(f) !== '').at(-1)
return this.frames
.filter((f) => {
const stripped = stripAnsi(f)
return stripped !== '' && stripped !== '\n'
})
.at(-1)
}

write(data: string): boolean {
Expand All @@ -413,18 +456,93 @@ class Output {

public maybePrintLastFrame() {
if (this.stream instanceof Stream) {
process.stdout.write(`${this.stream.lastFrame()}\n`)
process.stdout.write(`${this.stream.lastFrame()}`)
} else {
process.stdout.write('\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 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)

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()
}

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()
}

/**
* Renders a table with the given data.
* @param options see {@link TableOptions}
*/
export function printTable<T extends Record<string, unknown>>(options: TableOptions<T>): void {
if (options.data.length > 50_000) {
renderTableInChunks(options)
return
}

const output = new Output()
const instance = render(<Table {...options} />, {stdout: output.stream})
instance.unmount()
Expand All @@ -443,6 +561,10 @@ export function printTables<T extends Record<string, unknown>[]>(
tables: {[P in keyof T]: TableOptions<T[P]>},
options?: Omit<ContainerProps, 'children'>,
): void {
if (tables.reduce((acc, table) => acc + table.data.length, 0) > 30_000) {
throw new Error('The data is too large to print multiple tables. Please use `printTable` instead.')
}

const output = new Output()
const leftMargin = options?.marginLeft ?? options?.margin ?? 0
const rightMargin = options?.marginRight ?? options?.margin ?? 0
Expand Down
Loading

0 comments on commit 16ea9d8

Please sign in to comment.