Merge branch 'master' into event-trigger-lock-timeout

This commit is contained in:
Phil Freeman 2020-09-09 10:20:28 -07:00 committed by GitHub
commit ff5786e5c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
858 changed files with 19977 additions and 13855 deletions

View File

@ -1,110 +0,0 @@
##
## This defines how our `hasura-bulldozer-mergebot` behaves on this repository.
## See: https://github.com/palantir/bulldozer
##
## Comments from the original example config are left here as documentation.
## "HASURA NOTE:" precedes comments about our choices here.
##
#
# "version" is the configuration version, currently "1".
version: 1
# "merge" defines how and when pull requests are merged. If the section is
# missing, bulldozer will consider all pull requests and use default settings.
merge:
# "trigger" defines the set of pull requests considered by bulldozer. If
# the section is missing, bulldozer considers all pull requests not excluded
# by the ignore conditions.
trigger:
# Pull requests with any of these labels (case-insensitive) are added to
# the trigger.
#
# HASURA NOTE: for now switch on only via opt-in with this label:
# CAREFUL!: if untrusted contributors can add labels, this would
# effectively give them write permissions so long as CI passed!
labels: ["auto-update-auto-merge"]
################## HASURA CAREFUL!: I think these are all dangerous (see above)
# Pull requests where the body or any comment contains any of these
# substrings are added to the trigger.
# comment_substrings: ["==MERGE_WHEN_READY=="]
# Pull requests where any comment matches one of these exact strings are
# added to the trigger.
# comments: ["Please merge this pull request!"]
# Pull requests where the body contains any of these substrings are added
# to the trigger.
# pr_body_substrings: ["==MERGE_WHEN_READY=="]
##################
# Pull requests targeting any of these branches are added to the trigger.
# branches: ["develop"]
# "ignore" defines the set of pull request ignored by bulldozer. If the
# section is missing, bulldozer considers all pull requests. It takes the
# same keys as the "trigger" section.
ignore:
# HASURA NOTE: some existing semantic tags; not sure how widely in use they are:
labels: ["s/do-not-merge", "s/wip"]
# comment_substrings: ["==DO_NOT_MERGE=="]
# "method" defines the merge method. The available options are "merge",
# "rebase", "squash", and "ff-only".
method: merge
# Allows the merge method that is used when auto-merging a PR to be different based on the
# target branch. The keys of the hash are the target branch name, and the values are the merge method that
# will be used for PRs targeting that branch. The valid values are the same as for the "method" key.
# Note: If the target branch does not match any of the specified keys, the "method" key is used instead.
branch_method:
# develop: squash
master: merge
# "options" defines additional options for the individual merge methods.
options:
# "squash" options are only used when the merge method is "squash"
# squash:
# # "title" defines how the title of the commit message is created when
# # generating a squash commit. The options are "pull_request_title",
# # "first_commit_title", and "github_default_title". The default is
# # "pull_request_title".
# title: "pull_request_title"
# # "body" defines how the body of the commit message is created when
# # generating a squash commit. The options are "pull_request_body",
# # "summarize_commits", and "empty_body". The default is "empty_body".
# body: "empty_body"
# # If "body" is "pull_request_body", then the commit message will be the
# # part of the pull request body surrounded by "message_delimiter"
# # strings. This is disabled (empty string) by default.
# message_delimiter: ==COMMIT_MSG==
# "required_statuses" is a list of additional status contexts that must pass
# before bulldozer can merge a pull request. This is useful if you want to
# require extra testing for automated merges, but not for manual merges.
# required_statuses:
# - "ci/circleci: ete-tests"
# If true, bulldozer will delete branches after their pull requests merge.
#
# HASURA NOTE: this is easily undone in the github UI:
delete_after_merge: true
# "update" defines how and when to update pull request branches. Unlike with
# merges, if this section is missing, bulldozer will not update any pull requests.
update:
# "trigger" defines the set of pull requests that should be updated by
# bulldozer. It accepts the same keys as the trigger in the "merge" block.
#
# HASURA NOTE: we definitely don't want to always update. auto-update
# (without merge) might be convenient and no harm adding it here.
trigger:
labels: ["auto-update-auto-merge", "auto-update"]
# "ignore" defines the set of pull requests that should not be updated by
# bulldozer. It accepts the same keys as the ignore in the "merge" block.
# ignore:
# labels: ["Do Not Update"]

View File

@ -1,9 +1,7 @@
# anchor refs to be used elsewhere # anchor refs to be used elsewhere
refs: refs:
constants: constants:
# TODO upload to hasura docker hub: - &server_builder_image hasura/graphql-engine-server-builder:2020-08-26
# - &server_builder_image hasura/graphql-engine-server-builder:2020-01-14
- &server_builder_image jberryman/graphql-engine-server-builder-8.10:2020-04-29
skip_job_on_ciignore: &skip_job_on_ciignore skip_job_on_ciignore: &skip_job_on_ciignore
run: run:
name: checking if job should be terminated or not name: checking if job should be terminated or not
@ -165,10 +163,10 @@ jobs:
- checkout - checkout
- restore_cache: - restore_cache:
keys: keys:
- cabal-store-v2-{{ checksum "server/cabal.project" }}-{{ checksum "server/graphql-engine.cabal" }}-{{ checksum "server/cabal.project.freeze" }} - cabal-store-v3-{{ checksum "server/cabal.project" }}-{{ checksum "server/graphql-engine.cabal" }}-{{ checksum "server/cabal.project.freeze" }}
- cabal-store-v2-{{ checksum "server/cabal.project" }}-{{ checksum "server/graphql-engine.cabal" }}- - cabal-store-v3-{{ checksum "server/cabal.project" }}-{{ checksum "server/graphql-engine.cabal" }}-
- cabal-store-v2-{{ checksum "server/cabal.project" }}- - cabal-store-v3-{{ checksum "server/cabal.project" }}-
- cabal-store-v2- - cabal-store-v3-
- run: - run:
name: Install latest postgresql client tools name: Install latest postgresql client tools
command: | command: |
@ -187,7 +185,7 @@ jobs:
make enable_coverage=true ci-build make enable_coverage=true ci-build
fi fi
- save_cache: - save_cache:
key: cabal-store-v2-{{ checksum "server/cabal.project" }}-{{ checksum "server/graphql-engine.cabal" }}-{{ checksum "server/cabal.project.freeze" }} key: cabal-store-v3-{{ checksum "server/cabal.project" }}-{{ checksum "server/graphql-engine.cabal" }}-{{ checksum "server/cabal.project.freeze" }}
paths: paths:
- ~/.cabal/packages - ~/.cabal/packages
- ~/.cabal/store - ~/.cabal/store

View File

@ -1,8 +1,6 @@
# Don't update this without updating the # Don't update this without updating the
# packager imager of graphql-engine # packager imager of graphql-engine
FROM phadej/ghc:8.10.1-stretch FROM haskell:8.10.2-stretch
# TODO https://github.com/haskell/docker-haskell/issues/17
#FROM haskell:8.10.1
ARG docker_ver="17.09.0-ce" ARG docker_ver="17.09.0-ce"
ARG postgres_ver="12" ARG postgres_ver="12"
@ -14,7 +12,10 @@ RUN apt-get -y update \
&& echo "deb http://apt.postgresql.org/pub/repos/apt/ stretch-pgdg main" > /etc/apt/sources.list.d/pgdg.list \ && echo "deb http://apt.postgresql.org/pub/repos/apt/ stretch-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
&& curl -s https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - \ && curl -s https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - \
&& apt-get -y update \ && apt-get -y update \
&& apt-get install -y g++ gcc libc6-dev libpq-dev libffi-dev libgmp-dev make xz-utils zlib1g-dev git gnupg upx netcat python3 python3-pip postgresql-client-${postgres_ver} postgresql-client-common \ && apt-get install -y \
g++ gcc git gnupg libc6-dev libffi-dev libgmp-dev libkrb5-dev \
libpq-dev libssl-dev make netcat postgresql-client-${postgres_ver} \
postgresql-client-common python3 python3-pip upx xz-utils zlib1g-dev \
&& curl -sL https://deb.nodesource.com/setup_${node_ver} | bash - \ && curl -sL https://deb.nodesource.com/setup_${node_ver} | bash - \
&& apt-get install -y nodejs \ && apt-get install -y nodejs \
&& curl -Lo /tmp/docker-${docker_ver}.tgz https://download.docker.com/linux/static/stable/x86_64/docker-${docker_ver}.tgz \ && curl -Lo /tmp/docker-${docker_ver}.tgz https://download.docker.com/linux/static/stable/x86_64/docker-${docker_ver}.tgz \

View File

@ -0,0 +1,13 @@
diff --git a/server/tests-py/validate.py b/server/tests-py/validate.py
index 3eecd52a..a18b3113 100644
--- a/server/tests-py/validate.py
+++ b/server/tests-py/validate.py
@@ -318,7 +318,7 @@ def assert_graphql_resp_expected(resp_orig, exp_response_orig, query, resp_hdrs=
# If it is a batch GraphQL query, compare each individual response separately
for (exp, out) in zip(as_list(exp_response), as_list(resp)):
matched_ = equal_CommentedMap(exp, out)
- if is_err_msg(exp):
+ if is_err_msg(exp) and is_err_msg(out):
if not matched_:
warnings.warn("Response does not have the expected error message\n" + dump_str.getvalue())
return resp, matched

View File

@ -6,7 +6,9 @@
# and sets some of the required variables that run.sh needs, # and sets some of the required variables that run.sh needs,
# before executing run.sh # before executing run.sh
set -euo pipefail set -euo pipefail
ROOT="${BASH_SOURCE[0]%/*}" cd "${BASH_SOURCE[0]%/*}"
ROOT="${PWD}"
cd - > /dev/null
SERVER_DIR="$ROOT/../../server" SERVER_DIR="$ROOT/../../server"
@ -18,8 +20,8 @@ echo "server binary: $SERVER_BINARY"
cd - cd -
set +x set +x
export SERVER_OUTPUT_DIR="server-output" export SERVER_OUTPUT_DIR="$ROOT/server-output"
export LATEST_SERVER_BINARY="./graphql-engine-latest" export LATEST_SERVER_BINARY="$ROOT/graphql-engine-latest"
# Create Python virtualenv # Create Python virtualenv
if ! [ -f ".venv/bin/activate" ] ; then if ! [ -f ".venv/bin/activate" ] ; then
@ -40,7 +42,8 @@ log_duration=on
port=$PG_PORT port=$PG_PORT
EOF EOF
) )
export HASURA_GRAPHQL_DATABASE_URL="postgres://postgres:$PGPASSWORD@127.0.0.1:$PG_PORT/postgres" # Pytest is giving out deprecated warnings when postgres:// is used
export HASURA_GRAPHQL_DATABASE_URL="postgresql://postgres:$PGPASSWORD@127.0.0.1:$PG_PORT/postgres"
DOCKER_PSQL="docker exec -u postgres -it $PG_CONTAINER_NAME psql -p $PG_PORT" DOCKER_PSQL="docker exec -u postgres -it $PG_CONTAINER_NAME psql -p $PG_PORT"

View File

