Fixed several UI issues in Databases plugin

Summary:
This diff improves showing details in the databases plugin in a few ways:

1. if there is large content, it is no longer rendered of screen
1. the section is now scrollable
1. made layout vertical rather than horizontal to have more space vor values
1. show the type of a value
1. better detection of JSON versus non-json values (primitives would be picked up as json, but in contrast binary ascii such as used in instagram wouldn't). This fixes also string to be rendered incorrectly by accidentally parsing them as numbers, such as can be seen in https://fb.workplace.com/groups/flippersupport/permalink/948112715669387/

Changelog: [Databases] Fixed several layout issues when inspecting rows, and added better JSON support and support for larger responses.

Reviewed By: cekkaewnumchai

Differential Revision: D23317689

fbshipit-source-id: 47ab5164e25da003e0d1a4ae7ccb1537b637e572
This commit is contained in:
Michel Weststrate
2020-08-25 08:48:07 -07:00
committed by Facebook GitHub Bot
parent 7f9d4c35a0
commit 8d4ff8d48e
2 changed files with 98 additions and 91 deletions

View File

@@ -9,23 +9,25 @@
import React, {useMemo, useState, useEffect, useReducer} from 'react'; import React, {useMemo, useState, useEffect, useReducer} from 'react';
import { import {
Text,
Input, Input,
DetailSidebar, DetailSidebar,
Panel, Panel,
ManagedTable,
TableRows,
TableBodyRow,
ManagedDataInspector, ManagedDataInspector,
Value, Value,
valueToNullableString, valueToNullableString,
renderValue, renderValue,
Layout,
Button, Button,
styled, styled,
produce, produce,
colors,
} from 'flipper'; } from 'flipper';
type TableRow = {
col: string;
type: Value['type'];
value: React.ReactElement;
};
type DatabaseDetailSidebarProps = { type DatabaseDetailSidebarProps = {
columnLabels: Array<string>; columnLabels: Array<string>;
columnValues: Array<Value>; columnValues: Array<Value>;
@@ -41,29 +43,48 @@ const EditTriggerSection = styled.div({
paddingRight: '10px', paddingRight: '10px',
}); });
function sidebarRows(labels: Array<string>, values: Array<Value>): TableRows { const TableDetailRow = styled.div({
borderBottom: `1px solid ${colors.blackAlpha10}`,
padding: 8,
});
const TableDetailRowTitle = styled.div({
fontWeight: 'bold',
marginBottom: 8,
});
const TableDetailRowType = styled.span({
color: colors.light20,
marginLeft: 8,
fontWeight: 'normal',
});
const TableDetailRowValue = styled.div({});
function sidebarRows(labels: Array<string>, values: Array<Value>): TableRow[] {
return labels.map((label, idx) => buildSidebarRow(label, values[idx])); return labels.map((label, idx) => buildSidebarRow(label, values[idx]));
} }
function buildSidebarRow(key: string, val: Value): TableBodyRow { function buildSidebarRow(key: string, val: Value): TableRow {
let output = renderValue(val, true); let output = renderValue(val, true);
// TODO(T60896483): Narrow the scope of this try/catch block. if (
if (val.type === 'string') { (val.type === 'string' || val.type === 'blob') &&
(val.value[0] === '[' || val.value[0] === '{')
) {
try { try {
const parsed = JSON.parse(val.value); // eslint-disable-next-line
output = ( var parsed = JSON.parse(val.value);
<ManagedDataInspector data={parsed} expandRoot={false} collapsed />
);
} catch (_error) {} } catch (_error) {}
if (parsed) {
output = (
<ManagedDataInspector data={parsed} expandRoot={true} collapsed />
);
}
} }
return { return {
columns: { col: key,
col: {value: <Text>{key}</Text>}, type: val.type,
val: {
value: output, value: output,
},
},
key: key,
}; };
} }
@@ -71,35 +92,32 @@ function sidebarEditableRows(
labels: Array<string>, labels: Array<string>,
values: Array<Value>, values: Array<Value>,
rowDispatch: (action: RowAction) => void, rowDispatch: (action: RowAction) => void,
): TableRows { ): TableRow[] {
return labels.map((label, idx) => return labels.map((label, idx) =>
buildSidebarEditableRow( buildSidebarEditableRow(label, values[idx], (value: string | null) =>
label, rowDispatch({type: 'set', key: label, value}),
valueToNullableString(values[idx]),
(value: string | null) => rowDispatch({type: 'set', key: label, value}),
), ),
); );
} }
function buildSidebarEditableRow( function buildSidebarEditableRow(
key: string, key: string,
value: string | null, val: Value,
onUpdateValue: (value: string | null) => void, onUpdateValue: (value: string | null) => void,
): TableBodyRow { ): TableRow {
if (val.type === 'blob' || !val.type) {
return buildSidebarRow(key, val);
}
return { return {
columns: { col: key,
col: {value: <Text>{key}</Text>}, type: val.type,
val: {
value: ( value: (
<EditField <EditField
key={key} key={key}
initialValue={value} initialValue={valueToNullableString(val)}
onUpdateValue={onUpdateValue} onUpdateValue={onUpdateValue}
/> />
), ),
},
},
key: key,
}; };
} }
@@ -120,24 +138,11 @@ const EditField = React.memo(
}} }}
placeholder={value === null ? 'NULL' : undefined} placeholder={value === null ? 'NULL' : undefined}
data-testid={'update-query-input'} data-testid={'update-query-input'}
style={{width: '100%'}}
/> />
); );
}, },
); );
const cols = {
col: {
value: 'Column',
resizable: true,
},
val: {
value: 'Value',
resizable: true,
},
};
const colSizes = {
col: '35%',
val: 'flex',
};
type RowState = {changes: {[key: string]: string | null}; updated: boolean}; type RowState = {changes: {[key: string]: string | null}; updated: boolean};
type RowAction = type RowAction =
@@ -181,8 +186,7 @@ export default React.memo(function DatabaseDetailSidebar(
floating={false} floating={false}
collapsable={true} collapsable={true}
padded={false}> padded={false}>
<Layout.Top> {onSave ? (
{onSave && (
<EditTriggerSection> <EditTriggerSection>
{editing ? ( {editing ? (
<> <>
@@ -200,18 +204,18 @@ export default React.memo(function DatabaseDetailSidebar(
<Button onClick={() => setEditing(true)}>Edit</Button> <Button onClick={() => setEditing(true)}>Edit</Button>
)} )}
</EditTriggerSection> </EditTriggerSection>
)} ) : null}
<ManagedTable <div>
highlightableRows={false} {rows.map((row) => (
columnSizes={colSizes} <TableDetailRow key={row.col}>
multiline={true} <TableDetailRowTitle>
columns={cols} {row.col}
autoHeight={true} <TableDetailRowType>({row.type})</TableDetailRowType>
floating={false} </TableDetailRowTitle>
zebra={false} <TableDetailRowValue>{row.value}</TableDetailRowValue>
rows={rows} </TableDetailRow>
/> ))}
</Layout.Top> </div>
</Panel> </Panel>
</DetailSidebar> </DetailSidebar>
); );

View File

@@ -134,12 +134,15 @@ test('editing some field after trigger Edit', async () => {
fireEvent.click(res.getByText('Edit')); fireEvent.click(res.getByText('Edit'));
// still find all values because it needs to show up // still find all values because it needs to show up
for (const value of values) { for (const value of values) {
if (value.type === 'blob') { const searchValue = value.value?.toString();
continue; expect(
} (value.type === 'null'
const searchValue: string = ? res.queryAllByPlaceholderText('NULL')
value.type === 'null' ? 'NULL' : value.value.toString(); : value.type === 'blob'
expect(res.queryAllByText(searchValue).length).toBeGreaterThan(0); ? res.queryAllByText(searchValue!)
: res.queryAllByDisplayValue(searchValue!)
).length,
).toBeGreaterThan(0);
} }
// expect the last one to contain value of 'db_1_column9_value' // expect the last one to contain value of 'db_1_column9_value'