add react graphql tutorial (#2061)

This commit is contained in:
Praveen Durairaj 2019-04-24 16:57:27 +05:30 committed by Shahidh K Muhammed
parent 733101bf85
commit f4ddfd88a0
331 changed files with 114279 additions and 1 deletions

View File

@ -1,10 +1,11 @@
# Community
This directory contains community-contributed code and content that supplements Hasura GraphQL Engine. It includes several example applications built using GraphQL Engine to demonstrate its features, several boilerplates for users to get started with auth webhooks, triggers, etc. and some community tooling around the Engine.
This directory contains community-contributed code and content that supplements Hasura GraphQL Engine. It includes several example applications built using GraphQL Engine to demonstrate its features, several boilerplates for users to get started with auth webhooks, triggers, etc. and some community tooling around the Engine. There are also graphql tutorials built using GraphQL Engine as the backend. These tutorials help in learning how to integrate GraphQL APIs with your favourite web/mobile framework.
The following categories of community content can be found in their respective sub-folders:
- [Boilerplates](boilerplates)
- [Learn](learn)
- [Sample Apps](sample-apps)
- [Tools](tools)

View File

@ -0,0 +1 @@
remote-hasura.yaml

View File

@ -0,0 +1,5 @@
# GraphQL Tutorials
Visit [learn.hasura.io](https://learn.hasura.io) to learn how to integrate GraphQL APIs with your favourite web/mobile framework in a real world scenario.
_This directory contains all the source code the powers GraphQL Turtorials_

View File

@ -0,0 +1,39 @@
const { query } = require('graphqurl@0.3.2');
module.exports = function (user, context, cb) {
// Perform any asynchronous actions, e.g. send notification to Slack.
let userIdPrefix = 'auth0|';
if (context.connection.name !== 'Username-Password-Authentication') {
userIdPrefix = 'google-oauth2|';
}
query(
{
query: `
mutation($userId: String!, $nickname: String) {
insert_users(
objects: [{ auth0_id: $userId, name: $nickname }]
on_conflict: {
constraint: users_pkey
update_columns: [last_seen, name]
}
) {
affected_rows
}
}
`,
endpoint: 'https://learn.hasura.io/graphql',
headers: {
'x-hasura-admin-secret': '<replace-with-admin-secret>'
},
variables: {
userId: userIdPrefix + user.id,
nickname: user.email.split('@')[0]
}
}
).then((response) => {
cb();
})
.catch((error) => {
console.error(error);
cb();
});
};

View File

@ -0,0 +1,15 @@
function (user, context, callback) {
const userId = user.user_id;
const nickname = user.nickname;
request.post({
headers: {'content-type' : 'application/json', 'x-hasura-admin-secret': ''},
url: 'https://learn.hasura.io/graphql',
body: `{\"query\":\"mutation($userId: String!, $nickname: String) {\\n insert_users(\\n objects: [{ auth0_id: $userId, name: $nickname }]\\n on_conflict: {\\n constraint: users_pkey\\n update_columns: [last_seen, name]\\n }\\n ) {\\n affected_rows\\n }\\n }\",\"variables\":{\"userId\":\"${userId}\",\"nickname\":\"${nickname}\"}}`
}, function(error, response, body){
console.log(body);
callback(null, user, context);
});
}

View File

@ -0,0 +1,4 @@
{
"presets": ["stage-0", "env", "react"],
"plugins": ["transform-class-properties", "transform-react-remove-prop-types", "istanbul"]
}

View File

@ -0,0 +1,120 @@
---
# Output debugging info
# loglevel: debug
# Major version of Bootstrap: 3 or 4
bootstrapVersion: 3
# If Bootstrap version 3 is used - turn on/off custom icon font path
useCustomIconFontPath: false
# Webpack loaders, order matters
styleLoaders:
- style
- css
- sass
# Extract styles to stand-alone css file
# Different settings for different environments can be used,
# It depends on value of NODE_ENV environment variable
# This param can also be set in webpack config:
# entry: 'bootstrap-loader/extractStyles'
#extractStyles: false
env:
development:
extractStyles: false
production:
extractStyles: true
# Customize Bootstrap variables that get imported before the original Bootstrap variables.
# Thus, derived Bootstrap variables can depend on values from here.
# See the Bootstrap _variables.scss file for examples of derived Bootstrap variables.
#
preBootstrapCustomizations: ./src/theme/variables.scss
# This gets loaded after bootstrap/variables is loaded
# Thus, you may customize Bootstrap variables
# based on the values established in the Bootstrap _variables.scss file
#
bootstrapCustomizations: ./src/theme/bootstrap.overrides.scss
# Import your custom styles here
# Usually this endpoint-file contains list of @imports of your application styles
#
# appStyles: ./path/to/your/app/styles/endpoint.scss
### Bootstrap styles
styles:
# Mixins
mixins: true
# Reset and dependencies
normalize: true
print: true
glyphicons: true
# Core CSS
scaffolding: true
type: true
code: true
grid: true
tables: true
forms: true
buttons: true
# Components
component-animations: true
dropdowns: true
button-groups: true
input-groups: true
navs: true
navbar: true
breadcrumbs: true
pagination: true
pager: true
labels: true
badges: true
jumbotron: true
thumbnails: true
alerts: true
progress-bars: true
media: true
list-group: true
panels: true
wells: true
responsive-embed: true
close: true
# Components w/ JavaScript
modals: true
tooltip: true
popovers: true
carousel: true
# Utility classes
utilities: true
responsive-utilities: true
### Bootstrap scripts
#scripts: false
scripts:
transition: false
alert: false
button: true
carousel: false
collapse: false
dropdown: true
modal: true
tooltip: false
popover: false
scrollspy: false
tab: false
affix: false

View File

@ -0,0 +1 @@
node_modules

View File

@ -0,0 +1,2 @@
webpack/*
src/utils.js

View File

@ -0,0 +1,101 @@
{ "extends": "eslint-config-airbnb",
"env": {
"browser": true,
"node": true,
"mocha": true,
"cypress/globals": true
},
"parser": "babel-eslint",
"rules": {
"allowForLoopAfterthoughts": true,
"react/no-multi-comp": 0,
"import/default": 0,
"import/no-duplicates": 0,
"import/named": 0,
"import/first": 0,
"import/namespace": 0,
"import/no-unresolved": 0,
"import/no-named-as-default": 2,
"import/extensions": 0,
"import/no-extraneous-dependencies": 0,
"import/prefer-default-export": 0,
"comma-dangle": 0,
"id-length": [1, {"min": 1, "properties": "never"}],
"indent": [2, 2, {"SwitchCase": 1}],
"no-console": 0,
"arrow-parens": 0,
"no-alert": 0,
"no-plusplus": 0,
"no-unsafe-negation": 0,
"no-loop-func": 0,
"no-lonely-if": 0,
"no-bitwise": 0,
"global-require": 0,
"no-param-reassign": 0,
"no-underscore-dangle": 0,
"no-useless-return": 0,
"no-restricted-syntax": 0,
"no-prototype-builtins": 0,
"array-callback-return": 0,
"no-useless-concat": 0,
"consistent-return": 0,
"class-methods-use-this": 0,
"arrow-body-style": 0,
"prefer-template": 0,
"prefer-spread": 0,
"object-shorthand": 0,
"object-curly-newline": 0,
"spaced-comment": 0,
"prefer-destructuring": ["error", {"object": false, "array": false}],
"prefer-rest-params": 0,
"function-paren-newline": 0,
"no-case-declarations": 0,
"no-restricted-globals": 0,
"no-unneeded-ternary": 0,
"no-mixed-operators": 0,
"no-return-assign": 0,
"operator-assignment": 0,
"strict": 0,
"react/jsx-no-duplicate-props": 0,
"react/jsx-filename-extension": 0,
"react/jsx-curly-brace-presence": 0,
"react/forbid-prop-types": 0,
"react/require-default-props": 0,
"react/no-unused-prop-types": 0,
"react/no-string-refs": 0,
"react/no-unused-state": 0,
"react/no-array-index-key": 0,
"react/jsx-no-bind": 0,
"react/prop-types": 0,
"react/prefer-stateless-function": 0,
"react/no-unescaped-entities": 0,
"jsx-a11y/click-events-have-key-events": 0,
"jsx-a11y/no-static-element-interactions": 0,
"jsx-a11y/no-noninteractive-element-interactions": 0,
"jsx-a11y/label-has-for": 0,
"jsx-a11y/anchor-is-valid": 0,
"jsx-a11y/lang": 0,
"jsx-a11y/alt-text": 0,
"max-len": 0
},
"plugins": [
"react", "import", "cypress"
],
"settings": {
"import/parser": "babel-eslint",
"parser": "babel-esling",
"import/resolve": {
"moduleDirectory": ["node_modules", "src"]
}
},
"globals": {
"__DEVELOPMENT__": true,
"__CLIENT__": true,
"__SERVER__": true,
"__DISABLE_SSR__": true,
"__DEVTOOLS__": true,
"socket": true,
"webpackIsomorphicTools": true
}
}

View File

@ -0,0 +1,12 @@
.env
cypress/videos
cypress/screenshots
.nyc_output
node_modules
static/dist
webpack-assets.json
webpack-stats.json
npm-debug.log
*.swp
coverage
.idea/*

View File

@ -0,0 +1,5 @@
*
**/*
!static/dist/main.js
!static/dist/vendor.js
!static/dist/main.css

View File

@ -0,0 +1,23 @@
FROM node:8.9-alpine
# Create app directory
ENV GRAPHQL_ENDPOINT=https://learn.hasura.io/graphql
ENV REACT_APP_CALLBACK_URL=https://learn.hasura.io/graphql/graphiql/callback
WORKDIR /app
# Install app dependencies
RUN npm config set unsafe-perm true
RUN npm -g install serve
# A wildcard is used to ensure both package.json AND package-lock.json are copied
COPY package*.json ./
RUN npm install
# Bundle app source
COPY . /app
#Build react/vue/angular bundle static files
RUN npm run build
EXPOSE 8080
# serve dist folder on port 8080
CMD ["serve", "-s", "static", "-p", "8080"]

View File

@ -0,0 +1,32 @@
## GraphiQL
This version of GraphiQL is a fork of the original version with a simple header management UI.
You can access it live here - https://learn.hasura.io/graphql/graphiql
## Usage of Environment Variables
This app uses a few environment variables which are required for development. The production build uses values directly present in index.html serving this app.
We use [dotenv](https://github.com/motdotla/dotenv) for setting environment variables for development. Create a `.env` file in the root directory (wherever package.json is) and set the following values. Replace accordingly for testing.
```
PORT=3000
NODE_ENV=development
GRAPHQL_ENDPOINT=http://localhost:8090/v1alpha1/graphql
HEADER_STRING='{}'
VARIABLE_STRING='{}'
QUERY_STRING='query { test_table { id } }'
REACT_APP_CALLBACK_URL='http://localhost:3000/callback'
```
**Note**
The .env file should not be in version control.
## Deployment
```
$ npm run build
```
The static assets will be generated in `static` folder. There is an index.html file referencing the css and js assets inside `dist` folder.

View File

@ -0,0 +1,9 @@
module.exports = {
hmrPort: parseInt(process.env.PORT, 10) + 1 || 3001,
hmrHost: process.env.HOST || '127.0.0.1',
appHost: '0.0.0.0',
port: { development: process.env.PORT, production: 8080 },
assetsPrefix: '/rstatic',
webpackPrefix: '/rstatic/dist/',
appPrefix: '/rapp',
};

View File

@ -0,0 +1,15 @@
// enable runtime transpilation to use ES6/7 in node
const fs = require('fs');
const babelrc = fs.readFileSync('.babelrc');
let config;
try {
config = JSON.parse(babelrc);
} catch (err) {
console.error('==> ERROR: Error parsing your .babelrc.');
console.error(err);
}
require('babel-core/register')(config);

View File

@ -0,0 +1,33 @@
#!/usr/bin/env node
require('./server.babel'); // babel registration (runtime transpilation for node)
const path = require('path');
const rootDir = path.resolve(__dirname, '..');
/**
* Define isomorphic constants.
*/
global.__CLIENT__ = false;
global.__SERVER__ = true;
global.__DISABLE_SSR__ = false; // <----- DISABLES SERVER SIDE RENDERING FOR ERROR DEBUGGING
global.__DEVELOPMENT__ = process.env.NODE_ENV !== 'production';
if (__DEVELOPMENT__) {
if (
!require('piping')({
//Fork the process and supervise the child for hot-reloading code
hook: true,
ignore: /(\/\.|~$|\.json|\.scss$)/i,
})
) {
return; //The parent process ends, and child process continues from below
}
}
const WebpackIsomorphicTools = require('webpack-isomorphic-tools');
global.webpackIsomorphicTools = new WebpackIsomorphicTools(
require('../webpack/webpack-isomorphic-tools')
).server(rootDir, () => {
require('../src/server');
});
require('../src/server');

View File

@ -0,0 +1,4 @@
{
"name": "graphqurl-graphiql",
"alias": "hasura-graphiql.now.sh"
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,143 @@
{
"name": "graphql-tutorials-graphiql",
"description": "Explore GraphQL APIs with headers",
"author": "Hasura (https://github.com/hasura/graphql-engine)",
"license": "MIT",
"version": "0.1.0",
"repository": {
"type": "git",
"url": "https://github.com/hasura/graphql-engine"
},
"keywords": [],
"scripts": {
"start": "concurrently --kill-others \"npm run start-prod\"",
"start-prod": "better-npm-run start-prod",
"build": "webpack --progress -p --colors --display-error-details --config webpack/prod.config.js",
"build-unused": "webpack --verbose --colors --display-error-details --config webpack/prod.config.js --json | webpack-unused -s src",
"lint": "eslint -c .eslintrc src api",
"start-dev": "better-npm-run start-dev",
"watch-client": "better-npm-run watch-client",
"dev": "concurrently --kill-others \"npm run watch-client\" \"npm run start-dev\" ",
"cypress": "cypress open",
"test": "cypress run --spec 'cypress/integration/test_complete.js' --config baseUrl=$CYPRESS_BASE_URL"
},
"betterScripts": {
"start-prod": {
"command": "node ./bin/server.js",
"env": {
"NODE_PATH": "./src",
"NODE_ENV": "production",
"PORT": 8080
}
},
"start-dev": {
"command": "node -r dotenv/config ./bin/server.js"
},
"watch-client": {
"command": "node -r dotenv/config webpack/webpack-dev-server.js"
}
},
"dependencies": {
"apollo-link": "^1.2.2",
"apollo-link-ws": "^1.0.8",
"auth0-js": "^9.8.2",
"babel-polyfill": "^6.26.0",
"deep-equal": "^1.0.1",
"graphiql": "^0.11.11",
"graphiql-explorer": "^0.3.7",
"graphql": "^14.0.2",
"hasura-console-graphiql": "0.0.1",
"history": "^3.0.0",
"hoist-non-react-statics": "^1.0.3",
"invariant": "^2.2.0",
"isomorphic-fetch": "^2.2.1",
"less": "^3.7.1",
"lru-memoize": "^1.0.0",
"map-props": "^1.0.0",
"multireducer": "^1.0.2",
"piping": "^0.3.0",
"prettier": "^1.13.0",
"pretty-error": "^1.2.0",
"prop-types": "^15.6.0",
"query-string": "^6.1.0",
"react": "^16.4.0",
"react-dom": "^16.4.0",
"react-helmet": "^5.2.0",
"react-hot-loader": "^4.2.0",
"react-progress-bar-plus": "^1.3.1",
"react-redux": "^5.0.6",
"react-router": "^3.2.0",
"react-router-redux": "^4.0.8",
"redux": "^4.0.0",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.2.0",
"subscriptions-transport-ws": "^0.9.12",
"valid-url": "^1.0.9"
},
"devDependencies": {
"babel-core": "^6.26.0",
"babel-eslint": "^8.2.3",
"babel-loader": "^7.1.2",
"babel-plugin-istanbul": "^4.1.6",
"babel-plugin-react-transform": "~3.0.0",
"babel-plugin-transform-react-remove-prop-types": "^0.4.10",
"babel-plugin-typecheck": "^2.0.0",
"babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-0": "^6.24.1",
"babel-runtime": "~6.26.0",
"better-npm-run": "^0.1.0",
"bootstrap-loader": "^2.2.0",
"bootstrap-sass": "^3.3.7",
"clean-webpack-plugin": "^0.1.17",
"concurrently": "^3.5.0",
"css-loader": "^0.28.11",
"dotenv": "^5.0.1",
"eslint": "^4.19.1",
"eslint-config-airbnb": "16.1.0",
"eslint-loader": "^1.0.0",
"eslint-plugin-chai-friendly": "^0.4.1",
"eslint-plugin-cypress": "^2.0.1",
"eslint-plugin-import": "^2.12.0",
"eslint-plugin-jsx-a11y": "^6.0.3",
"eslint-plugin-react": "^7.9.1",
"express": "^4.13.3",
"express-session": "^1.12.1",
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^1.1.11",
"font-awesome": "^4.7.0",
"font-awesome-webpack": "0.0.4",
"husky": "^0.14.3",
"ignore-loader": "^0.1.2",
"jquery": "^3.3.1",
"json-loader": "^0.5.4",
"less-loader": "^4.1.0",
"lint-staged": "^6.1.1",
"mini-css-extract-plugin": "^0.4.0",
"node-sass": "^4.9.2",
"nyc": "^12.0.2",
"optimize-css-assets-webpack-plugin": "^4.0.2",
"react-a11y": "^0.2.6",
"react-addons-test-utils": "^15.0.3",
"react-transform-hmr": "1.0.4",
"redux-devtools": "^3.4.1",
"redux-devtools-dock-monitor": "^1.1.2",
"redux-devtools-log-monitor": "^1.3.0",
"resolve-url-loader": "^2.3.0",
"sass-loader": "^7.0.1",
"sinon": "^1.17.7",
"style-loader": "^0.20.3",
"timekeeper": "1.0.0",
"uglifyjs-webpack-plugin": "^1.2.7",
"unused-files-webpack-plugin": "^3.4.0",
"url-loader": "^1.0.1",
"webpack": "^4.14.0",
"webpack-cli": "^3.0.8",
"webpack-dev-middleware": "^3.1.3",
"webpack-hot-middleware": "^2.22.2",
"webpack-isomorphic-tools": "^3.0.5"
},
"engines": {
"node": ">=8.9.1"
}
}

View File

@ -0,0 +1,94 @@
/**
* THIS IS THE ENTRY POINT FOR THE CLIENT, JUST LIKE server.js IS THE ENTRY POINT FOR THE SERVER.
*/
// import 'babel-polyfill';
import React from 'react';
import ReactDOM from 'react-dom';
import { createLogger } from 'redux-logger';
import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
import { Router, browserHistory } from 'react-router';
import { routerMiddleware, syncHistoryWithStore } from 'react-router-redux';
import { compose, createStore, applyMiddleware } from 'redux';
import { useBasename } from 'history';
import getRoutes from './routes';
import reducer from './reducer';
// Create the store
let _finalCreateStore;
if (__DEVELOPMENT__) {
_finalCreateStore = compose(
applyMiddleware(thunk, routerMiddleware(browserHistory), createLogger()),
require('redux-devtools').persistState(
window.location.href.match(/[?&]debug_session=([^&]+)\b/)
)
)(createStore);
} else {
_finalCreateStore = compose(
applyMiddleware(thunk, routerMiddleware(browserHistory))
)(createStore);
}
const hashLinkScroll = () => {
const { hash } = window.location;
if (hash !== '') {
// Push onto callback queue so it runs after the DOM is updated,
// this is required when navigating from a different page so that
// the element is rendered on the page before trying to getElementById.
setTimeout(() => {
const id = hash.replace('#', '');
const element = document.getElementById(id);
if (element) {
element.scrollIntoView();
}
}, 0);
} else {
// This is a hack to solve the issue with scroll retention during page change.
setTimeout(() => {
const element = document.getElementsByTagName('body');
if (element && element.length > 0) {
element[0].scrollIntoView();
}
}, 0);
}
};
const store = _finalCreateStore(reducer);
const history = syncHistoryWithStore(browserHistory, store);
/* ****************************************************************** */
// Enable hot reloading
if (__DEVELOPMENT__ && module.hot) {
module.hot.accept('./reducer', () => {
store.replaceReducer(require('./reducer'));
});
}
// Main routes and rendering
const main = (
<Router
history={useBasename(() => history)({ basename: '/graphql/graphiql' })}
// history={useBasename(() => history)({ basename: '/' })}
routes={getRoutes(store)}
onUpdate={hashLinkScroll}
/>
);
const dest = document.getElementById('content');
ReactDOM.render(
<Provider store={store} key="provider">
{main}
</Provider>,
dest
);
if (process.env.NODE_ENV !== 'production') {
window.React = React; // enable debugger
}

View File

@ -0,0 +1,30 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import Helmet from 'react-helmet';
class PageNotFound extends Component {
render() {
const styles = require('./Styles.scss');
return (
<div className={styles.viewcontainer}>
<Helmet title="404 - Page Not Found" />
<div className={'container ' + styles.centerContent}>
<div className={'row ' + styles.message}>
<div className="col-xs-8">
<h1>404</h1>
This page doesn't exist.
</div>
</div>
</div>
</div>
);
}
}
PageNotFound.propTypes = {
dispatch: PropTypes.func.isRequired,
};
export default connect()(PageNotFound);

View File

@ -0,0 +1,42 @@
.container {
padding: 0;
}
.viewcontainer {
height: 100vh;
width: 100vw;
display: table;
.centerContent{
display: table-cell;
vertical-align: middle;
.message {
padding: 50px 20%;
}
.message h1 {
font-size: 54px;
font-weight: bold;
}
.message p {
margin-left: 15px;
}
.message p > a {
font-weight: bold;
}
}
}
.header {
background: #eee;
h2 {
margin: 0;
padding: 26px;
float: left;
line-height: 26px;
}
.nav {
padding: 20px;
float: left;
}
}

View File

@ -0,0 +1,247 @@
import defaultState from './state';
// import fetch from 'isomorphic-fetch';
import { SubscriptionClient } from 'subscriptions-transport-ws';
import { WebSocketLink } from 'apollo-link-ws';
import { parse } from 'graphql';
import { execute } from 'apollo-link';
import { push } from 'react-router-redux';
const REQUEST_HEADER_CHANGED = 'ApiExplorer/REQUEST_HEADER_CHANGED';
const REQUEST_HEADER_ADDED = 'ApiExplorer/REQUEST_HEADER_ADDED';
const REQUEST_HEADER_REMOVED = 'ApiExplorer/REQUEST_HEADER_REMOVED';
const FOCUS_ROLE_HEADER = 'ApiExplorer/FOCUS_ROLE_HEADER';
const UNFOCUS_ROLE_HEADER = 'ApiExplorer/UNFOCUS_ROLE_HEADER';
const GRAPHQL_ENDPOINT_CHANGED = 'ApiExplorer/GRAPHQL_ENDPOINT_CHANGED';
import { getHeadersAsJSON } from './utils';
const focusHeaderTextbox = () => ({ type: FOCUS_ROLE_HEADER });
const unfocusTypingHeader = () => ({ type: UNFOCUS_ROLE_HEADER });
const updateGraphQLEndpoint = (endpoint) => {
return (dispatch) => {
dispatch({ type: GRAPHQL_ENDPOINT_CHANGED, data: endpoint });
// set local storage
window.localStorage.setItem('ONLINE_GRAPHIQL_ENDPOINT', endpoint);
dispatch(push('/graphiql'));
};
};
const getRemoteQueries = (queryUrl, cb) => {
fetch(queryUrl)
.then(resp => resp.text().then(cb))
.catch(e => console.log('Invalid query URL: ', e));
};
const createWsClient = (url, headers) => {
const gqlUrl = new URL(url);
let websocketProtocol = 'ws';
if (gqlUrl.protocol === 'https:') {
websocketProtocol = 'wss';
}
const headersFinal = getHeadersAsJSON(headers);
const graphqlUrl = `${websocketProtocol}://${url.split('//')[1]}`;
const client = new SubscriptionClient(graphqlUrl, {
connectionParams: {
headers: {
...headersFinal,
},
},
reconnect: true,
});
return client;
};
const graphqlSubscriber = (graphQLParams, url, headers) => {
const link = new WebSocketLink(createWsClient(url, headers));
try {
const fetcher = operation => {
operation.query = parse(operation.query);
return execute(link, operation);
};
return fetcher(graphQLParams);
} catch (e) {
return e.json();
}
};
const isSubscription = graphQlParams => {
const queryDoc = parse(graphQlParams.query);
for (const definition of queryDoc.definitions) {
if (definition.kind === 'OperationDefinition') {
const operation = definition.operation;
if (operation === 'subscription') {
return true;
}
}
}
return false;
};
const graphQLFetcherFinal = (graphQLParams, url, headers) => {
if (isSubscription(graphQLParams)) {
return graphqlSubscriber(graphQLParams, url, headers);
}
return fetch(url, {
method: 'POST',
headers: getHeadersAsJSON(headers),
body: JSON.stringify(graphQLParams),
}).then(response => response.json());
};
const changeRequestHeader = (index, key, newValue, isDisabled) => ({
type: REQUEST_HEADER_CHANGED,
data: {
index: index,
keyName: key,
newValue: newValue,
isDisabled: isDisabled,
},
});
const addRequestHeader = (key, value) => ({
type: REQUEST_HEADER_ADDED,
data: {
key: key,
value: value,
},
});
const removeRequestHeader = index => {
return {
type: REQUEST_HEADER_REMOVED,
data: index,
};
};
// This method adds the new header and moves the empty header to the bottom of the list
const getHeadersAfterAddingNewHeader = (headers, newHeader) => {
const nonEmptyHeaders = headers.filter(header => {
return !header.isNewHeader;
});
nonEmptyHeaders.push(newHeader);
nonEmptyHeaders.push({
key: '',
value: '',
isActive: false,
isNewHeader: true,
});
return nonEmptyHeaders;
};
// This method adds a new empty header if no empty header is present
const getChangedHeaders = (headers, changedHeaderDetails) => {
const newHeaders = Object.assign([], headers);
if (newHeaders[changedHeaderDetails.index].isNewHeader) {
newHeaders[changedHeaderDetails.index].isNewHeader = false;
newHeaders[changedHeaderDetails.index].isActive = true;
newHeaders[changedHeaderDetails.index].isDisabled = false;
}
if (changedHeaderDetails.keyName === 'isActive') {
newHeaders[changedHeaderDetails.index].isActive = !newHeaders[
changedHeaderDetails.index
].isActive;
} else {
newHeaders[changedHeaderDetails.index][changedHeaderDetails.keyName] =
changedHeaderDetails.newValue;
}
if (changedHeaderDetails.isDisabled === true) {
newHeaders[changedHeaderDetails.index].isDisabled = true;
} else {
newHeaders[changedHeaderDetails.index].isDisabled = false;
}
const nonEmptyHeaders = newHeaders.filter(header => {
return !header.isNewHeader;
});
nonEmptyHeaders.push({
key: '',
value: '',
isActive: false,
isNewHeader: true,
isDisabled: false,
});
return nonEmptyHeaders;
};
const apiExplorerReducer = (state = defaultState, action) => {
switch (action.type) {
case REQUEST_HEADER_CHANGED:
return {
...state,
displayedApi: {
...state.displayedApi,
request: {
...state.displayedApi.request,
headers: getChangedHeaders(
state.displayedApi.request.headers,
action.data
),
},
},
};
case REQUEST_HEADER_ADDED:
return {
...state,
displayedApi: {
...state.displayedApi,
request: {
...state.displayedApi.request,
headers: getHeadersAfterAddingNewHeader(
state.displayedApi.request.headers,
{
key: action.data.key,
value: action.data.value,
isActive: true,
isNewHeader: false,
}
),
},
},
};
case REQUEST_HEADER_REMOVED:
return {
...state,
displayedApi: {
...state.displayedApi,
request: {
...state.displayedApi.request,
headers: state.displayedApi.request.headers.filter((header, i) => {
return !(i === action.data);
}),
},
},
};
case UNFOCUS_ROLE_HEADER:
return {
...state,
headerFocus: false,
};
case FOCUS_ROLE_HEADER:
return {
...state,
headerFocus: true,
};
case GRAPHQL_ENDPOINT_CHANGED:
return {
...state,
graphqlEndpoint: action.data,
};
default:
return state;
}
};
export default apiExplorerReducer;
export {
changeRequestHeader,
addRequestHeader,
removeRequestHeader,
graphQLFetcherFinal,
focusHeaderTextbox,
unfocusTypingHeader,
getRemoteQueries,
updateGraphQLEndpoint,
};

View File

@ -0,0 +1,75 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import ApiRequestWrapper from './ApiRequestWrapper';
import Helmet from 'react-helmet';
import { push } from 'react-router-redux';
class ApiExplorer extends Component {
componentWillMount() {
let localStorageUrl;
if (window.__env.graphqlEndpoint && window.__env.graphqlEndpoint !== 'undefined') {
localStorageUrl = window.__env.graphqlEndpoint;
} else {
localStorageUrl = window.localStorage.getItem('ONLINE_GRAPHIQL_ENDPOINT');
}
if (!this.props.graphqlEndpoint && (localStorageUrl === 'undefined' || localStorageUrl === null)) {
this.props.dispatch(push('/'));
}
}
render() {
let localStorageUrl;
if (window.__env.graphqlEndpoint && window.__env.graphqlEndpoint !== 'undefined') {
localStorageUrl = window.__env.graphqlEndpoint;
} else {
localStorageUrl = window.localStorage.getItem('ONLINE_GRAPHIQL_ENDPOINT');
}
const styles = require('./ApiExplorer.scss');
const wrapperClass = styles.apiExplorerWrapper;
const requestStyles = '';
const wdClass = '';
const requestWrapper = (
<ApiRequestWrapper
credentials={this.props.credentials}
explorerData={this.props.explorerData}
details={this.props.displayedApi.details}
request={this.props.displayedApi.request}
requestStyles={requestStyles}
dispatch={this.props.dispatch}
wdStyles={wdClass}
route={this.props.route}
dataHeaders={this.props.dataHeaders}
headerFocus={this.props.headerFocus}
queryParams={this.props.location.query}
graphqlEndpoint={localStorageUrl}
auth={this.props.route.auth}
/>
);
return (
<div className={'container-fluid ' + styles.padd_remove}>
<Helmet
title="GraphiQL Online with Headers | Built by Hasura"
description="An online version of GraphiQL. Manage headers easily. Test your GraphQL servers."
/>
<div className={wrapperClass}>{requestWrapper}</div>
</div>
);
}
}
ApiExplorer.propTypes = {
modalState: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
route: PropTypes.object.isRequired,
headerFocus: PropTypes.bool.isRequired,
location: PropTypes.object.isRequired,
};
export default ApiExplorer;

View File

@ -0,0 +1,867 @@
@import "../Common/Common.scss";
.display_inl {
display: inline-block;
}
.width_80 {
width: 80%;
}
.responseHeader {
color: #788095;
font-weight: bold;
font-size: 14px;
}
.admin_token_align {
vertical-align: middle;
margin-left: 2px;
}
.marginBottom {
margin-bottom: 15px;
}
.apiExplorerMini {
width: 80%;
}
.wrapperOnBoarding {
// width: 80% !important;
}
.panelGreyed {
opacity: 0.1;
}
.requestGreyed {
opacity: 0.1;
}
.panelInFocus {
}
.requestInFocus {
}
.cursorNotAllowed {
cursor: not-allowed;
button {
cursor: not-allowed;
}
}
.authBtnWrapper button {
text-align: right;
position: absolute;
right: 20px;
top: 10px;
font-size: 12px;
}
.overlay {
position: fixed;
display: block;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ddd;
z-index: 20;
}
.overlay .overlayContent {
position: absolute;
top: 40%;
left: 50%;
font-size: 18px;
text-align: center;
transform: translate(-50%,-50%);
-ms-transform: translate(-50%,-50%);
}
.overlay .overlayHeading {
font-size: 24px;
font-weight: 500;
margin-bottom: 20px;
color: #444;
}
.overlay .overlayMessage {
color: #5f5f5f;
}
.overlay .overlayAction {
margin-top: 10px;
}
.apiExplorerWrapper {
display: flex;
height: $mainContainerHeight;
.ApiRequestWrapperVH {
height: 100%;
width: 100%;
}
.apiCollectionWrapper {
background-color: #fff;
height: 100%;
overflow-y: auto;
// Changed it from overfllow-y: scroll
.apiCollectionTabWrapper {
.apiCollectionTab {
-webkit-padding-start: 0px;
-webkit-margin-before: 0;
-webkit-margin-after: 0;
-moz-padding-start: 0px;
-moz-margin-before: 0;
-moz-margin-after: 0;
border-bottom: 1px solid #D8D8D8;
position: absolute;
width: 20%;
min-width: 240px;
background-color: #fff;
z-index: 10;
.apiCollectionTabList {
list-style-type: none;
display: inline-block;
width: 50%;
text-align: center;
padding: 10px 11px;
font-weight: bold;
font-size: 16px;
color: #D8D8D8;
cursor: pointer;
}
.activeApiCollectionTab {
border-bottom: 3px solid #ffca27;
color: #6B6B6B;
}
.apiCollectionTabList:focus {
outline: none;
}
}
.apiCollectionClearHistory {
padding: 0 15px;
margin: 0;
border-bottom: 1px solid #e5e5e5;
/*
position: absolute;
bottom: 0;
transform: translateX(-88%);
*/
.apiCollectionClearHistoryButton {
/*
float: right;
margin: 10px 0;
cursor: pointer;
*/
margin: 10px 0;
cursor: pointer;
position: fixed;
bottom: 15px;
text-align: center;
width: 20%;
margin-left: -15px;
padding-top: 10px;
border-top: 1px solid #ccc;
background-color: #fff;
z-index: 1;
i {
padding-right: 5px;
}
}
}
.apiPaddTop {
padding-top: 46px !important;
}
.apiCollectionTabListDetails {
padding: 0 15px;
margin: 10px 0;
// padding-top: 46px;
.apiCollectionTabListHead {
padding-left: 15px;
padding-bottom: 10px;
font-weight: bold;
font-size: 15px;
.serviceBaseDomain {
color: #bbb;
}
}
.add_ellipsis {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.apiCollectionGetPost {
.apiCollectionGetWrapper {
padding: 5px 0;
cursor: pointer;
.apiCollectionGet {
text-align: left;
color: #70CD00;
font-size: 12px;
font-weight: bold;
padding-left: 15px;
}
.apiCollectionGetDetailsWrapper {
display: flex;
align-items: center;
.apiCollectionGetDetails {
word-wrap: break-word;
padding-right: 10px !important
}
.apiCollectionPostDetails {
word-wrap: break-word;
padding-right: 10px !important
}
.apiRightArrowWrapper {
padding-left: 5px;
}
}
.activeApiCollectionGetWrapperIcon {
display: none;
}
}
.activeApiCollectionGetWrapper {
background-color: #FFF3D5;
border-radius: 4px;
.activeApiCollectionGetWrapperIcon {
display: block;
}
}
.apiCollectionPostWrapper {
padding: 5px 0;
cursor: pointer;
.apiCollectionPost {
text-align: left;
color: #FD9540;
font-size: 12px;
font-weight: bold;
padding-left: 15px;
}
.apiCollectionGetDetailsWrapper {
display: flex;
align-items: center;
.apiCollectionGetDetails {
word-wrap: break-word;
padding-right: 10px !important
}
.apiCollectionPostDetails {
word-wrap: break-word;
padding-right: 10px !important
}
.apiRightArrowWrapper {
padding-left: 5px;
}
}
.activeApiCollectionGetWrapperIcon {
display: none;
}
}
.activeApiCollectionGetWrapper {
background-color: #FFF3D5;
border-radius: 4px;
.activeApiCollectionGetWrapperIcon {
display: block;
}
}
}
}
}
}
.apiContentPadd {
padding-top: 20px;
border-color: rgba(23, 42, 58, .1);
border-width: 2px;
border-bottom-style: solid;
padding-bottom: 10px;
width: 100%;
float: left;
}
.closeHeader {
cursor: pointer;
padding-top: 8px;
font-size: 16px;
float: right;
display: inline-block;
}
.showAccessKey{
cursor: pointer;
padding-top: 8px;
padding-right: 8px;
font-size: 16px;
float: left;
display: inline-block;
}
.apiRequestWrapper {
.file_upload_wrapper {
width: 100%;
text-align: left;
display: inline-block;
padding: 20px;
border: 1px solid #ccc;
background-color: #fff;
margin-bottom: 15px;
input[type="file"] {
display: inline-block;
width: 162px;
}
}
.apiRequestheader {
font-weight: bold;
font-size: 18px;
padding-bottom: 10px;
color: #000;
}
.apiHasura {
font-size: 14px;
text-align: right;
i {
color: #757575;;
font-size: 22px;
position: absolute;
right: 15px;
top: 15px;
}
i:hover {
color: #000;
}
}
.built {
text-align: right;
font-size: 14px;
position: absolute;
right: 40px;
top: 16px;
i {
color: #f93c18;
}
}
.changeEndpoint {
padding: 10px 10px !important;
margin-left: 10px;
}
.apiRequestContent {
font-size: 14px;
float: left;
font-weight: bold;
a {
color: #FEC53D;
text-decoration: underline;
}
a:hover {
color: #FEC53D;
text-decoration: underline;
}
code {
background-color: transparent;
border: 1px solid #767E93;
padding: 1px 4px !important;
color: #767E93;
}
}
.apiPostRequestWrapper {
// padding: 20px 0;
// padding-top: 20px;
padding-top: 20px;
background-color: #f8fafb;
.inputGroupWrapper {
-webkit-box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.2);
-moz-box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.2);
box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.2);
border-radius: 5px;
.inputGroupBtn {
button {
width: 100px;
border: 0;
padding: 10px 12px;
background-color: #F9F9F9;
color: #FD9540;
font-size: 14px;
font-weight: bold;
text-align: left;
.caret {
position: absolute;
right: 10px;
top: 16px;
}
}
}
.inputGroupInput {
border: 0;
box-shadow: none;
padding: 10px 12px;
background-color: #fff;
}
}
.sendBtn {
button {
width: 100%;
text-align: center;
height: 39px;
color: #606060;
font-weight: bold;
border-radius: 5px;
background-color: #FEC53D;
border: 1px solid #FEC53D;
/*
background-color: #FFCA27;
border: 1px solid #FFCA27;
*/
&:hover {
background-color: #F2B130;
}
}
/*
button:hover
{
border: 1px solid #F2B130;
background-color: #F2B130;
}
*/
}
.generateBtn {
button {
width: 100%;
background-color: transparent;
text-align: center;
height: 39px;
border: 1px solid #606060;
color: #606060;
font-weight: bold;
border-radius: 5px;
}
button:hover {
border: 1px solid #606060;
background-color: #efefef;
}
}
}
.responseWrapper {
clear: both;
display: flex;
align-items: center;
.responseHeader {
color: #788095;
font-weight: bold;
font-size: 14px;
.viewDetails {
padding-left: 10px;
font-weight: normal;
color: #FFCA27;
}
.addAdminToken {
text-align: right;
padding-left: 15px;
}
}
.addAdminToken {
text-align: right;
padding-left: 15px;
}
}
}
.apiResponseWrapper {
.apiResponseheaderWrapper {
border-bottom: 1px solid #ccc;
margin-bottom: 20px;
.apiResponseheader {
font-weight: bold;
font-size: 14px;
padding-bottom: 10px;
// color: #000;
color: #788095
}
.statusDetails {
display: inline-block;
float: right;
padding-left: 20px;
.statusView {
padding-left: 5px;
font-weight: normal;
color: #FFCA27;
}
}
}
.helpTextWrapper {
padding: 15px;
border: 1px solid #ccc;
background-color: #fff;
clear: both;
margin-bottom: 20px;
i {
padding-right: 10px;
}
pre {
margin-top: 10px;
border-radius: 0;
}
.copyBtn {
padding: 9px;
}
}
.suggestionTextColor {
color: #111;
background-color: #ffd760;
border-color: #ffd760;
}
.noResponseWrapper {
width: 100%;
min-height: 200px;
background-color: #fff;
-webkit-box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.2);
-moz-box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.2);
box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.2);
clear: both;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20px;
.noResponseContainer {
width: 325px;
.noResponseHeader {
font-size: 18px;
opacity: 0.6;
}
.barWrapper {
padding-top: 15px;
text-align: center;
.bigBar {
width: 56%;
margin-right: 7px;
height: 20px;
background-color: #EEEEEE;
display: inline-block;
border-radius: 4px;
}
.mediumBar {
width: 23%;
margin-right: 7px;
height: 20px;
background-color: #FFCA27;
display: inline-block;
border-radius: 4px;
}
.smallBar {
width: 13%;
margin-right: 7px;
height: 20px;
background-color: #EEEEEE;
display: inline-block;
border-radius: 4px;
}
}
}
}
.responseHeader {
padding-top: 15px;
color: #788095;
font-weight: bold;
font-size: 14px;
clear: both;
.viewDetails {
padding-left: 10px;
font-weight: normal;
color: #FFCA27;
}
}
}
}
// Common
.responseTable {
padding-top: 15px;
.tableBorder {
background-color: #fff;
border: 1px solid #E3E5E5;
thead {
tr {
th {
border-bottom: 0px;
}
}
}
tbody {
tr {
td {
border-top: 0;
// padding: 5px;
padding: 0px 5px;
min-width: 50px;
.responseTableInput {
background-color: transparent;
border: 0;
box-shadow: none;
border-radius: 0;
// padding: 0;
}
}
.headerPadd {
padding: 15px !important;
}
.borderTop {
border-top: 1px solid #ccc;
}
.tableTdLeft {
padding-left: 5%;
}
.tableEnterKey {
// padding: 10px 0;
padding: 0px 5px 0px 4.5%;
}
.tableLastTd {
padding-left: 5px !important;
}
}
}
}
.headerHeading {
background-color: #f5f5f5;
font-weight: bold;
padding-left: 15px;
}
}
.queryBuilderWrapper {
padding-bottom: 20px;
.queryBuilderTab {
ul {
border: 1px solid #E7E7E7;
-webkit-padding-start: 0px;
-moz-padding-start: 0px;
display: inline-block;
li {
list-style-type: none;
display: inline-block;
padding: 12px 20px;
width: 150px;
text-align: center;
color: #788094;
cursor: pointer;
background-color: #fff;
font-weight: bold;
}
li:focus {
outline: none;
}
.activeQueryBuilderTab {
background-color: #FFF050;
}
}
}
}
.AceEditorWrapper {
-webkit-box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.2);
-moz-box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.2);
box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.2);
/*
margin-top: 15px;
margin-bottom: 20px;
margin-top: 20px;
*/
}
.queryBuilderLayout {
background-color: #fff;
// padding: 15px;
}
.queryBuilderLayoutSub {
padding: 20px;
}
.qbTabHeading {
font-weight: bold;
padding-top: 15px;
padding-left: 20px;
padding-bottom: 15px;
i {
padding-left: 5px;
}
}
.common_checkbox {
opacity: 0;
position: absolute;
.common_checkbox_label {
display: inline-block;
vertical-align: middle;
margin: 0px;
cursor: pointer;
position: relative;
}
}
.common_checkbox_label {
margin-bottom: 0px !important;
padding-top: 5px;
}
.common_checkbox + .common_checkbox_label:before {
content: '';
background: #fff;
border: 1px solid #ddd;
display: inline-block;
vertical-align: middle;
width: 20px;
height: 20px;
padding-top: 2px;
margin-right: 2px;
text-align: center;
border-radius:4px;
cursor:pointer;
}
label
{
font-weight: normal;
}
.common_checkbox:checked + .common_checkbox_label:before {
content: url('./tick.png');
background: #FFCA27;
color: #fff;
padding-top: 4px;
}
.authPanelSubHeadings {
font-size: 16px;
font-weight: italic;
padding-left: 15px;
padding-bottom: 10px;
}
.apiResponseTab {
padding-top: 20px;
.apiResponseTabUl {
display: inline-block;
-webkit-padding-start: 0px;
-webkit-box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.1);
-moz-box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.1);
box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.1);
.apiResponseTabList {
display: inline-block;
padding: 15px 25px;
list-style-type: none;
background-color: #fff;
font-weight: 600;
color: #6B6B6B;
cursor: pointer;
}
.apiResponseTabList:focus {
outline: none;
}
.activeApiResponseTab {
background-color: #FFF3D5
}
}
.apiResponseTabPanel {
.AceEditorWrapper {
margin-top: 15px;
margin-bottom: 15px;
}
}
}

