diff --git a/packages/graphql-playground-react/src/components/Playground.tsx b/packages/graphql-playground-react/src/components/Playground.tsx index 02ea09e28..9a669b941 100644 --- a/packages/graphql-playground-react/src/components/Playground.tsx +++ b/packages/graphql-playground-react/src/components/Playground.tsx @@ -27,7 +27,7 @@ import { } from '../state/sessions/actions' import { setConfigString } from '../state/general/actions' import { initState } from '../state/workspace/actions' -import { GraphQLSchema } from 'graphql' +import { GraphQLSchema, printSchema } from 'graphql' import { createStructuredSelector } from 'reselect' import { getIsConfigTab, @@ -140,7 +140,11 @@ export class Playground extends React.PureComponent { if (props.schema) { return } - if (this.mounted && this.state.schema) { + if ( + this.mounted && + this.state.schema && + !props.settings['schema.enablePolling'] + ) { this.setState({ schema: undefined }) } let first = true @@ -240,6 +244,7 @@ export class Playground extends React.PureComponent { async schemaGetter(propsInput?: Props & ReduxProps) { const props = this.props || propsInput const endpoint = props.sessionEndpoint || props.endpoint + const currentSchema = this.state.schema try { const data = { endpoint, @@ -255,11 +260,11 @@ export class Playground extends React.PureComponent { data.endpoint === this.props.endpoint || data.endpoint === this.props.sessionEndpoint ) { - this.setState({ schema: newSchema }) + this.updateSchema(currentSchema, newSchema, props) } }) if (schema) { - this.setState({ schema: schema.schema }) + this.updateSchema(currentSchema, schema.schema, props) this.props.schemaFetchingSuccess(data.endpoint, schema.tracingSupported) this.backoff.stop() } @@ -346,6 +351,22 @@ export class Playground extends React.PureComponent { ) } + private updateSchema( + currentSchema: GraphQLSchema | undefined, + newSchema: GraphQLSchema, + props: Readonly<{ children?: React.ReactNode }> & + Readonly, + ) { + const currentSchemaStr = currentSchema ? printSchema(currentSchema) : null + const newSchemaStr = printSchema(newSchema) + if ( + newSchemaStr !== currentSchemaStr || + !props.settings['schema.enablePolling'] + ) { + this.setState({ schema: newSchema }) + } + } + get httpApiPrefix() { return this.props.endpoint.match(/(https?:\/\/.*?)\/?/)![1] } diff --git a/packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLEditor.tsx b/packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLEditor.tsx index ce14c5a08..580d14e35 100644 --- a/packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLEditor.tsx +++ b/packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLEditor.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { GraphQLSchema } from 'graphql' +import { GraphQLSchema, printSchema } from 'graphql' import EditorWrapper from '../EditorWrapper' import { styled } from '../../../styled' import { getSDL } from '../util/createSDL' @@ -64,10 +64,12 @@ class SDLEditor extends React.PureComponent { this.editor.on('scroll', this.handleScroll) this.editor.refresh() } - componentDidUpdate(prevProps: Props) { const CodeMirror = require('codemirror') - if (this.props.schema !== prevProps.schema) { + const currentSchemaStr = this.props.schema && printSchema(this.props.schema) + const prevSchemaStr = prevProps.schema && printSchema(prevProps.schema) + if (currentSchemaStr !== prevSchemaStr) { + const initialScroll = this.editor.getScrollInfo() this.cachedValue = getSDL( this.props.schema, @@ -79,6 +81,9 @@ class SDLEditor extends React.PureComponent { this.props.settings['schema.disableComments'], ), ) + if (this.props.settings['schema.enablePolling']) { + this.editor.scrollTo(initialScroll.left, initialScroll.top) + } CodeMirror.signal(this.editor, 'change', this.editor) } if (this.props.width !== prevProps.width) { diff --git a/packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLHeader.tsx b/packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLHeader.tsx index d9c93d5f1..c5589276f 100644 --- a/packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLHeader.tsx +++ b/packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLHeader.tsx @@ -80,10 +80,12 @@ export default class SDLHeader extends React.Component { const SchemaHeader = styled.div` display: flex; + flex-direction: row; height: 64px; width: 100%; + margin-right: 108px; align-items: center; - justify-content: space-between; + justify-content: flex-start; outline: none; user-select: none; ` @@ -99,7 +101,6 @@ const Box = styled.div` ` const Title = styled.div` - flex: 1; color: ${p => styleHelper(p).title}; cursor: default; font-size: 14px; @@ -109,6 +110,7 @@ const Title = styled.div` letter-spacing: 1px; user-select: none !important; padding: 16px; + padding-right: 5px; ` const Download = styled(Button)` @@ -136,6 +138,7 @@ const styleHelper = p => { if (p.theme.mode === 'dark') { return { title: 'white', + subtitle: '#8B959C', download: { text: p.open ? '#8B959C' : 'white', button: p.theme.colours.darkBlue, @@ -148,6 +151,7 @@ const styleHelper = p => { } return { title: p.theme.colours.darkBlue, + subtitle: 'rgba(61, 88, 102, 0.5)', download: { text: p.open ? 'rgba(61, 88, 102, 0.5)' : p.theme.colours.darkBlue, button: '#f6f6f6', diff --git a/packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLView.tsx b/packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLView.tsx index 7050a23f6..2d90645ce 100644 --- a/packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLView.tsx +++ b/packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLView.tsx @@ -32,6 +32,7 @@ interface DispatchFromProps { toggleDocs: (sessionId: string) => any setDocsVisible: (sessionId: string, open: boolean) => any changeWidthDocs: (sessionId: string, width: number) => any + setSchemaUpdated: () => void } class SDLView extends React.Component< diff --git a/packages/graphql-playground-react/src/components/Playground/TopBar/Polling.tsx b/packages/graphql-playground-react/src/components/Playground/TopBar/Polling.tsx new file mode 100644 index 000000000..3b725bee4 --- /dev/null +++ b/packages/graphql-playground-react/src/components/Playground/TopBar/Polling.tsx @@ -0,0 +1,42 @@ +import * as React from 'react' +import PollingIcon from './PollingIcon' + +export interface Props { + isPollingSchema: boolean + onReloadSchema: () => void +} + +class SchemaPolling extends React.Component { + timer: any + + componentDidMount() { + this.startPolling() + } + componentWillUnmount() { + this.clearTimer() + } + componentWillReceiveProps(nextProps: Props) { + if (nextProps.isPollingSchema !== this.props.isPollingSchema) { + this.startPolling(nextProps) + } + } + + render() { + return + } + private startPolling(props: Props = this.props) { + this.clearTimer() + if (props.isPollingSchema) { + this.timer = setInterval(() => props.onReloadSchema(), 2000) + } + } + + private clearTimer() { + if (this.timer) { + clearInterval(this.timer) + this.timer = null + } + } +} + +export default SchemaPolling diff --git a/packages/graphql-playground-react/src/components/Playground/TopBar/PollingIcon.tsx b/packages/graphql-playground-react/src/components/Playground/TopBar/PollingIcon.tsx new file mode 100644 index 000000000..63e03a1bd --- /dev/null +++ b/packages/graphql-playground-react/src/components/Playground/TopBar/PollingIcon.tsx @@ -0,0 +1,49 @@ +import * as React from 'react' +import { styled, keyframes, css } from '../../../styled/index' +import BasePositioner from './Positioner' + +export interface Props { + animate: boolean + disabled?: boolean + onClick?: () => void +} + +const PollingIcon: React.SFC = props => ( + + + +) + +export default PollingIcon + +const pulse = keyframes` +0% { + box-shadow: 0 0 0 0 rgba(139, 149, 156, 0.4); +} +70% { + box-shadow: 0 0 0 10px rgba(139, 149, 156, 0); +} +100% { + box-shadow: 0 0 0 0 rgba(139, 149, 156, 0); +} +` + +const Positioner = styled(BasePositioner)` + display: flex; + justify-content: center; + align-items: center; +` +const Icon = styled.div` + display: block; + width: 8px; + height: 8px; + border-radius: 50%; + background: ${p => p.theme.editorColours.pollingIcon}; + box-shadow: 0 0 0 ${p => p.theme.editorColours.pollingIconShadow}; + ${p => + p.animate + ? css` + animation: ${pulse} 2s infinite; + ` + : undefined}; +` diff --git a/packages/graphql-playground-react/src/components/Playground/TopBar/Positioner.tsx b/packages/graphql-playground-react/src/components/Playground/TopBar/Positioner.tsx new file mode 100644 index 000000000..c076666ba --- /dev/null +++ b/packages/graphql-playground-react/src/components/Playground/TopBar/Positioner.tsx @@ -0,0 +1,6 @@ +import { styled } from '../../../styled/index' + +export default styled.div` + width: 20px; + height: 20px; +` diff --git a/packages/graphql-playground-react/src/components/Playground/TopBar/Reload.tsx b/packages/graphql-playground-react/src/components/Playground/TopBar/Reload.tsx new file mode 100644 index 000000000..a57d2e163 --- /dev/null +++ b/packages/graphql-playground-react/src/components/Playground/TopBar/Reload.tsx @@ -0,0 +1,16 @@ +import * as React from 'react' +import ReloadIcon from './ReloadIcon' + +export interface Props { + isReloadingSchema: boolean + onReloadSchema?: () => void +} + +const Reload: React.SFC = props => ( + +) + +export default Reload diff --git a/packages/graphql-playground-react/src/components/Playground/TopBar/ReloadIcon.tsx b/packages/graphql-playground-react/src/components/Playground/TopBar/ReloadIcon.tsx index 7705fb34d..f3438a7ad 100644 --- a/packages/graphql-playground-react/src/components/Playground/TopBar/ReloadIcon.tsx +++ b/packages/graphql-playground-react/src/components/Playground/TopBar/ReloadIcon.tsx @@ -1,14 +1,20 @@ import * as React from 'react' -import { styled, keyframes } from '../../../styled/index' +import { styled, keyframes, css } from '../../../styled/index' +import BasePositioner from './Positioner' export interface Props { - isReloadingSchema: boolean - onReloadSchema?: () => void + animate: boolean + disabled?: boolean + onClick?: () => void } const ReloadIcon: React.SFC = props => ( - - + + = props => ( strokeWidth="1.5" fill="none" strokeLinecap="round" - isReloadingSchema={props.isReloadingSchema} + animate={props.animate} /> @@ -50,45 +56,42 @@ const refreshFrames = keyframes` // again the animation const reloadAction = props => keyframes` 0% { - transform: rotate(${props.isReloadingSchema ? 0 : 360}deg); + transform: rotate(${props.animate ? 0 : 360}deg); } 100% { - transform: rotate(${props.isReloadingSchema ? 360 : 720}deg); + transform: rotate(${props.animate ? 360 : 720}deg); }` -const Positioner = styled.div` - position: absolute; - right: 5px; - width: 20px; - height: 20px; - cursor: pointer; - transform: rotateY(180deg); -` - const Svg = styled.svg` fill: ${p => p.theme.editorColours.icon}; transition: 0.1s linear all; - - &:hover { - fill: ${p => p.theme.editorColours.iconHover}; - } + ${p => + p.disabled + ? undefined + : css` + &:hover { + fill: ${p => p.theme.editorColours.iconHover}; + } + `}; +` +const Positioner = styled(BasePositioner)` + cursor: ${({ disabled = false }) => (disabled ? 'auto' : 'pointer')}; + transform: rotateY(180deg); ` - const Circle = styled('circle')` fill: none; stroke: ${p => p.theme.editorColours.icon}; stroke-dasharray: 37.68; transition: opacity 0.3s ease-in-out; - opacity: ${p => (p.isReloadingSchema ? 1 : 0)}; + opacity: ${p => (p.animate ? 1 : 0)}; transform-origin: 9.5px 10px; - animation: ${refreshFrames} 2s linear - ${p => (p.isReloadingSchema ? 'infinite' : '')}; + animation: ${refreshFrames} 2s linear ${p => (p.animate ? 'infinite' : '')}; ` const Icon = styled('path')` transition: opacity 0.3s ease-in-out; - opacity: ${p => (p.isReloadingSchema ? 0 : 1)}; + opacity: ${p => (p.animate ? 0 : 1)}; transform-origin: 9.5px 10px; animation: ${reloadAction} 0.5s linear; ` diff --git a/packages/graphql-playground-react/src/components/Playground/TopBar/SchemaReload.tsx b/packages/graphql-playground-react/src/components/Playground/TopBar/SchemaReload.tsx new file mode 100644 index 000000000..982426a32 --- /dev/null +++ b/packages/graphql-playground-react/src/components/Playground/TopBar/SchemaReload.tsx @@ -0,0 +1,26 @@ +import * as React from 'react' +import ReloadIcon from './Reload' +import PollingIcon from './Polling' + +export interface Props { + isPollingSchema: boolean + isReloadingSchema: boolean + onReloadSchema: () => any +} + +export default (props: Props) => { + if (props.isPollingSchema) { + return ( + + ) + } + return ( + + ) +} diff --git a/packages/graphql-playground-react/src/components/Playground/TopBar/TopBar.tsx b/packages/graphql-playground-react/src/components/Playground/TopBar/TopBar.tsx index fc28d14a2..b0d2c22dd 100644 --- a/packages/graphql-playground-react/src/components/Playground/TopBar/TopBar.tsx +++ b/packages/graphql-playground-react/src/components/Playground/TopBar/TopBar.tsx @@ -3,7 +3,7 @@ import { styled } from '../../../styled/index' import * as copy from 'copy-to-clipboard' import Share from '../../Share' -import ReloadIcon from './ReloadIcon' +import SchemaReload from './SchemaReload' import { createStructuredSelector } from 'reselect' import { getEndpoint, @@ -21,6 +21,7 @@ import { } from '../../../state/sessions/actions' import { share } from '../../../state/sharing/actions' import { openHistory } from '../../../state/general/actions' +import { getSettings } from '../../../state/workspace/reducers' export interface Props { endpoint: string @@ -34,6 +35,8 @@ export interface Props { openHistory: () => void share: () => void refetchSchema: () => void + + settings } class TopBar extends React.Component { @@ -45,7 +48,7 @@ class TopBar extends React.Component { }), } render() { - const { endpointUnreachable } = this.props + const { endpointUnreachable, settings } = this.props return ( @@ -65,10 +68,21 @@ class TopBar extends React.Component { ) : ( - +
+ +
)} @@ -141,6 +155,7 @@ const mapStateToProps = createStructuredSelector({ fixedEndpoint: getFixedEndpoint, isReloadingSchema: getIsReloadingSchema, endpointUnreachable: getEndpointUnreachable, + settings: getSettings, }) export default connect( @@ -196,6 +211,7 @@ const UrlBar = styled('input')` : p.theme.editorColours.textInactive}; border: 1px solid ${p => p.theme.editorColours.background}; padding: 6px 12px; + padding-left: 30px; font-size: 13px; flex: 1; ` diff --git a/packages/graphql-playground-react/src/constants.ts b/packages/graphql-playground-react/src/constants.ts index 26b1a6db9..706cfd605 100644 --- a/packages/graphql-playground-react/src/constants.ts +++ b/packages/graphql-playground-react/src/constants.ts @@ -142,6 +142,7 @@ export function getDefaultSession(endpoint: string) { queryRunning: false, operations: List([]), isReloadingSchema: false, + isSchemaPendingUpdate: false, responseExtensions: {}, queryVariablesActive: false, endpointUnreachable: false, diff --git a/packages/graphql-playground-react/src/state/sessions/reducers.ts b/packages/graphql-playground-react/src/state/sessions/reducers.ts index 8f39d6612..adaa52fcf 100644 --- a/packages/graphql-playground-react/src/state/sessions/reducers.ts +++ b/packages/graphql-playground-react/src/state/sessions/reducers.ts @@ -82,6 +82,7 @@ export class Session extends Record(getDefaultSession('')) { currentQueryEndTime?: Date isReloadingSchema: boolean + isSchemaPendingUpdate: boolean responseExtensions: any queryVariablesActive: boolean diff --git a/packages/graphql-playground-react/src/state/sessions/selectors.ts b/packages/graphql-playground-react/src/state/sessions/selectors.ts index 23c30dd05..c92b3148d 100644 --- a/packages/graphql-playground-react/src/state/sessions/selectors.ts +++ b/packages/graphql-playground-react/src/state/sessions/selectors.ts @@ -67,6 +67,7 @@ export const getCurrentQueryStartTime = makeSessionSelector( ) export const getCurrentQueryEndTime = makeSessionSelector('currentQueryEndTime') export const getIsReloadingSchema = makeSessionSelector('isReloadingSchema') + export const getResponseExtensions = makeSessionSelector('responseExtensions') export const getQueryVariablesActive = makeSessionSelector( 'queryVariablesActive', diff --git a/packages/graphql-playground-react/src/state/workspace/reducers.ts b/packages/graphql-playground-react/src/state/workspace/reducers.ts index 58b9e0079..b16dcd18b 100644 --- a/packages/graphql-playground-react/src/state/workspace/reducers.ts +++ b/packages/graphql-playground-react/src/state/workspace/reducers.ts @@ -51,6 +51,7 @@ export const defaultSettings: ISettings = { 'request.credentials': 'omit', 'tracing.hideTracingResponse': true, 'schema.disableComments': true, + 'schema.enablePolling': true, } // tslint:disable-next-line:max-classes-per-file diff --git a/packages/graphql-playground-react/src/styled/theme.ts b/packages/graphql-playground-react/src/styled/theme.ts index aba1b68b2..8c5d74954 100644 --- a/packages/graphql-playground-react/src/styled/theme.ts +++ b/packages/graphql-playground-react/src/styled/theme.ts @@ -85,6 +85,8 @@ export interface EditorColours { executeButtonSubscriptionHover: string icon: string iconHover: string + pollingIcon: string + pollingIconShadow: string button: string buttonHover: string buttonText: string @@ -216,6 +218,8 @@ export const darkEditorColours: EditorColours = { executeButtonSubscriptionHover: '#f36c65', icon: 'rgb(74, 85, 95)', iconHover: 'rgba(255, 255, 255, 0.6)', + pollingIcon: 'rgba(139, 149, 156, 1)', + pollingIconShadow: 'rgba(139, 149, 156, 0.4)', button: '#0F202D', buttonHover: '#122535', buttonText: 'rgba(255,255,255,0.6)', @@ -270,6 +274,8 @@ export const lightEditorColours: EditorColours = { executeButtonSubscriptionHover: '#f36c65', icon: 'rgb(194, 200, 203)', iconHover: 'rgba(23, 42, 58, 0.6)', + pollingIcon: 'rgba(139, 149, 156, 1)', + pollingIconShadow: 'rgba(139, 149, 156, 0.4)', button: '#d8dbde', buttonHover: 'rgba(20, 37, 51, 0.2)', buttonText: 'rgba(23, 42, 58, 0.8)', diff --git a/packages/graphql-playground-react/src/types.ts b/packages/graphql-playground-react/src/types.ts index 2ae56f7ba..c51e88d65 100644 --- a/packages/graphql-playground-react/src/types.ts +++ b/packages/graphql-playground-react/src/types.ts @@ -27,4 +27,5 @@ export interface ISettings { ['tracing.hideTracingResponse']: boolean ['request.credentials']: 'omit' | 'include' | 'same-origin' ['schema.disableComments']: boolean + ['schema.enablePolling']: boolean }