Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add @graphiql/plugin-batch-request #2994

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"npm": "please_use_yarn_instead"
},
"scripts": {
"build": "yarn build:clean && yarn build:cm6-graphql && yarn build:packages && yarn build:graphiql-react && yarn build:graphiql-plugin-explorer && yarn build:graphiql-plugin-code-exporter && yarn build:graphiql",
"build": "yarn build:clean && yarn build:cm6-graphql && yarn build:packages && yarn build:graphiql-react && yarn build:graphiql-plugin-explorer && yarn build:graphiql-plugin-code-exporter && yarn build:graphiql-plugin-batch-request && yarn build:graphiql",
"build-bundles": "yarn prebuild-bundles && wsrun -p -m -s build-bundles",
"build-bundles-clean": "rimraf '{packages,examples,plugins}/**/{bundle,cdn,webpack}' && yarn workspace graphiql run build-bundles-clean",
"build-clean": "wsrun -m build-clean ",
Expand All @@ -40,6 +40,7 @@
"build:graphiql": "yarn tsc resources/tsconfig.graphiql.json",
"build:graphiql-plugin-explorer": "yarn workspace @graphiql/plugin-explorer run build",
"build:graphiql-plugin-code-exporter": "yarn workspace @graphiql/plugin-code-exporter run build",
"build:graphiql-plugin-batch-request": "yarn workspace @graphiql/plugin-batch-request run build",
"build:graphiql-react": "yarn workspace @graphiql/react run build",
"build:packages": "yarn tsc",
"build:watch": "yarn tsc --watch",
Expand Down
3 changes: 3 additions & 0 deletions packages/graphiql-plugin-batch-request/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": ["plugin:react/jsx-runtime"]
}
25 changes: 25 additions & 0 deletions packages/graphiql-plugin-batch-request/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
types
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
1 change: 1 addition & 0 deletions packages/graphiql-plugin-batch-request/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @graphiql/plugin-batch-request
58 changes: 58 additions & 0 deletions packages/graphiql-plugin-batch-request/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# GraphiQL Batch Request Plugin

This package provides a plugin that allows sending a batch request to a GraphQL Server into the GraphiQI UI.
samuelAndalon marked this conversation as resolved.
Show resolved Hide resolved

## Install

Use your favoriton package manager to install the package:

```sh
npm i -S @graphiql/plugin-batch-request
```

The following packages are peer dependencies, so make sure you have them installed as well:

```sh
npm i -S react react-dom graphql
```

## Usage

The plugin scope is for sending multiple GraphQL operations as an array, so GraphQL server requires to be configured to allow
arrays.
samuelAndalon marked this conversation as resolved.
Show resolved Hide resolved

```jsx
import { useBatchRequestPlugin } from '@graphiql/plugin-batch-request';
import { createGraphiQLFetcher } from '@graphiql/toolkit';
import { GraphiQL } from 'graphiql';
import { useState } from 'react';

import 'graphiql/graphiql.css';
import '@graphiql/plugin-batch-request/dist/style.css';

const url = 'https://countries.trevorblades.com/graphql';

const fetcher = createGraphiQLFetcher({
url
});

function GraphiQLWithExplorer() {
const [query, setQuery] = useState(DEFAULT_QUERY);
const batchRequestPlugin = useBatchRequestPlugin({ url });
return (
<GraphiQL
fetcher={fetcher}
query={query}
onEditQuery={setQuery}
plugins={[batchRequestPlugin]}
/>
);
}
```


### Example

Sending a batch request to spacex GraphQL server:

https://user-images.githubusercontent.com/6611331/212411159-336abe77-5f0a-4453-9de3-62abe039168f.mov
22 changes: 22 additions & 0 deletions packages/graphiql-plugin-batch-request/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title></title>
<style>
body {
height: 100%;
margin: 0;
width: 100%;
overflow: hidden;
}
#root {
height: 100vh;
}
</style>
<link rel="stylesheet" href="https://unpkg.com/graphiql/graphiql.min.css" />
</head>
<body>
<div id="root"></div>
<script type="module" src="./test-plugin.tsx"></script>
</body>
</html>
50 changes: 50 additions & 0 deletions packages/graphiql-plugin-batch-request/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"name": "@graphiql/plugin-batch-request",
"version": "0.0.1",
"repository": {
"type": "git",
"url": "https://github.com/graphql/graphiql",
"directory": "packages/graphiql-plugin-batch-request"
},
"main": "dist/graphiql-plugin-batch-request.cjs.js",
"module": "dist/graphiql-plugin-batch-request.es.js",
"types": "types/index.d.ts",
"license": "MIT",
"keywords": [
"react",
"graphql",
"graphiql",
"plugin",
"batch-request"
],
"files": [
"dist",
"src",
"types"
],
"scripts": {
"dev": "vite",
"build": "tsc --emitDeclarationOnly && node resources/copy-types.mjs && vite build",
"preview": "vite preview"
},
"dependencies": {
"@graphiql/react": "^0.15.0",
"@fortawesome/fontawesome-free": "6.2.1",
"react-checkbox-tree": "1.8.0"
},
"peerDependencies": {
"graphql": "^15.5.0 || ^16.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^1.3.0",
"typescript": "^4.6.3",
"vite": "^2.9.13",
"graphql": "^16.4.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"@graphiql/toolkit": "^0.8.0",
"graphiql": "^2.2.0"
}
}
11 changes: 11 additions & 0 deletions packages/graphiql-plugin-batch-request/resources/copy-types.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const base = path.resolve(path.dirname(__filename), '..');