View File

@ -0,0 +1,13 @@
import ApiExplorer from './ApiExplorer';
const generatedApiExplorer = connect => {
const mapStateToProps = state => {
return {
...state.apiexplorer,
credentials: {},
};
};
return connect(mapStateToProps)(ApiExplorer);
};
export default generatedApiExplorer;

View File

@ -0,0 +1,322 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { push } from 'react-router-redux';
import {
changeRequestParams,
addRequestHeader,
changeRequestHeader,
removeRequestHeader,
updateFileObject,
focusHeaderTextbox,
unfocusTypingHeader,
} from './Actions';
import GraphiQLWrapper from './GraphiQLWrapper';
const styles = require('./ApiExplorer.scss');
class ApiRequest extends Component {
constructor(props) {
super(props);
this.state = {};
this.state.accessKeyVisible = false;
this.state.bodyAllowedMethods = ['POST'];
this.state.tabIndex = 0;
}
onRequestParamsChanged = newValue => {
this.props.dispatch(changeRequestParams(newValue));
};
onHeaderValueChanged(e) {
const index = parseInt(e.target.getAttribute('data-header-id'), 10);
const key = e.target.getAttribute('data-element-name');
const newValue = e.target.value;
this.props.dispatch(changeRequestHeader(index, key, newValue, false));
}
onDeleteHeaderClicked(e) {
const index = parseInt(e.target.getAttribute('data-header-id'), 10);
this.props.dispatch(removeRequestHeader(index));
}
onNewHeaderKeyChanged(e) {
this.handleTypingTimeouts();
this.props.dispatch(addRequestHeader(e.target.value, ''));
}
onNewHeaderValueChanged(e) {
this.handleTypingTimeouts();
this.props.dispatch(addRequestHeader('', e.target.value));
}
onKeyUpAtNewHeaderField(e) {
if (e.keyCode === 13) {
this.props.dispatch(
addRequestHeader(this.state.newHeader.key, this.state.newHeader.value)
);
}
}
getUrlBar() {
return (
<div
id="stickyHeader"
className={
styles.apiPostRequestWrapper +
' ' +
styles.wd100 +
' ' +
styles.stickyHeader
}
>
<div className={'col-xs-12 ' + styles.padd_remove}>
<div
className={
'input-group ' +
styles.inputGroupWrapper
}
>
<div
className={
'input-group-btn ' +
styles.inputGroupBtn
}
>
<button type="button" className={'btn btn-default'}>
POST
</button>
</div>
<input
defaultValue={this.props.url}
readOnly
type="text"
className={
styles.inputGroupInput +
' form-control '
}
/>
</div>
</div>
<div className={styles.stickySeparator} />
</div>
);
}
getHeaderTitleView() {
return (
<div className={styles.responseWrapper}>
<div className={'col-xs-12 ' + styles.padd_remove}>
<div className={styles.responseHeader}>Request Headers</div>
</div>
</div>
);
}
getHeaderRows() {
const rows = this.props.headers.map((header, i) => {
return (
<tr key={i}>
{header.isNewHeader ? null : (
<td>
<input
type="checkbox"
name="sponsored"
className={styles.common_checkbox + ' common_checkbox'}
id={i + 1}
checked={header.isActive}
data-header-id={i}
onChange={this.onHeaderValueChanged.bind(this)}
data-element-name="isActive"
/>
<label
htmlFor={i + 1}
className={
styles.common_checkbox_label + ' common_checkbox_label'
}
/>
</td>
)}
<td
colSpan={header.isNewHeader ? '2' : '1'}
className={
header.isNewHeader
? styles.border_right +
' ' +
styles.tableTdLeft +
' ' +
styles.borderTop +
' ' +
styles.tableEnterKey
: styles.border_right
}
>
<input
className={'form-control ' + styles.responseTableInput}
value={header.key}
disabled={header.isDisabled === true ? true : false}
data-header-id={i}
placeholder="Enter Key"
data-element-name="key"
onChange={this.onHeaderValueChanged.bind(this)}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
type="text"
data-test={`header-key-${i}`}
/>
</td>
<td
colSpan={header.isNewHeader ? '2' : '1'}
className={
header.isNewHeader
? styles.borderTop +
' ' +
styles.tableEnterKey +
' ' +
styles.tableLastTd
: ''
}
>
<input
className={'form-control ' + styles.responseTableInput}
value={header.value}
disabled={header.isDisabled === true ? true : false}
data-header-id={i}
placeholder="Enter Value"
data-element-name="value"
onChange={this.onHeaderValueChanged.bind(this)}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
data-test={`header-value-${i}`}
type={
header.key === 'X-Hasura-Admin-Secret' &&
!this.state.accessKeyVisible
? 'password'
: 'text'
}
/>
</td>
{header.isNewHeader ? null : (
<td>
{header.key === 'X-Hasura-Admin-Secret' ? (
<i
className={styles.showAccessKey + ' fa fa-eye'}
data-header-id={i}
aria-hidden="true"
onClick={this.onShowAccessKeyClicked.bind(this)}
/>
) : null}
<i
className={styles.closeHeader + ' fa fa-times'}
data-header-id={i}
aria-hidden="true"
onClick={this.onDeleteHeaderClicked.bind(this)}
/>
</td>
)}
</tr>
);
});
return rows;
}
getHeaderTableView() {
return (
<div className={styles.responseTable}>
<table className={'table ' + styles.tableBorder}>
<thead>
<tr>
<th className={styles.wd4 + ' ' + styles.headerHeading} />
<th
className={
styles.wd48 +
' ' +
styles.border_right +
' ' +
styles.headerHeading
}
>
Key
</th>
<th className={styles.wd48 + ' ' + styles.headerHeading}>
Value
</th>
<th className={styles.wd4 + ' ' + styles.headerHeading} />
</tr>
</thead>
<tbody>{this.getHeaderRows()}</tbody>
</table>
</div>
);
}
getHeaderBody() {
return (
<div className={styles.responseHeader + ' ' + styles.marginBottom}>
Request Body
</div>
);
}
getValidBody() {
switch (this.props.bodyType) {
case 'graphql':
return (
<GraphiQLWrapper
data={this.props}
dispatch={this.props.dispatch}
headerFocus={this.props.headerFocus}
queryParams={this.props.queryParams}
/>
);
default:
return '';
}
}
changeEndpoint() {
this.props.dispatch(push('/'));
}
handleFocus = () => {
this.props.dispatch(focusHeaderTextbox());
};
handleBlur = () => {
this.props.dispatch(unfocusTypingHeader());
};
handleFileChange(e) {
if (e.target.files.length > 0) {
this.props.dispatch(updateFileObject(e.target.files[0]));
}
}
render() {
return (
<div className={styles.apiRequestWrapper}>
{this.getUrlBar()}
<hr />
{this.getHeaderTitleView()}
{this.getHeaderTableView()}
{this.getValidBody()}
</div>
);
}
}
ApiRequest.propTypes = {
method: PropTypes.string.isRequired,
url: PropTypes.string.isRequired,
headers: PropTypes.array,
params: PropTypes.string,
dispatch: PropTypes.func.isRequired,
explorerData: PropTypes.object.isRequired,
credentials: PropTypes.object.isRequired,
bodyType: PropTypes.string.isRequired,
route: PropTypes.object.isRequired,
numberOfTables: PropTypes.number.isRequired,
headerFocus: PropTypes.bool.isRequired,
queryParams: PropTypes.object.isRequired,
};
export default ApiRequest;

