Major reimplementation of object representations (no sorting/filtering)

This commit is contained in:
Chen Asraf
2018-01-21 22:10:44 +02:00
parent 89a9f749ec
commit 6fa9eba635
24 changed files with 148 additions and 628 deletions

View File

@@ -5,6 +5,7 @@
"homepage": "https://redar.com/",
"dependencies": {
"@types/chrome": "^0.0.56",
"@types/classnames": "^2.2.3",
"@types/flux": "^3.1.4",
"@types/jest": "^21.1.8",
"@types/node": "^8.0.47",
@@ -16,6 +17,7 @@
"axios": "^0.17.1",
"case-sensitive-paths-webpack-plugin": "2.1.1",
"chalk": "1.1.3",
"classnames": "^2.2.5",
"copy-webpack-plugin": "^4.3.0",
"css-loader": "0.28.4",
"dotenv": "4.0.0",

View File

@@ -9,13 +9,13 @@ class {{Name}} extends React.Component<I.IProps, I.IState> {
}
render() {
const classNames = [
const className = [
css.{{Name}},
this.props.className
].join(' ')
return (
<div className={classNames}>
<div className={className}>
{{Name}} Component
</div>
)

View File

@@ -4,16 +4,12 @@ import * as Immutable from 'immutable'
const ActionTypes = {
UPDATE_RESPONSE: 'UPDATE_RESPONSE',
UPDATE_TABLE: 'UPDATE_TABLE',
UPDATE_COLUMNS: 'UPDATE_COLUMNS',
UPDATE_VIEWKEY: 'UPDATE_VIEWKEY',
UPDATE_REQ_TYPE: 'UPDATE_REQ_TYPE',
}
const StoreKeys = {
Response: 'RESPONSE',
Table: 'TABLE',
Columns: 'COLUMNS',
ViewKey: 'VIEWKEY',
RequestType: 'REQ_TYPE'
}
@@ -40,29 +36,43 @@ class AppStore extends ReduceStore<IState, IAction> {
}
getInitialState() {
return Immutable.OrderedMap<string, any>()
const t = Immutable.OrderedMap<string, any>([
[StoreKeys.ViewKey, localStorage.lastViewKey || '']
])
console.debug(t)
return t
}
reduce(state: IState, action: IAction) {
switch (action.name) {
case ActionTypes.UPDATE_COLUMNS:
return state.set(StoreKeys.Columns, action.payload)
case ActionTypes.UPDATE_TABLE:
return state.set(StoreKeys.Table, action.payload)
case ActionTypes.UPDATE_RESPONSE:
let viewKey = state.get(StoreKeys.ViewKey)
if (localStorage.lastViewKey !== '' && action.payload && !action.payload.hasOwnProperty(viewKey)) {
viewKey = this.getViewKey(action.payload)
state = state.set(StoreKeys.ViewKey, viewKey)
}
return state.set(StoreKeys.Response, action.payload)
case ActionTypes.UPDATE_VIEWKEY:
localStorage.lastViewKey = action.payload
return state.set(StoreKeys.ViewKey, action.payload)
default:
return state
}
}
private getViewKey(data: any) {
for (const k in data) {
if (data.hasOwnProperty(k) && data[k] && data[k].constructor === Array) {
return k
}
}
return ''
}
}
export function dispatch(name: TActionName | string, payload: any) {
AppDispatcher.dispatch({
name, payload
})
AppDispatcher.dispatch({ name, payload })
}
function register(name: TActionName | string, callback: (payload: any) => void): string {

0
src/common/Utils.ts Normal file
View File

View File

@@ -1,46 +0,0 @@
@import "../../_variables.css";
.Cell {
/* */
}
.pre, .pre-alt, .obj-container {
overflow: hidden;
font-family: monospace;
max-width: 150px;
background: #eee;
margin: 4px 3px;
border: 1px solid #aaa;
padding: 4px;
}
.pre {
overflow-x: auto;
width: 100%;
}
.pre-alt {
display: inline-block;
vertical-align: top;
width: unset;
}
.popover {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.array {
background: #ffb;
}
.any {
background: #f1d7ea;
}
.string {
background: #dfa
}

View File

@@ -1,9 +0,0 @@
export const Cell: string;
export const cell: string;
export const pre: string;
export const preAlt: string;
export const objContainer: string;
export const popover: string;
export const array: string;
export const any: string;
export const string: string;

View File

@@ -1,11 +0,0 @@
export interface IProps {
data: any
depth?: number
className?: string
}
export interface IState {
tableData: any
depth: number
dataVisible: boolean
}

View File

@@ -1,104 +0,0 @@
import * as React from 'react'
import * as css from './Cell.css'
import * as I from './Cell.module'
import DataTable from 'components/DataTable/DataTable'
import * as Repr from './Repr'
const MAX_DEPTH = 5
class Cell extends React.Component<I.IProps, I.IState> {
constructor(props: I.IProps) {
super(props)
this.state = {
tableData: props.data,
dataVisible: false,
depth: props.depth || 0,
}
}
private isRepresentable(obj?: any) {
return (
!obj ||
obj.constructor === String ||
obj.constructor === Boolean ||
typeof obj === 'undefined' ||
typeof obj === 'boolean' ||
typeof obj === 'number'
)
}
private expandData() {
this.setState({ dataVisible: true })
}
private parse() {
const obj = this.props.data
const className = this.props.className || ''
if (this.state.depth >= MAX_DEPTH) {
return (
<span>&hellip;</span>
)
}
if (this.isRepresentable(obj)) {
return (
<Repr.JSON className={[className, css.pre, css.string].join(' ')}>
{obj}
</Repr.JSON>
)
}
if (obj.constructor === Array && (!obj.length || obj[0].constructor === {}.constructor)) {
const items = obj.slice(0, 3)
return (
<Repr.Any className={[className, css.preAlt, css.array].join(' ')}
title={'Array (' + obj.length + ')'}>
{items.map((item, i) => (
<Cell key={'item-' + i} className={[css.objContainer].join(' ')}
depth={this.state.depth + 1}
data={item} />
))}
</Repr.Any>
)
}
const keys = Object.keys(obj)
return keys.length ? (
<Repr.Any className={[className, css.preAlt].join(' ')}
title={'Object (' + keys.length + ' keys)'}
onClick={(e) => this.expandData()}>
{keys.map((s, i) => (
<Repr.Any key={'key-' + i}
className={[css.preAlt, css.string].join(' ')}>
{s}: {JSON.stringify(obj[s])}
</Repr.Any>
))}
</Repr.Any>
) : (
<Repr.Any className={[className, css.preAlt].join(' ')}
title="Empty Object" />
)
}
render() {
const classNames = [
css.Cell,
].join(' ')
return (
<div className={classNames}>
{this.parse()}
{this.state.dataVisible ? (
<div className={css.popover}>
{/* <DataTable data={this.state.tableData} static={true} store={null} /> */}
</div>
) : ''}
</div>
)
}
}
export default Cell

View File

@@ -1,53 +0,0 @@
import * as React from 'react'
import * as css from './Cell.css'
interface ReprProps {
className?: string
preClassName?: string
children?: any
[prop: string]: any
}
interface AnyProps {
title?: string
}
export function Any({children, className, preClassName, title, ...props}: ReprProps & AnyProps) {
return (
<div className={className} {...props}>
{title ? <label>{title}</label> : ''}
<div className={preClassName}>
{children}
</div>
</div>
)
}
function _JSON({children, className, preClassName, title, ...props}: ReprProps & AnyProps) {
if (!children || children.constructor !== Array) {
children = [children]
}
return (
<div className={className} {...props}>
{title ? <label>{title}</label> : ''}
{children.map((child, i) => {
let str = JSON.stringify(child, null, '\t').split(`\\`).join('')
const maxLen = 400
if (str.length > maxLen) {
str = str.slice(0, maxLen) + '\u2026'
}
return (
<div key={['child', 'i', Math.random().toString()].join('_')}
className={preClassName}>
{str}
</div>
)
})}
</div>
)
}
export { _JSON as JSON }

View File

@@ -1,80 +0,0 @@
@import "../../_variables.css";
.DataTable {
width: 100%;
padding: 5px;
border-spacing: 2px;
border-collapse: separate;
& thead th {
background: #acf;
padding: 4px;
}
& tbody td {
padding: 4px;
padding-right: 8px;
}
& tr:nth-child(even) td {
background: #eee;
&.col-id {
background: #ddd;
}
}
& tbody tr:not(:last-child) td, & thead th {
border-bottom: 1px solid #aaa;
}
& tbody td, & thead th {
vertical-align: top;
& label {
display: block;
text-transform: uppercase;
color: #555;
margin-bottom: 5px;
font-weight: bold;
font-size: 0.95em;
font-family: $font-family;
}
}
}
.col-unknown {
min-width: 80px;
}
.col-name {
color: $text-main;
text-decoration: none;
display: inline-block;
margin-bottom: 5px;
&:hover {
text-decoration: underline;
}
&.sorting-by {
font-weight: bold;
}
}
.filter-input {
width: 100%;
border: 1px solid #ccc;
min-width: 80px;
padding: 7px 4px;
}
.sort-caret {
vertical-align: middle;
font-size: 1.3em;
}
.col-id {
min-width: 60px;
background: #eee;
}

View File

@@ -1,8 +0,0 @@
export const DataTable: string;
export const dataTable: string;
export const colId: string;
export const colUnknown: string;
export const colName: string;
export const sortingBy: string;
export const filterInput: string;
export const sortCaret: string;

View File

@@ -1,20 +0,0 @@
export interface Props {
store: any
className?: string
static?: boolean
data?: any[]
}
export interface Filters {
[key: string]: string | undefined
}
export type FilterFunc = (a: any, b: any) => boolean
export interface State {
columns: string[]
data: any[]
filters: Filters
sortKey: string
sortDesc: boolean
}

View File

@@ -1,216 +0,0 @@
import * as React from 'react'
import * as I from './DataTable.module'
import * as css from './DataTable.css'
import Dispatcher, { register, dispatch, ActionTypes, StoreKeys } from 'common/Dispatcher'
import Cell from 'components/Cell/Cell'
class DataTable extends React.Component<I.Props, I.State> {
private columns: string[]
private listeners: string[]
private filterFuncs: { [open: string]: I.FilterFunc } = {
'>': (a, b) => parseFloat(a) > parseFloat(b),
'>=': (a, b) => parseFloat(a) >= parseFloat(b),
'<': (a, b) => parseFloat(a) < parseFloat(b),
'<=': (a, b) => parseFloat(a) <= parseFloat(b),
'=': (a, b) => JSON.stringify(a) === JSON.stringify(b),
// default: contains
'_': (a, b) => String(a).toLowerCase().indexOf(String(b).toLowerCase()) > -1
}
constructor(props: I.Props) {
super(props)
const columns = props.store.get(StoreKeys.Columns, [])
this.state = {
columns,
data: props.data || props.store.get(StoreKeys.Table, []),
filters: {},
sortKey: columns[0],
sortDesc: false
}
}
public componentWillMount() {
if (this.props.static) {
return
}
this.listeners = [
register(ActionTypes.UPDATE_COLUMNS, (columns: any) => {
let { sortKey } = this.state
if (columns.indexOf(sortKey) === -1) {
sortKey = columns[0]
}
this.setState({
columns: columns || [],
sortKey,
})
}),
register(ActionTypes.UPDATE_TABLE, (table: any) => {
this.setState({
data: this.parseData(table || [])
})
})
]
}
public componentWillUnmount() {
if (this.props.static) {
return
}
this.listeners.forEach(l => Dispatcher.unregister(l))
}
private updateData(data: any) {
const parsed = this.parseData(data.data)
this.setState({
data: parsed,
columns: data.columns
})
}
private parseData(data: any) {
let parsed = data
if (!parsed) {
return []
}
parsed = parsed.map((row: any, i: number) => {
row._id = row._id || row.id || i
return row
})
return parsed
}
private getColumnRowData(row: any, i: number) {
return this.state.columns.map((col, j) => {
const s = (css as any)
const camelCase = col
.replace(/([^a-z]+[a-z0-9])/i, ($1) => $1.toUpperCase())
.replace(/[^a-z]/i, '')
const asClsName = camelCase[0].toUpperCase() + camelCase.slice(1)
const cls = [
s.hasOwnProperty('col' + asClsName) ? s['col' + asClsName] : css.colUnknown,
['col', j].join('-'),
['col', col].join('-')
].join(' ')
return (
<td key={[col, i, j].join('_')}
className={cls}>
<Cell data={row[col]} />
</td>
)
})
}
private setFilter(key: string, value: string) {
this.setState((cur: I.State) => {
const filters = cur.filters
filters[key] = value
return { filters }
})
}
private filterAndSortData() {
let { data, filters } = this.state
for (const key in filters) {
if (filters.hasOwnProperty(key) && filters[key] && filters[key]!.length) {
data = data.filter((value: any) => {
return this.compareFilter(value[key], filters[key]!)
})
if (data.length === 0) {
break
}
}
}
const skey = this.state.sortKey
const cmp = (a, b) => a > b ? 1 : a === b ? 0 : -1
const sort = (a, b) => !skey ? 1 : this.state.sortDesc ? cmp(b[skey], a[skey]) : cmp(a[skey], b[skey])
data = data.sort(sort)
return data
}
private sortBy(key: string) {
const { sortKey, sortDesc } = this.state
const desc = sortKey === key && !sortDesc
this.setState({ sortKey: key, sortDesc: desc })
}
private compareFilter(itemValue: any, filterStr: string) {
const [ filterOper, filterValue ] = filterStr.split(/\b/).map(s => String(s).trim())
const filterKey = this.filterFuncs.hasOwnProperty(filterOper) ? filterOper : '_' // default: contains
return this.filterFuncs[filterOper](itemValue, filterValue)
}
public render() {
return (
<div className={this.props.className || ''}>
<table className={css.dataTable}>
<thead>
<tr>
{this.state.columns.map(col => {
const anchorCls = [
css.colName,
this.state.sortKey === col ? css.sortingBy : ''
].join(' ')
const caretCls = [
css.sortCaret,
'material-icons'
].join(' ')
return (
<th key={col}>
<a href="#"
onClick={(e) => { e.preventDefault(); this.sortBy(col) }}
className={anchorCls}>
{col}
<span className={caretCls}>
{this.state.sortKey === col && this.state.sortDesc ?
'keyboard_arrow_down' : 'keyboard_arrow_up'}
</span>
</a>
<div>
<input type="text"
className={css.filterInput}
value={this.state.filters[col] || ''}
placeholder={`filter "${col}"`}
onChange={(e) => {
const { value } = e.target
this.setFilter(col, value)
}}
/>
</div>
</th>
)
})}
</tr>
</thead>
<tbody>
{this.filterAndSortData().map((row, i) => (
<tr key={`row_${i}`}>
{this.getColumnRowData(row, i)}
</tr>
))}
</tbody>
</table>
</div>
)
}
}
export default DataTable

View File

@@ -105,11 +105,18 @@ class Header extends React.Component<I.IProps, I.IState> {
}
private get requestPayload() {
if (!this.state.requestType || !this.requestTypeMap.hasOwnProperty(this.state.requestType)) {
if (!this.state.requestType
|| !this.requestTypeMap.hasOwnProperty(this.state.requestType)
|| !this.state.requestPayload.length) {
return undefined
}
return this.requestTypeMap[this.state.requestType](this.state.requestPayload)
try {
return this.requestTypeMap[this.state.requestType](this.state.requestPayload)
} catch (e) {
console.error(e)
return "Can't parse response"
}
}
private getDataColumns(data?: any) {

View File

@@ -21,16 +21,16 @@ class KeyList extends React.Component<I.IProps, I.IState> {
public componentWillMount() {
this.listeners = [
register(ActionTypes.UPDATE_RESPONSE, (data: any) => {
const viewKey = this.props.store.get(StoreKeys.ViewKey, '')
this.setState({
keyList: this.keyListFromObject(data || {}),
viewKey: this.getViewKey(data || {}),
viewKey,
})
}),
register(ActionTypes.UPDATE_VIEWKEY, (data: any) => {
this.setState({
viewKey: data
})
this.setState({ viewKey: data })
})
]
}
@@ -43,54 +43,21 @@ class KeyList extends React.Component<I.IProps, I.IState> {
return [''].concat(Object.keys(data))
}
private getViewKey(data: any) {
let viewKey = this.state.viewKey
if (viewKey && data.hasOwnProperty(viewKey)) {
return viewKey
}
for (const k in data) {
if (data.hasOwnProperty(k) && data[k] && data[k].constructor === Array) {
return k
}
}
return ''
}
private selectItem(key: string) {
const body = this.props.store.get(StoreKeys.Response, {})
const tableData = key !== '' ? body[key] : [body]
this.setState({ viewKey: key }, () => {
dispatch(ActionTypes.UPDATE_VIEWKEY, key)
dispatch(ActionTypes.UPDATE_TABLE, tableData)
dispatch(ActionTypes.UPDATE_COLUMNS, this.columnListFromRow(tableData && tableData.length ? tableData[0] : []))
})
}
private columnListFromRow(row: any) {
const keys = ['_id']
Object.keys(row).sort().forEach(k => {
k = k.toLowerCase()
if (k !== '_id' && k !== 'id') {
keys.push(k)
}
})
return keys
}
private get keyListElements() {
const fullData = this.props.store.get(StoreKeys.Response, {})
const viewKey = this.state.viewKey
return this.state.keyList.map((key: string) => {
const className = [
css.item,
key === '' || (fullData[key] && fullData[key].constructor === Array) ? css.valid : '',
this.state.viewKey === key ? css.selected : ''
viewKey === key ? css.selected : ''
].join(' ')
return (

View File

@@ -10,6 +10,7 @@
vertical-align: top;
border: 1px solid #ccc;
border-radius: 4px;
min-width: 120px;
& > h3 {
font-size: 0.8rem;

View File

@@ -1,5 +1,7 @@
export type ClassNameFunc = (index?: number, item?: string) => string
export interface IProps {
className?: string
className?: string | ClassNameFunc
data: any
}

View File

@@ -8,7 +8,8 @@ class RObject extends React.Component<I.IProps, I.IState> {
this.state = {}
}
private getCorrectRepr(obj: any, cls?: string) {
private getCorrectRepr(obj: any) {
let cls = this.props.className
try {
switch (typeof obj) {
case 'number':
@@ -19,12 +20,20 @@ class RObject extends React.Component<I.IProps, I.IState> {
const isArray = obj && obj.constructor === Array
if (!keys.length) {
return <span>{JSON.stringify(obj)}</span>
if (typeof cls === 'function') {
cls = cls()
}
return <span className={cls}>{JSON.stringify(obj)}</span>
}
return keys.map((k: string) => {
return keys.map((k: string, i: number) => {
let tempCls = cls
if (typeof tempCls === 'function') {
tempCls = tempCls(i, k)
}
tempCls += ' ' + css.RObject
return (
<div className={cls} key={'obj-' + k}>
<div className={tempCls as string} key={'obj-' + k}>
<h3>{!isArray ? k : typeof obj}</h3>
<RObject data={obj[k]} />
</div>
@@ -36,13 +45,8 @@ class RObject extends React.Component<I.IProps, I.IState> {
}
}
render() {
const classNames = [
css.RObject,
this.props.className
].join(' ')
const repr = this.getCorrectRepr(this.props.data, classNames)
render() {
const repr = this.getCorrectRepr(this.props.data)
return repr
}
}

View File

@@ -4,6 +4,36 @@
/* */
}
.obj {
// max-width: 11vw;
.table {
display: grid;
grid-row-gap: 8px;
padding: 10px;
& h3 {
font-weight: bold;
text-transform: uppercase;
color: $text-light;
font-size: 0.9em;
}
}
.cell {
width: 100%;
margin: 0 !important;
border-left-width: 0px !important;
border-radius: 0 !important;
&.row-start {
border-radius: 5px 0 0 5px !important;
border-left-width: 1px !important;
margin-left: 0 !important;
}
&.row-end {
border-radius: 0 5px 5px 0 !important;
}
& > h3 {
display: none;
}
}

View File

@@ -1,3 +1,6 @@
export const ResponseRepr: string;
export const responseRepr: string;
export const obj: string;
export const table: string;
export const cell: string;
export const rowStart: string;
export const rowEnd: string;

View File

@@ -3,6 +3,7 @@ import * as css from './ResponseRepr.css'
import * as I from './ResponseRepr.module'
import * as D from 'common/Dispatcher'
import RObject from 'components/RObject/RObject'
import * as classNames from 'classnames'
class ResponseRepr extends React.Component<I.IProps, I.IState> {
private listeners: string[]
@@ -16,6 +17,10 @@ class ResponseRepr extends React.Component<I.IProps, I.IState> {
componentDidMount() {
this.listeners = [
D.register(D.ActionTypes.UPDATE_RESPONSE, (response) => {
const viewKey = this.props.store.get(D.StoreKeys.ViewKey)
if (viewKey && Object.keys(response).indexOf(viewKey) > -1) {
response = response[viewKey]
}
this.setState({ response })
}),
@@ -33,19 +38,45 @@ class ResponseRepr extends React.Component<I.IProps, I.IState> {
private getRObjectList() {
const { response } = this.state
let colAmt
if (response && response.constructor === Array) {
return response.map((row, i) => {
return (<RObject key={`row-${i}`} className={css.obj} data={row} />)
})
const keys = Object.keys(response[0] || {})
colAmt = keys.length
return (
<div className={css.table}
style={{gridTemplateColumns: `repeat(${colAmt}, auto)`}}>
{keys.map((key) => <h3 key={`col-th-${key}`}>{key}</h3>)}
{response.map((row, i) => {
const cls = (j) => {
return classNames(css.cell, {
[css.rowStart]: j % colAmt === 0,
[css.rowEnd]: j % colAmt === colAmt - 1,
})
}
return (
<RObject className={cls}
key={`row-${i}`}
data={row} />
)
})}
</div>
)
}
colAmt = Object.keys(response || {})
return (
<RObject className={css.obj} data={response} />
<div className={css.table}
style={{gridTemplateRows: `repeat(${colAmt}, auto)`}}>
<RObject data={response} />
</div>
)
}
render() {
const classNames = [
const className = [
css.ResponseRepr,
this.props.className
].join(' ')
@@ -53,7 +84,7 @@ class ResponseRepr extends React.Component<I.IProps, I.IState> {
const repr = this.getRObjectList()
return (
<div className={classNames}>
<div className={className}>
{repr}
</div>
)

View File

@@ -13,16 +13,18 @@
width: 100%;
height: 100%;
overflow: auto;
display: grid;
grid-template-columns: [sidebar] 20vw [main] 80vw;
display: flex;
}
.key-list {
grid-column: sidebar;
flex-shrink: 1;
flex-grow: 0;
min-width: 200px;
max-width: 15vw;
}
.table {
grid-column: main;
flex-grow: 1;
}
.scrollable {

View File

@@ -47,7 +47,7 @@
"check-open-brace",
"check-whitespace"
],
"quotemark": [true, "single", "jsx-double"],
"quotemark": [true, "single", "jsx-double", "avoid-escape"],
"radix": true,
"semicolon": [true, "never"],
"switch-default": true,

View File

@@ -8,6 +8,10 @@
dependencies:
"@types/filesystem" "*"
"@types/classnames@^2.2.3":
version "2.2.3"
resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.3.tgz#3f0ff6873da793870e20a260cada55982f38a9e5"
"@types/events@*":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@types/events/-/events-1.1.0.tgz#93b1be91f63c184450385272c47b6496fd028e02"
@@ -910,6 +914,10 @@ clap@^1.0.9:
dependencies:
chalk "^1.1.3"
classnames@^2.2.5:
version "2.2.5"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d"
clean-css@4.1.x:
version "4.1.9"
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.1.9.tgz#35cee8ae7687a49b98034f70de00c4edd3826301"