277 lines
7.6 KiB
JavaScript
277 lines
7.6 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
import {
|
|
createFactory,
|
|
Component,
|
|
} from "resource://devtools/client/shared/vendor/react.mjs";
|
|
import * as dom from "resource://devtools/client/shared/vendor/react-dom-factories.mjs";
|
|
import PropTypes from "resource://devtools/client/shared/vendor/react-prop-types.mjs";
|
|
import { createFactories } from "resource://devtools/client/shared/react-utils.mjs";
|
|
|
|
import TreeViewClass from "resource://devtools/client/shared/components/tree/TreeView.mjs";
|
|
import { BucketProperty } from "resource://devtools/client/shared/components/tree/ObjectProvider.mjs";
|
|
import JsonToolbarClass from "resource://devtools/client/jsonview/components/JsonToolbar.mjs";
|
|
import JsonBreadcrumbClass from "resource://devtools/client/jsonview/components/JsonBreadcrumb.mjs";
|
|
|
|
import {
|
|
JSON_NUMBER,
|
|
MODE,
|
|
} from "resource://devtools/client/shared/components/reps/reps/constants.mjs";
|
|
import { Rep } from "resource://devtools/client/shared/components/reps/reps/rep.mjs";
|
|
|
|
const TreeView = createFactory(TreeViewClass);
|
|
const { JsonToolbar } = createFactories(JsonToolbarClass);
|
|
const { JsonBreadcrumb } = createFactories(JsonBreadcrumbClass);
|
|
const { div } = dom;
|
|
|
|
const MAX_STRING_LENGTH = 250;
|
|
|
|
function isObject(value) {
|
|
return Object(value) === value;
|
|
}
|
|
|
|
/**
|
|
* This template represents the 'JSON' panel. The panel is
|
|
* responsible for rendering an expandable tree that allows simple
|
|
* inspection of JSON structure.
|
|
*/
|
|
class JsonPanel extends Component {
|
|
static get propTypes() {
|
|
return {
|
|
data: PropTypes.oneOfType([
|
|
PropTypes.string,
|
|
PropTypes.array,
|
|
PropTypes.object,
|
|
PropTypes.bool,
|
|
PropTypes.number,
|
|
]),
|
|
dataSize: PropTypes.number,
|
|
expandedNodes: PropTypes.instanceOf(Set),
|
|
searchFilter: PropTypes.string,
|
|
actions: PropTypes.object,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Splits a path string by unescaped forward slashes (`/`).
|
|
*
|
|
* Escaped slashes (`\/`) are preserved as literal `/` characters
|
|
* in the resulting segments. Escape characters (`\`) are removed.
|
|
*
|
|
* Examples:
|
|
* splitPath("a/b/c") → ["a", "b", "c"]
|
|
* splitPath("a\\/b/c") → ["a/b", "c"]
|
|
* splitPath("/a/b/") → ["a", "b"]
|
|
*
|
|
* @param {string} path
|
|
* The input path string. Forward slashes can be escaped using `\`.
|
|
*
|
|
* @returns {string[]}
|
|
* An array of non-empty path segments with escape characters removed.
|
|
*/
|
|
static splitPath(path) {
|
|
return path
|
|
.split(/(?<!\\)\//)
|
|
.map(segment => segment.replace(/\\(.)/g, "$1"))
|
|
.filter(_s => _s.length);
|
|
}
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = {};
|
|
this.onKeyPress = this.onKeyPress.bind(this);
|
|
this.onFilter = this.onFilter.bind(this);
|
|
this.renderValue = this.renderValue.bind(this);
|
|
this.renderTree = this.renderTree.bind(this);
|
|
this.onRowSelected = this.onRowSelected.bind(this);
|
|
this.updateBreadcrumbs = this.updateBreadcrumbs.bind(this);
|
|
}
|
|
|
|
componentDidMount() {
|
|
document.addEventListener("keypress", this.onKeyPress, true);
|
|
document.getElementById("json-scrolling-panel").focus();
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
document.removeEventListener("keypress", this.onKeyPress, true);
|
|
}
|
|
|
|
onKeyPress() {
|
|
// XXX shortcut for focusing the Filter field (see Bug 1178771).
|
|
}
|
|
|
|
onRowSelected(selectedRowPath) {
|
|
if (!selectedRowPath || typeof selectedRowPath !== "string") {
|
|
return;
|
|
}
|
|
const rowPathParts = JsonPanel.splitPath(selectedRowPath);
|
|
this.updateBreadcrumbs(rowPathParts);
|
|
}
|
|
|
|
/**
|
|
* @param {string[]} rowPathParts
|
|
*/
|
|
updateBreadcrumbs(rowPathParts) {
|
|
let jsonData = this.props.data;
|
|
const breadcrumbs = [];
|
|
|
|
for (let i = 0; i < rowPathParts.length; i++) {
|
|
if (typeof jsonData !== "object") {
|
|
break;
|
|
}
|
|
const key = rowPathParts[i];
|
|
// Skip bucket range segments like "[0…99]"
|
|
if (jsonData[key] === undefined && Array.isArray(jsonData)) {
|
|
continue;
|
|
}
|
|
jsonData = jsonData[key];
|
|
breadcrumbs.push({ type: getJsonValueType(jsonData), text: key });
|
|
}
|
|
|
|
if (breadcrumbs.length) {
|
|
this.setState({ breadcrumbs });
|
|
}
|
|
}
|
|
|
|
onFilter(object) {
|
|
if (!this.props.searchFilter) {
|
|
return true;
|
|
}
|
|
|
|
const searchFilter = this.props.searchFilter.toLowerCase();
|
|
|
|
// For bucket nodes, check if any of their children match
|
|
if (object instanceof BucketProperty) {
|
|
const { object: array, startIndex, endIndex } = object;
|
|
for (let i = startIndex; i <= endIndex; i++) {
|
|
const childJson = JSON.stringify(array[i]);
|
|
if (childJson.toLowerCase().includes(searchFilter)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
const json = object.name + JSON.stringify(object.value);
|
|
return json.toLowerCase().includes(searchFilter);
|
|
}
|
|
|
|
renderValue(props) {
|
|
const member = props.member;
|
|
|
|
// Hide value for bucket nodes (they show ranges like [0…99])
|
|
if (member.type === "bucket") {
|
|
return null;
|
|
}
|
|
|
|
// Hide object summary when non-empty object is expanded (bug 1244912).
|
|
if (isObject(member.value) && member.hasChildren && member.open) {
|
|
return null;
|
|
}
|
|
|
|
// Render the value (summary) using Reps library.
|
|
return Rep(
|
|
Object.assign({}, props, {
|
|
cropLimit: MAX_STRING_LENGTH,
|
|
noGrip: true,
|
|
isInContentPage: true,
|
|
})
|
|
);
|
|
}
|
|
|
|
renderTree() {
|
|
// Append custom column for displaying values. This column
|
|
// Take all available horizontal space.
|
|
const columns = [
|
|
{
|
|
id: "value",
|
|
width: "100%",
|
|
},
|
|
];
|
|
|
|
// Render tree component.
|
|
return TreeView({
|
|
object: this.props.data,
|
|
mode: MODE.LONG,
|
|
bucketLargeArrays: true,
|
|
onFilter: this.onFilter,
|
|
columns,
|
|
renderValue: this.renderValue,
|
|
expandedNodes: this.props.expandedNodes,
|
|
maxStringLength: MAX_STRING_LENGTH,
|
|
onRowSelected: this.onRowSelected,
|
|
});
|
|
}
|
|
|
|
render() {
|
|
let content;
|
|
const data = this.props.data;
|
|
|
|
if (!isObject(data)) {
|
|
content = div(
|
|
{ className: "jsonPrimitiveValue" },
|
|
Rep({
|
|
object: data,
|
|
})
|
|
);
|
|
} else if (data instanceof Error) {
|
|
content = div({ className: "jsonParseError" }, data + "");
|
|
} else if (data.type === JSON_NUMBER) {
|
|
content = div(
|
|
{ className: "jsonPrimitiveValue" },
|
|
Rep({
|
|
object: data,
|
|
noGrip: true,
|
|
})
|
|
);
|
|
} else {
|
|
content = this.renderTree();
|
|
}
|
|
|
|
return div(
|
|
{ className: "jsonPanelBox tab-panel-inner" },
|
|
JsonToolbar({
|
|
actions: this.props.actions,
|
|
dataSize: this.props.dataSize,
|
|
}),
|
|
div(
|
|
{
|
|
className: "panelContent",
|
|
id: "json-scrolling-panel",
|
|
tabIndex: 0,
|
|
},
|
|
content
|
|
),
|
|
JsonBreadcrumb({
|
|
items: this.state.breadcrumbs,
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
// Helpers
|
|
|
|
/**
|
|
* Determines the type of a given value.
|
|
* Returns a string representing the type of the input value.
|
|
*
|
|
* @param {*} value - The value whose type is to be determined.
|
|
* @returns {string} The type of the input value as a string.
|
|
* `array`, `null`, `object`, `number`, `boolean`, `string`
|
|
*/
|
|
function getJsonValueType(value) {
|
|
if (value?.type === JSON_NUMBER) {
|
|
return "number";
|
|
}
|
|
if (Array.isArray(value)) {
|
|
return "array";
|
|
} else if (value === null) {
|
|
return "null";
|
|
}
|
|
return typeof value;
|
|
}
|
|
|
|
export default { JsonPanel };
|