diff --git a/package-lock.json b/package-lock.json index 346e4361..00d16eaa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18155,9 +18155,9 @@ } }, "jsonpath-plus": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-5.0.4.tgz", - "integrity": "sha512-w3pI3PewtwIrrGRCFvTkCkKu8IrOwjsqoYRxvxuXQjPB0udEtAuBY0B6/SEztsxMmuIHVHGFQ0knVnTCPW9qYw==" + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-5.0.7.tgz", + "integrity": "sha512-7TS6wsiw1s2UMK/A6nA4n0aUJuirCVhJ87nWX5je5MPOl0z5VTr2qs7nMP8NZ2ed3rlt6kePTqddgVPE9F0i0w==" }, "jsprim": { "version": "1.4.1", diff --git a/package.json b/package.json index 871941ba..0707cec4 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "color": "^3.1.3", "framer-motion": "^2.9.5", "fuse.js": "^6.4.2", - "jsonpath-plus": "^5.0.4", + "jsonpath-plus": "^5.0.7", "lodash": "^4.17.20", "mark.js": "^8.11.1", "moment": "^2.29.1", diff --git a/src/__snapshots__/storybook.test.ts.snap b/src/__snapshots__/storybook.test.ts.snap index d9fcd830..b6973cc2 100644 --- a/src/__snapshots__/storybook.test.ts.snap +++ b/src/__snapshots__/storybook.test.ts.snap @@ -1047,7 +1047,8 @@ exports[`Storyshots Code Default 1`] = ` "attachedResources": { "count-of-attached-eni": 3, "does-any-eni-have-public-ip": true, - "are-there-flows-from-internet-in-vpcflows": false + "are-there-flows-from-internet-in-vpcflows": false, + "a-really-really-really-really-really-really-really-really-really-really-really-really-really-really-really-reallyreally-really-really-long-key": null } } }, @@ -1526,6 +1527,720 @@ exports[`Storyshots Input Placeholder 1`] = ` `; +exports[`Storyshots JSONPathPicker Default 1`] = ` +
+
+ +
+
+
+ + + + + +
+
+ + { + +
    +
  • + + "dassana" + + + : + + + { + +
      +
    • + + "context" + + + : + + + { + +
        +
      • + + "attachedResources" + + + : + + + { + +
          +
        • + + "count-of-attached-eni" + + + : + + + 3 + + + , + +
        • +
        • + + "does-any-eni-have-public-ip" + + + : + + + true + + + , + +
        • +
        • + + "are-there-flows-from-internet-in-vpcflows" + + + : + + + false + +
        • +
        + + + } + + +
      • +
      + + + } + + +
    • +
    + + + } + + + , + + +
  • +
  • + + "resourceConfig" + + + : + + + { + +
      +
    • + + "configuration" + + + : + + + { + +
        +
      • + + "description" + + + : + + + "ssh-from-world" + + + , + +
      • +
      • + + "groupName" + + + : + + + "prod-cache" + + + , + +
      • +
      • + + "ipPermissions" + + + : + + + + [ + + +
          +
        1. + + { + +
            +
          • + + "fromPort" + + + : + + + 22 + + + , + +
          • +
          • + + "ipProtocol" + + + : + + + "tcp" + + + , + +
          • +
          • + + "ipv6Ranges" + + + : + + + + [ + + +
              +
            1. + + { + +
                +
              • + + "cidrIpv6" + + + : + + + "::/0" + +
              • +
              + + + } + + +
            2. +
            + + + ] + + + , + + +
          • +
          • + + "prefixListIds" + + + : + + + + [ + + + ] + + + , + + +
          • +
          • + + "toPort" + + + : + + + 22 + + + , + +
          • +
          • + + "userIdGroupPairs" + + + : + + + + [ + + + ] + + + , + + +
          • +
          • + + "ipv4Ranges" + + + : + + + + [ + + +
              +
            1. + + { + +
                +
              • + + "cidrIp" + + + : + + + "0.0.0.0/0" + +
              • +
              + + + } + + +
            2. +
            + + + ] + + +
          • +
          + + + } + + +
        2. +
        + + + ] + + +
      • +
      + + + } + + + , + + +
    • +
    • + + "a-really-really-really-really-really-really-really-really-really-really-really-really-really-really-really-reallyreally-really-really-long-key" + + + : + + + null + +
    • +
    + + + } + + +
  • +
+ + + } + + +
+
+
+
+`; + exports[`Storyshots Link Click 1`] = `

Click on the bordered box to use keyboard shortcuts

Press Up, Down, Left, Right arrow keys to navigate. Press enter or alphabet keys to select/deselect.

A
CISO
B
Sr Leadership
C
SecOps
D
Cloud Architect
E
DevOps
F
NetSec
G
AppDev
H
Compliance
I
Other @@ -1851,24 +2566,24 @@ exports[`Storyshots MultipleChoice Single 1`] = ` className="light storyWrapper-0-2-2" >

Click on the bordered box to use keyboard shortcuts

Press Up, Down, Left, Right arrow keys to navigate. Press enter or alphabet keys to select/deselect.

A
CISO
B
Sr Leadership
C
SecOps
D
Cloud Architect
E
DevOps
F
NetSec
G
AppDev
H
Compliance
I
Other @@ -2087,24 +2802,24 @@ exports[`Storyshots MultipleChoice Single Column 1`] = ` className="light storyWrapper-0-2-2" >

Click on the bordered box to use keyboard shortcuts

Press Up, Down, Left, Right arrow keys to navigate. Press enter or alphabet keys to select/deselect.

A
SecOps
B
Cloud Architect
C
DevOps
D
NetSec
E
AppDev
F
Compliance
G
Other @@ -2272,7 +2987,7 @@ exports[`Storyshots Notification Error 1`] = `
`; @@ -2294,7 +3009,7 @@ exports[`Storyshots Notification Info 1`] = `
`; @@ -2316,7 +3031,7 @@ exports[`Storyshots Notification Success 1`] = `
`; @@ -2338,7 +3053,7 @@ exports[`Storyshots Notification Warning 1`] = `
`; @@ -2424,7 +3139,7 @@ exports[`Storyshots PageLoader Default 1`] = ` className="light storyWrapper-0-2-2" >
loading_lightbulb.svg @@ -2598,31 +3313,31 @@ exports[`Storyshots Radio Group Loading 1`] = ` className="light storyWrapper-0-2-2" >
 
 
  @@ -2750,7 +3465,7 @@ exports[`Storyshots Select Default 1`] = ` className="decorator-0-2-3" >
Lorem @@ -2960,7 +3675,7 @@ exports[`Storyshots Select Icon 1`] = ` className="decorator-0-2-3" >
AWS @@ -3082,7 +3797,7 @@ exports[`Storyshots Select Search 1`] = ` className="decorator-0-2-3" >
press enter @@ -3310,19 +4025,19 @@ exports[`Storyshots ShortcutMicrocopy Icon Only 1`] = ` className="light storyWrapper-0-2-2" >
press @@ -3336,24 +4051,24 @@ exports[`Storyshots ShortcutMicrocopy Mixed 1`] = ` className="light storyWrapper-0-2-2" >
press cmd @@ -3361,12 +4076,12 @@ exports[`Storyshots ShortcutMicrocopy Mixed 1`] = ` + Enter
press cmd @@ -3423,12 +4138,12 @@ exports[`Storyshots ShortcutMicrocopy Predefined Keys 1`] = ` + enter @@ -3442,7 +4157,7 @@ exports[`Storyshots Skeleton Circle 1`] = ` className="light storyWrapper-0-2-2" >   @@ -3454,27 +4169,27 @@ exports[`Storyshots Skeleton Count 1`] = ` className="light storyWrapper-0-2-2" >           @@ -3486,7 +4201,7 @@ exports[`Storyshots Skeleton Default 1`] = ` className="light storyWrapper-0-2-2" >   @@ -3498,20 +4213,20 @@ exports[`Storyshots TableDrawer Simple Drawer 1`] = ` className="light storyWrapper-0-2-2" >
vpc-flow-logs-are-disabled @@ -3850,10 +4565,10 @@ exports[`Storyshots TableDrawer Simple Drawer 1`] = ` style={Object {}} >
FlowLog @@ -3867,10 +4582,10 @@ exports[`Storyshots TableDrawer Simple Drawer 1`] = ` style={Object {}} >
visibility @@ -3879,7 +4594,7 @@ exports[`Storyshots TableDrawer Simple Drawer 1`] = `
instance-is-exposed-to-internet @@ -3923,10 +4638,10 @@ exports[`Storyshots TableDrawer Simple Drawer 1`] = ` style={Object {}} >
instance @@ -3940,10 +4655,10 @@ exports[`Storyshots TableDrawer Simple Drawer 1`] = ` style={Object {}} >
network @@ -3952,7 +4667,7 @@ exports[`Storyshots TableDrawer Simple Drawer 1`] = `
default-security-group-should-not-allow-traffic @@ -3996,10 +4711,10 @@ exports[`Storyshots TableDrawer Simple Drawer 1`] = ` style={Object {}} >
security-group @@ -4013,10 +4728,10 @@ exports[`Storyshots TableDrawer Simple Drawer 1`] = ` style={Object {}} >
networking @@ -4025,7 +4740,7 @@ exports[`Storyshots TableDrawer Simple Drawer 1`] = `
ssh-from-internet @@ -4069,10 +4784,10 @@ exports[`Storyshots TableDrawer Simple Drawer 1`] = ` style={Object {}} >
security-group @@ -4086,10 +4801,10 @@ exports[`Storyshots TableDrawer Simple Drawer 1`] = ` style={Object {}} >
network @@ -4098,7 +4813,7 @@ exports[`Storyshots TableDrawer Simple Drawer 1`] = `
ebs-volume-is-not-encrypted @@ -4142,10 +4857,10 @@ exports[`Storyshots TableDrawer Simple Drawer 1`] = ` style={Object {}} >
volume @@ -4159,10 +4874,10 @@ exports[`Storyshots TableDrawer Simple Drawer 1`] = ` style={Object {}} >
cryptography @@ -4432,7 +5147,7 @@ exports[`Storyshots TimeInput Error 1`] = ` className="decorator-0-2-3" >
= ({ const [isCopied, setIsCopied] = useState(false) - const copyCode = () => { + const copyCode = () => copyToClipboard(stringifyCode(code), () => setIsCopied(true)) - } useEffect(() => { if (isCopied) setTimeout(() => setIsCopied(false), 1250) diff --git a/src/components/Code/utils.ts b/src/components/Code/utils.ts index 45dba7d0..118c6ca1 100644 --- a/src/components/Code/utils.ts +++ b/src/components/Code/utils.ts @@ -23,7 +23,7 @@ interface CopyToClipboard { export const copyToClipboard: CopyToClipboard = (str, callback) => navigator.clipboard.writeText(str).then(callback) -const codePalette = { +export const codePalette = { [dark]: { background: blacks['darken-20'] }, @@ -70,14 +70,14 @@ const lightCommonColor = themedStyles[light].base.color const { shade } = ColorManipulationTypes -const prismColors = { +export const tokenColors = { [dark]: { boolean: reds.base, char: darkCommonColor, className: darkCommonColor, comment: darkCommonColor, function: darkCommonColor, - keyword: darkCommonColor, + keyword: reds.base, lineHighlight: darkCommonColor, method: darkCommonColor, number: reds.base, @@ -95,7 +95,7 @@ const prismColors = { className: lightCommonColor, comment: lightCommonColor, function: lightCommonColor, - keyword: lightCommonColor, + keyword: oranges.base, lineHighlight: lightCommonColor, method: lightCommonColor, number: oranges.base, @@ -119,7 +119,7 @@ const generateThemedPreCodeStyles = (themeType: ThemeType) => { } = themedStyles[themeType] return { - ...Object.entries(prismColors[themeType]).reduce( + ...Object.entries(tokenColors[themeType]).reduce( (acc, [key, val]) => ({ ...acc, [`& .token.${key}`]: { @@ -169,6 +169,7 @@ export const useStyles = createUseStyles({ ...font.label, fontFamily: 'Fira Code, monospace', fontWeight: fontWeight.light, + tabSize: 3, textShadow: 'none' }, ...generateThemedCodeStyles(light), diff --git a/src/components/JSONPathPicker/JSONPathPicker.stories.tsx b/src/components/JSONPathPicker/JSONPathPicker.stories.tsx new file mode 100644 index 00000000..627928e1 --- /dev/null +++ b/src/components/JSONPathPicker/JSONPathPicker.stories.tsx @@ -0,0 +1,77 @@ +import { Input } from 'components/Input' +import { JSONPathPicker, JSONPathPickerProps } from '.' +import { Meta, Story } from '@storybook/react/types-6-0' +import React, { useState } from 'react' + +export default { + component: JSONPathPicker, + title: 'JSONPathPicker' +} as Meta + +const Template: Story = args => { + const [path, setPath] = useState('$.dassana') + + return ( +
+ setPath(e.target.value)} + value={path} + /> +
+ { + setPath(path) + console.log(path) + }} + path={path} + /> +
+ ) +} + +/* eslint-disable sort-keys */ +const sampleJSON = { + dassana: { + context: { + attachedResources: { + 'count-of-attached-eni': 3, + 'does-any-eni-have-public-ip': true, + 'are-there-flows-from-internet-in-vpcflows': false + } + } + }, + resourceConfig: { + configuration: { + description: 'ssh-from-world', + groupName: 'prod-cache', + ipPermissions: [ + { + fromPort: 22, + ipProtocol: 'tcp', + ipv6Ranges: [ + { + cidrIpv6: '::/0' + } + ], + prefixListIds: [], + toPort: 22, + userIdGroupPairs: [], + ipv4Ranges: [ + { + cidrIp: '0.0.0.0/0' + } + ] + } + ] + }, + 'a-really-really-really-really-really-really-really-really-really-really-really-really-really-really-really-reallyreally-really-really-long-key': null + } +} +/* eslint-enable sort-keys */ + +export const Default = Template.bind({}) +Default.args = { + json: sampleJSON +} diff --git a/src/components/JSONPathPicker/index.tsx b/src/components/JSONPathPicker/index.tsx new file mode 100644 index 00000000..b11fef59 --- /dev/null +++ b/src/components/JSONPathPicker/index.tsx @@ -0,0 +1,63 @@ +import cn from 'classnames' +import { CodeControls } from 'components/Code/CodeControls' +import { recursiveRender } from './utils' +import { useStyles } from './styles' +import { copyToClipboard, stringifyCode } from 'components/Code/utils' +import React, { FC, useEffect, useState } from 'react' + +export type JSONValue = + | string + | number + | boolean + | null + | JSONValue[] + | { [key: string]: JSONValue } + +export interface JSONPathPickerProps { + classes?: string[] + displayControls?: boolean + json: Record + path: string + onChange: (path: string) => void +} + +export const JSONPathPicker: FC = ({ + classes = [], + displayControls = true, + json, + path, + onChange +}: JSONPathPickerProps) => { + const compClasses = useStyles() + + const [isCopied, setIsCopied] = useState(false) + + const copyJSON = () => + copyToClipboard(stringifyCode(json), () => setIsCopied(true)) + + useEffect(() => { + if (isCopied) setTimeout(() => setIsCopied(false), 1250) + }, [isCopied]) + + return ( +
+ {displayControls && ( + + )} +
+ {recursiveRender({ + classes: compClasses, + currPath: '$', + isLastItem: true, + onChange, + pickedPath: path, + remainingJSON: json + })} +
+
+ ) +} diff --git a/src/components/JSONPathPicker/styles.ts b/src/components/JSONPathPicker/styles.ts new file mode 100644 index 00000000..05d24c05 --- /dev/null +++ b/src/components/JSONPathPicker/styles.ts @@ -0,0 +1,122 @@ +import { createUseStyles } from 'react-jss' +import { codePalette, tokenColors } from '../Code/utils' +import { styleguide, themedStyles, ThemeType } from '../assets/styles' + +const { + colors: { blacks, grays }, + font, + fontWeight, + spacing +} = styleguide + +const { light, dark } = ThemeType + +const { dark: darkTokenClrs, light: lightTokenClrs } = tokenColors + +const tokenStyles = { + [dark]: { + '& $boolean': { color: darkTokenClrs.boolean }, + '& $null': { color: darkTokenClrs.keyword }, + '& $number': { color: darkTokenClrs.number }, + '& $operator': { color: darkTokenClrs.operator }, + '& $property': { + '&:hover': { + borderBottom: `1px solid ${darkTokenClrs.property}` + }, + color: darkTokenClrs.property + }, + '& $punctuation': { color: darkTokenClrs.punctuation }, + '& $string': { color: darkTokenClrs.string } + }, + [light]: { + boolean: { color: lightTokenClrs.boolean }, + null: { color: lightTokenClrs.keyword }, + number: { color: lightTokenClrs.number }, + operator: { + color: lightTokenClrs.operator, + padding: { left: spacing.xs / 2, right: spacing.xs } + }, + property: { + '&:hover': { + borderBottom: `1px solid ${lightTokenClrs.property}` + }, + color: lightTokenClrs.property, + cursor: 'pointer' + }, + punctuation: { + color: lightTokenClrs.punctuation, + padding: { left: spacing.xs / 2 } + }, + string: { color: lightTokenClrs.string } + } +} + +const generateThemedContainerStyles = (themeType: ThemeType) => { + const { background } = codePalette[themeType] + const { + base: { color } + } = themedStyles[themeType] + + const { + base: { borderColor } + } = themedStyles[themeType] + + return { + background, + border: `1px solid ${borderColor}`, + color + } +} + +const styles = { + ...tokenStyles[light], + container: { + '& ul, & ol': { + '& li': { + listStyle: 'none', + margin: 0, + padding: 0, + whiteSpace: 'nowrap' + }, + height: 'min-content', + margin: 0, + padding: 0, + paddingLeft: spacing['m+'], + whiteSpace: 'nowrap', + width: 'min-content' + }, + '&:hover': { + '& $controls': { opacity: 1 } + }, + ...font.label, + ...generateThemedContainerStyles(light), + fontFamily: 'Fira Code, monospace', + fontWeight: fontWeight.light, + height: '100%', + padding: spacing.s, + position: 'relative', + width: '100%' + }, + controls: { opacity: 0 }, + pickedItem: { backgroundColor: grays.base }, + wrapper: { + height: '100%', + overflow: 'auto', + whiteSpace: 'nowrap', + width: '100%' + }, + // eslint-disable-next-line sort-keys + '@global': { + [`.${dark}`]: { + ...tokenStyles[dark], + '& $container': generateThemedContainerStyles(dark), + '& $pickedItem': { + backgroundColor: blacks['lighten-10'] + } + } + } +} + +export const useStyles = createUseStyles(styles) + +export type Classes = Record diff --git a/src/components/JSONPathPicker/utils.tsx b/src/components/JSONPathPicker/utils.tsx new file mode 100644 index 00000000..f1031120 --- /dev/null +++ b/src/components/JSONPathPicker/utils.tsx @@ -0,0 +1,289 @@ +import { Classes } from './styles' +import cn from 'classnames' +import { getJSONPathArr } from 'components/utils' +import isEmpty from 'lodash/isEmpty' +import isNull from 'lodash/isNull' +import { JSONValue } from '.' +import React, { ReactNode } from 'react' + +enum Relationship { + other, + self, + ancestor +} + +const { other, self, ancestor } = Relationship +/** + * Gets the relationship between current path and the picked path + * @returns { Relationship } - Can either be "other", "self" or "ancestor" + */ +const getPathRelationship = ( + currPath: string, + pickedPath: string +): Relationship => { + if (!pickedPath) return other + + const pickedAttrs = getJSONPathArr(pickedPath) + const pickedLen = pickedAttrs.length + + const currAttrs = getJSONPathArr(currPath) + const currLen = currAttrs.length + + if (currLen > pickedLen) return other + + for (let i = 0; i < currLen; i++) { + const isInPath = + currAttrs[i] === pickedAttrs[i] || pickedAttrs[i] === '*' + + if (!isInPath) return other + } + + return currLen === pickedLen ? self : ancestor +} + +/* -x-x-x-x- Helper functions to render JSON -x-x-x-x- */ + +// ----------------------- Types ----------------------- + +type RemainingJSON = JSONValue | Record + +interface RenderParams { + classes: Classes + pickedPath: string + isLastItem?: boolean + currPath: string + onChange: (pickedPath: string) => void + remainingJSON: RemainingJSON +} + +enum Types { + array = 'array', + boolean = 'boolean', + null = 'null', + number = 'number', + object = 'object', + string = 'string' +} + +const { array, boolean, null: nullType, number, object, string } = Types + +// ----------------------------------------------------- + +const renderComma = ({ + classes, + isLastItem +}: Pick): ReactNode => + isLastItem ? <> : renderPunctuation(',', classes) + +const renderPunctuation = ( + punctuation: string, + classes: RenderParams['classes'] +): ReactNode => {punctuation} + +const renderArray = ({ + classes, + isLastItem, + onChange, + pickedPath, + currPath, + remainingJSON +}: RenderParams): ReactNode => { + const arr = remainingJSON as Record[] + const relation = getPathRelationship(currPath, pickedPath) + + const pickedItemClasses = cn({ + [classes.pickedItem]: relation === Relationship.self + }) + + return ( + <> + {arr.length === 0 ? ( + + {renderPunctuation('[', classes)} + {renderPunctuation(']', classes)} + {renderComma({ classes, isLastItem })} + + ) : ( + <> + {renderPunctuation('[', classes)} +
    + {arr.map((item, i) => ( +
  1. + {recursiveRender({ + classes, + currPath: `${currPath}[${i}]`, + isLastItem: arr.length - 1 === i, + onChange, + pickedPath, + remainingJSON: item + })} +
  2. + ))} +
+ + {renderPunctuation(']', classes)} + {renderComma({ classes, isLastItem })} + + + )} + + ) +} + +const renderObject = ({ + classes, + isLastItem, + onChange, + pickedPath, + remainingJSON, + currPath +}: RenderParams): ReactNode => { + const json = remainingJSON as Record + const remainingKeys = Object.keys(json) + + const relation = getPathRelationship(currPath, pickedPath) + + const pickedItemClasses = cn({ + [classes.pickedItem]: relation === Relationship.self + }) + + return isEmpty(json) ? ( + + {renderPunctuation('{', classes)} + {renderPunctuation('}', classes)} + {renderComma({ classes, isLastItem })} + + ) : ( + <> + {renderPunctuation('{', classes)} +
    + {remainingKeys.map((key, i) => ( +
  • + onChange(`${currPath}.${key}`)} + >{`"${key}"`} + : + {recursiveRender({ + classes, + currPath: `${currPath}.${key}`, + isLastItem: remainingKeys.length - 1 === i, + onChange, + pickedPath, + remainingJSON: json[key] + })} +
  • + ))} +
+ + {renderPunctuation('}', classes)} + {renderComma({ classes, isLastItem })} + + + ) +} + +interface RenderPrimitiveParams extends RenderParams { + type: Types.boolean | Types.number | Types.null +} +const renderPrimitive = ({ + classes, + pickedPath, + remainingJSON, + isLastItem, + currPath, + type +}: RenderPrimitiveParams): ReactNode => { + const relation = getPathRelationship(currPath, pickedPath) + + const pickedItemClasses = cn({ + [classes[type]]: true, + [classes.pickedItem]: relation === Relationship.self + }) + + return ( + <> + + {JSON.stringify(remainingJSON)} + + {renderComma({ classes, isLastItem })} + + ) +} + +const renderString = ({ + classes, + pickedPath, + remainingJSON, + isLastItem, + currPath +}: RenderParams): ReactNode => { + const relation = getPathRelationship(currPath, pickedPath) + + const pickedItemClasses = cn({ + [classes.string]: true, + [classes.pickedItem]: relation === Relationship.self + }) + + return ( + <> + {`"${remainingJSON}"`} + {renderComma({ classes, isLastItem })} + + ) +} + +// ------------------------------------------------- + +const mappedTypesToRenderFns = { + [array]: renderArray, + [boolean]: (params: RenderParams) => + renderPrimitive({ ...params, type: boolean }), + [nullType]: (params: RenderParams) => + renderPrimitive({ ...params, type: nullType }), + [number]: (params: RenderParams) => + renderPrimitive({ ...params, type: number }), + [object]: renderObject, + [string]: renderString +} + +const getRemainingJSONType = (remainingJSON: RemainingJSON) => { + if (isNull(remainingJSON)) return nullType + else if (Array.isArray(remainingJSON)) return array + else { + const type = typeof remainingJSON + + switch (type) { + case 'number': + case 'bigint': + return number + case 'object': + return object + case 'string': + return string + case 'boolean': + return boolean + default: + return nullType + } + } +} + +export const recursiveRender = ({ + classes, + pickedPath, + isLastItem, + currPath, + onChange, + remainingJSON +}: RenderParams): ReactNode => + mappedTypesToRenderFns[getRemainingJSONType(remainingJSON)]({ + classes, + currPath, + isLastItem, + onChange, + pickedPath, + remainingJSON + }) + +/* -x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x- */ diff --git a/src/components/Table/utils.tsx b/src/components/Table/utils.tsx index 95953724..16b808ae 100644 --- a/src/components/Table/utils.tsx +++ b/src/components/Table/utils.tsx @@ -3,9 +3,7 @@ import bytes from 'bytes' import { CellWithTooltip } from './CellWithTooltip' import { ColoredDot } from 'components/ColoredDot' import { EditableCell } from './EditableCell' -import { getJSONPathValue } from 'components/utils' import isUndefined from 'lodash/isUndefined' -import { JSONPath } from 'jsonpath-plus' import moment from 'moment' import { ColumnFormats, @@ -20,6 +18,7 @@ import { RenderPropsIcon } from './types' import { defaultIconHeight, MultipleIcons } from './MultipleIcons' +import { getJSONPathArr, getJSONPathValue } from 'components/utils' import { Icon, IconName, IconProps } from '../Icon' import { Link, LinkProps } from '../Link' import React, { Key, MouseEvent } from 'react' @@ -106,10 +105,11 @@ export function processData( partialData[dataIndex as keyof TableData] = value } - //@ts-ignore - const pathArr: string[] = JSONPath.toPathArray(`$.${dataIndex}`) + const pathArr: string[] = getJSONPathArr(`$.${dataIndex}`) + // Fix for this issue https://github.com/JSONPath-Plus/JSONPath/issues/102 + if (pathArr[0] === '$') pathArr.shift() - if (pathArr && pathArr.length) { + if (pathArr.length) { partialData[pathArr[0] as keyof TableData] = item[pathArr[0]] } }) diff --git a/src/components/index.ts b/src/components/index.ts index 9ff98b54..dcb2e654 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -14,6 +14,7 @@ export * from './Form' export * from './Input' export * from './Icon' export * from './IconButton' +export * from './JSONPathPicker' export * from './Link' export * from './Modal' export * from './Menu' diff --git a/src/components/utils.ts b/src/components/utils.ts index cc2f9c0e..ddbb0492 100644 --- a/src/components/utils.ts +++ b/src/components/utils.ts @@ -134,6 +134,12 @@ export const getJSONPathValue = (path: string, obj: Record) => { if (value && Array.isArray(value)) return value[0] } +/** + * Takes a JSON path as string and converts to an array + */ +export const getJSONPathArr = (path: string): string[] => + JSONPath.toPathArray(path) + export const getPopupContainerProps = ( popupContainerSelector = '' ): PopupContainerProps => {