View File

@ -0,0 +1,76 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class ApiRequestDetails extends Component {
login() {
this.props.auth.login();
}
logout() {
this.props.auth.logout();
}
render() {
const { isAuthenticated } = this.props.auth;
const styles = require('./ApiExplorer.scss');
const loginButton = (
<button
id="qsLoginBtn"
className={'btn btn-primary'}
onClick={this.login.bind(this)}
>
Log In
</button>
);
const logoutButton = (
<button
id="qsLogoutBtn"
className={'btn btn-danger'}
onClick={this.logout.bind(this)}
>
Log Out
</button>
);
const loginOverlay = (
<div className={styles.overlay}>
<div className={styles.overlayContent}>
<div className={styles.overlayHeading}>
GraphiQL
</div>
<div className={styles.overlayMessage}>
Login to explore your GraphQL APIs securely.
</div>
<div className={styles.overlayAction}>
{loginButton}
</div>
</div>
</div>
);
return (
<div>
{ isAuthenticated() || loginOverlay }
<div className={styles.apiRequestWrapper + ' ' + styles.apiContentPadd}>
<div className={styles.apiRequestContent}>{this.props.description}</div>
<div className={styles.authBtnWrapper}>
{ isAuthenticated() ? logoutButton : loginButton }
</div>
</div>
</div>
);
}
}
ApiRequestDetails.propTypes = {
title: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
};
export default ApiRequestDetails;

