Skip to content

Commit

Permalink
Merge pull request #82 from wp-graphql/issue-3-auth-switch
Browse files Browse the repository at this point in the history
feat: public user query
  • Loading branch information
jasonbahl authored Mar 30, 2024
2 parents 063ee42 + a323514 commit 7aba286
Show file tree
Hide file tree
Showing 14 changed files with 6,149 additions and 3,501 deletions.
9,077 changes: 5,627 additions & 3,450 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"sort-package-json": "^2.7.0"
},
"dependencies": {
"@graphiql/plugin-explorer": "^1.0.3",
"@wordpress/components": "^27.0.0",
"@wordpress/data": "^9.22.0",
"@wordpress/element": "^5.23.0",
Expand Down
153 changes: 117 additions & 36 deletions src/components/Editor.jsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,21 @@
/* global WPGRAPHQL_IDE_DATA */
import React from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { GraphiQL } from 'graphiql';
import { useDispatch, useSelect } from '@wordpress/data';
import { parse, visit } from 'graphql';
import { explorerPlugin } from '@graphiql/plugin-explorer';

import { PrettifyButton } from './toolbarButtons/PrettifyButton';
import { CopyQueryButton } from './toolbarButtons/CopyQueryButton';
import { MergeFragmentsButton } from './toolbarButtons/MergeFragmentsButton';
import { ShareDocumentButton } from './toolbarButtons/ShareDocumentButton';
import { ToggleAuthButton } from './toolbarButtons/ToggleAuthButton';

import 'graphiql/graphiql.min.css';

const fetcher = async ( graphQLParams ) => {
const { graphqlEndpoint } = window.WPGRAPHQL_IDE_DATA;

const response = await fetch( graphqlEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify( graphQLParams ),
credentials: 'same-origin', // or 'include' if your endpoint is on a different domain
} );

return response.json();
};
import 'graphiql/graphiql.min.css';

