Skip to content

Commit

Permalink
fix: logic for 2D JSON data detection (#840)
Browse files Browse the repository at this point in the history
Only truly 2D JSON data should have a grid. This fixes erroneous DataGrid rendering
  • Loading branch information
jkaster authored Oct 4, 2021
1 parent eb1731f commit 3d18b93
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
testTextResponse,
testUnknownResponse,
testBogusJsonResponse,
testOneRowComplexJson,
} from '../../test-data'
import { ShowResponse } from './ShowResponse'

Expand All @@ -50,13 +51,20 @@ describe('ShowResponse', () => {
).toBeInTheDocument()
})

test('it renders json responses', () => {
test('it renders 2D json responses', () => {
renderWithTheme(<ShowResponse response={testJsonResponse} />)
const tab = screen.getByRole('tabpanel')
expect(tab).toHaveTextContent('"key1"')
expect(tab).toHaveTextContent('"value1"')
})

test('it renders no grid for one-row complex json', () => {
renderWithTheme(<ShowResponse response={testOneRowComplexJson} />)
expect(screen.queryByRole('tabpanel')).not.toBeInTheDocument()
expect(screen.getByText('"fields"')).toBeInTheDocument()
expect(screen.getByText('"orders.id"')).toBeInTheDocument()
})

test('it renders text responses', () => {
renderWithTheme(<ShowResponse response={testTextResponse} />)
expect(
Expand Down
88 changes: 75 additions & 13 deletions packages/run-it/src/components/ShowResponse/responseUtils.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,23 +32,23 @@ import {
testTextResponse,
testUnknownResponse,
} from '../../test-data'
import { isColumnar, pickResponseHandler } from './responseUtils'
import { isColumnar, pickResponseHandler, canTabulate } from './responseUtils'

describe('responseUtils', () => {
describe('isColumnar', () => {
test('detects simple 2D data', () => {
const inputs = [[1, 'two', false, new Date(), undefined, null]]
const actual = isColumnar(inputs)
const data = [[1, 'two', false, new Date(), undefined, null]]
const actual = isColumnar(data)
expect(actual).toEqual(true)
})
test('considers any non-Date object complex', () => {
const inputs = [[{ a: 'A', b: 'B' }, 'two', false, new Date()]]
const actual = isColumnar(inputs)
const data = [[{ a: 'A', b: 'B' }, 'two', false, new Date()]]
const actual = isColumnar(data)
expect(actual).toEqual(false)
})
test('considers empty object complex', () => {
const inputs = [[{}, 'two', false, new Date()]]
const actual = isColumnar(inputs)
const data = [[{}, 'two', false, new Date()]]
const actual = isColumnar(data)
expect(actual).toEqual(false)
})
test('considers any array complex', () => {
Expand All @@ -57,17 +57,79 @@ describe('responseUtils', () => {
expect(actual).toEqual(false)
})
test('considers a 1D array as non-columnar', () => {
const inputs = [[], 'two', false, new Date()]
const actual = isColumnar(inputs)
const data = [[], 'two', false, new Date()]
const actual = isColumnar(data)
expect(actual).toEqual(false)
})
test('considers an empty array as non-columnar', () => {
const inputs = [[]]
const actual = isColumnar(inputs)
const data = [[]]
const actual = isColumnar(data)
expect(actual).toEqual(false)
})
test('considers a uniform array of objects a table', () => {
const json = [{ key1: 'value1' }]
const actual = canTabulate(json)
expect(actual).toEqual(true)
})
test('considers create_query json as complex', () => {
const json = JSON.parse(`
{
"id": 520,
"view": "orders",
"fields": [
"orders.id",
"users.age",
"users.city"
],
"pivots": [],
"fill_fields": [],
"filters": null,
"filter_expression": "",
"sorts": [],
"limit": "",
"column_limit": "",
"total": null,
"row_total": "",
"subtotals": [],
"vis_config": null,
"filter_config": null,
"visible_ui_sections": "",
"slug": "64zJjJw",
"client_id": "zfn3SwIaaHbJTbsXSJ0JO7",
"share_url": "https://localhost:9999/x/zfn3SwIaaHbJTbsXSJ0JO7",
"expanded_share_url": "https://localhost:9999/explore/thelook/orders?fields=orders.id,users.age,users.city&origin=share-expanded",
"url": "/explore/thelook/orders?fields=orders.id,users.age,users.city",
"has_table_calculations": false,
"model": "thelook",
"dynamic_fields": "",
"query_timezone": "",
"quick_calcs": null,
"analysis_config": null,
"can": {
"run": true,
"see_results": true,
"explore": true,
"create": true,
"show": true,
"cost_estimate": true,
"index": true,
"see_lookml": true,
"see_aggregate_table_lookml": true,
"see_derived_table_lookml": true,
"see_sql": true,
"save": true,
"generate_drill_links": true,
"download": true,
"download_unlimited": true,
"use_custom_fields": true,
"schedule": true
}
} `)
const actual = canTabulate(json)
expect(actual).toEqual(false)
})
test('considers connection json as complex', () => {
const inputs = JSON.parse(`
const json = JSON.parse(`
[
{
"name": "looker_external",
Expand Down Expand Up @@ -149,7 +211,7 @@ describe('responseUtils', () => {
}
}]
`)
const actual = isColumnar(inputs)
const actual = canTabulate(json)
expect(actual).toEqual(false)
})
})
Expand Down
67 changes: 63 additions & 4 deletions packages/run-it/src/components/ShowResponse/responseUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,19 +73,78 @@ export const isColumnar = (data: any[]) => {
return !complex
}

enum ItemType {
Array = 'a',
Object = 'o',
Simple = 's',
Undefined = 'u',
}

/**
* Is this an array, an object, a value, or undefined
* @param value to check
*/
const itemType = (value: any): ItemType => {
if (!value) return ItemType.Undefined
if (Array.isArray(value)) return ItemType.Array
if (value instanceof Object) return ItemType.Object
return ItemType.Simple
}

/**
* Get the 2D type mapping for the object
* @param json to analyze
*/
const getTypes = (json: any) => {
const types = [new Set<ItemType>(), new Set<ItemType>()]
if (!json) {
types[0].add(ItemType.Undefined)
return types
}
for (const key of Object.keys(json)) {
const value = json[key]
const type = itemType(value)
types[0].add(type)
switch (type) {
case ItemType.Array:
case ItemType.Object:
Object.keys(value).forEach((k) => {
const v = value[k]
types[1].add(itemType(v))
})
break
}
}
return types
}

/**
* Is this a uniform object that can be converted into a table?
* @param json to analyze
*/
export const canTabulate = (json: any) => {
const types = getTypes(json)
return (
types[0].size === 1 &&
(types[0].has(ItemType.Array) || types[0].has(ItemType.Object)) &&
types[1].size <= 1
)
}

/**
* Show JSON responses
*
* Shows the JSON in a syntax-highlighted fashion
* If the JSON is parseable as 2D row/column data it will also be shown in grid
* If JSON cannot be parsed it will be show as is
* If JSON cannot be parsed it will be shown as is
* @param response
*/
const ShowJSON = (response: IRawResponse) => {
const content = response.body.toString()
const data = json2Csv(content)
const showGrid = isColumnar(data.data)
const json = JSON.stringify(JSON.parse(response.body), null, 2)
const parsed = JSON.parse(response.body)
const data = canTabulate(parsed) ? json2Csv(content) : undefined
const showGrid = data && isColumnar(data.data)
const json = JSON.stringify(parsed, null, 2)
const raw = copyRaw(json, 'json')
if (showGrid) return <DataGrid data={data.data} raw={raw} />
return raw
Expand Down
1 change: 1 addition & 0 deletions packages/run-it/src/test-data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,6 @@ export {
testUnknownResponse,
testErrorResponse,
testBogusJsonResponse,
testOneRowComplexJson,
} from './responses'
export { api } from './specs'
61 changes: 61 additions & 0 deletions packages/run-it/src/test-data/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,67 @@ export const testJsonResponse: IRawResponse = {
body: Buffer.from('[{"key1": "value1" }]'),
}

export const testOneRowComplexJson: IRawResponse = {
url: 'https://some/json/data',
headers: { 'content-type': 'application/json' },
contentType: 'application/json',
ok: true,
statusCode: 200,
statusMessage: 'OK',
body: Buffer.from(`{
"id": 520,
"view": "orders",
"fields": [
"orders.id",
"users.age",
"users.city"
],
"pivots": [],
"fill_fields": [],
"filters": null,
"filter_expression": "",
"sorts": [],
"limit": "",
"column_limit": "",
"total": null,
"row_total": "",
"subtotals": [],
"vis_config": null,
"filter_config": null,
"visible_ui_sections": "",
"slug": "64zJjJw",
"client_id": "zfn3SwIaaHbJTbsXSJ0JO7",
"share_url": "https://localhost:9999/x/zfn3SwIaaHbJTbsXSJ0JO7",
"expanded_share_url": "https://localhost:9999/explore/thelook/orders?fields=orders.id,users.age,users.city&origin=share-expanded",
"url": "/explore/thelook/orders?fields=orders.id,users.age,users.city",
"has_table_calculations": false,
"model": "thelook",
"dynamic_fields": "",
"query_timezone": "",
"quick_calcs": null,
"analysis_config": null,
"can": {
"run": true,
"see_results": true,
"explore": true,
"create": true,
"show": true,
"cost_estimate": true,
"index": true,
"see_lookml": true,
"see_aggregate_table_lookml": true,
"see_derived_table_lookml": true,
"see_sql": true,
"save": true,
"generate_drill_links": true,
"download": true,
"download_unlimited": true,
"use_custom_fields": true,
"schedule": true
}
}`),
}

export const testTextResponse: IRawResponse = {
url: 'https://some/text/data',
headers: { 'content-type': 'text/plain;charset=utf-8' },
Expand Down

0 comments on commit 3d18b93

Please sign in to comment.