View File

@ -0,0 +1,67 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import ApiRequest from './ApiRequest';
import ApiRequestDetails from './ApiRequestDetails';
class ApiRequestWrapper extends Component {
render() {
const styles = require('./ApiExplorer.scss');
return (
<div
id="apiRequestBlock"
className={
this.props.wdStyles +
' ' +
styles.padd_left +
' ' +
styles.padd_right +
' ' +
styles.ApiRequestWrapperVH +
' ' +
this.props.requestStyles
}
>
<ApiRequestDetails
title={this.props.details.title}
description={this.props.details.description}
auth={this.props.auth}
/>
<ApiRequest
bodyType={
this.props.request.bodyType ? this.props.request.bodyType : ''
}
credentials={this.props.credentials}
method={this.props.request.method}
url={this.props.graphqlEndpoint}
headers={this.props.request.headers}
params={this.props.request.params}
explorerData={this.props.explorerData}
dispatch={this.props.dispatch}
dataHeaders={this.props.dataHeaders}
numberOfTables={this.props.numberOfTables}
headerFocus={this.props.headerFocus}
queryParams={this.props.queryParams}
auth={this.props.auth}
/>
</div>
);
}
}
ApiRequestWrapper.propTypes = {
details: PropTypes.object.isRequired,
request: PropTypes.object.isRequired,
explorerData: PropTypes.object.isRequired,
credentials: PropTypes.object.isRequired,
bodyType: PropTypes.string,
showHelpBulb: PropTypes.bool,
requestStyles: PropTypes.string,
wdStyles: PropTypes.string,
dispatch: PropTypes.func,
numberOfTables: PropTypes.number,
headerFocus: PropTypes.bool.isRequired,
graphqlEndpoint: PropTypes.string,
queryParams: PropTypes.object.isRequired,
};
export default ApiRequestWrapper;

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="-216 218 66 66" style="enable-background:new -216 218 66 66;" xml:space="preserve">
<style type="text/css">
.st0{fill:#97A9FF;}
.st1{fill:#FFFFFF;}
</style>
<g id="Layer_1">
<title>Discord</title>
<desc>Created with Sketch.</desc>
<g id="Page-1">
<g id="Hasura_Jun29-Update" transform="translate(-1157.000000, -872.000000)">
<g id="Group-6" transform="translate(1066.000000, 872.000000)">
<g id="Discord" transform="translate(91.000000, 0.000000)">
<g>
<g id="Twitter-Copy">
<path id="back" class="st0" d="M-216,251c0-18.2,14.8-33,33-33s33,14.8,33,33s-14.8,33-33,33S-216,269.2-216,251z"/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g id="Layer_2">
<g>
<path class="st1" d="M-172.5,266.6l-2.5-2.9c4.8-1.3,6.7-4.6,6.7-4.6c-6.1,5.7-22.6,6-29.1,0c0,0,1.6,3,6.5,4.6l-2.7,2.9
c-8.2-0.1-11.3-5.6-11.3-5.6c0-12.1,5.4-21.8,5.4-21.8c5.5-3.9,10.5-3.9,10.5-3.9l0.4,0.5c-6.8,1.9-9.7,4.9-9.7,4.9
c7.9-5.4,24.3-4.9,30.7,0c0.1-0.1-3.9-3.3-10-4.9l0.6-0.5c0,0,5,0,10.5,3.9c0,0,5.4,9.7,5.4,21.8
C-161.2,261-164.3,266.5-172.5,266.6z"/>
<ellipse class="st0" cx="-190" cy="253.5" rx="3.9" ry="4.1"/>
<ellipse class="st0" cx="-176.5" cy="253.5" rx="3.9" ry="4.1"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="8" viewBox="0 0 13 8"><path fill="none" stroke="#4D4D4D" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.6" opacity=".7" transform="rotate(90 5 6.5)" d="M0 10L5 5 0 0"/></svg>

After

Width:  |  Height:  |  Size: 249 B

View File

@ -0,0 +1,29 @@
import React from 'react';
import PropTypes from 'prop-types';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, info: null };
}
componentDidCatch(error, info) {
this.setState({ hasError: true, info: info });
// most likely a localstorage issue
window.localStorage.clear();
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return <div>{this.props.children}</div>;
}
return this.props.children;
}
}
ErrorBoundary.propTypes = {
children: PropTypes.element,
};
export default ErrorBoundary;

View File

@ -0,0 +1,149 @@
import React, { Component } from 'react';
import GraphiQL from 'hasura-console-graphiql';
import { getHeadersAsJSON } from './utils';
import { getIntrospectionQuery, buildClientSchema } from 'graphql';
import PropTypes from 'prop-types';
import ErrorBoundary from './ErrorBoundary';
import { graphQLFetcherFinal, getRemoteQueries } from './Actions';
import GraphiQLExplorer from 'graphiql-explorer';
import { makeDefaultArg, getDefaultScalarArgValue } from './onegraphUtils';
import './GraphiQL.css';
class GraphiQLWrapper extends Component {
constructor(props) {
super(props);
this.state = {
schema: null,
error: false,
queries: null,
onBoardingEnabled: false,
explorerIsOpen: true,
query: ''
};
const queryFile = this.props.queryParams
? this.props.queryParams.query_file
: null;
if (queryFile) {
getRemoteQueries(queryFile, queries =>
this.setState({ ...this.state, queries })
);
}
}
_handleEditQuery = (query: string): void => this.setState({query});
_handleToggleExplorer = () => {
this.setState({explorerIsOpen: !this.state.explorerIsOpen});
}
shouldComponentUpdate(nextProps) {
return !nextProps.headerFocus;
}
introspect() {
const { url, headers } = this.props.data;
fetch(url, {
method: 'POST',
headers: getHeadersAsJSON(headers || []),
body: JSON.stringify({
query: getIntrospectionQuery(),
}),
})
.then(response => response.json())
.then(result => {
this.setState({
schema: buildClientSchema(result.data)
});
})
.catch(error => {
this.setState({
schema: null
});
})
}
componentDidMount() {
this.introspect();
}
render() {
const styles = require('../Common/Common.scss');
const { variables, query } = window.__env;
const graphQLFetcher = graphQLParams => {
return graphQLFetcherFinal(
graphQLParams,
this.props.data.url,
this.props.data.headers
);
};
const graphiqlProps = {
fetcher: graphQLFetcher,
};
if (query || Object.keys(variables).length !== 0) {
graphiqlProps.query = query;
if (variables !== 'undefined') {
graphiqlProps.variables = JSON.stringify(variables, null, 2);
}
} else if (this.state.queries) {
graphiqlProps.query = this.state.queries;
}
return (
<ErrorBoundary>
<div
className={
'react-container-graphql ' +
styles.wd100 +
' ' +
styles.graphQLHeight
}
>
<div className="graphiql-container">
<GraphiQLExplorer
schema={this.state.schema}
query={this.state.query}
onEdit={this._handleEditQuery}
explorerIsOpen={this.state.explorerIsOpen}
onToggleExplorer={this._handleToggleExplorer}
getDefaultScalarArgValue={getDefaultScalarArgValue}
makeDefaultArg={makeDefaultArg}
/>
<GraphiQL {...graphiqlProps}
ref={ref => (this._graphiql = ref)}
schema={this.state.schema}
query={this.state.query}
onEditQuery={this._handleEditQuery}
>
<GraphiQL.Toolbar>
<GraphiQL.Button
onClick={() => this._graphiql.handlePrettifyQuery()}
label="Prettify"
title="Prettify Query (Shift-Ctrl-P)"
/>
<GraphiQL.Button
onClick={() => this._graphiql.handleToggleHistory()}
label="History"
title="Show History"
/>
<GraphiQL.Button
onClick={this._handleToggleExplorer}
label="Explorer"
title="Toggle Explorer"
/>
</GraphiQL.Toolbar>
</GraphiQL>
</div>
</div>
</ErrorBoundary>
);
}
}
GraphiQLWrapper.propTypes = {
dispatch: PropTypes.func.isRequired,
data: PropTypes.object.isRequired,
headerFocus: PropTypes.bool.isRequired,
};
export default GraphiQLWrapper;

View File