/**
* Filter the Buttons to allow 3rd parties to add their own buttons to the GraphiQL Toolbar.
* Editor component encapsulating the GraphiQL IDE.
* Manages authentication state and integrates custom toolbar buttons.
*/
const toolbarButtons = {
copy: CopyQueryButton,
Expand All @@ -35,43 +24,135 @@ const toolbarButtons = {
share: ShareDocumentButton,
};

const explorer = explorerPlugin();

import '../../styles/explorer.css';

export function Editor() {
const query = useSelect( ( select ) => {
return select( 'wpgraphql-ide' ).getQuery();
} );
const query = useSelect( ( select ) =>
select( 'wpgraphql-ide' ).getQuery()
);
const shouldRenderStandalone = useSelect( ( select ) =>
select( 'wpgraphql-ide' ).shouldRenderStandalone()
);
const { setDrawerOpen } = useDispatch( 'wpgraphql-ide' );

const shouldRenderStandalone = useSelect( ( select ) => {
return select( 'wpgraphql-ide' ).shouldRenderStandalone();
const [ isAuthenticated, setIsAuthenticated ] = useState( () => {
const storedState = localStorage.getItem( 'graphiql:isAuthenticated' );
return storedState !== null ? storedState === 'true' : true;
} );

const { setDrawerOpen } = useDispatch( 'wpgraphql-ide' );
const [ schema, setSchema ] = useState( undefined );

useEffect( () => {
// create a ref
const ref = React.createRef();
// find the target element in the DOM
const element = document.querySelector(
'[aria-label="Re-fetch GraphQL schema"]'
);
// if the element exists
if ( element ) {
// assign the ref to the element
element.ref = ref;
// listen to click events on the element
element.addEventListener( 'click', () => {
setSchema( undefined );
} );
}
}, [ schema ] );

useEffect( () => {
localStorage.setItem(
'graphiql:isAuthenticated',
isAuthenticated.toString()
);
}, [ isAuthenticated ] );

const fetcher = useCallback(
async ( graphQLParams ) => {
let isIntrospectionQuery = false;

try {
// Parse the GraphQL query to AST only once and in a try-catch to handle potential syntax errors gracefully
const queryAST = parse( graphQLParams.query );

// Visit each node in the AST efficiently to check for introspection fields
visit( queryAST, {
Field( node ) {
if (
node.name.value === '__schema' ||
node.name.value === '__typename'
) {
isIntrospectionQuery = true;
return visit.BREAK; // Early exit if introspection query is detected
}
},
} );
} catch ( error ) {
console.error( 'Error parsing GraphQL query:', error );
}

const { graphqlEndpoint } = window.WPGRAPHQL_IDE_DATA;
const headers = {
'Content-Type': 'application/json',
};

const response = await fetch( graphqlEndpoint, {
method: 'POST',
headers,
body: JSON.stringify( graphQLParams ),
credentials: isIntrospectionQuery
? 'include'
: isAuthenticated
? 'same-origin'
: 'omit',
} );

return response.json();
},
[ isAuthenticated ]
);

const toggleAuthentication = () => setIsAuthenticated( ! isAuthenticated );

return (
<>
<GraphiQL query={ query } fetcher={ fetcher }>
<span id="wpgraphql-ide-app">
<GraphiQL
query={ query }
fetcher={ fetcher }
schema={ schema }
onSchemaChange={ ( newSchema ) => {
if ( schema !== newSchema ) {
setSchema( newSchema );
}
} }
plugins={[ explorer ]}
>
<GraphiQL.Toolbar>
{ Object.entries( toolbarButtons ).map(
( [ key, Button ] ) => (
<Button key={ key } />
)
) }
<ToggleAuthButton
isAuthenticated={ isAuthenticated }
toggleAuthentication={ toggleAuthentication }
/>
<PrettifyButton />
<CopyQueryButton />
<MergeFragmentsButton />
<ShareDocumentButton />
</GraphiQL.Toolbar>
<GraphiQL.Logo>
{ ! shouldRenderStandalone ? (
{ ! shouldRenderStandalone && (
<button
className="button EditorDrawerCloseButton"
onClick={ () => setDrawerOpen( false ) }
>
X
X{ ' ' }
<span className="screen-reader-text">
close drawer
</span>
</button>
) : (
<span />
) }
</GraphiQL.Logo>
</GraphiQL>
</>
</span>
);
}
4 changes: 2 additions & 2 deletions src/components/toolbarButtons/ShareDocumentButton.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/* global WPGRAPHQL_IDE_DATA */
import React from 'react';
import { Icon, external } from '@wordpress/icons';
import { VisuallyHidden, ToolbarButton } from '@wordpress/components';
import { useEditorContext } from '@graphiql/react';
import { VisuallyHidden } from '@wordpress/components';
import { useEditorContext, ToolbarButton } from '@graphiql/react';
import LZString from 'lz-string';
import { useCopyToClipboard } from '../../hooks/useCopyToClipboard';

Expand Down
41 changes: 41 additions & 0 deletions src/components/toolbarButtons/ToggleAuthButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react';
import clsx from 'clsx';
import { ToolbarButton } from '@graphiql/react';

import styles from '../../../styles/ToggleAuthButton.module.css';

/**
* Component to toggle the authentication state within the GraphiQL IDE.
*
* @param {Object} props Component props.
* @param {boolean} props.isAuthenticated Indicates if the current state is authenticated.
* @param {Function} props.toggleAuthentication Function to toggle the authentication state.
*/
export const ToggleAuthButton = ( {
isAuthenticated,
toggleAuthentication,
} ) => {
const avatarUrl = window.WPGRAPHQL_IDE_DATA?.context?.avatarUrl;
const title = isAuthenticated
? 'Switch to execute as a public user'
: 'Switch to execute as the logged-in user';

return (
<ToolbarButton
className={ clsx( 'graphiql-un-styled', 'graphiql-toolbar-button graphiql-auth-button', {
[ styles.authAvatarPublic ]: ! isAuthenticated,
'is-authenticated': isAuthenticated,
'is-public': ! isAuthenticated,
} ) }
onClick={ toggleAuthentication }
label={ title }
>
<span
className={ styles.authAvatar }
style={ { backgroundImage: `url(${ avatarUrl ?? '' })` } }
>
<span className={ styles.authBadge }></span>
</span>
</ToolbarButton>
);
};
15 changes: 3 additions & 12 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// WordPress dependencies for hooks, data handling, and component rendering.
import { createHooks } from '@wordpress/hooks';
import { register, dispatch } from '@wordpress/data';
import { register } from '@wordpress/data';
import { createRoot } from '@wordpress/element';
import * as GraphQL from "graphql/index.js";

// Local imports including the store configuration and the main App component.
import { store } from './store';
Expand All @@ -10,24 +11,14 @@ import { App } from './App';
// Register the store with wp.data to make it available throughout the plugin.
register( store );

/**
* Registers a plugin within the WPGraphQL IDE ecosystem.
*
* @param {string} name - The name of the plugin to register.
* @param {Object} config - The configuration object for the plugin.
*/
function registerPlugin( name, config ) {
dispatch( store ).registerPlugin( name, config );
}

// Create a central event hook system for the WPGraphQL IDE.
export const hooks = createHooks();

// Expose a global variable for the IDE, facilitating extension through external scripts.
window.WPGraphQLIDE = {
registerPlugin,
hooks,
store,
GraphQL
};

/**
Expand Down
58 changes: 58 additions & 0 deletions styles/ToggleAuthButton.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
.authAvatar {
display: inline-block;
width: 32px;
height: 32px;
border-radius: 50%;
background-size: cover;
position: relative;
}

.authAvatarPublic {
filter: grayscale(100%) !important;

.authBadge {
animation: bounceOut 0.5s forwards;
}
}

.authBadge {
width: 10px;
height: 10px;
background-color: #52c41a; /* Success color */
border-radius: 50%;
display: block;
position: absolute;
right: 0;
top: 0;
border: 2px solid white;
transition: background-color 1.5s, transform 1.5s;
animation: bounceIn 0.5s forwards;
}

@keyframes bounceIn {
0% {
transform: scale(0.2);
opacity: 0;
}
60% {
transform: scale(1.2);
opacity: 1;
}
100% {
transform: scale(1);
}
}

@keyframes bounceOut {
0% {
transform: scale(1);
}
50% {
transform: scale(1.2);
opacity: 1;
}
100% {
transform: scale(0.2);
opacity: 0;
}
}
Empty file added styles/explorer.css
Empty file.
Loading

0 comments on commit 7aba286

Please sign in to comment.