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
223 lines
5.5 KiB
TypeScript
223 lines
5.5 KiB
TypeScript
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @format
|
|
*/
|
|
|
|
import React, {useMemo, useState, useEffect, useReducer} from 'react';
|
|
import {
|
|
Input,
|
|
DetailSidebar,
|
|
Panel,
|
|
ManagedDataInspector,
|
|
Value,
|
|
valueToNullableString,
|
|
renderValue,
|
|
Button,
|
|
styled,
|
|
produce,
|
|
colors,
|
|
} from 'flipper';
|
|
|
|
type TableRow = {
|
|
col: string;
|
|
type: Value['type'];
|
|
value: React.ReactElement;
|
|
};
|
|
|
|
type DatabaseDetailSidebarProps = {
|
|
columnLabels: Array<string>;
|
|
columnValues: Array<Value>;
|
|
onSave?: ((changes: {[key: string]: string | null}) => void) | undefined;
|
|
};
|
|
|
|
const EditTriggerSection = styled.div({
|
|
display: 'flex',
|
|
justifyContent: 'flex-end',
|
|
width: '100%',
|
|
paddingTop: '3px',
|
|
paddingBottom: '3px',
|
|
paddingRight: '10px',
|
|
});
|
|
|
|
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]));
|
|
}
|
|
|
|
function buildSidebarRow(key: string, val: Value): TableRow {
|
|
let output = renderValue(val, true);
|
|
if (
|
|
(val.type === 'string' || val.type === 'blob') &&
|
|
(val.value[0] === '[' || val.value[0] === '{')
|
|
) {
|
|
try {
|
|
// eslint-disable-next-line
|
|
var parsed = JSON.parse(val.value);
|
|
} catch (_error) {}
|
|
if (parsed) {
|
|
output = (
|
|
<ManagedDataInspector data={parsed} expandRoot={true} collapsed />
|
|
);
|
|
}
|
|
}
|
|
return {
|
|
col: key,
|
|
type: val.type,
|
|
value: output,
|
|
};
|
|
}
|
|
|
|
function sidebarEditableRows(
|
|
labels: Array<string>,
|
|
values: Array<Value>,
|
|
rowDispatch: (action: RowAction) => void,
|
|
): TableRow[] {
|
|
return labels.map((label, idx) =>
|
|
buildSidebarEditableRow(label, values[idx], (value: string | null) =>
|
|
rowDispatch({type: 'set', key: label, value}),
|
|
),
|
|
);
|
|
}
|
|
|
|
function buildSidebarEditableRow(
|
|
key: string,
|
|
val: Value,
|
|
onUpdateValue: (value: string | null) => void,
|
|
): TableRow {
|
|
if (val.type === 'blob' || !val.type) {
|
|
return buildSidebarRow(key, val);
|
|
}
|
|
return {
|
|
col: key,
|
|
type: val.type,
|
|
value: (
|
|
<EditField
|
|
key={key}
|
|
initialValue={valueToNullableString(val)}
|
|
onUpdateValue={onUpdateValue}
|
|
/>
|
|
),
|
|
};
|
|
}
|
|
|
|
const EditField = React.memo(
|
|
(props: {
|
|
initialValue: string | null;
|
|
onUpdateValue: (value: string | null) => void;
|
|
}) => {
|
|
const {initialValue, onUpdateValue} = props;
|
|
const [value, setValue] = useState<string | null>(initialValue);
|
|
useEffect(() => setValue(initialValue), [initialValue]);
|
|
return (
|
|
<Input
|
|
value={value || ''}
|
|
onChange={(e) => {
|
|
setValue(e.target.value);
|
|
onUpdateValue(e.target.value);
|
|
}}
|
|
placeholder={value === null ? 'NULL' : undefined}
|
|
data-testid={'update-query-input'}
|
|
style={{width: '100%'}}
|
|
/>
|
|
);
|
|
},
|
|
);
|
|
|
|
type RowState = {changes: {[key: string]: string | null}; updated: boolean};
|
|
type RowAction =
|
|
| {type: 'set'; key: string; value: string | null}
|
|
| {type: 'reset'};
|
|
|
|
const rowStateReducer = produce((draftState: RowState, action: RowAction) => {
|
|
switch (action.type) {
|
|
case 'set':
|
|
draftState.changes[action.key] = action.value;
|
|
draftState.updated = true;
|
|
return;
|
|
case 'reset':
|
|
draftState.changes = {};
|
|
draftState.updated = false;
|
|
return;
|
|
}
|
|
});
|
|
|
|
export default React.memo(function DatabaseDetailSidebar(
|
|
props: DatabaseDetailSidebarProps,
|
|
) {
|
|
const [editing, setEditing] = useState(false);
|
|
const [rowState, rowDispatch] = useReducer(rowStateReducer, {
|
|
changes: {},
|
|
updated: false,
|
|
});
|
|
const {columnLabels, columnValues, onSave} = props;
|
|
useEffect(() => rowDispatch({type: 'reset'}), [columnLabels, columnValues]);
|
|
const rows = useMemo(
|
|
() =>
|
|
editing
|
|
? sidebarEditableRows(columnLabels, columnValues, rowDispatch)
|
|
: sidebarRows(columnLabels, columnValues),
|
|
[columnLabels, columnValues, editing],
|
|
);
|
|
return (
|
|
<DetailSidebar>
|
|
<Panel
|
|
heading="Row details"
|
|
floating={false}
|
|
collapsable={true}
|
|
padded={false}>
|
|
{onSave ? (
|
|
<EditTriggerSection>
|
|
{editing ? (
|
|
<>
|
|
<Button
|
|
disabled={!rowState.updated}
|
|
onClick={() => {
|
|
onSave(rowState.changes);
|
|
setEditing(false);
|
|
}}>
|
|
Save
|
|
</Button>
|
|
<Button onClick={() => setEditing(false)}>Close</Button>
|
|
</>
|
|
) : (
|
|
<Button onClick={() => setEditing(true)}>Edit</Button>
|
|
)}
|
|
</EditTriggerSection>
|
|
) : null}
|
|
<div>
|
|
{rows.map((row) => (
|
|
<TableDetailRow key={row.col}>
|
|
<TableDetailRowTitle>
|
|
{row.col}
|
|
<TableDetailRowType>({row.type})</TableDetailRowType>
|
|
</TableDetailRowTitle>
|
|
<TableDetailRowValue>{row.value}</TableDetailRowValue>
|
|
</TableDetailRow>
|
|
))}
|
|
</div>
|
|
</Panel>
|
|
</DetailSidebar>
|
|
);
|
|
});
|