@ -0,0 +1,351 @@
@import url('https://fonts.googleapis.com/css?family=Raleway:300,400,600');
@import url('https://fonts.googleapis.com/css?family=Open+Sans:400,600');
body
{
font-size: 16px;
font-family: 'Open Sans'
}
.wd100
{
width: 100%;
float: left;
}
.displayFlex
{
display: flex;
align-items: center;
}
.boxShadow
{
-webkit-box-shadow: 0px 0px 20px 0px rgba(0, 0, 0, 0.3);
-moz-box-shadow: 0px 0px 20px 0px rgba(0, 0, 0, 0.3);
box-shadow: 0px 0px 20px 0px rgba(0, 0, 0, 0.3);
}
.commonBtn
{
text-align: center;
padding: 25px 0;
clear: both;
button
{
border-radius: 25px;
background-color: #4767a1;
padding: 15px 30px;
border: 0;
color: #fff;
font-family: 'Raleway';
font-size: 14px;
font-weight: 600;
letter-spacing: 2px;
}
button:hover
{
background-color: #3b5f93;
}
button:focus
{
outline: none;
}
}
.lightGrayBgColor
{
background-color: #f7f7f7;
}
.blueBgColor
{
background-color: #303b4e;
}
.pageHeader
{
font-family: 'Raleway';
font-weight: 600;
font-size: 44px;
color: #4D4D4D;
}
.pageSubHeader
{
font-family: 'Raleway';
font-weight: 600;
font-size: 28px;
text-align: center;
color: #102261;
}
.pageDescription
{
text-align: center;
color: #565656;
padding-bottom: 15px;
font-size: 16px;
line-height: 1.7;
font-family: 'Open Sans';
a
{
color: #f93c18;
}
a:hover
{
color: #e0270e;
text-decoration: none;
}
}
.pageDescriptionSmall
{
text-align: center;
color: #f93c18;
font-size: 14px;
font-weight: 600;
padding-bottom: 10px;
font-family: 'Open Sans';
a
{
color: #f93c18;
i
{
color: #f93c18;
}
}
a:hover
{
text-decoration: none;
}
}
.noPadd
{
padding-left: 0;
padding-right: 0;
}
.commonSectionWrapper
{
padding: 75px 0;
}
.powerfulGraphQLWrapper
{
display: flex;
align-items: center;
.pageSubHeader
{
padding-bottom: 30px;
}
.pageSubHeader, .pageDescription
{
text-align: left;
padding-right: 50px;
}
.powerfulGraphQLImg
{
text-align: right;
img
{
width: 95%;
display: inline-block;
}
}
}
.easyStepsWrapper
{
.pageSubHeader
{
padding-bottom: 30px;
}
.stepsListWrapper
{
.stepsList
{
.stepsIcon
{
text-align: center;
padding-bottom: 20px;
img
{
max-height: 60px;
display: inline-block;
}
}
.pageDescription
{
padding: 0 5px;
padding-bottom: 20px;
}
}
}
.easyStepsImg
{
background-color: #3edad7;
border-radius: 5px;
text-align: center;
padding: 30px;
width: 90%;
margin: 0 0 0 auto;
img
{
width: 65px;
display: inline-block;
}
.pageDescription
{
padding-top: 20px;
color: #fff;
font-size: 24px;
font-weight: 600;
font-family: 'Raleway';
}
}
}
.footerWrapper
{
padding: 20px 0px;
.socialWrapper
{
text-align: center;
.socialIcon
{
display: inline-block;
padding: 0px 10px;
img
{
width: 50px;
display: inline-block;
opacity: 0.7;
}
img:hover
{
opacity: 1;
}
}
}
.pageDescription
{
color: #fff;
i
{
color: #f93c18;
}
a
{
color: #fff;
}
a:hover
{
color: #f93c18;
}
}
.pageDescriptionSmall
{
padding-top: 20px;
color: #fff;
font-size: 12px;
font-weight: 400;
}
}
@media (max-width: 767px)
{
.pageHeader
{
font-size: 30px;
text-align: center;
padding-bottom: 20px;
}
.pageSubHeader
{
font-size: 22px !important;
text-align: center;
}
.pageDescription
{
font-size: 14px;
text-align: center;
}
.pageDescriptionSmall
{
font-size: 13px;
text-align: center;
}
.commonSectionWrapper
{
padding: 30px 0;
}
.displayFlex
{
display: block;
}
.powerfulGraphQLWrapper
{
display: block;
.pageSubHeader, .pageDescription
{
text-align: center;
padding-right: 0;
padding: 10px 15px;
}
.powerfulGraphQLImg
{
text-align: center;
padding-top: 20px;
img
{
width: 90%;
}
}
}
.easyStepsWrapper
{
.stepsListWrapper
{
.stepsList
{
padding-bottom: 0px;
.stepsIcon
{
padding-bottom: 10px;
}
}
}
.easyStepsImg
{
width: 95%;
margin: 0 auto;
}
}
.footerWrapper
{
.pageDescriptionSmall
{
padding: 0px 10px;
padding-top: 15px;
}
}
}
@media (min-width: 768px) and (max-width: 991px)
{
.commonSectionWrapper
{
padding: 30px 0;
}
.pageHeader
{
font-size: 35px;
text-align: center;
padding-bottom: 20px;
}
.easyStepsWrapper
{
.stepsListWrapper
{
.stepsList
{
padding-bottom: 30px;
.stepsIcon
{
padding-bottom: 10px;
}
}
}
.easyStepsImg
{
width: 100%;
margin: 0 auto;
padding: 15px;
}
}
}

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="66px" height="66px" viewBox="0 0 66 66" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 49.3 (51167) - http://www.bohemiancoding.com/sketch -->
<title>Twitter</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Hasura_Jun29-Update" transform="translate(-1066.000000, -872.000000)">
<g id="Group-6" transform="translate(1066.000000, 872.000000)">
<g id="Twitter">
<g>
<path d="M0,33 C0,14.7746024 14.7746024,0 33,0 C51.2253976,0 66,14.7746024 66,33 C66,51.2253976 51.2253976,66 33,66 C14.7746024,66 0,51.2253976 0,33 Z" id="back" fill="#55ACEE"></path>
<path d="M32.5362646,26.7393379 L32.6058642,27.8966431 L31.4458703,27.7549322 C27.2234923,27.2117073 23.5347116,25.3694664 20.4027279,22.2754464 L18.8715359,20.7402457 L18.477138,21.8739324 C17.6419423,24.401109 18.1755395,27.0699965 19.9155304,28.8650005 C20.8435256,29.8569763 20.6347267,29.9986872 19.033935,29.4082253 C18.477138,29.2192775 17.9899405,29.0775667 17.9435407,29.1484221 C17.7811416,29.3137514 18.3379387,31.4630325 18.7787364,32.3132975 C19.3819332,33.4942212 20.6115268,34.6515264 21.9571198,35.3364621 L23.0939139,35.879687 L21.7483209,35.9033055 C20.4491277,35.9033055 20.4027279,35.9269239 20.5419272,36.4229119 C21.0059248,37.9581126 22.8387152,39.5877873 24.8803045,40.2963415 L26.318697,40.7923294 L25.0659036,41.5481205 C23.2099133,42.6345703 21.0291246,43.2486506 18.848336,43.2958876 C17.8043415,43.319506 16.9459459,43.4139799 16.9459459,43.4848353 C16.9459459,43.7210201 19.7763312,45.0436546 21.4235226,45.563261 C26.3650968,47.0984617 32.2346662,46.4371445 36.6426431,43.815494 C39.7746268,41.9496346 42.9066105,38.2415343 44.3682028,34.6515264 C45.1569987,32.7384301 45.9457946,29.242896 45.9457946,27.5659844 C45.9457946,26.4795347 46.0153942,26.3378238 47.3145874,25.0388078 C48.0801834,24.2830167 48.7993797,23.4563701 48.938579,23.2201854 C49.1705778,22.7714344 49.1473779,22.7714344 47.9641841,23.1729484 C45.9921943,23.8815026 45.7137958,23.7870287 46.6881907,22.7241974 C47.407387,21.9684063 48.2657825,20.5985348 48.2657825,20.1970208 C48.2657825,20.1261654 47.9177843,20.2442577 47.5233864,20.456824 C47.1057885,20.6930087 46.1777934,21.0472858 45.481797,21.2598521 L44.2290035,21.6613661 L43.0922095,20.8819565 C42.4658128,20.456824 41.5842174,19.9844545 41.1202198,19.8427437 C39.937026,19.5120851 38.1274354,19.559322 37.060241,19.9372176 C34.1602561,21.0000489 32.3274657,23.7397918 32.5362646,26.7393379 Z" id="Shape" fill="#FFFFFF"></path>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="7" height="14" viewBox="0 0 7 14"><path fill="#788095" d="M.535.5L6.465 7l-5.93 6.5z"/></svg>

After

Width:  |  Height:  |  Size: 140 B

View File

@ -0,0 +1,21 @@
import GraphiQLExplorer from 'graphiql-explorer';
export const makeDefaultArg = () => {
return false;
};
export const getDefaultScalarArgValue = (parentField, arg, argType) => {
return GraphiQLExplorer.defaultValue(argType);
};
export const getExplorerWidthFromLocalStorage = () => {
const val = parseInt(
window.localStorage.getItem('graphiql:explorerWidth'),
10
);
return isNaN(val) ? 350 : val;
};
export const setExplorerWidthInLocalStorage = width => {
localStorage.setItem('graphiql:explorerWidth', width);
};

View File

@ -0,0 +1,92 @@
const existingHeaders = window.__env.headers;
const authToken = window.localStorage.getItem('auth0:id_token');
const defaultHeader = [
{
key: 'Content-Type',
value: 'application/json',
isActive: true,
isNewHeader: false,
isDisabled: true,
},
];
if (existingHeaders) {
Object.keys(existingHeaders).forEach((key) => {
defaultHeader.push({
key,
value: existingHeaders[key],
isActive: true,
isNewHeader: false,
})
});
}
if (authToken) {
defaultHeader.push({
key: 'Authorization',
value: 'Bearer ' + authToken,
isActive: true,
isNewHeader: false,
isDisabled: true
})
}
defaultHeader.push({
key: '',
value: '',
isActive: false,
isNewHeader: true,
});
const getUrl = path => {
return `${window.__env.graphqlEndpoint}`;
};
const dataApisContent = [];
// check project version
dataApisContent.push({
id: 'DataApi-3',
details: {
title: '',
description:
'Explore GraphQL APIs with headers',
category: 'data',
},
request: {
method: 'POST',
url: getUrl('/v1alpha1/graphql'),
headers: defaultHeader,
bodyType: 'graphql',
params: JSON.stringify({}, null, 4),
},
});
const dataApis = {
title: 'Data',
content: dataApisContent,
};
const explorerData = {
sendingRequest: false,
enableResponseSection: false,
response: {},
fileObj: null,
};
const defaultApi = dataApis.content[0];
const defaultState = {
currentTab: 0,
displayedApi: defaultApi,
modalState: {
isOpen: false,
isCopied: false,
},
explorerData,
authApiExpanded: 'Username-password Login',
headerFocus: false,
graphqlEndpoint: ''
};
export default defaultState;
export { defaultApi, defaultHeader };

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

View File

@ -0,0 +1,12 @@
const getHeadersAsJSON = headers => {
const headerJSON = {};
const nonEmptyHeaders = headers.filter(header => {
return (header.key || header.value) && header.isActive;
});
nonEmptyHeaders.forEach(header => {
headerJSON[header.key] = header.value;
});
return headerJSON;
};
export { getHeadersAsJSON };

View File

@ -0,0 +1,98 @@
import defaultState from './State';
const LOAD_REQUEST = 'App/ONGOING_REQUEST';
const DONE_REQUEST = 'App/DONE_REQUEST';
const FAILED_REQUEST = 'App/FAILED_REQUEST';
const ERROR_REQUEST = 'App/ERROR_REQUEST';
const CONNECTION_FAILED = 'App/CONNECTION_FAILED';
const CLOSE_MODAL = 'App/CLOSE_MODAL';
const NOTIF_EXPANDED = 'App/NOTIF_EXPANDED';
const NOTIF_MSG = 'App/NOTIF_MSG';
const notifExpand = isExpanded => ({ type: NOTIF_EXPANDED, data: isExpanded });
const notifMsg = finalMsg => ({ type: NOTIF_MSG, data: finalMsg });
const progressBarReducer = (state = defaultState, action) => {
switch (action.type) {
case LOAD_REQUEST:
return {
...state,
ongoingRequest: true,
percent: 10,
requestSuccess: null,
requestError: null,
connectionFailed: false,
};
case DONE_REQUEST:
return {
...state,
percent: 100,
ongoingRequest: false,
requestSuccess: true,
requestError: null,
connectionFailed: false,
};
case FAILED_REQUEST:
return {
...state,
percent: 100,
ongoingRequest: false,
requestSuccess: null,
requestError: true,
connectionFailed: false,
};
case ERROR_REQUEST:
return {
...state,
modalOpen: true,
error: action.data,
reqURL: action.url,
reqData: action.params,
statusCode: action.statusCode,
connectionFailed: false,
};
case CONNECTION_FAILED:
return {
...state,
modalOpen: true,
error: true,
connectionFailed: true,
};
case CLOSE_MODAL:
return {
...state,
modalOpen: false,
};
case NOTIF_EXPANDED:
return {
...state,
isNotifExpanded: action.data,
};
case NOTIF_MSG:
return {
...state,
notifMsg: action.data,
};
default:
return state;
}
};
export default progressBarReducer;
export {
LOAD_REQUEST,
DONE_REQUEST,
FAILED_REQUEST,
ERROR_REQUEST,
CLOSE_MODAL,
CONNECTION_FAILED,
NOTIF_EXPANDED,
notifExpand,
notifMsg,
};

View File

@ -0,0 +1,75 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import ProgressBar from 'react-progress-bar-plus';
import './progress-bar.scss';
import { NOTIF_EXPANDED } from './Actions';
import ErrorBoundary from './ErrorBoundary';
class App extends Component {
componentDidMount() {
// Hide the loader once the react component is ready.
// NOTE: This will execute only onces (since this is parent component for all other component).
const className = document.getElementById('content').className;
document.getElementById('content').className = className + ' show';
document.getElementById('loading').style.display = 'none';
}
onModalClose = () => {
this.props.dispatch({ type: NOTIF_EXPANDED, data: false });
};
render() {
const {
ongoingRequest,
percent,
intervalTime,
children,
} = this.props;
return (
<ErrorBoundary>
<div>
{ongoingRequest && (
<ProgressBar
percent={percent}
autoIncrement={true} // eslint-disable-line react/jsx-boolean-value
intervalTime={intervalTime}
spinner={false}
/>
)}
<div>{children}</div>
</div>
</ErrorBoundary>
);
}
}
App.propTypes = {
reqURL: PropTypes.string,
reqData: PropTypes.object,
statusCode: PropTypes.number,
modalOpen: PropTypes.bool,
error: PropTypes.object,
ongoingRequest: PropTypes.bool,
requestError: PropTypes.bool,
connectionFailed: PropTypes.bool,
intervalTime: PropTypes.number,
percent: PropTypes.number,
children: PropTypes.element,
dispatch: PropTypes.func.isRequired,
isNotifExpanded: PropTypes.bool,
notifMsg: PropTypes.string,
};
const mapStateToProps = state => {
return {
...state.progressBar,
};
};
export default connect(mapStateToProps)(App);

View File

@ -0,0 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, info: null };
}
componentDidCatch(error, info) {
// Display fallback UI
this.setState({ hasError: true, info: info });
// You can also log the error to an error reporting service
// logErrorToMyService(error, info);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return <div>Something went wrong</div>;
}
return this.props.children;
}
}
ErrorBoundary.propTypes = {
children: PropTypes.element,
};
export default ErrorBoundary;

View File

@ -0,0 +1,12 @@
const defaultState = {
percent: 0,
intervalTime: 200,
ongoingRequest: false,
requestSuccess: null,
requestError: null,
modalOpen: false,
error: null,
isNotifExpanded: false,
notifMsg: null,
};
export default defaultState;

View File

@ -0,0 +1,79 @@
.alertDanger
{
position: sticky;
position: -webkit-sticky;
position: -moz-sticky;
position: -o-sticky;
position: -ms-sticky;
top: 0;
z-index: 1000;
}
.notifModalDialog {
width: 800px;
margin: 30px auto;
right: 100px;
top: -15px;
}
:global {
@keyframes react-progress-spinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.react-progress-bar {
position: fixed;
top: 0;
left: 0;
width: 100%;
visibility: visible;
opacity: 1;
transition: all 400ms;
z-index: 9999;
&.react-progress-bar-on-top {
height: 100%;
}
&.react-progress-bar-hide {
opacity: 0;
visibility: hidden;
z-index: -10;
}
}
.react-progress-bar-percent {
height: 2px;
background: #e8694d;
box-shadow: 0 2px 5px #d3291c, 0 2px 5px #d3291c;
transition: all 200ms ease;
}
.react-progress-bar-spinner {
display: block;
position: fixed;
top: 15px;
}
.react-progress-bar-spinner-left {
left: 15px;
right: auto;
}
.react-progress-bar-spinner-right {
left: auto;
right: 15px;
}
.react-progress-bar-spinner-icon {
width: 18px;
height: 18px;
box-sizing: border-box;
border: solid 2px transparent;
border-top-color: #d3291c;
border-left-color: #d3291c;
border-radius: 50%;
animation: react-progress-spinner 400ms linear infinite;
}
}

View File

@ -0,0 +1,66 @@
import auth0 from "auth0-js";
import { AUTH_CONFIG } from "./auth0-variables";
export default class Auth {
auth0 = new auth0.WebAuth({
domain: AUTH_CONFIG.domain,
clientID: AUTH_CONFIG.clientId,
redirectUri: AUTH_CONFIG.callbackUrl,
audience: `https://${AUTH_CONFIG.domain}/userinfo`,
responseType: "token id_token",
scope: "openid profile"
});
constructor() {
this.login = this.login.bind(this);
this.logout = this.logout.bind(this);
this.handleAuthentication = this.handleAuthentication.bind(this);
this.isAuthenticated = this.isAuthenticated.bind(this);
}
login() {
this.auth0.authorize();
}
handleAuthentication = client => {
this.auth0.parseHash((err, authResult) => {
if (authResult && authResult.accessToken && authResult.idToken) {
this.setSession(authResult);
} else if (err) {
console.error(err);
window.location.replace("/graphql/graphiql");
alert(`Error: ${err.error}. Check the console for further details.`);
}
});
};
setSession(authResult) {
// Set the time that the access token will expire at
let expiresAt = JSON.stringify(
authResult.expiresIn * 1000 + new Date().getTime()
);
localStorage.setItem("auth0:access_token", authResult.accessToken);
localStorage.setItem("auth0:id_token", authResult.idToken);
localStorage.setItem("auth0:expires_at", expiresAt);
localStorage.setItem("auth0:id_token:sub", authResult.idTokenPayload.sub);
// navigate to the home route
window.location.replace("/graphql/graphiql");
}
logout() {
// Clear access token and ID token from local storage
localStorage.removeItem("auth0:access_token");
localStorage.removeItem("auth0:id_token");
localStorage.removeItem("auth0:expires_at");
localStorage.removeItem("auth0:id_token:sub");
// navigate to the home route
// history.replace("/home");
window.location.replace("/graphql/graphiql");
}
isAuthenticated() {
// Check whether the current time is past the
// access token's expiry time
let expiresAt = JSON.parse(localStorage.getItem("auth0:expires_at"));
return new Date().getTime() < expiresAt;
}
}

View File

@ -0,0 +1,7 @@
import { authDomain, authClientId, callbackUrl } from "./constants";
export const AUTH_CONFIG = {
domain: authDomain,
clientId: authClientId,
callbackUrl: callbackUrl
};

View File

@ -0,0 +1,7 @@
export const GRAPHQL_URL =
"https://learn.hasura.io/graphql";
export const REALTIME_GRAPHQL_URL =
"wss://learn.hasura.io/graphql";
export const authClientId = "P38qnFo1lFAQJrzkun--wEzqljVNGcWW";
export const authDomain = "graphql-tutorials.auth0.com";
export const callbackUrl = window.__env.callbackUrl;

View File

@ -0,0 +1,3 @@
import createHistory from "history/createBrowserHistory";
export default createHistory();

View File

