Skip to content

Commit

Permalink
feat: simplify filtering
Browse files Browse the repository at this point in the history
  • Loading branch information
mdonnalley committed Aug 23, 2024
1 parent e7057fb commit bf20670
Show file tree
Hide file tree
Showing 5 changed files with 42 additions and 114 deletions.
23 changes: 13 additions & 10 deletions examples/basic.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ansis from 'ansis'
import {pathToFileURL} from 'node:url'
import terminalLink from 'terminal-link'

import {makeTable} from '../src/index.js'
Expand Down Expand Up @@ -867,7 +868,12 @@ const versions = [
'https://developer.salesforce.com/media/salesforce-cli/sf/versions/2.38.6/1d0ec8e/sf-v2.38.6-1d0ec8e-darwin-arm64.tar.gz',
version: '2.38.6',
},
]
].map((v) => ({
...v,
location: v.location.startsWith('http')
? terminalLink(new URL(v.location).pathname.split('/').at(-1) ?? v.location, v.location)
: v.location,
}))

const deployResult = [
{
Expand Down Expand Up @@ -1840,27 +1846,24 @@ makeTable({
borderStyle: 'headers-only-with-underline',
columns: ['version', 'location', 'channel'],
data: versions,
filter: {
channel: /^.+$/,
// version: /^2.5\d/,
},
// filter: (row) => /^.+$/.test(row.channel),
headerOptions: {
bold: true,
color: '#905de8',
formatter: 'capitalCase',
},
overflow: 'wrap',
// sort: {
// channel: 'desc',
// },
})

// console.log()
makeTable({
align: 'left',
borderStyle: 'headers-only-with-underline',
columns: ['state', 'fullName', 'type'],
data: deployResult,
filter: {
state: 'Changed',
type: /^A/,
},
filter: (row) => row.state === 'Changed' && row.type.startsWith('A'),
headerOptions: {
bold: true,
color: 'blueBright',
Expand Down
42 changes: 24 additions & 18 deletions src/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import stripAnsi from 'strip-ansi'

import {BORDER_SKELETONS} from './skeletons.js'
import {CellProps, Column, Config, Percentage, RowConfig, RowProps, ScalarDict, TableProps} from './types.js';
import {allKeysInCollection, filterData, getColumns, getHeadings, intersperse, sortData, truncate, wrap} from './utils.js';
import {allKeysInCollection, getColumns, getHeadings, intersperse, sortData, truncate, wrap} from './utils.js';

function determineConfiguredWidth(providedWidth: number | Percentage | undefined): number {
if (!providedWidth) return process.stdout.columns
Expand Down Expand Up @@ -38,22 +38,31 @@ function determineWidthToUse<T>(columns: Column<T>[], configuredWidth: number):
}

export function Table<T extends ScalarDict>(props: Pick<TableProps<T>, 'data'> & Partial<TableProps<T>>) {
const {
align = 'left',
borderStyle = 'all',
data,
filter,
headerOptions = {bold: true, color: 'blue'},
maxWidth,
overflow = 'truncate',
padding = 1,
sort,
} = props

const config: Config<T> = {
align: props.align ?? 'left',
borderStyle: props.borderStyle ?? 'all',
columns: props.columns ?? allKeysInCollection(props.data),
data: props.data,
headerOptions: props.headerOptions ?? {
bold: true,
color: 'blue',
},
maxWidth: determineConfiguredWidth(props.maxWidth),
overflow: props.overflow ?? 'truncate',
padding: props.padding ?? 1,
align,
borderStyle,
columns: props.columns ?? allKeysInCollection(data),
data,
headerOptions,
maxWidth: determineConfiguredWidth(maxWidth),
overflow,
padding,
}
const columns = getColumns(config)
const headings = getHeadings(config)
const data = sortData(filterData(props.data, props.filter ?? {}), props.sort)
const processedData = sortData(filter ? data.filter((row) => filter(row)) : data, sort)

const dataComponent = row<T>({
cell: Cell,
Expand Down Expand Up @@ -100,11 +109,11 @@ export function Table<T extends ScalarDict>(props: Pick<TableProps<T>, 'data'> &
})

return (
<Box flexDirection="column" width={determineWidthToUse(columns, config.maxWidth)}>
<Box flexDirection="column" width={determineWidthToUse(columns, config.maxWidth)} paddingBottom={1}>
{headerComponent({columns, data: {}, key: 'header'})}
{headingComponent({columns, data: headings, key: 'heading'})}
{headerFooterComponent({columns, data: {}, key: 'footer'})}
{data.map((row, index) => {
{processedData.map((row, index) => {
// Calculate the hash of the row based on its value and position
const key = `row-${sha1(row)}-${index}`

Expand Down Expand Up @@ -188,11 +197,8 @@ function row<T extends ScalarDict>(config: RowConfig): (props: RowProps<T>) => R

return (
<Box flexDirection="row">
{/* Left */}
<Skeleton height={height}>{skeleton.left}</Skeleton>
{/* Data */}
{...elements}
{/* Right */}
<Skeleton height={height}>{skeleton.right}</Skeleton>
</Box>
)
Expand Down
23 changes: 4 additions & 19 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,31 +108,16 @@ export type TableProps<T extends ScalarDict> = {
*/
align?: ColumnAlignment
/**
* Filter the data in the table.
*
* Each key in the object should correspond to a column in the table. The value can be a string or a regular expression.
*
* @example
* ```js
* const data = [
* {name: 'Alice', age: 30},
* {name: 'Bob', age: 25},
* {name: 'Charlie', age: 35},
* ]
*
* // filter the name column with a string
* makeTable({data, filter: {name: 'Alice'}})
*
* // filter the name column with a regular expression
* makeTable({data, filter: {name: /^A/}})
* ```
* Apply a filter to each row in the table.
*/
filter?: Partial<Record<keyof T, boolean | string | RegExp>>
filter?: (row: T) => boolean
/**
* Sort the data in the table.
*
* Each key in the object should correspond to a column in the table. The value can be 'asc' or 'desc'.
*
* The order of the keys determines the order of the sorting. The first key is the primary sort key, the second key is the secondary sort key, and so on.
*
* @example
* ```js
* const data = [
Expand Down
24 changes: 0 additions & 24 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,30 +27,6 @@ export function intersperse<T, I>(intersperser: (index: number) => I, elements:
return interspersed
}

export function filterData<T extends ScalarDict>(
data: T[],
filter: Partial<Record<keyof T, boolean | string | RegExp>>,
): T[] {
return data.filter((row) => {
for (const key in row) {
if (key in row) {
const f = filter[key]
const value = stripAnsi(String(row[key]))
if (f !== undefined && typeof f === 'boolean') {
const convertedBoolean = value === 'true' ? true : value === 'false' ? false : value
return f === convertedBoolean
}

if (!f) continue
if (typeof f === 'string' && !value.includes(f)) return false
if (f instanceof RegExp && !f.test(value)) return false
}
}

return true
})
}

export function sortData<T extends ScalarDict>(
data: T[],
sort?: Partial<Record<keyof T, 'asc' | 'desc'>> | undefined,
Expand Down
44 changes: 1 addition & 43 deletions test/util.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {config, expect} from 'chai'

import {filterData, intersperse, sortData, truncate, wrap} from '../src/utils.js'
import {intersperse, sortData, truncate, wrap} from '../src/utils.js'

config.truncateThreshold = 0

Expand All @@ -12,48 +12,6 @@ describe('intersperse', () => {
})
})

describe('filterData', () => {
it('should filter data using string', () => {
const data = [
{age: 30, name: 'Alice'},
{age: 25, name: 'Bob'},
{age: 35, name: 'Charlie'},
]
const filter = {name: 'Alice'}
const expected = [{age: 30, name: 'Alice'}]
expect(filterData(data, filter)).to.deep.equal(expected)
})

it('should filter data using regular expression', () => {
const data = [
{age: 30, name: 'Alice'},
{age: 25, name: 'Bob'},
{age: 35, name: 'Charlie'},
]
const filter = {name: /^A/}
const expected = [{age: 30, name: 'Alice'}]
expect(filterData(data, filter)).to.deep.equal(expected)
})

it('should filter data using boolean', () => {
const data = [
{age: 30, employed: true, name: 'Alice'},
{age: 25, employed: false, name: 'Bob'},
{age: 35, employed: false, name: 'Charlie'},
]
const filter = {employed: true}
const expected = [{age: 30, employed: true, name: 'Alice'}]
expect(filterData(data, filter)).to.deep.equal(expected)
})

it('should filter despite ansi characters', () => {
const data = [{name: '\u001B[31mAlice\u001B[39m'}]
const filter = {name: 'Alice'}
const expected = data
expect(filterData(data, filter)).to.deep.equal(expected)
})
})

describe('sortData', () => {
it('should sort data in ascending order', () => {
const data = [
Expand Down

0 comments on commit bf20670

Please sign in to comment.