From 9a4cb0a587cae09cdfbe8781eef8ad3f92fbc3cd Mon Sep 17 00:00:00 2001 From: xavcz Date: Fri, 19 Jan 2018 14:04:00 +0100 Subject: [PATCH 1/6] feat(TopBar): animate the icon when reloading schema --- .../components/Playground/GraphQLEditor.tsx | 23 ++-- .../Playground/TopBar/ReloadIcon.tsx | 110 ++++++++++++++++++ .../components/Playground/TopBar/TopBar.tsx | 32 +---- 3 files changed, 129 insertions(+), 36 deletions(-) create mode 100644 packages/graphql-playground-react/src/components/Playground/TopBar/ReloadIcon.tsx diff --git a/packages/graphql-playground-react/src/components/Playground/GraphQLEditor.tsx b/packages/graphql-playground-react/src/components/Playground/GraphQLEditor.tsx index b8075e374..3d5396336 100644 --- a/packages/graphql-playground-react/src/components/Playground/GraphQLEditor.tsx +++ b/packages/graphql-playground-react/src/components/Playground/GraphQLEditor.tsx @@ -123,6 +123,7 @@ export interface State { tracingSupported?: boolean queryVariablesActive: boolean endpointUnreachable: boolean + isReloadingSchema: boolean } export interface SimpleProps { @@ -503,6 +504,7 @@ export class GraphQLEditor extends React.PureComponent< onClickShare={this.props.onClickShare} sharing={this.props.sharing} onReloadSchema={this.reloadSchema} + isReloadingSchema={this.state.isReloadingSchema} fixedEndpoint={this.props.fixedEndpoint} endpointUnreachable={this.state.endpointUnreachable} /> @@ -757,14 +759,19 @@ export class GraphQLEditor extends React.PureComponent< // Private methods public reloadSchema = async () => { - const result = await this.props.schemaFetcher.refetch( - this.props.session.endpoint || this.props.endpoint, - this.convertHeaders(this.props.session.headers), - ) - if (result) { - const { schema } = result - this.setState({ schema }) - this.renewStacks(schema) + try { + this.setState({ isReloadingSchema: true }) + const result = await this.props.schemaFetcher.refetch( + this.props.session.endpoint || this.props.endpoint, + this.convertHeaders(this.props.session.headers), + ) + if (result) { + const { schema } = result + this.setState({ schema, isReloadingSchema: false }) + this.renewStacks(schema) + } + } catch (e) { + this.setState({ isReloadingSchema: false }) } } diff --git a/packages/graphql-playground-react/src/components/Playground/TopBar/ReloadIcon.tsx b/packages/graphql-playground-react/src/components/Playground/TopBar/ReloadIcon.tsx new file mode 100644 index 000000000..2cf66ca0f --- /dev/null +++ b/packages/graphql-playground-react/src/components/Playground/TopBar/ReloadIcon.tsx @@ -0,0 +1,110 @@ +import * as React from 'react' +import { styled, keyframes } from '../../../styled/index' +import * as theme from 'styled-theming' +import { StyledComponentClass } from 'styled-components' + +export interface Props { + isReloadingSchema: boolean + onReloadSchema?: () => void +} + +export default class ReloadIcon extends React.Component { + render() { + return ( + + + + + + + ) + } +} + +const iconColor = theme('mode', { + light: p => p.theme.colours.darkBlue20, + dark: p => p.theme.colours.white20, +}) + +const iconColorHover = theme('mode', { + light: p => p.theme.colours.darkBlue60, + dark: p => p.theme.colours.white60, +}) + +const refreshFrames = keyframes` +0% { + transform: rotate(0deg); + stroke-dashoffset: 7.92; +} + +50% { + transform: rotate(720deg); + stroke-dashoffset: 37.68; +} + +100% { + transform: rotate(1080deg); + stroke-dashoffset: 7.92; +} +` + +// same result for these 2 keyframes, however when the props change +// it makes the element animated with these keyframes to trigger +// again the animation +const reloadAction = props => keyframes` +0% { + transform: rotate(${props.isReloadingSchema ? 0 : 360}deg); +} + +100% { + transform: rotate(${props.isReloadingSchema ? 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: ${iconColor}; + transition: 0.1s linear all; + + &:hover { + fill: ${iconColorHover}; + } +` + +const showWhenReloading = bool => props => + props.isReloadingSchema ? Number(bool) : Number(!bool) + +const Circle: StyledComponentClass = styled.circle` + fill: none; + stroke: ${iconColor}; + stroke-dasharray: 37.68; + transition: opacity 0.3s ease-in-out; + opacity: ${showWhenReloading(true)}; + transform-origin: 9.5px 10px; + animation: ${refreshFrames} 2s linear infinite; +` + +const Icon: StyledComponentClass = styled.path` + transition: opacity 0.3s ease-in-out; + opacity: ${showWhenReloading(false)}; + transform-origin: 9.5px 10px; + animation: ${reloadAction} 0.5s linear; +` 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 cc43adf5b..1e0b9586e 100644 --- a/packages/graphql-playground-react/src/components/Playground/TopBar/TopBar.tsx +++ b/packages/graphql-playground-react/src/components/Playground/TopBar/TopBar.tsx @@ -4,8 +4,8 @@ import * as theme from 'styled-theming' import { darken, lighten } from 'polished' import * as CopyToClipboard from 'react-copy-to-clipboard' import Share, { SharingProps } from '../../Share' +import ReloadIcon from './ReloadIcon' import { StyledComponentClass } from 'styled-components' -import { Icon } from 'graphcool-styles' import * as cx from 'classnames' export interface Props { @@ -17,6 +17,7 @@ export interface Props { curl: string onClickShare?: () => void onReloadSchema?: () => void + isReloadingSchema: boolean sharing?: SharingProps fixedEndpoint?: boolean endpointUnreachable: boolean @@ -45,10 +46,8 @@ export default class TopBar extends React.Component { ) : ( )} @@ -100,16 +99,6 @@ const fontColor = theme('mode', { dark: p => p.theme.colours.white60, }) -const iconColor = theme('mode', { - light: p => p.theme.colours.darkBlue20, - dark: p => p.theme.colours.white20, -}) - -const iconColorHover = theme('mode', { - light: p => p.theme.colours.darkBlue60, - dark: p => p.theme.colours.white60, -}) - const barBorder = theme('mode', { light: p => p.theme.colours.darkBlue20, dark: p => '#09141c', @@ -172,19 +161,6 @@ const ReachError = styled.div` color: #f25c54; ` -const ReloadIcon = styled(Icon)` - position: absolute; - right: 5px; - cursor: pointer; - svg { - fill: ${iconColor}; - transition: 0.1s linear all; - &:hover { - fill: ${iconColorHover}; - } - } -` as any // TODO remove this once typings are fixed - const Spinner = styled.div` & { width: 40px; From 87325f6575431c8c7df51eed50c95e69eec34737 Mon Sep 17 00:00:00 2001 From: xavcz Date: Sat, 20 Jan 2018 16:35:04 +0100 Subject: [PATCH 2/6] refactor(TopBar): use styled/withProps to type styled SVG components --- .../components/Playground/TopBar/ReloadIcon.tsx | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) 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 2cf66ca0f..24f6a9d3d 100644 --- a/packages/graphql-playground-react/src/components/Playground/TopBar/ReloadIcon.tsx +++ b/packages/graphql-playground-react/src/components/Playground/TopBar/ReloadIcon.tsx @@ -1,7 +1,6 @@ import * as React from 'react' -import { styled, keyframes } from '../../../styled/index' +import { styled, keyframes, withProps } from '../../../styled/index' import * as theme from 'styled-theming' -import { StyledComponentClass } from 'styled-components' export interface Props { isReloadingSchema: boolean @@ -89,22 +88,19 @@ const Svg = styled.svg` } ` -const showWhenReloading = bool => props => - props.isReloadingSchema ? Number(bool) : Number(!bool) - -const Circle: StyledComponentClass = styled.circle` +const Circle = withProps()(styled.circle) ` fill: none; stroke: ${iconColor}; stroke-dasharray: 37.68; transition: opacity 0.3s ease-in-out; - opacity: ${showWhenReloading(true)}; + opacity: ${p => p.isReloadingSchema ? 1 : 0}; transform-origin: 9.5px 10px; animation: ${refreshFrames} 2s linear infinite; ` -const Icon: StyledComponentClass = styled.path` +const Icon = withProps()(styled.path) ` transition: opacity 0.3s ease-in-out; - opacity: ${showWhenReloading(false)}; + opacity: ${p => p.isReloadingSchema ? 0 : 1}; transform-origin: 9.5px 10px; animation: ${reloadAction} 0.5s linear; ` From b59c6e65725540ef19ded88e677070ab0f8db945 Mon Sep 17 00:00:00 2001 From: xavcz Date: Sat, 20 Jan 2018 16:38:08 +0100 Subject: [PATCH 3/6] fix(GraphQLEditor): init state.isReloadingSchema to false --- .../components/Playground/GraphQLEditor.tsx | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/packages/graphql-playground-react/src/components/Playground/GraphQLEditor.tsx b/packages/graphql-playground-react/src/components/Playground/GraphQLEditor.tsx index 3d5396336..c9ea7b043 100644 --- a/packages/graphql-playground-react/src/components/Playground/GraphQLEditor.tsx +++ b/packages/graphql-playground-react/src/components/Playground/GraphQLEditor.tsx @@ -139,7 +139,7 @@ export interface ToolbarButtonProps extends SimpleProps { export class GraphQLEditor extends React.PureComponent< Props & LocalThemeInterface & ReduxProps, State -> { + > { static Logo: (props: SimpleProps) => JSX.Element static Toolbar: (props: SimpleProps) => JSX.Element static Footer: (props: SimpleProps) => JSX.Element @@ -191,10 +191,10 @@ export class GraphQLEditor extends React.PureComponent< props.storage || typeof window !== 'undefined' ? window.localStorage : { - setItem: () => null, - removeItem: () => null, - getItem: () => null, - } + setItem: () => null, + removeItem: () => null, + getItem: () => null, + } // Determine the initial query to display. const query = @@ -218,10 +218,10 @@ export class GraphQLEditor extends React.PureComponent< props.operationName !== undefined ? props.operationName : getSelectedOperationName( - null, - this.storageGet('operationName'), - queryFacts && queryFacts.operations, - ) + null, + this.storageGet('operationName'), + queryFacts && queryFacts.operations, + ) let queryVariablesActive = this.storageGet('queryVariablesActive') queryVariablesActive = @@ -254,6 +254,7 @@ export class GraphQLEditor extends React.PureComponent< selectedVariableNames: [], queryVariablesActive, endpointUnreachable: false, + isReloadingSchema: false, ...queryFacts, } @@ -273,7 +274,7 @@ export class GraphQLEditor extends React.PureComponent< // Utility for keeping CodeMirror correctly sized. this.codeMirrorSizer = new CodeMirrorSizer() - ;(global as any).g = this + ; (global as any).g = this } componentWillReceiveProps(nextProps) { @@ -566,13 +567,13 @@ export class GraphQLEditor extends React.PureComponent< onRunQuery={this.handleEditorRunQuery} /> ) : ( - - )} + + )} @@ -669,7 +670,7 @@ export class GraphQLEditor extends React.PureComponent< .join(' ') return `curl '${ this.props.session.endpoint - }' ${headersString} --data-binary '${data}' --compressed` + }' ${headersString} --data-binary '${data}' --compressed` } setQueryVariablesRef = ref => { @@ -806,9 +807,9 @@ export class GraphQLEditor extends React.PureComponent< this.props.schemaFetcher .fetch( - this.props.session.endpoint || this.props.endpoint, - this.convertHeaders(this.props.session.headers), - ) + this.props.session.endpoint || this.props.endpoint, + this.convertHeaders(this.props.session.headers), + ) .then(result => { if (result) { const { schema, tracingSupported } = result @@ -1297,11 +1298,11 @@ const DragBar = styled.div` cursor: col-resize; ` -const QueryDragBar = styled(DragBar)` +const QueryDragBar = styled(DragBar) ` right: 0px; ` -const ResultDragBar = styled(DragBar)` +const ResultDragBar = styled(DragBar) ` left: 0px; z-index: 1; ` From 4df1da30e6cbbc23510fbb6c36928e9e28cf3f08 Mon Sep 17 00:00:00 2001 From: xavcz Date: Sat, 20 Jan 2018 16:47:10 +0100 Subject: [PATCH 4/6] feat(TopBar): notify the user when reloading the schema fails It depends on state.unreachableEndpoint, also used when sending the introspection query --- .../src/components/Playground/GraphQLEditor.tsx | 11 +++++++++-- .../src/components/Playground/TopBar/TopBar.tsx | 10 +++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/graphql-playground-react/src/components/Playground/GraphQLEditor.tsx b/packages/graphql-playground-react/src/components/Playground/GraphQLEditor.tsx index c9ea7b043..c147fbdb3 100644 --- a/packages/graphql-playground-react/src/components/Playground/GraphQLEditor.tsx +++ b/packages/graphql-playground-react/src/components/Playground/GraphQLEditor.tsx @@ -768,11 +768,18 @@ export class GraphQLEditor extends React.PureComponent< ) if (result) { const { schema } = result - this.setState({ schema, isReloadingSchema: false }) + this.setState({ + schema, + isReloadingSchema: false, + endpointUnreachable: false, + }) this.renewStacks(schema) } } catch (e) { - this.setState({ isReloadingSchema: false }) + this.setState({ + isReloadingSchema: false, + endpointUnreachable: true, + }) } } 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 1e0b9586e..68a5fd7f7 100644 --- a/packages/graphql-playground-react/src/components/Playground/TopBar/TopBar.tsx +++ b/packages/graphql-playground-react/src/components/Playground/TopBar/TopBar.tsx @@ -45,11 +45,11 @@ export default class TopBar extends React.Component { ) : ( - - )} + + )} From 19b90a4e2d42b9c4448bc6e7fe8a2e3a89f40fa7 Mon Sep 17 00:00:00 2001 From: xavcz Date: Sat, 20 Jan 2018 17:20:01 +0100 Subject: [PATCH 5/6] =?UTF-8?q?style(TopBar):=20fade=20in/out=20the=20erro?= =?UTF-8?q?r=20message=20if=20the=20endpoint=20is=20unreachable=20+=20use?= =?UTF-8?q?=20sc/keyframes=20for=20the=20pulsing=20component:=20styled-com?= =?UTF-8?q?ponents=20auto-prefix=20animations=20to=20be=20cross-browser=20?= =?UTF-8?q?=F0=9F=98=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/Playground/TopBar/TopBar.tsx | 70 ++++++++----------- 1 file changed, 29 insertions(+), 41 deletions(-) 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 68a5fd7f7..bf9be1864 100644 --- a/packages/graphql-playground-react/src/components/Playground/TopBar/TopBar.tsx +++ b/packages/graphql-playground-react/src/components/Playground/TopBar/TopBar.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { styled } from '../../../styled/index' +import { styled, keyframes, withProps } from '../../../styled' import * as theme from 'styled-theming' import { darken, lighten } from 'polished' import * as CopyToClipboard from 'react-copy-to-clipboard' @@ -23,6 +23,10 @@ export interface Props { endpointUnreachable: boolean } +interface ErrorMessageProps { + visibleIf: boolean +} + export default class TopBar extends React.Component { render() { const { endpointUnreachable } = this.props @@ -39,17 +43,14 @@ export default class TopBar extends React.Component { disabled={this.props.fixedEndpoint} className={cx({ active: !this.props.fixedEndpoint })} /> - {endpointUnreachable ? ( - - Server cannot be reached - - - ) : ( - - )} + + Server cannot be reached + + + @@ -153,46 +154,33 @@ const UrlBarWrapper = styled.div` align-items: center; ` -const ReachError = styled.div` +const ReachError = withProps()(styled.div) ` + opacity: ${p => p.visibleIf ? 1 : 0}; + transition: opacity 0.5s ease-out; position: absolute; - right: 5px; + right: -5px; display: flex; align-items: center; + justify-content: flex-end; color: #f25c54; + user-select: none; ` -const Spinner = styled.div` - & { - width: 40px; - height: 40px; - margin: 40px auto; - background-color: #333; - border-radius: 100%; - -webkit-animation: sk-pulseScaleOut 1s infinite ease-in-out; - animation: sk-pulseScaleOut 1s infinite ease-in-out; - } - - @-webkit-keyframes sk-pulseScaleOut { - 0% { - -webkit-transform: scale(0); - transform: scale(0); - } - 100% { - -webkit-transform: scale(1); - transform: scale(1); - opacity: 0; - } - } - - @keyframes sk-pulseScaleOut { +const pulseScaleOut = keyframes` 0% { - -webkit-transform: scale(0); transform: scale(0); } 100% { - -webkit-transform: scale(1); transform: scale(1); opacity: 0; } - } ` + +const Spinner = styled.div` + width: 40px; + height: 40px; + margin: 40px auto; + background-color: #333; + border-radius: 100%; + animation: ${pulseScaleOut} 1s infinite ease-in-out; +` \ No newline at end of file From 000946fd2f334e5c46f1157731c5073010043281 Mon Sep 17 00:00:00 2001 From: xavcz Date: Sat, 20 Jan 2018 17:33:37 +0100 Subject: [PATCH 6/6] Revert "style(TopBar): fade in/out the error message if the endpoint is unreachable" This reverts commit 19b90a4e2d42b9c4448bc6e7fe8a2e3a89f40fa7. --- .../components/Playground/TopBar/TopBar.tsx | 70 +++++++++++-------- 1 file changed, 41 insertions(+), 29 deletions(-) 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 bf9be1864..68a5fd7f7 100644 --- a/packages/graphql-playground-react/src/components/Playground/TopBar/TopBar.tsx +++ b/packages/graphql-playground-react/src/components/Playground/TopBar/TopBar.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { styled, keyframes, withProps } from '../../../styled' +import { styled } from '../../../styled/index' import * as theme from 'styled-theming' import { darken, lighten } from 'polished' import * as CopyToClipboard from 'react-copy-to-clipboard' @@ -23,10 +23,6 @@ export interface Props { endpointUnreachable: boolean } -interface ErrorMessageProps { - visibleIf: boolean -} - export default class TopBar extends React.Component { render() { const { endpointUnreachable } = this.props @@ -43,14 +39,17 @@ export default class TopBar extends React.Component { disabled={this.props.fixedEndpoint} className={cx({ active: !this.props.fixedEndpoint })} /> - - Server cannot be reached - - - + {endpointUnreachable ? ( + + Server cannot be reached + + + ) : ( + + )} @@ -154,33 +153,46 @@ const UrlBarWrapper = styled.div` align-items: center; ` -const ReachError = withProps()(styled.div) ` - opacity: ${p => p.visibleIf ? 1 : 0}; - transition: opacity 0.5s ease-out; +const ReachError = styled.div` position: absolute; - right: -5px; + right: 5px; display: flex; align-items: center; - justify-content: flex-end; color: #f25c54; - user-select: none; ` -const pulseScaleOut = keyframes` +const Spinner = styled.div` + & { + width: 40px; + height: 40px; + margin: 40px auto; + background-color: #333; + border-radius: 100%; + -webkit-animation: sk-pulseScaleOut 1s infinite ease-in-out; + animation: sk-pulseScaleOut 1s infinite ease-in-out; + } + + @-webkit-keyframes sk-pulseScaleOut { 0% { + -webkit-transform: scale(0); transform: scale(0); } 100% { + -webkit-transform: scale(1); transform: scale(1); opacity: 0; } -` + } -const Spinner = styled.div` - width: 40px; - height: 40px; - margin: 40px auto; - background-color: #333; - border-radius: 100%; - animation: ${pulseScaleOut} 1s infinite ease-in-out; -` \ No newline at end of file + @keyframes sk-pulseScaleOut { + 0% { + -webkit-transform: scale(0); + transform: scale(0); + } + 100% { + -webkit-transform: scale(1); + transform: scale(1); + opacity: 0; + } + } +`