@ -0,0 +1,33 @@
import React, { Component } from "react";
import loading from "./loading.svg";
class Callback extends Component {
componentDidMount() {
console.log(this.props);
if (/access_token|id_token|error/.test(this.props.location.hash)) {
this.props.route.auth.handleAuthentication();
}
}
render() {
const style = {
position: "absolute",
display: "flex",
justifyContent: "center",
height: "100vh",
width: "100vw",
top: 0,
bottom: 0,
left: 0,
right: 0,
backgroundColor: "white"
};
return (
<div style={style}>
<img src={loading} alt="loading" />
</div>
);
}
}
export default Callback;

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="utf-8"?><svg width='120px' height='120px' xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" class="uil-ring"><rect x="0" y="0" width="100" height="100" fill="none" class="bk"></rect><defs><filter id="uil-ring-shadow" x="-100%" y="-100%" width="300%" height="300%"><feOffset result="offOut" in="SourceGraphic" dx="0" dy="0"></feOffset><feGaussianBlur result="blurOut" in="offOut" stdDeviation="0"></feGaussianBlur><feBlend in="SourceGraphic" in2="blurOut" mode="normal"></feBlend></filter></defs><path d="M10,50c0,0,0,0.5,0.1,1.4c0,0.5,0.1,1,0.2,1.7c0,0.3,0.1,0.7,0.1,1.1c0.1,0.4,0.1,0.8,0.2,1.2c0.2,0.8,0.3,1.8,0.5,2.8 c0.3,1,0.6,2.1,0.9,3.2c0.3,1.1,0.9,2.3,1.4,3.5c0.5,1.2,1.2,2.4,1.8,3.7c0.3,0.6,0.8,1.2,1.2,1.9c0.4,0.6,0.8,1.3,1.3,1.9 c1,1.2,1.9,2.6,3.1,3.7c2.2,2.5,5,4.7,7.9,6.7c3,2,6.5,3.4,10.1,4.6c3.6,1.1,7.5,1.5,11.2,1.6c4-0.1,7.7-0.6,11.3-1.6 c3.6-1.2,7-2.6,10-4.6c3-2,5.8-4.2,7.9-6.7c1.2-1.2,2.1-2.5,3.1-3.7c0.5-0.6,0.9-1.3,1.3-1.9c0.4-0.6,0.8-1.3,1.2-1.9 c0.6-1.3,1.3-2.5,1.8-3.7c0.5-1.2,1-2.4,1.4-3.5c0.3-1.1,0.6-2.2,0.9-3.2c0.2-1,0.4-1.9,0.5-2.8c0.1-0.4,0.1-0.8,0.2-1.2 c0-0.4,0.1-0.7,0.1-1.1c0.1-0.7,0.1-1.2,0.2-1.7C90,50.5,90,50,90,50s0,0.5,0,1.4c0,0.5,0,1,0,1.7c0,0.3,0,0.7,0,1.1 c0,0.4-0.1,0.8-0.1,1.2c-0.1,0.9-0.2,1.8-0.4,2.8c-0.2,1-0.5,2.1-0.7,3.3c-0.3,1.2-0.8,2.4-1.2,3.7c-0.2,0.7-0.5,1.3-0.8,1.9 c-0.3,0.7-0.6,1.3-0.9,2c-0.3,0.7-0.7,1.3-1.1,2c-0.4,0.7-0.7,1.4-1.2,2c-1,1.3-1.9,2.7-3.1,4c-2.2,2.7-5,5-8.1,7.1 c-0.8,0.5-1.6,1-2.4,1.5c-0.8,0.5-1.7,0.9-2.6,1.3L66,87.7l-1.4,0.5c-0.9,0.3-1.8,0.7-2.8,1c-3.8,1.1-7.9,1.7-11.8,1.8L47,90.8 c-1,0-2-0.2-3-0.3l-1.5-0.2l-0.7-0.1L41.1,90c-1-0.3-1.9-0.5-2.9-0.7c-0.9-0.3-1.9-0.7-2.8-1L34,87.7l-1.3-0.6 c-0.9-0.4-1.8-0.8-2.6-1.3c-0.8-0.5-1.6-1-2.4-1.5c-3.1-2.1-5.9-4.5-8.1-7.1c-1.2-1.2-2.1-2.7-3.1-4c-0.5-0.6-0.8-1.4-1.2-2 c-0.4-0.7-0.8-1.3-1.1-2c-0.3-0.7-0.6-1.3-0.9-2c-0.3-0.7-0.6-1.3-0.8-1.9c-0.4-1.3-0.9-2.5-1.2-3.7c-0.3-1.2-0.5-2.3-0.7-3.3 c-0.2-1-0.3-2-0.4-2.8c-0.1-0.4-0.1-0.8-0.1-1.2c0-0.4,0-0.7,0-1.1c0-0.7,0-1.2,0-1.7C10,50.5,10,50,10,50z" fill="#337ab7" filter="url(#uil-ring-shadow)"><animateTransform attributeName="transform" type="rotate" from="0 50 50" to="360 50 50" repeatCount="indefinite" dur="1s"></animateTransform></path></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,14 @@
import React, { Component } from 'react';
class BreadCrumb extends Component {
render() {
const styles = require('./BreadCrumb.scss');
return <div className={styles.breadcrumbWrapper}>{this.props.data}</div>;
}
}
BreadCrumb.propTypes = {
data: React.PropTypes.string.isRequired,
};
export default BreadCrumb;

View File

@ -0,0 +1,9 @@
@import "../Common.scss";
.breadcrumbWrapper {
padding: 20px 0px;
padding-bottom: 0px;
font-size: 16px;
font-weight: 400;
color: #767E93;
}

View File

@ -0,0 +1,37 @@
@import "./Common.scss";
button.control {
width: 100%;
}
.subTopic {
font-weight: bold;
}
.readyIcon {
color: green;
}
.failIcon {
color: red;
}
.eventContainer {
overflow: auto;
}
.logContainer {
overflow: auto;
}
table td.longOne {
max-width: 500px;
}
.endpoint {
padding-right: 5px;
}
.floatRight {
float: right;
margin-right: 20px;
}

View File

@ -0,0 +1,24 @@
import React from 'react';
const Spinner = () => {
const styles = require('./Spinner.scss');
return (
<div className={styles.sk_circle}>
<div className={styles.sk_circle1 + ' ' + styles.sk_child} />
<div className={styles.sk_circle2 + ' ' + styles.sk_child} />
<div className={styles.sk_circle3 + ' ' + styles.sk_child} />
<div className={styles.sk_circle4 + ' ' + styles.sk_child} />
<div className={styles.sk_circle5 + ' ' + styles.sk_child} />
<div className={styles.sk_circle6 + ' ' + styles.sk_child} />
<div className={styles.sk_circle7 + ' ' + styles.sk_child} />
<div className={styles.sk_circle8 + ' ' + styles.sk_child} />
<div className={styles.sk_circle9 + ' ' + styles.sk_child} />
<div className={styles.sk_circle10 + ' ' + styles.sk_child} />
<div className={styles.sk_circle11 + ' ' + styles.sk_child} />
<div className={styles.sk_circle12 + ' ' + styles.sk_child} />
</div>
);
};
export default Spinner;

View File

@ -0,0 +1,121 @@
.sk_circle {
margin: 100px auto;
width: 40px;
height: 40px;
position: relative;
}
.sk_circle .sk_child {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
}
.sk_circle .sk_child:before {
content: '';
display: block;
margin: 0 auto;
width: 15%;
height: 15%;
background-color: #333;
border-radius: 100%;
-webkit-animation: sk_circleBounceDelay 1.2s infinite ease-in-out both;
animation: sk_circleBounceDelay 1.2s infinite ease-in-out both;
}
.sk_circle .sk_circle2 {
-webkit-transform: rotate(30deg);
-ms-transform: rotate(30deg);
transform: rotate(30deg); }
.sk_circle .sk_circle3 {
-webkit-transform: rotate(60deg);
-ms-transform: rotate(60deg);
transform: rotate(60deg); }
.sk_circle .sk_circle4 {
-webkit-transform: rotate(90deg);
-ms-transform: rotate(90deg);
transform: rotate(90deg); }
.sk_circle .sk_circle5 {
-webkit-transform: rotate(120deg);
-ms-transform: rotate(120deg);
transform: rotate(120deg); }
.sk_circle .sk_circle6 {
-webkit-transform: rotate(150deg);
-ms-transform: rotate(150deg);
transform: rotate(150deg); }
.sk_circle .sk_circle7 {
-webkit-transform: rotate(180deg);
-ms-transform: rotate(180deg);
transform: rotate(180deg); }
.sk_circle .sk_circle8 {
-webkit-transform: rotate(210deg);
-ms-transform: rotate(210deg);
transform: rotate(210deg); }
.sk_circle .sk_circle9 {
-webkit-transform: rotate(240deg);
-ms-transform: rotate(240deg);
transform: rotate(240deg); }
.sk_circle .sk_circle10 {
-webkit-transform: rotate(270deg);
-ms-transform: rotate(270deg);
transform: rotate(270deg); }
.sk_circle .sk_circle11 {
-webkit-transform: rotate(300deg);
-ms-transform: rotate(300deg);
transform: rotate(300deg); }
.sk_circle .sk_circle12 {
-webkit-transform: rotate(330deg);
-ms-transform: rotate(330deg);
transform: rotate(330deg); }
.sk_circle .sk_circle2:before {
-webkit-animation-delay: -1.1s;
animation-delay: -1.1s; }
.sk_circle .sk_circle3:before {
-webkit-animation-delay: -1s;
animation-delay: -1s; }
.sk_circle .sk_circle4:before {
-webkit-animation-delay: -0.9s;
animation-delay: -0.9s; }
.sk_circle .sk_circle5:before {
-webkit-animation-delay: -0.8s;
animation-delay: -0.8s; }
.sk_circle .sk_circle6:before {
-webkit-animation-delay: -0.7s;
animation-delay: -0.7s; }
.sk_circle .sk_circle7:before {
-webkit-animation-delay: -0.6s;
animation-delay: -0.6s; }
.sk_circle .sk_circle8:before {
-webkit-animation-delay: -0.5s;
animation-delay: -0.5s; }
.sk_circle .sk_circle9:before {
-webkit-animation-delay: -0.4s;
animation-delay: -0.4s; }
.sk_circle .sk_circle10:before {
-webkit-animation-delay: -0.3s;
animation-delay: -0.3s; }
.sk_circle .sk_circle11:before {
-webkit-animation-delay: -0.2s;
animation-delay: -0.2s; }
.sk_circle .sk_circle12:before {
-webkit-animation-delay: -0.1s;
animation-delay: -0.1s; }
@-webkit-keyframes sk_circleBounceDelay {
0%, 80%, 100% {
-webkit-transform: scale(0);
transform: scale(0);
} 40% {
-webkit-transform: scale(1);
transform: scale(1);
}
}
@keyframes sk_circleBounceDelay {
0%, 80%, 100% {
-webkit-transform: scale(0);
transform: scale(0);
} 40% {
-webkit-transform: scale(1);
transform: scale(1);
}
}

View File

@ -0,0 +1,90 @@
@import url('https://fonts.googleapis.com/css?family=Raleway:400,600');
@import url('https://fonts.googleapis.com/css?family=Open+Sans:300,400');
body {
font-family: 'Open Sans';
font-size: 16px;
margin: 0;
}
.login {
text-align: center;
}
.loginTextbox {
font-size: 16px;
height: 43px;
width: 74%;
margin-right: 1%;
font-weight: 300;
border: 1px solid #ececec;
border-radius: 5px;
padding: 0 0 0 10px;
display: inline-block;
}
.loginButton {
height: 45px;
width: 10%;
display: inline-block;
border-radius: 5px;
background-color: green !important;
cursor: pointer;
font-size: 16px;
margin-right: 1%;
}
.loginButton:hover {
background-color: #f6f6f6;
}
.loginHeading {
text-align: center;
font-family: 'Raleway';
margin-top: 0;
padding-bottom: 20px;
}
.loginWrapper {
width: 600px;
padding: 30px;
margin: 0 auto;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
border: 1px solid #ececec;
border-radius: 5px;
background-color: #f6f6f6;
}
.footerWrapper {
position: fixed;
bottom: 20px;
width: 100%;
display: flex;
justify-content: center;
}
.apiHasura {
font-size: 14px;
text-align: right;
}
.apiHasura i {
color: #757575;;
font-size: 22px;
}
.apiHasura i:hover {
color: #000;
}
.built {
text-align: right;
font-size: 14px;
margin-right: 10px;
}
.built i {
color: #f93c18;
}

View File

@ -0,0 +1,73 @@
import React from 'react';
import PropTypes from 'prop-types';
import Helmet from 'react-helmet';
import './App.css';
import { updateGraphQLEndpoint } from '../ApiExplorer/Actions';
class LoginComponent extends React.Component {
constructor() {
super();
this.state = { graphqlEndpoint: '' };
}
setGraphQLEndpoint(e) {
this.setState({ ...this.state, graphqlEndpoint: e.target.value });
}
render() {
const { dispatch } = this.props;
return (
<div>
<div className="loginWrapper">
<Helmet
title="GraphiQL Online with Headers | Built by Hasura"
description="An online version of GraphiQL. Manage headers easily. Test your GraphQL servers."
/>
<h2 className="loginHeading"> Online GraphiQL </h2>
<div className="login">
<div>
<form>
<input
type="text"
id="username"
className="loginTextbox"
placeholder="Enter GraphQL Endpoint URL"
onChange={this.setGraphQLEndpoint.bind(this)}
/>
<button
className="loginButton"
type="submit"
onClick={(e) => {
e.preventDefault();
const emailRegex = /^(http[s]?:\/\/){0,1}(www\.){0,1}[a-zA-Z0-9\.\-]+\.[a-zA-Z]{2,5}[\.]{0,1}/;
if (!emailRegex.test(this.state.graphqlEndpoint)) {
alert('Please enter a valid URL');
} else {
dispatch(updateGraphQLEndpoint(this.state.graphqlEndpoint));
}
}}
>
<i className={'fa fa-sign-in'} />
</button>
</form>
</div>
</div>
</div>
<div className="footerWrapper">
<div className="built">
Built with <i className="fa fa-heart" /> by <a href={'http://hasura.io/'} target={'_blank'}>Hasura</a>
</div>
<div className="apiHasura">
<a href="https://github.com/hasura/graphql-engine/tree/master/community/tools/graphiql-online" target={'_blank'}>
<i className="fa fa-github" />
</a>
</div>
</div>
</div>
);
}
}
LoginComponent.propTypes = {
dispatch: PropTypes.func.isRequired,
};
export default LoginComponent;

View File

@ -0,0 +1,12 @@
import LoginComponent from './Login';
const generatedLoginComponent = connect => {
const mapStateToProps = state => {
return {
...state.apiexplorer,
};
};
return connect(mapStateToProps)(LoginComponent);
};
export default generatedLoginComponent;

View File

@ -0,0 +1,6 @@
/**
* Export component modules
*/
export App from './App/App';
export PageNotFound from './404/PageNotFound';

View File

@ -0,0 +1,24 @@
/* @flow */
const appConfig = require('../appconfig');
const host = appConfig.appHost;
const port = appConfig.port[process.env.NODE_ENV || 'development'];
// require('babel-polyfill');
const environment = {
development: {
isProduction: false,
},
production: {
isProduction: true,
},
}[process.env.NODE_ENV || 'development'];
module.exports = Object.assign(
{
host: host,
port: port,
},
environment
);

View File

@ -0,0 +1,107 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Helmet from 'react-helmet';
/**
* Wrapper component containing HTML metadata and boilerplate tags.
* Used in server-side code only to wrap the string output of the
* rendered route component.
*
* The only thing this component doesn't (and can't) include is the
* HTML doctype declaration, which is added to the rendered output
* by the server.js file.
*/
export default class Html extends Component {
static propTypes = {
assets: PropTypes.object,
component: PropTypes.node,
baseDomain: PropTypes.string,
};
render() {
const { assets } = this.props;
const head = Helmet.rewind();
return (
<html lang="en-us">
<head>
<link rel="icon" type="image/png" href="/rstatic/favicon.png" />
<title>GraphiQL Online with Headers | Built by Hasura</title>
<meta name="title" content="GraphiQL Online with Headers | Built by Hasura" />
<meta name="description" content="An online version of GraphiQL. Manage headers easily. Test your GraphQL servers" />
{Object.keys(assets.styles).map((style, key) => (
<link
href={assets.styles[style]}
key={key}
media="screen, projection"
rel="stylesheet"
type="text/css"
charSet="UTF-8"
/>
))}
<link
rel="stylesheet"
href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
crossOrigin="anonymous"
/>
<script
dangerouslySetInnerHTML={{
__html: `window.__env={
graphqlEndpoint: '${process.env.GRAPHQL_ENDPOINT}',
query: '${process.env.QUERY_STRING}',
headers: '${process.env.HEADER_STRING}',
variables: '${process.env.VARIABLE_STRING}',
callbackUrl: '${process.env.REACT_APP_CALLBACK_URL}',
environment: '${process.env.NODE_ENV}'
};`,
}}
/>
</head>
<body>
<style
dangerouslySetInnerHTML={{
__html: `
.content {
display: 'none';
opacity: 0;
transition: opacity .20s linear;
}
.content.show {
display: 'block';
opacity: 1;
transition: opacity .20s linear;
}
`,
}}
/>
<div
id="loading"
dangerouslySetInnerHTML={{
__html: `<div class="page-loading" style="
min-height: 100vh;
width: 100%;
display: flex;
align-items: center;
font-family: sans-serif;
justify-content: center;
">
<span class="" style="
font-size: 2em;
margin-top: -3em;
color: #848484;
">
Loading...
</span>
</div>`,
}}
/>
<div id="content" className="content" />
<script src={assets.javascript.main} charSet="UTF-8" />
</body>
</html>
);
}
}

