Merge branch 'master' into issue-4035-check-computed-field

This commit is contained in:
Rakesh Emmadi 2020-03-23 10:44:48 +05:30 committed by GitHub
commit c12dd1bae1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
106 changed files with 5249 additions and 1284 deletions

View File

@ -49,10 +49,10 @@ echo "Changes in this build:"
echo $changes
echo
if [[ ${#changes[@]} -gt 0 ]]; then
# If there's still changes left, then we have stuff to build, leave the commit alone.
if [[ ! -z "$changes" ]]; then
# If there's still changes left, then we have stuff to build, leave the commit alone.
echo "Files that are not ignored present in commits, need to build, succeed the job"
exit
exit
fi
echo "Only ignored files are present in commits, build is not required, write the skip_job file"

View File

@ -101,6 +101,8 @@ refs:
# Setting default number of threads to 2
# since circleci allocates 2 cpus per test container
GHCRTS: -N2
# Until we can use a real webserver for TestEventFlood, limit concurrency:
HASURA_GRAPHQL_EVENTS_HTTP_POOL_SIZE: 8
HASURA_GRAPHQL_DATABASE_URL: postgresql://gql_test:@localhost:5432/gql_test
HASURA_GRAPHQL_DATABASE_URL_2: postgresql://gql_test:@localhost:5432/gql_test2
GRAPHQL_ENGINE: /build/_server_output/graphql-engine

View File

@ -6,23 +6,25 @@
- Introducing Actions: https://docs.hasura.io/1.0/graphql/manual/actions/index.html
- Downgrade command: https://hasura.io/docs/1.0/graphql/manual/deployment/downgrading.html#downgrading-hasura-graphql-engine
- console: add multi select to data table and bulk delete (#3735)
Added a checkbox to each row on Browse Rows view that allows selecting one or more rows from the table and bulk delete them.
- console: add multi select in browse rows to allow bulk delete (close #1739) (#3735)
Adds a checkbox to each row on Browse Rows view that allows selecting one or more rows from the table and bulk delete them.
- console: allow setting check constraints during table create (#3881)
Added a component that allows adding check constraints while creating a new table in the same way as it can be done on the `Modify` view.
Adds a component that allows adding check constraints while creating a new table in the same way as it can be done on the `Modify` view.
### Select dropdown for Enum columns (console)
- console: add dropdown for enum fields in insert/edit rows page (close #3748) (#3810)
If a table has a field referencing an Enum table via a foreign key, then there will be a select dropdown with all possible enum values on `Insert Row` and `Edit Row` views on the Console.
(close #3748) (#3810)
If a table has a field referencing an enum table via a foreign key, then there will be a select dropdown with all possible enum values for that field on `Insert Row` and `Edit Row` views.
- console: generate unique exported metadata filenames (close #1772) (#4106)
Exporting metadata from the console will now generate metadata files of the form `hasura_metadata_<timestamp>.json`.
### Other changes
- console: disable editing action relationships
- cli: fix typo in cli example for squash (fix #4047) (#4049)
- console: fix run_sql migration modal messaging (close #4020) (#4060)
- docs: add note on pg versions for actions (#4034)
@ -81,5 +83,12 @@
- add meta descriptions to actions docs (#4082)
- `HASURA_GRAPHQL_EVENTS_FETCH_INTERVAL` changes semantics slightly: we only sleep for the interval
when there were previously no events to process. Potential space leak fixed. (#3839)
- console: track runtime errors (#4083)
- auto-include `__typename` field in custom types' objects (fix #4063)
- fix postgres query error when computed fields included in mutation response (fix #4035)
- squash some potential space leaks (#3937)
- docs: bump MarupSafe version (#4102)
- server: validate action webhook response to conform to action output type (fix #3977)
- server: preserve cookie headers from sync action webhook (close #4021)
- server: add 'ID' to default scalars in custom types (fix #4061)
- console: add design system base components (#3866)
- server: fix postgres query error when computed fields included in mutation response (fix #4035)

View File

@ -5,6 +5,7 @@
"@babel/preset-typescript"
],
"plugins": [
"babel-plugin-styled-components",
"transform-react-remove-prop-types",
"@babel/plugin-proposal-class-properties",
"@babel/plugin-syntax-dynamic-import",

View File

@ -1,13 +1,14 @@
{ "extends": "eslint-config-airbnb",
{
"extends": ["plugin:@typescript-eslint/recommended", "eslint-config-airbnb"],
"env": {
"browser": true,
"node": true,
"mocha": true,
"cypress/globals": true
},
"parser": "babel-eslint",
"parser": "@typescript-eslint/parser",
"rules": {
"allowForLoopAfterthoughts": true,
"allowForLoopAfterthoughts": 0,
"react/no-multi-comp": 0,
"import/default": 0,
"import/no-duplicates": 0,
@ -20,8 +21,8 @@
"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}],
"id-length": [1, { "min": 1, "properties": "never" }],
"indent": [2, 2, { "SwitchCase": 1 }],
"no-console": 0,
"arrow-parens": 0,
"no-alert": 0,
@ -47,7 +48,7 @@
"camelcase": 0,
"object-curly-newline": 0,
"spaced-comment": 0,
"prefer-destructuring": ["error", {"object": false, "array": false}],
"prefer-destructuring": ["error", { "object": false, "array": false }],
"prefer-rest-params": 0,
"function-paren-newline": 0,
"no-case-declarations": 0,
@ -82,12 +83,25 @@
"max-len": 0,
"no-continue": 0,
"eqeqeq": 0,
"no-nested-ternary": 0
"no-nested-ternary": 0,
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": 2,
"@typescript-eslint/indent": 0,
"@typescript-eslint/explicit-function-return-type": 0,
"@typescript-eslint/prefer-interface": 0,
"@typescript-eslint/no-namespace": "off",
"@typescript-eslint/camelcase": 0,
"@typescript-eslint/explicit-member-accessibility": 0,
"@typescript-eslint/no-non-null-assertion": 0,
"@typescript-eslint/no-object-literal-type-assertion": "off",
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/no-parameter-properties": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/no-useless-constructor": "error",
"@typescript-eslint/no-unused-expressions": ["error"]
},
"plugins": [
"react", "import", "cypress"
],
"plugins": ["react", "import", "cypress", "@typescript-eslint/eslint-plugin"],
"settings": {
"import/parser": "babel-eslint",
"parser": "babel-esling",
@ -103,6 +117,30 @@
"__DEVTOOLS__": true,
"socket": true,
"webpackIsomorphicTools": true,
"CONSOLE_ASSET_VERSION": true
}
"CONSOLE_ASSET_VERSION": true
},
"overrides": [
{
"files": ["*.ts", "*.tsx"],
"rules": {
/**
* Disable things that are checked by Typescript
*/
"import/no-unresolved": 0,
"getter-return": "off",
"no-dupe-args": "off",
"no-dupe-keys": "off",
"no-unreachable": "off",
"valid-typeof": "off",
"no-const-assign": "off",
"no-new-symbol": "off",
"no-this-before-super": "off",
"no-undef": "off",
"no-dupe-class-members": "off",
"no-redeclare": "off",
"no-useless-constructor": "off",
"no-unused-expressions": "off"
}
}
]
}

2556
console/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -27,11 +27,11 @@
"test": "cypress run --spec 'cypress/integration/**/**/test.js' --key $CYPRESS_KEY --parallel --record"
},
"lint-staged": {
"*.js": [
"*.{js,ts,tsx}": [
"eslint --fix",
"git add"
],
"*.{js,json,css,md}": [
"*.{js,json,ts,tsx,css,md}": [
"prettier --single-quote --trailing-comma es5 --write",
"git add"
]
@ -86,6 +86,7 @@
"react-copy-to-clipboard": "^5.0.0",
"react-dom": "16.8.6",
"react-helmet": "^5.2.0",
"react-icons": "^3.9.0",
"react-modal": "^3.1.2",
"react-notification-system": "^0.2.17",
"react-notification-system-redux": "^1.2.0",
@ -101,6 +102,8 @@
"redux-thunk": "^2.2.0",
"sanitize-filename": "^1.6.1",
"semver": "5.5.1",
"styled-components": "^5.0.1",
"styled-system": "^5.1.5",
"subscriptions-transport-ws": "^0.9.12",
"uuid": "^3.0.1",
"valid-url": "^1.0.9"
@ -128,11 +131,44 @@
"@babel/preset-typescript": "^7.8.3",
"@babel/register": "^7.0.0",
"@babel/runtime": "^7.0.0",
"@types/clean-webpack-plugin": "^0.1.3",
"@types/concurrently": "^5.1.0",
"@types/cypress": "^1.1.3",
"@types/dotenv": "^8.2.0",
"@types/express": "^4.17.3",
"@types/express-session": "^1.17.0",
"@types/extract-text-webpack-plugin": "^3.0.4",
"@types/file-loader": "^4.2.0",
"@types/fork-ts-checker-webpack-plugin": "^0.4.5",
"@types/jquery": "^3.3.33",
"@types/mini-css-extract-plugin": "^0.9.1",
"@types/node-sass": "^4.11.0",
"@types/optimize-css-assets-webpack-plugin": "^5.0.1",
"@types/react": "^16.9.23",
"@types/react-addons-test-utils": "^0.14.25",
"@types/react-dom": "^16.9.5",
"@types/react-helmet": "^5.0.15",
"@types/react-hot-loader": "^4.1.1",
"@types/react-redux": "^7.1.7",
"@types/react-router": "^5.1.4",
"@types/react-router-redux": "^5.0.18",
"@types/redux-devtools": "^3.0.47",
"@types/redux-devtools-dock-monitor": "^1.1.33",
"@types/redux-devtools-log-monitor": "^1.0.34",
"@types/redux-logger": "^3.0.7",
"@types/sinon": "^7.5.2",
"@types/terser-webpack-plugin": "^2.2.0",
"@types/unused-files-webpack-plugin": "^3.4.1",
"@types/webpack": "^4.41.7",
"@types/webpack-bundle-analyzer": "^2.13.3",
"@types/webpack-dev-middleware": "^3.7.0",
"@types/webpack-hot-middleware": "^2.25.0",
"@typescript-eslint/eslint-plugin": "^2.24.0",
"@typescript-eslint/parser": "^2.24.0",
"babel-eslint": "^9.0.0",
"babel-loader": "^8.0.0",
"babel-plugin-istanbul": "^5.1.1",
"babel-plugin-styled-components": "^1.10.6",
"babel-plugin-transform-react-remove-prop-types": "^0.4.10",
"babel-plugin-typecheck": "^2.0.0",
"better-npm-run": "^0.1.0",
@ -143,7 +179,7 @@
"css-loader": "^0.28.11",
"cypress": "^3.2.0",
"dotenv": "^5.0.1",
"eslint": "^4.19.1",
"eslint": "^6.5.1",
"eslint-config-airbnb": "16.1.0",
"eslint-loader": "^1.0.0",
"eslint-plugin-chai-friendly": "^0.4.1",

View File

@ -42,6 +42,7 @@ const globals = {
featuresCompatibility: window.__env.serverVersion
? getFeaturesCompatibility(window.__env.serverVersion)
: null,
isProduction,
};
if (globals.consoleMode === SERVER_CONSOLE_MODE) {

View File

@ -19,7 +19,9 @@ import getRoutes from './routes';
import reducer from './reducer';
import globals from './Globals';
import Endpoints from './Endpoints';
import { filterEventsBlockList, sanitiseUrl } from './telemetryFilter';
import { RUN_TIME_ERROR } from './components/Main/Actions';
/** telemetry **/
let analyticsConnection;
@ -57,39 +59,47 @@ function analyticsLogger({ getState }) {
return next => action => {
// Call the next dispatch method in the middleware chain.
const returnValue = next(action);
// check if analytics tracking is enabled
if (telemetryEnabled) {
const serverVersion = getState().main.serverVersion;
const actionType = action.type;
const url = sanitiseUrl(window.location.pathname);
const reqBody = {
server_version: serverVersion,
event_type: actionType,
url,
console_mode: consoleMode,
cli_uuid: cliUUID,
server_uuid: getState().telemetry.hasura_uuid,
};
let isLocationType = false;
if (actionType === '@@router/LOCATION_CHANGE') {
isLocationType = true;
}
// filter events
if (!filterEventsBlockList.includes(actionType)) {
// When the connection is open, send data to the server
if (
analyticsConnection &&
analyticsConnection.readyState === analyticsConnection.OPEN
) {
// When the connection is open, send data to the server
const serverVersion = getState().main.serverVersion;
const url = sanitiseUrl(window.location.pathname);
const reqBody = {
server_version: serverVersion,
event_type: actionType,
url,
console_mode: consoleMode,
cli_uuid: cliUUID,
server_uuid: getState().telemetry.hasura_uuid,
};
const isLocationType = actionType === '@@router/LOCATION_CHANGE';
if (isLocationType) {
// capture page views
const payload = action.payload;
reqBody.url = sanitiseUrl(payload.pathname);
}
const isErrorType = actionType === RUN_TIME_ERROR;
if (isErrorType) {
reqBody.data = action.data;
}
// Send the data
analyticsConnection.send(
JSON.stringify({ data: reqBody, topic: globals.telemetryTopic })
); // Send the data
);
// check for possible error events and store more data?
} else {
// retry websocket connection
@ -97,6 +107,7 @@ function analyticsLogger({ getState }) {
}
}
}
// This will likely be the action itself, unless
// a middleware further in chain changed it.
return returnValue;

View File

@ -4,6 +4,8 @@ import { connect } from 'react-redux';
import ProgressBar from 'react-progress-bar-plus';
import Notifications from 'react-notification-system-redux';
import { hot } from 'react-hot-loader';
import { ThemeProvider } from 'styled-components';
import ErrorBoundary from '../Error/ErrorBoundary';
import {
loadConsoleOpts,
@ -11,6 +13,8 @@ import {
} from '../../telemetry/Actions';
import { showTelemetryNotification } from '../../telemetry/Notifications';
import { theme } from '../UIKit/theme';
class App extends Component {
componentDidMount() {
const { dispatch } = this.props;
@ -71,21 +75,23 @@ class App extends Component {
}
return (
<ErrorBoundary metadata={metadata} dispatch={dispatch}>
<div>
{connectionFailMsg}
{ongoingRequest && (
<ProgressBar
percent={percent}
autoIncrement={true} // eslint-disable-line react/jsx-boolean-value
intervalTime={intervalTime}
spinner={false}
/>
)}
<div>{children}</div>
<Notifications notifications={notifications} />
</div>
</ErrorBoundary>
<ThemeProvider theme={theme}>
<ErrorBoundary metadata={metadata} dispatch={dispatch}>
<div>
{connectionFailMsg}
{ongoingRequest && (
<ProgressBar
percent={percent}
autoIncrement={true} // eslint-disable-line react/jsx-boolean-value
intervalTime={intervalTime}
spinner={false}
/>
)}
<div>{children}</div>
<Notifications notifications={notifications} />
</div>
</ErrorBoundary>
</ThemeProvider>
);
}
}

View File

@ -17,9 +17,6 @@ const Editor = ({ mode, ...props }) => {
tabSize={2}
setOptions={{
showLineNumbers: true,
enableBasicAutocompletion: true,
enableSnippets: true,
behavioursEnabled: true,
}}
{...props}
/>

View File

@ -1,10 +1,23 @@
@import url('https://fonts.googleapis.com/css?family=Gudea:400,700');
@import url('https://fonts.googleapis.com/css?family=Roboto:300,400,500,700,900');
body {
background-color: #f8fafb;
}
h1, .h1, h2, .h2, h3, .h3, h4, .h4, h5, .h5, h6, .h6 {
h1,
.h1,
h2,
.h2,
h3,
.h3,
h4,
.h4,
h5,
.h5,
h6,
.h6 {
margin-top: 0;
margin-bottom: 0;
-webkit-margin-before: 0;
@ -25,9 +38,9 @@ h1, .h1, h2, .h2, h3, .h3, h4, .h4, h5, .h5, h6, .h6 {
}
input::-moz-focus-inner {
outline: 0;
-moz-outline: none;
border:0;
outline: 0;
-moz-outline: none;
border: 0;
}
select:-moz-focusring {
@ -36,12 +49,15 @@ select:-moz-focusring {
}
select::-moz-focus-inner {
outline: 0;
-moz-outline: none;
border: 0;
outline: 0;
-moz-outline: none;
border: 0;
}
option::-moz-focus-inner { border: 0; outline: 0 }
option::-moz-focus-inner {
border: 0;
outline: 0;
}
table {
font-size: 14px;
@ -49,8 +65,8 @@ table {
table thead tr th,
table tbody tr th {
background-color: #F2F2F2 !important;
color: #4D4D4D;
background-color: #f2f2f2 !important;
color: #4d4d4d;
font-weight: 600 !important;
}
@ -69,7 +85,7 @@ table tbody tr th {
// background: #444;
// color: $navbar-inverse-color;
color: #333;
border: 1px solid #E5E5E5;
border: 1px solid #e5e5e5;
border-bottom: 0px;
// background-color: #F8F8F8;
background-color: #fff;
@ -96,12 +112,12 @@ table tbody tr th {
li {
transition: color 0.5s;
font-size: 16px;
border-bottom: 1px solid #E6E6E6;
border-bottom: 1px solid #e6e6e6;
padding: 0px 0px;
// border-left: 5px solid transparent;
a {
color: #767E93;
color: #767e93;
background-color: #f0f0f0;
width: 100%;
word-wrap: break-word;
@ -126,7 +142,7 @@ table tbody tr th {
a {
text-decoration: none;
background-color: #FFF3D5;
background-color: #fff3d5;
display: inline-block;
}
}
@ -319,18 +335,18 @@ input {
.sidebarCreateTable {
a {
color: #767E93;
color: #767e93;
i {
color: #767E93;
color: #767e93;
}
}
a:hover {
color: #767E93;
color: #767e93;
i {
color: #767E93;
color: #767e93;
}
}
}
@ -376,7 +392,7 @@ input {
}
.ApiExplorerTableDefault {
background-color: #F3F3F3;
background-color: #f3f3f3;
}
td {
@ -403,7 +419,7 @@ input {
}
.ApiExplorerTableDefault {
background-color: #F3F3F3;
background-color: #f3f3f3;
}
}
}
@ -576,7 +592,7 @@ input {
.code_space {
padding: 3px 10px;
border: 1px solid #F9EAEF;
border: 1px solid #f9eaef;
}
code {
@ -622,7 +638,6 @@ code {
padding-bottom: 20px;
}
.add_padd_bottom {
padding-bottom: 25px;
}
@ -635,7 +650,6 @@ code {
padding-top: 20px !important;
}
.clear_fix {
clear: both;
}
@ -664,7 +678,7 @@ code {
.response_btn_success {
button {
padding: 2px 4px;
background-color: #59A21C;
background-color: #59a21c;
border: 1px solid #539719;
color: #fff;
font-size: 13px;
@ -679,7 +693,7 @@ code {
.response_btn_error {
button {
padding: 2px 4px;
background-color: #AC2925;
background-color: #ac2925;
border: 1px solid #761c19;
color: #fff;
font-size: 13px;
@ -694,8 +708,8 @@ code {
.response_btn_default {
button {
padding: 2px 4px;
background-color: #E6E6E6;
border: 1px solid #D7D7D7;
background-color: #e6e6e6;
border: 1px solid #d7d7d7;
color: #000;
font-size: 13px;
font-weight: 600;
@ -709,7 +723,7 @@ code {
.response_btn_success {
button {
padding: 2px 4px;
background-color: #59A21C;
background-color: #59a21c;
border: 1px solid #539719;
color: #fff;
font-size: 13px;
@ -724,7 +738,7 @@ code {
.response_btn_error {
button {
padding: 2px 4px;
background-color: #AC2925;
background-color: #ac2925;
border: 1px solid #761c19;
color: #fff;
font-size: 13px;
@ -739,8 +753,8 @@ code {
.response_btn_default {
button {
padding: 2px 4px;
background-color: #E6E6E6;
border: 1px solid #D7D7D7;
background-color: #e6e6e6;
border: 1px solid #d7d7d7;
color: #000;
font-size: 13px;
font-weight: 600;
@ -753,7 +767,7 @@ code {
.input_group_input {
border: none;
background-color: #F0F4F7 !important;
background-color: #f0f4f7 !important;
box-shadow: none;
}
@ -798,7 +812,7 @@ code {
padding: 20px;
background-color: #fff;
width: 80%;
border: 1px solid #F0F4F7;
border: 1px solid #f0f4f7;
margin-bottom: 20px;
}
@ -810,17 +824,17 @@ code {
}
.yellow_button {
background-color: #FEC53D;
background-color: #fec53d;
border-radius: 3px;
color: #606060;
border: 1px solid #FEC53D;
border: 1px solid #fec53d;
padding: 5px 10px;
font-size: 13px;
font-weight: 600;
line-height: 1.5;
&:hover {
background-color: #F2B130;
background-color: #f2b130;
}
}
@ -833,7 +847,7 @@ code {
.yellow_button:focus {
outline: none;
background-color: #F2B130;
background-color: #f2b130;
}
.default_button {
@ -874,22 +888,22 @@ code {
}
.exploreButton {
background-color: #FEC53D;
background-color: #fec53d;
border-radius: 5px;
color: #000;
border: 1px solid #FEC53D;
border: 1px solid #fec53d;
padding: 5px 10px;
font-size: 13px;
font-weight: 600;
line-height: 1.5;
&:hover {
background-color: #F2B130;
background-color: #f2b130;
color: #fff;
}
&:focus {
background-color: #F2B130;
background-color: #f2b130;
}
}
@ -898,14 +912,15 @@ code {
}
.text_gray {
color: #767E96
color: #767e96;
}
.text_link {
color: #337ab7;
}
.text_link:hover, .text_link:focus {
.text_link:hover,
.text_link:focus {
color: #23527c;
}
@ -987,7 +1002,7 @@ code {
.heading_text {
font-size: 18px;
font-weight: bold;
padding-bottom: 20px
padding-bottom: 20px;
}
.editable_heading_text {
@ -1054,13 +1069,12 @@ code {
button {
padding: 3px 8px;
background-color: #27AE60;
border: 1px solid #27AE60;
background-color: #27ae60;
border: 1px solid #27ae60;
font-size: 13px;
font-weight: 600;
color: #fff;
border-radius: 5px;
}
}
@ -1074,7 +1088,7 @@ code {
}
.nav {
border-bottom: 1px solid #E6E6E6;
border-bottom: 1px solid #e6e6e6;
margin-left: -15px !important;
padding-left: 15px !important;
@ -1110,12 +1124,12 @@ code {
border-radius: 4px;
background-color: #f8fafb;
/* color: #333; */
border: 1px solid #E6E6E6;
border: 1px solid #e6e6e6;
border-bottom: 0;
border-radius: 4px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
border-top: 3px solid #FFC627;
border-top: 3px solid #ffc627;
a {
color: #333;
@ -1127,7 +1141,7 @@ code {
}
.common_nav {
border-bottom: 1px solid #E6E6E6;
border-bottom: 1px solid #e6e6e6;
margin-left: -15px !important;
padding-left: 15px !important;
@ -1163,12 +1177,12 @@ code {
border-radius: 4px;
background-color: #f8fafb;
/* color: #333; */
border: 1px solid #E6E6E6;
border: 1px solid #e6e6e6;
border-bottom: 0;
border-radius: 4px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
border-top: 3px solid #FFC627;
border-top: 3px solid #ffc627;
a {
color: #333;
@ -1243,7 +1257,7 @@ code {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #262A35;
background-color: #262a35;
margin: 0 5px;
display: inline-block;
}
@ -1252,7 +1266,7 @@ code {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #D8D8D8;
background-color: #d8d8d8;
margin: 0 5px;
display: inline-block;
}
@ -1366,7 +1380,7 @@ code {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 15px;
margin-bottom: 15px;
}
.pkEditorExpandedText {
@ -1386,4 +1400,3 @@ $mainContainerHeight: calc(100vh - 50px - 25px);
/* Min container width below which horizontal scroll will appear */
$minContainerWidth: 1200px;

View File

@ -2,10 +2,10 @@ $suggestion-width: 280px;
$set-top: 34px;
$suggestion-padding: 6px 12px;
.container {
.container {
position: relative;
}
.input {
.input {
border: 1px solid #aaa;
border-radius: 4px;
}
@ -57,16 +57,16 @@ $suggestion-padding: 6px 12px;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-tap-highlight-color: rgba(0,0,0,0);
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
box-sizing: border-box;
&:hover {
background-color: #DEEBFF;
background-color: #deebff;
}
}
.suggestionHighlighted {
background-color: #DEEBFF;
background-color: #deebff;
}
.sectionTitle {

View File

@ -76,7 +76,10 @@ const Headers = ({ headers, setHeaders }) => {
};
return (
<div className={`${styles.display_flex} ${styles.add_mar_bottom_mid}`}>
<div
className={`${styles.display_flex} ${styles.add_mar_bottom_mid}`}
key={i}
>
{getHeaderNameInput()}
{getHeaderValueInput()}
{getRemoveButton()}

View File

@ -22,144 +22,152 @@
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;
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);
-ms-transform: rotate(30deg);
transform: rotate(30deg);
}
.sk_circle .sk_circle3 {
-webkit-transform: rotate(60deg);
-ms-transform: rotate(60deg);
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);
-ms-transform: rotate(90deg);
transform: rotate(90deg);
}
.sk_circle .sk_circle5 {
-webkit-transform: rotate(120deg);
-ms-transform: rotate(120deg);
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);
-ms-transform: rotate(150deg);
transform: rotate(150deg);
}
.sk_circle .sk_circle7 {
-webkit-transform: rotate(180deg);
-ms-transform: rotate(180deg);
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);
-ms-transform: rotate(210deg);
transform: rotate(210deg);
}
.sk_circle .sk_circle9 {
-webkit-transform: rotate(240deg);
-ms-transform: rotate(240deg);
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); }
-ms-transform: rotate(270deg);
transform: rotate(270deg);
}
.sk_circle .sk_circle11 {
-webkit-transform: rotate(300deg);
-ms-transform: rotate(300deg);
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); }
-ms-transform: rotate(330deg);
transform: rotate(330deg);
}
.sk_circle .sk_circle2:before {
-webkit-animation-delay: -1.1s;
animation-delay: -1.1s;
animation-delay: -1.1s;
}
.sk_circle .sk_circle3:before {
-webkit-animation-delay: -1s;
animation-delay: -1s;
animation-delay: -1s;
}
.sk_circle .sk_circle4:before {
-webkit-animation-delay: -0.9s;
animation-delay: -0.9s;
animation-delay: -0.9s;
}
.sk_circle .sk_circle5:before {
-webkit-animation-delay: -0.8s;
animation-delay: -0.8s;
animation-delay: -0.8s;
}
.sk_circle .sk_circle6:before {
-webkit-animation-delay: -0.7s;
animation-delay: -0.7s;
animation-delay: -0.7s;
}
.sk_circle .sk_circle7:before {
-webkit-animation-delay: -0.6s;
animation-delay: -0.6s;
animation-delay: -0.6s;
}
.sk_circle .sk_circle8:before {
-webkit-animation-delay: -0.5s;
animation-delay: -0.5s;
animation-delay: -0.5s;
}
.sk_circle .sk_circle9:before {
-webkit-animation-delay: -0.4s;
animation-delay: -0.4s;
animation-delay: -0.4s;
}
.sk_circle .sk_circle10:before {
-webkit-animation-delay: -0.3s;
animation-delay: -0.3s;
animation-delay: -0.3s;
}
.sk_circle .sk_circle11:before {
-webkit-animation-delay: -0.2s;
animation-delay: -0.2s;
animation-delay: -0.2s;
}
.sk_circle .sk_circle12:before {
-webkit-animation-delay: -0.1s;
animation-delay: -0.1s;
animation-delay: -0.1s;
}
@-webkit-keyframes sk_circleBounceDelay {
0%, 80%, 100% {
0%,
80%,
100% {
-webkit-transform: scale(0);
transform: scale(0);
} 40% {
transform: scale(0);
}
40% {
-webkit-transform: scale(1);
transform: scale(1);
transform: scale(1);
}
}
@keyframes sk_circleBounceDelay {
0%, 80%, 100% {
0%,
80%,
100% {
-webkit-transform: scale(0);
transform: scale(0);
} 40% {
-webkit-transform: scale(1);
transform: scale(1);
transform: scale(0);
}
}
40% {
-webkit-transform: scale(1);
transform: scale(1);
}
}

View File

@ -120,6 +120,7 @@
.ReactTable .rt-table {
overflow: unset;
}
.ReactTable .rt-table .rt-tbody {
margin-bottom: 10px;
border: solid 1px rgba(0, 0, 0, 0.05);

View File

@ -242,6 +242,45 @@ export const getFileExtensionFromFilename = filename => {
return filename.match(/\.[0-9a-z]+$/i)[0];
};
// return time in format YYYY_MM_DD_hh_mm_ss_s
export const getCurrTimeForFileName = () => {
const currTime = new Date();
const year = currTime
.getFullYear()
.toString()
.padStart(4, '0');
const month = (currTime.getMonth() + 1).toString().padStart(2, '0');
const day = currTime
.getDate()
.toString()
.padStart(2, '0');
const hours = currTime
.getHours()
.toString()
.padStart(2, '0');
const minutes = currTime
.getMinutes()
.toString()
.padStart(2, '0');
const seconds = currTime
.getSeconds()
.toString()
.padStart(2, '0');
const milliSeconds = currTime
.getMilliseconds()
.toString()
.padStart(3, '0');
return [year, month, day, hours, minutes, seconds, milliSeconds].join('_');
};
export const isValidTemplateLiteral = literal_ => {
const literal = literal_.trim();
if (!literal) return false;

View File

@ -9,6 +9,7 @@ import Spinner from '../Common/Spinner/Spinner';
import PageNotFound, { NotFoundError } from './PageNotFound';
import RuntimeError from './RuntimeError';
import { registerRunTimeError } from '../Main/Actions';
class ErrorBoundary extends React.Component {
initialState = {
@ -40,6 +41,11 @@ class ErrorBoundary extends React.Component {
this.setState({ hasError: true, info: info, error: error });
// trigger telemetry
dispatch(
registerRunTimeError({ message: error.message, stack: error.stack })
);
dispatch(loadInconsistentObjects(true)).then(() => {
if (this.props.metadata.inconsistentObjects.length > 0) {
if (!isMetadataStatusPage()) {

View File

@ -99,9 +99,7 @@ const Login = ({ dispatch }) => {
type="checkbox"
checked={shouldPersist}
onChange={toggleShouldPersist}
className={`${styles.add_mar_right_small} ${
styles.remove_margin_top
} ${styles.cursorPointer}`}
className={`${styles.add_mar_right_small} ${styles.remove_margin_top} ${styles.cursorPointer}`}
/>
Remember in this browser
</label>

View File

@ -1,4 +1,4 @@
@import "../Common/Common.scss";
@import '../Common/Common.scss';
.container {
display: flex;
@ -26,8 +26,8 @@
}
.mainWrapper {
background-color: #F8FAFB;
}
background-color: #f8fafb;
}
.input_addon_group {
margin-bottom: 10px;
padding: 0 15px;
@ -58,18 +58,18 @@
}
.login_btn {
padding: 0 15px;
padding-bottom: 15px;
padding-top: 10px;
padding: 0 15px;
padding-bottom: 15px;
padding-top: 10px;
button {
background-color: #27AE60;
height: 60px;
border: 0;
border-radius: 5px;
color: #fff;
text-transform: uppercase;
}
button {
background-color: #27ae60;
height: 60px;
border: 0;
border-radius: 5px;
color: #fff;
text-transform: uppercase;
}
}
.loginHeading {
@ -95,4 +95,4 @@
.loginCenter {
margin-top: -100px;
}
}

View File

@ -22,6 +22,12 @@ const UPDATE_ADMIN_SECRET_INPUT = 'Main/UPDATE_ADMIN_SECRET_INPUT';
const LOGIN_IN_PROGRESS = 'Main/LOGIN_IN_PROGRESS';
const LOGIN_ERROR = 'Main/LOGIN_ERROR';
const RUN_TIME_ERROR = 'Main/RUN_TIME_ERROR';
const registerRunTimeError = data => ({
type: RUN_TIME_ERROR,
data,
});
/* Server config constants*/
const FETCHING_SERVER_CONFIG = 'Main/FETCHING_SERVER_CONFIG';
const SERVER_CONFIG_FETCH_SUCCESS = 'Main/SERVER_CONFIG_FETCH_SUCCESS';
@ -273,6 +279,8 @@ const mainReducer = (state = defaultState, action) => {
return { ...state, loginInProgress: action.data };
case LOGIN_ERROR:
return { ...state, loginError: action.data };
case RUN_TIME_ERROR: // To trigger telemetry event
return state;
case FETCHING_SERVER_CONFIG:
return {
...state,
@ -327,4 +335,6 @@ export {
fetchServerConfig,
loadLatestServerVersion,
featureCompatibilityInit,
RUN_TIME_ERROR,
registerRunTimeError,
};

View File

@ -1,9 +1,9 @@
@import "~bootstrap-sass/assets/stylesheets/bootstrap/variables";
@import "../Common/Common.scss";
@import '~bootstrap-sass/assets/stylesheets/bootstrap/variables';
@import '../Common/Common.scss';
@font-face {
font-family: arcadeClassic;
src: url(https://storage.googleapis.com/hasura-graphql-engine/console/assets/ARCADECLASSIC.ttf);
font-family: arcadeClassic;
src: url(https://storage.googleapis.com/hasura-graphql-engine/console/assets/ARCADECLASSIC.ttf);
}
.container {
@ -22,15 +22,15 @@
}
.updateBannerWrapper {
position: fixed;
right: 0;
bottom: 0px;
width: 100%;
z-index: 10;
text-align: center;
background-color: #FFC627;
color: #43495a;
padding: 14.5px 10px;
position: fixed;
right: 0;
bottom: 0px;
width: 100%;
z-index: 10;
text-align: center;
background-color: #ffc627;
color: #43495a;
padding: 14.5px 10px;
.updateBanner {
display: flex;
@ -152,7 +152,7 @@
.clusterBtn {
background-color: transparent;
color: #DEDEDE;
color: #dedede;
font-weight: 700;
pointer-events: none;
@ -187,7 +187,8 @@
z-index: 101;
}
.setting_wrapper, .clusterInfoWrapper {
.setting_wrapper,
.clusterInfoWrapper {
.setting_dropdown {
.dropdown_menu {
left: inherit;
@ -208,20 +209,20 @@
}
.bubble {
display: inline-block;
font-size: 16px;
-webkit-transition: all 0.2s;
-moz-transition: all 0.2s;
-o-transition: all 0.2s;
transition: all 0.2s;
padding: 15px 60px;
color: #fff;
background-color: #FEC53D;
-webkit-animation: pulse 1s ease infinite;
-moz-animation: pulse 1s ease infinite;
-ms-animation: pulse 1s ease infinite;
-o-animation: pulse 1s ease infinite;
animation: pulse 1s ease infinite;
display: inline-block;
font-size: 16px;
-webkit-transition: all 0.2s;
-moz-transition: all 0.2s;
-o-transition: all 0.2s;
transition: all 0.2s;
padding: 15px 60px;
color: #fff;
background-color: #fec53d;
-webkit-animation: pulse 1s ease infinite;
-moz-animation: pulse 1s ease infinite;
-ms-animation: pulse 1s ease infinite;
-o-animation: pulse 1s ease infinite;
animation: pulse 1s ease infinite;
}
.onBoardingHighlight {
@ -243,7 +244,7 @@
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
background-color: #4D4D4D;
background-color: #4d4d4d;
}
.onBoardingDataFocus {
@ -298,7 +299,7 @@
}
.onBoardingHighlightInterior {
background-color: #4D4D4D;
background-color: #4d4d4d;
position: relative;
width: 300px;
border-radius: 5px;
@ -312,7 +313,7 @@
top: 126px;
}
.onBoardingBuilderPosition{
.onBoardingBuilderPosition {
left: 45%;
top: 320px;
}
@ -371,7 +372,7 @@
font-size: 25px;
font-family: 'Gudea';
font-weight: 400;
color: #FEC53D;
color: #fec53d;
padding-top: 20px;
padding-bottom: 20px;
}
@ -413,7 +414,7 @@
}
.sidebar {
color: #DEDEDE;
color: #dedede;
background-color: #43495a;
font-family: 'Gudea';
font-weight: 700;
@ -439,12 +440,13 @@
margin-bottom: 0;
margin-top: 16px;
a,a:visited {
color: #DEDEDE;
a,
a:visited {
color: #dedede;
}
a:hover {
color: #DEDEDE;
color: #dedede;
}
li {
@ -513,15 +515,15 @@
background-color: #515766;
li {
a {
padding: 6px 15px;
}
a {
padding: 6px 15px;
}
}
hr {
margin-top: 6px;
margin-bottom: 6px;
}
hr {
margin-top: 6px;
margin-bottom: 6px;
}
}
}
@ -536,14 +538,14 @@
}
.navSideBarActive {
border-bottom: 4px solid #FFC627;
border-bottom: 4px solid #ffc627;
i {
color: #FFC627;
color: #ffc627;
}
p {
color: #FFC627;
color: #ffc627;
}
}
@ -624,7 +626,7 @@
clear: both;
font-family: 'Gudea';
// color: #767E93;
color: #4D4D4D ;
color: #4d4d4d;
padding-top: 50px;
}
@ -674,8 +676,8 @@
button {
background-color: transparent;
border-radius: 5px;
color: #DEDEDE;
border: 1px solid #DEDEDE;
color: #dedede;
border: 1px solid #dedede;
padding: 4px 10px;
}
}
@ -736,7 +738,6 @@
float: right;
padding-bottom: 10px;
}
}
.exploreDisabled {
@ -744,7 +745,7 @@
}
.exploreSidebar {
box-shadow: -3px 7px 14px 0px rgba(222,222,222,1);
box-shadow: -3px 7px 14px 0px rgba(222, 222, 222, 1);
float: right;
width: 260px;
// background: beige;
@ -762,7 +763,7 @@
p {
font-size: 16px;
padding-top: 5px;
color: #AAAAAA;
color: #aaaaaa;
}
.exploreSidebarHeading {
@ -775,13 +776,13 @@
padding-top: 10px;
padding-bottom: 10px;
font-weight: bold;
color: #272B36;
color: #272b36;
}
.exploreSidebarDescription {
font-size: 16px;
padding-top: 10px;
color: #272B36;
color: #272b36;
height: calc(100vh - 260px);
overflow-y: auto;
@ -830,7 +831,7 @@
cursor: pointer;
i {
color: #AAAAAA;
color: #aaaaaa;
}
}
@ -843,17 +844,17 @@
}
.notificationCount {
position: absolute;
top: 0px;
right: 25px;
background-color: #FF993A;
width: 14px;
font-size: 10px;
height: 14px;
border-radius: 50%;
text-align: center;
vertical-align: middle;
color: #fff;
position: absolute;
top: 0px;
right: 25px;
background-color: #ff993a;
width: 14px;
font-size: 10px;
height: 14px;
border-radius: 50%;
text-align: center;
vertical-align: middle;
color: #fff;
}
.exploreSidebar.minimised {
@ -918,7 +919,7 @@
.selected {
bottom: 0;
background: #FFC627;
background: #ffc627;
position: absolute;
z-index: 999999999;
height: 4px;
@ -957,7 +958,7 @@
a {
//color: #FFC627;
color: #FFFFFF;
color: #ffffff;
text-decoration: none;
}
@ -969,27 +970,27 @@
@keyframes heartbeat {
0% {
transform: scale( .75 );
transform: scale(0.75);
}
20% {
transform: scale( 1 );
transform: scale(1);
}
40% {
transform: scale( .75 );
transform: scale(0.75);
}
60% {
transform: scale( 1 );
transform: scale(1);
}
80% {
transform: scale( .75 );
transform: scale(0.75);
}
100% {
transform: scale( .75 );
transform: scale(0.75);
}
}
@ -1086,7 +1087,7 @@
float: right;
// background-color: #0038d5;
background-color: #FFF;
background-color: #fff;
border: #43495a 2px dashed;
border-top: 0;
border-right: 0;
@ -1207,7 +1208,8 @@
.proWrapper {
position: relative;
padding: 12px 15px;
.proName, .proNameClicked {
.proName,
.proNameClicked {
font-size: 15px;
font-weight: bold;
font-stretch: normal;
@ -1216,7 +1218,7 @@
letter-spacing: 1px;
cursor: pointer;
&:hover {
opacity: .8;
opacity: 0.8;
}
}
.proName {
@ -1253,7 +1255,7 @@
right: 24px;
cursor: pointer;
&:hover {
opacity: .8;
opacity: 0.8;
}
}
}
@ -1279,7 +1281,7 @@
padding-bottom: 8px;
}
.featuresDescription {
opacity: .6;
opacity: 0.6;
}
}
}

View File

@ -50,10 +50,10 @@ const CodeTabs = ({
);
}
const files = codegenFiles.map(({ name, content }) => {
const files = codegenFiles.map(({ name, content }, i) => {
const getFileTab = (component, filename) => {
return (
<Tab eventKey={filename} title={filename}>
<Tab eventKey={filename} title={filename} key={i}>
{component}
</Tab>
);
@ -73,7 +73,7 @@ const CodeTabs = ({
}
});
return <Tabs id="uncontrolled-tab-example">{files} </Tabs>;
return <Tabs id="codegen-files-tabs">{files} </Tabs>;
};
export default CodeTabs;

View File

@ -40,7 +40,6 @@ export const getAllCodegenFrameworks = () => {
};
export const getCodegenFunc = framework => {
process.hrtime = () => null;
return fetch(getCodegenFilePath(framework))
.then(r => r.text())
.then(rawJsString => {

View File

@ -39,6 +39,7 @@ const HandlerEditor = ({
<input
type="checkbox"
checked={forwardClientHeaders}
readOnly
className={`${styles.add_mar_right_small}`}
/>
Forward client headers to webhook

View File

@ -35,6 +35,7 @@ const HandlerEditor = ({ value, onChange, className }) => {
<input
type="radio"
checked={value === 'synchronous'}
readOnly
className={styles.add_mar_right_small}
/>
Synchronous
@ -45,6 +46,7 @@ const HandlerEditor = ({ value, onChange, className }) => {
>
<input
type="radio"
readOnly
checked={value === 'asynchronous'}
className={styles.add_mar_right_small}
/>

View File

@ -138,7 +138,6 @@ const RelationshipEditor = ({
className={`${styles.select} form-control ${styles.add_pad_left}`}
placeholder="Enter relationship name"
data-test="rel-name"
disabled={isDisabled}
title={relNameInputTitle}
value={name}
/>
@ -191,17 +190,17 @@ const RelationshipEditor = ({
disabled={!name}
>
{// default unselected option
refSchema === '' && (
<option value={''} disabled>
{'-- reference schema --'}
</option>
)}
refSchema === '' && (
<option value={''} disabled>
{'-- reference schema --'}
</option>
)}
{// all reference schema options
orderedSchemaList.map((rs, j) => (
<option key={j} value={rs}>
{rs}
</option>
))}
orderedSchemaList.map((rs, j) => (
<option key={j} value={rs}>
{rs}
</option>
))}
</select>
</div>
);
@ -371,7 +370,7 @@ const RelationshipEditor = ({
};
const RelEditor = props => {
const { dispatch, relConfig, objectType } = props;
const { dispatch, relConfig, objectType, isNew } = props;
const [relConfigState, setRelConfigState] = React.useState(null);
@ -382,7 +381,7 @@ const RelEditor = props => {
<div>
<b>{relConfig.name}</b>
<div className={tableStyles.relationshipTopPadding}>
{getRelDef(relConfig)}
{getRelDef({ ...relConfig, typename: objectType.name })}
</div>
</div>
);
@ -418,20 +417,24 @@ const RelEditor = props => {
);
}
dispatch(
addActionRel({ ...relConfigState, typename: objectType.name }, toggle)
addActionRel(
{ ...relConfigState, typename: objectType.name },
toggle,
isNew ? null : relConfig
)
);
};
// function to remove the relationship
let removeFunc;
if (relConfig) {
if (!isNew) {
removeFunc = toggle => {
dispatch(removeActionRel(relConfig.name, objectType.name, toggle));
};
}
const expandButtonText = relConfig ? 'Edit' : 'Add a relationship';
const collapseButtonText = relConfig ? 'Close' : 'Cancel';
const expandButtonText = isNew ? 'Add a relationship' : 'Edit';
const collapseButtonText = isNew ? 'Cancel' : 'Close';
return (
<ExpandableEditor

View File

@ -34,6 +34,7 @@ const Relationships = ({
typename={objectType.name}
allTables={allTables}
schemaList={schemaList}
isNew
/>
</div>
);

View File

@ -76,7 +76,7 @@ export const getRelDef = relMeta => {
? `${relMeta.remote_table.schema}.${relMeta.remote_table.name}`
: relMeta.remote_table;
return `${lcol}${tableLabel} . ${rcol}`;
return `${relMeta.typename} . ${lcol}${tableLabel} . ${rcol}`;
};
export const removeTypeRelationship = (types, typename, relName) => {
@ -90,3 +90,15 @@ export const removeTypeRelationship = (types, typename, relName) => {
return t;
});
};
export const validateRelTypename = (types, typename, relname) => {
for (let i = types.length - 1; i >= 0; i--) {
const type = types[i];
if (type.kind === 'object' && type.name === typename) {
if ((type.relationships || []).some(r => r.name === relname)) {
return `Relationship with name "${relname}" already exists.`;
}
}
}
return null;
};

View File

@ -22,6 +22,7 @@ import {
import {
injectTypeRelationship,
removeTypeRelationship,
validateRelTypename,
} from './Relationships/utils';
import { getConfirmation } from '../../Common/utils/jsUtils';
import {
@ -406,11 +407,52 @@ export const deleteAction = currentAction => (dispatch, getState) => {
);
};
export const addActionRel = (relConfig, successCb) => (dispatch, getState) => {
export const addActionRel = (relConfig, successCb, existingRelConfig) => (
dispatch,
getState
) => {
const { types: existingTypes } = getState().types;
const typesWithRels = injectTypeRelationship(
existingTypes,
let typesWithRels = [...existingTypes];
let validationError;
if (existingRelConfig) {
// modifying existing relationship
// if the relationship is being renamed
if (existingRelConfig.name !== relConfig.name) {
// validate the new name
validationError = validateRelTypename(
existingTypes,
relConfig.typename,
relConfig.name
);
// remove old relationship from types
typesWithRels = removeTypeRelationship(
existingTypes,
relConfig.typename,
existingRelConfig.name
);
}
} else {
// creating a new relationship
// validate the relationship name
validationError = validateRelTypename(
existingTypes,
relConfig.typename,
relConfig.name
);
}
const errorMsg = 'Saving relationship failed';
if (validationError) {
return dispatch(showErrorNotification(errorMsg, validationError));
}
// add modified relationship to types
typesWithRels = injectTypeRelationship(
typesWithRels,
relConfig.typename,
relConfig
);
@ -426,10 +468,9 @@ export const addActionRel = (relConfig, successCb) => (dispatch, getState) => {
const upQueries = [customTypesQueryUp];
const downQueries = [customTypesQueryDown];
const migrationName = 'add_action_rel'; // TODO: better migration name
const requestMsg = 'Adding relationship...';
const successMsg = 'Relationship added successfully';
const errorMsg = 'Adding relationship failed';
const migrationName = `save_rel_${relConfig.name}_on_${relConfig.typename}`;
const requestMsg = 'Saving relationship...';
const successMsg = 'Relationship saved successfully';
const customOnSuccess = () => {
// dispatch(createActionRequestComplete());
dispatch(fetchCustomTypes());

View File

@ -204,9 +204,7 @@ const deleteRelMigrate = relMeta => (dispatch, getState) => {
const relChangesDown = [upQuery];
// Apply migrations
const migrationName = `drop_relationship_${relMeta.relName}_${
relMeta.lSchema
}_table_${relMeta.lTable}`;
const migrationName = `drop_relationship_${relMeta.relName}_${relMeta.lSchema}_table_${relMeta.lTable}`;
const requestMsg = 'Deleting Relationship...';
const successMsg = 'Relationship deleted';
@ -250,9 +248,7 @@ const addRelNewFromStateMigrate = () => (dispatch, getState) => {
const relChangesDown = [downQuery];
// Apply migrations
const migrationName = `add_relationship_${state.name}_table_${
state.lSchema
}_${state.lTable}`;
const migrationName = `add_relationship_${state.name}_table_${state.lSchema}_${state.lTable}`;
const requestMsg = 'Adding Relationship...';
const successMsg = 'Relationship created';
@ -568,9 +564,7 @@ const autoAddRelName = obj => (dispatch, getState) => {
const relChangesDown = [obj.downQuery];
// Apply migrations
const migrationName = `add_relationship_${relName}_table_${currentSchema}_${
obj.data.tableName
}`;
const migrationName = `add_relationship_${relName}_table_${currentSchema}_${obj.data.tableName}`;
const requestMsg = 'Adding Relationship...';
const successMsg = 'Relationship created';

View File

@ -6,9 +6,7 @@ const Logout = props => {
return (
<div
className={`${styles.clear_fix} ${styles.padd_left} ${styles.padd_top} ${
styles.metadata_wrapper
} container-fluid`}
className={`${styles.clear_fix} ${styles.padd_left} ${styles.padd_top} ${styles.metadata_wrapper} container-fluid`}
>
<div className={styles.subHeader}>
<h2 className={`${styles.heading_text} ${styles.remove_pad_bottom}`}>

View File

@ -7,7 +7,10 @@ import {
showErrorNotification,
} from '../../Common/Notification';
import { exportMetadata } from '../Actions';
import { downloadObjectAsJsonFile } from '../../../Common/utils/jsUtils';
import {
downloadObjectAsJsonFile,
getCurrTimeForFileName,
} from '../../../Common/utils/jsUtils';
class ExportMetadata extends Component {
constructor() {
@ -31,11 +34,19 @@ class ExportMetadata extends Component {
this.setState({ isExporting: true });
const successCallback = data => {
downloadObjectAsJsonFile('metadata', data);
const fileName =
'hasura_metadata_' + getCurrTimeForFileName() + '.json';
downloadObjectAsJsonFile(fileName, data);
this.setState({ isExporting: false });
dispatch(showSuccessNotification('Metadata exported successfully!'));
dispatch(
showSuccessNotification(
'Metadata exported successfully!',
`Metadata file "${fileName}"`
)
);
};
const errorCallback = error => {

View File

@ -41,7 +41,6 @@ const Sidebar = ({ location, metadata }) => {
title: 'Allowed Queries',
});
const adminSecret = getAdminSecret();
if (adminSecret && globals.consoleMode !== CLI_CONSOLE_MODE) {

View File

@ -0,0 +1,25 @@
import styled from 'styled-components';
import {
flexbox,
color,
border,
typography,
layout,
space,
shadow,
} from 'styled-system';
export const StyledAlertBox = styled.div`
${flexbox};
${color}
${border}
${typography}
${layout}
${space}
${shadow}
/* Alert type text */
span {
text-transform: capitalize;
}
`;

View File

@ -0,0 +1,56 @@
import React from 'react';
import { theme } from '../../theme';
import { Icon } from '../Icon';
import { StyledAlertBox } from './AlertBox';
import { Text } from '../Typography';
const alertBoxWidth = 866;
export const AlertBox = props => {
const { children, type } = props;
const backgroundColorValue = theme.alertBox[type]
? theme.alertBox[type].backgroundColor
: theme.alertBox.default.backgroundColor;
const borderColorValue = theme.alertBox[type]
? theme.alertBox[type].borderColor
: theme.alertBox.default.borderColor;
let alertMessage;
if (children) {
alertMessage = children;
} else {
alertMessage = theme.alertBox[type]
? theme.alertBox[type].message
: theme.alertBox.default.message;
}
return (
<StyledAlertBox
width={alertBoxWidth}
bg={backgroundColorValue}
borderRadius="xs"
fontSize="p"
borderLeft={4}
borderColor={borderColorValue}
boxShadow={2}
height="lg"
pl="md"
display="flex"
alignItems="center"
color="black.text"
{...props}
>
<Icon type={type} />
{type && (
<Text as="span" pl="md" fontWeight="medium">
{type}
</Text>
)}
<Text pl="md">{alertMessage}</Text>
</StyledAlertBox>
);
};

View File

@ -0,0 +1,45 @@
import styled, { css } from 'styled-components';
import {
layout,
space,
color,
border,
typography,
flexbox,
} from 'styled-system';
const hoverStyles = ({ type, theme, disabled, boxShadowColor }) => {
if (type === 'secondary' && !disabled) {
return css`
&:hover {
box-shadow: 0 2px 8px 0 ${boxShadowColor};
background: ${theme.colors.black.secondary};
color: ${theme.colors.white};
}
`;
} else if (!disabled) {
return css`
&:hover {
box-shadow: 0 2px 8px 0 ${boxShadowColor};
}
`;
}
return '';
};
export const StyledButton = styled.button`
appearance: button;
cursor: ${({ disabled }) => !disabled && 'pointer'};
&:disabled {
cursor: not-allowed;
}
${hoverStyles}
${layout}
${space}
${typography}
${color}
${border}
${flexbox}
`;

View File

@ -0,0 +1,62 @@
import React from 'react';
import { theme } from '../../theme';
import { Spinner } from '../Spinner';
import { StyledButton } from './Button';
export const Button = props => {
const { children, type, size, disabled, isLoading } = props;
const { button } = theme;
let colorValue;
let backgroundColorValue;
let boxShadowColorValue;
if (button[type]) {
colorValue = button[type].color;
backgroundColorValue = button[type].backgroundColor;
boxShadowColorValue = button[type].boxShadowColor;
} else {
colorValue = button.default.color;
backgroundColorValue = button.default.backgroundColor;
boxShadowColorValue = button.default.boxShadowColor;
}
const borderColorValue =
type === 'secondary' ? 'black.secondary' : backgroundColorValue;
const buttonHeight = size === 'large' ? 'lg' : 'sm';
const paddingX = size === 'large' ? 'lg' : 'md';
return (
<StyledButton
{...props}
height={buttonHeight}
px={paddingX}
opacity={disabled ? '0.5' : undefined}
color={colorValue}
bg={backgroundColorValue}
boxShadowColor={boxShadowColorValue}
fontSize="button"
fontWeight="bold"
display="flex"
justifyContent="center"
alignItems="center"
border={1}
borderRadius="xs"
borderColor={borderColorValue}
>
{children}
{isLoading && <Spinner size={size} ml={18} />}
</StyledButton>
);
};
Button.defaultProps = {
size: 'small',
isLoading: false,
disabled: false,
};

View File

@ -0,0 +1,72 @@
import styled from 'styled-components';
import {
color,
border,
typography,
layout,
space,
shadow,
} from 'styled-system';
export const StyledCheckBox = styled.div`
input[type='checkbox'] {
position: absolute;
opacity: 0;
& + label {
position: relative;
cursor: pointer;
}
& + label:before {
content: '';
display: inline-block;
vertical-align: text-top;
width: 18px;
height: 18px;
background: transparent;
border: 2px solid #939390;
border-radius: 2px;
margin-right: 8px;
}
&:hover + label:before {
border: 2px solid #454236;
}
&:checked + label:before {
background: #f8d721;
border: 2px solid #f8d721;
}
label. &:disabled + label {
color: #b8b8b8;
cursor: auto;
}
box. &:disabled + label:before {
box-shadow: none;
background: #ddd;
}
&:checked + label:after {
content: '';
position: absolute;
left: 4px;
top: 8px;
background: white;
width: 2px;
height: 2px;
box-shadow: 2px 0 0 white, 4px 0 0 white, 4px -2px 0 white,
4px -4px 0 white, 4px -6px 0 white, 4px -8px 0 white;
transform: rotate(45deg);
}
}
${color}
${border}
${typography}
${layout}
${space}
${shadow}
`;

View File

@ -0,0 +1,14 @@
import React from 'react';
import { StyledCheckBox } from './Checkbox';
export const Checkbox = props => {
const { children, name } = props;
return (
<StyledCheckBox {...props}>
<input id={name} type="checkbox" value="value1" />
<label htmlFor={name}>{children}</label>
</StyledCheckBox>
);
};

View File

@ -0,0 +1,9 @@
import styled from 'styled-components';
import { color, typography, layout, space } from 'styled-system';
export const StyledIcon = styled.svg`
${color}
${typography}
${layout}
${space}
`;

View File

@ -0,0 +1,56 @@
import React from 'react';
import {
FaCheckCircle,
FaFlask,
FaInfoCircle,
FaExclamationTriangle,
FaExclamationCircle,
FaDatabase,
FaPlug,
FaCloud,
FaCog,
FaQuestion,
} from 'react-icons/fa';
import { theme } from '../../theme';
import { StyledIcon } from './Icon';
const iconReferenceMap = {
success: FaCheckCircle,
info: FaInfoCircle,
warning: FaExclamationTriangle,
error: FaExclamationCircle,
graphiql: FaFlask,
database: FaDatabase,
schema: FaPlug,
event: FaCloud,
settings: FaCog,
question: FaQuestion,
default: FaExclamationCircle,
};
const iconWidth = 18;
const iconHeight = 18;
export const Icon = props => {
const { type } = props;
const { icon } = theme;
const iconColor = icon[type] ? icon[type].color : icon.default.color;
const CurrentActiveIcon = iconReferenceMap[type]
? iconReferenceMap[type]
: iconReferenceMap.default;
return (
<StyledIcon
fontSize="icon"
color={iconColor}
width={iconWidth}
height={iconHeight}
as={CurrentActiveIcon}
{...props}
/>
);
};

View File

@ -0,0 +1,80 @@
import styled from 'styled-components';
import {
color,
border,
typography,
layout,
space,
shadow,
} from 'styled-system';
export const StyledRadioButton = styled.div`
[type='radio']:checked,
[type='radio']:not(:checked) {
position: absolute;
left: -9999px;
}
[type='radio']:checked + label,
[type='radio']:not(:checked) + label {
position: relative;
padding-left: 28px;
cursor: pointer;
line-height: 20px;
display: inline-block;
color: #666;
}
[type='radio']:checked + label:before,
[type='radio']:not(:checked) + label:before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 20px;
height: 20px;
border: 2px solid #484538;
border-radius: 100%;
background: #fff;
}
[type='radio']:checked + label:before {
border: 2px solid #1fd6e5;
transition: all 0.2s ease;
}
[type='radio']:hover + label:before {
border: 2px solid #1fd6e5;
transition: all 0.2s ease;
}
[type='radio']:checked + label:after,
[type='radio']:not(:checked) + label:after {
content: '';
width: 10px;
height: 10px;
background: #1fd6e5;
position: absolute;
top: 5px;
left: 5px;
border-radius: 100%;
transition: all 0.2s ease;
}
[type='radio']:not(:checked) + label:after {
opacity: 0;
transform: scale(0);
}
[type='radio']:checked + label:after {
opacity: 1;
transform: scale(1);
}
${color}
${border}
${typography}
${layout}
${space}
${shadow}
`;

View File

@ -0,0 +1,14 @@
import React from 'react';
import { StyledRadioButton } from './RadioButton';
export const RadioButton = props => {
const { children, name } = props;
return (
<StyledRadioButton {...props}>
<input type="radio" id={name} name="radio-group" checked />
<label htmlFor={name}>{children}</label>
</StyledRadioButton>
);
};

View File

@ -0,0 +1,20 @@
import styled from 'styled-components';
import {
color,
border,
typography,
layout,
space,
shadow,
} from 'styled-system';
export const StyledSpinner = styled.div`
position: relative;
${color}
${border}
${typography}
${layout}
${space}
${shadow}
`;

View File

@ -0,0 +1,64 @@
import React from 'react';
import { css, keyframes } from 'styled-components';
import { StyledSpinner } from './Spinner';
const smallSpinnerSize = 17;
const largeSpinnerSize = 20;
const circleBounceDelay = keyframes`
0%,
80%,
100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
`;
const spinnerChildStyles = css`
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
&:before {
content: '';
display: block;
margin: 0 auto;
width: 15%;
height: 15%;
border-radius: 100%;
animation: ${circleBounceDelay} 1.2s infinite ease-in-out both;
background-color: #333;
}
`;
export const Spinner = props => {
const { size } = props;
const spinnerWidth = size === 'small' ? smallSpinnerSize : largeSpinnerSize;
const spinnerHeight = size === 'small' ? smallSpinnerSize : largeSpinnerSize;
return (
<StyledSpinner {...props} height={spinnerHeight} width={spinnerWidth}>
{Array.from(new Array(12), (_, i) => i).map(i => (
<div
key={i}
css={css`
${spinnerChildStyles}
transform: rotate(${30 * i}deg);
&:before {
animation-delay: ${-1.1 + i / 10}s;
}
`}
/>
))}
</StyledSpinner>
);
};

View File

@ -0,0 +1,67 @@
import styled, { css } from 'styled-components';
const checkedStyles = css`
background-color: #1fd6e5;
box-shadow: 0 0 1px #1fd6e5;
:before {
transform: translateX(20px);
}
`;
export const StyledSwitchButton = styled.div`
label {
position: relative;
display: inline-block;
width: 40px;
height: 20px;
input {
opacity: 0;
width: 0;
height: 0;
}
input:checked {
background-color: #1fd6e5;
}
input:focus {
box-shadow: 0 0 1px #1fd6e5;
}
input:checked {
transform: translateX(20px);
}
}
`;
export const StyledSlider = styled.span`
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #484538;
transition: 0.4s;
border-radius: 34px;
&:before {
border-radius: 50%;
position: absolute;
content: '';
height: 16px;
width: 16px;
left: 2px;
bottom: 2px;
background-color: white;
transition: 0.4s;
}
&:hover {
box-shadow: 0 0 1px #1fd6e5;
}
${({ checked }) => (checked ? checkedStyles : ' ')}
`;

View File

@ -0,0 +1,18 @@
import React, { useState } from 'react';
import { StyledSwitchButton, StyledSlider } from './SwitchButton';
export const SwitchButton = props => {
const [isChecked, toggleCheckbox] = useState(false);
const { children } = props;
return (
<StyledSwitchButton {...props}>
<label>
<input type="checkbox" onClick={() => toggleCheckbox(!isChecked)} />
<StyledSlider checked={isChecked} />
{children}
</label>
</StyledSwitchButton>
);
};

View File

@ -0,0 +1,77 @@
import styled, { css } from 'styled-components';
import {
typography,
border,
flexbox,
layout,
space,
color,
shadow,
} from 'styled-system';
const StyledTab = styled.div`
${color}
${border}
${typography}
${layout}
${space}
${shadow}
`;
const StyledTabList = styled.ul`
list-style-type: none;
${border}
${flexbox}
${layout}
${space}
${typography}
`;
StyledTabList.defaultProps = {
borderBottom: 1,
borderColor: 'grey.border',
display: 'flex',
justifyContent: 'flex-start',
alignItems: 'center',
px: 0,
};
const selectedBorderStyles = css`
border-color: ${props => props.theme.colors.tab};
`;
const StyledTabListItem = styled.li`
cursor: pointer;
&:hover {
color: ${props => props.theme.colors.black.text};
}
${typography}
${space}
${color}
${border}
${props => (props.selected ? selectedBorderStyles : '')};
`;
StyledTabListItem.defaultProps = {
fontSize: 'tab',
mr: 40,
pb: 'sm',
fontWeight: 'medium',
borderBottom: 4,
borderColor: 'transparent',
color: 'grey.tab',
};
const StyledTabContent = styled.div`
${color}
${border}
${typography}
${layout}
${space}
${shadow}
`;
export { StyledTab, StyledTabList, StyledTabListItem, StyledTabContent };

View File

@ -0,0 +1,41 @@
import React, { useState } from 'react';
import {
StyledTab,
StyledTabList,
StyledTabListItem,
StyledTabContent,
} from './Tabs';
export const Tabs = props => {
const [currentActiveTabIndex, changeCurrentActiveTab] = useState(0);
const { tabsData } = props;
const currentTabContent =
tabsData && tabsData.filter((_, index) => index === currentActiveTabIndex);
if (tabsData && tabsData.length > 0) {
return (
<StyledTab {...props}>
<StyledTabList>
{tabsData.map(({ title }, index) => (
<StyledTabListItem
key={title}
selected={index === currentActiveTabIndex}
onClick={() => changeCurrentActiveTab(index)}
>
{title}
</StyledTabListItem>
))}
</StyledTabList>
{currentTabContent &&
currentTabContent.map(({ tabContent }, index) => (
<StyledTabContent key={index}>{tabContent}</StyledTabContent>
))}
</StyledTab>
);
}
// In case when we forget to pass tabs data.
return <p>Please provide data for tabs</p>;
};

View File

@ -0,0 +1,9 @@
import styled from 'styled-components';
import { space } from 'styled-system';
export const StyledTooltip = styled.span`
display: inline-block;
position: relative;
${space}
`;

View File

@ -0,0 +1,25 @@
import React from 'react';
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
import Tooltip from 'react-bootstrap/lib/Tooltip';
import { StyledTooltip } from './Tooltip';
const tooltipGenerator = message => {
return <Tooltip id={message}>{message}</Tooltip>;
};
export const ToolTip = props => {
const { message, placement, children } = props;
return (
<OverlayTrigger placement={placement} overlay={tooltipGenerator(message)}>
<StyledTooltip aria-hidden="true" {...props}>
{children}
</StyledTooltip>
</OverlayTrigger>
);
};
ToolTip.defaultProps = {
placement: 'right',
};

View File

@ -0,0 +1,28 @@
import styled from 'styled-components';
import { typography, color, space, border } from 'styled-system';
export const StyledHeading = styled.h1`
${typography}
${color}
${space}
`;
export const StyledText = styled.p`
${typography}
${color}
${space}
${border}
`;
export const StyledTextLink = styled.a`
&&& {
text-decoration: none;
}
cursor: pointer;
${typography}
${color}
${space}
${border}
`;

View File

@ -0,0 +1,76 @@
import React from 'react';
import { StyledHeading, StyledText, StyledTextLink } from './Typography';
export const Heading = StyledHeading;
Heading.defaultProps = {
color: 'black.text',
};
/**
* @example
* Explainer Text
* lineHeight: 'explain'
* fontSize: 'explain'
* fontWeight: 'bold'
*/
export const Text = props => {
const { children, type, fontWeight, fontSize } = props;
const lineHeight = type === 'explain' ? 'body' : 'explain';
let fontWeightValue;
let fontSizeValue;
if (fontWeight) {
fontWeightValue = fontWeight;
} else if (type === 'explain') {
fontWeightValue = 'bold';
}
if (fontSize) {
fontSizeValue = fontSize;
} else {
fontSizeValue = type === 'explain' ? 'explain' : 'p';
}
return (
<StyledText
{...props}
lineHeight={lineHeight}
fontSize={fontSizeValue}
fontWeight={fontWeightValue}
color="black.text"
>
{children}
</StyledText>
);
};
Text.defaultProps = {
mb: 'zero',
mt: 'zero',
mr: 'zero',
ml: 'zero',
};
export const TextLink = props => {
const { children, underline } = props;
return (
<StyledTextLink
{...props}
borderBottom={underline ? 2 : 'none'}
borderColor={underline ? 'yellow.primary' : 'none'}
fontWeight="medium"
fontSize="p"
>
{children}
</StyledTextLink>
);
};
TextLink.defaultProps = {
color: 'black.text',
};

View File

@ -0,0 +1,10 @@
export * from './Tabs';
export * from './Spinner';
export * from './AlertBox';
export * from './Icon';
export * from './Button';
export * from './Typography';
export * from './Tooltip';
export * from './SwitchButton';
export * from './RadioButton';
export * from './Checkbox';

View File

@ -0,0 +1,381 @@
import React from 'react';
import {
Button,
AlertBox,
ToolTip,
Heading,
TextLink,
Text,
RadioButton,
Checkbox,
SwitchButton,
Tabs,
Spinner,
Icon,
} from '../atoms';
import { Flex } from './styles';
// Dummy data for Tabs ********************** //
const dummytabsData = [
{
title: 'Title 1',
tabContent: 'Content 1',
},
{
title: 'Title 2',
tabContent: 'Content 2',
},
{
title: 'Title 3',
tabContent: 'Content 3',
},
{
title: 'Title 4',
tabContent: 'Content 4',
},
{
title: 'Title 5',
tabContent: 'Content 5',
},
];
export const UIComponents = () => (
<React.Fragment>
{/* Buttons ~ large size */}
<Heading as="h2" mt="xl" mb="lg">
Buttons
</Heading>
<Heading as="h3">{'<Button />'}</Heading>
<Button m="lg">Default Button</Button>
<Heading as="h3">{'<Button type="primary" />'}</Heading>
<Button type="primary" m="lg">
Primary Button
</Button>
<Heading as="h3">{'<Button type="primary" isLoading="true" />'}</Heading>
<Button type="primary" m="lg" isLoading>
Primary Button
</Button>
<Heading as="h3">{'<Button type="primary" size="large" />'}</Heading>
<Button type="primary" size="large" m="lg">
Primary Button
</Button>
<Heading as="h3">
{'<Button type="primary" size="large" isLoading="true" />'}
</Heading>
<Button type="primary" size="large" m="lg" isLoading="true">
Primary Button
</Button>
<Heading as="h3">{'<Button type="secondary" size="large" />'}</Heading>
<Button type="secondary" size="large" m="lg">
Secondary Button
</Button>
<Heading as="h3">
{'<Button type="secondary" size="large" isLoading="true" />'}
</Heading>
<Button type="secondary" size="large" m="lg" isLoading="true">
Secondary Button
</Button>
<Heading as="h3">{'<Button type="success" size="large" />'}</Heading>
<Button type="success" size="large" m="lg">
Success Button
</Button>
<Heading as="h3">{'<Button type="danger" size="large" />'}</Heading>
<Button type="danger" size="large" m="lg">
Danger Button
</Button>
<Heading as="h3">{'<Button type="warning" size="large" />'}</Heading>
<Button type="warning" size="large" m="lg">
Warning Button
</Button>
<Heading as="h3">{'<Button type="info" size="large" />'}</Heading>
<Button type="info" size="large" m="lg">
Info Button
</Button>
{/* Disabled State */}
<Heading as="h3">{'<Button type="whatever" disabled />'}</Heading>
<Flex m="lg">
<Button type="primary" mr="lg" disabled>
Primary Button
</Button>
<Button type="secondary" mr="lg" disabled>
Secondary Button
</Button>
<Button type="success" mr="lg" disabled>
Success Button
</Button>
<Button type="danger" mr="lg" disabled>
Danger Button
</Button>
<Button type="warning" mr="lg" disabled>
Warning Button
</Button>
<Button type="info" mr="lg" disabled>
Info Button
</Button>
</Flex>
{/* Spinner *******************************/}
<Heading my="md" as="h3">
{'<Spinner size="small" />'}
</Heading>
<Spinner size="small" m="lg" />
<Heading mb="md" as="h3">
{'<Spinner size="large" />'}
</Heading>
<Spinner size="large" m="lg" />
{/* AlertBox *******************************/}
<Heading mb="lg" mt="xl" as="h2">
Alertbox
</Heading>
<Heading as="h3">{'<AlertBox />'}</Heading>
<AlertBox m="lg">Dummy Text!!</AlertBox>
<Heading as="h3">{'<AlertBox type="success" />'}</Heading>
<AlertBox type="success" m="lg">
Hello Testing!
</AlertBox>
<Heading as="h3">{'<AlertBox type="info" />'}</Heading>
<AlertBox type="info" m="lg" />
<Heading as="h3">{'<AlertBox type="warning" />'}</Heading>
<AlertBox type="warning" m="lg" />
<Heading as="h3">{'<AlertBox type="error" />'}</Heading>
<AlertBox type="error" m="lg" />
{/* Tabs *********************************/}
<Heading mt="xl" mb="lg" as="h2">
React Tab Component
</Heading>
<Heading mb="lg" as="h3">
{'<Tabs tabsData={array} />'}
</Heading>
<Tabs tabsData={dummytabsData} />
{/* Icons *********************************/}
<Heading as="h2" mt="xl" mb="lg">
Icon Component
</Heading>
<Heading my="lg" as="h3">
{'<Icon type="success" />'}
</Heading>
<Icon type="success" ml="xl" />
<Heading my="lg" as="h3">
{'<Icon type="success" color="yellow.primary />'}
</Heading>
<Icon type="success" color="yellow.primary" ml="xl" />
<Heading my="lg" as="h3">
{'<Icon type="info" />'}
</Heading>
<Icon type="info" ml="xl" />
<Heading my="lg" as="h3">
{'<Icon type="warning" />'}
</Heading>
<Icon type="warning" ml="xl" />
<Heading my="lg" as="h3">
{'<Icon type="error" />'}
</Heading>
<Icon type="error" ml="xl" />
<Heading my="lg" as="h3">
{'<Icon type="graphiql" />'}
</Heading>
<Icon type="graphiql" ml="xl" />
<Heading my="lg" as="h3">
{'<Icon type="database" />'}
</Heading>
<Icon type="database" ml="xl" />
<Heading my="lg" as="h3">
{'<Icon type="schema" />'}
</Heading>
<Icon type="schema" ml="xl" />
<Heading my="lg" as="h3">
{'<Icon type="event" />'}
</Heading>
<Icon type="event" ml="xl" />
<Heading my="lg" as="h3">
{'<Icon type="settings" />'}
</Heading>
<Icon type="settings" ml="xl" />
<Heading my="lg" as="h3">
{'<Icon type="question" />'}
</Heading>
<Icon type="question" ml="xl" />
<Heading my="lg" as="h3">
{'<Icon /> ~ default'}
</Heading>
<Icon ml="xl" />
{/* ToolTip ********************************/}
<Heading mb="lg" mt="xl" as="h2">
ToolTip Component
</Heading>
<Heading mb="lg" mt="xl" as="h3">
{'<ToolTip />'}
</Heading>
<ToolTip message="Dummy Text" ml="xl">
Hover me!!
</ToolTip>
<Heading mb="lg" mt="xl" as="h3">
{'<ToolTip placement="right" />'}
</Heading>
<ToolTip message="Dummy Text" placement="right" ml="xl">
Hover me!!
</ToolTip>
<Heading my="xl" as="h3">
{'<ToolTip placement="top" />'}
</Heading>
<ToolTip message="Primary Button" ml="xl" placement="top">
<Button type="primary" size="small">
Hover me!
</Button>
</ToolTip>
<Heading mb="lg" mt="xl" as="h3">
{'<ToolTip placement="left" />'}
</Heading>
<ToolTip message="Dummy Text" placement="left" ml="xl">
Hover me!!
</ToolTip>
<Heading mb="lg" mt="xl" as="h3">
{'<ToolTip placement="bottom" />'}
</Heading>
<ToolTip message="Dummy Text" placement="bottom" ml="xl">
Hover me!!
</ToolTip>
{/* Typography ******************************/}
{/* Heading */}
<Heading mb="lg" mt="xl" as="h2">
Typography
</Heading>
<Heading mb="lg" mt="xl" as="h3">
{'<Heading />'}
</Heading>
<Heading>Main Heading</Heading>
<Heading mb="lg" mt="xl" as="h3">
{'<Heading as="h2" />'}
</Heading>
<Heading as="h2">Subpage title</Heading>
<Heading mb="lg" mt="xl" as="h3">
{'<Heading as="h3" />'}
</Heading>
<Heading as="h3">Section Header</Heading>
<Heading mb="lg" mt="xl" as="h3">
{'<Heading as="h4" />'}
</Heading>
<Heading as="h4">Sub section Heading</Heading>
{/* TextLink */}
<Heading as="h2" mb="lg" mt="xl">
{'<TextLink />'}
</Heading>
<TextLink>Check it out</TextLink>
<Heading as="h2" mb="lg" mt="xl">
{'<TextLink underline />'}
</Heading>
<TextLink underline>Check it out</TextLink>
{/* Text */}
<Heading mb="lg" mt="xl" as="h2">
{'<Text />'}
</Heading>
<Text>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Semper quis lectus
nulla at volutpat diam ut venenatis. Sed viverra tellus in hac habitasse
platea dictumst. Id porta nibh venenatis cras. Velit dignissim sodales ut
eu sem. Turpis cursus in hac habitasse platea dictumst quisque. Integer
feugiat scelerisque varius morbi enim. Dui accumsan sit amet nulla. Donec
et odio pellentesque diam volutpat commodo sed. Augue eget arcu dictum
varius duis at. Nullam vehicula ipsum a arcu cursus vitae. Sapien et
ligula ullamcorper malesuada proin libero nunc. Nunc congue nisi vitae
suscipit tellus mauris a diam maecenas.
</Text>
<Heading my="md" as="h2" mb="lg" mt="xl">
{"<Text type='explain' />"}
</Heading>
{/* Explainer text */}
<Text type="explain">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Semper quis lectus
nulla at volutpat diam ut venenatis. Sed viverra tellus in hac habitasse
platea dictumst. Id porta nibh venenatis cras. Velit dignissim sodales ut
eu sem. Turpis cursus in hac habitasse platea dictumst quisque. Integer
feugiat scelerisque varius morbi enim. Dui accumsan sit amet nulla. Donec
et odio pellentesque diam volutpat commodo sed. Augue eget arcu dictum
varius duis at. Nullam vehicula ipsum a arcu cursus vitae. Sapien et
ligula ullamcorper malesuada proin libero nunc. Nunc congue nisi vitae
suscipit tellus mauris a diam maecenas.
</Text>
{/* RadioButton ******************************/}
<Heading mb="lg" mt="xl" as="h2">
{'<RadioButton name={id} />'}
</Heading>
<RadioButton mb="md" name="ex1">
Choice 1
</RadioButton>
<RadioButton mb="md" name="ex2">
Choice 2
</RadioButton>
{/* Checkbox ******************************/}
<Heading mb="lg" mt="xl" as="h2">
{'<Checkbox name={id} />'}
</Heading>
<Checkbox mb="md" name="test">
Option 1
</Checkbox>
<Checkbox mb="md" name="test2">
Option 2
</Checkbox>
{/* Switch Button */}
<Heading mb="lg" mt="xl" as="h2">
{'<SwitchButton />'}
</Heading>
<SwitchButton />
</React.Fragment>
);

View File

@ -0,0 +1,741 @@
import React from 'react';
import { Heading, Text } from '../atoms';
import { Flex, ColorSchemeDiv, BoxShadowDiv } from './styles';
export const StyleGuide = () => (
<React.Fragment>
<Heading mb="lg" as="h3">
Color Scheme
</Heading>
<Flex m="lg">
<Flex flexDirection="column" mr="40px">
<ColorSchemeDiv
bg="red.original"
borderRadius="circle"
width={90}
height={90}
/>
<Text mt="md" fontSize="button">
red.original
</Text>
</Flex>
<Flex flexDirection="column" mr="40px">
<ColorSchemeDiv
bg="red.primary"
borderRadius="circle"
width={90}
height={90}
/>
<Text mt="md" fontSize="button">
red.primary
</Text>
</Flex>
<Flex flexDirection="column" mr="40px">
<ColorSchemeDiv
bg="red.hover"
borderRadius="circle"
width={90}
height={90}
/>
<Text mt="md" fontSize="button">
red.hover
</Text>
</Flex>
<Flex flexDirection="column" mr="40px">
<ColorSchemeDiv
bg="red.light"
borderRadius="circle"
width={90}
height={90}
/>
<Text mt="md" fontSize="button">
red.light
</Text>
</Flex>
</Flex>
<Flex m="lg">
<Flex flexDirection="column" mr="40px">
<ColorSchemeDiv
bg="green.original"
borderRadius="circle"
width={90}
height={90}
/>
<Text mt="md" fontSize="button">
green.original
</Text>
</Flex>
<Flex flexDirection="column" mr="40px">
<ColorSchemeDiv
bg="green.primary"
borderRadius="circle"
width={90}
height={90}
/>
<Text mt="md" fontSize="button">
green.primary
</Text>
</Flex>
<Flex flexDirection="column" mr="40px">
<ColorSchemeDiv
bg="green.hover"
borderRadius="circle"
width={90}
height={90}
/>
<Text mt="md" fontSize="button">
green.hover
</Text>
</Flex>
<Flex flexDirection="column" mr="40px">
<ColorSchemeDiv
bg="green.light"
borderRadius="circle"
width={90}
height={90}
/>
<Text mt="md" fontSize="button">
green.light
</Text>
</Flex>
</Flex>
<Flex m="lg">
<Flex flexDirection="column" mr="40px">
<ColorSchemeDiv
bg="blue.original"
borderRadius="circle"
width={90}
height={90}
/>
<Text mt="md" fontSize="button">
blue.original
</Text>
</Flex>
<Flex flexDirection="column" mr="40px">
<ColorSchemeDiv
bg="blue.primary"
borderRadius="circle"
width={90}
height={90}
/>
<Text mt="md" fontSize="button">
blue.primary
</Text>
</Flex>
<Flex flexDirection="column" mr="40px">
<ColorSchemeDiv
bg="blue.hover"
borderRadius="circle"
width={90}
height={90}
/>
<Text mt="md" fontSize="button">
blue.hover
</Text>
</Flex>
<Flex flexDirection="column" mr="40px">
<ColorSchemeDiv
bg="blue.light"
borderRadius="circle"
width={90}
height={90}
/>
<Text mt="md" fontSize="button">
blue.light
</Text>
</Flex>
</Flex>
<Flex m="lg">
<Flex flexDirection="column" mr="40px">
<ColorSchemeDiv
bg="orange.original"
borderRadius="circle"
width={90}
height={90}
/>
<Text mt="md" fontSize="button">
orange.original
</Text>
</Flex>
<Flex flexDirection="column" mr="40px">
<ColorSchemeDiv
bg="orange.primary"
borderRadius="circle"
width={90}
height={90}
/>
<Text mt="md" fontSize="button">
orange.primary
</Text>
</Flex>
<Flex flexDirection="column" mr="40px">
<ColorSchemeDiv
bg="orange.hover"
borderRadius="circle"
width={90}
height={90}
/>
<Text mt="md" fontSize="button">
orange.hover
</Text>
</Flex>
<Flex flexDirection="column" mr="40px">
<ColorSchemeDiv
bg="orange.light"
borderRadius="circle"
width={90}
height={90}
/>
<Text mt="md" fontSize="button">
orange.light
</Text>
</Flex>
</Flex>
<Flex m="lg">
<Flex flexDirection="column" mr="40px">
<ColorSchemeDiv
bg="yellow.original"
borderRadius="circle"
width={90}
height={90}
/>
<Text mt="md" fontSize="button">
yellow.original
</Text>
</Flex>
<Flex flexDirection="column" mr="40px">
<ColorSchemeDiv
bg="yellow.primary"
borderRadius="circle"
width={90}
height={90}
/>
<Text mt="md" fontSize="button">
yellow.primary
</Text>
</Flex>
<Flex flexDirection="column" mr="40px">
<ColorSchemeDiv
bg="yellow.hover"
borderRadius="circle"
width={90}
height={90}
/>
<Text mt="md" fontSize="button">
yellow.hover
</Text>
</Flex>
</Flex>
<Flex m="lg">
<Flex flexDirection="column" mr="40px">
<ColorSchemeDiv
bg="grey.original"
borderRadius="circle"
width={90}
height={90}
/>
<Text mt="md" fontSize="button">
grey.original
</Text>
</Flex>
<Flex flexDirection="column" mr="40px">
<ColorSchemeDiv
bg="grey.tab"
borderRadius="circle"
width={90}
height={90}
/>
<Text mt="md" fontSize="button">
grey.tab
</Text>
</Flex>
<Flex flexDirection="column" mr="40px">
<ColorSchemeDiv
bg="grey.border"
borderRadius="circle"
width={90}
height={90}
/>
<Text mt="md" fontSize="button">
grey.border
</Text>
</Flex>
</Flex>
<Flex m="lg">
<Flex flexDirection="column" mr="40px">
<ColorSchemeDiv
bg="black.original"
borderRadius="circle"
width={90}
height={90}
/>
<Text mt="md" fontSize="button">
black.original
</Text>
</Flex>
<Flex flexDirection="column" mr="40px">
<ColorSchemeDiv
bg="black.text"
borderRadius="circle"
width={90}
height={90}
/>
<Text mt="md" fontSize="button">
black.text
</Text>
</Flex>
<Flex flexDirection="column" mr="40px">
<ColorSchemeDiv
bg="black.secondary"
borderRadius="circle"
width={90}
height={90}
/>
<Text mt="md" fontSize="button">
black.secondary
</Text>
</Flex>
<Flex flexDirection="column" mr="40px">
<ColorSchemeDiv
bg="black.hover"
borderRadius="circle"
width={90}
height={90}
/>
<Text mt="md" fontSize="button">
black.hover
</Text>
</Flex>
</Flex>
<Flex
flexDirection="column"
m="lg"
alignItems="flex-start"
justifyContent="center"
>
<ColorSchemeDiv bg="tab" borderRadius="circle" width={90} height={90} />
<Text mt="md" fontSize="button" pl="lg">
tab
</Text>
</Flex>
<Flex
flexDirection="column"
m="lg"
alignItems="flex-start"
justifyContent="center"
>
<ColorSchemeDiv
bg="white"
borderRadius="circle"
width={90}
height={90}
boxShadow={3}
/>
<Text mt="md" fontSize="button" pl="lg">
white
</Text>
</Flex>
{/* Shadows */}
<Heading mb="lg" mt="xl" as="h3">
Shadows
</Heading>
<Flex justifyContent="flex-start">
<Flex flexDirection="column" mr="xl">
<BoxShadowDiv
width={225}
height={125}
boxShadow={1}
borderRadius="xs"
bg="white"
/>
<Text mt="md" fontSize="button">
{'boxShadow={1}'}
</Text>
</Flex>
<Flex flexDirection="column" mr="xl">
<BoxShadowDiv
width={225}
height={125}
boxShadow={2}
borderRadius="xs"
bg="white"
/>
<Text mt="md" fontSize="button">
2
</Text>
</Flex>
<Flex flexDirection="column" mr="xl">
<BoxShadowDiv
width={225}
height={125}
boxShadow={3}
borderRadius="xs"
bg="white"
/>
<Text mt="md" fontSize="button">
3
</Text>
</Flex>
<Flex flexDirection="column" mr="xl">
<BoxShadowDiv
width={225}
height={125}
boxShadow={4}
borderRadius="xs"
bg="white"
/>
<Text mt="md" fontSize="button">
4
</Text>
</Flex>
</Flex>
{/* Border */}
<Heading mb="lg" mt="xl" as="h3">
Border
</Heading>
<Flex justifyContent="flex-start">
<Flex flexDirection="column" mr="lg">
<BoxShadowDiv
width={180}
height={120}
boxShadow={1}
border={0}
bg="white"
/>
<Text mt="md" fontSize="button" fontWeight="bold">
{'border={0} ~ 0'}
</Text>
<Text mt="sm" fontSize="button">
No border
</Text>
</Flex>
<Flex flexDirection="column" mr="lg">
<BoxShadowDiv
width={180}
height={120}
border={1}
borderRadius="xs"
bg="white"
/>
<Text mt="md" fontSize="button" fontWeight="bold">
1
</Text>
<Text mt="sm" fontSize="button">
1px solid
</Text>
</Flex>
<Flex flexDirection="column" mr="lg">
<BoxShadowDiv
width={180}
height={120}
border={2}
borderRadius="xs"
bg="white"
/>
<Text mt="md" fontSize="button" fontWeight="bold">
2
</Text>
<Text mt="sm" fontSize="button">
2px solid
</Text>
</Flex>
<Flex flexDirection="column" mr="lg">
<BoxShadowDiv width={180} height={120} border={3} bg="white" />
<Text mt="md" fontSize="button" fontWeight="bold">
3
</Text>
<Text mt="sm" fontSize="button">
3px solid
</Text>
</Flex>
<Flex flexDirection="column" mr="lg">
<BoxShadowDiv
width={180}
height={120}
border={4}
borderRadius="xs"
bg="white"
/>
<Text mt="md" fontSize="button" fontWeight="bold">
4
</Text>
<Text mt="sm" fontSize="button">
4px solid
</Text>
</Flex>
<Flex flexDirection="column">
<BoxShadowDiv width={180} height={120} border={5} bg="white" />
<Text mt="md" fontSize="button" fontWeight="bold">
5
</Text>
<Text mt="sm" fontSize="button">
5px solid
</Text>
</Flex>
</Flex>
{/* Border Radius */}
<Heading mb="lg" mt="xl" as="h3" fontWeight="bold">
Border Radius
</Heading>
<Flex justifyContent="flex-start">
<Flex flexDirection="column" mr="xl">
<BoxShadowDiv
width={125}
height={125}
boxShadow={1}
borderRadius="xs"
bg="white"
/>
<Text mt="md" fontSize="button" fontWeight="bold">
{"borderRadius='xs'"}
</Text>
<Text mt="sm" fontSize="button">
2px
</Text>
</Flex>
<Flex flexDirection="column" mr="xl">
<BoxShadowDiv
width={125}
height={125}
boxShadow={2}
borderRadius="sm"
bg="white"
/>
<Text mt="md" fontSize="button" fontWeight="bold">
sm
</Text>
<Text mt="sm" fontSize="button">
4px
</Text>
</Flex>
<Flex flexDirection="column" mr="xl">
<BoxShadowDiv
width={125}
height={125}
boxShadow={2}
borderRadius="md"
bg="white"
/>
<Text mt="md" fontSize="button" fontWeight="bold">
md
</Text>
<Text mt="sm" fontSize="button">
8px
</Text>
</Flex>
<Flex flexDirection="column" mr="xl">
<BoxShadowDiv
width={125}
height={125}
boxShadow={2}
borderRadius="lg"
bg="white"
/>
<Text mt="md" fontSize="button" fontWeight="bold">
lg
</Text>
<Text mt="sm" fontSize="button">
12px
</Text>
</Flex>
<Flex flexDirection="column" mr="xl">
<BoxShadowDiv
width={125}
height={125}
boxShadow={2}
borderRadius="xl"
bg="white"
/>
<Text mt="md" fontSize="button" fontWeight="bold">
xl
</Text>
<Text mt="sm" fontSize="button">
16px
</Text>
</Flex>
<Flex flexDirection="column">
<BoxShadowDiv
width={125}
height={125}
boxShadow={2}
borderRadius="circle"
bg="white"
/>
<Text mt="md" fontSize="button" fontWeight="bold">
circle
</Text>
<Text mt="sm" fontSize="button">
1000px
</Text>
</Flex>
</Flex>
{/* Font Weight */}
<Heading mb="lg" mt="xl" as="h3">
Font Weight
</Heading>
<Flex justifyContent="flex-start">
<Flex flexDirection="column" mr="xl">
<Heading fontWeight="normal">Aa</Heading>
<Text mt="md" fontSize="button">
{"fontWeight='normal'"}
</Text>
<Text mt="sm" fontSize="button" fontWeight="bold">
400
</Text>
</Flex>
<Flex flexDirection="column" mr="xl">
<Heading fontWeight="medium">Aa</Heading>
<Text mt="md" fontSize="button">
medium
</Text>
<Text mt="sm" fontSize="button" fontWeight="bold">
500
</Text>
</Flex>
<Flex flexDirection="column" mr="xl">
<Heading fontWeight="bold">Aa</Heading>
<Text mt="md" fontSize="button">
bold
</Text>
<Text mt="sm" fontSize="button" fontWeight="bold">
700
</Text>
</Flex>
</Flex>
{/* Font sizes */}
<Heading mb="lg" mt="xl" as="h3">
Font Size
</Heading>
<Flex justifyContent="flex-start">
<Flex flexDirection="column" mr="xl">
<Heading fontWeight="normal">Aa</Heading>
<Text mt="md" fontSize="button">
{"fontSize='h1'"}
</Text>
<Text mt="sm" fontSize="button" fontWeight="bold">
30px
</Text>
</Flex>
<Flex flexDirection="column" mr="xl">
<Heading fontWeight="normal" as="h2">
Aa
</Heading>
<Text mt="md" fontSize="button">
h2
</Text>
<Text mt="sm" fontSize="button" fontWeight="bold">
24px
</Text>
</Flex>
<Flex flexDirection="column" mr="xl">
<Heading fontWeight="normal" as="h3">
Aa
</Heading>
<Text mt="md" fontSize="button">
h3
</Text>
<Text mt="sm" fontSize="button" fontWeight="bold">
20px
</Text>
</Flex>
<Flex flexDirection="column" mr="xl">
<Heading fontWeight="normal" as="h4">
Aa
</Heading>
<Text mt="md" fontSize="button">
h4
</Text>
<Text mt="sm" fontSize="button" fontWeight="bold">
18px
</Text>
</Flex>
<Flex flexDirection="column" mr="xl">
<Heading fontWeight="normal" fontSize="p">
Aa
</Heading>
<Text mt="md" fontSize="button">
p
</Text>
<Text mt="sm" fontSize="button" fontWeight="bold">
16px
</Text>
</Flex>
<Flex flexDirection="column" mr="xl">
<Heading fontWeight="normal" fontSize="button">
Aa
</Heading>
<Text mt="md" fontSize="button">
button
</Text>
<Text mt="sm" fontSize="button" fontWeight="bold">
14px
</Text>
</Flex>
<Flex flexDirection="column" mr="xl">
<Heading fontWeight="normal" fontSize="explain">
Aa
</Heading>
<Text mt="md" fontSize="button">
explain
</Text>
<Text mt="sm" fontSize="button" fontWeight="bold">
12px
</Text>
</Flex>
<Flex flexDirection="column" mr="xl">
<Heading fontWeight="normal" fontSize="icon">
Aa
</Heading>
<Text mt="md" fontSize="icon">
icon
</Text>
<Text mt="sm" fontSize="button" fontWeight="bold">
20px
</Text>
</Flex>
</Flex>
</React.Fragment>
);

View File

@ -0,0 +1,81 @@
import styled from 'styled-components';
import {
flexbox,
typography,
space,
color,
border,
shadow,
layout,
} from 'styled-system';
// Parent Div ~ Global Styles ************* //
export const UIKitWrapperDiv = styled.div`
${typography}
${space}
${color}
`;
// Heading ************************* //
export const Heading = styled.h1`
${typography}
${color}
${space}
`;
// Paragraph ************************* //
export const Text = styled.p`
${typography}
${color}
${space}
${border}
`;
// Base Div *************************** //
export const Box = styled.div`
${color}
${border}
${typography}
${layout}
${space}
${shadow}
`;
// Flexbox Div ********************** //
export const Flex = styled(Box)`
${flexbox}
`;
Flex.defaultProps = {
display: 'flex',
alignItems: 'center',
};
/*
* Extending Base Div ~ Box for readability
* ColorSchemeDiv
* BoxShadowDiv
* Brush
* AlertMessageBox
*/
// Color Scheme Div ******************** //
export const ColorSchemeDiv = styled(Box)``;
// Shadow Div ********************************* //
export const BoxShadowDiv = styled(Box)``;
// Color Shades *************************** //
export const Brush = styled(Box)``;
// Alert Box ****************************** //
export const AlertMessageBox = styled(Box)``;

View File

@ -0,0 +1,16 @@
import React from 'react';
import { Heading } from './atoms';
import { StyleGuide } from './demo/StyleGuide';
import { UIComponents } from './demo/Components';
import { UIKitWrapperDiv } from './demo/styles';
const UIKit = () => (
<UIKitWrapperDiv py="lg" px="xl" mb="xl" bg="white" fontFamily="roboto">
<Heading mb="lg">Design System</Heading>
<StyleGuide />
<UIComponents />
</UIKitWrapperDiv>
);
export default UIKit;

View File

@ -0,0 +1,289 @@
// Theme specification for the Design-System.
const colors = {
red: {
original: '#ff0000',
primary: '#e53935',
hover: 'rgba(229, 57, 53, 0.4)',
light: '#f7e9e9',
},
green: {
original: '#008000',
primary: '#69cb43',
hover: 'rgba(123, 179, 66, 0.4)',
light: '#f0f8e7',
},
blue: {
original: '#0000ff',
primary: '#1f88e5',
hover: 'rgba(31, 136, 229, 0.4)',
light: '#f0f8ff',
},
orange: {
original: '#ffa500',
primary: '#fdb02c',
hover: 'rgba(253, 176, 44, 0.4)',
light: '#fff8ed',
},
yellow: {
original: '#ffff00',
primary: '#f8d721',
hover: 'rgba(204, 177, 25, 0.4)',
},
grey: {
original: '#888888',
tab: '#939390',
border: '#ededed',
},
black: {
original: '#000',
secondary: '#484538',
text: '#292822',
hover: 'rgba(0, 0, 0, 0.16)',
},
white: '#fff',
transparent: 'transparent',
tab: '#1fd6e5',
};
// ********************************** //
const button = {
primary: {
backgroundColor: colors.yellow.primary,
boxShadowColor: colors.yellow.hover,
color: colors.black.text,
},
secondary: {
backgroundColor: colors.white,
boxShadowColor: colors.black.hover,
color: colors.black.text,
},
success: {
backgroundColor: colors.green.primary,
boxShadowColor: colors.green.hover,
color: colors.white,
},
danger: {
backgroundColor: colors.red.primary,
boxShadowColor: colors.red.hover,
color: colors.white,
},
warning: {
backgroundColor: colors.orange.primary,
boxShadowColor: colors.orange.hover,
color: colors.white,
},
info: {
backgroundColor: colors.blue.primary,
boxShadowColor: colors.blue.hover,
color: colors.white,
},
default: {
backgroundColor: colors.yellow.primary,
boxShadowColor: colors.black.hover,
color: colors.white,
},
};
// ********************************** //
const alertBox = {
success: {
backgroundColor: colors.green.light,
borderColor: colors.green.primary,
message: 'You did something awesome. Well done!',
},
info: {
backgroundColor: colors.blue.light,
borderColor: colors.blue.primary,
message: 'You need to do something.',
},
warning: {
backgroundColor: colors.orange.light,
borderColor: colors.orange.primary,
message: 'You are about to do something wrong.',
},
error: {
backgroundColor: colors.red.light,
borderColor: colors.red.primary,
message: 'You did something wrong.',
},
default: {
backgroundColor: colors.green.light,
borderColor: colors.green.primary,
message: '',
},
};
// ********************************** //
const icon = {
success: {
color: colors.green.primary,
},
info: {
color: colors.blue.primary,
},
warning: {
color: colors.orange.primary,
},
error: {
color: colors.red.primary,
},
// type ~ out of range
default: {
color: colors.black.secondary,
},
};
// Border Radius ********************* //
const radii = [0, 2, 4, 8, 12, 16];
/* border-radius aliases
* xs: 2px (extra small)
* sm: 4px (small)
* md: 8px (medium)
* lg: 12px (large)
* xl: 16px (extra large)
* circle: 1000px
*/
radii.xs = radii[1];
radii.sm = radii[2];
radii.md = radii[3];
radii.lg = radii[4];
radii.xl = radii[5];
radii.circle = 1000;
// ********************************** //
const fontWeights = [0, 100, 200, 300, 400, 500, 600, 700, 800, 900];
/* font-weight aliases
* normal: 400
* medium: 500
* bold: 700
*/
fontWeights.normal = fontWeights[4];
fontWeights.medium = fontWeights[5];
fontWeights.bold = fontWeights[7];
// ********************************** //
const fontSizes = [12, 14, 16, 18, 20, 24, 30, 36, 48, 80, 96];
/* font-sizes aliases
* h1: 30px
* h2: 24px
* h3: 20px
* h4: 18px
* p: 16px
* button: 14px
* explain (Explainer Text): 12px
* icon: 20px
*/
fontSizes.h1 = fontSizes[6];
fontSizes.h2 = fontSizes[5];
fontSizes.h3 = fontSizes[4];
fontSizes.h4 = fontSizes[3];
fontSizes.p = fontSizes[2];
fontSizes.button = fontSizes[1];
fontSizes.tab = fontSizes[3];
fontSizes.explain = fontSizes[0];
fontSizes.icon = fontSizes[3];
// ****************************** //
const space = [0, 4, 6, 8, 10, 12, 14, 16, 18, 20, 32, 64];
/* space ~ margin / padding aliases
* zero: 0
* xs: 4px (extra small)
* sm: 8px (small)
* md: 16px (medium)
* lg: 32px (large)
* xl: 64px (extra large)
*/
space.zero = space[0];
space.xs = space[1];
space.sm = space[3];
space.md = space[7];
space.lg = space[10];
space.xl = space[11];
// ********************************** //
const lineHeights = [1.33, 1.5];
/* line-height aliases
* body: 1.5
* explain: 1.3 ~ Explainer Text
*/
lineHeights.body = lineHeights[1];
lineHeights.explain = lineHeights[0];
// ********************************** //
/* sizes aliases (width & height)
* sm: 40px
* lg: 48px
*/
const sizes = [40, 48];
sizes.sm = sizes[0];
sizes.lg = sizes[1];
// ********************************** //
export const theme = {
colors,
radii,
fonts: {
roboto: 'Roboto',
},
fontWeights,
fontSizes,
sizes,
space,
lineHeights,
shadows: [
0,
'0 0 3px 0 rgba(0, 0, 0, 0.16)',
'0 3px 6px 0 rgba(0, 0, 0, 0.16)',
'0 3px 10px 0 rgba(0, 0, 0, 0.16)',
'0 7px 24px 0 rgba(0, 0, 0, 0.32)',
],
borders: [0, '1px solid', '2px solid', '3px solid', '4px solid', '5px solid'],
button,
alertBox,
icon,
};

View File

@ -2,5 +2,6 @@ import App from './App/App';
import Main from './Main/Main';
import PageNotFound from './Error/PageNotFound';
import Login from './Login/Login';
import UIKit from './UIKit';
export { App, Main, PageNotFound, Login };
export { App, Main, PageNotFound, Login, UIKit };

View File

@ -37,6 +37,8 @@ import logoutContainer from './components/Services/Settings/Logout/Logout';
import { showErrorNotification } from './components/Services/Common/Notification';
import { CLI_CONSOLE_MODE } from './constants';
import UIKit from './components/UIKit/';
import { Heading } from './components/UIKit/atoms';
const routes = store => {
// load hasuractl migration status
@ -88,6 +90,19 @@ const routes = store => {
const actionsRouter = getActionsRouter(connect, store, composeOnEnterHooks);
const uiKitRouter = globals.isProduction ? null : (
<Route
path="/ui-elements"
// TODO: fix me
component={() => (
<div>
<Heading />
<UIKit />
</div>
)}
/>
);
return (
<Route path="/" component={App} onEnter={validateLogin(store)}>
<Route path="login" component={generatedLoginConnector(connect)} />
@ -127,6 +142,7 @@ const routes = store => {
{eventRouter}
{remoteSchemaRouter}
{actionsRouter}
{uiKitRouter}
</Route>
</Route>
<Route path="404" component={PageNotFound} status="404" />

View File

@ -157,6 +157,7 @@ module.exports = {
// set global consts
new webpack.DefinePlugin({
CONSOLE_ASSET_VERSION: Date.now().toString(),
'process.hrtime': () => null,
}),
webpackIsomorphicToolsPlugin.development(),
new ForkTsCheckerWebpackPlugin({

View File

@ -196,6 +196,7 @@ module.exports = {
NODE_ENV: JSON.stringify('production'),
},
CONSOLE_ASSET_VERSION: Date.now().toString(),
'process.hrtime': () => null,
}),
new ForkTsCheckerWebpackPlugin({
compilerOptions: {

View File

@ -351,8 +351,13 @@ An example:
"filter" : {
"author_id" : "X-HASURA-USER-ID"
},
"check" : {
"content" : {
"_ne": ""
}
},
"set":{
"id":"X-HASURA-USER-ID"
"updated_at" : "NOW()"
}
}
}
@ -360,11 +365,13 @@ An example:
This reads as follows - for the ``user`` role:
* Allow updating only those rows where the ``check`` passes i.e. the value of the ``author_id`` column of a row matches the value of the session variable ``X-HASURA-USER-ID`` value.
* Allow updating only those rows where the ``filter`` passes i.e. the value of the ``author_id`` column of a row matches the value of the session variable ``X-HASURA-USER-ID``.
* If the above ``check`` passes for a given row, allow updating only the ``title``, ``content`` and ``category`` columns (*as specified in the* ``columns`` *key*).
* If the above ``filter`` passes for a given row, allow updating only the ``title``, ``content`` and ``category`` columns (*as specified in the* ``columns`` *key*).
* When this update happens, the value of the column ``id`` will be automatically ``set`` to the value of the resolved session variable ``X-HASURA-USER-ID``.
* After the update happens, verify that the ``check`` condition holds for the updated row i.e. that the value in the ``content`` column is not empty.
* When this update happens, the value of the column ``updated_at`` will be automatically ``set`` to the current timestamp.
.. note::
@ -421,7 +428,11 @@ UpdatePermission
* - filter
- true
- :ref:`BoolExp`
- Only the rows where this expression holds true are deletable
- Only the rows where this precondition holds true are updatable
* - check
- false
- :ref:`BoolExp`
- Postcondition which must be satisfied by rows which have been updated
* - set
- false
- :ref:`ColumnPresetExp`

View File

@ -34,7 +34,7 @@ Exporting Hasura metadata
1. Click on the settings (⚙) icon at the top right corner of the console screen.
2. In the Hasura metadata actions page that opens, click on the ``Export Metadata`` button.
3. This will prompt a file download for ``metadata.json``. Save the file.
3. This will prompt a file download for ``hasura_metadata_<timestamp>.json``. Save the file.
.. tab:: API
@ -45,9 +45,9 @@ Exporting Hasura metadata
.. code-block:: bash
curl -d'{"type": "export_metadata", "args": {}}' http://localhost:8080/v1/query -o metadata.json
curl -d'{"type": "export_metadata", "args": {}}' http://localhost:8080/v1/query -o hasura_metadata.json
This command will create a ``metadata.json`` file.
This command will create a ``hasura_metadata.json`` file.
If an admin secret is set, add ``-H 'X-Hasura-Admin-Secret: <your-admin-secret>'`` as the API is an
admin-only API.
@ -67,7 +67,7 @@ before.
1. Click on the settings (⚙) icon at the top right corner of the console screen.
2. Click on ``Import Metadata`` button.
3. Choose a ``metadata.json`` file that was exported earlier.
3. Choose a ``hasura_metadata.json`` file that was exported earlier.
4. A notification should appear indicating the success or error.
.. tab:: API
@ -78,9 +78,9 @@ before.
.. code-block:: bash
curl -d'{"type":"replace_metadata", "args":'$(cat metadata.json)'}' http://localhost:8080/v1/query
curl -d'{"type":"replace_metadata", "args":'$(cat hasura_metadata.json)'}' http://localhost:8080/v1/query
This command reads the ``metadata.json`` file and makes a POST request to
This command reads the ``hasura_metadata.json`` file and makes a POST request to
replace the metadata.
If an admin secret is set, add ``-H 'X-Hasura-Admin-Secret: <your-admin-secret>'`` as the API is an
admin-only API.

View File

@ -33,7 +33,7 @@ data. One thing to note is that all the Postgres resources the metadata refers
to should already exist when the import happens, otherwise Hasura will throw an
error.
To understand the format of the ``metadata.json`` file, refer to :ref:`metadata_file_format`.
To understand the format of the ``hasura_metadata.json`` file, refer to :ref:`metadata_file_format`.
For more details on how to import and export metadata, refer to :ref:`manage_hasura_metadata`.

View File

@ -12,7 +12,7 @@ idna==2.6
imagesize==0.7.1
Jinja2==2.10.1
livereload==2.5.1
MarkupSafe==1.0
MarkupSafe==1.1.1
pathtools==0.1.2
port-for==0.3.1
Pygments==2.2.0

View File

@ -320,6 +320,9 @@ elif [ "$MODE" = "test" ]; then
########################################
cd "$PROJECT_ROOT/server"
# Until we can use a real webserver for TestEventFlood, limit concurrency
export HASURA_GRAPHQL_EVENTS_HTTP_POOL_SIZE=8
# We'll get an hpc error if these exist; they will be deleted below too:
rm -f graphql-engine-tests.tix graphql-engine.tix graphql-engine-combined.tix

View File

@ -132,6 +132,7 @@ constraints: any.Cabal ==2.4.0.1,
any.generic-arbitrary ==0.1.0,
any.ghc-boot-th ==8.6.5,
any.ghc-prim ==0.5.3,
any.ghc-heap-view ==0.6.0,
any.happy ==1.19.12,
happy +small_base,
any.hashable ==1.2.7.0,

View File

@ -192,6 +192,9 @@ library
-- testing
, QuickCheck
, generic-arbitrary
-- 0.6.1 is supposedly not okay for ghc 8.6:
-- https://github.com/nomeata/ghc-heap-view/issues/27
, ghc-heap-view == 0.6.0
, directory

View File

@ -9,6 +9,7 @@ import Control.Monad.STM (atomically)
import Control.Monad.Trans.Control (MonadBaseControl (..))
import Data.Aeson ((.=))
import Data.Time.Clock (UTCTime, getCurrentTime)
import GHC.AssertNF
import Options.Applicative
import System.Environment (getEnvironment, lookupEnv)
import System.Exit (exitFailure)
@ -203,6 +204,13 @@ runHGEServer
-- ^ start time
-> m ()
runHGEServer ServeOptions{..} InitCtx{..} initTime = do
-- Comment this to enable expensive assertions from "GHC.AssertNF". These will log lines to
-- STDOUT containing "not in normal form". In the future we could try to integrate this into
-- our tests. For now this is a development tool.
--
-- NOTE: be sure to compile WITHOUT code coverage, for this to work properly.
liftIO disableAssertNF
let sqlGenCtx = SQLGenCtx soStringifyNum
Loggers loggerCtx logger _ = _icLoggers

View File

@ -215,7 +215,7 @@ processEventQueue logger logenv httpMgr pool getSchemaCache EventEngineCtx{..} =
atomically $ do -- block until < HASURA_GRAPHQL_EVENTS_HTTP_POOL_SIZE threads:
capacity <- readTVar _eeCtxEventThreadsCapacity
check $ capacity > 0
writeTVar _eeCtxEventThreadsCapacity (capacity - 1)
writeTVar _eeCtxEventThreadsCapacity $! (capacity - 1)
-- since there is some capacity in our worker threads, we can launch another:
let restoreCapacity = liftIO $ atomically $
modifyTVar' _eeCtxEventThreadsCapacity (+ 1)

View File

@ -47,7 +47,8 @@ import Hasura.Prelude
import Hasura.RQL.DDL.Headers
import Hasura.RQL.Types
import Hasura.Server.Context
import Hasura.Server.Utils (RequestId, mkClientHeadersForward)
import Hasura.Server.Utils (RequestId, mkClientHeadersForward,
mkSetCookieHeaders)
import Hasura.Server.Version (HasVersion)
import qualified Hasura.GraphQL.Execute.LiveQuery as EL
@ -170,12 +171,11 @@ getExecPlanPartial userInfo sc enableAL req = do
-- to be executed
data ExecOp
= ExOpQuery !LazyRespTx !(Maybe EQ.GeneratedSqlMap)
| ExOpMutation !LazyRespTx
| ExOpMutation !N.ResponseHeaders !LazyRespTx
| ExOpSubs !EL.LiveQueryPlan
-- The graphql query is resolved into an execution operation
type ExecPlanResolved
= GQExecPlan ExecOp
type ExecPlanResolved = GQExecPlan ExecOp
getResolvedExecPlan
:: (HasVersion, MonadError QErr m, MonadIO m)
@ -215,8 +215,9 @@ getResolvedExecPlan pgExecCtx planCache userInfo sqlGenCtx
getExecPlanPartial userInfo sc enableAL req
forM partialExecPlan $ \(gCtx, rootSelSet) ->
case rootSelSet of
VQ.RMutation selSet ->
ExOpMutation <$> getMutOp gCtx sqlGenCtx userInfo httpManager reqHeaders selSet
VQ.RMutation selSet -> do
(tx, respHeaders) <- getMutOp gCtx sqlGenCtx userInfo httpManager reqHeaders selSet
pure $ ExOpMutation respHeaders tx
VQ.RQuery selSet -> do
(queryTx, plan, genSql) <- getQueryOp gCtx sqlGenCtx userInfo queryReusability selSet
traverse_ (addPlanToCache . EP.RPQuery) plan
@ -286,16 +287,16 @@ resolveMutSelSet
, MonadIO m
)
=> VQ.SelSet
-> m LazyRespTx
-> m (LazyRespTx, N.ResponseHeaders)
resolveMutSelSet fields = do
aliasedTxs <- forM (toList fields) $ \fld -> do
fldRespTx <- case VQ._fName fld of
"__typename" -> return $ return $ encJFromJValue mutationRootName
_ -> fmap liftTx . evalReusabilityT $ GR.mutFldToTx fld
"__typename" -> return (return $ encJFromJValue mutationRootName, [])
_ -> evalReusabilityT $ GR.mutFldToTx fld
return (G.unName $ G.unAlias $ VQ._fAlias fld, fldRespTx)
-- combines all transactions into a single transaction
return $ liftTx $ toSingleTx aliasedTxs
return (liftTx $ toSingleTx aliasedTxs, concatMap (snd . snd) aliasedTxs)
where
-- A list of aliased transactions for eg
-- [("f1", Tx r1), ("f2", Tx r2)]
@ -304,7 +305,7 @@ resolveMutSelSet fields = do
-- toSingleTx :: [(Text, LazyRespTx)] -> LazyRespTx
toSingleTx aliasedTxs =
fmap encJFromAssocList $
forM aliasedTxs $ \(al, tx) -> (,) al <$> tx
forM aliasedTxs $ \(al, (tx, _)) -> (,) al <$> tx
getMutOp
:: (HasVersion, MonadError QErr m, MonadIO m)
@ -314,17 +315,16 @@ getMutOp
-> HTTP.Manager
-> [N.Header]
-> VQ.SelSet
-> m LazyRespTx
-> m (LazyRespTx, N.ResponseHeaders)
getMutOp ctx sqlGenCtx userInfo manager reqHeaders selSet =
runE_ $ resolveMutSelSet selSet
peelReaderT $ resolveMutSelSet selSet
where
runE_ action = do
res <- runExceptT $ runReaderT action
peelReaderT action =
runReaderT action
( userInfo, queryCtxMap, mutationCtxMap
, typeMap, fldMap, ordByCtx, insCtxMap, sqlGenCtx
, manager, reqHeaders
)
either throwError return res
where
queryCtxMap = _gQueryCtxMap ctx
mutationCtxMap = _gMutationCtxMap ctx
@ -414,9 +414,7 @@ execRemoteGQ reqId userInfo reqHdrs q rsi opDef = do
L.unLogger logger $ QueryLog q Nothing reqId
(time, res) <- withElapsedTime $ liftIO $ try $ HTTP.httpLbs req manager
resp <- either httpThrow return res
let cookieHdrs = getCookieHdr (resp ^.. Wreq.responseHeader "Set-Cookie")
respHdrs = Just $ mkRespHeaders cookieHdrs
!httpResp = HttpResponse (encJFromLBS $ resp ^. Wreq.responseBody) respHdrs
let !httpResp = HttpResponse (encJFromLBS $ resp ^. Wreq.responseBody) $ mkSetCookieHeaders resp
return (time, httpResp)
where
@ -428,7 +426,3 @@ execRemoteGQ reqId userInfo reqHdrs q rsi opDef = do
userInfoToHdrs = map (\(k, v) -> (CI.mk $ CS.cs k, CS.cs v)) $
userInfoToList userInfo
getCookieHdr = fmap (\h -> ("Set-Cookie", h))
mkRespHeaders = map (\(k, v) -> Header (bsToTxt $ CI.original k, bsToTxt v))

View File

@ -50,6 +50,7 @@ import qualified StmContainers.Map as STMMap
import qualified System.Metrics.Distribution as Metrics
import Data.List.Split (chunksOf)
import GHC.AssertNF
import qualified Hasura.GraphQL.Execute.LiveQuery.TMap as TMap
@ -186,12 +187,13 @@ pushResultToCohort
-> LiveQueryMetadata
-> CohortSnapshot
-> IO ()
pushResultToCohort result respHashM (LiveQueryMetadata dTime) cohortSnapshot = do
pushResultToCohort result !respHashM (LiveQueryMetadata dTime) cohortSnapshot = do
prevRespHashM <- STM.readTVarIO respRef
-- write to the current websockets if needed
sinks <-
if isExecError result || respHashM /= prevRespHashM
then do
$assertNFHere respHashM -- so we don't write thunks to mutable vars
STM.atomically $ STM.writeTVar respRef respHashM
return (newSinks <> curSinks)
else
@ -375,4 +377,4 @@ pollQuery metrics batchSize pgExecCtx pgQuery handler =
-- from Postgres strictly and (2) even if we didnt, hashing will have to force the
-- whole thing anyway.
respHash = mkRespHash (encJToBS result)
in (GQSuccess result, Just respHash, actionMeta,) <$> Map.lookup respId cohortSnapshotMap
in (GQSuccess result, Just $! respHash, actionMeta,) <$> Map.lookup respId cohortSnapshotMap

View File

@ -21,6 +21,7 @@ import qualified StmContainers.Map as STMMap
import Control.Concurrent.Extended (sleep, forkImmortal)
import Control.Exception (mask_)
import Data.String
import GHC.AssertNF
import qualified Hasura.Logging as L
import qualified Hasura.GraphQL.Execute.LiveQuery.TMap as TMap
@ -73,6 +74,8 @@ addLiveQuery logger lqState plan onResultAction = do
responseId <- newCohortId
sinkId <- newSinkId
$assertNFHere subscriber -- so we don't write thunks to mutable vars
-- a handler is returned only when it is newly created
handlerM <- STM.atomically $ do
handlerM <- STMMap.lookup handlerId lqMap
@ -84,7 +87,7 @@ addLiveQuery logger lqState plan onResultAction = do
Nothing -> addToPoller sinkId responseId handler
return Nothing
Nothing -> do
poller <- newPoller
!poller <- newPoller
addToPoller sinkId responseId poller
STMMap.insert poller handlerId lqMap
return $ Just poller
@ -96,7 +99,9 @@ addLiveQuery logger lqState plan onResultAction = do
threadRef <- forkImmortal ("pollQuery."<>show sinkId) logger $ forever $ do
pollQuery metrics batchSize pgExecCtx query handler
sleep $ unRefetchInterval refetchInterval
STM.atomically $ STM.putTMVar (_pIOState handler) (PollerIOState threadRef metrics)
let !pState = PollerIOState threadRef metrics
$assertNFHere pState -- so we don't write thunks to mutable vars
STM.atomically $ STM.putTMVar (_pIOState handler) pState
pure $ LiveQueryId handlerId cohortKey sinkId
where
@ -106,11 +111,12 @@ addLiveQuery logger lqState plan onResultAction = do
handlerId = PollerKey role query
!subscriber = Subscriber alias onResultAction
addToCohort sinkId handlerC =
TMap.insert (Subscriber alias onResultAction) sinkId $ _cNewSubscribers handlerC
TMap.insert subscriber sinkId $ _cNewSubscribers handlerC
addToPoller sinkId responseId handler = do
newCohort <- Cohort responseId <$> STM.newTVar Nothing <*> TMap.new <*> TMap.new
!newCohort <- Cohort responseId <$> STM.newTVar Nothing <*> TMap.new <*> TMap.new
addToCohort sinkId newCohort
TMap.insert newCohort cohortKey $ _pCohorts handler

View File

@ -33,7 +33,7 @@ lookup :: (Eq k, Hashable k) => k -> TMap k v -> STM (Maybe v)
lookup k = fmap (Map.lookup k) . readTVar . unTMap
insert :: (Eq k, Hashable k) => v -> k -> TMap k v -> STM ()
insert v k mapTv = modifyTVar' (unTMap mapTv) $ Map.insert k v
insert !v k mapTv = modifyTVar' (unTMap mapTv) $ Map.insert k v
delete :: (Eq k, Hashable k) => k -> TMap k v -> STM ()
delete k mapTv = modifyTVar' (unTMap mapTv) $ Map.delete k

View File

@ -1,5 +1,6 @@
module Hasura.GraphQL.Resolve
( mutFldToTx
, queryFldToPGAST
, traverseQueryRootFldAST
, UnresolvedVal(..)
@ -120,29 +121,30 @@ mutFldToTx
, MonadIO m
)
=> V.Field
-> m RespTx
-> m (RespTx, HTTP.ResponseHeaders)
mutFldToTx fld = do
userInfo <- asks getter
opCtx <- getOpCtx $ V._fName fld
let noRespHeaders = fmap (,[])
case opCtx of
MCInsert ctx -> do
validateHdrs userInfo (_iocHeaders ctx)
RI.convertInsert (userRole userInfo) (_iocTable ctx) fld
noRespHeaders $ RI.convertInsert (userRole userInfo) (_iocTable ctx) fld
MCInsertOne ctx -> do
validateHdrs userInfo (_iocHeaders ctx)
RI.convertInsertOne (userRole userInfo) (_iocTable ctx) fld
noRespHeaders $ RI.convertInsertOne (userRole userInfo) (_iocTable ctx) fld
MCUpdate ctx -> do
validateHdrs userInfo (_uocHeaders ctx)
RM.convertUpdate ctx fld
noRespHeaders $ RM.convertUpdate ctx fld
MCUpdateByPk ctx -> do
validateHdrs userInfo (_uocHeaders ctx)
RM.convertUpdateByPk ctx fld
noRespHeaders $ RM.convertUpdateByPk ctx fld
MCDelete ctx -> do
validateHdrs userInfo (_docHeaders ctx)
RM.convertDelete ctx fld
noRespHeaders $ RM.convertDelete ctx fld
MCDeleteByPk ctx -> do
validateHdrs userInfo (_docHeaders ctx)
RM.convertDeleteByPk ctx fld
noRespHeaders $ RM.convertDeleteByPk ctx fld
MCAction ctx ->
RA.resolveActionMutation fld ctx (userVars userInfo)

View File

@ -41,7 +41,7 @@ import Hasura.RQL.DDL.Schema.Cache
import Hasura.RQL.DML.Select (asSingleRowJsonResp)
import Hasura.RQL.Types
import Hasura.RQL.Types.Run
import Hasura.Server.Utils (mkClientHeadersForward)
import Hasura.Server.Utils (mkClientHeadersForward, mkSetCookieHeaders)
import Hasura.Server.Version (HasVersion)
import Hasura.SQL.Types
import Hasura.SQL.Value (PGScalarValue (..), pgScalarValueToJson,
@ -97,13 +97,13 @@ resolveActionMutation
=> Field
-> ActionExecutionContext
-> UserVars
-> m RespTx
-> m (RespTx, HTTP.ResponseHeaders)
resolveActionMutation field executionContext sessionVariables =
case executionContext of
ActionExecutionSyncWebhook executionContextSync ->
resolveActionMutationSync field executionContextSync sessionVariables
ActionExecutionAsync ->
resolveActionMutationAsync field sessionVariables
(,[]) <$> resolveActionMutationAsync field sessionVariables
-- | Synchronously execute webhook handler and resolve response to action "output"
resolveActionMutationSync
@ -121,14 +121,15 @@ resolveActionMutationSync
=> Field
-> SyncActionExecutionContext
-> UserVars
-> m RespTx
-> m (RespTx, HTTP.ResponseHeaders)
resolveActionMutationSync field executionContext sessionVariables = do
let inputArgs = J.toJSON $ fmap annInpValueToJson $ _fArguments field
actionContext = ActionContext actionName
handlerPayload = ActionWebhookPayload actionContext sessionVariables inputArgs
manager <- asks getter
reqHeaders <- asks getter
webhookRes <- callWebhook manager outputType reqHeaders confHeaders forwardClientHeaders resolvedWebhook handlerPayload
(webhookRes, respHeaders) <- callWebhook manager outputType outputFields reqHeaders confHeaders
forwardClientHeaders resolvedWebhook handlerPayload
let webhookResponseExpression = RS.AEInput $ UVSQL $
toTxtValue $ WithScalarType PGJSONB $ PGValJSONB $ Q.JSONB $ J.toJSON webhookRes
selectAstUnresolved <-
@ -136,9 +137,9 @@ resolveActionMutationSync field executionContext sessionVariables = do
(_fType field) $ _fSelSet field
astResolved <- RS.traverseAnnSimpleSel resolveValTxt selectAstUnresolved
let jsonAggType = mkJsonAggSelect outputType
return $ asSingleRowJsonResp (RS.selectQuerySQL jsonAggType astResolved) []
return $ (,respHeaders) $ asSingleRowJsonResp (RS.selectQuerySQL jsonAggType astResolved) []
where
SyncActionExecutionContext actionName outputType definitionList resolvedWebhook confHeaders
SyncActionExecutionContext actionName outputType outputFields definitionList resolvedWebhook confHeaders
forwardClientHeaders = executionContext
{- Note: [Async action architecture]
@ -281,9 +282,6 @@ asyncActionsProcessor cacheRef pgPool httpManager = forever $ do
A.mapConcurrently_ (callHandler actionCache) asyncInvocations
threadDelay (1 * 1000 * 1000)
where
getActionDefinition actionCache actionName =
_aiDefinition <$> Map.lookup actionName actionCache
runTx :: (Monoid a) => Q.TxE QErr a -> IO a
runTx q = do
res <- runExceptT $ Q.runTx' pgPool q
@ -293,20 +291,23 @@ asyncActionsProcessor cacheRef pgPool httpManager = forever $ do
callHandler actionCache actionLogItem = do
let ActionLogItem actionId actionName reqHeaders
sessionVariables inputPayload = actionLogItem
case getActionDefinition actionCache actionName of
case Map.lookup actionName actionCache of
Nothing -> return ()
Just definition -> do
let webhookUrl = _adHandler definition
Just actionInfo -> do
let definition = _aiDefinition actionInfo
outputFields = _aiOutputFields actionInfo
webhookUrl = _adHandler definition
forwardClientHeaders = _adForwardClientHeaders definition
confHeaders = _adHeaders definition
outputType = _adOutputType definition
actionContext = ActionContext actionName
eitherRes <- runExceptT $ callWebhook httpManager outputType reqHeaders confHeaders
forwardClientHeaders webhookUrl $
ActionWebhookPayload actionContext sessionVariables inputPayload
eitherRes <- runExceptT $
callWebhook httpManager outputType outputFields reqHeaders confHeaders
forwardClientHeaders webhookUrl $
ActionWebhookPayload actionContext sessionVariables inputPayload
case eitherRes of
Left e -> setError actionId e
Right responsePayload -> setCompleted actionId $ J.toJSON responsePayload
Left e -> setError actionId e
Right (responsePayload, _) -> setCompleted actionId $ J.toJSON responsePayload
setError :: UUID.UUID -> QErr -> IO ()
setError actionId e =
@ -361,13 +362,15 @@ callWebhook
:: (HasVersion, MonadIO m, MonadError QErr m)
=> HTTP.Manager
-> GraphQLType
-> ActionOutputFields
-> [HTTP.Header]
-> [HeaderConf]
-> Bool
-> ResolvedWebhook
-> ActionWebhookPayload
-> m ActionWebhookResponse
callWebhook manager outputType reqHeaders confHeaders forwardClientHeaders resolvedWebhook actionWebhookPayload = do
-> m (ActionWebhookResponse, HTTP.ResponseHeaders)
callWebhook manager outputType outputFields reqHeaders confHeaders
forwardClientHeaders resolvedWebhook actionWebhookPayload = do
resolvedConfHeaders <- makeHeadersFromConf confHeaders
let clientHeaders = if forwardClientHeaders then mkClientHeadersForward reqHeaders else []
contentType = ("Content-Type", "application/json")
@ -396,14 +399,19 @@ callWebhook manager outputType reqHeaders confHeaders forwardClientHeaders resol
if | HTTP.statusIsSuccessful responseStatus -> do
let expectingArray = isListType outputType
addInternalToErr e = e{qeInternal = Just webhookResponseObject}
throw400Detail t = throwError $ addInternalToErr $ err400 Unexpected t
webhookResponse <- modifyQErr addInternalToErr $ decodeValue responseValue
case webhookResponse of
AWRArray{} -> when (not expectingArray) $
throw400Detail "expecting object for action webhook response but got array"
AWRObject{} -> when expectingArray $
throw400Detail "expecting array for action webhook response but got object"
pure webhookResponse
-- Incase any error, add webhook response in internal
modifyQErr addInternalToErr $ do
webhookResponse <- decodeValue responseValue
case webhookResponse of
AWRArray objs -> do
when (not expectingArray) $
throwUnexpected "expecting object for action webhook response but got array"
mapM_ validateResponseObject objs
AWRObject obj -> do
when expectingArray $
throwUnexpected "expecting array for action webhook response but got object"
validateResponseObject obj
pure (webhookResponse, mkSetCookieHeaders responseWreq)
| HTTP.statusIsClientError responseStatus -> do
ActionWebhookErrorResponse message maybeCode <-
@ -414,6 +422,23 @@ callWebhook manager outputType reqHeaders confHeaders forwardClientHeaders resol
| otherwise ->
throw500WithDetail "internal error" webhookResponseObject
where
throwUnexpected = throw400 Unexpected
-- Webhook response object should conform to action output fields
validateResponseObject obj = do
-- Fields not specified in the output type shouldn't be present in the response
let extraFields = filter (not . flip Map.member outputFields) $ map G.Name $ Map.keys obj
when (not $ null extraFields) $ throwUnexpected $
"unexpected fields in webhook response: " <> showNames extraFields
void $ flip Map.traverseWithKey outputFields $ \fieldName fieldTy ->
-- When field is non-nullable, it has to present in the response with no null value
when (not $ G.isNullable fieldTy) $ case Map.lookup (G.unName fieldName) obj of
Nothing -> throwUnexpected $
"field " <> fieldName <<> " expected in webhook response, but not found"
Just v -> when (v == J.Null) $ throwUnexpected $
"expecting not null value for field " <>> fieldName
annInpValueToJson :: AnnInpVal -> J.Value
annInpValueToJson annInpValue =

View File

@ -107,6 +107,7 @@ data SyncActionExecutionContext
= SyncActionExecutionContext
{ _saecName :: !ActionName
, _saecOutputType :: !GraphQLType
, _saecOutputFields :: !ActionOutputFields
, _saecDefinitionList :: ![(PGCol, PGScalarType)]
, _saecWebhook :: !ResolvedWebhook
, _saecHeaders :: ![HeaderConf]

View File

@ -68,6 +68,7 @@ mkMutationField actionName actionInfo definitionList =
ActionSynchronous ->
ActionExecutionSyncWebhook $ SyncActionExecutionContext actionName
(_adOutputType definition)
(_aiOutputFields actionInfo)
definitionList
(_adHandler definition)
(_adHeaders definition)

View File

@ -20,6 +20,7 @@ import qualified Hasura.GraphQL.Execute as E
import qualified Hasura.Logging as L
import qualified Hasura.Server.Telemetry.Counters as Telem
import qualified Language.GraphQL.Draft.Syntax as G
import qualified Network.HTTP.Types as HTTP
runGQ
:: ( HasVersion
@ -41,8 +42,8 @@ runGQ reqId userInfo reqHdrs req = do
userInfo sqlGenCtx enableAL sc scVer httpManager reqHdrs req
case execPlan of
E.GExPHasura resolvedOp -> do
(telemTimeIO, telemQueryType, resp) <- runHasuraGQ reqId req userInfo resolvedOp
return (telemCacheHit, Telem.Local, (telemTimeIO, telemQueryType, HttpResponse resp Nothing))
(telemTimeIO, telemQueryType, respHdrs, resp) <- runHasuraGQ reqId req userInfo resolvedOp
return (telemCacheHit, Telem.Local, (telemTimeIO, telemQueryType, HttpResponse resp respHdrs))
E.GExPRemote rsi opDef -> do
let telemQueryType | G._todType opDef == G.OperationTypeMutation = Telem.Mutation
| otherwise = Telem.Query
@ -73,7 +74,7 @@ runGQBatched reqId userInfo reqHdrs reqs =
-- responses with distinct headers, so just do the simplest thing
-- in this case, and don't forward any.
let removeHeaders =
flip HttpResponse Nothing
flip HttpResponse []
. encJFromList
. map (either (encJFromJValue . encodeGQErr False) _hrBody)
try = flip catchError (pure . Left) . fmap Right
@ -89,7 +90,7 @@ runHasuraGQ
-> GQLReqUnparsed
-> UserInfo
-> E.ExecOp
-> m (DiffTime, Telem.QueryType, EncJSON)
-> m (DiffTime, Telem.QueryType, HTTP.ResponseHeaders, EncJSON)
-- ^ Also return 'Mutation' when the operation was a mutation, and the time
-- spent in the PG query; for telemetry.
runHasuraGQ reqId query userInfo resolvedOp = do
@ -98,15 +99,15 @@ runHasuraGQ reqId query userInfo resolvedOp = do
E.ExOpQuery tx genSql -> do
-- log the generated SQL and the graphql query
L.unLogger logger $ QueryLog query genSql reqId
runLazyTx' pgExecCtx tx
E.ExOpMutation tx -> do
([],) <$> runLazyTx' pgExecCtx tx
E.ExOpMutation respHeaders tx -> do
-- log the graphql query
L.unLogger logger $ QueryLog query Nothing reqId
runLazyTx pgExecCtx Q.ReadWrite $ withUserInfo userInfo tx
(respHeaders,) <$> runLazyTx pgExecCtx Q.ReadWrite (withUserInfo userInfo tx)
E.ExOpSubs _ ->
throw400 UnexpectedPayload
"subscriptions are not supported over HTTP, use websockets instead"
resp <- liftEither respE
(respHdrs, resp) <- liftEither respE
let !json = encodeGQResp $ GQSuccess $ encJToLBS resp
telemQueryType = case resolvedOp of E.ExOpMutation{} -> Telem.Mutation ; _ -> Telem.Query
return (telemTimeIO, telemQueryType, json)
return (telemTimeIO, telemQueryType, respHdrs, json)

View File

@ -35,6 +35,7 @@ import qualified StmContainers.Map as STMMap
import Control.Concurrent.Extended (sleep)
import Control.Exception.Lifted
import Data.String
import GHC.AssertNF
import qualified ListT
import Hasura.EncJSON
@ -62,7 +63,7 @@ import qualified Hasura.Server.Telemetry.Counters as Telem
-- this to track a connection's operations so we can remove them from 'LiveQueryState', and
-- log.
--
-- NOTE!: This must be kept consistent with the global 'LiveQueryState', in 'onClose'
-- NOTE!: This must be kept consistent with the global 'LiveQueryState', in 'onClose'
-- and 'onStart'.
type OperationMap
= STMMap.Map OperationId (LQ.LiveQueryId, Maybe OperationName)
@ -79,10 +80,10 @@ data ErrRespType
data WSConnState
-- headers from the client for websockets
= CSNotInitialised !WsHeaders
| CSInitError Text
| CSInitError !Text
-- headers from the client (in conn params) to forward to the remote schema
-- and JWT expiry time if any
| CSInitialised UserInfo (Maybe TC.UTCTime) [H.Header]
| CSInitialised !UserInfo !(Maybe TC.UTCTime) ![H.Header]
data WSConnData
= WSConnData
@ -108,9 +109,9 @@ sendMsgWithMetadata wsConn msg (LQ.LiveQueryMetadata execTime) =
liftIO $ WS.sendMsg wsConn $ WS.WSQueueResponse bs wsInfo
where
bs = encodeServerMsg msg
wsInfo = Just $ WS.WSEventInfo
{ WS._wseiQueryExecutionTime = Just $ realToFrac execTime
, WS._wseiResponseSize = Just $ BL.length bs
wsInfo = Just $! WS.WSEventInfo
{ WS._wseiQueryExecutionTime = Just $! realToFrac execTime
, WS._wseiResponseSize = Just $! BL.length bs
}
data OpDetail
@ -232,8 +233,7 @@ onConn (L.Logger logger) corsPolicy wsId requestHead = do
<*> pure errType
let acceptRequest = WS.defaultAcceptRequest
{ WS.acceptSubprotocol = Just "graphql-ws"}
return $ Right $ WS.AcceptWith connData acceptRequest
(Just keepAliveAction) (Just jwtExpiryHandler)
return $ Right $ WS.AcceptWith connData acceptRequest keepAliveAction jwtExpiryHandler
reject qErr = do
logger $ mkWsErrorLog Nothing (WsConnInfo wsId Nothing Nothing) (ERejected qErr)
@ -325,7 +325,8 @@ onStart serverEnv wsConn (StartMsg opId q) = catchAndIgnore $ do
runHasuraGQ timerTot telemCacheHit reqId query userInfo = \case
E.ExOpQuery opTx genSql ->
execQueryOrMut Telem.Query genSql $ runLazyTx' pgExecCtx opTx
E.ExOpMutation opTx ->
-- Response headers discarded over websockets
E.ExOpMutation _ opTx ->
execQueryOrMut Telem.Mutation Nothing $
runLazyTx pgExecCtx Q.ReadWrite $ withUserInfo userInfo opTx
E.ExOpSubs lqOp -> do
@ -333,10 +334,13 @@ onStart serverEnv wsConn (StartMsg opId q) = catchAndIgnore $ do
L.unLogger logger $ QueryLog query Nothing reqId
-- NOTE!: we mask async exceptions higher in the call stack, but it's
-- crucial we don't lose lqId after addLiveQuery returns successfully.
lqId <- liftIO $ LQ.addLiveQuery logger lqMap lqOp liveQOnChange
!lqId <- liftIO $ LQ.addLiveQuery logger lqMap lqOp liveQOnChange
let !opName = _grOperationName q
liftIO $ $assertNFHere $! (lqId, opName) -- so we don't write thunks to mutable vars
liftIO $ STM.atomically $
-- NOTE: see crucial `lookup` check above, ensuring this doesn't clobber:
STMMap.insert (lqId, _grOperationName q) opId opMap
STMMap.insert (lqId, opName) opId opMap
logOpEv ODStarted (Just reqId)
where
@ -534,14 +538,20 @@ onConnInit logger manager wsConn authMode connParamsM = do
res <- resolveUserInfo logger manager headers authMode
case res of
Left e -> do
liftIO $ STM.atomically $ STM.writeTVar (_wscUser $ WS.getData wsConn) $
CSInitError $ qeError e
let !initErr = CSInitError $ qeError e
liftIO $ do
$assertNFHere initErr -- so we don't write thunks to mutable vars
STM.atomically $ STM.writeTVar (_wscUser $ WS.getData wsConn) initErr
let connErr = ConnErrMsg $ qeError e
logWSEvent logger wsConn $ EConnErr connErr
sendMsg wsConn $ SMConnErr connErr
Right (userInfo, expTimeM) -> do
liftIO $ STM.atomically $ STM.writeTVar (_wscUser $ WS.getData wsConn) $
CSInitialised userInfo expTimeM paramHeaders
let !csInit = CSInitialised userInfo expTimeM paramHeaders
liftIO $ do
$assertNFHere csInit -- so we don't write thunks to mutable vars
STM.atomically $ STM.writeTVar (_wscUser $ WS.getData wsConn) csInit
sendMsg wsConn SMConnAck
-- TODO: send it periodically? Why doesn't apollo's protocol use
-- ping/pong frames of websocket spec?
@ -603,8 +613,8 @@ createWSServerApp
-> WSServerEnv
-> WS.PendingConnection -> m ()
-- ^ aka generalized 'WS.ServerApp'
createWSServerApp authMode serverEnv =
WS.createServerApp (_wseServer serverEnv) handlers
createWSServerApp authMode serverEnv = \ !pendingConn ->
WS.createServerApp (_wseServer serverEnv) handlers pendingConn
where
handlers =
WS.WSHandlers

View File

@ -41,6 +41,7 @@ import qualified Data.UUID.V4 as UUID
import Data.Word (Word16)
import GHC.Int (Int64)
import Hasura.Prelude
import GHC.AssertNF
import qualified ListT
import qualified Network.WebSockets as WS
import qualified StmContainers.Map as STMMap
@ -141,7 +142,9 @@ closeConnWithCode wsConn code bs = do
-- writes to a queue instead of the raw connection
-- so that sendMsg doesn't block
sendMsg :: WSConn a -> WSQueueResponse -> IO ()
sendMsg wsConn = STM.atomically . STM.writeTQueue (_wcSendQ wsConn)
sendMsg wsConn = \ !resp -> do
$assertNFHere resp -- so we don't write thunks to mutable vars
STM.atomically $ STM.writeTQueue (_wcSendQ wsConn) resp
type ConnMap a = STMMap.Map WSId (WSConn a)
@ -193,8 +196,8 @@ data AcceptWith a
= AcceptWith
{ _awData :: !a
, _awReq :: !WS.AcceptRequest
, _awKeepAlive :: !(Maybe (WSConn a -> IO ()))
, _awOnJwtExpiry :: !(Maybe (WSConn a -> IO ()))
, _awKeepAlive :: !(WSConn a -> IO ())
, _awOnJwtExpiry :: !(WSConn a -> IO ())
}
type OnConnH m a = WSId -> WS.RequestHead -> m (Either WS.RejectRequest (AcceptWith a))
@ -216,7 +219,8 @@ createServerApp
-- aka WS.ServerApp
-> WS.PendingConnection
-> m ()
createServerApp (WSServer logger@(L.Logger writeLog) serverStatus) wsHandlers pendingConn = do
{-# INLINE createServerApp #-}
createServerApp (WSServer logger@(L.Logger writeLog) serverStatus) wsHandlers !pendingConn = do
wsId <- WSId <$> liftIO UUID.nextRandom
writeLog $ WSLog wsId EConnectionRequest Nothing
status <- liftIO $ STM.readTVarIO serverStatus
@ -247,11 +251,17 @@ createServerApp (WSServer logger@(L.Logger writeLog) serverStatus) wsHandlers pe
liftIO $ WS.rejectRequestWith pendingConn rejectRequest
writeLog $ WSLog wsId ERejected Nothing
onAccept wsId (AcceptWith a acceptWithParams keepAliveM onJwtExpiryM) = do
onAccept wsId (AcceptWith a acceptWithParams keepAlive onJwtExpiry) = do
conn <- liftIO $ WS.acceptRequestWith pendingConn acceptWithParams
writeLog $ WSLog wsId EAccepted Nothing
sendQ <- liftIO STM.newTQueueIO
let wsConn = WSConn wsId logger conn sendQ a
let !wsConn = WSConn wsId logger conn sendQ a
-- TODO there are many thunks here. Difficult to trace how much is retained, and
-- how much of that would be shared anyway.
-- Requires a fork of 'wai-websockets' and 'websockets', it looks like.
-- Adding `package` stanzas with -Xstrict -XStrictData for those two packages
-- helped, cutting the number of thunks approximately in half.
liftIO $ $assertNFHere wsConn -- so we don't write thunks to mutable vars
let whenAcceptingInsertConn = liftIO $ STM.atomically $ do
status <- STM.readTVar serverStatus
@ -284,21 +294,15 @@ createServerApp (WSServer logger@(L.Logger writeLog) serverStatus) wsHandlers pe
liftIO $ WS.sendTextData conn msg
writeLog $ WSLog wsId (EMessageSent $ TBS.fromLBS msg) wsInfo
let withAsyncM mAction cont = case mAction of
Nothing -> cont Nothing
Just action -> LA.withAsync (liftIO $ action wsConn) $
\actRef -> cont $ Just actRef
-- withAsync lets us be very sure that if e.g. an async exception is raised while we're
-- forking that the threads we launched will be cleaned up. See also below.
LA.withAsync rcv $ \rcvRef -> do
LA.withAsync send $ \sendRef -> do
withAsyncM keepAliveM $ \keepAliveRefM -> do
withAsyncM onJwtExpiryM $ \onJwtExpiryRefM -> do
LA.withAsync (liftIO $ keepAlive wsConn) $ \keepAliveRef -> do
LA.withAsync (liftIO $ onJwtExpiry wsConn) $ \onJwtExpiryRef -> do
-- terminates on WS.ConnectionException and JWT expiry
let waitOnRefs = catMaybes [keepAliveRefM, onJwtExpiryRefM]
<> [rcvRef, sendRef]
let waitOnRefs = [keepAliveRef, onJwtExpiryRef, rcvRef, sendRef]
-- withAnyCancel re-raises exceptions from forkedThreads, and is guarenteed to cancel in
-- case of async exceptions raised while blocking here:
try (LA.waitAnyCancel waitOnRefs) >>= \case

View File

@ -118,7 +118,7 @@ rFirst (Rule r) = Rule \s (a, c) k -> r s a \s' b r' -> k s' (b, c) (rFirst r')
{-# INLINABLE[0] rFirst #-}
{-# RULES
"first/id" rFirst rId = rId
"first/arr" forall f. rFirst (rArr f) = rArr (second f)
"first/arr" forall f. rFirst (rArr f) = rArr (first f)
"first/arrM" forall f. rFirst (rArrM f) = rArrM (runKleisli (first (Kleisli f)))
"first/push" [~1] forall f g. rFirst (f `rComp` g) = rFirst f `rComp` rFirst g
"first/pull" [1] forall f g. rFirst f `rComp` rFirst g = rFirst (f `rComp` g)

View File

@ -81,7 +81,7 @@ resolveAction
:: (QErrM m, MonadIO m)
=> (NonObjectTypeMap, AnnotatedObjects)
-> ActionDefinitionInput
-> m ResolvedActionDefinition
-> m (ResolvedActionDefinition, ActionOutputFields)
resolveAction customTypes actionDefinition = do
let responseType = unGraphQLType $ _adOutputType actionDefinition
responseBaseType = G.getBaseType responseType
@ -96,8 +96,10 @@ resolveAction customTypes actionDefinition = do
<> showNamedTy argumentBaseType <>
" should be a scalar/enum/input_object"
-- Check if the response type is an object
getObjectTypeInfo responseBaseType
traverse resolveWebhook actionDefinition
annFields <- _aotAnnotatedFields <$> getObjectTypeInfo responseBaseType
let outputFields = Map.fromList $ map (unObjectFieldName *** fst) $ Map.toList annFields
resolvedDef <- traverse resolveWebhook actionDefinition
pure (resolvedDef, outputFields)
where
getNonObjectTypeInfo typeName = do
let nonObjectTypeMap = unNonObjectTypeMap $ fst $ customTypes

View File

@ -48,8 +48,7 @@ validateCustomTypeDefinitions tableCache customTypes = do
enumTypes =
Set.fromList $ map (unEnumTypeName . _etdName) enumDefinitions
-- TODO, clean it up maybe?
defaultScalars = map G.NamedType ["Int", "Float", "String", "Boolean"]
defaultScalars = map G.NamedType ["Int", "Float", "String", "Boolean", "ID"]
validateEnum
:: (MonadValidate [CustomTypeValidationError] m)

View File

@ -256,7 +256,7 @@ class (ToJSON a) => IsPerm a where
getPermAcc2
:: DropPerm a -> PermAccessor (PermInfo a)
getPermAcc2 _ = permAccessor
addPermP2 :: (IsPerm a, MonadTx m, HasSystemDefined m) => QualifiedTable -> PermDef a -> m ()
addPermP2 tn pd = do
let pt = permAccToType $ getPermAcc1 pd

View File

@ -266,10 +266,10 @@ buildSchemaCacheRule = proc (catalogMetadata, invalidationKeys) -> do
addActionContext e = "in action " <> name <<> "; " <> e
(| withRecordInconsistency (
(| modifyErrA ( do
resolvedDef <- bindErrorA -< resolveAction resolvedCustomTypes def
(resolvedDef, outFields) <- bindErrorA -< resolveAction resolvedCustomTypes def
let permissionInfos = map (ActionPermissionInfo . _apmRole) actionPermissions
permissionMap = mapFromL _apiRole permissionInfos
returnA -< ActionInfo name resolvedDef permissionMap comment
returnA -< ActionInfo name outFields resolvedDef permissionMap comment
)
|) addActionContext)
|) metadataObj)

View File

@ -13,8 +13,10 @@ module Hasura.RQL.Types.Action
, ResolvedWebhook(..)
, ResolvedActionDefinition
, ActionOutputFields
, ActionInfo(..)
, aiName
, aiOutputFields
, aiDefinition
, aiPermissions
, aiComment
@ -117,13 +119,15 @@ data ActionPermissionInfo
$(J.deriveToJSON (J.aesonDrop 4 J.snakeCase) ''ActionPermissionInfo)
type ActionPermissionMap = Map.HashMap RoleName ActionPermissionInfo
type ActionOutputFields = Map.HashMap G.Name G.GType
data ActionInfo
= ActionInfo
{ _aiName :: !ActionName
, _aiDefinition :: !ResolvedActionDefinition
, _aiPermissions :: !ActionPermissionMap
, _aiComment :: !(Maybe Text)
{ _aiName :: !ActionName
, _aiOutputFields :: !ActionOutputFields
, _aiDefinition :: !ResolvedActionDefinition
, _aiPermissions :: !ActionPermissionMap
, _aiComment :: !(Maybe Text)
} deriving (Show, Eq)
$(J.deriveToJSON (J.aesonDrop 3 J.snakeCase) ''ActionInfo)
$(makeLenses ''ActionInfo)

View File

@ -7,7 +7,7 @@ import Control.Concurrent.MVar.Lifted
import Control.Exception (IOException, try)
import Control.Lens (view, _2)
import Control.Monad.Stateless
import Control.Monad.Trans.Control (MonadBaseControl)
import Control.Monad.Trans.Control (MonadBaseControl)
import Data.Aeson hiding (json)
import Data.Either (isRight)
import Data.Int (Int64)
@ -21,6 +21,7 @@ import Web.Spock.Core ((<//>))
import qualified Control.Concurrent.Async.Lifted.Safe as LA
import qualified Data.ByteString.Lazy as BL
import qualified Data.CaseInsensitive as CI
import qualified Data.HashMap.Strict as M
import qualified Data.HashSet as S
import qualified Data.Text as T
@ -71,7 +72,7 @@ data SchemaCacheRef
-- 1. Allow maximum throughput for serving requests (/v1/graphql) (as each
-- request reads the current schemacache)
-- 2. We don't want to process more than one request at any point of time
-- which would modify the schema cache as such queries are expensive.
-- which would modify the schema cache as such queries are expensive.
--
-- Another option is to consider removing this lock in place of `_scrCache ::
-- MVar ...` if it's okay or in fact correct to block during schema update in
@ -79,7 +80,7 @@ data SchemaCacheRef
-- situation (in between building new schemacache and before writing it to
-- the IORef) where we serve a request with a stale schemacache but I guess
-- it is an okay trade-off to pay for a higher throughput (I remember doing a
-- bunch of benchmarks to test this hypothesis).
-- bunch of benchmarks to test this hypothesis).
, _scrCache :: IORef (RebuildableSchemaCache Run, SchemaCacheVer)
, _scrOnChange :: IO ()
-- ^ an action to run when schemacache changes
@ -143,7 +144,7 @@ withSCUpdate scr logger action = do
(!res, !newSC) <- action
liftIO $ do
-- update schemacache in IO reference
modifyIORef' cacheRef $ \(_, prevVer) ->
modifyIORef' cacheRef $ \(_, prevVer) ->
let !newVer = incSchemaCacheVer prevVer
in (newSC, newVer)
-- log any inconsistent objects
@ -198,6 +199,10 @@ buildQCtx = do
sqlGenCtx <- scSQLGenCtx . hcServerCtx <$> ask
return $ QCtx userInfo cache sqlGenCtx
setHeader :: MonadIO m => HTTP.Header -> Spock.ActionT m ()
setHeader (headerName, headerValue) =
Spock.setHeader (bsToTxt $ CI.original headerName) (bsToTxt headerValue)
-- | Typeclass representing the metadata API authorization effect
class MetadataApiAuthorization m where
authorizeMetadataApi :: RQLQuery -> UserInfo -> Handler m ()
@ -270,24 +275,22 @@ mkSpockAction serverCtx qErrEncoder qErrModifier apiHandler = do
case result of
JSONResp (HttpResponse encJson h) ->
possiblyCompressedLazyBytes userInfo reqId req reqBody qTime (encJToLBS encJson)
(pure jsonHeader <> mkHeaders h) reqHeaders
(pure jsonHeader <> h) reqHeaders
RawResp (HttpResponse rawBytes h) ->
possiblyCompressedLazyBytes userInfo reqId req reqBody qTime rawBytes (mkHeaders h) reqHeaders
possiblyCompressedLazyBytes userInfo reqId req reqBody qTime rawBytes h reqHeaders
possiblyCompressedLazyBytes userInfo reqId req reqBody qTime respBytes respHeaders reqHeaders = do
let (compressedResp, mEncodingHeader, mCompressionType) =
compressResponse (Wai.requestHeaders req) respBytes
encodingHeader = maybe [] pure mEncodingHeader
reqIdHeader = (requestIdHeader, unRequestId reqId)
reqIdHeader = (requestIdHeader, txtToBs $ unRequestId reqId)
allRespHeaders = pure reqIdHeader <> encodingHeader <> respHeaders
lift $ logHttpSuccess logger userInfo reqId req reqBody respBytes compressedResp qTime mCompressionType reqHeaders
mapM_ (uncurry Spock.setHeader) allRespHeaders
mapM_ setHeader allRespHeaders
Spock.lazyBytes compressedResp
mkHeaders = maybe [] (map unHeader)
v1QueryHandler
:: (HasVersion, MonadIO m, MonadBaseControl IO m, MetadataApiAuthorization m)
v1QueryHandler
:: (HasVersion, MonadIO m, MonadBaseControl IO m, MetadataApiAuthorization m)
=> RQLQuery -> Handler m (HttpResponse EncJSON)
v1QueryHandler query = do
userInfo <- asks hcUser
@ -296,7 +299,7 @@ v1QueryHandler query = do
logger <- scLogger . hcServerCtx <$> ask
res <- bool (fst <$> dbAction) (withSCUpdate scRef logger dbAction) $
queryModifiesSchemaCache query
return $ HttpResponse res Nothing
return $ HttpResponse res []
where
-- Hit postgres
dbAction = do
@ -341,14 +344,14 @@ gqlExplainHandler query = do
sqlGenCtx <- scSQLGenCtx . hcServerCtx <$> ask
enableAL <- scEnableAllowlist . hcServerCtx <$> ask
res <- GE.explainGQLQuery pgExecCtx sc sqlGenCtx enableAL query
return $ HttpResponse res Nothing
return $ HttpResponse res []
v1Alpha1PGDumpHandler :: (MonadIO m) => PGD.PGDumpReqBody -> Handler m APIResp
v1Alpha1PGDumpHandler b = do
onlyAdmin
ci <- scConnInfo . hcServerCtx <$> ask
output <- PGD.execPGDump b ci
return $ RawResp $ HttpResponse output (Just [Header sqlHeader])
return $ RawResp $ HttpResponse output [sqlHeader]
consoleAssetsHandler
:: (MonadIO m, HttpLog m)
@ -366,7 +369,7 @@ consoleAssetsHandler logger dir path = do
either (onError reqHeaders) onSuccess eFileContents
where
onSuccess c = do
mapM_ (uncurry Spock.setHeader) headers
mapM_ setHeader headers
Spock.lazyBytes c
onError :: (MonadIO m, HttpLog m) => [HTTP.Header] -> IOException -> Spock.ActionT m ()
onError hdrs = raiseGenericApiError logger hdrs . err404 NotFound . T.pack . show
@ -375,7 +378,7 @@ consoleAssetsHandler logger dir path = do
(fileName, encHeader) = case T.stripSuffix ".gz" fn of
Just v -> (v, [gzipHeader])
Nothing -> (fn, [])
mimeType = bsToTxt $ defaultMimeLookup fileName
mimeType = defaultMimeLookup fileName
headers = ("Content-Type", mimeType) : encHeader
class (Monad m) => ConsoleRenderer m where
@ -552,7 +555,7 @@ httpApp corsCfg serverCtx enableConsole consoleAssetsDir enableTelemetry = do
else Spock.setStatus HTTP.status500 >> Spock.text "ERROR"
Spock.get "v1/version" $ do
uncurry Spock.setHeader jsonHeader
setHeader jsonHeader
Spock.lazyBytes $ encode $ object [ "version" .= currentVersion ]
when enableMetadata $ do
@ -578,7 +581,7 @@ httpApp corsCfg serverCtx enableConsole consoleAssetsDir enableTelemetry = do
mkGetHandler $ do
onlyAdmin
let res = encJFromJValue $ runGetConfig (scAuthMode serverCtx)
return $ JSONResp $ HttpResponse res Nothing
return $ JSONResp $ HttpResponse res []
when enableGraphQL $ do
Spock.post "v1alpha1/graphql" $ spockAction GH.encodeGQErr id $
@ -592,22 +595,22 @@ httpApp corsCfg serverCtx enableConsole consoleAssetsDir enableTelemetry = do
mkGetHandler $ do
onlyAdmin
respJ <- liftIO $ EKG.sampleAll $ scEkgStore serverCtx
return $ JSONResp $ HttpResponse (encJFromJValue $ EKG.sampleToJson respJ) Nothing
return $ JSONResp $ HttpResponse (encJFromJValue $ EKG.sampleToJson respJ) []
Spock.get "dev/plan_cache" $ spockAction encodeQErr id $
mkGetHandler $ do
onlyAdmin
respJ <- liftIO $ E.dumpPlanCache $ scPlanCache serverCtx
return $ JSONResp $ HttpResponse (encJFromJValue respJ) Nothing
return $ JSONResp $ HttpResponse (encJFromJValue respJ) []
Spock.get "dev/subscriptions" $ spockAction encodeQErr id $
mkGetHandler $ do
onlyAdmin
respJ <- liftIO $ EL.dumpLiveQueriesState False $ scLQState serverCtx
return $ JSONResp $ HttpResponse (encJFromJValue respJ) Nothing
return $ JSONResp $ HttpResponse (encJFromJValue respJ) []
Spock.get "dev/subscriptions/extended" $ spockAction encodeQErr id $
mkGetHandler $ do
onlyAdmin
respJ <- liftIO $ EL.dumpLiveQueriesState True $ scLQState serverCtx
return $ JSONResp $ HttpResponse (encJFromJValue respJ) Nothing
return $ JSONResp $ HttpResponse (encJFromJValue respJ) []
forM_ [Spock.GET, Spock.POST] $ \m -> Spock.hookAny m $ \_ -> do
req <- Spock.request
@ -672,6 +675,6 @@ raiseGenericApiError logger headers qErr = do
reqBody <- liftIO $ Wai.strictRequestBody req
reqId <- getRequestId $ Wai.requestHeaders req
lift $ logHttpError logger Nothing reqId req (Left reqBody) qErr headers
uncurry Spock.setHeader jsonHeader
setHeader jsonHeader
Spock.setStatus $ qeStatus qErr
Spock.lazyBytes $ encode qErr

View File

@ -19,20 +19,20 @@ module Hasura.Server.Auth
) where
import Control.Concurrent.Extended (forkImmortal)
import Control.Exception (try)
import Control.Exception (try)
import Control.Lens
import Data.Aeson
import Data.IORef (newIORef)
import Data.Time.Clock (UTCTime)
import Hasura.Server.Version (HasVersion)
import Data.IORef (newIORef)
import Data.Time.Clock (UTCTime)
import Hasura.Server.Version (HasVersion)
import qualified Data.Aeson as J
import qualified Data.ByteString.Lazy as BL
import qualified Data.HashMap.Strict as Map
import qualified Data.Text as T
import qualified Network.HTTP.Client as H
import qualified Network.HTTP.Types as N
import qualified Network.Wreq as Wreq
import qualified Data.Aeson as J
import qualified Data.ByteString.Lazy as BL
import qualified Data.HashMap.Strict as Map
import qualified Data.Text as T
import qualified Network.HTTP.Client as H
import qualified Network.HTTP.Types as N
import qualified Network.Wreq as Wreq
import Hasura.HTTP
import Hasura.Logging
@ -294,7 +294,7 @@ getUserInfoWithExpTime logger manager rawHeaders = \case
userInfoWhenNoAdminSecret = \case
Nothing -> throw401 $ adminSecretHeader <> "/"
<> deprecatedAccessKeyHeader <> " required, but not found"
<> deprecatedAccessKeyHeader <> " required, but not found"
Just role -> return $ mkUserInfo role usrVars
withNoExpTime a = (, Nothing) <$> a

View File

@ -20,6 +20,7 @@ import Data.Parser.CacheControl (parseMaxAge)
import Data.Time.Clock (NominalDiffTime, UTCTime, diffUTCTime,
getCurrentTime)
import Data.Time.Format (defaultTimeLocale, parseTimeM)
import GHC.AssertNF
import Network.URI (URI)
import Hasura.HTTP
@ -28,7 +29,7 @@ import Hasura.Prelude
import Hasura.RQL.Types
import Hasura.Server.Auth.JWT.Internal (parseHmacKey, parseRsaKey)
import Hasura.Server.Auth.JWT.Logging
import Hasura.Server.Utils (fmapL, userRoleHeader)
import Hasura.Server.Utils (fmapL, getRequestHeader, userRoleHeader)
import Hasura.Server.Version (HasVersion)
import qualified Control.Concurrent.Extended as C
@ -148,8 +149,10 @@ updateJwkRef (Logger logger) manager url jwkRef = do
logAndThrow err
let parseErr e = JFEJwkParseError (T.pack e) $ "Error parsing JWK from url: " <> urlT
jwkset <- either (logAndThrow . parseErr) return $ J.eitherDecode respBody
liftIO $ writeIORef jwkRef jwkset
!jwkset <- either (logAndThrow . parseErr) return $ J.eitherDecode' respBody
liftIO $ do
$assertNFHere jwkset -- so we don't write thunks to mutable vars
writeIORef jwkRef jwkset
-- first check for Cache-Control header to get max-age, if not found, look for Expires header
let cacheHeader = resp ^? Wreq.responseHeader "Cache-Control"
@ -294,8 +297,7 @@ processAuthZHeader jwtCtx headers authzHeader = do
-- see if there is a x-hasura-role header, or else pick the default role
getCurrentRole defaultRole =
let userRoleHeaderB = CS.cs userRoleHeader
mUserRole = snd <$> find (\h -> fst h == CI.mk userRoleHeaderB) headers
let mUserRole = getRequestHeader userRoleHeader headers
in maybe defaultRole RoleName $ mUserRole >>= mkNonEmptyText . bsToTxt
decodeJSON val = case J.fromJSON val of

View File

@ -24,7 +24,7 @@ compressionTypeToTxt CTGZip = "gzip"
compressResponse
:: NH.RequestHeaders
-> BL.ByteString
-> (BL.ByteString, Maybe (Text, Text), Maybe CompressionType)
-> (BL.ByteString, Maybe NH.Header, Maybe CompressionType)
compressResponse reqHeaders unCompressedResp =
let compressionTypeM = getRequestedCompression reqHeaders
appendCompressionType (res, headerM) = (res, headerM, compressionTypeM)

View File

@ -1,20 +1,13 @@
module Hasura.Server.Context
( HttpResponse(..)
, Header (..)
, Headers
)
(HttpResponse(..))
where
import Hasura.Prelude
newtype Header
= Header { unHeader :: (Text, Text) }
deriving (Show, Eq)
type Headers = [Header]
import qualified Network.HTTP.Types as HTTP
data HttpResponse a
= HttpResponse
{ _hrBody :: !a
, _hrHeaders :: !(Maybe Headers)
, _hrHeaders :: !HTTP.ResponseHeaders
} deriving (Functor, Foldable, Traversable)

View File

@ -17,6 +17,7 @@ import Data.Aeson
import Data.Aeson.Casing
import Data.Aeson.TH
import Data.IORef
import GHC.AssertNF
import qualified Control.Concurrent.Extended as C
import qualified Control.Concurrent.STM as STM
@ -159,6 +160,7 @@ listener sqlGenCtx pool logger httpMgr updateEventRef
Left e -> logError logger threadType $ TEJsonParse $ T.pack e
Right payload -> do
logInfo logger threadType $ object ["received_event" .= payload]
$assertNFHere payload -- so we don't write thunks to mutable vars
-- Push a notify event to Queue
STM.atomically $ STM.writeTVar updateEventRef $ Just payload

View File

@ -1,6 +1,7 @@
{-# LANGUAGE TypeApplications #-}
module Hasura.Server.Utils where
import Control.Lens ((^..))
import Data.Aeson
import Data.Char
import Data.List (find)
@ -21,6 +22,7 @@ import qualified Data.UUID.V4 as UUID
import qualified Language.Haskell.TH.Syntax as TH
import qualified Network.HTTP.Client as HC
import qualified Network.HTTP.Types as HTTP
import qualified Network.Wreq as Wreq
import qualified Text.Regex.TDFA as TDFA
import qualified Text.Regex.TDFA.ByteString as TDFA
@ -30,45 +32,42 @@ newtype RequestId
= RequestId { unRequestId :: Text }
deriving (Show, Eq, ToJSON, FromJSON)
jsonHeader :: (T.Text, T.Text)
jsonHeader :: HTTP.Header
jsonHeader = ("Content-Type", "application/json; charset=utf-8")
sqlHeader :: (T.Text, T.Text)
sqlHeader :: HTTP.Header
sqlHeader = ("Content-Type", "application/sql; charset=utf-8")
htmlHeader :: (T.Text, T.Text)
htmlHeader :: HTTP.Header
htmlHeader = ("Content-Type", "text/html; charset=utf-8")
gzipHeader :: (T.Text, T.Text)
gzipHeader :: HTTP.Header
gzipHeader = ("Content-Encoding", "gzip")
brHeader :: (T.Text, T.Text)
brHeader = ("Content-Encoding", "br")
userRoleHeader :: T.Text
userRoleHeader :: IsString a => a
userRoleHeader = "x-hasura-role"
deprecatedAccessKeyHeader :: T.Text
deprecatedAccessKeyHeader :: IsString a => a
deprecatedAccessKeyHeader = "x-hasura-access-key"
adminSecretHeader :: T.Text
adminSecretHeader :: IsString a => a
adminSecretHeader = "x-hasura-admin-secret"
userIdHeader :: T.Text
userIdHeader :: IsString a => a
userIdHeader = "x-hasura-user-id"
requestIdHeader :: T.Text
requestIdHeader :: IsString a => a
requestIdHeader = "x-request-id"
getRequestHeader :: B.ByteString -> [HTTP.Header] -> Maybe B.ByteString
getRequestHeader :: HTTP.HeaderName -> [HTTP.Header] -> Maybe B.ByteString
getRequestHeader hdrName hdrs = snd <$> mHeader
where
mHeader = find (\h -> fst h == CI.mk hdrName) hdrs
mHeader = find (\h -> fst h == hdrName) hdrs
getRequestId :: (MonadIO m) => [HTTP.Header] -> m RequestId
getRequestId headers =
-- generate a request id for every request if the client has not sent it
case getRequestHeader (txtToBs requestIdHeader) headers of
case getRequestHeader requestIdHeader headers of
Nothing -> RequestId <$> liftIO generateFingerprint
Just reqId -> return $ RequestId $ bsToTxt reqId
@ -173,6 +172,12 @@ mkClientHeadersForward reqHeaders =
"User-Agent" -> Just ("X-Forwarded-User-Agent", hdrValue)
_ -> Nothing
mkSetCookieHeaders :: Wreq.Response a -> HTTP.ResponseHeaders
mkSetCookieHeaders resp =
map (headerName,) $ resp ^.. Wreq.responseHeader headerName
where
headerName = "Set-Cookie"
filterRequestHeaders :: [HTTP.Header] -> [HTTP.Header]
filterRequestHeaders =
filterHeaders $ Set.fromList commonClientHeadersIgnored

View File

@ -257,7 +257,12 @@ def evts_webhook(request):
web_server.join()
@pytest.fixture(scope='module')
def actions_webhook(hge_ctx):
def actions_fixture(hge_ctx):
pg_version = hge_ctx.pg_version
if pg_version < 100000: # version less than 10.0
pytest.skip('Actions are not supported on Postgres version < 10')
# Start actions' webhook server
webhook_httpd = ActionsWebhookServer(hge_ctx, server_address=('127.0.0.1', 5593))
web_server = threading.Thread(target=webhook_httpd.serve_forever)
web_server.start()

View File

@ -186,6 +186,10 @@ class ActionsWebhookHandler(http.server.BaseHTTPRequestHandler):
elif req_path == "/invalid-response":
self._send_response(HTTPStatus.OK, "some-string")
elif req_path == "/mirror-action":
resp, status = self.mirror_action()
self._send_response(status, resp)
else:
self.send_response(HTTPStatus.NO_CONTENT)
self.end_headers()
@ -263,6 +267,11 @@ class ActionsWebhookHandler(http.server.BaseHTTPRequestHandler):
response = resp['data']['insert_user']['returning']
return response, HTTPStatus.OK
def mirror_action(self):
response = self.req_json['input']['arg']
return response, HTTPStatus.OK
def check_email(self, email):
regex = '^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$'
return re.search(regex,email)
@ -279,6 +288,7 @@ class ActionsWebhookHandler(http.server.BaseHTTPRequestHandler):
def _send_response(self, status, body):
self.send_response(status)
self.send_header('Content-Type', 'application/json')
self.send_header('Set-Cookie', 'abcd')
self.end_headers()
self.wfile.write(json.dumps(body).encode("utf-8"))
@ -333,7 +343,7 @@ class EvtsWebhookHandler(http.server.BaseHTTPRequestHandler):
"headers": req_headers})
# A very slightly more sane/performant http server.
# See: https://stackoverflow.com/a/14089457/176841
# See: https://stackoverflow.com/a/14089457/176841
#
# TODO use this elsewhere, or better yet: use e.g. bottle + waitress
class ThreadedHTTPServer(ThreadingMixIn, http.server.HTTPServer):
@ -409,7 +419,7 @@ class HGECtx:
self.ws_client = GQLWsClient(self, '/v1/graphql')
# HGE version
result = subprocess.run(['../../scripts/get-version.sh'], shell=False, stdout=subprocess.PIPE, check=True)
env_version = os.getenv('VERSION')
self.version = env_version if env_version else result.stdout.decode('utf-8').strip()
@ -421,6 +431,11 @@ class HGECtx:
raise HGECtxError(repr(e))
assert st_code == 200, resp
# Postgres version
pg_version_text = self.sql('show server_version_num').fetchone()['server_version_num']
self.pg_version = int(pg_version_text)
def reflect_tables(self):
self.meta.reflect(bind=self.engine)

View File

@ -0,0 +1,22 @@
description: Expected field not found in response
url: /v1/graphql
status: 200
response:
errors:
- extensions:
internal:
webhook_response:
name: Alice
path: $
code: unexpected
message: field "id" expected in webhook response, but not found
query:
variables:
name: Alice
query: |
mutation ($name: String) {
mirror(arg: {name: $name}){
id
name
}
}

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