mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 09:22:43 +03:00
Merge branch 'master' into issue-4035-check-computed-field
This commit is contained in:
commit
c12dd1bae1
@ -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"
|
||||
|
@ -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
|
||||
|
27
CHANGELOG.md
27
CHANGELOG.md
@ -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)
|
@ -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",
|
||||
|
@ -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
2556
console/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
@ -42,6 +42,7 @@ const globals = {
|
||||
featuresCompatibility: window.__env.serverVersion
|
||||
? getFeaturesCompatibility(window.__env.serverVersion)
|
||||
: null,
|
||||
isProduction,
|
||||
};
|
||||
|
||||
if (globals.consoleMode === SERVER_CONSOLE_MODE) {
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -17,9 +17,6 @@ const Editor = ({ mode, ...props }) => {
|
||||
tabSize={2}
|
||||
setOptions={{
|
||||
showLineNumbers: true,
|
||||
enableBasicAutocompletion: true,
|
||||
enableSnippets: true,
|
||||
behavioursEnabled: true,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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()}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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()) {
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -40,7 +40,6 @@ export const getAllCodegenFrameworks = () => {
|
||||
};
|
||||
|
||||
export const getCodegenFunc = framework => {
|
||||
process.hrtime = () => null;
|
||||
return fetch(getCodegenFilePath(framework))
|
||||
.then(r => r.text())
|
||||
.then(rawJsString => {
|
||||
|
@ -39,6 +39,7 @@ const HandlerEditor = ({
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={forwardClientHeaders}
|
||||
readOnly
|
||||
className={`${styles.add_mar_right_small}`}
|
||||
/>
|
||||
Forward client headers to webhook
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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
|
||||
|
@ -34,6 +34,7 @@ const Relationships = ({
|
||||
typename={objectType.name}
|
||||
allTables={allTables}
|
||||
schemaList={schemaList}
|
||||
isNew
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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());
|
||||
|
@ -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';
|
||||
|
@ -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}`}>
|
||||
|
@ -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 => {
|
||||
|
@ -41,7 +41,6 @@ const Sidebar = ({ location, metadata }) => {
|
||||
title: 'Allowed Queries',
|
||||
});
|
||||
|
||||
|
||||
const adminSecret = getAdminSecret();
|
||||
|
||||
if (adminSecret && globals.consoleMode !== CLI_CONSOLE_MODE) {
|
||||
|
25
console/src/components/UIKit/atoms/AlertBox/AlertBox.js
Normal file
25
console/src/components/UIKit/atoms/AlertBox/AlertBox.js
Normal 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;
|
||||
}
|
||||
`;
|
56
console/src/components/UIKit/atoms/AlertBox/index.js
Normal file
56
console/src/components/UIKit/atoms/AlertBox/index.js
Normal 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>
|
||||
);
|
||||
};
|
45
console/src/components/UIKit/atoms/Button/Button.js
Normal file
45
console/src/components/UIKit/atoms/Button/Button.js
Normal 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}
|
||||
`;
|
62
console/src/components/UIKit/atoms/Button/index.js
Normal file
62
console/src/components/UIKit/atoms/Button/index.js
Normal 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,
|
||||
};
|
72
console/src/components/UIKit/atoms/Checkbox/Checkbox.js
Normal file
72
console/src/components/UIKit/atoms/Checkbox/Checkbox.js
Normal 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}
|
||||
`;
|
14
console/src/components/UIKit/atoms/Checkbox/index.js
Normal file
14
console/src/components/UIKit/atoms/Checkbox/index.js
Normal 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>
|
||||
);
|
||||
};
|
9
console/src/components/UIKit/atoms/Icon/Icon.js
Normal file
9
console/src/components/UIKit/atoms/Icon/Icon.js
Normal 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}
|
||||
`;
|
56
console/src/components/UIKit/atoms/Icon/index.js
Normal file
56
console/src/components/UIKit/atoms/Icon/index.js
Normal 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}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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}
|
||||
`;
|
14
console/src/components/UIKit/atoms/RadioButton/index.js
Normal file
14
console/src/components/UIKit/atoms/RadioButton/index.js
Normal 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>
|
||||
);
|
||||
};
|
20
console/src/components/UIKit/atoms/Spinner/Spinner.js
Normal file
20
console/src/components/UIKit/atoms/Spinner/Spinner.js
Normal 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}
|
||||
`;
|
64
console/src/components/UIKit/atoms/Spinner/index.js
Normal file
64
console/src/components/UIKit/atoms/Spinner/index.js
Normal 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>
|
||||
);
|
||||
};
|
@ -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 : ' ')}
|
||||
`;
|
18
console/src/components/UIKit/atoms/SwitchButton/index.js
Normal file
18
console/src/components/UIKit/atoms/SwitchButton/index.js
Normal 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>
|
||||
);
|
||||
};
|
77
console/src/components/UIKit/atoms/Tabs/Tabs.js
Normal file
77
console/src/components/UIKit/atoms/Tabs/Tabs.js
Normal 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 };
|
41
console/src/components/UIKit/atoms/Tabs/index.js
Normal file
41
console/src/components/UIKit/atoms/Tabs/index.js
Normal 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>;
|
||||
};
|
9
console/src/components/UIKit/atoms/Tooltip/Tooltip.js
Normal file
9
console/src/components/UIKit/atoms/Tooltip/Tooltip.js
Normal 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}
|
||||
`;
|
25
console/src/components/UIKit/atoms/Tooltip/index.js
Normal file
25
console/src/components/UIKit/atoms/Tooltip/index.js
Normal 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',
|
||||
};
|
28
console/src/components/UIKit/atoms/Typography/Typography.js
Normal file
28
console/src/components/UIKit/atoms/Typography/Typography.js
Normal 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}
|
||||
`;
|
76
console/src/components/UIKit/atoms/Typography/index.js
Normal file
76
console/src/components/UIKit/atoms/Typography/index.js
Normal 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',
|
||||
};
|
10
console/src/components/UIKit/atoms/index.js
Normal file
10
console/src/components/UIKit/atoms/index.js
Normal 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';
|
381
console/src/components/UIKit/demo/Components.js
Normal file
381
console/src/components/UIKit/demo/Components.js
Normal 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>
|
||||
);
|
741
console/src/components/UIKit/demo/StyleGuide.js
Normal file
741
console/src/components/UIKit/demo/StyleGuide.js
Normal 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>
|
||||
);
|
81
console/src/components/UIKit/demo/styles/index.js
Normal file
81
console/src/components/UIKit/demo/styles/index.js
Normal 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)``;
|
16
console/src/components/UIKit/index.js
Normal file
16
console/src/components/UIKit/index.js
Normal 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;
|
289
console/src/components/UIKit/theme/index.js
Normal file
289
console/src/components/UIKit/theme/index.js
Normal 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,
|
||||
};
|
@ -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 };
|
||||
|
@ -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" />
|
||||
|
@ -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({
|
||||
|
@ -196,6 +196,7 @@ module.exports = {
|
||||
NODE_ENV: JSON.stringify('production'),
|
||||
},
|
||||
CONSOLE_ASSET_VERSION: Date.now().toString(),
|
||||
'process.hrtime': () => null,
|
||||
}),
|
||||
new ForkTsCheckerWebpackPlugin({
|
||||
compilerOptions: {
|
||||
|
@ -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`
|
||||
|
@ -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.
|
||||
|
@ -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`.
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
|
@ -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 didn’t, 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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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 =
|
||||
|
@ -107,6 +107,7 @@ data SyncActionExecutionContext
|
||||
= SyncActionExecutionContext
|
||||
{ _saecName :: !ActionName
|
||||
, _saecOutputType :: !GraphQLType
|
||||
, _saecOutputFields :: !ActionOutputFields
|
||||
, _saecDefinitionList :: ![(PGCol, PGScalarType)]
|
||||
, _saecWebhook :: !ResolvedWebhook
|
||||
, _saecHeaders :: ![HeaderConf]
|
||||
|
@ -68,6 +68,7 @@ mkMutationField actionName actionInfo definitionList =
|
||||
ActionSynchronous ->
|
||||
ActionExecutionSyncWebhook $ SyncActionExecutionContext actionName
|
||||
(_adOutputType definition)
|
||||
(_aiOutputFields actionInfo)
|
||||
definitionList
|
||||
(_adHandler definition)
|
||||
(_adHeaders definition)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user