View File

@ -0,0 +1,9 @@
/**
* Return the status code from the last matched route with a status property.
*
* @param matchedRoutes
* @returns {Number|null}
*/
export default matchedRoutes => {
return matchedRoutes.reduce((prev, cur) => cur.status || prev, null);
};

View File

@ -0,0 +1,43 @@
import { createRoutes } from 'react-router/lib/RouteUtils';
// Wrap the hooks so they don't fire if they're called before
// the store is initialised. This only happens when doing the first
// client render of a route that has an onEnter hook
function makeHooksSafe(routes, store) {
if (Array.isArray(routes)) {
return routes.map(route => makeHooksSafe(route, store));
}
const onEnter = routes.onEnter;
if (onEnter) {
routes.onEnter = function safeOnEnter(...args) {
try {
store.getState();
} catch (err) {
if (onEnter.length === 3) {
args[2]();
}
// There's no store yet so ignore the hook
return;
}
onEnter.apply(null, args);
};
}
if (routes.childRoutes) {
makeHooksSafe(routes.childRoutes, store);
}
if (routes.indexRoute) {
makeHooksSafe(routes.indexRoute, store);
}
return routes;
}
export default function makeRouteHooksSafe(_getRoutes) {
return store => makeHooksSafe(createRoutes(_getRoutes(store)), store);
}

View File

@ -0,0 +1,12 @@
import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux';
import apiExplorerReducer from 'components/ApiExplorer/Actions';
import progressBarReducer from 'components/App/Actions';
const reducer = combineReducers({
progressBar: progressBarReducer,
apiexplorer: apiExplorerReducer,
routing: routerReducer,
});
export default reducer;

View File

@ -0,0 +1,30 @@
import React from 'react';
import { Route, IndexRoute } from 'react-router';
import { connect } from 'react-redux';
import { App, PageNotFound } from 'components';
import generatedApiExplorer from './components/ApiExplorer/ApiExplorerGenerator';
import Auth from './components/Auth/Auth';
import Callback from './components/Callback/Callback';
const auth = new Auth();
const routes = () => {
// loads schema
return (
<Route path="/" component={App} auth={auth}>
{/*
<Route path="">
<IndexRoute component={generatedLoginComponent(connect)} />
</Route>
*/}
<Route path="">
<IndexRoute auth={auth} component={generatedApiExplorer(connect)} />
</Route>
<Route path="404" component={PageNotFound} status="404" />
<Route exact path="/callback" auth={auth} component={Callback} />
<Route path="*" component={PageNotFound} status="404" />
</Route>
);
};
export default routes;

View File

@ -0,0 +1,59 @@
import Express from 'express';
import React from 'react';
import ReactDOM from 'react-dom/server';
import config from './config';
// import favicon from 'serve-favicon';
import path from 'path';
import Html from './helpers/Html';
import http from 'http';
global.__DISABLE_SSR__ = true;
// Express middleware
const app = new Express();
const server = new http.Server(app);
/* Constants */
/* Routes and middleware */
// app.use(favicon(path.join(__dirname, '..', 'static', 'favicon.ico')));
app.use('/rstatic', Express.static(path.join(__dirname, '..', 'static')));
app.use((req, res) => {
if (__DEVELOPMENT__) {
// Do not cache webpack stats: the script file would change since
// hot module replacement is enabled in the development env
webpackIsomorphicTools.refresh();
}
// Initialize the store here
function hydrateOnClient() {
res.send(
'<!doctype html>\n' +
ReactDOM.renderToString(
<Html assets={webpackIsomorphicTools.assets()} />
)
);
}
if (__DISABLE_SSR__) {
hydrateOnClient();
return;
}
});
if (config.port) {
server.listen(config.port, config.host, err => {
if (err) {
console.error(err);
}
console.info(
'==> 💻 Open http://%s:%s in a browser to view the app.',
config.host,
config.port
);
});
} else {
console.error(
'==> ERROR: No PORT environment variable has been specified'
);
}

View File

@ -0,0 +1,71 @@
/**
* Bootstrap configuration for bootstrap-sass-loader
*
* Scripts are disabled to not load jQuery.
* If you depend on Bootstrap scripts consider react-bootstrap instead.
* https://github.com/react-bootstrap/react-bootstrap
*
* In order to keep the bundle size low in production
* disable components you don't use.
*
*/
module.exports = {
preBootstrapCustomizations: './src/theme/variables.scss',
mainSass: './src/theme/bootstrap.overrides.scss',
verbose: false,
debug: false,
scripts: {
transition: false,
alert: false,
button: true,
carousel: false,
collapse: false,
dropdown: true,
modal: true,
tooltip: false,
popover: false,
scrollspy: false,
tab: false,
affix: false,
},
styles: {
mixins: true,
normalize: true,
print: true,
glyphicons: true,
scaffolding: true,
type: true,
code: true,
grid: true,
tables: true,
forms: true,
buttons: true,
'component-animations': true,
dropdowns: true,
'button-groups': true,
'input-groups': true,
navs: true,
navbar: true,
breadcrumbs: true,
pagination: true,
pager: true,
labels: true,
badges: true,
jumbotron: true,
thumbnails: true,
alerts: true,
'progress-bars': true,
media: true,
'list-group': true,
panels: true,
wells: true,
'responsive-embed': true,
close: true,
modals: true,
tooltip: true,
popovers: true,
carousel: false,
utilities: true,
'responsive-utilities': true,
},
};

View File

@ -0,0 +1,8 @@
const bootstrapConfig = require('./bootstrap.config.js');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
bootstrapConfig.styleLoaders = ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader', 'sass-loader'],
});
module.exports = bootstrapConfig;

View File

@ -0,0 +1,198 @@
/**
* Override Bootstrap styles that you can't modify via variables here.
*
*/
input[type="radio"], input[type="checkbox"]
{
margin: 0 5px 0px 0px;
}
label
{
margin-bottom: 0 !important;
}
.navbar-brand {
position: relative;
padding-left: 50px;
}
.graphiql-container .topBar {
height: auto !important;
}
.navbar-default .navbar-nav > .active > a,
.navbar-default .navbar-nav > .active > a:hover,
.navbar-default .navbar-nav > .active > a:focus {
color: #33e0ff;
background-color: transparent;
}
.control-label
{
padding-top: 0;
}
form {
margin-bottom: 0em;
}
.form-control:focus {
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 0px rgba(102, 175, 233, 0.6);
}
.form-control {
// border-radius: 0;
height: auto;
}
select.form-control
{
border-radius: 0;
-webkit-border-radius: 0px;
}
.btn {
// border-radius: 0;
outline: none;
}
.btn:active, .btn.active
{
box-shadow: none !important;
}
.btn:focus, .btn.focus, .btn:active:focus, .btn:active.focus, .btn.active:focus, .btn.active.focus
{
outline: none !important;
}
.btn:focus
{
outline: none !important;
}
.alert {
word-wrap: break-word;
}
.btn-warning {
background-color: #FEC53D !important;
border-color: #FEC53D !important;
color: #000 !important;
padding: 5px 10px !important;
font-size: 12px !important;
line-height: 1.5 !important;
}
.btn-warning:hover {
color: #000;
background-color: #ec971f;
border-color: #d58512;
}
// Generate Code modal
.padd_remove
{
padding-left: 0 !important;
padding-right: 0 !important;
}
.generateCodeWrapper
{
width: 65% !important;
.modal-content
{
.modal-header
{
background-color: #F8FAFB;
color: #222 ;
padding: 20px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
border-bottom: 0;
border-bottom: 1px solid #ccc;
h4
{
font-size: 16px;
font-weight: bold;
}
button
{
text-shadow: none;
color: #C4C6C6;
opacity: 1;
span
{
font-weight: normal;
}
}
}
.modal-body
{
padding: 25px;
.generateBodyWrapper
{
.generateButtonWrapper
{
width: 100%;
float: left;
padding-bottom: 20px;
.generateSelect
{
button
{
width: auto;
padding: 10px 35px;
padding-left: 10px;
background-color: #F9F9F9;
color: #606060;
font-size: 14px;
font-weight: bold;
text-align: left;
border: 1px solid #ccc;
position: relative;
.dropCaret
{
position: absolute;
right: 10px;
top: 16px;
}
}
}
.generateCopyButton
{
text-align: right;
font-weight: bold;
button {
background-color: #FEC53D;
border-radius: 5px;
color: #606060;
border: 1px solid #FEC53D;
padding: 5px 10px;
font-size: 12px;
line-height: 1.5;
&:hover {
background-color: #F2B130;
}
}
}
.generateCopyButtonDisabled
{
text-align: right;
button
{
border-radius: 5px;
border: 1px solid #606060;
height: 39px;
padding: 5px 15px;
font-size: 15px;
background-color: #FFF;
text-align: center;
color: #606060;
font-weight: bold;
}
}
.ace_editor
{
min-height: 150px;
border: 1px solid #ccc;
border-radius: 5px;
}
.ace-github
{
min-height: 150px;
border: 1px solid #ccc;
border-radius: 5px;
}
}
}
}
}
}

View File

@ -0,0 +1,18 @@
/**
* Configuration file for font-awesome-webpack
*
* In order to keep the bundle size low in production,
* disable components you don't use.
*
*/
module.exports = {
styles: {
mixins: true,
core: true,
icons: true,
larger: true,
path: true,
animated: true,
},
};

View File

@ -0,0 +1,7 @@
/**
* Configuration file for font-awesome-webpack
*
*/
// Example:
// @fa-border-color: #ddd;

View File

@ -0,0 +1,8 @@
const fontAwesomeConfig = require('./font-awesome.config.js');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
fontAwesomeConfig.styleLoader = ExtractTextPlugin.extract({
fallback: 'style-loader',
use: 'css-loader!less-loader',
});
module.exports = fontAwesomeConfig;

View File

@ -0,0 +1,27 @@
/**
* Define scss variables here.
*
* Available options for Bootstrap:
* http://getbootstrap.com/customize/
*
*/
// Custom Colors
$cyan: #33e0ff;
$humility: #777;
// Bootstrap Variables
$brand-primary: darken(#428bca, 6.5%);
$brand-secondary: #e25139;
$brand-success: #5cb85c;
$brand-warning: #f0ad4e;
$brand-danger: #d9534f;
$brand-info: #5bc0de;
$text-color: #333;
$font-size-base: 14px;
$font-family-sans-serif: "Helvetica Neue", Helvetica, sans-serif;
$table-bg-hover: #EBF7DE !default;
$table-bg-active: $table-bg-hover !default;

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,65 @@
<html lang="en-us">
<head>
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-59768903-1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-59768903-1');
</script>
<link rel="icon" type="image/png" href="./favicon.png" />
<title>GraphiQL Online with Headers | Built by Hasura</title>
<meta name="title" content="GraphiQL Online with Headers | Built by Hasura" />
<meta name="description" content="An online version of GraphiQL. Manage headers easily. Test your GraphQL servers" />
<script>
window.__env = {
graphqlEndpoint: 'https://learn.hasura.io/graphql',
callbackUrl: 'https://learn.hasura.io/graphql/graphiql/callback',
headers: '',
variables: '',
query: ''
};
</script>
</head>
<body>
<style>
.mainContent {
display: 'none';
opacity: 0;
transition: opacity .20s linear;
}
.mainContent.show {
display: 'block';
opacity: 1;
transition: opacity .20s linear;
}
</style>
<div id="loading">
<div class="page-loading" style="
min-height: 100vh;
width: 100%;
display: flex;
align-items: center;
font-family: sans-serif;
justify-content: center;
">
<span class="" style="
font-size: 2em;
margin-top: -3em;
color: #848484;
">
Loading...
</span>
</div>
</div>
<div id="content" class="mainContent"></div>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"/>
<link rel="stylesheet" href="https://storage.googleapis.com/graphql-engine-cdn.hasura.io/learn-hasura/graphiql/main.css" charset="UTF-8"/>
<script src="https://storage.googleapis.com/graphql-engine-cdn.hasura.io/learn-hasura/graphiql/vendor.js" charset="UTF-8"></script>
<script src="https://storage.googleapis.com/graphql-engine-cdn.hasura.io/learn-hasura/graphiql/main.js" charset="UTF-8"></script>
</body>
</html>

View File

@ -0,0 +1,116 @@
---
# Output debugging info
# loglevel: debug
# Major version of Bootstrap: 3 or 4
bootstrapVersion: 3
# If Bootstrap version 3 is used - turn on/off custom icon font path
useCustomIconFontPath: false
# Webpack loaders, order matters
styleLoaders:
- style-loader
- css-loader
- sass-loader
# Extract styles to stand-alone css file
# Different settings for different environments can be used,
# It depends on value of NODE_ENV environment variable
# This param can also be set in webpack config:
# entry: 'bootstrap-loader/extractStyles'
extractStyles: true
# env:
# development:
# extractStyles: false
# production:
# extractStyles: true
# Customize Bootstrap variables that get imported before the original Bootstrap variables.
# Thus original Bootstrap variables can depend on values from here. All the bootstrap
# variables are configured with !default, and thus, if you define the variable here, then
# that value is used, rather than the default. However, many bootstrap variables are derived
# from other bootstrap variables, and thus, you want to set this up before we load the
# official bootstrap versions.
# For example, _variables.scss contains:
# $input-color: $gray !default;
# This means you can define $input-color before we load _variables.scss
#preBootstrapCustomizations: ./src/theme/variables.scss
# This gets loaded after bootstrap/variables is loaded and before bootstrap is loaded.
# A good example of this is when you want to override a bootstrap variable to be based
# on the default value of bootstrap. This is pretty specialized case. Thus, you normally
# just override bootrap variables in preBootstrapCustomizations so that derived
# variables will use your definition.
#
# For example, in _variables.scss:
# $input-height: (($font-size-base * $line-height) + ($input-padding-y * 2) + ($border-width * 2)) !default;
# This means that you could define this yourself in preBootstrapCustomizations. Or you can do
# this in bootstrapCustomizations to make the input height 10% bigger than the default calculation.
# Thus you can leverage the default calculations.
# $input-height: $input-height * 1.10;
# bootstrapCustomizations: ./app/styles/bootstrap/customizations.scss
# Import your custom styles here. You have access to all the bootstrap variables. If you require
# your sass files separately, you will not have access to the bootstrap variables, mixins, clases, etc.
# Usually this endpoint-file contains list of @imports of your application styles.
#appStyles: ./src/theme/bootstrap.overrides.scss
### Bootstrap styles
styles:
# Mixins
mixins: true
# Reset and dependencies
normalize: true
print: true
glyphicons: true
# Core CSS
scaffolding: true
type: true
code: true
grid: true
tables: true
forms: true
buttons: true
# Components
component-animations: true
dropdowns: true
button-groups: true
input-groups: true
navs: true
navbar: true
breadcrumbs: true
pagination: true
pager: true
labels: true
badges: true
jumbotron: true
thumbnails: true
alerts: true
progress-bars: true
media: true
list-group: true
panels: true
wells: true
responsive-embed: true
close: true
# Components w/ JavaScript
modals: true
tooltip: true
popovers: true
carousel: true
# Utility classes
utilities: true
responsive-utilities: true
### Bootstrap scripts
scripts: false

View File

@ -0,0 +1,186 @@
// require('babel-polyfill');
// Webpack config for development
const fs = require('fs');
const path = require('path');
const webpack = require('webpack');
const assetsPath = path.resolve(__dirname, '../static/dist');
const appconfig = require('../appconfig');
const host = appconfig.hmrHost;
const port = appconfig.hmrPort;
const autoprefixer = require('autoprefixer');
const WebpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin');
const webpackIsomorphicToolsPlugin = new WebpackIsomorphicToolsPlugin(
require('./webpack-isomorphic-tools')
);
// const { UnusedFilesWebpackPlugin } = require('unused-files-webpack-plugin');
const babelrc = fs.readFileSync('./.babelrc');
let babelrcObject = {};
try {
babelrcObject = JSON.parse(babelrc);
} catch (err) {
console.error('==> ERROR: Error parsing your .babelrc.');
console.error(err);
}
const babelrcObjectDevelopment =
(babelrcObject.env && babelrcObject.env.development) || {};
const babelLoaderQuery = Object.assign(
{},
babelrcObject,
babelrcObjectDevelopment
);
delete babelLoaderQuery.env;
babelLoaderQuery.plugins = babelLoaderQuery.plugins || [];
if (babelLoaderQuery.plugins.indexOf('react-transform') < 0) {
babelLoaderQuery.plugins.push('react-transform');
}
babelLoaderQuery.extra = babelLoaderQuery.extra || {};
if (!babelLoaderQuery.extra['react-transform']) {
babelLoaderQuery.extra['react-transform'] = {};
}
if (!babelLoaderQuery.extra['react-transform'].transforms) {
babelLoaderQuery.extra['react-transform'].transforms = [];
}
babelLoaderQuery.extra['react-transform'].transforms.push({
transform: 'react-transform-hmr',
imports: ['react'],
locals: ['module'],
});
module.exports = {
mode: 'development',
devtool: 'inline-source-map',
context: path.resolve(__dirname, '..'),
entry: {
main: [
'webpack-hot-middleware/client?path=http://' +
host +
':' +
port +
'/__webpack_hmr',
'bootstrap-loader?extractStyles',
'font-awesome-webpack!./src/theme/font-awesome.config.js',
'./src/client.js',
],
},
output: {
path: assetsPath,
filename: '[name]-[hash].js',
chunkFilename: '[name]-[chunkhash].js',
publicPath: 'http://' + host + ':' + port + appconfig.webpackPrefix,
},
module: {
rules: [
{
test: /\.mjs$/,
include: /node_modules/,
type: 'javascript/auto',
},
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: 'babel-loader',
},
{
test: /\.flow$/,
loader: 'ignore-loader',
},
{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 1,
},
},
],
},
{
test: /\.scss$/,
use: [
'style-loader',
'css-loader?modules&importLoaders=2&sourceMap&localIdentName=[local]___[hash:base64:5]',
'sass-loader?outputStyle=expanded&sourceMap',
],
},
{
test: /\.woff(\?v=\d+\.\d+\.\d+)?$/,
use: [
{
loader: 'url-loader',
options: { limit: 10000, mimetype: 'application/font-woff' },
},
],
},
{
test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/,
use: [
{
loader: 'url-loader',
options: { limit: 10000, mimetype: 'application/font-woff' },
},
],
},
{
test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
use: [
{
loader: 'url-loader',
options: { limit: 10000, mimetype: 'application/octet-stream' },
},
],
},
{
test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
use: [{ loader: 'file-loader' }],
},
{
test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
use: [
{
loader: 'url-loader',
options: { limit: 10000, mimetype: 'image/svg+xml' },
},
],
},
{
test: webpackIsomorphicToolsPlugin.regular_expression('images'),
use: [{ loader: 'url-loader', options: { limit: 10240 } }],
},
],
},
resolve: {
modules: ['src', 'node_modules'],
extensions: ['.json', '.js', '.jsx', '.mjs'],
},
plugins: [
// hot reload
// new UnusedFilesWebpackPlugin({}),
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.LoaderOptionsPlugin({
postcss: [autoprefixer],
}),
new webpack.IgnorePlugin(/webpack-stats\.json$/),
new webpack.DefinePlugin({
__CLIENT__: true,
__SERVER__: false,
__DEVELOPMENT__: true,
__DEVTOOLS__: true, // <-------- DISABLE redux-devtools HERE
}),
webpackIsomorphicToolsPlugin.development(),
],
};

