diff --git a/.nvmrc b/.nvmrc index c9d82507f..53217dcfa 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v14.17.0 \ No newline at end of file +v14.18.0 \ No newline at end of file diff --git a/packages/bruno-app/package.json b/packages/bruno-app/package.json index 74fe4ab59..6db522baf 100644 --- a/packages/bruno-app/package.json +++ b/packages/bruno-app/package.json @@ -30,6 +30,7 @@ "idb": "^7.0.0", "immer": "^9.0.15", "lodash": "^4.17.21", + "markdown-it": "^13.0.1", "mousetrap": "^1.6.5", "nanoid": "3.3.4", "next": "12.3.1", diff --git a/packages/bruno-graphql-docs/package.json b/packages/bruno-graphql-docs/package.json index a7be20db4..2da58a083 100644 --- a/packages/bruno-graphql-docs/package.json +++ b/packages/bruno-graphql-docs/package.json @@ -10,12 +10,23 @@ "@rollup/plugin-commonjs": "^23.0.2", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-typescript": "^9.0.2", + "@types/markdown-it": "^12.2.3", "@types/react": "^18.0.25", + "graphql": "^16.6.0", + "markdown-it": "^13.0.1", + "postcss": "^8.4.18", "react": "^17.0.2", + "react-dom": "^17.0.2", "rollup": "3.2.5", "rollup-plugin-dts": "^5.0.0", "rollup-plugin-peer-deps-external": "^2.2.4", + "rollup-plugin-postcss": "^4.0.2", "rollup-plugin-terser": "^7.0.2", "typescript": "^4.8.4" + }, + "peerDependencies": { + "graphql": "^16.6.0", + "markdown-it": "^13.0.1", + "react": "^17.0.2" } } diff --git a/packages/bruno-graphql-docs/rollup.config.js b/packages/bruno-graphql-docs/rollup.config.js index e02818914..dd2424c5f 100644 --- a/packages/bruno-graphql-docs/rollup.config.js +++ b/packages/bruno-graphql-docs/rollup.config.js @@ -1,13 +1,14 @@ -import resolve from "@rollup/plugin-node-resolve"; -import commonjs from "@rollup/plugin-commonjs"; -import typescript from "@rollup/plugin-typescript"; -import dts from "rollup-plugin-dts"; -import { terser } from "rollup-plugin-terser"; -import peerDepsExternal from 'rollup-plugin-peer-deps-external'; +const { nodeResolve } = require("@rollup/plugin-node-resolve"); +const commonjs = require("@rollup/plugin-commonjs"); +const typescript = require("@rollup/plugin-typescript"); +const dts = require("rollup-plugin-dts"); +const postcss = require("rollup-plugin-postcss"); +const { terser } = require("rollup-plugin-terser"); +const peerDepsExternal = require('rollup-plugin-peer-deps-external'); const packageJson = require("./package.json"); -export default [ +module.exports = [ { input: "src/index.ts", output: [ @@ -23,12 +24,23 @@ export default [ }, ], plugins: [ + postcss({ + minimize: true, + extensions: ['.css'] + }), peerDepsExternal(), - resolve(), + nodeResolve({ + extensions: ['.css'] + }), commonjs(), 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()], } ]; \ No newline at end of file diff --git a/packages/bruno-graphql-docs/src/GraphDocs.tsx b/packages/bruno-graphql-docs/src/GraphDocs.tsx deleted file mode 100644 index d398691a9..000000000 --- a/packages/bruno-graphql-docs/src/GraphDocs.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; - -class GraphDocs extends React.Component { - render() { - return "Graphql Docs Explorer" - } -} - -export default GraphDocs; diff --git a/packages/bruno-graphql-docs/src/components/DocExplorer.tsx b/packages/bruno-graphql-docs/src/components/DocExplorer.tsx new file mode 100644 index 000000000..fcd76a10e --- /dev/null +++ b/packages/bruno-graphql-docs/src/components/DocExplorer.tsx @@ -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 = ( +
{'Error fetching schema'}
+ ); + } else if (schema === undefined) { + // Schema is undefined when it is being loaded via introspection. + content = ( +
+
+
+ ); + } else if (!schema) { + // Schema is null when it explicitly does not exist, typically due to + // an error during introspection. + content =
{'No Schema Available'}
; + } else if (navItem.search) { + content = ( + + ); + } else if (navStack.length === 1) { + content = ( + + ); + } else if (isType(navItem.def)) { + content = ( + + ); + } else { + content = ( + + ); + } + + 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 ( +
+
+
+ {prevName && ( + + )} +
+ {navItem.title || navItem.name} +
+
{this.props.children}
+
+
+ {shouldSearchBoxAppear && ( + + )} + {content} +
+
+
+ ); + } + + // 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); + }; +} diff --git a/packages/bruno-graphql-docs/src/components/DocExplorer/Argument.tsx b/packages/bruno-graphql-docs/src/components/DocExplorer/Argument.tsx new file mode 100644 index 000000000..1ea1b83ea --- /dev/null +++ b/packages/bruno-graphql-docs/src/components/DocExplorer/Argument.tsx @@ -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 ( + + {arg.name} + {': '} + + {showDefaultValue !== false && } + + ); +} diff --git a/packages/bruno-graphql-docs/src/components/DocExplorer/DefaultValue.tsx b/packages/bruno-graphql-docs/src/components/DocExplorer/DefaultValue.tsx new file mode 100644 index 000000000..574dd97b6 --- /dev/null +++ b/packages/bruno-graphql-docs/src/components/DocExplorer/DefaultValue.tsx @@ -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 ( + + {' = '} + + {printDefault(astFromValue(field.defaultValue, field.type))} + + + ); + } + + return null; +} diff --git a/packages/bruno-graphql-docs/src/components/DocExplorer/Directive.tsx b/packages/bruno-graphql-docs/src/components/DocExplorer/Directive.tsx new file mode 100644 index 000000000..ab6ed7ce5 --- /dev/null +++ b/packages/bruno-graphql-docs/src/components/DocExplorer/Directive.tsx @@ -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 ( + + {'@'} + {directive.name.value} + + ); +} diff --git a/packages/bruno-graphql-docs/src/components/DocExplorer/FieldDoc.tsx b/packages/bruno-graphql-docs/src/components/DocExplorer/FieldDoc.tsx new file mode 100644 index 000000000..c77892f07 --- /dev/null +++ b/packages/bruno-graphql-docs/src/components/DocExplorer/FieldDoc.tsx @@ -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 = ( +
+
{'arguments'}
+ {field.args + .filter(arg => !arg.deprecationReason) + .map((arg: GraphQLArgument) => ( +
+
+ +
+ + {arg && 'deprecationReason' in arg && ( + + )} +
+ ))} +
+ ); + const deprecatedArgs = field.args.filter(arg => + Boolean(arg.deprecationReason), + ); + if (deprecatedArgs.length > 0) { + deprecatedArgsDef = ( +
+
{'deprecated arguments'}
+ {!showDeprecated ? ( + + ) : ( + deprecatedArgs.map((arg, i) => ( +
+
+ +
+ + {arg && 'deprecationReason' in arg && ( + + )} +
+ )) + )} +
+ ); + } + } + + let directivesDef; + if ( + field && + field.astNode && + field.astNode.directives && + field.astNode.directives.length > 0 + ) { + directivesDef = ( +
+
{'directives'}
+ {field.astNode.directives.map((directive: DirectiveNode) => ( +
+
+ +
+
+ ))} +
+ ); + } + + return ( +
+ + {field && 'deprecationReason' in field && ( + + )} +
+
{'type'}
+ +
+ {argsDef} + {directivesDef} + {deprecatedArgsDef} +
+ ); +} diff --git a/packages/bruno-graphql-docs/src/components/DocExplorer/MarkdownContent.tsx b/packages/bruno-graphql-docs/src/components/DocExplorer/MarkdownContent.tsx new file mode 100644 index 000000000..690d2669c --- /dev/null +++ b/packages/bruno-graphql-docs/src/components/DocExplorer/MarkdownContent.tsx @@ -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 | null | undefined; + +const md = new MD({ + // render urls as links, à la github-flavored markdown + linkify: true, +}); + +type MarkdownContentProps = { + markdown?: Maybe; + className?: string; +}; + +export default function MarkdownContent({ + markdown, + className, +}: MarkdownContentProps) { + if (!markdown) { + return
; + } + + return ( +
+ ); +} diff --git a/packages/bruno-graphql-docs/src/components/DocExplorer/SchemaDoc.tsx b/packages/bruno-graphql-docs/src/components/DocExplorer/SchemaDoc.tsx new file mode 100644 index 000000000..0a873973c --- /dev/null +++ b/packages/bruno-graphql-docs/src/components/DocExplorer/SchemaDoc.tsx @@ -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 ( +
+ +
+
{'root types'}
+
+ {'query'} + {': '} + +
+ {mutationType && ( +
+ {'mutation'} + {': '} + +
+ )} + {subscriptionType && ( +
+ {'subscription'} + {': '} + +
+ )} +
+
+ ); +} diff --git a/packages/bruno-graphql-docs/src/components/DocExplorer/SearchBox.tsx b/packages/bruno-graphql-docs/src/components/DocExplorer/SearchBox.tsx new file mode 100644 index 000000000..6f45e54ea --- /dev/null +++ b/packages/bruno-graphql-docs/src/components/DocExplorer/SearchBox.tsx @@ -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 ( + + ); + } + + handleChange: ChangeEventHandler = event => { + const value = event.currentTarget.value; + this.setState({ value }); + this.debouncedOnSearch(value); + }; + + handleClear = () => { + this.setState({ value: '' }); + this.props.onSearch(''); + }; +} diff --git a/packages/bruno-graphql-docs/src/components/DocExplorer/SearchResults.tsx b/packages/bruno-graphql-docs/src/components/DocExplorer/SearchResults.tsx new file mode 100644 index 000000000..d8bd30c31 --- /dev/null +++ b/packages/bruno-graphql-docs/src/components/DocExplorer/SearchResults.tsx @@ -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( +
+ +
, + ); + } + + 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 = ( +
+ {withinType !== type && [ + , + '.', + ]} + onClickField(field, type, event)}> + {field.name} + + {matchingArgs && [ + '(', + + {matchingArgs.map(arg => ( + + ))} + , + ')', + ]} +
+ ); + + if (withinType === type) { + matchedWithin.push(match); + } else { + matchedFields.push(match); + } + }); + } + } + + if ( + matchedWithin.length + matchedTypes.length + matchedFields.length === + 0 + ) { + return {'No results found.'}; + } + + if (withinType && matchedTypes.length + matchedFields.length > 0) { + return ( +
+ {matchedWithin} +
+
{'other results'}
+ {matchedTypes} + {matchedFields} +
+
+ ); + } + + return ( +
+ {matchedWithin} + {matchedTypes} + {matchedFields} +
+ ); + } +} + +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; + } +} diff --git a/packages/bruno-graphql-docs/src/components/DocExplorer/TypeDoc.tsx b/packages/bruno-graphql-docs/src/components/DocExplorer/TypeDoc.tsx new file mode 100644 index 000000000..1f1468e75 --- /dev/null +++ b/packages/bruno-graphql-docs/src/components/DocExplorer/TypeDoc.tsx @@ -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 = ( +
+
{typesTitle}
+ {types.map(subtype => ( +
+ +
+ ))} +
+ ); + } + + // 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 = ( +
+
{'fields'}
+ {fields + .filter(field => !field.deprecationReason) + .map(field => ( + + ))} +
+ ); + + const deprecatedFields = fields.filter(field => + Boolean(field.deprecationReason), + ); + if (deprecatedFields.length > 0) { + deprecatedFieldsDef = ( +
+
{'deprecated fields'}
+ {!this.state.showDeprecated ? ( + + ) : ( + deprecatedFields.map(field => ( + + )) + )} +
+ ); + } + } + + let valuesDef: ReactNode; + let deprecatedValuesDef: ReactNode; + if (type instanceof GraphQLEnumType) { + const values = type.getValues(); + valuesDef = ( +
+
{'values'}
+ {values + .filter(value => Boolean(!value.deprecationReason)) + .map(value => ( + + ))} +
+ ); + + const deprecatedValues = values.filter(value => + Boolean(value.deprecationReason), + ); + if (deprecatedValues.length > 0) { + deprecatedValuesDef = ( +
+
{'deprecated values'}
+ {!this.state.showDeprecated ? ( + + ) : ( + deprecatedValues.map(value => ( + + )) + )} +
+ ); + } + } + + return ( +
+ + {type instanceof GraphQLObjectType && typesDef} + {fieldsDef} + {deprecatedFieldsDef} + {valuesDef} + {deprecatedValuesDef} + {!(type instanceof GraphQLObjectType) && typesDef} +
+ ); + } + + handleShowDeprecated = () => this.setState({ showDeprecated: true }); +} + +type FieldProps = { + type: GraphQLType; + field: FieldType; + onClickType: OnClickTypeFunction; + onClickField: OnClickFieldFunction; +}; + +function Field({ type, field, onClickType, onClickField }: FieldProps) { + return ( +
+ onClickField(field, type, event)}> + {field.name} + + {'args' in field && + field.args && + field.args.length > 0 && [ + '(', + + {field.args + .filter(arg => !arg.deprecationReason) + .map(arg => ( + + ))} + , + ')', + ]} + {': '} + + + {field.description && ( + + )} + {'deprecationReason' in field && field.deprecationReason && ( + + )} +
+ ); +} + +type EnumValue = { + value: GraphQLEnumValue; +}; + +function EnumValue({ value }: EnumValue) { + return ( +
+
{value.name}
+ + {value.deprecationReason && ( + + )} +
+ ); +} diff --git a/packages/bruno-graphql-docs/src/components/DocExplorer/TypeLink.tsx b/packages/bruno-graphql-docs/src/components/DocExplorer/TypeLink.tsx new file mode 100644 index 000000000..cab065f47 --- /dev/null +++ b/packages/bruno-graphql-docs/src/components/DocExplorer/TypeLink.tsx @@ -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 | null | undefined; + +type TypeLinkProps = { + type?: Maybe; + onClick?: OnClickTypeFunction; +}; + +export default function TypeLink(props: TypeLinkProps) { + const onClick = props.onClick ? props.onClick : () => null; + return renderType(props.type, onClick); +} + +function renderType(type: Maybe, onClick: OnClickTypeFunction) { + if (type instanceof GraphQLNonNull) { + return ( + + {renderType(type.ofType, onClick)} + {'!'} + + ); + } + if (type instanceof GraphQLList) { + return ( + + {'['} + {renderType(type.ofType, onClick)} + {']'} + + ); + } + return ( + { + event.preventDefault(); + onClick(type as GraphQLNamedType, event); + }} + href="#"> + {type?.name} + + ); +} diff --git a/packages/bruno-graphql-docs/src/components/DocExplorer/types.ts b/packages/bruno-graphql-docs/src/components/DocExplorer/types.ts new file mode 100644 index 000000000..3922663ff --- /dev/null +++ b/packages/bruno-graphql-docs/src/components/DocExplorer/types.ts @@ -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, +) => void; + +export type OnClickFieldOrTypeFunction = + | OnClickFieldFunction + | OnClickTypeFunction; diff --git a/packages/bruno-graphql-docs/src/index.css b/packages/bruno-graphql-docs/src/index.css new file mode 100644 index 000000000..d35627864 --- /dev/null +++ b/packages/bruno-graphql-docs/src/index.css @@ -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%); +} diff --git a/packages/bruno-graphql-docs/src/index.ts b/packages/bruno-graphql-docs/src/index.ts index 54d83c494..88cc2983e 100644 --- a/packages/bruno-graphql-docs/src/index.ts +++ b/packages/bruno-graphql-docs/src/index.ts @@ -1,5 +1,8 @@ -import GraphDocs from "./GraphDocs"; +import { DocExplorer } from "./components/DocExplorer"; + +// Todo: Rollup throws error +import './index.css'; export { - GraphDocs -} \ No newline at end of file + DocExplorer +} diff --git a/packages/bruno-graphql-docs/src/utility/debounce.ts b/packages/bruno-graphql-docs/src/utility/debounce.ts new file mode 100644 index 000000000..833b8ba15 --- /dev/null +++ b/packages/bruno-graphql-docs/src/utility/debounce.ts @@ -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 any>( + duration: number, + fn: F, +) { + let timeout: number | null; + return function (this: any, ...args: Parameters) { + if (timeout) { + window.clearTimeout(timeout); + } + timeout = window.setTimeout(() => { + timeout = null; + fn.apply(this, args); + }, duration); + }; +}