mirror of
https://github.com/usebruno/bruno.git
synced 2025-06-20 19:58:10 +02:00
feat: standalone graphiql docs explorer
This commit is contained in:
parent
a59ae75809
commit
3753fd1e20
@ -30,6 +30,7 @@
|
|||||||
"idb": "^7.0.0",
|
"idb": "^7.0.0",
|
||||||
"immer": "^9.0.15",
|
"immer": "^9.0.15",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"markdown-it": "^13.0.1",
|
||||||
"mousetrap": "^1.6.5",
|
"mousetrap": "^1.6.5",
|
||||||
"nanoid": "3.3.4",
|
"nanoid": "3.3.4",
|
||||||
"next": "12.3.1",
|
"next": "12.3.1",
|
||||||
|
@ -10,12 +10,23 @@
|
|||||||
"@rollup/plugin-commonjs": "^23.0.2",
|
"@rollup/plugin-commonjs": "^23.0.2",
|
||||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||||
"@rollup/plugin-typescript": "^9.0.2",
|
"@rollup/plugin-typescript": "^9.0.2",
|
||||||
|
"@types/markdown-it": "^12.2.3",
|
||||||
"@types/react": "^18.0.25",
|
"@types/react": "^18.0.25",
|
||||||
|
"graphql": "^16.6.0",
|
||||||
|
"markdown-it": "^13.0.1",
|
||||||
|
"postcss": "^8.4.18",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
|
"react-dom": "^17.0.2",
|
||||||
"rollup": "3.2.5",
|
"rollup": "3.2.5",
|
||||||
"rollup-plugin-dts": "^5.0.0",
|
"rollup-plugin-dts": "^5.0.0",
|
||||||
"rollup-plugin-peer-deps-external": "^2.2.4",
|
"rollup-plugin-peer-deps-external": "^2.2.4",
|
||||||
|
"rollup-plugin-postcss": "^4.0.2",
|
||||||
"rollup-plugin-terser": "^7.0.2",
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
"typescript": "^4.8.4"
|
"typescript": "^4.8.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"graphql": "^16.6.0",
|
||||||
|
"markdown-it": "^13.0.1",
|
||||||
|
"react": "^17.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import resolve from "@rollup/plugin-node-resolve";
|
const { nodeResolve } = require("@rollup/plugin-node-resolve");
|
||||||
import commonjs from "@rollup/plugin-commonjs";
|
const commonjs = require("@rollup/plugin-commonjs");
|
||||||
import typescript from "@rollup/plugin-typescript";
|
const typescript = require("@rollup/plugin-typescript");
|
||||||
import dts from "rollup-plugin-dts";
|
const dts = require("rollup-plugin-dts");
|
||||||
import { terser } from "rollup-plugin-terser";
|
const postcss = require("rollup-plugin-postcss");
|
||||||
import peerDepsExternal from 'rollup-plugin-peer-deps-external';
|
const { terser } = require("rollup-plugin-terser");
|
||||||
|
const peerDepsExternal = require('rollup-plugin-peer-deps-external');
|
||||||
|
|
||||||
const packageJson = require("./package.json");
|
const packageJson = require("./package.json");
|
||||||
|
|
||||||
export default [
|
module.exports = [
|
||||||
{
|
{
|
||||||
input: "src/index.ts",
|
input: "src/index.ts",
|
||||||
output: [
|
output: [
|
||||||
@ -23,12 +24,23 @@ export default [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
|
postcss({
|
||||||
|
minimize: true,
|
||||||
|
extensions: ['.css']
|
||||||
|
}),
|
||||||
peerDepsExternal(),
|
peerDepsExternal(),
|
||||||
resolve(),
|
nodeResolve({
|
||||||
|
extensions: ['.css']
|
||||||
|
}),
|
||||||
commonjs(),
|
commonjs(),
|
||||||
typescript({ tsconfig: "./tsconfig.json" }),
|
typescript({ tsconfig: "./tsconfig.json" }),
|
||||||
terser(),
|
terser()
|
||||||
],
|
],
|
||||||
external: ["react", "react-dom", "styled-components"]
|
external: ["react", "react-dom", "index.css"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "dist/esm/index.d.ts",
|
||||||
|
output: [{ file: "dist/index.d.ts", format: "esm" }],
|
||||||
|
plugins: [dts.default()],
|
||||||
}
|
}
|
||||||
];
|
];
|
@ -1,9 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
class GraphDocs extends React.Component {
|
|
||||||
render() {
|
|
||||||
return "Graphql Docs Explorer"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default GraphDocs;
|
|
233
packages/bruno-graphql-docs/src/components/DocExplorer.tsx
Normal file
233
packages/bruno-graphql-docs/src/components/DocExplorer.tsx
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 GraphQL Contributors.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import { GraphQLSchema, isType, GraphQLNamedType, GraphQLError } from 'graphql';
|
||||||
|
import { FieldType } from './DocExplorer/types';
|
||||||
|
|
||||||
|
import FieldDoc from './DocExplorer/FieldDoc';
|
||||||
|
import SchemaDoc from './DocExplorer/SchemaDoc';
|
||||||
|
import SearchBox from './DocExplorer/SearchBox';
|
||||||
|
import SearchResults from './DocExplorer/SearchResults';
|
||||||
|
import TypeDoc from './DocExplorer/TypeDoc';
|
||||||
|
|
||||||
|
type NavStackItem = {
|
||||||
|
name: string;
|
||||||
|
title?: string;
|
||||||
|
search?: string;
|
||||||
|
def?: GraphQLNamedType | FieldType;
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialNav: NavStackItem = {
|
||||||
|
name: 'Schema',
|
||||||
|
title: 'Documentation Explorer',
|
||||||
|
};
|
||||||
|
|
||||||
|
type DocExplorerProps = {
|
||||||
|
schema?: GraphQLSchema | null;
|
||||||
|
schemaErrors?: readonly GraphQLError[];
|
||||||
|
children?: ReactNode | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DocExplorerState = {
|
||||||
|
navStack: NavStackItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DocExplorer
|
||||||
|
*
|
||||||
|
* Shows documentations for GraphQL definitions from the schema.
|
||||||
|
*
|
||||||
|
* Props:
|
||||||
|
*
|
||||||
|
* - schema: A required GraphQLSchema instance that provides GraphQL document
|
||||||
|
* definitions.
|
||||||
|
*
|
||||||
|
* Children:
|
||||||
|
*
|
||||||
|
* - Any provided children will be positioned in the right-hand-side of the
|
||||||
|
* top bar. Typically this will be a "close" button for temporary explorer.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export class DocExplorer extends React.Component<
|
||||||
|
DocExplorerProps,
|
||||||
|
DocExplorerState
|
||||||
|
> {
|
||||||
|
// handleClickTypeOrField: OnClickTypeFunction | OnClickFieldFunction
|
||||||
|
constructor(props: DocExplorerProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = { navStack: [initialNav] };
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldComponentUpdate(
|
||||||
|
nextProps: DocExplorerProps,
|
||||||
|
nextState: DocExplorerState,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
this.props.schema !== nextProps.schema ||
|
||||||
|
this.state.navStack !== nextState.navStack ||
|
||||||
|
this.props.schemaErrors !== nextProps.schemaErrors
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { schema, schemaErrors } = this.props;
|
||||||
|
const navStack = this.state.navStack;
|
||||||
|
const navItem = navStack[navStack.length - 1];
|
||||||
|
|
||||||
|
let content;
|
||||||
|
if (schemaErrors) {
|
||||||
|
content = (
|
||||||
|
<div className="error-container">{'Error fetching schema'}</div>
|
||||||
|
);
|
||||||
|
} else if (schema === undefined) {
|
||||||
|
// Schema is undefined when it is being loaded via introspection.
|
||||||
|
content = (
|
||||||
|
<div className="spinner-container">
|
||||||
|
<div className="spinner" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (!schema) {
|
||||||
|
// Schema is null when it explicitly does not exist, typically due to
|
||||||
|
// an error during introspection.
|
||||||
|
content = <div className="error-container">{'No Schema Available'}</div>;
|
||||||
|
} else if (navItem.search) {
|
||||||
|
content = (
|
||||||
|
<SearchResults
|
||||||
|
searchValue={navItem.search}
|
||||||
|
withinType={navItem.def as GraphQLNamedType}
|
||||||
|
schema={schema}
|
||||||
|
onClickType={this.handleClickType}
|
||||||
|
onClickField={this.handleClickField}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (navStack.length === 1) {
|
||||||
|
content = (
|
||||||
|
<SchemaDoc schema={schema} onClickType={this.handleClickType} />
|
||||||
|
);
|
||||||
|
} else if (isType(navItem.def)) {
|
||||||
|
content = (
|
||||||
|
<TypeDoc
|
||||||
|
schema={schema}
|
||||||
|
type={navItem.def}
|
||||||
|
onClickType={this.handleClickType}
|
||||||
|
onClickField={this.handleClickField}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
content = (
|
||||||
|
<FieldDoc
|
||||||
|
field={navItem.def as FieldType}
|
||||||
|
onClickType={this.handleClickType}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldSearchBoxAppear =
|
||||||
|
navStack.length === 1 ||
|
||||||
|
(isType(navItem.def) && 'getFields' in navItem.def);
|
||||||
|
|
||||||
|
let prevName;
|
||||||
|
if (navStack.length > 1) {
|
||||||
|
prevName = navStack[navStack.length - 2].name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="graphql-docs-container">
|
||||||
|
<section
|
||||||
|
className="doc-explorer"
|
||||||
|
key={navItem.name}
|
||||||
|
aria-label="Documentation Explorer">
|
||||||
|
<div className="doc-explorer-title-bar">
|
||||||
|
{prevName && (
|
||||||
|
<button
|
||||||
|
className="doc-explorer-back"
|
||||||
|
onClick={this.handleNavBackClick}
|
||||||
|
aria-label={`Go back to ${prevName}`}>
|
||||||
|
{prevName}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<div className="doc-explorer-title">
|
||||||
|
{navItem.title || navItem.name}
|
||||||
|
</div>
|
||||||
|
<div className="doc-explorer-rhs">{this.props.children}</div>
|
||||||
|
</div>
|
||||||
|
<div className="doc-explorer-contents">
|
||||||
|
{shouldSearchBoxAppear && (
|
||||||
|
<SearchBox
|
||||||
|
value={navItem.search}
|
||||||
|
placeholder={`Search ${navItem.name}...`}
|
||||||
|
onSearch={this.handleSearch}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public API
|
||||||
|
showDoc(typeOrField: GraphQLNamedType | FieldType) {
|
||||||
|
const navStack = this.state.navStack;
|
||||||
|
const topNav = navStack[navStack.length - 1];
|
||||||
|
if (topNav.def !== typeOrField) {
|
||||||
|
this.setState({
|
||||||
|
navStack: navStack.concat([
|
||||||
|
{
|
||||||
|
name: typeOrField.name,
|
||||||
|
def: typeOrField,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public API
|
||||||
|
showDocForReference(reference: any) {
|
||||||
|
if (reference && reference.kind === 'Type') {
|
||||||
|
this.showDoc(reference.type);
|
||||||
|
} else if (reference.kind === 'Field') {
|
||||||
|
this.showDoc(reference.field);
|
||||||
|
} else if (reference.kind === 'Argument' && reference.field) {
|
||||||
|
this.showDoc(reference.field);
|
||||||
|
} else if (reference.kind === 'EnumValue' && reference.type) {
|
||||||
|
this.showDoc(reference.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public API
|
||||||
|
showSearch(search: string) {
|
||||||
|
const navStack = this.state.navStack.slice();
|
||||||
|
const topNav = navStack[navStack.length - 1];
|
||||||
|
navStack[navStack.length - 1] = { ...topNav, search };
|
||||||
|
this.setState({ navStack });
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.setState({ navStack: [initialNav] });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNavBackClick = () => {
|
||||||
|
if (this.state.navStack.length > 1) {
|
||||||
|
this.setState({ navStack: this.state.navStack.slice(0, -1) });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClickType = (type: GraphQLNamedType) => {
|
||||||
|
this.showDoc(type);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClickField = (field: FieldType) => {
|
||||||
|
this.showDoc(field);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSearch = (value: string) => {
|
||||||
|
this.showSearch(value);
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 GraphQL Contributors.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { GraphQLArgument } from 'graphql';
|
||||||
|
import TypeLink from './TypeLink';
|
||||||
|
import DefaultValue from './DefaultValue';
|
||||||
|
import { OnClickTypeFunction } from './types';
|
||||||
|
|
||||||
|
type ArgumentProps = {
|
||||||
|
arg: GraphQLArgument;
|
||||||
|
onClickType: OnClickTypeFunction;
|
||||||
|
showDefaultValue?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Argument({
|
||||||
|
arg,
|
||||||
|
onClickType,
|
||||||
|
showDefaultValue,
|
||||||
|
}: ArgumentProps) {
|
||||||
|
return (
|
||||||
|
<span className="arg">
|
||||||
|
<span className="arg-name">{arg.name}</span>
|
||||||
|
{': '}
|
||||||
|
<TypeLink type={arg.type} onClick={onClickType} />
|
||||||
|
{showDefaultValue !== false && <DefaultValue field={arg} />}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 GraphQL Contributors.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { astFromValue, print, ValueNode } from 'graphql';
|
||||||
|
import { FieldType } from './types';
|
||||||
|
|
||||||
|
const printDefault = (ast?: ValueNode | null): string => {
|
||||||
|
if (!ast) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return print(ast);
|
||||||
|
};
|
||||||
|
|
||||||
|
type DefaultValueProps = {
|
||||||
|
field: FieldType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function DefaultValue({ field }: DefaultValueProps) {
|
||||||
|
// field.defaultValue could be null or false, so be careful here!
|
||||||
|
if ('defaultValue' in field && field.defaultValue !== undefined) {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{' = '}
|
||||||
|
<span className="arg-default-value">
|
||||||
|
{printDefault(astFromValue(field.defaultValue, field.type))}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 GraphQL Contributors.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { DirectiveNode } from 'graphql';
|
||||||
|
|
||||||
|
type DirectiveProps = {
|
||||||
|
directive: DirectiveNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Directive({ directive }: DirectiveProps) {
|
||||||
|
return (
|
||||||
|
<span className="doc-category-item" id={directive.name.value}>
|
||||||
|
{'@'}
|
||||||
|
{directive.name.value}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,129 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 GraphQL Contributors.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import Argument from './Argument';
|
||||||
|
import Directive from './Directive';
|
||||||
|
import MarkdownContent from './MarkdownContent';
|
||||||
|
import TypeLink from './TypeLink';
|
||||||
|
import { GraphQLArgument, DirectiveNode } from 'graphql';
|
||||||
|
import { OnClickTypeFunction, FieldType } from './types';
|
||||||
|
|
||||||
|
type FieldDocProps = {
|
||||||
|
field?: FieldType;
|
||||||
|
onClickType: OnClickTypeFunction;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function FieldDoc({ field, onClickType }: FieldDocProps) {
|
||||||
|
const [showDeprecated, handleShowDeprecated] = React.useState(false);
|
||||||
|
let argsDef;
|
||||||
|
let deprecatedArgsDef;
|
||||||
|
if (field && 'args' in field && field.args.length > 0) {
|
||||||
|
argsDef = (
|
||||||
|
<div id="doc-args" className="doc-category">
|
||||||
|
<div className="doc-category-title">{'arguments'}</div>
|
||||||
|
{field.args
|
||||||
|
.filter(arg => !arg.deprecationReason)
|
||||||
|
.map((arg: GraphQLArgument) => (
|
||||||
|
<div key={arg.name} className="doc-category-item">
|
||||||
|
<div>
|
||||||
|
<Argument arg={arg} onClickType={onClickType} />
|
||||||
|
</div>
|
||||||
|
<MarkdownContent
|
||||||
|
className="doc-value-description"
|
||||||
|
markdown={arg.description}
|
||||||
|
/>
|
||||||
|
{arg && 'deprecationReason' in arg && (
|
||||||
|
<MarkdownContent
|
||||||
|
className="doc-deprecation"
|
||||||
|
markdown={arg?.deprecationReason}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
const deprecatedArgs = field.args.filter(arg =>
|
||||||
|
Boolean(arg.deprecationReason),
|
||||||
|
);
|
||||||
|
if (deprecatedArgs.length > 0) {
|
||||||
|
deprecatedArgsDef = (
|
||||||
|
<div id="doc-deprecated-args" className="doc-category">
|
||||||
|
<div className="doc-category-title">{'deprecated arguments'}</div>
|
||||||
|
{!showDeprecated ? (
|
||||||
|
<button
|
||||||
|
className="show-btn"
|
||||||
|
onClick={() => handleShowDeprecated(!showDeprecated)}>
|
||||||
|
{'Show deprecated arguments...'}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
deprecatedArgs.map((arg, i) => (
|
||||||
|
<div key={i}>
|
||||||
|
<div>
|
||||||
|
<Argument arg={arg} onClickType={onClickType} />
|
||||||
|
</div>
|
||||||
|
<MarkdownContent
|
||||||
|
className="doc-value-description"
|
||||||
|
markdown={arg.description}
|
||||||
|
/>
|
||||||
|
{arg && 'deprecationReason' in arg && (
|
||||||
|
<MarkdownContent
|
||||||
|
className="doc-deprecation"
|
||||||
|
markdown={arg?.deprecationReason}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let directivesDef;
|
||||||
|
if (
|
||||||
|
field &&
|
||||||
|
field.astNode &&
|
||||||
|
field.astNode.directives &&
|
||||||
|
field.astNode.directives.length > 0
|
||||||
|
) {
|
||||||
|
directivesDef = (
|
||||||
|
<div id="doc-directives" className="doc-category">
|
||||||
|
<div className="doc-category-title">{'directives'}</div>
|
||||||
|
{field.astNode.directives.map((directive: DirectiveNode) => (
|
||||||
|
<div key={directive.name.value} className="doc-category-item">
|
||||||
|
<div>
|
||||||
|
<Directive directive={directive} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<MarkdownContent
|
||||||
|
className="doc-type-description"
|
||||||
|
markdown={field?.description || 'No Description'}
|
||||||
|
/>
|
||||||
|
{field && 'deprecationReason' in field && (
|
||||||
|
<MarkdownContent
|
||||||
|
className="doc-deprecation"
|
||||||
|
markdown={field?.deprecationReason}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="doc-category">
|
||||||
|
<div className="doc-category-title">{'type'}</div>
|
||||||
|
<TypeLink type={field?.type} onClick={onClickType} />
|
||||||
|
</div>
|
||||||
|
{argsDef}
|
||||||
|
{directivesDef}
|
||||||
|
{deprecatedArgsDef}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 GraphQL Contributors.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import MD from 'markdown-it';
|
||||||
|
|
||||||
|
type Maybe<T> = T | null | undefined;
|
||||||
|
|
||||||
|
const md = new MD({
|
||||||
|
// render urls as links, à la github-flavored markdown
|
||||||
|
linkify: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
type MarkdownContentProps = {
|
||||||
|
markdown?: Maybe<string>;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function MarkdownContent({
|
||||||
|
markdown,
|
||||||
|
className,
|
||||||
|
}: MarkdownContentProps) {
|
||||||
|
if (!markdown) {
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={className}
|
||||||
|
dangerouslySetInnerHTML={{ __html: md.render(markdown) }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 GraphQL Contributors.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import TypeLink from './TypeLink';
|
||||||
|
import MarkdownContent from './MarkdownContent';
|
||||||
|
import { GraphQLSchema } from 'graphql';
|
||||||
|
import { OnClickTypeFunction } from './types';
|
||||||
|
|
||||||
|
type SchemaDocProps = {
|
||||||
|
schema: GraphQLSchema;
|
||||||
|
onClickType: OnClickTypeFunction;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render the top level Schema
|
||||||
|
export default function SchemaDoc({ schema, onClickType }: SchemaDocProps) {
|
||||||
|
const queryType = schema.getQueryType();
|
||||||
|
const mutationType = schema.getMutationType && schema.getMutationType();
|
||||||
|
const subscriptionType =
|
||||||
|
schema.getSubscriptionType && schema.getSubscriptionType();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<MarkdownContent
|
||||||
|
className="doc-type-description"
|
||||||
|
markdown={
|
||||||
|
schema.description ||
|
||||||
|
'A GraphQL schema provides a root type for each kind of operation.'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="doc-category">
|
||||||
|
<div className="doc-category-title">{'root types'}</div>
|
||||||
|
<div className="doc-category-item">
|
||||||
|
<span className="keyword">{'query'}</span>
|
||||||
|
{': '}
|
||||||
|
<TypeLink type={queryType} onClick={onClickType} />
|
||||||
|
</div>
|
||||||
|
{mutationType && (
|
||||||
|
<div className="doc-category-item">
|
||||||
|
<span className="keyword">{'mutation'}</span>
|
||||||
|
{': '}
|
||||||
|
<TypeLink type={mutationType} onClick={onClickType} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{subscriptionType && (
|
||||||
|
<div className="doc-category-item">
|
||||||
|
<span className="keyword">{'subscription'}</span>
|
||||||
|
{': '}
|
||||||
|
<TypeLink type={subscriptionType} onClick={onClickType} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 GraphQL Contributors.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { ChangeEventHandler } from 'react';
|
||||||
|
|
||||||
|
import debounce from '../../utility/debounce';
|
||||||
|
|
||||||
|
type OnSearchFn = (value: string) => void;
|
||||||
|
|
||||||
|
type SearchBoxProps = {
|
||||||
|
value?: string;
|
||||||
|
placeholder: string;
|
||||||
|
onSearch: OnSearchFn;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SearchBoxState = {
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class SearchBox extends React.Component<
|
||||||
|
SearchBoxProps,
|
||||||
|
SearchBoxState
|
||||||
|
> {
|
||||||
|
debouncedOnSearch: OnSearchFn;
|
||||||
|
|
||||||
|
constructor(props: SearchBoxProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = { value: props.value || '' };
|
||||||
|
this.debouncedOnSearch = debounce(200, this.props.onSearch);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<label className="search-box">
|
||||||
|
<div className="search-box-icon" aria-hidden="true">
|
||||||
|
{'\u26b2'}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
value={this.state.value}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
type="text"
|
||||||
|
placeholder={this.props.placeholder}
|
||||||
|
aria-label={this.props.placeholder}
|
||||||
|
/>
|
||||||
|
{this.state.value && (
|
||||||
|
<button
|
||||||
|
className="search-box-clear"
|
||||||
|
onClick={this.handleClear}
|
||||||
|
aria-label="Clear search input">
|
||||||
|
{'\u2715'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange: ChangeEventHandler<HTMLInputElement> = event => {
|
||||||
|
const value = event.currentTarget.value;
|
||||||
|
this.setState({ value });
|
||||||
|
this.debouncedOnSearch(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClear = () => {
|
||||||
|
this.setState({ value: '' });
|
||||||
|
this.props.onSearch('');
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,164 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 GraphQL Contributors.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import { GraphQLSchema, GraphQLNamedType } from 'graphql';
|
||||||
|
|
||||||
|
import Argument from './Argument';
|
||||||
|
import TypeLink from './TypeLink';
|
||||||
|
import { OnClickFieldFunction, OnClickTypeFunction } from './types';
|
||||||
|
|
||||||
|
type SearchResultsProps = {
|
||||||
|
schema: GraphQLSchema;
|
||||||
|
withinType?: GraphQLNamedType;
|
||||||
|
searchValue: string;
|
||||||
|
onClickType: OnClickTypeFunction;
|
||||||
|
onClickField: OnClickFieldFunction;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class SearchResults extends React.Component<
|
||||||
|
SearchResultsProps,
|
||||||
|
{}
|
||||||
|
> {
|
||||||
|
shouldComponentUpdate(nextProps: SearchResultsProps) {
|
||||||
|
return (
|
||||||
|
this.props.schema !== nextProps.schema ||
|
||||||
|
this.props.searchValue !== nextProps.searchValue
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const searchValue = this.props.searchValue;
|
||||||
|
const withinType = this.props.withinType;
|
||||||
|
const schema = this.props.schema;
|
||||||
|
const onClickType = this.props.onClickType;
|
||||||
|
const onClickField = this.props.onClickField;
|
||||||
|
|
||||||
|
const matchedWithin: ReactNode[] = [];
|
||||||
|
const matchedTypes: ReactNode[] = [];
|
||||||
|
const matchedFields: ReactNode[] = [];
|
||||||
|
|
||||||
|
const typeMap = schema.getTypeMap();
|
||||||
|
let typeNames = Object.keys(typeMap);
|
||||||
|
|
||||||
|
// Move the within type name to be the first searched.
|
||||||
|
if (withinType) {
|
||||||
|
typeNames = typeNames.filter(n => n !== withinType.name);
|
||||||
|
typeNames.unshift(withinType.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const typeName of typeNames) {
|
||||||
|
if (
|
||||||
|
matchedWithin.length + matchedTypes.length + matchedFields.length >=
|
||||||
|
100
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = typeMap[typeName];
|
||||||
|
if (withinType !== type && isMatch(typeName, searchValue)) {
|
||||||
|
matchedTypes.push(
|
||||||
|
<div className="doc-category-item" key={typeName}>
|
||||||
|
<TypeLink type={type} onClick={onClickType} />
|
||||||
|
</div>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type && 'getFields' in type) {
|
||||||
|
const fields = type.getFields();
|
||||||
|
Object.keys(fields).forEach(fieldName => {
|
||||||
|
const field = fields[fieldName];
|
||||||
|
let matchingArgs;
|
||||||
|
|
||||||
|
if (!isMatch(fieldName, searchValue)) {
|
||||||
|
if ('args' in field && field.args.length) {
|
||||||
|
matchingArgs = field.args.filter(arg =>
|
||||||
|
isMatch(arg.name, searchValue),
|
||||||
|
);
|
||||||
|
if (matchingArgs.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = (
|
||||||
|
<div className="doc-category-item" key={typeName + '.' + fieldName}>
|
||||||
|
{withinType !== type && [
|
||||||
|
<TypeLink key="type" type={type} onClick={onClickType} />,
|
||||||
|
'.',
|
||||||
|
]}
|
||||||
|
<a
|
||||||
|
className="field-name"
|
||||||
|
onClick={event => onClickField(field, type, event)}>
|
||||||
|
{field.name}
|
||||||
|
</a>
|
||||||
|
{matchingArgs && [
|
||||||
|
'(',
|
||||||
|
<span key="args">
|
||||||
|
{matchingArgs.map(arg => (
|
||||||
|
<Argument
|
||||||
|
key={arg.name}
|
||||||
|
arg={arg}
|
||||||
|
onClickType={onClickType}
|
||||||
|
showDefaultValue={false}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</span>,
|
||||||
|
')',
|
||||||
|
]}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (withinType === type) {
|
||||||
|
matchedWithin.push(match);
|
||||||
|
} else {
|
||||||
|
matchedFields.push(match);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
matchedWithin.length + matchedTypes.length + matchedFields.length ===
|
||||||
|
0
|
||||||
|
) {
|
||||||
|
return <span className="doc-alert-text">{'No results found.'}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (withinType && matchedTypes.length + matchedFields.length > 0) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{matchedWithin}
|
||||||
|
<div className="doc-category">
|
||||||
|
<div className="doc-category-title">{'other results'}</div>
|
||||||
|
{matchedTypes}
|
||||||
|
{matchedFields}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="doc-search-items">
|
||||||
|
{matchedWithin}
|
||||||
|
{matchedTypes}
|
||||||
|
{matchedFields}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMatch(sourceText: string, searchValue: string) {
|
||||||
|
try {
|
||||||
|
const escaped = searchValue.replace(/[^_0-9A-Za-z]/g, ch => '\\' + ch);
|
||||||
|
return sourceText.search(new RegExp(escaped, 'i')) !== -1;
|
||||||
|
} catch (e) {
|
||||||
|
return sourceText.toLowerCase().indexOf(searchValue.toLowerCase()) !== -1;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,260 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 GraphQL Contributors.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import {
|
||||||
|
GraphQLSchema,
|
||||||
|
GraphQLObjectType,
|
||||||
|
GraphQLInterfaceType,
|
||||||
|
GraphQLUnionType,
|
||||||
|
GraphQLEnumType,
|
||||||
|
GraphQLType,
|
||||||
|
GraphQLEnumValue,
|
||||||
|
} from 'graphql';
|
||||||
|
|
||||||
|
import Argument from './Argument';
|
||||||
|
import MarkdownContent from './MarkdownContent';
|
||||||
|
import TypeLink from './TypeLink';
|
||||||
|
import DefaultValue from './DefaultValue';
|
||||||
|
import { FieldType, OnClickTypeFunction, OnClickFieldFunction } from './types';
|
||||||
|
|
||||||
|
type TypeDocProps = {
|
||||||
|
schema: GraphQLSchema;
|
||||||
|
type: GraphQLType;
|
||||||
|
onClickType: OnClickTypeFunction;
|
||||||
|
onClickField: OnClickFieldFunction;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TypeDocState = {
|
||||||
|
showDeprecated: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class TypeDoc extends React.Component<
|
||||||
|
TypeDocProps,
|
||||||
|
TypeDocState
|
||||||
|
> {
|
||||||
|
constructor(props: TypeDocProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = { showDeprecated: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldComponentUpdate(nextProps: TypeDocProps, nextState: TypeDocState) {
|
||||||
|
return (
|
||||||
|
this.props.type !== nextProps.type ||
|
||||||
|
this.props.schema !== nextProps.schema ||
|
||||||
|
this.state.showDeprecated !== nextState.showDeprecated
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const schema = this.props.schema;
|
||||||
|
const type = this.props.type;
|
||||||
|
const onClickType = this.props.onClickType;
|
||||||
|
const onClickField = this.props.onClickField;
|
||||||
|
|
||||||
|
let typesTitle: string | null = null;
|
||||||
|
let types: readonly (GraphQLObjectType | GraphQLInterfaceType)[] = [];
|
||||||
|
if (type instanceof GraphQLUnionType) {
|
||||||
|
typesTitle = 'possible types';
|
||||||
|
types = schema.getPossibleTypes(type);
|
||||||
|
} else if (type instanceof GraphQLInterfaceType) {
|
||||||
|
typesTitle = 'implementations';
|
||||||
|
types = schema.getPossibleTypes(type);
|
||||||
|
} else if (type instanceof GraphQLObjectType) {
|
||||||
|
typesTitle = 'implements';
|
||||||
|
types = type.getInterfaces();
|
||||||
|
}
|
||||||
|
|
||||||
|
let typesDef;
|
||||||
|
if (types && types.length > 0) {
|
||||||
|
typesDef = (
|
||||||
|
<div id="doc-types" className="doc-category">
|
||||||
|
<div className="doc-category-title">{typesTitle}</div>
|
||||||
|
{types.map(subtype => (
|
||||||
|
<div key={subtype.name} className="doc-category-item">
|
||||||
|
<TypeLink type={subtype} onClick={onClickType} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// InputObject and Object
|
||||||
|
let fieldsDef;
|
||||||
|
let deprecatedFieldsDef;
|
||||||
|
if (type && 'getFields' in type) {
|
||||||
|
const fieldMap = type.getFields();
|
||||||
|
const fields = Object.keys(fieldMap).map(name => fieldMap[name]);
|
||||||
|
fieldsDef = (
|
||||||
|
<div id="doc-fields" className="doc-category">
|
||||||
|
<div className="doc-category-title">{'fields'}</div>
|
||||||
|
{fields
|
||||||
|
.filter(field => !field.deprecationReason)
|
||||||
|
.map(field => (
|
||||||
|
<Field
|
||||||
|
key={field.name}
|
||||||
|
type={type}
|
||||||
|
field={field}
|
||||||
|
onClickType={onClickType}
|
||||||
|
onClickField={onClickField}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const deprecatedFields = fields.filter(field =>
|
||||||
|
Boolean(field.deprecationReason),
|
||||||
|
);
|
||||||
|
if (deprecatedFields.length > 0) {
|
||||||
|
deprecatedFieldsDef = (
|
||||||
|
<div id="doc-deprecated-fields" className="doc-category">
|
||||||
|
<div className="doc-category-title">{'deprecated fields'}</div>
|
||||||
|
{!this.state.showDeprecated ? (
|
||||||
|
<button className="show-btn" onClick={this.handleShowDeprecated}>
|
||||||
|
{'Show deprecated fields...'}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
deprecatedFields.map(field => (
|
||||||
|
<Field
|
||||||
|
key={field.name}
|
||||||
|
type={type}
|
||||||
|
field={field}
|
||||||
|
onClickType={onClickType}
|
||||||
|
onClickField={onClickField}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let valuesDef: ReactNode;
|
||||||
|
let deprecatedValuesDef: ReactNode;
|
||||||
|
if (type instanceof GraphQLEnumType) {
|
||||||
|
const values = type.getValues();
|
||||||
|
valuesDef = (
|
||||||
|
<div className="doc-category">
|
||||||
|
<div className="doc-category-title">{'values'}</div>
|
||||||
|
{values
|
||||||
|
.filter(value => Boolean(!value.deprecationReason))
|
||||||
|
.map(value => (
|
||||||
|
<EnumValue key={value.name} value={value} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const deprecatedValues = values.filter(value =>
|
||||||
|
Boolean(value.deprecationReason),
|
||||||
|
);
|
||||||
|
if (deprecatedValues.length > 0) {
|
||||||
|
deprecatedValuesDef = (
|
||||||
|
<div className="doc-category">
|
||||||
|
<div className="doc-category-title">{'deprecated values'}</div>
|
||||||
|
{!this.state.showDeprecated ? (
|
||||||
|
<button className="show-btn" onClick={this.handleShowDeprecated}>
|
||||||
|
{'Show deprecated values...'}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
deprecatedValues.map(value => (
|
||||||
|
<EnumValue key={value.name} value={value} />
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<MarkdownContent
|
||||||
|
className="doc-type-description"
|
||||||
|
markdown={
|
||||||
|
('description' in type && type.description) || 'No Description'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{type instanceof GraphQLObjectType && typesDef}
|
||||||
|
{fieldsDef}
|
||||||
|
{deprecatedFieldsDef}
|
||||||
|
{valuesDef}
|
||||||
|
{deprecatedValuesDef}
|
||||||
|
{!(type instanceof GraphQLObjectType) && typesDef}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleShowDeprecated = () => this.setState({ showDeprecated: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
type FieldProps = {
|
||||||
|
type: GraphQLType;
|
||||||
|
field: FieldType;
|
||||||
|
onClickType: OnClickTypeFunction;
|
||||||
|
onClickField: OnClickFieldFunction;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Field({ type, field, onClickType, onClickField }: FieldProps) {
|
||||||
|
return (
|
||||||
|
<div className="doc-category-item">
|
||||||
|
<a
|
||||||
|
className="field-name"
|
||||||
|
onClick={event => onClickField(field, type, event)}>
|
||||||
|
{field.name}
|
||||||
|
</a>
|
||||||
|
{'args' in field &&
|
||||||
|
field.args &&
|
||||||
|
field.args.length > 0 && [
|
||||||
|
'(',
|
||||||
|
<span key="args">
|
||||||
|
{field.args
|
||||||
|
.filter(arg => !arg.deprecationReason)
|
||||||
|
.map(arg => (
|
||||||
|
<Argument key={arg.name} arg={arg} onClickType={onClickType} />
|
||||||
|
))}
|
||||||
|
</span>,
|
||||||
|
')',
|
||||||
|
]}
|
||||||
|
{': '}
|
||||||
|
<TypeLink type={field.type} onClick={onClickType} />
|
||||||
|
<DefaultValue field={field} />
|
||||||
|
{field.description && (
|
||||||
|
<MarkdownContent
|
||||||
|
className="field-short-description"
|
||||||
|
markdown={field.description}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{'deprecationReason' in field && field.deprecationReason && (
|
||||||
|
<MarkdownContent
|
||||||
|
className="doc-deprecation"
|
||||||
|
markdown={field.deprecationReason}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type EnumValue = {
|
||||||
|
value: GraphQLEnumValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
function EnumValue({ value }: EnumValue) {
|
||||||
|
return (
|
||||||
|
<div className="doc-category-item">
|
||||||
|
<div className="enum-value">{value.name}</div>
|
||||||
|
<MarkdownContent
|
||||||
|
className="doc-value-description"
|
||||||
|
markdown={value.description}
|
||||||
|
/>
|
||||||
|
{value.deprecationReason && (
|
||||||
|
<MarkdownContent
|
||||||
|
className="doc-deprecation"
|
||||||
|
markdown={value.deprecationReason}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 GraphQL Contributors.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
GraphQLList,
|
||||||
|
GraphQLNonNull,
|
||||||
|
GraphQLType,
|
||||||
|
GraphQLNamedType,
|
||||||
|
} from 'graphql';
|
||||||
|
import { OnClickTypeFunction } from './types';
|
||||||
|
|
||||||
|
type Maybe<T> = T | null | undefined;
|
||||||
|
|
||||||
|
type TypeLinkProps = {
|
||||||
|
type?: Maybe<GraphQLType>;
|
||||||
|
onClick?: OnClickTypeFunction;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function TypeLink(props: TypeLinkProps) {
|
||||||
|
const onClick = props.onClick ? props.onClick : () => null;
|
||||||
|
return renderType(props.type, onClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderType(type: Maybe<GraphQLType>, onClick: OnClickTypeFunction) {
|
||||||
|
if (type instanceof GraphQLNonNull) {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{renderType(type.ofType, onClick)}
|
||||||
|
{'!'}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type instanceof GraphQLList) {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{'['}
|
||||||
|
{renderType(type.ofType, onClick)}
|
||||||
|
{']'}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
className="type-name"
|
||||||
|
onClick={event => {
|
||||||
|
event.preventDefault();
|
||||||
|
onClick(type as GraphQLNamedType, event);
|
||||||
|
}}
|
||||||
|
href="#">
|
||||||
|
{type?.name}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
import { MouseEvent } from 'react';
|
||||||
|
import {
|
||||||
|
GraphQLField,
|
||||||
|
GraphQLInputField,
|
||||||
|
GraphQLArgument,
|
||||||
|
GraphQLObjectType,
|
||||||
|
GraphQLInterfaceType,
|
||||||
|
GraphQLInputObjectType,
|
||||||
|
GraphQLType,
|
||||||
|
GraphQLNamedType,
|
||||||
|
} from 'graphql';
|
||||||
|
|
||||||
|
export type FieldType =
|
||||||
|
| GraphQLField<{}, {}, {}>
|
||||||
|
| GraphQLInputField
|
||||||
|
| GraphQLArgument;
|
||||||
|
|
||||||
|
export type OnClickFieldFunction = (
|
||||||
|
field: FieldType,
|
||||||
|
type?:
|
||||||
|
| GraphQLObjectType
|
||||||
|
| GraphQLInterfaceType
|
||||||
|
| GraphQLInputObjectType
|
||||||
|
| GraphQLType,
|
||||||
|
event?: MouseEvent,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
export type OnClickTypeFunction = (
|
||||||
|
type: GraphQLNamedType,
|
||||||
|
event?: MouseEvent<HTMLAnchorElement>,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
export type OnClickFieldOrTypeFunction =
|
||||||
|
| OnClickFieldFunction
|
||||||
|
| OnClickTypeFunction;
|
298
packages/bruno-graphql-docs/src/index.css
Normal file
298
packages/bruno-graphql-docs/src/index.css
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
.graphql-docs-container .doc-explorer {
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .doc-explorer-title-bar,
|
||||||
|
.graphql-docs-container .history-title-bar {
|
||||||
|
cursor: default;
|
||||||
|
display: flex;
|
||||||
|
height: 34px;
|
||||||
|
line-height: 14px;
|
||||||
|
padding: 8px 8px 5px;
|
||||||
|
position: relative;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .doc-explorer-title,
|
||||||
|
.graphql-docs-container .history-title {
|
||||||
|
flex: 1;
|
||||||
|
font-weight: bold;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding: 10px 0 10px 10px;
|
||||||
|
text-align: center;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
user-select: text;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .doc-explorer-back {
|
||||||
|
color: #3B5998;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: -7px 0 -6px -8px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding: 17px 12px 16px 16px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
background: 0;
|
||||||
|
border: 0;
|
||||||
|
line-height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-explorer-narrow .doc-explorer-back {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .doc-explorer-back:before {
|
||||||
|
border-left: 2px solid #3B5998;
|
||||||
|
border-top: 2px solid #3B5998;
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
height: 9px;
|
||||||
|
margin: 0 3px -1px 0;
|
||||||
|
position: relative;
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
width: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .doc-explorer-rhs {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .doc-explorer-contents,
|
||||||
|
.graphql-docs-container .history-contents {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-top: 1px solid #d6d6d6;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 20px 15px;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 47px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .doc-explorer-contents {
|
||||||
|
min-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .doc-type-description p:first-child ,
|
||||||
|
.graphql-docs-container .doc-type-description blockquote:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .doc-explorer-contents a {
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .doc-explorer-contents a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .doc-value-description > :first-child {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .doc-value-description > :last-child {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .doc-type-description code,
|
||||||
|
.graphql-docs-container .doc-type-description pre,
|
||||||
|
.graphql-docs-container .doc-category code,
|
||||||
|
.graphql-docs-container .doc-category pre {
|
||||||
|
--saf-0: rgba(var(--sk_foreground_low,29,28,29),0.13);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.50001;
|
||||||
|
font-variant-ligatures: none;
|
||||||
|
white-space: pre;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: normal;
|
||||||
|
-webkit-tab-size: 4;
|
||||||
|
-moz-tab-size: 4;
|
||||||
|
tab-size: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .doc-type-description code,
|
||||||
|
.graphql-docs-container .doc-category code {
|
||||||
|
padding: 2px 3px 1px;
|
||||||
|
border: 1px solid var(--saf-0);
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: rgba(var(--sk_foreground_min,29,28,29),.04);
|
||||||
|
color: #e01e5a;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .doc-category {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .doc-category-title {
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
color: #777;
|
||||||
|
cursor: default;
|
||||||
|
font-size: 14px;
|
||||||
|
font-variant: small-caps;
|
||||||
|
font-weight: bold;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
margin: 0 -15px 10px 0;
|
||||||
|
padding: 10px 0;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .doc-category-item {
|
||||||
|
margin: 12px 0;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .keyword {
|
||||||
|
color: #B11A04;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .type-name {
|
||||||
|
color: #CA9800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .field-name {
|
||||||
|
color: #1F61A0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .field-short-description {
|
||||||
|
color: #666;
|
||||||
|
margin-left: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .enum-value {
|
||||||
|
color: #0B7FC7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .arg-name {
|
||||||
|
color: #8B2BB9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .arg {
|
||||||
|
display: block;
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .arg:first-child:last-child,
|
||||||
|
.graphql-docs-container .arg:first-child:nth-last-child(2),
|
||||||
|
.graphql-docs-container .arg:first-child:nth-last-child(2) ~ .arg {
|
||||||
|
display: inherit;
|
||||||
|
margin: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .arg:first-child:nth-last-child(2):after {
|
||||||
|
content: ', ';
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .arg-default-value {
|
||||||
|
color: #43A047;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .doc-deprecation {
|
||||||
|
background: #fffae8;
|
||||||
|
box-shadow: inset 0 0 1px #bfb063;
|
||||||
|
color: #867F70;
|
||||||
|
line-height: 16px;
|
||||||
|
margin: 8px -8px;
|
||||||
|
max-height: 80px;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .doc-deprecation:before {
|
||||||
|
content: 'Deprecated:';
|
||||||
|
color: #c79b2e;
|
||||||
|
cursor: default;
|
||||||
|
display: block;
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: bold;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
line-height: 1;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .doc-deprecation > :first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .doc-deprecation > :last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .show-btn {
|
||||||
|
-webkit-appearance: initial;
|
||||||
|
display: block;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: solid 1px #ccc;
|
||||||
|
text-align: center;
|
||||||
|
padding: 8px 12px 10px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #fbfcfc;
|
||||||
|
color: #555;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .search-box {
|
||||||
|
border-bottom: 1px solid #d3d6db;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: -15px -15px 12px 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .search-box-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
display: block;
|
||||||
|
font-size: 24px;
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .search-box .search-box-clear {
|
||||||
|
background-color: #d0d0d0;
|
||||||
|
border-radius: 12px;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 1px 5px 2px;
|
||||||
|
position: absolute;
|
||||||
|
right: 3px;
|
||||||
|
user-select: none;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .search-box .search-box-clear:hover {
|
||||||
|
background-color: #b9b9b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .search-box > input {
|
||||||
|
border: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
padding: 6px 24px 8px 20px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graphql-docs-container .error-container {
|
||||||
|
font-weight: bold;
|
||||||
|
left: 0;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
opacity: 0.5;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: uppercase;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(0, -50%);
|
||||||
|
}
|
@ -1,5 +1,8 @@
|
|||||||
import GraphDocs from "./GraphDocs";
|
import { DocExplorer } from "./components/DocExplorer";
|
||||||
|
|
||||||
|
// Todo: Rollup throws error
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
GraphDocs
|
DocExplorer
|
||||||
}
|
}
|
26
packages/bruno-graphql-docs/src/utility/debounce.ts
Normal file
26
packages/bruno-graphql-docs/src/utility/debounce.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 GraphQL Contributors.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provided a duration and a function, returns a new function which is called
|
||||||
|
* `duration` milliseconds after the last call.
|
||||||
|
*/
|
||||||
|
export default function debounce<F extends (...args: any[]) => any>(
|
||||||
|
duration: number,
|
||||||
|
fn: F,
|
||||||
|
) {
|
||||||
|
let timeout: number | null;
|
||||||
|
return function (this: any, ...args: Parameters<F>) {
|
||||||
|
if (timeout) {
|
||||||
|
window.clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
timeout = window.setTimeout(() => {
|
||||||
|
timeout = null;
|
||||||
|
fn.apply(this, args);
|
||||||
|
}, duration);
|
||||||
|
};
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user