@ -12,7 +12,9 @@ set -euo pipefail
# # echo an error message before exiting # # echo an error message before exiting
# trap 'echo "\"${last_command}\" command filed with exit code $?."' EXIT # trap 'echo "\"${last_command}\" command filed with exit code $?."' EXIT
ROOT="${BASH_SOURCE[0]%/*}" cd "${BASH_SOURCE[0]%/*}"
ROOT="${PWD}"
cd - > /dev/null
download_with_etag_check() { download_with_etag_check() {
URL="$1" URL="$1"
@ -119,6 +121,17 @@ trap rm_worktree ERR
make_latest_release_worktree() { make_latest_release_worktree() {
git worktree add --detach "$WORKTREE_DIR" "$RELEASE_VERSION" git worktree add --detach "$WORKTREE_DIR" "$RELEASE_VERSION"
cd "$WORKTREE_DIR"
# FIX ME: Remove the patch below after the next stable release
# The --avoid-error-message-checks in pytest was implementated as a rather relaxed check than
# what we intended to have. In versions <= v1.3.0,
# this check allows response to be success even if the expected response is a failure.
# The patch below fixes that issue.
# The `git apply` should give errors from next release onwards,
# since this change is going to be included in the next release version
git apply "${ROOT}/err_msg.patch" || \
(log "Remove the git apply in make_latest_release_worktree function" && false)
cd - > /dev/null
} }
cleanup_hasura_metadata_if_present() { cleanup_hasura_metadata_if_present() {
@ -148,7 +161,18 @@ get_server_upgrade_tests() {
cd $RELEASE_PYTEST_DIR cd $RELEASE_PYTEST_DIR
tmpfile="$(mktemp --dry-run)" tmpfile="$(mktemp --dry-run)"
set -x set -x
python3 -m pytest -q --collect-only --collect-upgrade-tests-to-file "$tmpfile" -m 'allow_server_upgrade_test and not skip_server_upgrade_test' "${args[@]}" 1>/dev/null 2>/dev/null # FIX ME: Deselecting some introspection tests from the previous test suite
# which throw errors on the latest build. Even when the output of the current build is more accurate.
# Remove these deselects after the next stable release
python3 -m pytest -q --collect-only --collect-upgrade-tests-to-file "$tmpfile" \
-m 'allow_server_upgrade_test and not skip_server_upgrade_test' \
--deselect test_schema_stitching.py::TestRemoteSchemaBasic::test_introspection \
--deselect test_schema_stitching.py::TestAddRemoteSchemaCompareRootQueryFields::test_schema_check_arg_default_values_and_field_and_arg_types \
--deselect test_graphql_mutations.py::TestGraphqlInsertPermission::test_user_with_no_backend_privilege \
--deselect test_graphql_mutations.py::TestGraphqlInsertPermission::test_backend_user_no_admin_secret_fail \
--deselect test_graphql_mutations.py::TestGraphqlMutationCustomSchema::test_update_article \
--deselect test_graphql_queries.py::TestGraphQLQueryEnums::test_introspect_user_role \
"${args[@]}" 1>/dev/null 2>/dev/null
set +x set +x
cat "$tmpfile" cat "$tmpfile"
cd - >/dev/null cd - >/dev/null
@ -174,11 +198,12 @@ run_server_upgrade_pytest() {
set -x set -x
# With --avoid-error-message-checks, we are only going to throw warnings if the error message has changed between releases # With --avoid-error-message-checks, we are only going to throw warnings if the error message has changed between releases
# FIX ME: Remove the deselect below after the next stable release
pytest --hge-urls "${HGE_URL}" --pg-urls "$HASURA_GRAPHQL_DATABASE_URL" \ pytest --hge-urls "${HGE_URL}" --pg-urls "$HASURA_GRAPHQL_DATABASE_URL" \
--avoid-error-message-checks "$@" \ --avoid-error-message-checks "$@" \
-m 'allow_server_upgrade_test and not skip_server_upgrade_test' \ -m 'allow_server_upgrade_test and not skip_server_upgrade_test' \
--deselect test_graphql_mutations.py::TestGraphqlUpdateBasic::test_numerics_inc \ --deselect test_graphql_mutations.py::TestGraphqlInsertPermission::test_user_with_no_backend_privilege \
--deselect test_graphql_mutations.py::TestGraphqlMutationCustomSchema::test_update_article \
--deselect test_graphql_queries.py::TestGraphQLQueryEnums::test_introspect_user_role \
-v $tests_to_run -v $tests_to_run
set +x set +x
cd - cd -

View File

@ -191,9 +191,7 @@ pip3 install -r requirements.txt
# node js deps # node js deps
curl -sL https://deb.nodesource.com/setup_8.x | bash - curl -sL https://deb.nodesource.com/setup_8.x | bash -
apt-get install -y nodejs apt-get install -y nodejs
npm_config_loglevel=error npm install $PYTEST_ROOT/remote_schemas/nodejs/ (cd remote_schemas/nodejs && npm_config_loglevel=error npm ci)
npm install apollo-server graphql
mkdir -p "$OUTPUT_FOLDER/hpc" mkdir -p "$OUTPUT_FOLDER/hpc"
@ -385,6 +383,51 @@ kill_hge_servers
unset HASURA_GRAPHQL_JWT_SECRET unset HASURA_GRAPHQL_JWT_SECRET
# test JWT with Claims map
echo -e "\n$(time_elapsed): <########## TEST GRAPHQL-ENGINE WITH ADMIN SECRET AND JWT (with claims_map and values are json path) #####################################>\n"
TEST_TYPE="jwt-claims-map-with-json-path-values"
export HASURA_GRAPHQL_JWT_SECRET="$(jq -n --arg key "$(cat $OUTPUT_FOLDER/ssl/jwt_public.key)" '{ type: "RS512", key: $key , claims_map: {"x-hasura-user-id": {"path":"$.['"'"'https://myapp.com/jwt/claims'"'"'].user.id"}, "x-hasura-allowed-roles": {"path":"$.['"'"'https://myapp.com/jwt/claims'"'"'].role.allowed"}, "x-hasura-default-role": {"path":"$.['"'"'https://myapp.com/jwt/claims'"'"'].role.default"}}}')"
run_hge_with_args serve
wait_for_port 8080
pytest -n 1 -vv --hge-urls "$HGE_URL" --pg-urls "$HASURA_GRAPHQL_DATABASE_URL" --hge-key="$HASURA_GRAPHQL_ADMIN_SECRET" --hge-jwt-key-file="$OUTPUT_FOLDER/ssl/jwt_private.key" --hge-jwt-conf="$HASURA_GRAPHQL_JWT_SECRET" test_jwt_claims_map.py::TestJWTClaimsMapBasic
kill_hge_servers
unset HASURA_GRAPHQL_JWT_SECRET
echo -e "\n$(time_elapsed): <########## TEST GRAPHQL-ENGINE WITH ADMIN SECRET AND JWT (with claims_map and values are json path with default values set) #####################################>\n"
TEST_TYPE="jwt-claims-map-with-json-path-values-with-default-values"
export HASURA_GRAPHQL_JWT_SECRET="$(jq -n --arg key "$(cat $OUTPUT_FOLDER/ssl/jwt_public.key)" '{ type: "RS512", key: $key , claims_map: {"x-hasura-user-id": {"path":"$.['"'"'https://myapp.com/jwt/claims'"'"'].user.id", "default":"1"}, "x-hasura-allowed-roles": {"path":"$.['"'"'https://myapp.com/jwt/claims'"'"'].role.allowed", "default":["user","editor"]}, "x-hasura-default-role": {"path":"$.['"'"'https://myapp.com/jwt/claims'"'"'].role.default","default":"user"}}}')"
run_hge_with_args serve
wait_for_port 8080
pytest -n 1 -vv --hge-urls "$HGE_URL" --pg-urls "$HASURA_GRAPHQL_DATABASE_URL" --hge-key="$HASURA_GRAPHQL_ADMIN_SECRET" --hge-jwt-key-file="$OUTPUT_FOLDER/ssl/jwt_private.key" --hge-jwt-conf="$HASURA_GRAPHQL_JWT_SECRET" test_jwt_claims_map.py::TestJWTClaimsMapBasic
kill_hge_servers
unset HASURA_GRAPHQL_JWT_SECRET
echo -e "\n$(time_elapsed): <########## TEST GRAPHQL-ENGINE WITH ADMIN SECRET AND JWT (with claims_map and values are literal values) #####################################>\n"
TEST_TYPE="jwt-claims-map-with-literal-values"
export HASURA_GRAPHQL_JWT_SECRET="$(jq -n --arg key "$(cat $OUTPUT_FOLDER/ssl/jwt_public.key)" '{ type: "RS512", key: $key , claims_map: {"x-hasura-user-id": {"path":"$.['"'"'https://myapp.com/jwt/claims'"'"'].user.id"}, "x-hasura-allowed-roles": ["user","editor"], "x-hasura-default-role": "user","x-hasura-custom-header":"custom-value"}}')"
run_hge_with_args serve
wait_for_port 8080
pytest -n 1 -vv --hge-urls "$HGE_URL" --pg-urls "$HASURA_GRAPHQL_DATABASE_URL" --hge-key="$HASURA_GRAPHQL_ADMIN_SECRET" --hge-jwt-key-file="$OUTPUT_FOLDER/ssl/jwt_private.key" --hge-jwt-conf="$HASURA_GRAPHQL_JWT_SECRET" test_jwt_claims_map.py::TestJWTClaimsMapWithStaticHasuraClaimsMapValues
kill_hge_servers
unset HASURA_GRAPHQL_JWT_SECRET
# test with CORS modes # test with CORS modes
echo -e "\n$(time_elapsed): <########## TEST GRAPHQL-ENGINE WITH CORS DOMAINS ########>\n" echo -e "\n$(time_elapsed): <########## TEST GRAPHQL-ENGINE WITH CORS DOMAINS ########>\n"

128
.kodiak.toml Normal file
View File

@ -0,0 +1,128 @@
###############################################################################
## Configuration for auto-merge / auto-update bot
##
## See: https://kodiakhq.com/
###############################################################################
# Kodiak's configuration file should be placed at `.kodiak.toml` (repository
# root) or `.github/.kodiak.toml`.
# docs: https://kodiakhq.com/docs/config-reference
# version is the only required setting in a kodiak config.
# `1` is the only valid setting for this field.
version = 1
[merge]
# Label to enable Kodiak to merge a PR.
# By default, Kodiak will only act on PRs that have this label. You can disable
# this requirement via `merge.require_automerge_label`.
automerge_label = "auto-update-auto-merge"
# Require that the automerge label (`merge.automerge_label`) be set for Kodiak
# to merge a PR.
#
# When disabled, Kodiak will immediately attempt to merge any PR that passes all
# GitHub branch protection requirements.
require_automerge_label = true
# If a PR's title matches this regex, Kodiak will not merge the PR. This is
# useful to prevent merging work-in-progress PRs.
#
# Setting `merge.blocking_title_regex = ""` disables this option.
blocking_title_regex = "" # default: "^WIP:.*", options: "" (disables regex), a regex string (e.g. ".*DONT\s*MERGE.*")
# Kodiak will not merge a PR with any of these labels.
blocking_labels = ["s/do-not-merge", "s/wip"] # default: []
# Choose merge method for Kodiak to use.
#
# Kodiak will report a configuration error if the selected merge method is
# disabled for a repository.
#
# If you're using the "Require signed commits" GitHub Branch Protection setting
# to require commit signatures, "merge" or "squash" are the only compatible options. "rebase" will cause Kodiak to raise a configuration error.
method = "merge" # default: "merge", options: "merge", "squash", "rebase"
# Once a PR is merged, delete the branch. This option behaves like the GitHub
# repository setting "Automatically delete head branches", which automatically
# deletes head branches after pull requests are merged.
delete_branch_on_merge = true # default: false
# If there is a merge conflict, make a comment on the PR and remove the
# automerge label. This option only applies when `merge.require_automerge_label`
# is enabled.
notify_on_conflict = true # default: true
# Don't wait for in-progress status checks on a PR to finish before updating the
# branch.
optimistic_updates = true # default: true
# Don't wait for specified status checks when merging a PR. If a configured
# status check is incomplete when a PR is being merged, Kodiak will skip the PR.
# Use this option for status checks that run indefinitely, like deploy jobs or
# the WIP GitHub App.
dont_wait_on_status_checks = [] # default: [], options: list of check names (e.g. ["ci/circleci: lint_api"])
# If a PR is passing all checks and is able to be merged, merge it without
# placing it in the merge queue. This option adds some unfairness where PRs
# waiting in the queue the longest are not served first.
prioritize_ready_to_merge = true # default: false
# Never merge a PR. This option can be used with `update.always` to
# automatically update a PR without merging.
do_not_merge = false # default: false
[merge.message]
# By default (`"github_default"`), GitHub uses the title of a PR's first commit
# for the merge commit title. `"pull_request_title"` uses the PR title for the
# merge commit.
title = "github_default" # default: "github_default", options: "github_default", "pull_request_title"
# By default (`"github_default"`), GitHub combines the titles of a PR's commits
# to create the body text of a merge commit. `"pull_request_body"` uses the
# content of the PR to generate the body content while `"empty"` sets an empty
# body.
body = "github_default" # default: "github_default", options: "github_default", "pull_request_body", "empty"
# Append the Pull Request URL to the merge message. Makes navigating to the PR
# from the commit easier.
include_pull_request_url = true # default: false
# Add the PR number to the merge commit title. This setting replicates GitHub's
# behavior of automatically adding the PR number to the title of merges created
# through the UI. This option only applies when `merge.message.title` does not
# equal `"github_default"`.
include_pr_number = true # default: true
# Control the text used in the merge commit. The GitHub default is markdown, but
# `"plain_text"` or `"html"` can be used to render the pull request body as text
# or HTML. This option only applies when `merge.message.body = "pull_request_body"`.
body_type = "markdown" # default: "markdown", options: "plain_text", "markdown", "html"
# Strip HTML comments (`<!-- some HTML comment -->`) from merge commit body.
# This setting is useful for stripping HTML comments created by PR templates.
# This option only applies when `merge.message.body_type = "markdown"`.
strip_html_comments = false # default: false
[update]
# Update a PR whenever out of date with the base branch. The PR will be updated
# regardless of merge requirements (e.g. failing status checks, missing reviews,
# blacklist labels).
#
# Kodiak will only update PRs with the `merge.automerge_label` label or if
# `update.require_automerge_label = false`.
#
# When enabled, _Kodiak will not be able to efficiently update PRs._ If you have
# multiple PRs against a target like `master`, any time a commit is added to
# `master` _all_ of those PRs against `master` will update. For `N` PRs against
# a target you will see at least `N(N-1)/2` updates. If this configuration
# option was disabled you would only see at least `N-1` updates.
always = false # default: false
# When enabled, Kodiak will only update PRs that have an automerge label
# (configured via `merge.automerge_label`). When disable, Kodiak will update any
# PR. This option only applies when `update.always = true`.
require_automerge_label = true # default: true

View File

@ -2,6 +2,75 @@
## Next release ## Next release
### Server - Support for mapping session variables to default JWT claims
Some auth providers do not let users add custom claims in JWT. In such cases, the server can take a JWT configuration option called `claims_map` to specify a mapping of Hasura session variables to values in existing claims via JSONPath or literal values.
Example:-
Consider the following JWT claim:
```
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iat": 1516239022,
"user": {
"id": "ujdh739kd",
"appRoles": ["user", "editor"]
}
}
```
The corresponding JWT config can be:
```
{
"type":"RS512",
"key": "<The public Key>",
"claims_map": {
"x-hasura-allowed-roles": {"path":"$.user.appRoles"},
"x-hasura-default-role": {"path":"$.user.appRoles[0]","default":"user"},
"x-hasura-user-id": {"path":"$.user.id"}
}
}
```
### Breaking changes
This release contains the [PDV refactor (#4111)](https://github.com/hasura/graphql-engine/pull/4111), a significant rewrite of the internals of the server, which did include some breaking changes:
- The semantics of explicit `null` values in `where` filters have changed according to the discussion in [issue 704](https://github.com/hasura/graphql-engine/issues/704#issuecomment-635571407): an explicit `null` value in a comparison input object will be treated as an error rather than resulting in the expression being evaluated to `True`. For instance: `delete_users(where: {id: {_eq: $userId}}) { name }` will yield an error if `$userId` is `null` instead of deleting all users.
- The validation of required headers has been fixed (closing #14 and #3659):
- if a query selects table `bar` through table `foo` via a relationship, the required permissions headers will be the union of the required headers of table `foo` and table `bar` (we used to only check the headers of the root table);
- if an insert does not have an `on_conflict` clause, it will not require the update permissions headers.
### Bug fixes and improvements
(Add entries here in the order of: server, console, cli, docs, others)
- server: some mutations that cannot be performed will no longer be in the schema (for instance, `delete_by_pk` mutations won't be shown to users that do not have select permissions on all primary keys) (#4111)
- server: miscellaneous description changes (#4111)
- server: treat the absence of `backend_only` configuration and `backend_only: false` equally (closing #5059) (#4111)
- server: allow remote relationships joining `type` column with `[type]` input argument as spec allows this coercion (fixes #5133)
- console: allow user to cascade Postgres dependencies when dropping Postgres objects (close #5109) (#5248)
- console: mark inconsistent remote schemas in the UI (close #5093) (#5181)
- cli: add missing global flags for seeds command (#5565)
- docs: add docs page on networking with docker (close #4346) (#4811)
## `v1.3.2`
### Bug fixes and improvements
(Add entries here in the order of: server, console, cli, docs, others)
- server: fixes column masking in select permission for computed fields regression (fix #5696)
## `v1.3.1`, `v1.3.1-beta.1`
### Breaking change ### Breaking change
Headers from environment variables starting with `HASURA_GRAPHQL_` are not allowed Headers from environment variables starting with `HASURA_GRAPHQL_` are not allowed
@ -24,6 +93,7 @@ If you do have such headers configured, then you must update the header configur
- console: handle nested fragments in allowed queries (close #5137) (#5252) - console: handle nested fragments in allowed queries (close #5137) (#5252)
- console: update sidebar icons for different action and trigger types (#5445) - console: update sidebar icons for different action and trigger types (#5445)
- console: make add column UX consistent with others (#5486) - console: make add column UX consistent with others (#5486)
- console: add "identity" to frequently used columns (close #4279) (#5360)
- cli: improve error messages thrown when metadata apply fails (#5513) - cli: improve error messages thrown when metadata apply fails (#5513)
- cli: fix issue with creating seed migrations while using tables with capital letters (closes #5532) (#5549) - cli: fix issue with creating seed migrations while using tables with capital letters (closes #5532) (#5549)
- build: introduce additional log kinds for cli-migrations image (#5529) - build: introduce additional log kinds for cli-migrations image (#5529)
@ -66,7 +136,6 @@ If you do have such headers configured, then you must update the header configur
## `v1.3.0-beta.3` ## `v1.3.0-beta.3`
### Bug fixes and improvements ### Bug fixes and improvements
(Add entries here in the order of: server, console, cli, docs, others) (Add entries here in the order of: server, console, cli, docs, others)

View File

@ -12,7 +12,7 @@ import (
const completionCmdExample = `# Bash const completionCmdExample = `# Bash
# Linux # Linux
# Add Bash completion file using: # Add Bash completion file using:
$ sudo hasura completion bash --file=/etc/bash.completion.d/hasura $ sudo hasura completion bash --file=/etc/bash_completion.d/hasura
# Mac # Mac
# Install bash-completion using homebrew: # Install bash-completion using homebrew:
$ brew install bash-completion $ brew install bash-completion

View File

@ -2,6 +2,7 @@ package commands
import ( import (
"github.com/hasura/graphql-engine/cli" "github.com/hasura/graphql-engine/cli"
"github.com/hasura/graphql-engine/cli/util"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
@ -28,14 +29,21 @@ func NewSeedCmd(ec *cli.ExecutionContext) *cobra.Command {
newSeedCreateCmd(ec), newSeedCreateCmd(ec),
newSeedApplyCmd(ec), newSeedApplyCmd(ec),
) )
seedCmd.PersistentFlags().String("endpoint", "", "http(s) endpoint for Hasura GraphQL Engine")
seedCmd.PersistentFlags().String("admin-secret", "", "admin secret for Hasura GraphQL Engine")
seedCmd.PersistentFlags().String("access-key", "", "access key for Hasura GraphQL Engine")
seedCmd.PersistentFlags().MarkDeprecated("access-key", "use --admin-secret instead")
v.BindPFlag("endpoint", seedCmd.PersistentFlags().Lookup("endpoint")) f := seedCmd.PersistentFlags()
v.BindPFlag("admin_secret", seedCmd.PersistentFlags().Lookup("admin-secret"))
v.BindPFlag("access_key", seedCmd.PersistentFlags().Lookup("access-key")) f.String("endpoint", "", "http(s) endpoint for Hasura GraphQL Engine")
f.String("admin-secret", "", "admin secret for Hasura GraphQL Engine")
f.String("access-key", "", "access key for Hasura GraphQL Engine")
f.MarkDeprecated("access-key", "use --admin-secret instead")
f.Bool("insecure-skip-tls-verify", false, "skip TLS verification and disable cert checking (default: false)")
f.String("certificate-authority", "", "path to a cert file for the certificate authority")
util.BindPFlag(v, "endpoint", f.Lookup("endpoint"))
util.BindPFlag(v, "admin_secret", f.Lookup("admin-secret"))
util.BindPFlag(v, "access_key", f.Lookup("access-key"))
util.BindPFlag(v, "insecure_skip_tls_verify", f.Lookup("insecure-skip-tls-verify"))
util.BindPFlag(v, "certificate_authority", f.Lookup("certificate-authority"))
return seedCmd return seedCmd
} }

View File

@ -31,7 +31,7 @@ func newSeedCreateCmd(ec *cli.ExecutionContext) *cobra.Command {
} }
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "create seed_name", Use: "create seed_name",
Short: "create a new seed file", Short: "Create a new seed file",
Example: ` # Create a new seed file and use editor to add SQL: Example: ` # Create a new seed file and use editor to add SQL:
hasura seed create new_table_seed hasura seed create new_table_seed

View File

@ -1,22 +1,19 @@
# svelte-graphql-app # svelte-graphql-app
A sample [Svelte 3](https://svelte.dev) app to demonstrate usage of GraphQL Queries, Mutations and Subscriptions with [svelte-apollo](https://github.com/timhall/svelte-apollo), Hasura GraphQL engine and Postgres as database. Forked from the standard svelte [template](https://github.com/sveltejs/template) A sample [Svelte 3](https://svelte.dev) app to demonstrate usage of GraphQL Queries, Mutations and Subscriptions with [svelte-apollo](https://github.com/timhall/svelte-apollo), Hasura Cloud and Postgres as database. Forked from the standard svelte [template](https://github.com/sveltejs/template)
[![Edit svelte-graphql](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/hasura/graphql-engine/tree/master/community/sample-apps/svelte-apollo?fontsize=14) [![Edit svelte-graphql](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/hasura/graphql-engine/tree/master/community/sample-apps/svelte-apollo?fontsize=14)
## Deploy Hasura ## Create new Hasura Cloud project
- Deploy Postgres and GraphQL Engine on Heroku: - Create new Hasura Cloud project with the `Try a free database with Heroku` option.
[![Deploy to Please check out our [docs](https://hasura.io/docs/cloud/1.0/manual/getting-started/index.html) for the detailed steps.
heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/hasura/graphql-engine-heroku)
Please checkout our [docs](https://hasura.io/docs/1.0/graphql/manual/deployment/index.html) for other deployment methods - Get the app URL (something like `https://<my-project-name>.hasura.app`)
- Get the Heroku app URL (say `my-app.herokuapp.com`)
- Create `author` table: - Create `author` table:
Open Hasura console: visit https://my-app.herokuapp.com on a browser Open your Hasura Cloud project's console: visit `https://<my-project-name>.hasura.app` on a browser
Navigate to `Data` section in the top nav bar and create a table as follows: Navigate to `Data` section in the top nav bar and create a table as follows:
![Create author table](../gatsby-postgres-graphql/assets/add_table.jpg) ![Create author table](../gatsby-postgres-graphql/assets/add_table.jpg)
@ -72,7 +69,7 @@ npm install
}); });
``` ```
Replace the `uri` argument with your Hasura GraphQL Endpoint for both `wsLink` and `httpLink` Replace the `uri` argument with your Hasura GraphQL Endpoint (something like `https://<my-project-name>.hasura.app/v1/graphql`) for both `wsLink` and `httpLink`
Start [Rollup](https://rollupjs.org): Start [Rollup](https://rollupjs.org):
@ -99,4 +96,3 @@ now
``` ```
This will deploy the app on Now 2.0 Platform and you have the Svetle app running live :) This will deploy the app on Now 2.0 Platform and you have the Svetle app running live :)

View File

@ -214,12 +214,20 @@ export const passModifyPkey = () => {
cy.get(getElementFromAlias('modify-table-edit-pks')).click(); cy.get(getElementFromAlias('modify-table-edit-pks')).click();
cy.get(getElementFromAlias('primary-key-select-1')).select('1'); cy.get(getElementFromAlias('primary-key-select-1')).select('1');
cy.get(getElementFromAlias('modify-table-pks-save')).click(); cy.get(getElementFromAlias('modify-table-pks-save')).click();
cy.get(getElementFromAlias('pk-config-text')).within(() => {
cy.get('b').contains(getColName(0));
cy.get('b').contains('id');
});
cy.wait(5000); cy.wait(5000);
// TODO
// test disappearance expect
// (cy.get(getElementFromAlias('modify-table-column-1-remove'))).not.to.exist;
cy.get(getElementFromAlias('remove-pk-column-1')).click(); cy.get(getElementFromAlias('remove-pk-column-1')).click();
cy.get(getElementFromAlias('modify-table-pks-save')).click(); cy.get(getElementFromAlias('modify-table-pks-save')).click();
cy.get(getElementFromAlias('pk-config-text')).within(() => {
cy.get('b').contains('id');
});
cy.get(getElementFromAlias('pk-config-text')).within(() => {
cy.get('b').should('not.contain', getColName(0));
});
cy.get(getElementFromAlias('modify-table-close-pks')).click(); cy.get(getElementFromAlias('modify-table-close-pks')).click();
cy.wait(3000); cy.wait(3000);
}; };

View File

@ -3627,22 +3627,67 @@
} }
}, },
"@typescript-eslint/eslint-plugin": { "@typescript-eslint/eslint-plugin": {
"version": "2.24.0", "version": "3.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.24.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.8.0.tgz",
"integrity": "sha512-wJRBeaMeT7RLQ27UQkDFOu25MqFOBus8PtOa9KaT5ZuxC1kAsd7JEHqWt4YXuY9eancX0GK9C68i5OROnlIzBA==", "integrity": "sha512-lFb4VCDleFSR+eo4Ew+HvrJ37ZH1Y9ZyE+qyP7EiwBpcCVxwmUc5PAqhShCQ8N8U5vqYydm74nss+a0wrrCErw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@typescript-eslint/experimental-utils": "2.24.0", "@typescript-eslint/experimental-utils": "3.8.0",
"eslint-utils": "^1.4.3", "debug": "^4.1.1",
"functional-red-black-tree": "^1.0.1", "functional-red-black-tree": "^1.0.1",
"regexpp": "^3.0.0", "regexpp": "^3.0.0",
"semver": "^7.3.2",
"tsutils": "^3.17.1" "tsutils": "^3.17.1"
}, },
"dependencies": { "dependencies": {
"@typescript-eslint/experimental-utils": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.8.0.tgz",
"integrity": "sha512-o8T1blo1lAJE0QDsW7nSyvZHbiDzQDjINJKyB44Z3sSL39qBy5L10ScI/XwDtaiunoyKGLiY9bzRk4YjsUZl8w==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.3",
"@typescript-eslint/types": "3.8.0",
"@typescript-eslint/typescript-estree": "3.8.0",
"eslint-scope": "^5.0.0",
"eslint-utils": "^2.0.0"
}
},
"@typescript-eslint/typescript-estree": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.8.0.tgz",
"integrity": "sha512-MTv9nPDhlKfclwnplRNDL44mP2SY96YmPGxmMbMy6x12I+pERcxpIUht7DXZaj4mOKKtet53wYYXU0ABaiXrLw==",
"dev": true,
"requires": {
"@typescript-eslint/types": "3.8.0",
"@typescript-eslint/visitor-keys": "3.8.0",
"debug": "^4.1.1",
"glob": "^7.1.6",
"is-glob": "^4.0.1",
"lodash": "^4.17.15",
"semver": "^7.3.2",
"tsutils": "^3.17.1"
}
},
"eslint-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz",
"integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==",
"dev": true,
"requires": {
"eslint-visitor-keys": "^1.1.0"
}
},
"regexpp": { "regexpp": {
"version": "3.0.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.0.0.tgz", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz",
"integrity": "sha512-Z+hNr7RAVWxznLPuA7DIh8UNX1j9CDrUQxskw9IrBE1Dxue2lyXT+shqEIeLUjrokxIP8CMy1WkjgG3rTsd5/g==", "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==",
"dev": true
},
"semver": {
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
"integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==",
"dev": true "dev": true
} }
} }
@ -3682,6 +3727,12 @@
"eslint-visitor-keys": "^1.1.0" "eslint-visitor-keys": "^1.1.0"
} }
}, },
"@typescript-eslint/types": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-3.8.0.tgz",
"integrity": "sha512-8kROmEQkv6ss9kdQ44vCN1dTrgu4Qxrd2kXr10kz2NP5T8/7JnEfYNxCpPkArbLIhhkGLZV3aVMplH1RXQRF7Q==",
"dev": true
},
"@typescript-eslint/typescript-estree": { "@typescript-eslint/typescript-estree": {
"version": "2.24.0", "version": "2.24.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.24.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.24.0.tgz",
@ -3705,6 +3756,15 @@
} }
} }
}, },
"@typescript-eslint/visitor-keys": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.8.0.tgz",
"integrity": "sha512-gfqQWyVPpT9NpLREXNR820AYwgz+Kr1GuF3nf1wxpHD6hdxI62tq03ToomFnDxY0m3pUB39IF7sil7D5TQexLA==",
"dev": true,
"requires": {
"eslint-visitor-keys": "^1.1.0"
}
},
"@webassemblyjs/ast": { "@webassemblyjs/ast": {
"version": "1.8.5", "version": "1.8.5",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz",
@ -7678,6 +7738,16 @@
"integrity": "sha1-NfDVy8ZMLj7WLpO0saevBbp+1Nc=", "integrity": "sha1-NfDVy8ZMLj7WLpO0saevBbp+1Nc=",
"dev": true "dev": true
}, },
"eslint-scope": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz",
"integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==",
"dev": true,
"requires": {
"esrecurse": "^4.1.0",
"estraverse": "^4.1.1"
}
},
"eslint-utils": { "eslint-utils": {
"version": "1.4.3", "version": "1.4.3",
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz",

View File

@ -19,7 +19,6 @@
"build": "webpack --progress -p --colors --display-error-details --config webpack/prod.config.js", "build": "webpack --progress -p --colors --display-error-details --config webpack/prod.config.js",
"server-build": "make server-build", "server-build": "make server-build",
"build-unused": "webpack --verbose --colors --display-error-details --config webpack/prod.config.js --json | webpack-unused -s src", "build-unused": "webpack --verbose --colors --display-error-details --config webpack/prod.config.js --json | webpack-unused -s src",
"postinstall": "webpack --display-error-details --config webpack/prod.config.js",
"cypress": "cypress open", "cypress": "cypress open",
"test": "cypress run --spec 'cypress/integration/**/**/test.ts' --key $CYPRESS_KEY --parallel --record", "test": "cypress run --spec 'cypress/integration/**/**/test.ts' --key $CYPRESS_KEY --parallel --record",
"lint": "eslint -c .eslintrc src --ext .js,.ts,.tsx", "lint": "eslint -c .eslintrc src --ext .js,.ts,.tsx",
@ -147,7 +146,7 @@
"@types/webpack-dev-middleware": "3.7.0", "@types/webpack-dev-middleware": "3.7.0",
"@types/webpack-hot-middleware": "2.25.0", "@types/webpack-hot-middleware": "2.25.0",
"@types/ws": "7.2.3", "@types/ws": "7.2.3",
"@typescript-eslint/eslint-plugin": "2.24.0", "@typescript-eslint/eslint-plugin": "3.8.0",
"@typescript-eslint/parser": "2.24.0", "@typescript-eslint/parser": "2.24.0",
"autoprefixer": "9.7.6", "autoprefixer": "9.7.6",
"babel-eslint": "10.1.0", "babel-eslint": "10.1.0",

View File

@ -28,7 +28,7 @@ export const requireAsyncGlobals = ({ dispatch }) => {
return (nextState, finalState, callback) => { return (nextState, finalState, callback) => {
Promise.all([ Promise.all([
dispatch(loadConsoleOpts()), dispatch(loadConsoleOpts()),
dispatch(fetchServerConfig()), dispatch(fetchServerConfig),
]).finally(callback); ]).finally(callback);
}; };
}; };

View File

@ -11,7 +11,7 @@ type DropDownButtonProps = {
}[]; }[];
dataKey: string; dataKey: string;
dataIndex?: string; dataIndex?: string;
onButtonChange: (e: React.MouseEvent<{}>) => void; onButtonChange: (e: React.MouseEvent<unknown>) => void;
onInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void; onInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
value?: string; value?: string;
inputVal: string; inputVal: string;

View File

@ -1,5 +1,5 @@
@import "~bootstrap-sass/assets/stylesheets/bootstrap/variables"; @import '~bootstrap-sass/assets/stylesheets/bootstrap/variables';
@import "../../Common.scss"; @import '../../Common.scss';
.container { .container {
} }
@ -28,8 +28,8 @@
// background: #444; // background: #444;
// color: $navbar-inverse-color; // color: $navbar-inverse-color;
color: #333; color: #333;
border: 1px solid #E5E5E5; border: 1px solid #e5e5e5;
background-color: #F8F8F8; background-color: #f8f8f8;
/* /*
a,a:visited { a,a:visited {
@ -66,7 +66,7 @@
*/ */
a { a {
color: #767E93; color: #767e93;
word-wrap: break-word; word-wrap: break-word;
} }
} }
@ -75,7 +75,7 @@
padding: 7px 0; padding: 7px 0;
// color: $navbar-inverse-link-hover-color; // color: $navbar-inverse-link-hover-color;
transition: color 0.5s; transition: color 0.5s;
pointer: cursor; cursor: pointer;
} }
} }
} }
@ -113,7 +113,7 @@
.sidebarHeading { .sidebarHeading {
font-weight: bold; font-weight: bold;
display: inline-block; display: inline-block;
color: #767E93; color: #767e93;
font-size: 15px; font-size: 15px;
} }
} }
@ -148,19 +148,23 @@
padding-left: 5px !important; padding-left: 5px !important;
display: initial !important; display: initial !important;
.tableIcon, .functionIcon { .tableIcon,
//display: inline; .functionIcon {
margin-right: 5px; margin-right: 5px;
font-size: 12px; font-size: 12px;
width: 12px; width: 12px;
} }
.icon_mar_left {
margin-left: 5px;
}
} }
} }
.noChildren { .noChildren {
font-weight: 400 !important; font-weight: 400 !important;
padding-bottom: 10px !important; padding-bottom: 10px !important;
color: #767E93 !important; color: #767e93 !important;
} }
li:first-child { li:first-child {
@ -170,11 +174,16 @@
.activeLink { .activeLink {
a { a {
// border-left: 4px solid #FFC627; color: #fd9540 !important;
color: #FD9540!important;
} }
} }
.padLeft4 {
margin-left: 8px;
top: 12px;
font-size: 14px;
}
.floatRight { .floatRight {
float: right; float: right;
margin-right: 20px; margin-right: 20px;

View File

@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
import { Connect } from 'react-redux';
import styles from './RightContainer.scss'; import styles from './RightContainer.scss';
const RightContainer: React.FC = ({ children }) => { const RightContainer: React.FC = ({ children }) => {
@ -16,6 +15,4 @@ const RightContainer: React.FC = ({ children }) => {
); );
}; };
const rightContainerConnector = (connect: Connect) => connect()(RightContainer); export default RightContainer;
export default rightContainerConnector;

View File

@ -0,0 +1,3 @@
import RightContainer from './RightContainer';
export { RightContainer };

View File

@ -1,3 +0,0 @@
import rightContainerConnector from './RightContainer/RightContainer';
export { rightContainerConnector };

View File

@ -91,7 +91,7 @@ const SearchableSelect: React.FC<SearchableSelectProps> = ({
const customStyles: Record<string, any> = {}; const customStyles: Record<string, any> = {};
if (styleOverrides) { if (styleOverrides) {
Object.keys(styleOverrides).forEach(comp => { Object.keys(styleOverrides).forEach(comp => {
customStyles[comp] = (provided: object) => ({ customStyles[comp] = (provided: Record<string, unknown>) => ({
...provided, ...provided,
...styleOverrides[comp], ...styleOverrides[comp],
}); });

View File

@ -11,3 +11,6 @@
width: 18px; width: 18px;
height: 18px; height: 18px;
} }
.react-toggle--focus .react-toggle-thumb {
box-shadow: none;
}

View File

@ -1,4 +1,4 @@
export const UNSAFE_keys = <T extends object>(source: T) => export const UNSAFE_keys = <T extends Record<string, unknown>>(source: T) =>
Object.keys(source) as Array<keyof T>; Object.keys(source) as Array<keyof T>;
export type Json = export type Json =

View File

@ -11,9 +11,9 @@ import RuntimeError from './RuntimeError';
import { registerRunTimeError } from '../Main/Actions'; import { registerRunTimeError } from '../Main/Actions';
export interface Metadata { export interface Metadata {
inconsistentObjects: object[]; inconsistentObjects: Record<string, unknown>[];
ongoingRequest: boolean; ongoingRequest: boolean;
allowedQueries: object[]; allowedQueries: Record<string, unknown>[];
} }
export interface ErrorBoundaryProps { export interface ErrorBoundaryProps {

View File

@ -3,17 +3,26 @@ import { UPDATE_DATA_HEADERS } from '../Services/Data/DataActions';
import { saveAdminSecretState } from '../AppState'; import { saveAdminSecretState } from '../AppState';
import { ADMIN_SECRET_HEADER_KEY, CLI_CONSOLE_MODE } from '../../constants'; import { ADMIN_SECRET_HEADER_KEY, CLI_CONSOLE_MODE } from '../../constants';
import requestAction from '../../utils/requestAction'; import requestAction from '../../utils/requestAction';
import { Dispatch } from '../../types';
import globals from '../../Globals'; import globals from '../../Globals';
type VerifyLoginOptions = {
adminSecret: string;
shouldPersist: boolean;
successCallback: () => void;
errorCallback: (err: Error) => void;
dispatch: Dispatch;
};
export const verifyLogin = ({ export const verifyLogin = ({
adminSecret, adminSecret,
shouldPersist, shouldPersist,
successCallback, successCallback,
errorCallback, errorCallback,
dispatch, dispatch,
}) => { }: VerifyLoginOptions) => {
const url = Endpoints.getSchema; const url = Endpoints.getSchema;
const requestOptions = { const requestOptions: RequestInit = {
credentials: globalCookiePolicy, credentials: globalCookiePolicy,
method: 'POST', method: 'POST',
headers: { headers: {

View File

@ -4,6 +4,7 @@ import requestAction from '../../utils/requestAction';
import requestActionPlain from '../../utils/requestActionPlain'; import requestActionPlain from '../../utils/requestActionPlain';
import Endpoints, { globalCookiePolicy } from '../../Endpoints'; import Endpoints, { globalCookiePolicy } from '../../Endpoints';
import { getFeaturesCompatibility } from '../../helpers/versionUtils'; import { getFeaturesCompatibility } from '../../helpers/versionUtils';
import { getRunSqlQuery } from '../Common/utils/v1QueryUtils';
const SET_MIGRATION_STATUS_SUCCESS = 'Main/SET_MIGRATION_STATUS_SUCCESS'; const SET_MIGRATION_STATUS_SUCCESS = 'Main/SET_MIGRATION_STATUS_SUCCESS';
const SET_MIGRATION_STATUS_ERROR = 'Main/SET_MIGRATION_STATUS_ERROR'; const SET_MIGRATION_STATUS_ERROR = 'Main/SET_MIGRATION_STATUS_ERROR';
@ -22,6 +23,8 @@ const EXPORT_METADATA_ERROR = 'Main/EXPORT_METADATA_ERROR';
const UPDATE_ADMIN_SECRET_INPUT = 'Main/UPDATE_ADMIN_SECRET_INPUT'; const UPDATE_ADMIN_SECRET_INPUT = 'Main/UPDATE_ADMIN_SECRET_INPUT';
const LOGIN_IN_PROGRESS = 'Main/LOGIN_IN_PROGRESS'; const LOGIN_IN_PROGRESS = 'Main/LOGIN_IN_PROGRESS';
const LOGIN_ERROR = 'Main/LOGIN_ERROR'; const LOGIN_ERROR = 'Main/LOGIN_ERROR';
const POSTGRES_VERSION_SUCCESS = 'Main/POSTGRES_VERSION_SUCCESS';
const POSTGRES_VERSION_ERROR = 'Main/POSTGRES_VERSION_ERROR';
const RUN_TIME_ERROR = 'Main/RUN_TIME_ERROR'; const RUN_TIME_ERROR = 'Main/RUN_TIME_ERROR';
const registerRunTimeError = data => ({ const registerRunTimeError = data => ({
@ -52,6 +55,29 @@ const setReadOnlyMode = data => ({
data, data,
}); });
export const fetchPostgresVersion = (dispatch, getState) => {
const req = getRunSqlQuery('SELECT version()');
const options = {
method: 'POST',
credentials: globalCookiePolicy,
body: JSON.stringify(req),
headers: getState().tables.dataHeaders,
};
return dispatch(requestAction(Endpoints.query, options)).then(
({ result }) => {
if (result.length > 1 && result[1].length) {
const matchRes = result[1][0].match(/[0-9]{1,}(\.[0-9]{1,})?/);
if (matchRes.length) {
dispatch({ type: POSTGRES_VERSION_SUCCESS, payload: matchRes[0] });
return;
}
}
dispatch({ type: POSTGRES_VERSION_ERROR });
}
);
};
const featureCompatibilityInit = () => { const featureCompatibilityInit = () => {
return (dispatch, getState) => { return (dispatch, getState) => {
const { serverVersion } = getState().main; const { serverVersion } = getState().main;
@ -110,7 +136,7 @@ const loadServerVersion = () => dispatch => {
); );
}; };
const fetchServerConfig = () => (dispatch, getState) => { const fetchServerConfig = (dispatch, getState) => {
const url = Endpoints.serverConfig; const url = Endpoints.serverConfig;
const options = { const options = {
method: 'GET', method: 'GET',
@ -318,6 +344,16 @@ const mainReducer = (state = defaultState, action) => {
...state, ...state,
featuresCompatibility: { ...action.data }, featuresCompatibility: { ...action.data },
}; };
case POSTGRES_VERSION_SUCCESS:
return {
...state,
postgresVersion: action.payload,
};
case POSTGRES_VERSION_ERROR:
return {
...state,
postgresVersion: null,
};
default: default:
return state; return state;
} }

View File

@ -7,7 +7,6 @@ import Tooltip from 'react-bootstrap/lib/Tooltip';
import * as tooltips from './Tooltips'; import * as tooltips from './Tooltips';
import globals from '../../Globals'; import globals from '../../Globals';
import { getPathRoot } from '../Common/utils/urlUtils'; import { getPathRoot } from '../Common/utils/urlUtils';
import Spinner from '../Common/Spinner/Spinner'; import Spinner from '../Common/Spinner/Spinner';
import WarningSymbol from '../Common/WarningSymbol/WarningSymbol'; import WarningSymbol from '../Common/WarningSymbol/WarningSymbol';
import logo from './images/white-logo.svg'; import logo from './images/white-logo.svg';
@ -21,6 +20,7 @@ import {
loadLatestServerVersion, loadLatestServerVersion,
featureCompatibilityInit, featureCompatibilityInit,
emitProClickedEvent, emitProClickedEvent,
fetchPostgresVersion,
} from './Actions'; } from './Actions';
import { import {
@ -45,7 +45,7 @@ import {
import ToolTip from '../Common/Tooltip/Tooltip'; import ToolTip from '../Common/Tooltip/Tooltip';
import { setPreReleaseNotificationOptOutInDB } from '../../telemetry/Actions'; import { setPreReleaseNotificationOptOutInDB } from '../../telemetry/Actions';
import { Icon } from '../UIKit/atoms/Icon'; import { Icon } from '../UIKit/atoms/Icon';
import { ProPopup } from './components/ProPopup'; import { Help, ProPopup } from './components/';
class Main extends React.Component { class Main extends React.Component {
constructor(props) { constructor(props) {
@ -82,7 +82,9 @@ class Main extends React.Component {
}); });
}); });
dispatch(fetchServerConfig()); dispatch(fetchPostgresVersion);
dispatch(fetchServerConfig);
} }
toggleProPopup = () => { toggleProPopup = () => {
@ -655,14 +657,7 @@ class Main extends React.Component {
{getSettingsSelectedMarker()} {getSettingsSelectedMarker()}
</div> </div>
</Link> </Link>
<a <Help isSelected={currentActiveBlock === 'support'} />
id="help"
href="https://hasura.io/help"
target="_blank"
rel="noopener noreferrer"
>
<div className={styles.headerRightNavbarBtn}>HELP</div>
</a>
{getLoveSection()} {getLoveSection()}
</div> </div>
</div> </div>

View File

@ -897,18 +897,15 @@
.headerRightNavbarBtn { .headerRightNavbarBtn {
display: flex; display: flex;
align-items: center; align-items: center;
// background-color: #22283b;
padding: 15px; padding: 15px;
position: relative; position: relative;
color: #fff;
cursor: pointer; cursor: pointer;
color: #fff;
&:hover { &:hover {
span {
opacity: 0.7; opacity: 0.7;
font-weight: 600; font-weight: 600;
} }
}
.selected { .selected {
bottom: 0; bottom: 0;

View File

@ -25,7 +25,8 @@ export interface MainState {
error: Error | null; error: Error | null;
isFetching: boolean; isFetching: boolean;
}; };
featuresCompatibility: object; featuresCompatibility: Record<string, unknown>;
postgresVersion: string | null;
} }
const defaultState: MainState = { const defaultState: MainState = {
@ -56,6 +57,7 @@ const defaultState: MainState = {
isFetching: false, isFetching: false,
}, },
featuresCompatibility: {}, featuresCompatibility: {},
postgresVersion: null,
}; };
export default defaultState; export default defaultState;

View File

@ -0,0 +1,20 @@
import React from 'react';
import { Link } from 'react-router';
import styles from '../Main.scss';
export const Help = ({ isSelected }: { isSelected: boolean }) => {
return (
<Link to="/support/forums/">
<div className={styles.headerRightNavbarBtn}>
HELP
{isSelected ? (
<span
className={styles.selected}
style={{ width: '90%', marginLeft: '5%' }}
/>
) : null}
</div>
</Link>
);
};

View File

@ -0,0 +1,4 @@
import { Help } from './Help';
import { ProPopup } from './ProPopup';
export { Help, ProPopup };

View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" id="baseline-arrow_forward-24px" width="16" height="16" viewBox="0 0 16 16">
<defs>
<style>
.cls-1{fill:none}.cls-2{fill:#303030}
</style>
</defs>
<path id="Path_290" d="M0 0h16v16H0z" class="cls-1" data-name="Path 290"/>
<path id="Path_291" d="M9.333 4l-.94.94 3.72 3.727H4V10h8.113l-3.72 3.727.94.94 5.333-5.333z" class="cls-2" data-name="Path 291" transform="translate(-1.333 -1.333)"/>
</svg>

After

Width:  |  Height:  |  Size: 481 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" id="prefix__streamline-icon-interface-alert-information-circle_16x16" width="12.5" height="12.5" data-name="streamline-icon-interface-alert-information-circle@16x16" viewBox="0 0 12.5 12.5">
<path id="prefix__Path_5128" d="M6.25 0a6.25 6.25 0 1 0 6.25 6.25A6.25 6.25 0 0 0 6.25 0zm.781 9.375a.781.781 0 0 1-1.562 0v-2.5a.781.781 0 0 1 1.563 0zM6.25 5A1.25 1.25 0 1 1 7.5 3.75 1.25 1.25 0 0 1 6.25 5z" data-name="Path 5128" style="fill:#292822"/>
</svg>

After

Width:  |  Height:  |  Size: 497 B

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { Route, IndexRedirect } from 'react-router'; import { Route, IndexRedirect } from 'react-router';
import { rightContainerConnector } from '../../Common/Layout'; import { RightContainer } from '../../Common/Layout/RightContainer';
import Container from './Containers/Main'; import Container from './Containers/Main';
import { fetchActions } from './ServerIO'; import { fetchActions } from './ServerIO';
import globals from '../../../Globals'; import globals from '../../../Globals';
@ -36,7 +36,7 @@ const getActionsRouter = (connect, store, composeOnEnterHooks) => {
onChange={actionsInit(store)} onChange={actionsInit(store)}
> >
<IndexRedirect to="manage" /> <IndexRedirect to="manage" />
<Route path="manage" component={rightContainerConnector(connect)}> <Route path="manage" component={RightContainer}>
<IndexRedirect to="actions" /> <IndexRedirect to="actions" />
<Route path="actions" component={ActionsLandingPage(connect)} /> <Route path="actions" component={ActionsLandingPage(connect)} />
<Route path="add" component={AddAction(connect)} /> <Route path="add" component={AddAction(connect)} />
@ -51,7 +51,7 @@ const getActionsRouter = (connect, store, composeOnEnterHooks) => {
component={ActionPermissions(connect)} component={ActionPermissions(connect)}
/> />
</Route> </Route>
<Route path="types" component={rightContainerConnector(connect)}> <Route path="types" component={RightContainer}>
<IndexRedirect to="manage" /> <IndexRedirect to="manage" />
<Route path="manage" component={TypesManage(connect)} /> <Route path="manage" component={TypesManage(connect)} />
<Route path="relationships" component={TypesRelationships(connect)} /> <Route path="relationships" component={TypesRelationships(connect)} />

View File

@ -272,6 +272,8 @@ const analyzeFetcher = (headers, mode) => {
const changeRequestHeader = (index, key, newValue, isDisabled) => { const changeRequestHeader = (index, key, newValue, isDisabled) => {
return (dispatch, getState) => { return (dispatch, getState) => {
websocketSubscriptionClient = null;
const currentState = getState().apiexplorer; const currentState = getState().apiexplorer;
const updatedHeader = { const updatedHeader = {

View File

@ -11,14 +11,14 @@ export type OperationData = {
displayName: string; displayName: string;
type: OperationTypeNode; type: OperationTypeNode;
variableName: string; variableName: string;
variables: object; variables: Record<string, unknown>;
operationDefinition: OperationDefinitionNode; operationDefinition: OperationDefinitionNode;
}; };
export type GenerateOptions = { export type GenerateOptions = {
serverUrl: string; serverUrl: string;
headers: { [name: string]: string }; headers: { [name: string]: string };
context: object; context: Record<string, unknown>;
operationDataList: Array<OperationData>; operationDataList: Array<OperationData>;
options: OptionValues; options: OptionValues;
}; };

View File

@ -110,7 +110,8 @@ export const getErrorMessage = (
notificationMessage = error.code; notificationMessage = error.code;
} }
} else if ('internal' in error && 'error' in error.internal) { } else if ('internal' in error && 'error' in error.internal) {
notificationMessage = `${error.code} : ${error.internal.error.message}`; notificationMessage = `${error.internal.error.message}.
${error.internal.error.description}`;
} else if ('custom' in error) { } else if ('custom' in error) {
notificationMessage = error.custom; notificationMessage = error.custom;
} else if ('code' in error && 'error' in error && 'path' in error) { } else if ('code' in error && 'error' in error && 'path' in error) {

View File

@ -443,6 +443,7 @@ class AddTable extends Component {
columnDefaultFunctions, columnDefaultFunctions,
columnTypeCasts, columnTypeCasts,
checkConstraints, checkConstraints,
postgresVersion,
} = this.props; } = this.props;
const getCreateBtnText = () => { const getCreateBtnText = () => {
@ -493,6 +494,7 @@ class AddTable extends Component {
onSelect={setFreqUsedColumn} onSelect={setFreqUsedColumn}
action={'add'} action={'add'}
dispatch={dispatch} dispatch={dispatch}
postgresVersion={postgresVersion}
/> />
</div> </div>
<hr /> <hr />
@ -586,6 +588,7 @@ const mapStateToProps = state => ({
columnTypeCasts: state.tables.columnTypeCasts, columnTypeCasts: state.tables.columnTypeCasts,
columnDataTypeFetchErr: state.tables.columnDataTypeFetchErr, columnDataTypeFetchErr: state.tables.columnDataTypeFetchErr,
schemaList: state.tables.schemaList, schemaList: state.tables.schemaList,
postgresVersion: state.main.postgresVersion,
}); });
const addTableConnector = connect => connect(mapStateToProps)(AddTable); const addTableConnector = connect => connect(mapStateToProps)(AddTable);

View File

@ -1,8 +1,27 @@
import React from 'react'; import React from 'react';
import Dropdown from '../../../../Common/Dropdown/Dropdown'; import Dropdown from '../../../../Common/Dropdown/Dropdown';
import Button from '../../../../Common/Button/Button'; import Button from '../../../../Common/Button/Button';
import { Dispatch } from '../../../../../types';
import { Column } from '../../../../../utils/postgresColumnTypes';
const frequentlyUsedColumns = [ type ColumnAction = 'add' | 'modify';
interface FrequentlyUsedColumn {
name: string;
validFor: ColumnAction[];
type: Column | string;
typeText: string;
primary?: boolean;
default?: string;
defaultText?: string;
dependentSQLGenerator?: (
schemaName: string,
tableName: string,
columnName: string
) => { upSql: string; downSql: string };
minPGVersion?: number;
}
const frequentlyUsedColumns: FrequentlyUsedColumn[] = [
{ {
name: 'id', name: 'id',
validFor: ['add'], validFor: ['add'],
@ -17,6 +36,14 @@ const frequentlyUsedColumns = [
typeText: 'bigint (auto-increment)', typeText: 'bigint (auto-increment)',
primary: true, primary: true,
}, },
{
name: 'id',
validFor: ['add'],
type: 'int GENERATED BY DEFAULT AS IDENTITY',
typeText: 'int (identity, generated by default)',
primary: true,
minPGVersion: 10,
},
{ {
name: 'id', name: 'id',
validFor: ['add'], validFor: ['add'],
@ -69,10 +96,10 @@ IS 'trigger to set value of column "${columnName}" to current timestamp on row u
}, },
]; ];
const getFreqUsedColDisplayInfo = c => { const getFreqUsedColDisplayInfo = (c: FrequentlyUsedColumn) => {
const title = c.name; const title = c.name;
const typeText = c.typeText + '; '; const typeText = `${c.typeText}; `;
const defaultText = const defaultText =
c.defaultText || c.default c.defaultText || c.default
? `default: ${c.defaultText || c.default}; ` ? `default: ${c.defaultText || c.default}; `
@ -87,14 +114,26 @@ const getFreqUsedColDisplayInfo = c => {
}; };
}; };
interface FrequentlyUsedColumnSelectorProps {
onSelect: any;
action: ColumnAction | null;
dispatch: Dispatch | null;
postgresVersion: string | null;
}
const FrequentlyUsedColumnSelector = ({ const FrequentlyUsedColumnSelector = ({
onSelect, onSelect,
action = null, action = null,
dispatch = null, dispatch = null,
}) => { postgresVersion,
const frequentlyUsedColumnsOptions = () => { }: FrequentlyUsedColumnSelectorProps) => {
return frequentlyUsedColumns const frequentlyUsedColumnsOptions = frequentlyUsedColumns
.filter(fuc => !action || fuc.validFor.includes(action)) .filter(fuc => !action || fuc.validFor.includes(action))
.filter(col =>
postgresVersion && col.minPGVersion
? parseFloat(postgresVersion) >= col.minPGVersion
: true
)
.map(fuc => { .map(fuc => {
const { title, subTitle } = getFreqUsedColDisplayInfo(fuc); const { title, subTitle } = getFreqUsedColDisplayInfo(fuc);
return { return {
@ -109,15 +148,14 @@ const FrequentlyUsedColumnSelector = ({
onClick: () => (dispatch ? dispatch(onSelect(fuc)) : onSelect(fuc)), onClick: () => (dispatch ? dispatch(onSelect(fuc)) : onSelect(fuc)),
}; };
}); });
};
return ( return (
<Dropdown <Dropdown
testId={'frequently-used-columns'} testId="frequently-used-columns"
options={frequentlyUsedColumnsOptions()} options={frequentlyUsedColumnsOptions}
position="bottom" position="bottom"
key={'frequently-used-columns'} key="frequently-used-columns"
keyPrefix={'frequently-used-columns'} keyPrefix="frequently-used-columns"
> >
<Button color="white" size="xs"> <Button color="white" size="xs">
+ Frequently used columns + Frequently used columns

View File

@ -1,11 +1,10 @@
import React from 'react'; import React from 'react';
import { ThunkDispatch } from 'redux-thunk';
import { AnyAction } from 'redux';
import ReloadMetadata from '../../../Settings/MetadataOptions/ReloadMetadata'; import ReloadMetadata from '../../../Settings/MetadataOptions/ReloadMetadata';
import { Dispatch } from '../../../../../types';
export interface ReloadEnumValuesButtonProps { export interface ReloadEnumValuesButtonProps {
isEnum: boolean; isEnum: boolean;
dispatch: ThunkDispatch<{}, {}, AnyAction>; dispatch: Dispatch;
tooltipStyle?: string; tooltipStyle?: string;
} }

View File

@ -56,9 +56,9 @@ export interface TableRowProps {
refName: 'valueNode' | 'nullNode' | 'defaultNode' | 'insertRadioNode', refName: 'valueNode' | 'nullNode' | 'defaultNode' | 'insertRadioNode',
node: HTMLInputElement | null node: HTMLInputElement | null
) => void; ) => void;
enumOptions: object; enumOptions: Record<string, unknown>;
index: number; index: number;
clone?: object; clone?: Record<string, unknown>;
onChange?: (e: React.ChangeEvent<HTMLInputElement>, val: unknown) => void; onChange?: (e: React.ChangeEvent<HTMLInputElement>, val: unknown) => void;
onFocus?: (e: React.FocusEvent<HTMLInputElement>) => void; onFocus?: (e: React.FocusEvent<HTMLInputElement>) => void;
prevValue?: unknown; prevValue?: unknown;

View File

@ -659,7 +659,10 @@ const makeMigrationCall = (
} }
customOnSuccess(data, globals.consoleMode, currMigrationMode); customOnSuccess(data, globals.consoleMode, currMigrationMode);
}; };
const retryMigration = (err = {}, errMsg = '') => { const retryMigration = (err = {}, errMsg = '', isPgCascade = false) => {
const errorDetails = getErrorMessage('', err);
const errorDetailsLines = errorDetails.split('\n');
dispatch( dispatch(
showNotification( showNotification(
{ {
@ -667,8 +670,9 @@ const makeMigrationCall = (
level: 'error', level: 'error',
message: ( message: (
<p> <p>
{getErrorMessage('', err)} {errorDetailsLines.map((m, i) => (
<br /> <div key={i}>{m}</div>
))}
<br /> <br />
Do you want to drop the dependent items as well? Do you want to drop the dependent items as well?
</p> </p>
@ -680,7 +684,7 @@ const makeMigrationCall = (
makeMigrationCall( makeMigrationCall(
dispatch, dispatch,
getState, getState,
cascadeUpQueries(upQueries), // cascaded new up queries cascadeUpQueries(upQueries, isPgCascade), // cascaded new up queries
downQueries, downQueries,
migrationName, migrationName,
customOnSuccess, customOnSuccess,
@ -689,6 +693,7 @@ const makeMigrationCall = (
successMsg, successMsg,
errorMsg, errorMsg,
shouldSkipSchemaReload, shouldSkipSchemaReload,
false,
true // prevent further retry true // prevent further retry
), ),
}, },
@ -700,8 +705,10 @@ const makeMigrationCall = (
const onError = err => { const onError = err => {
if (!isRetry) { if (!isRetry) {
const dependecyError = getDependencyError(err); const { dependencyError, pgDependencyError } = getDependencyError(err);
if (dependecyError) return retryMigration(dependecyError, errorMsg); if (dependencyError) return retryMigration(dependencyError, errorMsg);
if (pgDependencyError)
return retryMigration(pgDependencyError, errorMsg, true);
} }
dispatch(handleMigrationErrors(errorMsg, err)); dispatch(handleMigrationErrors(errorMsg, err));

View File

@ -27,7 +27,7 @@ import {
// metadataConnector, // metadataConnector,
} from '.'; } from '.';
import { rightContainerConnector } from '../../Common/Layout'; import { RightContainer } from '../../Common/Layout/RightContainer';
import { import {
fetchDataInit, fetchDataInit,
@ -51,7 +51,7 @@ const makeDataRouter = (
return ( return (
<Route path="data" component={dataPageConnector(connect)}> <Route path="data" component={dataPageConnector(connect)}>
<IndexRedirect to="schema/public" /> <IndexRedirect to="schema/public" />
<Route path="schema" component={rightContainerConnector(connect)}> <Route path="schema" component={RightContainer}>
<IndexRedirect to="public" /> <IndexRedirect to="public" />
<Route path=":schema" component={schemaConnector(connect)} /> <Route path=":schema" component={schemaConnector(connect)} />
<Route path=":schema/tables" component={schemaConnector(connect)} /> <Route path=":schema/tables" component={schemaConnector(connect)} />

View File

@ -42,6 +42,7 @@ const executeSQL = (isMigration, migrationName, statementTimeout) => (
const { isTableTrackChecked, isCascadeChecked, sql } = getState().rawSQL; const { isTableTrackChecked, isCascadeChecked, sql } = getState().rawSQL;
const { migrationMode, readOnlyMode } = getState().main; const { migrationMode, readOnlyMode } = getState().main;
const isStatementTimeout = statementTimeout && !isMigration;
const migrateUrl = returnMigrateUrl(migrationMode); const migrateUrl = returnMigrateUrl(migrationMode);
@ -49,7 +50,7 @@ const executeSQL = (isMigration, migrationName, statementTimeout) => (
const schemaChangesUp = []; const schemaChangesUp = [];
if (statementTimeout && !isMigration) { if (isStatementTimeout) {
schemaChangesUp.push( schemaChangesUp.push(
getRunSqlQuery( getRunSqlQuery(
getStatementTimeoutSql(statementTimeout), getStatementTimeoutSql(statementTimeout),
@ -111,7 +112,10 @@ const executeSQL = (isMigration, migrationName, statementTimeout) => (
} }
dispatch(showSuccessNotification('SQL executed!')); dispatch(showSuccessNotification('SQL executed!'));
dispatch(fetchDataInit()).then(() => { dispatch(fetchDataInit()).then(() => {
dispatch({ type: REQUEST_SUCCESS, data }); dispatch({
type: REQUEST_SUCCESS,
data: data && (isStatementTimeout ? data[1] : data[0]),
});
}); });
dispatch(fetchTrackedFunctions()); dispatch(fetchTrackedFunctions());
}, },
@ -168,11 +172,7 @@ const rawSQLReducer = (state = defaultState, action) => {
lastSuccess: null, lastSuccess: null,
}; };
case REQUEST_SUCCESS: case REQUEST_SUCCESS:
if ( if (action.data && action.data.result_type === 'CommandOk') {
action.data &&
action.data[0] &&
action.data[0].result_type === 'CommandOk'
) {
return { return {
...state, ...state,
ongoingRequest: false, ongoingRequest: false,
@ -188,8 +188,8 @@ const rawSQLReducer = (state = defaultState, action) => {
lastError: null, lastError: null,
lastSuccess: true, lastSuccess: true,
resultType: 'tuples', resultType: 'tuples',
result: action.data[0].result.slice(1), result: action.data.result.slice(1),
resultHeaders: action.data[0].result[0], resultHeaders: action.data.result[0],
}; };
case REQUEST_ERROR: case REQUEST_ERROR:
return { return {

View File

@ -692,7 +692,7 @@ class Schema extends Component {
className={styles.add_mar_top} className={styles.add_mar_top}
key={'non-trackable-custom-functions'} key={'non-trackable-custom-functions'}
> >
<CollapsibleToggle title={heading} isOpen> <CollapsibleToggle title={heading}>
<div className={`${styles.padd_left_remove} col-xs-12`}> <div className={`${styles.padd_left_remove} col-xs-12`}>
{getNonTrackableFuncList()} {getNonTrackableFuncList()}
</div> </div>

View File

@ -126,6 +126,7 @@ const ColumnCreator = ({
dataTypes: restTypes = [], dataTypes: restTypes = [],
validTypeCasts, validTypeCasts,
columnDefaultFunctions, columnDefaultFunctions,
postgresVersion,
}) => { }) => {
const { const {
colName, colName,
@ -256,6 +257,7 @@ const ColumnCreator = ({
<FrequentlyUsedColumnSelector <FrequentlyUsedColumnSelector
onSelect={frequentlyUsedColumn.onSelect} onSelect={frequentlyUsedColumn.onSelect}
action={'modify'} action={'modify'}
postgresVersion={postgresVersion}
/> />
); );
}; };

View File

@ -89,6 +89,7 @@ class ModifyTable extends React.Component {
schemaList, schemaList,
tableEnum, tableEnum,
rootFieldsEdit, rootFieldsEdit,
postgresVersion,
} = this.props; } = this.props;
const dataTypeIndexMap = getAllDataTypeMap(dataTypes); const dataTypeIndexMap = getAllDataTypeMap(dataTypes);
@ -249,6 +250,7 @@ class ModifyTable extends React.Component {
dataTypes={dataTypes} dataTypes={dataTypes}
validTypeCasts={validTypeCasts} validTypeCasts={validTypeCasts}
columnDefaultFunctions={columnDefaultFunctions} columnDefaultFunctions={columnDefaultFunctions}
postgresVersion={postgresVersion}
/> />
<hr /> <hr />
{getComputedFieldsSection()} {getComputedFieldsSection()}
@ -352,6 +354,7 @@ const mapStateToProps = (state, ownProps) => ({
validTypeCasts: state.tables.columnTypeCasts, validTypeCasts: state.tables.columnTypeCasts,
columnDataTypeFetchErr: state.tables.columnDataTypeFetchErr, columnDataTypeFetchErr: state.tables.columnDataTypeFetchErr,
schemaList: state.tables.schemaList, schemaList: state.tables.schemaList,
postgresVersion: state.main.postgresVersion,
...state.tables.modify, ...state.tables.modify,
}); });

View File

@ -55,7 +55,9 @@ const PrimaryKeyEditor = ({
); );
// label next to the button when the editor is expanded // label next to the button when the editor is expanded
const pkEditorExpandedLabel = () => <div>{pkConfigText}</div>; const pkEditorExpandedLabel = () => (
<div data-test="pk-config-text">{pkConfigText}</div>
);
// expanded editor content // expanded editor content
const pkEditorExpanded = () => ( const pkEditorExpanded = () => (
@ -95,7 +97,9 @@ const PrimaryKeyEditor = ({
); );
}; };
useEffect(setPkEditState, [columns]); useEffect(() => {
setPkEditState();
}, [columns.length]);
// remove // remove
const onRemove = () => { const onRemove = () => {

View File

@ -5,7 +5,7 @@ import RawSqlButton from '../Common/Components/RawSqlButton';
export interface ViewDefinitionsProps { export interface ViewDefinitionsProps {
dispatch: () => void; dispatch: () => void;
sql: string | object; sql: string | Record<string, unknown>;
} }
const ViewDefinitions: React.FC<ViewDefinitionsProps> = ({ dispatch, sql }) => ( const ViewDefinitions: React.FC<ViewDefinitionsProps> = ({ dispatch, sql }) => (

View File

@ -1766,7 +1766,7 @@ class Permissions extends Component {
'Backend only', 'Backend only',
tooltip, tooltip,
backendStatus, backendStatus,
'https://docs.hasura.io/1.0/graphql/manual/auth/authorization/permission-rules.html#backend-only-inserts' 'https://hasura.io/docs/1.0/graphql/manual/auth/authorization/permission-rules.html#backend-only'
)} )}
useDefaultTitleStyle useDefaultTitleStyle
testId={'toggle-backend-only'} testId={'toggle-backend-only'}

View File

@ -112,11 +112,11 @@ class RelationshipEditor extends React.Component {
<div className={styles.display_flex}> <div className={styles.display_flex}>
{getEditBtn()} {getEditBtn()}
<b className={styles.textNoNewLine}>{relName}</b> <b className={styles.textNoNewLine}>{relName}</b>
</div>
<GqlCompatibilityWarning <GqlCompatibilityWarning
identifier={relName} identifier={relName}
className={styles.add_mar_left_small} className={styles.add_mar_left_small}
/> />
</div>
<div className={tableStyles.relationshipTopPadding}> <div className={tableStyles.relationshipTopPadding}>
<p className={styles.textNoNewLine}>{getRelDef(relConfig)}</p> <p className={styles.textNoNewLine}>{getRelDef(relConfig)}</p>
</div> </div>

View File

@ -791,13 +791,23 @@ WHERE
export const isColTypeString = colType => export const isColTypeString = colType =>
['text', 'varchar', 'char', 'bpchar', 'name'].includes(colType); ['text', 'varchar', 'char', 'bpchar', 'name'].includes(colType);
export const cascadeUpQueries = (upQueries = []) => const cascadePGSqlQuery = sql => {
if (sql[sql.length - 1] === ';')
return sql.substr(0, sql.length - 1) + ' CASCADE;';
// SQL might have a " at the end
else if (sql[sql.length - 2] === ';')
return sql.substr(0, sql.length - 2) + ' CASCADE;';
return sql + ' CASCADE;';
};
export const cascadeUpQueries = (upQueries = [], isPgCascade = false) =>
upQueries.map((i = {}) => { upQueries.map((i = {}) => {
if (i.type === 'run_sql' || i.type === 'untrack_table') { if (i.type === 'run_sql' || i.type === 'untrack_table') {
return { return {
...i, ...i,
args: { args: {
...i.args, ...i.args,
...(isPgCascade && { sql: cascadePGSqlQuery(i.args.sql) }),
cascade: true, cascade: true,
}, },
}; };
@ -808,14 +818,27 @@ export const cascadeUpQueries = (upQueries = []) =>
export const getDependencyError = (err = {}) => { export const getDependencyError = (err = {}) => {
if (err.code == ERROR_CODES.dependencyError.code) { if (err.code == ERROR_CODES.dependencyError.code) {
// direct dependency error // direct dependency error
return err; return { dependencyError: err };
} else if (err.code == ERROR_CODES.dataApiError.code) {
// message is coming as error, further parssing willbe based on message key
const actualError = isJsonString(err.message)
? JSON.parse(err.message)
: {};
if (actualError.code == ERROR_CODES.dependencyError.code) {
return { ...actualError, message: actualError.error };
} }
if (err.code == ERROR_CODES.dataApiError.code) {
// with CLI mode, error is getting as a string with the key `message`
err = isJsonString(err.message) ? JSON.parse(err.message) : {};
} }
if (err.code == ERROR_CODES.dependencyError.code)
return {
dependencyError: { ...err, message: err.error },
};
if (
err.code === ERROR_CODES.postgresError.code &&
err?.internal?.error?.status_code === '2BP01' // pg dependent error > https://www.postgresql.org/docs/current/errcodes-appendix.html
)
return {
pgDependencyError: {
...err,
message: `${err?.internal?.error?.message}:\n
${err?.internal?.error?.description || ''}`,
},
};
return {};
}; };

View File

@ -111,6 +111,7 @@ const RedeliverEvent: React.FC<Props> = ({ dispatch, eventId }) => {
resizable resizable
manual manual
showPagination={false} showPagination={false}
freezeWhenExpanded
SubComponent={(logRow: any) => { SubComponent={(logRow: any) => {
const finalIndex = logRow.index; const finalIndex = logRow.index;
const finalRow = logs[finalIndex]; const finalRow = logs[finalIndex];

View File

@ -119,7 +119,7 @@ type ExternalProps = RouteComponentProps<
{ {
triggerName: string; triggerName: string;
}, },
{} unknown
>; >;
const mapStateToProps: MapStateToProps<PropsFromState, ExternalProps> = ( const mapStateToProps: MapStateToProps<PropsFromState, ExternalProps> = (

View File

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import { Connect } from 'react-redux'; import { Connect } from 'react-redux';
import { Route, IndexRedirect, EnterHook, RouterState } from 'react-router'; import { Route, IndexRedirect, EnterHook, RouterState } from 'react-router';
import rightContainerConnector from '../../Common/Layout/RightContainer/RightContainer';
import Container from './Container'; import Container from './Container';
import { fetchTriggers } from './ServerIO'; import { fetchTriggers } from './ServerIO';
import globals from '../../../Globals'; import globals from '../../../Globals';
@ -52,6 +51,7 @@ import {
AdhocEventLogs, AdhocEventLogs,
AdhocEventsInfo, AdhocEventsInfo,
} from './AdhocEvents'; } from './AdhocEvents';
import { RightContainer } from '../../Common/Layout/RightContainer';
const triggersInit = (dispatch: Dispatch): EnterHook => { const triggersInit = (dispatch: Dispatch): EnterHook => {
return ( return (
@ -87,10 +87,7 @@ const getTriggersRouter = (
onEnter={composeOnEnterHooks([triggersInit(store.dispatch)])} onEnter={composeOnEnterHooks([triggersInit(store.dispatch)])}
> >
<IndexRedirect to={dataEventsPrefix} /> <IndexRedirect to={dataEventsPrefix} />
<Route <Route path={dataEventsPrefix} component={RightContainer}>
path={dataEventsPrefix}
component={rightContainerConnector(connect)}
>
<IndexRedirect to={getDataEventsLandingRoute('relative')} /> <IndexRedirect to={getDataEventsLandingRoute('relative')} />
<Route path={getAddETRoute('relative')} component={AddEventTrigger} /> <Route path={getAddETRoute('relative')} component={AddEventTrigger} />
<Route <Route
@ -114,10 +111,7 @@ const getTriggersRouter = (
component={EventTriggerLanding} component={EventTriggerLanding}
/> />
</Route> </Route>
<Route <Route path={scheduledEventsPrefix} component={RightContainer}>
path={scheduledEventsPrefix}
component={rightContainerConnector(connect)}
>
<IndexRedirect to={getScheduledEventsLandingRoute('relative')} /> <IndexRedirect to={getScheduledEventsLandingRoute('relative')} />
<Route <Route
path={getAddSTRoute('relative')} path={getAddSTRoute('relative')}
@ -144,10 +138,7 @@ const getTriggersRouter = (
component={ScheduledTriggeModify} component={ScheduledTriggeModify}
/> />
</Route> </Route>
<Route <Route path={adhocEventsPrefix} component={RightContainer}>
path={adhocEventsPrefix}
component={rightContainerConnector(connect)}
>
<IndexRedirect to={getAdhocEventsInfoRoute('relative')} /> <IndexRedirect to={getAdhocEventsInfoRoute('relative')} />
<Route <Route
path={getAddAdhocEventRoute('relative')} path={getAddAdhocEventRoute('relative')}

View File

@ -26,7 +26,7 @@ export type RouterTriggerProps = RouteComponentProps<
{ {
triggerName: string; triggerName: string;
}, },
{} unknown
>; >;
export type TriggerKind = 'event' | 'cron'; export type TriggerKind = 'event' | 'cron';

View File

@ -1,7 +1,4 @@
/* */
import { listState } from './state'; import { listState } from './state';
/* */
import Endpoints, { globalCookiePolicy } from '../../../Endpoints'; import Endpoints, { globalCookiePolicy } from '../../../Endpoints';
import requestAction from '../../../utils/requestAction'; import requestAction from '../../../utils/requestAction';
import dataHeaders from '../Data/Common/Headers'; import dataHeaders from '../Data/Common/Headers';
@ -10,9 +7,7 @@ import returnMigrateUrl from '../Data/Common/getMigrateUrl';
import { CLI_CONSOLE_MODE, SERVER_CONSOLE_MODE } from '../../../constants'; import { CLI_CONSOLE_MODE, SERVER_CONSOLE_MODE } from '../../../constants';
import { loadMigrationStatus } from '../../Main/Actions'; import { loadMigrationStatus } from '../../Main/Actions';
import { handleMigrationErrors } from '../../../utils/migration'; import { handleMigrationErrors } from '../../../utils/migration';
import { showSuccessNotification } from '../Common/Notification'; import { showSuccessNotification } from '../Common/Notification';
import { filterInconsistentMetadataObjects } from '../Settings/utils';
/* Action constants */ /* Action constants */
@ -51,20 +46,9 @@ const fetchRemoteSchemas = () => {
dispatch({ type: FETCH_REMOTE_SCHEMAS }); dispatch({ type: FETCH_REMOTE_SCHEMAS });
return dispatch(requestAction(url, options)).then( return dispatch(requestAction(url, options)).then(
data => { data => {
let consistentRemoteSchemas = data;
const { inconsistentObjects } = getState().metadata;
if (inconsistentObjects.length > 0) {
consistentRemoteSchemas = filterInconsistentMetadataObjects(
data,
inconsistentObjects,
'remote_schemas'
);
}
dispatch({ dispatch({
type: REMOTE_SCHEMAS_FETCH_SUCCESS, type: REMOTE_SCHEMAS_FETCH_SUCCESS,
data: consistentRemoteSchemas, data,
}); });
return Promise.resolve(); return Promise.resolve();
}, },

View File

@ -318,7 +318,6 @@ const modifyRemoteSchema = () => {
return (dispatch, getState) => { return (dispatch, getState) => {
const currState = getState().remoteSchemas.addData; const currState = getState().remoteSchemas.addData;
const remoteSchemaName = currState.name.trim().replace(/ +/g, ''); const remoteSchemaName = currState.name.trim().replace(/ +/g, '');
// const url = Endpoints.getSchema;
const upQueryArgs = []; const upQueryArgs = [];
const downQueryArgs = []; const downQueryArgs = [];
const migrationName = 'update_remote_schema_' + remoteSchemaName; const migrationName = 'update_remote_schema_' + remoteSchemaName;
@ -345,9 +344,10 @@ const modifyRemoteSchema = () => {
}, },
}; };
resolveObj.definition.headers = [ resolveObj.definition.headers = getReqHeader(
...getReqHeader(getState().remoteSchemas.headerData.headers), getState().remoteSchemas.headerData.headers
]; );
if (resolveObj.definition.url) { if (resolveObj.definition.url) {
delete resolveObj.definition.url_from_env; delete resolveObj.definition.url_from_env;
} else { } else {

View File

@ -22,6 +22,7 @@ import { NotFoundError } from '../../../Error/PageNotFound';
import globals from '../../../../Globals'; import globals from '../../../../Globals';
import { getConfirmation } from '../../../Common/utils/jsUtils'; import { getConfirmation } from '../../../Common/utils/jsUtils';
import styles from '../RemoteSchema.scss';
const prefixUrl = globals.urlPrefix + appPrefix; const prefixUrl = globals.urlPrefix + appPrefix;
@ -49,17 +50,17 @@ class Edit extends React.Component {
]); ]);
} }
UNSAFE_componentWillReceiveProps(nextProps) { componentDidUpdate(prevProps) {
if ( if (
nextProps.params.remoteSchemaName !== this.props.params.remoteSchemaName prevProps.params.remoteSchemaName !== this.props.params.remoteSchemaName
) { ) {
Promise.all([ Promise.all([
this.props.dispatch( this.props.dispatch(
fetchRemoteSchema(nextProps.params.remoteSchemaName) fetchRemoteSchema(this.props.params.remoteSchemaName)
), ),
this.props.dispatch({ this.props.dispatch({
type: VIEW_REMOTE_SCHEMA, type: VIEW_REMOTE_SCHEMA,
data: nextProps.params.remoteSchemaName, data: this.props.params.remoteSchemaName,
}), }),
]); ]);
} }
@ -120,11 +121,23 @@ class Edit extends React.Component {
throw new NotFoundError(); throw new NotFoundError();
} }
const styles = require('../RemoteSchema.scss'); const {
isFetching,
const { isFetching, isRequesting, editState } = this.props; isRequesting,
editState,
inconsistentObjects,
} = this.props;
const { remoteSchemaName } = this.props.params; const { remoteSchemaName } = this.props.params;
const inconsistencyDetails = inconsistentObjects.find(
inconObj =>
inconObj.type === 'remote_schema' &&
inconObj?.definition?.name === remoteSchemaName
);
const fixInconsistencyMsg =
'This remote schema is in an inconsistent state. Please fix inconsistencies and reload metadata first';
const generateMigrateBtns = () => { const generateMigrateBtns = () => {
return 'isModify' in editState && !editState.isModify ? ( return 'isModify' in editState && !editState.isModify ? (
<div className={styles.commonBtn}> <div className={styles.commonBtn}>
@ -137,7 +150,8 @@ class Edit extends React.Component {
this.modifyClick(); this.modifyClick();
}} }}
data-test={'remote-schema-edit-modify-btn'} data-test={'remote-schema-edit-modify-btn'}
disabled={isRequesting} disabled={isRequesting || inconsistencyDetails}
title={inconsistencyDetails ? fixInconsistencyMsg : ''}
> >
Modify Modify
</Button> </Button>
@ -148,7 +162,8 @@ class Edit extends React.Component {
e.preventDefault(); e.preventDefault();
this.handleDeleteRemoteSchema(e); this.handleDeleteRemoteSchema(e);
}} }}
disabled={isRequesting} disabled={isRequesting || inconsistencyDetails}
title={inconsistencyDetails ? fixInconsistencyMsg : ''}
data-test={'remote-schema-edit-delete-btn'} data-test={'remote-schema-edit-delete-btn'}
> >
{isRequesting ? 'Deleting ...' : 'Delete'} {isRequesting ? 'Deleting ...' : 'Delete'}
@ -254,6 +269,7 @@ const mapStateToProps = state => {
...state.remoteSchemas.headerData, ...state.remoteSchemas.headerData,
allRemoteSchemas: state.remoteSchemas.listData.remoteSchemas, allRemoteSchemas: state.remoteSchemas.listData.remoteSchemas,
dataHeaders: { ...state.tables.dataHeaders }, dataHeaders: { ...state.tables.dataHeaders },
inconsistentObjects: state.metadata.inconsistentObjects,
}; };
}; };

View File

@ -1,28 +1,56 @@
import React from 'react'; import React from 'react';
import CommonTabLayout from '../../../Common/Layout/CommonTabLayout/CommonTabLayout'; import CommonTabLayout from '../../../Common/Layout/CommonTabLayout/CommonTabLayout';
import tabInfo from './tabInfo'; import tabInfo from './tabInfo';
import Tooltip from 'react-bootstrap/lib/Tooltip';
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
import { push } from 'react-router-redux'; import { push } from 'react-router-redux';
import { import {
fetchRemoteSchema, fetchRemoteSchema,
RESET, RESET,
getHeaderEvents, getHeaderEvents,
} from '../Add/addRemoteSchemaReducer'; } from '../Add/addRemoteSchemaReducer';
import { VIEW_REMOTE_SCHEMA } from '../Actions'; import { VIEW_REMOTE_SCHEMA } from '../Actions';
import ReloadRemoteSchema from '../../Settings/MetadataOptions/ReloadRemoteSchema'; import ReloadRemoteSchema from '../../Settings/MetadataOptions/ReloadRemoteSchema';
import { appPrefix } from '../constants'; import { appPrefix } from '../constants';
import { NotFoundError } from '../../../Error/PageNotFound';
import globals from '../../../../Globals'; import globals from '../../../../Globals';
import styles from '../RemoteSchema.scss';
import ToolTip from '../../../Common/Tooltip/Tooltip';
import WarningSymbol from '../../../Common/WarningSymbol/WarningSymbol';
const prefixUrl = globals.urlPrefix + appPrefix; const prefixUrl = globals.urlPrefix + appPrefix;
const RSHeadersDisplay = ({ data }) =>
data.length > 0 ? (
<tr>
<td>Headers</td>
<td>
{data &&
data
.filter(header => !!header.name)
.map((header, index) => [
<tr key={header}>
<td>
{`${header.name}: `}
{header.type === 'static'
? header.value
: '<' + header.value + '>'}
</td>
</tr>,
index !== data.length - 1 ? <hr /> : null,
])}
</td>
</tr>
) : null;
const RSReloadSchema = ({ readOnlyMode, remoteSchemaName, ...props }) =>
!readOnlyMode && remoteSchemaName && remoteSchemaName.length > 0 ? (
<div className={`${styles.commonBtn} ${styles.detailsRefreshButton}`}>
<ReloadRemoteSchema {...props} remoteSchemaName={remoteSchemaName} />
<ToolTip
placement="right"
message="If your remote schema has changed, you need to refresh the GraphQL Engine metadata to query the modified schema"
/>
</div>
) : null;
class ViewStitchedSchema extends React.Component { class ViewStitchedSchema extends React.Component {
componentDidMount() { componentDidMount() {
const { remoteSchemaName } = this.props.params; const { remoteSchemaName } = this.props.params;
@ -35,17 +63,17 @@ class ViewStitchedSchema extends React.Component {
]); ]);
} }
UNSAFE_componentWillReceiveProps(nextProps) { componentDidUpdate(prevProps) {
if ( if (
nextProps.params.remoteSchemaName !== this.props.params.remoteSchemaName prevProps.params.remoteSchemaName !== this.props.params.remoteSchemaName
) { ) {
Promise.all([ Promise.all([
this.props.dispatch( this.props.dispatch(
fetchRemoteSchema(nextProps.params.remoteSchemaName) fetchRemoteSchema(this.props.params.remoteSchemaName)
), ),
this.props.dispatch({ this.props.dispatch({
type: VIEW_REMOTE_SCHEMA, type: VIEW_REMOTE_SCHEMA,
data: nextProps.params.remoteSchemaName, data: this.props.params.remoteSchemaName,
}), }),
]); ]);
} }
@ -69,19 +97,14 @@ class ViewStitchedSchema extends React.Component {
} }
render() { render() {
const currentRemoteSchema = this.props.allRemoteSchemas.find(
r => r.name === this.props.params.remoteSchemaName
);
if (!currentRemoteSchema) {
// throw a 404 exception
throw new NotFoundError();
}
const styles = require('../RemoteSchema.scss');
const { remoteSchemaName } = this.props.params; const { remoteSchemaName } = this.props.params;
const { manualUrl, envName, headers, readOnlyMode } = this.props; const {
manualUrl,
envName,
headers,
readOnlyMode,
inconsistentObjects,
} = this.props;
const filterHeaders = headers.filter(h => !!h.name); const filterHeaders = headers.filter(h => !!h.name);
@ -92,21 +115,14 @@ class ViewStitchedSchema extends React.Component {
}, },
{ {
title: 'Manage', title: 'Manage',
url: appPrefix + '/' + 'manage', url: `${appPrefix}/manage`,
}, },
]; ];
if (remoteSchemaName) { if (remoteSchemaName) {
breadCrumbs.push({ breadCrumbs.push({
title: remoteSchemaName.trim(), title: remoteSchemaName.trim(),
url: url: `${appPrefix}/manage/${remoteSchemaName.trim()}/details`,
appPrefix +
'/' +
'manage' +
'/' +
remoteSchemaName.trim() +
'/' +
'details',
}); });
breadCrumbs.push({ breadCrumbs.push({
title: 'details', title: 'details',
@ -114,33 +130,18 @@ class ViewStitchedSchema extends React.Component {
}); });
} }
const refresh = ( let tabInfoCopy = tabInfo;
<Tooltip id="tooltip-cascade">
If your remote schema has changed, you need to refresh the GraphQL
Engine metadata to query the modified schema
</Tooltip>
);
if (readOnlyMode) { if (readOnlyMode) {
delete tabInfo.modify; const { modify, ...rest } = tabInfoCopy;
tabInfoCopy = rest;
} }
const showReloadRemoteSchema = const inconsistencyDetails = inconsistentObjects.find(
!readOnlyMode && remoteSchemaName && remoteSchemaName.length > 0 ? ( inconObj =>
<div className={styles.commonBtn + ' ' + styles.detailsRefreshButton}> inconObj.type === 'remote_schema' &&
<span> inconObj?.definition?.name === remoteSchemaName
<ReloadRemoteSchema );
{...this.props}
remoteSchemaName={remoteSchemaName}
/>
</span>
<span>
<OverlayTrigger placement="right" overlay={refresh}>
<i className="fa fa-question-circle" aria-hidden="true" />
</OverlayTrigger>
</span>
</div>
) : null;
return ( return (
<div <div
@ -150,7 +151,7 @@ class ViewStitchedSchema extends React.Component {
appPrefix={appPrefix} appPrefix={appPrefix}
currentTab="details" currentTab="details"
heading={remoteSchemaName} heading={remoteSchemaName}
tabsInfo={tabInfo} tabsInfo={tabInfoCopy}
breadCrumbs={breadCrumbs} breadCrumbs={breadCrumbs}
baseUrl={`${appPrefix}/manage/${remoteSchemaName}`} baseUrl={`${appPrefix}/manage/${remoteSchemaName}`}
/> />
@ -164,37 +165,35 @@ class ViewStitchedSchema extends React.Component {
<td>GraphQL Server URL</td> <td>GraphQL Server URL</td>
<td>{manualUrl || `<${envName}>`}</td> <td>{manualUrl || `<${envName}>`}</td>
</tr> </tr>
{filterHeaders.length > 0 ? ( <RSHeadersDisplay data={filterHeaders} />
<tr>
<td>Headers</td>
<td>
{filterHeaders &&
filterHeaders
.filter(k => !!k.name)
.map((h, i) => [
<tr key={i}>
<td>
{h.name} :{' '}
{h.type === 'static'
? h.value
: '<' + h.value + '>'}
</td>
</tr>,
i !== filterHeaders.length - 1 ? <hr /> : null,
])}
</td>
</tr>
) : null}
{/*
<tr>
<td>Webhook</td>
<td>in-use/bypassed</td>
</tr>
*/}
</tbody> </tbody>
</table> </table>
</div> </div>
{showReloadRemoteSchema} {inconsistencyDetails && (
<div className={styles.add_mar_bottom}>
<div className={styles.subheading_text}>
<WarningSymbol tooltipText={'Inconsistent schema'} />
<span className={styles.add_mar_left_mid}>
This remote schema is in an inconsistent state.
</span>
</div>
<div>
<b>Reason:</b> {inconsistencyDetails.reason}
</div>
<div>
<i>
(Please resolve the inconsistencies and reload the remote
schema. Fields from this remote schema are currently not
exposed over the GraphQL API)
</i>
</div>
</div>
)}
<RSReloadSchema
readOnlyMode={readOnlyMode}
remoteSchemaName={remoteSchemaName}
{...this.props}
/>
</div> </div>
<br /> <br />
<br /> <br />
@ -210,6 +209,7 @@ const mapStateToProps = state => {
allRemoteSchemas: state.remoteSchemas.listData.remoteSchemas, allRemoteSchemas: state.remoteSchemas.listData.remoteSchemas,
dataHeaders: { ...state.tables.dataHeaders }, dataHeaders: { ...state.tables.dataHeaders },
readOnlyMode: state.main.readOnlyMode, readOnlyMode: state.main.readOnlyMode,
inconsistentObjects: state.metadata.inconsistentObjects,
}; };
}; };

View File

@ -1,4 +1,4 @@
@import "../../Common/Common.scss"; @import '../../Common/Common.scss';
.addPaddCommom { .addPaddCommom {
padding: 10px 0; padding: 10px 0;
@ -42,17 +42,12 @@
width: 100%; width: 100%;
padding: 20px; padding: 20px;
text-align: center; text-align: center;
// height: 200px;
// border: 1px solid #000;
img {
}
} }
.commonBtn { .commonBtn {
text-align: center; text-align: center;
padding: 20px 0; padding: 20px 0;
padding-bottom: 10px padding-bottom: 10px;
} }
.readMore { .readMore {
@ -61,7 +56,7 @@
} }
} }
.iconWrapper{ .iconWrapper {
padding: 20px 0; padding: 20px 0;
.icon { .icon {
@ -140,7 +135,7 @@
} }
.red_button { .red_button {
color: #FFF; color: #fff;
} }
a { a {
@ -166,7 +161,6 @@
.set_line_height { .set_line_height {
line-height: 26px; line-height: 26px;
} }
} }
.remoteSchemaImg { .remoteSchemaImg {
@ -193,7 +187,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
a{ a {
color: #909090; color: #909090;
} }
@ -260,24 +254,24 @@
display: inline-block; display: inline-block;
} }
.instructionsWrapper, .instructionsWrapperPos { .instructionsWrapper,
.instructionsWrapperPos {
margin-top: 20px; margin-top: 20px;
border-top: 1px solid #DEDEDE; border-top: 1px solid #dedede;
.instructions { .instructions {
padding: 12px 0; padding: 12px 0;
} }
} }
.instructionsWrapper .instructionsWrapper {
{
position: relative; position: relative;
} }
.instructionsWrapperPos .instructionsWrapperPos {
{
position: static; position: static;
} }
.instructionsWrapper:hover, .instructionsWrapperPos:hover { .instructionsWrapper:hover,
.instructionsWrapperPos:hover {
.instructions { .instructions {
color: #505050 color: #505050;
} }
.rightArrow { .rightArrow {

View File

@ -5,10 +5,10 @@ import PropTypes from 'prop-types';
import LeftContainer from '../../Common/Layout/LeftContainer/LeftContainer'; import LeftContainer from '../../Common/Layout/LeftContainer/LeftContainer';
import PageContainer from '../../Common/Layout/PageContainer/PageContainer'; import PageContainer from '../../Common/Layout/PageContainer/PageContainer';
import RemoteSchemaSubSidebar from './RemoteSchemaSubSidebar'; import RemoteSchemaSubSidebar from './RemoteSchemaSubSidebar';
import styles from '../../Common/TableCommon/Table.scss';
class RemoteSchemaPageContainer extends React.Component { class RemoteSchemaPageContainer extends React.Component {
render() { render() {
const styles = require('../../Common/TableCommon/Table.scss');
const { appPrefix, children } = this.props; const { appPrefix, children } = this.props;
const currentLocation = location.pathname; const currentLocation = location.pathname;

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { Route, IndexRedirect } from 'react-router'; import { Route, IndexRedirect } from 'react-router';
import { rightContainerConnector } from '../../Common/Layout'; import { RightContainer } from '../../Common/Layout/RightContainer';
import globals from '../../../Globals'; import globals from '../../../Globals';
import { import {
remoteSchemaPageConnector, remoteSchemaPageConnector,
@ -95,7 +95,7 @@ const getRemoteSchemaRouter = (connect, store, composeOnEnterHooks) => {
onChange={fetchInitialData(store)} onChange={fetchInitialData(store)}
> >
<IndexRedirect to="manage" /> <IndexRedirect to="manage" />
<Route path="manage" component={rightContainerConnector(connect)}> <Route path="manage" component={RightContainer}>
<IndexRedirect to="schemas" /> <IndexRedirect to="schemas" />
<Route path="schemas" component={landingConnector(connect)} /> <Route path="schemas" component={landingConnector(connect)} />
<Route path="add" component={addConnector(connect)} /> <Route path="add" component={addConnector(connect)} />

View File

@ -2,6 +2,8 @@ import React from 'react';
import { Link } from 'react-router'; import { Link } from 'react-router';
import LeftSubSidebar from '../../Common/Layout/LeftSubSidebar/LeftSubSidebar'; import LeftSubSidebar from '../../Common/Layout/LeftSubSidebar/LeftSubSidebar';
import styles from '../../Common/Layout/LeftSubSidebar/LeftSubSidebar.scss';
import WarningSymbol from '../../Common/WarningSymbol/WarningSymbol';
const RemoteSchemaSubSidebar = ({ const RemoteSchemaSubSidebar = ({
appPrefix, appPrefix,
@ -12,8 +14,12 @@ const RemoteSchemaSubSidebar = ({
filterItem, filterItem,
viewRemoteSchema, viewRemoteSchema,
main, main,
...props
}) => { }) => {
const styles = require('../../Common/Layout/LeftSubSidebar/LeftSubSidebar.scss'); const { inconsistentObjects } = props.metadata;
const inconsistentRemoteSchemas = inconsistentObjects.filter(
inconObject => inconObject.type === 'remote_schema'
);
function tableSearch(e) { function tableSearch(e) {
const searchTerm = e.target.value; const searchTerm = e.target.value;
@ -35,7 +41,7 @@ const RemoteSchemaSubSidebar = ({
const getChildList = () => { const getChildList = () => {
const _dataList = searchQuery ? filtered : dataList; const _dataList = searchQuery ? filtered : dataList;
let childList; let childList = [];
if (_dataList.length === 0) { if (_dataList.length === 0) {
childList = ( childList = (
<li <li
@ -46,8 +52,10 @@ const RemoteSchemaSubSidebar = ({
</li> </li>
); );
} else { } else {
if (_dataList.length > 0) {
childList = _dataList.map((d, i) => { childList = _dataList.map((d, i) => {
let activeTableClass = ''; let activeTableClass = '';
if ( if (
d.name === viewRemoteSchema && d.name === viewRemoteSchema &&
location.pathname.includes(viewRemoteSchema) location.pathname.includes(viewRemoteSchema)
@ -55,6 +63,10 @@ const RemoteSchemaSubSidebar = ({
activeTableClass = styles.activeLink; activeTableClass = styles.activeLink;
} }
const inconsistentCurrentSchema = inconsistentRemoteSchemas.find(
elem => elem.definition.name === d.name
);
return ( return (
<li <li
className={activeTableClass} className={activeTableClass}
@ -62,19 +74,30 @@ const RemoteSchemaSubSidebar = ({
data-test={`remote-schema-sidebar-links-${i + 1}`} data-test={`remote-schema-sidebar-links-${i + 1}`}
> >
<Link <Link
to={appPrefix + '/manage/' + d.name + '/details'} to={`${appPrefix}/manage/${d.name}/details`}
data-test={d.name} data-test={d.name}
> >
<i <i
className={styles.tableIcon + ' fa fa-code-fork'} className={`${styles.tableIcon} fa fa-code-fork`}
aria-hidden="true" aria-hidden="true"
/> />
{d.name} {d.name}
{inconsistentCurrentSchema ? (
<WarningSymbol
customStyle={styles.padLeft4}
tooltipText={
'This remote schema is in an inconsistent state. ' +
'Fields from this remote schema are currently not exposed over the GraphQL API'
}
tooltipPlacement="right"
/>
) : null}
</Link> </Link>
</li> </li>
); );
}); });
} }
}
return childList; return childList;
}; };

View File

@ -208,11 +208,6 @@ const handleInconsistentObjects = inconsistentObjects => {
inconsistentObjects, inconsistentObjects,
'functions' 'functions'
); );
const filteredRemoteSchemas = filterInconsistentMetadataObjects(
remoteSchemas,
inconsistentObjects,
'remote_schemas'
);
const filteredActions = filterInconsistentMetadataObjects( const filteredActions = filterInconsistentMetadataObjects(
actions, actions,
inconsistentObjects, inconsistentObjects,
@ -221,7 +216,7 @@ const handleInconsistentObjects = inconsistentObjects => {
dispatch(setConsistentSchema(filteredSchema)); dispatch(setConsistentSchema(filteredSchema));
dispatch(setConsistentFunctions(filteredFunctions)); dispatch(setConsistentFunctions(filteredFunctions));
dispatch(setConsistentRemoteSchemas(filteredRemoteSchemas)); dispatch(setConsistentRemoteSchemas(remoteSchemas));
dispatch(setActions(filteredActions)); dispatch(setActions(filteredActions));
} }
}; };
@ -234,9 +229,7 @@ export const loadInconsistentObjects = (reloadConfig, successCb, failureCb) => {
const { shouldReloadMetadata, shouldReloadRemoteSchemas } = reloadConfig; const { shouldReloadMetadata, shouldReloadRemoteSchemas } = reloadConfig;
const loadQuery = shouldReloadMetadata const loadQuery = shouldReloadMetadata
? getReloadCacheAndGetInconsistentObjectsQuery( ? getReloadCacheAndGetInconsistentObjectsQuery(shouldReloadRemoteSchemas)
shouldReloadRemoteSchemas === false ? false : true
)
: inconsistentObjectsQuery; : inconsistentObjectsQuery;
dispatch({ type: LOADING_METADATA }); dispatch({ type: LOADING_METADATA });

View File

@ -5,13 +5,13 @@ import Sidebar from './Sidebar';
import PageContainer from '../../Common/Layout/PageContainer/PageContainer'; import PageContainer from '../../Common/Layout/PageContainer/PageContainer';
type Metadata = { type Metadata = {
inconsistentObjects: object[]; inconsistentObjects: Record<string, unknown>[];
ongoingRequest: boolean; ongoingRequest: boolean;
allowedQueries: object[]; allowedQueries: Record<string, any>[];
}; };
type ExternalProps = { type ExternalProps = {
location: RouteComponentProps<{}, {}>['location']; location: RouteComponentProps<unknown, unknown>['location'];
children: JSX.Element; children: JSX.Element;
}; };
@ -55,6 +55,8 @@ const mapStateToProps = (state: DerivedState) => {
type StateProps = ReturnType<typeof mapStateToProps>; type StateProps = ReturnType<typeof mapStateToProps>;
const connector = (connect: Connect) => const connector = (connect: Connect) =>
connect<StateProps, {}, {}, DerivedState>(mapStateToProps)(Container); connect<StateProps, unknown, unknown, DerivedState>(mapStateToProps)(
Container
);
export default connector; export default connector;

View File

@ -2,6 +2,7 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Button from '../../../Common/Button/Button'; import Button from '../../../Common/Button/Button';
import { reloadRemoteSchema } from '../Actions'; import { reloadRemoteSchema } from '../Actions';
import metaDataStyles from '../Settings.scss';
import { import {
showSuccessNotification, showSuccessNotification,
@ -17,7 +18,6 @@ class ReloadRemoteSchema extends Component {
render() { render() {
const { dispatch, remoteSchemaName } = this.props; const { dispatch, remoteSchemaName } = this.props;
const { isReloading } = this.state; const { isReloading } = this.state;
const metaDataStyles = require('../Settings.scss');
const reloadRemoteMetadataHandler = () => { const reloadRemoteMetadataHandler = () => {
this.setState({ isReloading: true }); this.setState({ isReloading: true });
dispatch( dispatch(

View File

@ -133,7 +133,7 @@ const MetadataStatus = ({ dispatch, metadata }) => {
of the metadata of the metadata
</div> </div>
<div className={styles.add_mar_top_small}> <div className={styles.add_mar_top_small}>
The console will also not be able to display these inconsistent The console might also not be able to display these inconsistent
objects objects
</div> </div>
</div> </div>

View File

@ -10,11 +10,11 @@ import { getAdminSecret } from '../ApiExplorer/ApiRequest/utils';
import styles from '../../Common/TableCommon/Table.scss'; import styles from '../../Common/TableCommon/Table.scss';
interface Metadata { interface Metadata {
inconsistentObjects: object[]; inconsistentObjects: Record<string, unknown>[];
} }
type SidebarProps = { type SidebarProps = {
location: RouteComponentProps<{}, {}>['location']; location: RouteComponentProps<unknown, unknown>['location'];
metadata: Metadata; metadata: Metadata;
}; };

View File

@ -0,0 +1,109 @@
import React from 'react';
import styles from './Support.scss';
import discord from './images/discord.svg';
import docs from './images/docs.svg';
import stackOverflow from './images/stack-overflow.svg';
import github from './images/github.svg';
const CHECK_FORUMS = `
If you need any help with developing on Hasura, you can check out
these various Hasura forums. Our community members include some very
experienced engineers from some of the worlds most exciting
companies, and many of them have been using Hasura in production for a
long time.`;
const supportListState = [
{
brand: discord,
title: 'Discord',
description:
'Our community hangs out here. Join discord to ask/help folks in the community.',
link: 'https://discord.com/invite/hasura',
},
{
brand: docs,
title: 'Docs',
description: 'Head to docs to search for what youre looking for.',
link: 'https://hasura.io/docs/',
},
{
brand: stackOverflow,
title: 'StackOverflow',
description: 'Ask your Hasura questions here and tag as hasura',
link: 'https://stackoverflow.com/questions/tagged/hasura',
},
{
brand: github,
title: 'GitHub',
description:
'Create an issue on GitHub to report bugs, suggest improvements or give us a star!',
link: 'https://github.com/hasura/graphql-engine/',
},
];
const HelpPage = () => {
return (
<div
className={`${styles.padd_left_remove} ${styles.supportForumWrapper} container-fluid ${styles.padd_top}`}
>
<div className={styles.padd_left}>
<h2 className={`${styles.headerText} ${styles.inline_block}`}>
Support Forums
</h2>
<div className={`${styles.descriptionText} ${styles.wd60}`}>
{CHECK_FORUMS}
</div>
<div className={styles.supportWrapper}>
{supportListState.map((list, index) => {
return (
<div
className={`col-md-6 col-sm-6 col-xs-12 ${styles.padd_remove} ${styles.supportDisplay}`}
>
<a
key={index}
href={list.link}
target="_blank"
rel="noopener noreferrer"
className={styles.supportFlex}
>
<div className={styles.supportList}>
<div className={styles.supportBrand}>
<img src={list.brand} alt={list.title} />
</div>
<div className={styles.supportContainer}>
<div className={styles.title}>{list.title}</div>
<div className={styles.descriptionText}>
{list.description}
</div>
</div>
</div>
</a>
</div>
);
})}
</div>
<div className={`${styles.descriptionText} ${styles.wd60}`}>
If you would like to talk to our Product Specialists, head to our{' '}
<a
href="https://hasura.io/help"
// eslint-disable-next-line react/jsx-no-target-blank
target="_blank"
>
help page
</a>{' '}
to chat with us or{' '}
<a
href="https://calendly.com/hasura/prod-expert-call"
// eslint-disable-next-line react/jsx-no-target-blank
target="_blank"
>
set up a call
</a>
.
</div>
</div>
</div>
);
};
export default HelpPage;

View File

@ -0,0 +1,60 @@
@import '../../Common/Common.scss';
.supportForumWrapper {
.headerText {
padding-bottom: 16px;
}
.supportWrapper {
padding-top: 40px;
padding-bottom: 32px;
width: 75%;
display: flex;
list-style: none;
flex-wrap: wrap;
clear: both;
.supportDisplay {
display: flex;
}
.supportFlex {
display: flex;
flex-direction: column;
width: 100%;
&:hover {
text-decoration: none;
}
}
.supportList {
padding: 22px;
width: calc(100% - 15px);
margin-right: 15px;
margin-bottom: 15px;
border-radius: 4px;
border: solid 1px #bbbbbb;
display: flex;
flex: 1 0 auto;
.supportBrand {
padding-right: 10px;
}
.supportContainer {
.title {
font-size: 15px;
font-weight: bold;
color: #303030;
border-bottom: 1px solid transparent;
padding-bottom: 8px;
}
.descriptionText {
font-weight: normal;
color: #303030;
font-size: 15px;
}
}
&:hover {
background-color: #ecf0f2;
.title {
text-decoration: underline;
}
}
}
}
}

View File

@ -0,0 +1,29 @@
import React from 'react';
import { Link } from 'react-router';
import { RightContainer } from '../../Common/Layout/RightContainer';
import LeftContainer from '../../Common/Layout/LeftContainer/LeftContainer';
import PageContainer from '../../Common/Layout/PageContainer/PageContainer';
import styles from '../../Common/TableCommon/Table.scss';
const helmetTitle = 'Support Forums | Hasura';
const LeftBar = () => (
<LeftContainer>
<ul>
<li role="presentation" className={styles.active}>
<Link className={styles.linkBorder} to="/support/forums/">
Support Forums
</Link>
</li>
</ul>
</LeftContainer>
);
export const SupportContainer: React.FC = ({ children }) => {
return (
<PageContainer helmet={helmetTitle} leftContainer={<LeftBar />}>
<RightContainer>{children}</RightContainer>
</PageContainer>
);
};

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g id="prefix__Group_8285" data-name="Group 8285" transform="translate(.334)">
<path id="prefix__discord-brands" d="M13.474 11.025a1.188 1.188 0 1 1-1.184-1.288 1.236 1.236 0 0 1 1.184 1.288zm-5.42-1.288a1.293 1.293 0 0 0 0 2.576 1.236 1.236 0 0 0 1.184-1.288 1.229 1.229 0 0 0-1.184-1.288zm12.255-7.346V23.21c-2.924-2.584-1.989-1.728-5.385-4.886l.615 2.147H2.379A2.385 2.385 0 0 1 0 18.081V2.391A2.385 2.385 0 0 1 2.379 0H17.93a2.385 2.385 0 0 1 2.379 2.391zM17 13.392a15.541 15.541 0 0 0-1.67-6.765 5.739 5.739 0 0 0-3.261-1.219l-.162.186A7.724 7.724 0 0 1 14.8 7.068a9.861 9.861 0 0 0-8.669-.337c-.429.2-.685.337-.685.337a7.829 7.829 0 0 1 3.052-1.52l-.116-.139a5.739 5.739 0 0 0-3.264 1.218 15.541 15.541 0 0 0-1.671 6.766 4.208 4.208 0 0 0 3.54 1.764s.429-.522.778-.963a3.609 3.609 0 0 1-2.031-1.369c.171.119.452.274.476.29a8.459 8.459 0 0 0 7.242.406 6.641 6.641 0 0 0 1.335-.685 3.662 3.662 0 0 1-2.1 1.381c.348.441.766.94.766.94A4.242 4.242 0 0 0 17 13.392z" transform="translate(1.762 .146)" style="fill:#505050"/>
<path id="prefix__Rectangle_4799" d="M0 0H24V24H0z" data-name="Rectangle 4799" transform="translate(-.334)" style="fill:none"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<defs>
<style>
.prefix__cls-1{fill:none;stroke:#505050;stroke-linecap:round;stroke-linejoin:round;stroke-width:1.5px}
</style>
</defs>
<g id="prefix__streamline-icon-paginate-filter-text_24x24" data-name="streamline-icon-paginate-filter-text@24x24" transform="translate(0 .004)">
<path id="prefix__Path_5887" d="M3.75.746h19.5v19.5H3.75z" class="prefix__cls-1" data-name="Path 5887"/>
<path id="prefix__Path_5888" d="M20.25 23.246h-18a1.5 1.5 0 0 1-1.5-1.5v-18" class="prefix__cls-1" data-name="Path 5888"/>
<path id="prefix__Path_5889" d="M8.25 6.746h10.5" class="prefix__cls-1" data-name="Path 5889"/>
<path id="prefix__Path_5890" d="M8.25 9.746h10.5" class="prefix__cls-1" data-name="Path 5890"/>
<path id="prefix__Path_5891" d="M8.25 12.746h7.5" class="prefix__cls-1" data-name="Path 5891"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 976 B

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24.021" height="23.585" viewBox="0 0 24.021 23.585">
<g id="prefix__streamline-icon-developer-community-github-1_24x24_1_" data-name="streamline-icon-developer-community-github-1@24x24 (1)" transform="translate(.08 -.021)">
<path id="prefix__Path_5872" d="M14.41 22.35a.5.5 0 0 0 .64.48 11.25 11.25 0 1 0-6.24 0 .5.5 0 0 0 .64-.48V20A2.81 2.81 0 0 1 6 18.28 6.07 6.07 0 0 0 4.64 16c2.85.69 2.9 2.54 4.84 1.67a4 4 0 0 1 .63-1.82c-2.2-.25-4.52-.6-4.52-4.4a3.84 3.84 0 0 1 1-2.66 3.56 3.56 0 0 1 .1-2.62s.83-.27 2.73 1a9.39 9.39 0 0 1 5 0c1.89-1.28 2.72-1 2.72-1a3.56 3.56 0 0 1 .1 2.62 3.83 3.83 0 0 1 1 2.66c0 3.81-2.32 4.15-4.53 4.39a3.83 3.83 0 0 1 .68 2.33z" data-name="Path 5872" style="fill:none;stroke:#505050;stroke-linecap:round;stroke-linejoin:round;stroke-width:1.5px"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 855 B

View File

@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20.075" height="23.735" viewBox="0 0 20.075 23.735">
<defs>
<style>
.prefix__cls-1{fill:none;stroke:#505050;stroke-linecap:round;stroke-linejoin:round;stroke-width:1.5px}
</style>
</defs>
<g id="prefix__streamline-icon-developer-community-stack-overflow_24x24" data-name="streamline-icon-developer-community-stack-overflow@24x24" transform="translate(-2.1 -.015)">
<path id="prefix__Path_5873" d="M17.85 14.46V21a2 2 0 0 1-2 2h-11a2 2 0 0 1-2-2v-6.5" class="prefix__cls-1" data-name="Path 5873"/>
<path id="prefix__Path_5874" d="M14.25 18.96h-8" class="prefix__cls-1" data-name="Path 5874"/>
<path id="prefix__Path_5875" d="M14.63 16.27l-7.82-1.66" class="prefix__cls-1" data-name="Path 5875"/>
<path id="prefix__Path_5876" d="M15.57 13.72l-7.31-3.24" class="prefix__cls-1" data-name="Path 5876"/>
<path id="prefix__Path_5877" d="M17.01 11.42l-6.48-4.69" class="prefix__cls-1" data-name="Path 5877"/>
<path id="prefix__Path_5878" d="M18.9 9.48l-5.36-5.94" class="prefix__cls-1" data-name="Path 5878"/>
<path id="prefix__Path_5879" d="M21.15 7.96l-4.01-6.92" class="prefix__cls-1" data-name="Path 5879"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

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

View File

@ -0,0 +1,45 @@
import styled from 'styled-components';
import {
flexbox,
color,
border,
typography,
layout,
space,
shadow,
FlexboxProps,
ColorProps,
BorderProps,
TypographyProps,
LayoutProps,
SpaceProps,
ShadowProps,
} from 'styled-system';
import { BoxProps, Box } from '../Box';
interface StyledAlertBoxOwnProps
extends FlexboxProps,
ColorProps,
BorderProps,
TypographyProps,
LayoutProps,
SpaceProps,
ShadowProps {}
export interface StyledAlertBoxProps extends StyledAlertBoxOwnProps, BoxProps {}
export const StyledAlertBox = styled(Box)<StyledAlertBoxProps>`
${flexbox};
${color}
${border}
${typography}
${layout}
${space}
${shadow}
/* Alert type text */
span {
text-transform: capitalize;
}
`;

View File

@ -1,13 +1,19 @@
import React from 'react'; import React from 'react';
import { theme } from '../../theme'; import { theme, Theme } from '../../theme';
import { Icon } from '../Icon'; import { Icon, IconProps } from '../Icon';
import { StyledAlertBox } from './AlertBox'; import { StyledAlertBox, StyledAlertBoxProps } from './AlertBox';
import { Text } from '../Typography'; import { Text } from '../Typography';
const alertBoxWidth = 866; const alertBoxWidth = 866;
export const AlertBox = props => { export interface AlertBoxProps
extends IconProps,
Omit<StyledAlertBoxProps, 'size'> {
type: keyof Theme['alertBox'];
}
export const AlertBox: React.FC<AlertBoxProps> = props => {
const { children, type } = props; const { children, type } = props;
const backgroundColorValue = theme.alertBox[type] const backgroundColorValue = theme.alertBox[type]

View File

@ -112,8 +112,8 @@ const iconReferenceMap = {
}; };
export type IconProps = { export type IconProps = {
pointer: boolean; pointer?: boolean;
size: number; size?: number;
type: keyof Theme['icon']; type: keyof Theme['icon'];
}; };

View File

@ -16,14 +16,17 @@ Heading.defaultProps = {
* fontSize: 'explain' * fontSize: 'explain'
* fontWeight: 'bold' * fontWeight: 'bold'
*/ */
export type TextProps = { export type TextProps = {
type: keyof Theme['lineHeights']; type?: keyof Theme['lineHeights'];
fontWeight: keyof Theme['fontWeights']; fontWeight?: keyof Theme['fontWeights'];
fontSize: keyof Theme['fontSizes']; fontSize?: keyof Theme['fontSizes'];
mb: keyof Theme['space']; mb?: keyof Theme['space'];
mt: keyof Theme['space']; mt?: keyof Theme['space'];
mr: keyof Theme['space']; mr?: keyof Theme['space'];
ml: keyof Theme['space']; ml?: keyof Theme['space'];
pl?: keyof Theme['space'];
as?: keyof JSX.IntrinsicElements | React.ComponentType<any>;
}; };
export const Text: React.FC<TextProps> = props => { export const Text: React.FC<TextProps> = props => {
@ -64,6 +67,9 @@ Text.defaultProps = {
mt: 'zero', mt: 'zero',
mr: 'zero', mr: 'zero',
ml: 'zero', ml: 'zero',
pl: 'zero',
fontWeight: 'normal',
fontSize: 'p',
}; };
type TextLinkProps = { type TextLinkProps = {

View File

@ -81,10 +81,6 @@ const Html: React.FC<HtmlProps> = props => {
<div id="content" className="content" /> <div id="content" className="content" />
<script src={`${assets.javascript.main}`} charSet="UTF-8" /> <script src={`${assets.javascript.main}`} charSet="UTF-8" />
{/*
<script src="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.13.1/build/highlight.min.js" />
<script type="text/javascript" src="https://unpkg.com/sql-formatter@latest/dist/sql-formatter.min.js" />
*/}
</body> </body>
</html> </html>
); );

View File

@ -0,0 +1,31 @@
import { useEffect } from 'react';
const MOUSEDOWN = 'mousedown';
const TOUCHSTART = 'touchstart';
type HandledEvents = [typeof MOUSEDOWN, typeof TOUCHSTART];
type HandledEventsType = HandledEvents[number];
type PossibleEvent = {
[Type in HandledEventsType]: HTMLElementEventMap[Type];
}[HandledEventsType];
type Handler = (event: PossibleEvent) => void;
export const useOnClickOutside = (
ref: React.RefObject<HTMLElement>,
handler: Handler
) => {
useEffect(() => {
const listener = (event: PossibleEvent) => {
if (!ref.current || ref.current.contains(event.target as Node)) {
return;
}
handler(event);
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
}, [ref, handler]);
};

View File

@ -0,0 +1,10 @@
import React, { useState } from 'react';
export const useToggle = (
initialValue: boolean
): [boolean, () => void, React.Dispatch<React.SetStateAction<boolean>>] => {
const [value, setValue] = useState(initialValue);
const toggleValue = () => setValue(prev => !prev);
return [value, toggleValue, setValue];
};

View File

@ -40,6 +40,8 @@ import { showErrorNotification } from './components/Services/Common/Notification
import { CLI_CONSOLE_MODE } from './constants'; import { CLI_CONSOLE_MODE } from './constants';
import UIKit from './components/UIKit/'; import UIKit from './components/UIKit/';
import { Heading } from './components/UIKit/atoms'; import { Heading } from './components/UIKit/atoms';
import { SupportContainer } from './components/Services/Support/SupportContainer';
import HelpPage from './components/Services/Support/HelpPage';
const routes = store => { const routes = store => {
// load hasuractl migration status // load hasuractl migration status
@ -71,7 +73,6 @@ const routes = store => {
return; return;
}; };
const _dataRouterUtils = dataRouterUtils(connect, store, composeOnEnterHooks); const _dataRouterUtils = dataRouterUtils(connect, store, composeOnEnterHooks);
const requireSchema = _dataRouterUtils.requireSchema; const requireSchema = _dataRouterUtils.requireSchema;
const dataRouter = _dataRouterUtils.makeDataRouter; const dataRouter = _dataRouterUtils.makeDataRouter;
@ -146,6 +147,9 @@ const routes = store => {
{actionsRouter} {actionsRouter}
{eventsRouter} {eventsRouter}
{uiKitRouter} {uiKitRouter}
<Route path="support" component={SupportContainer}>
<Route path="forums" component={HelpPage} />
</Route>
</Route> </Route>
</Route> </Route>
<Route path="404" component={PageNotFound} status="404" /> <Route path="404" component={PageNotFound} status="404" />

View File

@ -26,10 +26,10 @@ type Telemetry = {
const setConsoleOptsInDB = ( const setConsoleOptsInDB = (
opts: TelemetryState['console_opts'], opts: TelemetryState['console_opts'],
successCb: (arg: object) => void, successCb: (arg: Record<string, any>) => void,
errorCb: (arg: Error) => void errorCb: (arg: Error) => void
) => ( ) => (
dispatch: ThunkDispatch<ReduxState, {}, AnyAction>, dispatch: ThunkDispatch<ReduxState, unknown, AnyAction>,
getState: GetReduxState getState: GetReduxState
) => { ) => {
const url = Endpoints.getSchema; const url = Endpoints.getSchema;
@ -66,7 +66,7 @@ const setConsoleOptsInDB = (
// eslint-disable-next-line consistent-return // eslint-disable-next-line consistent-return
return dispatch(requestAction(url, options)).then( return dispatch(requestAction(url, options)).then(
(data: object) => { (data: Record<string, unknown>) => {
if (successCb) { if (successCb) {
successCb(data); successCb(data);
} }
@ -86,7 +86,7 @@ const telemetryNotificationShown = () => (
}; };
const setTelemetryNotificationShownInDB = () => { const setTelemetryNotificationShownInDB = () => {
const successCb = (data: object) => { const successCb = (data: Record<string, unknown>) => {
console.log( console.log(
`Updated telemetry notification status in db ${JSON.stringify(data)}` `Updated telemetry notification status in db ${JSON.stringify(data)}`
); );
@ -108,7 +108,7 @@ const setTelemetryNotificationShownInDB = () => {
}; };
const setPreReleaseNotificationOptOutInDB = () => ( const setPreReleaseNotificationOptOutInDB = () => (
dispatch: ThunkDispatch<ReduxState, {}, AnyAction> dispatch: ThunkDispatch<ReduxState, unknown, AnyAction>
) => { ) => {
const successCb = () => { const successCb = () => {
dispatch( dispatch(
@ -132,7 +132,7 @@ const setPreReleaseNotificationOptOutInDB = () => (
const loadConsoleOpts = () => { const loadConsoleOpts = () => {
return ( return (
dispatch: ThunkDispatch<ReduxState, {}, AnyAction>, dispatch: ThunkDispatch<ReduxState, unknown, AnyAction>,
getState: GetReduxState getState: GetReduxState
) => { ) => {
const url = Endpoints.getSchema; const url = Endpoints.getSchema;
@ -172,12 +172,12 @@ const loadConsoleOpts = () => {
interface SetConsoleOptsAction { interface SetConsoleOptsAction {
type: typeof SET_CONSOLE_OPTS; type: typeof SET_CONSOLE_OPTS;
data: object; data: Record<string, unknown>;
} }
interface SetNotificationShowAction { interface SetNotificationShowAction {
type: typeof SET_NOTIFICATION_SHOWN; type: typeof SET_NOTIFICATION_SHOWN;
data?: object; data?: Record<string, unknown>;
} }
interface SetHasuraUuid { interface SetHasuraUuid {
@ -193,7 +193,7 @@ type TelemetryActionTypes =
export const requireConsoleOpts = ({ export const requireConsoleOpts = ({
dispatch, dispatch,
}: { }: {
dispatch: ThunkDispatch<ReduxState, {}, AnyAction>; dispatch: ThunkDispatch<ReduxState, unknown, AnyAction>;
}) => (nextState: ReduxState, replaceState: ReduxState, callback: any) => { }) => (nextState: ReduxState, replaceState: ReduxState, callback: any) => {
dispatch(loadConsoleOpts()).finally(callback); dispatch(loadConsoleOpts()).finally(callback);
}; };

View File

@ -6,13 +6,13 @@ import { setTelemetryNotificationShownInDB } from './Actions';
import { ReduxState } from '../types'; import { ReduxState } from '../types';
const onRemove = () => { const onRemove = () => {
return (dispatch: ThunkDispatch<ReduxState, {}, AnyAction>) => { return (dispatch: ThunkDispatch<ReduxState, unknown, AnyAction>) => {
dispatch(setTelemetryNotificationShownInDB()); dispatch(setTelemetryNotificationShownInDB());
}; };
}; };
const showTelemetryNotification = () => { const showTelemetryNotification = () => {
return (dispatch: ThunkDispatch<ReduxState, {}, AnyAction>) => { return (dispatch: ThunkDispatch<ReduxState, unknown, AnyAction>) => {
dispatch( dispatch(
Notifications.show({ Notifications.show({
position: 'tr', position: 'tr',

View File

@ -0,0 +1,94 @@
/**
* THIS IS A WORK IN PROGRESS
*/
export interface IntegerColumnType {
int2: number;
int4: number;
int8: number;
smallint: number;
int: number;
bigint: number;
}
export interface PrecisionColumnType {
real: number;
float4: number;
float: number;
float8: number;
numeric: number;
decimal: number;
}
export interface SerialColumnType {
smallserial: number;
serial: number;
bigserial: number;
}
export interface StringColumnType {
uuid: string;
text: string;
varchar: string;
char: string;
citext: string;
}
export interface BooleanColumnType {
bit: boolean;
bool: boolean;
boolean: boolean;
}
export interface DateTimeColumnType {
date: string;
timestamp: string;
timestamptz: string;
time: string;
timetz: string;
interval: string;
}
export interface NetworkAddressColumnType {
inet: string;
cidr: string;
macaddr: string;
macaddr8: string;
}
export interface MiscellaneousColumnType {
money: number;
}
export interface JSONColumnType {
json: Record<string, unknown>;
jsonb: Record<string, unknown>;
}
export type JSONColumn = keyof JSONColumnType;
export interface ByteColumnType {
bytea: string;
}
export type IntegerColumn = keyof IntegerColumnType;
export type PrecisionColumn = keyof PrecisionColumnType;
export type SerialColumn = keyof SerialColumnType;
export type StringColumn = keyof StringColumnType;
export type BooleanColumn = keyof BooleanColumnType;
export type DateTimeColumn = keyof DateTimeColumnType;
export type NetworkAddressColumn = keyof NetworkAddressColumnType;
export type MiscellaneousColumn = keyof MiscellaneousColumnType;
export type ByteColumn = keyof ByteColumnType;
export type Column =
| IntegerColumn
| PrecisionColumn
| SerialColumn
| StringColumn
| BooleanColumn
| DateTimeColumn
| NetworkAddressColumn
| MiscellaneousColumn
| JSONColumn
| ByteColumn;

View File

@ -1,5 +1,7 @@
.. title:: 404 - Page Not Found .. title:: 404 - Page Not Found
:orphan:
404 - Page Not Found 404 - Page Not Found
--------------------- ---------------------

View File

@ -14,8 +14,8 @@ window.hdocs = (function () {
docsearch({ docsearch({
appId: 'WCBB1VVLRC', appId: 'WCBB1VVLRC',
apiKey: '298d448cd9d7ed93fbab395658da19e8', apiKey: HDOCS_ALGOLIA_API_KEY,
indexName: 'graphql-docs-prod', indexName: HDOCS_ALGOLIA_INDEX,
inputSelector: '#search_element', inputSelector: '#search_element',
transformData: hdocs.transformSearchData, transformData: hdocs.transformSearchData,
debug: false debug: false
@ -116,13 +116,22 @@ window.hdocs = (function () {
}); });
} }
const searchField = document.getElementById('search_element');
const searchHelp = document.getElementById('search_help');
const hideHelp = function () { searchHelp.classList.add('hide'); }
if (suggestions.length === 0) { if (suggestions.length === 0) {
setTimeout(function () { document.getElementById('search_help').classList.remove('hide'); }, 100); setTimeout(function () { searchHelp.classList.remove('hide'); }, 100);
document.getElementById('search_element').addEventListener('blur', function () { searchField.addEventListener('blur', function () {
setTimeout(function () { document.getElementById('search_help').classList.add('hide'); }, 100); setTimeout(hideHelp, 100);
}); }, { once: true });
} else if (!document.getElementById('search_help').classList.contains('hide')) { searchField.addEventListener('input', function () {
document.getElementById('search_help').classList.add('hide'); if (searchField.value === '') {
setTimeout(hideHelp, 100);
}
}, { once: true });
} else if (!searchHelp.classList.contains('hide')) {
hideHelp();
} }
return suggestions; return suggestions;

119
docs/_static/scripts/mysql-subscribe.js vendored Normal file
View File

@ -0,0 +1,119 @@
const mysql_email_input = document.getElementById('mysql-EMAIL');
const mysql_submit_btn = document.getElementById('mysql-embedded-subscribe');
const mysql_success_status = document.getElementById('mysql-success-response');
const mysql_status_error = document.getElementById('mysql-error-response');
mysql_email_input.addEventListener('input', function() {
mysql_submit_btn.value = 'Subscribe';
mysql_submit_btn.disabled = false;
mysql_status_error.innerHTML = '';
mysql_status_error.classList.add('hide');
});
const showMySQLErrorMsg = () => {
mysql_submit_btn.value = 'Subscribe';
mysql_submit_btn.disabled = false;
mysql_status_error.innerHTML = 'Please enter a valid email';
mysql_status_error.classList.remove('hide');
clearMySQLMsg();
};
const clearMySQLMsg = () => {
setTimeout(function(){
mysql_success_status.innerHTML = '';
mysql_status_error.innerHTML = '';
mysql_success_status.classList.add('hide');
mysql_status_error.classList.add('hide');
mysql_submit_btn.disabled = true;
//reset input
mysql_email_input.value = '';
}, 3000);
}
const submitMySQLForm = function (form) {
let gqlEndpoint = 'https://graphql-engine-website.hasura.io/v1/graphql';
if(window.location.host !== "hasura.io") {
gqlEndpoint = 'https://graphql-engine-website.hasura-stg.hasura-app.io/v1/graphql';
}
// change button state
mysql_submit_btn.value = 'Subscribing...';
mysql_submit_btn.disabled = true;
const email = form.elements["EMAIL"].value;
const hbs_context = {
"hutk": readCookie("hubspotutk"), // include this parameter and set it to the hubspotutk cookie value to enable cookie tracking on your submission
"pageUri": window.location.host + window.location.pathname,
"pageName": document.title,
};
const gqlMutation = `mutation notifyDatabaseSupport($object: databaseSupportInput!) {
notifyDatabaseSupport(object: $object) {
id
}
}`;
const object = {
"category": "docs",
"database": "MySQL",
"email": email,
"hbs_context": hbs_context
};
fetch(gqlEndpoint, {
method: 'POST',
body: JSON.stringify({
query: gqlMutation,
variables: { object: object }
}),
})
.then(response => response.json())
.then(data => {
// change button state
mysql_submit_btn.value = 'Subscribe';
mysql_submit_btn.disabled = false;
if (data && data.data && data.data.notifyDatabaseSupport.id) {
mysql_success_status.innerHTML = 'Subscribed!';
mysql_success_status.classList.remove('hide');
} else {
if(data.errors && data.errors[0].message.includes('Uniqueness violation')) {
mysql_status_error.innerHTML = 'You have already subscribed';
} else {
mysql_status_error.innerHTML = 'Something went wrong';
}
mysql_status_error.classList.remove('hide');
}
clearMySQLMsg();
})
.catch((error) => {
console.error('Error:', error);
mysql_submit_btn.value = 'Subscribe';
mysql_submit_btn.disabled = false;
});
};
document.addEventListener('submit', function (event) {
// Only run on forms flagged for mysql-subscribe-form validation
if (!event.target.classList.contains('mysql-subscribe-form')) return;
// Prevent form from submitting
event.preventDefault();
// email validation
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if(!emailPattern.test(mysql_email_input.value)) {
showMySQLErrorMsg();
return;
}
// Otherwise, let the form submit normally
submitMySQLForm(event.target);
}, false);

View File

@ -1,7 +1,7 @@
const email_input = document.getElementById('mce-EMAIL'); const email_input = document.getElementById('mce-EMAIL');
const submit_btn = document.getElementById('mc-embedded-subscribe'); const submit_btn = document.getElementById('mc-embedded-subscribe');
const mcStatusSuccess = document.querySelector('.mce-success-response'); const mcStatusSuccess = document.getElementById('mce-success-response');
const mcStatusError = document.querySelector('.mce-error-response'); const mcStatusError = document.getElementById('mce-error-response');
email_input.addEventListener('input', function() { email_input.addEventListener('input', function() {
submit_btn.value = 'Subscribe'; submit_btn.value = 'Subscribe';
@ -60,16 +60,19 @@ const submitNewsletterForm = function (form) {
"pageUri": window.location.host + window.location.pathname, "pageUri": window.location.host + window.location.pathname,
"pageName": document.title, "pageName": document.title,
}; };
const gqlMutation = `mutation docsNewsletterSignup($objects: [newsletterSignupInput!]! ) { const gqlMutation = `mutation docsNewsletterSignup($objects: [newsletterSignupInput!]! ) {
signupNewsletter(objects: $objects) { signupNewsletter(objects: $objects) {
affected_rows affected_rows
} }
}`; }`;
const objects = [{ const objects = [{
"email": email, "email": email,
"hbs_context": hbs_context, "hbs_context": hbs_context,
"category": "docs" "category": "docs"
}] }];
fetch(gqlEndpoint, { fetch(gqlEndpoint, {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({

View File

@ -15,13 +15,11 @@
} }
.body_content { .body_content {
font-family: 'Gudea';
font-size: 15px; font-size: 15px;
color: #333; color: #333;
} }
.small_content { .small_content {
font-family: 'Gudea';
font-size: 14px; font-size: 14px;
color: #333; color: #333;
} }
@ -101,6 +99,10 @@
padding-bottom: 0; padding-bottom: 0;
} }
.description {
margin-bottom: 20px;
}
.text_left { .text_left {
text-align: left; text-align: left;
} }

View File

@ -99,16 +99,16 @@ p {
} }
/* Newsletter */ /* Newsletter */
#mc_embed_signup { .mc_embed_signup {
display: flex; display: flex;
align-items: center; align-items: center;
} }
#mc_embed_signup .subscribe-form-content { .mc_embed_signup .subscribe-form-content {
font-weight: bold; font-weight: bold;
color: #001934; color: #001934;
padding-right: 40px; padding-right: 40px;
} }
#mc_embed_signup form { .mc_embed_signup form {
display: flex !important; display: flex !important;
padding: 0 0 0 0 !important; padding: 0 0 0 0 !important;
flex: 1 !important; flex: 1 !important;
@ -116,17 +116,17 @@ p {
font-size: 16px; font-size: 16px;
} }
/* todo: redesign & clean up */ /* todo: redesign & clean up */
#mc_embed_signup input:focus {border-color:#333;} .mc_embed_signup input:focus {border-color:#333;}
#mc_embed_signup .button {clear:both; background-color: #aaa; border: 0 none; border-radius:4px; transition: all 0.23s ease-in-out 0s; color: #FFFFFF; cursor: pointer; display: inline-block; font-size:15px; font-weight: normal; height: 32px; line-height: 32px; margin: 0 5px 10px 0; padding: 0 22px; text-align: center; text-decoration: none; vertical-align: top; white-space: nowrap; width: auto;} .mc_embed_signup .button {clear:both; background-color: #aaa; border: 0 none; border-radius:4px; transition: all 0.23s ease-in-out 0s; color: #FFFFFF; cursor: pointer; display: inline-block; font-size:15px; font-weight: normal; height: 32px; line-height: 32px; margin: 0 5px 10px 0; padding: 0 22px; text-align: center; text-decoration: none; vertical-align: top; white-space: nowrap; width: auto;}
#mc_embed_signup .button:hover {background-color:#777;} .mc_embed_signup .button:hover {background-color:#777;}
#mc_embed_signup .clear {clear:both;} .mc_embed_signup .clear {clear:both;}
#mc_embed_signup div#mce-responses {float:left; top:-1.4em; padding:0em .5em 0em .5em; overflow:hidden; width:90%; margin: 0 5%; clear: both;} .mc_embed_signup div.mce-responses {float:left; top:-1.4em; padding:0em .5em 0em .5em; overflow:hidden; width:90%; margin: 0 5%; clear: both;}
#mc_embed_signup div.response {margin:1em 0; padding:1em .5em .5em 0; font-weight:bold; float:left; top:-1.5em; z-index:1; width:80%;} .mc_embed_signup div.response {margin:1em 0; padding:1em .5em .5em 0; font-weight:bold; float:left; top:-1.5em; z-index:1; width:80%;}
#mc_embed_signup #mce-error-response {display:none;} /*.mc_embed_signup .mce-error-response {display:none;}*/
#mc_embed_signup #mce-success-response {color:#529214; display:none;} .mc_embed_signup .mce-success-response {color:#529214;}
#mc_embed_signup label.error {display:block; float:none; width:auto; margin-left:1.05em; text-align:left; padding:.5em 0;} .mc_embed_signup label.error {display:block; float:none; width:auto; margin-left:1.05em; text-align:left; padding:.5em 0;}
#mc-embedded-subscribe {clear:both; width:auto; display:block; margin:1em 0 1em 5%;} .mc-embedded-subscribe {clear:both; width:auto; display:block; margin:1em 0 1em 5%;}
.newsletter-link, .newsletter-link,
.newsletter-link:hover, .newsletter-link:hover,
@ -204,7 +204,7 @@ p {
.submit-box input:focus { .submit-box input:focus {
outline: none; outline: none;
} }
#mce-responses .error-message, #mce-responses .success-message { .mce-responses .error-message, .mce-responses .success-message {
background-color: #001934; background-color: #001934;
padding: 8px 12px !important; padding: 8px 12px !important;
border-radius: 4px; border-radius: 4px;
@ -216,13 +216,13 @@ p {
width: auto !important; width: auto !important;
} }
#mce-responses .error-message a, #mce-responses .success-message a { .mce-responses .error-message a, .mce-responses .success-message a {
color: #fff !important; color: #fff !important;
} }
#mce-responses .error-message { .mce-responses .error-message {
color: #efc371 !important; color: #efc371 !important;
} }
#mce-responses .success-message { .mce-responses .success-message {
color: #1cd3c6 !important; color: #1cd3c6 !important;
} }
#content_inner_wrapper { #content_inner_wrapper {
@ -640,6 +640,10 @@ article ol ol {
margin: 15px; margin: 15px;
} }
.sphinxsidebarwrapper > ul:not(.current) {
display: none;
}
#sidebar { #sidebar {
background-color: #001934; background-color: #001934;
width: 24% !important; width: 24% !important;
@ -1010,17 +1014,17 @@ article ol ol {
} }
} }
@media (min-width: 768px) and (max-width: 1385px) { @media (min-width: 768px) and (max-width: 1385px) {
#mc_embed_signup form { .mc_embed_signup form {
padding-top: 10px !important; padding-top: 10px !important;
} }
#mc_embed_signup { .mc_embed_signup {
display: block; display: block;
} }
#mc_embed_signup .subscribe-form-content { .mc_embed_signup .subscribe-form-content {
padding-right: 0; padding-right: 0;
text-align: center; text-align: center;
} }
#mce-responses .error-message, #mce-responses .success-message { .mce-responses .error-message, .mce-responses .success-message {
top: 34px !important; top: 34px !important;
} }
} }
@ -1030,20 +1034,20 @@ article ol ol {
top: 170px; top: 170px;
height: calc(100% - 170px); height: calc(100% - 170px);
} }
#mc_embed_signup { .mc_embed_signup {
display: block; display: block;
} }
#mc_embed_signup .subscribe-form-content { .mc_embed_signup .subscribe-form-content {
padding-right: 0; padding-right: 0;
text-align: center; text-align: center;
} }
#mc_embed_signup form { .mc_embed_signup form {
padding-top: 10px !important; padding-top: 10px !important;
} }
.submit-box input { .submit-box input {
padding: 0 4px !important; padding: 0 4px !important;
} }
#mce-responses .error-message, #mce-responses .success-message { .mce-responses .error-message, .mce-responses .success-message {
top: 34px !important; top: 34px !important;
} }
#input_search_box { #input_search_box {

View File

@ -140,6 +140,11 @@
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-PF5MQ2Z');</script> })(window,document,'script','dataLayer','GTM-PF5MQ2Z');</script>
<!-- End Google Tag Manager --> <!-- End Google Tag Manager -->
<!-- Posthog -->
<script>
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('a1dops3FFe8KioWsry6W6AVqCG_j-FXmw1LY2d6TrYU', {api_host: 'https://cloud-posthog.hasura-app.io'})
</script>
{%- if use_opensearch %} {%- if use_opensearch %}
<link rel="search" type="application/opensearchdescription+xml" <link rel="search" type="application/opensearchdescription+xml"
title="{% trans docstitle=docstitle|e %}Search within {{ docstitle }}{% endtrans %}" title="{% trans docstitle=docstitle|e %}Search within {{ docstitle }}{% endtrans %}"

View File

@ -4,6 +4,16 @@
{% set is_landing_page = true %} {% set is_landing_page = true %}
{%- endif %} {%- endif %}
{%- if pagename.startswith('graphql/core') %}
{% set is_core = true %}
{% set ALGOLIA_INDEX = 'graphql-docs-prod' %}
{% set ALGOLIA_API_KEY = '298d448cd9d7ed93fbab395658da19e8' %}
{%- elif pagename.startswith('graphql/cloud') %}
{% set is_cloud = true %}
{% set ALGOLIA_INDEX = 'cloud-docs-prod' %}
{% set ALGOLIA_API_KEY = 'cf84f05a225bedb72ce472dada63d29f' %}
{%- endif %}
{% set css_files = css_files + ['_static/graphiql/graphiql.css', '_static/styles/main.css'] %} {% set css_files = css_files + ['_static/graphiql/graphiql.css', '_static/styles/main.css'] %}
{%- if is_landing_page %} {%- if is_landing_page %}
@ -19,6 +29,10 @@
{% set deferred_script_files = ['_static/react/react.min.js', '_static/react/react-dom.min.js', '_static/graphiql/graphiql.min.js', '_static/scripts/hdocs.js', '_static/scripts/newsletter.js'] %} {% set deferred_script_files = ['_static/react/react.min.js', '_static/react/react-dom.min.js', '_static/graphiql/graphiql.min.js', '_static/scripts/hdocs.js', '_static/scripts/newsletter.js'] %}
{%- if pagename.endswith('mysql-preview') %}
{% set deferred_script_files = deferred_script_files + ['_static/scripts/mysql-subscribe.js'] %}
{%- endif %}
{%- macro secondnav() %} {%- macro secondnav() %}
{%- if prev %} {%- if prev %}
&laquo; <a href="{{ prev.link|e }}" title="{{ prev.title|e }}">previous</a> &laquo; <a href="{{ prev.link|e }}" title="{{ prev.title|e }}">previous</a>
@ -105,7 +119,7 @@
</a> </a>
</li> </li>
<li> <li>
<a href='https://hasura.io/docs/1.0/graphql/manual/getting-started/index.html'> <a href='https://hasura.io/docs/1.0/graphql/core/getting-started/index.html'>
<button class="commonBtn navBtnHome"> <button class="commonBtn navBtnHome">
Get Started Get Started
</button> </button>
@ -162,7 +176,7 @@
</a> </a>
</li> </li>
<li> <li>
<a class="menuLink" href='https://hasura.io/docs/1.0/graphql/manual/getting-started/index.html'> <a class="menuLink" href='https://hasura.io/docs/1.0/graphql/core/getting-started/index.html'>
<button class='commonBtn navBtnHome'> <button class='commonBtn navBtnHome'>
Get Started Get Started
</button> </button>
@ -187,12 +201,12 @@
<img src="{{ pathto('_images/layout/close-icon.svg', 1) }}" alt="Close"/> <img src="{{ pathto('_images/layout/close-icon.svg', 1) }}" alt="Close"/>
</div> </div>
<div class="tabbarContainerWrapper blueBgColor boderBottom"> <div class="tabbarContainerWrapper blueBgColor boderBottom">
<a href="" class="tabbarTabActive"> <a href="{{ pathto('graphql/core/index.html', 1) }}" {%- if is_core %} class="tabbarTabActive" {%- endif %}>
<span> <span>
Hasura Core Hasura Core
</span> </span>
</a> </a>
<a href="https://hasura.io/docs/cloud/1.0/manual/index.html"> <a href="{{ pathto('graphql/cloud/index.html', 1) }}" {%- if is_cloud %} class="tabbarTabActive" {%- endif %}>
<span> <span>
Hasura Cloud Hasura Cloud
</span> </span>
@ -280,29 +294,27 @@
</div> </div>
</div> </div>
<div class="subscribe-form-wrapper"> <div class="subscribe-form-wrapper">
<div id="mc_embed_signup"> <div id="mc_embed_signup" class="mc_embed_signup">
<div class="subscribe-form-content"> <div class="subscribe-form-content">
Stay up to date with product & security news Stay up to date with product & security news
<a target="_blank" class="newsletter-link" href="https://us13.campaign-archive.com/home/?u=9b63e92a98ecdc99732456b0e&id=f5c4f66bcf" rel="noopener">See past editions</a>
</div> </div>
<form id="mc-embedded-subscribe-form" name="mc-embedded-subscribe-form" class="validate post-subscription-form newsletter-form" target="_blank" rel="noopener" novalidate> <form id="mc-embedded-subscribe-form" name="mc-embedded-subscribe-form" class="validate post-subscription-form mc-embedded-subscribe-form newsletter-form" target="_blank" rel="noopener" novalidate>
<div class="full-width"> <div class="full-width">
<div class="input-box"> <div class="input-box">
<input type="email" name="EMAIL" id="mce-EMAIL" aria-label="Email" placeholder="Your Email Address" pattern="^([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22))*\x40([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d))*(\.\w{2,})+$" required> <input type="email" name="EMAIL" id="mce-EMAIL" class="mce-EMAIL" aria-label="Email" placeholder="Your Email Address" pattern="^([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22))*\x40([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d))*(\.\w{2,})+$" required>
</div> </div>
<div id="mce-responses" class="clear display-inline"> <div id="mce-responses" class="clear display-inline mce-responses">
<div class="mce-error-response response error-message hide"> <div id="mce-error-response" class="mce-error-response response error-message hide">
</div> </div>
<div class="mce-success-response response success-message hide"> <div id="mce-success-response" class="mce-success-response response success-message hide">
</div> </div>
</div> </div>
</div> </div>
<div style="position: absolute; left: -5000px;" aria-hidden="true"><input type="text" name="b_9b63e92a98ecdc99732456b0e_f5c4f66bcf" tabindex="-1" value=""></div> <div style="position: absolute; left: -5000px;" aria-hidden="true"><input type="text" name="b_9b63e92a98ecdc99732456b0e_f5c4f66bcf" tabindex="-1" value=""></div>
<div class="clear submit-box"> <div class="clear submit-box">
<input type="submit" disabled="true" value="Subscribe" name="subscribe" id="mc-embedded-subscribe" class="button"> <input type="submit" disabled="true" value="Subscribe" name="subscribe" id="mc-embedded-subscribe" class="button mc-embedded-subscribe">
</div> </div>
</form> </form>
</div> </div>
</div> </div>
<div class="footer-hasura-custom"> <div class="footer-hasura-custom">
@ -344,6 +356,8 @@
<script type="text/javascript"> <script type="text/javascript">
const HDOCS_BASE_DOMAIN = "{{ BASE_DOMAIN }}"; const HDOCS_BASE_DOMAIN = "{{ BASE_DOMAIN }}";
const HDOCS_GRAPHIQL_DEFAULT_ENDPOINT = "{{ GRAPHIQL_DEFAULT_ENDPOINT }}"; const HDOCS_GRAPHIQL_DEFAULT_ENDPOINT = "{{ GRAPHIQL_DEFAULT_ENDPOINT }}";
const HDOCS_ALGOLIA_INDEX = "{{ALGOLIA_INDEX}}";
const HDOCS_ALGOLIA_API_KEY = "{{ALGOLIA_API_KEY}}";
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
hdocs && hdocs.setup(); hdocs && hdocs.setup();

View File

@ -1,24 +1,3 @@
{%- if display_toc %} {%- if display_toc %}
<!--
<div class="header_main_logo inline-block mobile-hide">
<a href="https://{{ BASE_DOMAIN }}/" target="_blank" rel="noopener">
<div class="img_wrapper inline-block">
<img class="responsive logo_img" src="{{ pathto('_images/layout/logo-lite.svg', 1) }}" alt="Hasura Logo Light" />
</div>
</a>
<a class="docs_label" href="{{ pathto('', 1) }}">
<div class="inline-block hero"> docs </div>
</a>
<a class="version_txt">
<select value="{{ version }}" onchange="location = this.value;" class="selected" aria-label="Select Version">
{%- if version == '1.0' %}
<option class="option_val" value="https://{{ BASE_DOMAIN }}/docs/1.0/graphql/manual/index.html" selected="selected">v1.x</option>
{%- else -%}
<option class="option_val" value="https://{{ BASE_DOMAIN }}/docs/1.0/graphql/manual/index.html">v1.x</option>
{% endif %}
</select>
</a>
</div>
-->
{{ toc_full }} {{ toc_full }}
{%- endif %} {%- endif %}

View File

@ -5,25 +5,23 @@
<div class="box_head_wrapper"> <div class="box_head_wrapper">
<div class="box_head"> <div class="box_head">
<img src="{{ pathto('_images/landing/graphql.svg', 1) }}" alt="GraphQL engine"/> <img src="{{ pathto('_images/landing/graphql.svg', 1) }}" alt="GraphQL engine"/>
<h3 class="head_wrapper">1. The Hasura GraphQL engine</h3> <h3 class="head_wrapper">Hasura docs</h3>
</div>
<div class="view_all_wrapper small_content">
</div> </div>
</div> </div>
<div class="small_content space_wrapper text_left"> <div class="space_wrapper text_left">
This guide covers all Hasura GraphQL engine concepts and features. <p class="description">This guide covers all Hasura concepts and features.</p>
<br/> <br/> <ul>
</div> <li>
<div class="sign_in_wrapper space_wrapper"> <a class="docs_link" href="{{ pathto('graphql/core/index.html', 1) }}">
<div class="get_start"> Core docs
<img src="{{ pathto('_images/landing/manual.svg', 1) }}" alt="Manual"/>
</div>
<a href="{{ pathto('graphql/manual/index.html', 1) }}">
<div class="docs_link body_content">
Open manual
<img src="{{ pathto('_images/landing/right-arrow.svg', 1) }}" alt="Right Arrow" />
</div>
</a> </a>
</li>
<li>
<a class="docs_link" href="{{ pathto('graphql/cloud/index.html', 1) }}">
Cloud docs
</a>
</li>
</ul>
</div> </div>
</div> </div>
</div> </div>

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