View File

@ -0,0 +1,192 @@
// Webpack config for creating the production bundle.
const path = require('path');
const webpack = require('webpack');
const CleanPlugin = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const appconfig = require('../appconfig');
const relativeAssetsPath = '../static/dist';
const assetsPath = path.join(__dirname, relativeAssetsPath);
const WebpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin');
const webpackIsomorphicToolsPlugin = new WebpackIsomorphicToolsPlugin(
require('./webpack-isomorphic-tools')
);
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const cleanOptions = {
root: process.cwd(),
verbose: true,
dry: false,
};
module.exports = {
mode: 'production',
context: path.resolve(__dirname, '..'),
entry: {
main: ['bootstrap-loader?extractStyles', './src/client.js'],
},
output: {
path: assetsPath,
filename: '[name].js',
chunkFilename: '[name].js',
publicPath: appconfig.webpackPrefix,
},
module: {
rules: [
{
test: /\.mjs$/,
include: /node_modules/,
type: 'javascript/auto',
},
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: 'babel-loader',
},
{
test: /\.flow$/,
loader: 'ignore-loader',
},
{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 1,
},
},
],
},
{
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader?modules&importLoaders=2&sourceMap=false',
'sass-loader?outputStyle=expanded&sourceMap=false&sourceMapContents=false',
],
},
{
test: /\.woff(\?v=\d+\.\d+\.\d+)?$/,
use: [
{
loader: 'url-loader',
options: { limit: 10000, mimetype: 'application/font-woff' },
},
],
},
{
test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/,
use: [
{
loader: 'url-loader',
options: { limit: 10000, mimetype: 'application/font-woff' },
},
],
},
{
test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
use: [
{
loader: 'url-loader',
options: { limit: 10000, mimetype: 'application/octet-stream' },
},
],
},
{
test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
use: [{ loader: 'file-loader' }],
},
{
test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
use: [
{
loader: 'url-loader',
options: { limit: 10000, mimetype: 'image/svg+xml' },
},
],
},
{
test: webpackIsomorphicToolsPlugin.regular_expression('images'),
use: [{ loader: 'url-loader', options: { limit: 10240 } }],
},
],
},
resolve: {
modules: ['src', 'node_modules'],
extensions: ['.json', '.js', '.jsx', '.mjs'],
},
optimization: {
minimize: true,
runtimeChunk: {
name: 'vendor',
},
minimizer: [
new UglifyJSPlugin({
uglifyOptions: {
ecma: 8,
warnings: false,
compress: false,
mangle: true,
output: {
comments: false,
beautify: false,
},
toplevel: false,
nameCache: null,
ie8: false,
keep_classnames: undefined,
keep_fnames: false,
safari10: false,
},
}),
new OptimizeCssAssetsPlugin({
assetNameRegExp: /\.css$/,
}),
],
splitChunks: {
cacheGroups: {
commons: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'initial',
},
},
},
},
plugins: [
new CleanPlugin(['./static/dist/*.*'], cleanOptions),
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
}),
new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// both options are optional
filename: 'main.css',
chunkFilename: 'main.css',
}),
new webpack.DefinePlugin({
__CLIENT__: true,
__SERVER__: false,
__DEVELOPMENT__: false,
__DEVTOOLS__: false,
}),
// ignore dev config
new webpack.IgnorePlugin(/\.\/dev/, /\/config$/),
// set global consts
new webpack.DefinePlugin({
'process.env': {
// Useful to reduce the size of client-side libraries, e.g. react
NODE_ENV: JSON.stringify('production'),
},
}),
],
};

View File

@ -0,0 +1,36 @@
const Express = require('express');
const webpack = require('webpack');
const webpackConfig = require('./dev.config');
const compiler = webpack(webpackConfig);
const appconfig = require('../appconfig');
const host = appconfig.hmrHost;
const port = appconfig.hmrPort;
const serverOptions = {
contentBase: 'http://' + host + ':' + port,
logLevel: 'silent',
hot: true,
inline: true,
lazy: false,
publicPath: webpackConfig.output.publicPath,
headers: { 'Access-Control-Allow-Origin': '*' },
stats: { colors: true },
};
const app = new Express();
app.use(require('webpack-dev-middleware')(compiler, serverOptions));
app.use(require('webpack-hot-middleware')(compiler));
app.listen(port, function onAppListening(err) {
if (err) {
console.error(err);
} else {
console.info(
'==> 🚧 Webpack development server listening on port %s',
port
);
}
});

View File

@ -0,0 +1,101 @@
var WebpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin');
// see this link for more info on what all of this means
// https://github.com/halt-hammerzeit/webpack-isomorphic-tools
module.exports = {
// when adding "js" extension to asset types
// and then enabling debug mode, it may cause a weird error:
//
// [0] npm run start-prod exited with code 1
// Sending SIGTERM to other processes..
//
// debug: true,
assets: {
images: {
extensions: ['jpeg', 'jpg', 'png', 'gif'],
parser: WebpackIsomorphicToolsPlugin.url_loader_parser,
},
fonts: {
extensions: ['woff', 'woff2', 'ttf', 'eot'],
parser: WebpackIsomorphicToolsPlugin.url_loader_parser,
},
svg: {
extension: 'svg',
parser: WebpackIsomorphicToolsPlugin.url_loader_parser,
},
// this whole "bootstrap" asset type is only used once in development mode.
// the only place it's used is the Html.js file
// where a <style/> tag is created with the contents of the
// './src/theme/bootstrap.config.js' file.
// (the aforementioned <style/> tag can reduce the white flash
// when refreshing page in development mode)
//
// hooking into 'js' extension require()s isn't the best solution
// and I'm leaving this comment here in case anyone finds a better idea.
//bootstrap: {
// extension: 'js',
// include: ['./src/theme/bootstrap.config.js'],
// filter: function(module, regex, options, log) {
// function is_bootstrap_style(name) {
// return name.indexOf('./src/theme/bootstrap.config.js') >= 0;
// }
// if (options.development) {
// return is_bootstrap_style(module.name) && WebpackIsomorphicToolsPlugin.style_loader_filter(module, regex, options, log);
// }
// // no need for it in production mode
// },
// // in development mode there's webpack "style-loader",
// // so the module.name is not equal to module.name
// path: WebpackIsomorphicToolsPlugin.style_loader_path_extractor,
// parser: WebpackIsomorphicToolsPlugin.css_loader_parser
//},
style_modules: {
extensions: ['scss'],
filter: (module, regex, options, log) => {
if (options.development) {
// in development mode there's webpack "style-loader",
// so the module.name is not equal to module.name
return WebpackIsomorphicToolsPlugin.style_loader_filter(
module,
regex,
options,
log
);
} else {
// in production mode there's no webpack "style-loader",
// so the module.name will be equal to the asset path
return regex.test(module.name);
}
},
path: (module, options, log) => {
if (options.development) {
// in development mode there's webpack "style-loader",
// so the module.name is not equal to module.name
return WebpackIsomorphicToolsPlugin.style_loader_path_extractor(
module,
options,
log
);
} else {
// in production mode there's no webpack "style-loader",
// so the module.name will be equal to the asset path
return module.name;
}
},
parser: (module, options, log) => {
if (options.development) {
return WebpackIsomorphicToolsPlugin.css_modules_loader_parser(
module,
options,
log
);
} else {
// in production mode there's Extract Text Loader which extracts CSS text away
return module.source;
}
},
},
},
};

View File

@ -0,0 +1,9 @@
## Todo App Migrations
Update config.yaml to point to the right graphql-engine endpoint with/without admin_secret.
Run the following commands:
```
$ hasura migrate apply
```
This will apply the migrations and metadata.

View File

@ -0,0 +1 @@
endpoint: https://learn.hasura.io/graphql

View File

@ -0,0 +1,167 @@
--
-- PostgreSQL database dump
--
-- Dumped from database version 10.5 (Debian 10.5-1.pgdg90+1)
-- Dumped by pg_dump version 10.1
-- Started on 2018-10-08 16:36:23 IST
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SET check_function_bodies = false;
SET client_min_messages = warning;
SET row_security = off;
SET search_path = public, pg_catalog;
SET default_tablespace = '';
SET default_with_oids = false;
--
-- TOC entry 213 (class 1259 OID 24585)
-- Name: users; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE users (
id integer NOT NULL,
name text,
auth0_id text NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
last_seen timestamp with time zone DEFAULT now() NOT NULL
);
--
-- TOC entry 214 (class 1259 OID 24593)
-- Name: online_users; Type: VIEW; Schema: public; Owner: -
--
CREATE VIEW online_users AS
SELECT users.name,
users.last_seen
FROM users
WHERE (users.last_seen >= (now() - '00:00:30'::interval));
--
-- TOC entry 212 (class 1259 OID 24576)
-- Name: todos; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE todos (
id integer NOT NULL,
text text NOT NULL,
is_completed boolean DEFAULT false NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone,
is_public boolean DEFAULT false NOT NULL,
user_id text NOT NULL
);
--
-- TOC entry 215 (class 1259 OID 24597)
-- Name: todos_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE todos_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- TOC entry 2981 (class 0 OID 0)
-- Dependencies: 215
-- Name: todos_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE todos_id_seq OWNED BY todos.id;
--
-- TOC entry 216 (class 1259 OID 24599)
-- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE users_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- TOC entry 2982 (class 0 OID 0)
-- Dependencies: 216
-- Name: users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE users_id_seq OWNED BY users.id;
--
-- TOC entry 2839 (class 2604 OID 24612)
-- Name: todos id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY todos ALTER COLUMN id SET DEFAULT nextval('todos_id_seq'::regclass);
--
-- TOC entry 2842 (class 2604 OID 24613)
-- Name: users id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY users ALTER COLUMN id SET DEFAULT nextval('users_id_seq'::regclass);
--
-- TOC entry 2844 (class 2606 OID 24604)
-- Name: todos todos_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY todos
ADD CONSTRAINT todos_pkey PRIMARY KEY (id);
--
-- TOC entry 2846 (class 2606 OID 24606)
-- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY users
ADD CONSTRAINT users_pkey PRIMARY KEY (auth0_id);
--
-- TOC entry 2847 (class 2606 OID 24607)
-- Name: todos todos_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY todos
ADD CONSTRAINT todos_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(auth0_id);
--
-- TOC entry 2980 (class 0 OID 0)
-- Dependencies: 4
-- Name: public; Type: ACL; Schema: -; Owner: -
--
-- Completed on 2018-10-08 16:36:25 IST
--
-- PostgreSQL database dump complete
--

View File

@ -0,0 +1,3 @@
- args:
sql: alter table "public"."todos" rename column "title" to "text";
type: run_sql

View File

@ -0,0 +1,3 @@
- args:
sql: alter table "public"."todos" rename column "text" to "title";
type: run_sql

View File

@ -0,0 +1,4 @@
- args:
cascade: true
sql: drop table users cascade;
type: run_sql

View File

@ -0,0 +1,3 @@
- args:
sql: DROP TABLE "public"."users"
type: run_sql

View File

@ -0,0 +1,9 @@
- args:
sql: CREATE TABLE "public"."users"("id" text NOT NULL, "name" text NOT NULL, "created_at"
timestamptz NOT NULL DEFAULT now(), "last_seen" timestamptz NOT NULL, PRIMARY
KEY ("id") );
type: run_sql
- args:
name: users
schema: public
type: add_existing_table_or_view

View File

@ -0,0 +1,3 @@
- args:
sql: ALTER TABLE "public"."users" ALTER COLUMN "last_seen" SET NOT NULL;
type: run_sql

View File

@ -0,0 +1,3 @@
- args:
sql: ALTER TABLE "public"."users" ALTER COLUMN "last_seen" DROP NOT NULL;
type: run_sql

View File

@ -0,0 +1,3 @@
- args:
sql: alter table "public"."todos" drop constraint todos_user_id_fkey;
type: run_sql

View File

@ -0,0 +1,4 @@
- args:
sql: alter table "public"."todos" add constraint todos_user_id_fkey foreign key
("user_id") references "public"."users"("id") on update cascade on delete cascade;
type: run_sql

View File

@ -0,0 +1,6 @@
- args:
relationship: user
table:
name: todos
schema: public
type: drop_relationship

View File

@ -0,0 +1,8 @@
- args:
name: user
table:
name: todos
schema: public
using:
foreign_key_constraint_on: user_id
type: create_object_relationship

View File

@ -0,0 +1,6 @@
- args:
relationship: todos
table:
name: users
schema: public
type: drop_relationship

Some files were not shown because too many files have changed in this diff Show More