fs.copyFileSync(
path.resolve(base, 'src', 'graphiql-batch-request.d.ts'),
path.resolve(base, 'types', 'graphiql-batch-request.d.ts'),
);
187 changes: 187 additions & 0 deletions packages/graphiql-plugin-batch-request/src/batch-request-plugin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { PlayIcon, Spinner, StopIcon, useEditorContext } from '@graphiql/react';
import { FetcherParams } from '@graphiql/toolkit';
import { GraphiQLBatchRequestProps, TabsWithOperations } from 'graphiql-batch-request';
import { GraphQLError, parse, print } from 'graphql';
import { Kind } from 'graphql/language';
import { useState } from 'react';
import CheckboxTree from 'react-checkbox-tree';

import "@fortawesome/fontawesome-free/css/all.css";
import 'react-checkbox-tree/lib/react-checkbox-tree.css';
import './graphiql-batch-request.d.ts';
import './index.css';

export function BatchRequestPlugin({
url,
useAllOperations = false
}: GraphiQLBatchRequestProps) {
const { tabs, responseEditor } = useEditorContext({ nonNull: true });

let parsingError = '';
let tabsWithOperations: TabsWithOperations = {};
try {
tabsWithOperations = tabs
.filter(tab => tab.query !== '' && tab.query !== null)
.map(tab => {
const document = parse(tab.query as string);
return {
id: tab.id,
document,
operations: document.definitions.filter(
definition => definition.kind === Kind.OPERATION_DEFINITION
),
variables: tab.variables && tab.variables.trim() !== ''
? JSON.parse(tab.variables) :
undefined,
headers: tab.headers && tab.headers.trim() !== ''
? JSON.parse(tab.headers) :
undefined
}
})
.reduce((acc, tabWithOperations) => {
acc[tabWithOperations.id] = tabWithOperations;
return acc;
}, {} as any);
} catch(e: unknown) {
if (e instanceof GraphQLError || e instanceof SyntaxError) {
parsingError = e.message;
}
}

const operationValues: string[] = [];
const nodes = Object.values(tabsWithOperations).map(
(tabWithOperations, i) => ({
value: tabWithOperations.id,
label: `Tab ${i + 1}`,
children: tabWithOperations.operations.map((operation, j) => {
const operationValue = operation.name?.value
? `${tabWithOperations.id}|${operation.name.value}`
: `${tabWithOperations.id}|${j}`;
operationValues.push(operationValue);

return {
value: operationValue,
label: operation.name?.value ?? 'Unnamed operation'
}
})
})
);

const [batchResponseLoading, setBatchResponseLoading] = useState(false);
const [executeButtonDisabled, setExecuteButtonDisabled] = useState(
useAllOperations === false
);
const [selectedOperations, setSelectedOperations] = useState(
useAllOperations ? operationValues : []
);
const [expandedOperations, setExpandedOperations] = useState(
Object.keys(tabsWithOperations)
);


if (parsingError !== '') {
return (
<>
<p>Error parsing queries, verify your queries syntax in the tabs:</p>
<p>{parsingError}</p>
</>
)
}

const sendBatchRequest = () => {
const operations: FetcherParams[] = [];
let headers = {};
for (const selectedOperation of selectedOperations) {
const [tabId, selectedOperationName] = selectedOperation.split('|');
const tab = tabsWithOperations[tabId]
if (tab) {
const selectedOperationDefinition = tab.operations.find(
(operation, i) =>
operation.name?.value === selectedOperationName ||
`${tab.id}|${i}` === selectedOperation
)
if (selectedOperationDefinition) {
headers = {...headers, ...tab.headers};
operations.push({
operationName: selectedOperationDefinition.name?.value,
query: print(tab.document),
variables: tab.variables
})
};
}
}

setBatchResponseLoading(true);

window.fetch(url, {
method: 'POST',
body: JSON.stringify(operations),
headers: {
'content-type': 'application/json',
...headers
}
}).then(response => response.json())
.then(json => {
setBatchResponseLoading(false);
responseEditor?.setValue(JSON.stringify(json, null, 2))
})
};

return (
<section aria-label="Batch Request" className="graphiql-batch-request">
<div className="graphiql-batch-request-header">
<div className="graphiql-batch-request-header-content">
<div className="graphiql-batch-request-title">Batch Request</div>
</div>
<div className="graphiql-batch-request-send">
<button
disabled={executeButtonDisabled}
type="button"
className='graphiql-execute-button'
aria-label={`${batchResponseLoading ? 'Stop' : 'Execute'} query (Ctrl-Enter)`}
onClick={() => {
if (!batchResponseLoading) {
sendBatchRequest();
}
}}
>
{batchResponseLoading ? <StopIcon /> : <PlayIcon />}
</button>
</div>
</div>
<div className="graphiql-batch-request-content">
<div className="graphiql-batch-request-description">
<p style={{
'display': executeButtonDisabled ? 'block' : 'none'
}}>
A batch GraphQL request requires at least 1 operation.
</p>
<p style={{
'display': executeButtonDisabled === false && selectedOperations.length > 0 ? 'block' : 'none'
}}>
You have selected {selectedOperations.length === 1 ? `${selectedOperations.length} operation.` : `${selectedOperations.length} operations.`}
</p>
</div>
<CheckboxTree
icons={{
expandClose: <i className="fa-solid fa-angle-right" />,
expandOpen: <i className="fa-solid fa-angle-down" />,
parentClose: null,
parentOpen: null,
leaf: null
}}
nodes={nodes}
checked={selectedOperations}
expanded={expandedOperations}
onCheck={checked => {
setSelectedOperations(checked);
setExecuteButtonDisabled(checked.length === 0);
}}
onExpand={setExpandedOperations}
expandOnClick
/>
{ batchResponseLoading ? <Spinner/> : null }
</div>
</section>
);
}
Loading