apollo-client-devtools
Version:
Developer tools for Apollo Client, with GraphiQL integration, cache inspection, and query watching.
456 lines (386 loc) • 10.9 kB
JavaScript
import PropTypes from "prop-types";
import React from "react";
import { Sidebar } from "../Sidebar";
import classnames from "classnames";
import flattenDeep from "lodash.flattendeep";
import "./inspector.less";
export default class Inspector extends React.Component {
static childContextTypes = {
inspectorContext: PropTypes.object.isRequired,
};
constructor(props) {
super(props);
this.state = {
dataWithOptimistic: null,
ids: [],
selectedId: null,
toHighlight: {},
searchTerm: "",
};
this.setSearchTerm = this.setSearchTerm.bind(this);
}
updateDataInStore(dataWithOptimistic) {
let toHighlight = {};
if (this.state.searchTerm.length >= 3) {
toHighlight = highlightFromSearchTerm({
data: dataWithOptimistic,
query: this.state.searchTerm,
});
}
const unsortedIds = Object.keys(dataWithOptimistic).filter(
id => id[0] !== "$",
);
const highlightedIds = Object.keys(toHighlight).filter(
id => id[0] !== "$" && id !== "ROOT_QUERY",
);
const sortedIdsWithoutRoot = unsortedIds
.filter(id => id !== "ROOT_QUERY" && highlightedIds.indexOf(id) < 0)
.sort();
const ids = [...highlightedIds, "ROOT_QUERY", ...sortedIdsWithoutRoot];
const selectedId = this.state.selectedId || ids[0];
this.setState({
dataWithOptimistic,
toHighlight,
ids,
selectedId: this.state.selectedId || ids[0],
});
}
componentDidMount() {
// this.updateData();
if (this.props.state.inspector) {
this.updateDataInStore(this.props.state.inspector);
}
}
componentWillUnmount() {
clearTimeout(this._interval);
}
componentWillReceiveProps(nextProps) {
if (!nextProps) {
return [null, null, null, null];
}
this.updateDataInStore(nextProps.state.inspector);
}
getChildContext() {
return {
inspectorContext: {
dataWithOptimistic: this.state.dataWithOptimistic,
toHighlight: this.state.toHighlight,
selectId: this.selectId.bind(this),
},
};
}
componentWillUpdate(nextProps, nextState) {
if (nextState.selectedId !== this.state.selectedId) {
function isInViewport(element) {
const rect = element.getBoundingClientRect();
const html = document.documentElement;
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || html.clientHeight) &&
rect.right <= (window.innerWidth || html.clientWidth)
);
}
const node = document.getElementById(nextState.selectedId);
if (node && !isInViewport(node)) node.scrollIntoView();
}
}
selectId(id) {
this.setState({
selectedId: id,
});
}
setSearchTerm(searchTerm) {
let toHighlight = {};
if (searchTerm.length >= 3) {
toHighlight = highlightFromSearchTerm({
data: this.state.dataWithOptimistic,
query: searchTerm,
});
}
this.setState({
searchTerm,
toHighlight,
});
}
renderSidebarItem(id) {
let className = "inspector-sidebar-item";
if (id === this.state.selectedId) {
className += " active";
}
if (this.state.toHighlight[id]) {
className += " inspector-sidebar-highlighted";
}
return (
<div
id={id}
key={`inspector-sidebar-item-${id}`}
onClick={this.selectId.bind(this, id)}
className={className}
>
{id}
</div>
);
}
render() {
const { selectedId, dataWithOptimistic, searchTerm, ids } = this.state;
return (
<div className="inspector-panel body">
<div className="inspector-body">
<Sidebar className="inspector-sidebar" name="inspector-sidebar">
<h4 className="inspector-sidebar-title">Cache</h4>
<InspectorToolbar
searchTerm={searchTerm}
setSearchTerm={this.setSearchTerm}
/>
{ids && ids.map(id => this.renderSidebarItem(id))}
</Sidebar>
<div className="inspector-main">
{selectedId && (
<StoreTreeFieldSet
data={dataWithOptimistic}
dataId={selectedId}
expand={true}
/>
)}
</div>
</div>
</div>
);
}
}
const InspectorToolbar = ({ searchTerm, setSearchTerm }) => (
<div className="inspector-toolbar">
<input
className="inspector-search"
type="text"
placeholder="Search..."
onChange={evt => setSearchTerm(evt.target.value)}
value={searchTerm}
/>
</div>
);
function highlightFromSearchTerm({ data, query }) {
const toHighlight = {};
Object.keys(data).forEach(dataId => {
dfsSearch({
data,
regex: new RegExp(query),
toHighlight,
dataId,
});
});
return toHighlight;
}
function dfsSearch({ data, regex, toHighlight, pathToId = [], dataId }) {
const storeObj = data[dataId];
const storeObjHighlight = {};
Object.keys(storeObj).forEach(storeFieldKey => {
const arr = [storeObj[storeFieldKey]];
const flatArr = flattenDeep(arr);
flatArr.forEach(val => {
const valueMatches = typeof val === "string" && regex.test(val);
const keyMatches = regex.test(storeFieldKey);
if (valueMatches || keyMatches) {
storeObjHighlight[storeFieldKey] = val;
}
if (val && val.id && val.generated) {
dfsSearch({
data,
regex,
toHighlight,
pathToId: [...pathToId, [dataId, storeFieldKey]],
dataId: val.id,
});
}
});
});
if (Object.keys(storeObjHighlight).length > 0) {
toHighlight[dataId] = storeObjHighlight;
pathToId.forEach(pathSegment => {
toHighlight[pathSegment[0]] = toHighlight[pathSegment[0]] || {};
toHighlight[pathSegment[0]][pathSegment[1]] =
data[pathSegment[0]][pathSegment[1]];
});
}
}
// Props: data, dataId, expand
class StoreTreeFieldSet extends React.Component {
static contextTypes = {
inspectorContext: PropTypes.object.isRequired,
};
constructor(props) {
super();
this.state = {
expand: props.expand || props.dataId[0] === "$",
};
this.toggleExpand = this.toggleExpand.bind(this);
this.selectId = this.selectId.bind(this);
}
getStoreObj() {
return this.context.inspectorContext.dataWithOptimistic[this.props.dataId];
}
getHighlightObj() {
return this.context.inspectorContext.toHighlight[this.props.dataId];
}
shouldDisplayId() {
return this.props.dataId[0] !== "$";
}
keysToDisplay() {
return Object.keys(this.getStoreObj())
.filter(key => key !== "__typename")
.sort();
}
renderFieldSet({ doubleIndent }) {
const storeObj = this.getStoreObj();
const highlightObj = this.getHighlightObj();
let className = "store-tree-field-set";
if (doubleIndent) {
className += " double-indent";
}
return (
<div className={className}>
{this.keysToDisplay().map(key => (
<StoreTreeField
key={key}
storeKey={key}
value={storeObj[key]}
highlight={!!(highlightObj && highlightObj[key])}
/>
))}
</div>
);
}
toggleExpand() {
this.setState(({ expand }) => ({ expand: !expand }));
}
selectId() {
this.context.inspectorContext.selectId(this.props.dataId);
}
render() {
return (
<span>
{this.shouldDisplayId() && (
<span
className={classnames("store-tree-ref-id toggle", {
"store-tree-ref-id-inline": this.props.inArray,
})}
onClick={this.toggleExpand}
>
<span
className={classnames("triangle", {
toggled: !this.state.expand,
})}
>
▾
</span>
<span className="data-id">{this.props.dataId}</span>
<span className="jump-to-object" onClick={this.selectId} />
</span>
)}
<div>
{this.state.expand &&
this.renderFieldSet({ doubleIndent: this.shouldDisplayId() })}
</div>
</span>
);
}
}
const StoreTreeArray = ({ value }) => {
return (
<div className="store-tree-field-set">
{value.map((item, index) => (
<StoreTreeArrayItem key={index} item={item} index={index} />
))}
</div>
);
};
const StoreTreeArrayItem = ({ item, index }) => (
<div>
<span className="inspector-field-key">{index}</span>
:
<StoreTreeValue value={item} inArray={true} />
</div>
);
const StoreTreeObject = ({ value, highlight, inArray }) => {
if (isIdReference(value)) {
return (
<StoreTreeFieldSet
key={`storee-tree-field-set-${value.id}`}
dataId={value.id}
inArray={inArray}
/>
);
}
let className = "";
if (typeof value === "string") {
className += " inspector-value-string";
}
if (typeof value === "number") {
className += " inspector-value-number";
}
if (highlight) {
className += " inspector-highlight";
}
return <span className={className}>{JSON.stringify(value)}</span>;
};
// props: data, value
const StoreTreeValue = props => (
<span>
{Array.isArray(props.value) ? (
<StoreTreeArray {...props} />
) : (
<StoreTreeObject {...props} />
)}
</span>
);
// Props: data, storeKey, value
class StoreTreeField extends React.Component {
static contextTypes = {
inspectorContext: PropTypes.object.isRequired,
};
getPossibleTypename() {
let unwrapped = this.props.value;
let isArray = false;
while (Array.isArray(unwrapped)) {
unwrapped = unwrapped[0];
isArray = true;
}
const targetStoreObj =
unwrapped &&
unwrapped.id &&
this.context.inspectorContext.dataWithOptimistic[unwrapped.id];
const baseTypename = targetStoreObj && targetStoreObj.__typename;
if (baseTypename && isArray) {
return "[" + baseTypename + "]";
}
return baseTypename;
}
renderPossibleTypename() {
const __typename = this.getPossibleTypename();
if (!__typename) {
return "";
}
return <span className="inspector-typename">{__typename}</span>;
}
render() {
let className = "inspector-field-key";
if (this.props.highlight) {
className += " inspector-highlight";
}
return (
<div>
<span className={className}>{this.props.storeKey}</span>
: {this.renderPossibleTypename()}
<StoreTreeValue
value={this.props.value}
highlight={this.props.highlight}
/>
</div>
);
}
}
// Should be imported from AC
function isIdReference(storeObj) {
return storeObj && storeObj.type === "id";
}