diff --git a/.env.example b/.env.example
index 4a01bf885a..ad6adc3110 100644
--- a/.env.example
+++ b/.env.example
@@ -1,6 +1,6 @@
TOOLJET_HOST=http://localhost:8082
-ENCRYPTION_SERVICE_SALT=replace_with_salt
+LOCKBOX_MASTER_KEY=replace_with_encryption_key
SECRET_KEY_BASE=replace_with_secret_key_base
diff --git a/.rubocop.yml b/.rubocop.yml
new file mode 100644
index 0000000000..265f113105
--- /dev/null
+++ b/.rubocop.yml
@@ -0,0 +1,248 @@
+require: rubocop-rails
+AllCops:
+ SuggestExtensions: false
+ TargetRubyVersion: 2.7
+ # RuboCop has a bunch of cops enabled by default. This setting tells RuboCop
+ # to ignore them, so only the ones explicitly set in this file are enabled.
+ DisabledByDefault: true
+ Exclude:
+ - "**/templates/**/*"
+ - "**/vendor/**/*"
+ - "actionpack/lib/action_dispatch/journey/parser.rb"
+ - "db/schema.rb"
+ - "db/migrate/**/*"
+ - "node_modules/**/*"
+ - "bin/**"
+ - "config/application.rb"
+ - "config/boot.rb"
+ - "config/environment.rb"
+ - "config/environments/*.rb"
+ - "config/routes.rb"
+
+Rails/EnvironmentVariableAccess:
+ Enabled: false
+
+Rails/TimeZoneAssignment:
+ Enabled: true
+
+# Prefer &&/|| over and/or.
+Style/AndOr:
+ Enabled: true
+
+# Align `when` with `case`.
+Layout/CaseIndentation:
+ Enabled: true
+
+# Align comments with method definitions.
+Layout/CommentIndentation:
+ Enabled: true
+
+Layout/EmptyLineAfterMagicComment:
+ Enabled: true
+
+# In a regular class definition, no empty lines around the body.
+Layout/EmptyLinesAroundClassBody:
+ Enabled: true
+
+# In a regular method definition, no empty lines around the body.
+Layout/EmptyLinesAroundMethodBody:
+ Enabled: true
+
+# In a regular module definition, no empty lines around the body.
+Layout/EmptyLinesAroundModuleBody:
+ Enabled: true
+
+Layout/FirstArgumentIndentation:
+ Enabled: true
+
+# Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }.
+Style/HashSyntax:
+ Enabled: true
+
+# Method definitions after `private` or `protected` isolated calls need one
+# extra level of indentation.
+Layout/IndentationConsistency:
+ Enabled: true
+ EnforcedStyle: indented_internal_methods
+
+# Two spaces, no tabs (for indentation).
+Layout/IndentationWidth:
+ Enabled: true
+
+Layout/LeadingCommentSpace:
+ Enabled: true
+
+Layout/SpaceAfterColon:
+ Enabled: true
+
+Layout/SpaceAfterComma:
+ Enabled: true
+
+Layout/SpaceAroundEqualsInParameterDefault:
+ Enabled: true
+
+Layout/SpaceAroundKeyword:
+ Enabled: true
+
+# If we enable it then following code indentation fails
+# @current_row_project_name = row["Project"]
+# @current_row_date = row["Date"]
+# @current_row_email = row["Email"]
+# @current_row_client_name = row["Client"]
+# @current_row_task_name = row["Task"]
+# @current_row_notes = row["Notes"]
+# @current_row_hours = row["Hours"]
+Layout/SpaceAroundOperators:
+ Enabled: false
+
+Layout/SpaceBeforeComma:
+ Enabled: true
+
+Layout/SpaceBeforeFirstArg:
+ Enabled: true
+
+Style/DefWithParentheses:
+ Enabled: true
+
+# Defining a method with parameters needs parentheses.
+Style/MethodDefParentheses:
+ Enabled: true
+
+Style/FrozenStringLiteralComment:
+ Enabled: true
+ EnforcedStyle: always
+
+# Use `foo {}` not `foo{}`.
+Layout/SpaceBeforeBlockBraces:
+ Enabled: true
+
+# Use `foo { bar }` not `foo {bar}`.
+Layout/SpaceInsideBlockBraces:
+ Enabled: true
+
+# Use `{ a: 1 }` not `{a:1}`.
+Layout/SpaceInsideHashLiteralBraces:
+ Enabled: true
+
+Layout/SpaceInsideParens:
+ Enabled: true
+
+# Check quotes usage according to lint rule below.
+Style/StringLiterals:
+ Enabled: true
+ EnforcedStyle: double_quotes
+
+# This cop looks for trailing blank lines and a final newline in the source code.
+# This is being disabled because the requirement to have a final newline is illogical.
+Layout/TrailingEmptyLines:
+ Enabled: false
+
+# No trailing whitespace.
+Layout/TrailingWhitespace:
+ Enabled: true
+
+# Use quotes for string literals when they are enough.
+Style/RedundantPercentQ:
+ Enabled: true
+
+# Align `end` with the matching keyword or starting expression except for
+# assignments, where it should be aligned with the LHS.
+Layout/EndAlignment:
+ Enabled: true
+ EnforcedStyleAlignWith: variable
+
+# Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg.
+Lint/RequireParentheses:
+ Enabled: true
+
+Style/RedundantReturn:
+ Enabled: true
+ AllowMultipleReturnValues: true
+
+Style/Semicolon:
+ Enabled: true
+ AllowAsExpressionSeparator: true
+
+# Corrects usage of :true/:false to true/false
+Lint/BooleanSymbol:
+ Enabled: true
+
+# No space in method name and the arguments
+Lint/ParenthesesAsGroupedExpression:
+ Enabled: true
+
+# Enabled Rails cops for the command rubocop -a
+Rails:
+ Enabled: true
+
+# Correct usage of Date methods in Rails
+Rails/Date:
+ Enabled: true
+
+# Correct usage of TimeZone methods in Rails
+Rails/TimeZone:
+ Enabled: true
+
+Rails/ActiveRecordCallbacksOrder:
+ Enabled: true
+
+Rails/FindById:
+ Enabled: true
+
+Rails/Inquiry:
+ Enabled: false
+
+Rails/MailerName:
+ Enabled: true
+
+Rails/MatchRoute:
+ Enabled: true
+
+Rails/NegateInclude:
+ Enabled: true
+
+Rails/Pluck:
+ Enabled: true
+
+Rails/PluckInWhere:
+ Enabled: true
+
+Rails/RenderInline:
+ Enabled: true
+
+Rails/RenderPlainText:
+ Enabled: true
+
+Rails/ShortI18n:
+ Enabled: true
+
+Rails/WhereExists:
+ Enabled: true
+
+Rails/AfterCommitOverride:
+ Enabled: true
+
+Rails/SquishedSQLHeredocs:
+ Enabled: true
+
+Rails/WhereNot:
+ Enabled: true
+
+Rails/SkipsModelValidations:
+ Enabled: false
+
+Rails/AttributeDefaultBlockValue: # (new in 2.9)
+ Enabled: true
+Rails/WhereEquals: # (new in 2.9)
+ Enabled: false
+
+Rails/HelperInstanceVariable:
+ Enabled: false
+
+Rails/UnknownEnv:
+ Environments:
+ - production
+ - development
+ - test
+ - staging
+ - heroku
diff --git a/Gemfile b/Gemfile
index 8d34326725..e29e04cee7 100644
--- a/Gemfile
+++ b/Gemfile
@@ -38,12 +38,14 @@ gem 'typhoeus'
gem "mongo", "~> 2"
gem 'aws-sdk', '~> 3'
gem 'kaminari'
+gem 'lockbox'
group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug', platforms: %i[mri mingw x64_mingw]
gem 'dotenv-rails'
gem 'rubocop', require: false
+ gem 'rubocop-rails'
end
group :development do
diff --git a/Gemfile.lock b/Gemfile.lock
index 4d17c0dae5..63a944e78f 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1238,6 +1238,7 @@ GEM
listen (3.5.1)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
+ lockbox (0.6.4)
lograge (0.11.2)
actionpack (>= 4)
activesupport (>= 4)
@@ -1270,7 +1271,7 @@ GEM
racc (~> 1.4)
os (1.1.1)
parallel (1.20.1)
- parser (3.0.1.0)
+ parser (3.0.1.1)
ast (~> 2.4.1)
pg (1.2.3)
public_suffix (4.0.6)
@@ -1321,17 +1322,21 @@ GEM
request_store (1.5.0)
rack (>= 1.4)
rexml (3.2.5)
- rubocop (1.13.0)
+ rubocop (1.15.0)
parallel (~> 1.10)
parser (>= 3.0.0.0)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml
- rubocop-ast (>= 1.2.0, < 2.0)
+ rubocop-ast (>= 1.5.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0)
- rubocop-ast (1.4.1)
- parser (>= 2.7.1.5)
+ rubocop-ast (1.7.0)
+ parser (>= 3.0.1.1)
+ rubocop-rails (2.10.1)
+ activesupport (>= 4.2.0)
+ rack (>= 1.1)
+ rubocop (>= 1.7.0, < 2.0)
ruby-progressbar (1.11.0)
ruby2_keywords (0.0.4)
signet (0.15.0)
@@ -1375,6 +1380,7 @@ DEPENDENCIES
jwt
kaminari
listen (~> 3.3)
+ lockbox
lograge
mongo (~> 2)
mysql2
@@ -1385,6 +1391,7 @@ DEPENDENCIES
rails (~> 6.1.3, >= 6.1.3.1)
redis
rubocop
+ rubocop-rails
simple_command
spring
typhoeus
diff --git a/app.json b/app.json
index 7348f800de..98b335a3d3 100644
--- a/app.json
+++ b/app.json
@@ -18,8 +18,8 @@
"description": "Public URL of ToolJet installtion.",
"value": "https://app.tooljet.io"
},
- "ENCRYPTION_SERVICE_SALT": {
- "description": "Salt for encrypting datasource credentials.",
+ "LOCKBOX_MASTER_KEY": {
+ "description": "Key for encrypting datasource credentials.",
"value": ""
},
"SECRET_KEY_BASE": {
diff --git a/app/controllers/app_users_controller.rb b/app/controllers/app_users_controller.rb
index c3031e773d..a6b04b18e0 100644
--- a/app/controllers/app_users_controller.rb
+++ b/app/controllers/app_users_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AppUsersController < ApplicationController
def create
org_user_id = params[:org_user_id]
@@ -18,7 +20,7 @@ class AppUsersController < ApplicationController
if app_user.save
render json: { success: true }
else
- render json: { message: 'Could not create user' }, status: 500
+ render json: { message: "Could not create user" }, status: :internal_server_error
end
end
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index f7e8eba59b..67ac9b663e 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ApplicationController < ActionController::API
include Pundit
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
@@ -7,12 +9,12 @@ class ApplicationController < ActionController::API
private
- def authenticate_request
- @current_user = AuthorizeApiRequest.call(request.headers).result
- render json: { error: 'Not Authorized' }, status: 401 unless @current_user
- end
+ def authenticate_request
+ @current_user = AuthorizeApiRequest.call(request.headers).result
+ render json: { error: "Not Authorized" }, status: :unauthorized unless @current_user
+ end
- def user_not_authorized
- render json: { error: 'Access denied' }, status: :forbidden
- end
+ def user_not_authorized
+ render json: { error: "Access denied" }, status: :forbidden
+ end
end
diff --git a/app/controllers/apps_controller.rb b/app/controllers/apps_controller.rb
index 1c2d73f380..7fd086beec 100644
--- a/app/controllers/apps_controller.rb
+++ b/app/controllers/apps_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AppsController < ApplicationController
skip_before_action :authenticate_request, only: [:show]
@@ -13,44 +15,44 @@ class AppsController < ApplicationController
@scope = @folder.apps
end
- @apps = @scope.order('created_at desc')
+ @apps = @scope.order("created_at desc")
.page(params[:page])
.per(10)
.includes(:user)
- @meta = {
+ @meta = {
total_pages: @apps.total_pages,
folder_count: @scope.count,
total_count: App.where(organization: @current_user.organization).count,
- current_page: @apps.current_page
- }
+ current_page: @apps.current_page
+ }
end
def create
authorize App
@app = App.create({
- name: 'Untitled app',
+ name: "Untitled app",
organization: @current_user.organization,
- current_version: AppVersion.new(name: 'v0'),
+ current_version: AppVersion.new(name: "v0"),
user: @current_user
})
- AppUser.create(app: @app, user: @current_user, role: 'admin')
+ AppUser.create(app: @app, user: @current_user, role: "admin")
end
def show
- @app = App.find params[:id]
+ @app = App.find params[:id]
- # Logic to bypass auth for public apps
- unless @app.is_public
- authenticate_request
- authorize @app
- end
+ # Logic to bypass auth for public apps
+ unless @app.is_public
+ authenticate_request
+ authorize @app
+ end
end
def update
@app = App.find params[:id]
authorize @app
- @app.update(params['app'].permit('name', 'current_version_id', 'is_public'))
+ @app.update(params["app"].permit("name", "current_version_id", "is_public"))
end
def users
diff --git a/app/controllers/authentication_controller.rb b/app/controllers/authentication_controller.rb
index 548837d622..a031cdb1b7 100644
--- a/app/controllers/authentication_controller.rb
+++ b/app/controllers/authentication_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AuthenticationController < ApplicationController
skip_before_action :authenticate_request
@@ -5,7 +7,7 @@ class AuthenticationController < ApplicationController
command = AuthenticateUser.call(params[:email], params[:password])
if command.success?
- user = User.find_by_email params[:email]
+ user = User.find_by email: params[:email]
render json: { auth_token: command.result, first_name: user.first_name, last_name: user.last_name,
email: user.email }
else
@@ -15,15 +17,15 @@ class AuthenticationController < ApplicationController
def signup
# Check if the installation allows user signups
- if(ENV['DISABLE_SIGNUPS'] === "true")
- render json: {}, status: 500
+ if (ENV["DISABLE_SIGNUPS"] === "true")
+ render json: {}, status: :internal_server_error
else
email = params[:email]
password = SecureRandom.uuid
- org = Organization.create(name: 'new org')
+ org = Organization.create(name: "new org")
user = User.create(email: email, password: password, organization: org, invitation_token: SecureRandom.uuid)
- org_user = OrganizationUser.create(user: user, organization: org, role: 'admin')
+ org_user = OrganizationUser.create(user_id: user.id, organization_id: org.id, role: "admin")
# UserMailer.with(user: user, sender: @current_user).new_signup_email.deliver if org_user.save
end
diff --git a/app/controllers/data_queries_controller.rb b/app/controllers/data_queries_controller.rb
index 35b6ee2493..86ebaf3721 100644
--- a/app/controllers/data_queries_controller.rb
+++ b/app/controllers/data_queries_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class DataQueriesController < ApplicationController
skip_before_action :authenticate_request, only: [:run]
@@ -15,11 +17,10 @@ class DataQueriesController < ApplicationController
)
if @data_query.errors.present?
- render json: { message: 'Query could not be created' }, status: 500
+ render json: { message: "Query could not be created" }, status: :internal_server_error
else
- render json: { message: 'success' }
+ render json: { message: "success" }
end
-
end
def update
@@ -27,9 +28,9 @@ class DataQueriesController < ApplicationController
@data_query.update(options: params[:options], name: params[:name])
if @data_query.errors.present?
- render json: { message: 'Query could not be updated' }, status: 500
- else
- render json: { message: 'success' }
+ render json: { message: "Query could not be updated" }, status: :internal_server_error
+ else
+ render json: { message: "success" }
end
end
diff --git a/app/controllers/data_sources_controller.rb b/app/controllers/data_sources_controller.rb
index 06e7167388..f3cc43dc82 100644
--- a/app/controllers/data_sources_controller.rb
+++ b/app/controllers/data_sources_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class DataSourcesController < ApplicationController
def index
@data_sources = DataSource.where(app_id: params[:app_id])
@@ -8,17 +10,17 @@ class DataSourcesController < ApplicationController
options_to_save = {}
options.each do |option|
- if option['encrypted']
- credential = Credential.create(value: option['value'])
+ if option["encrypted"]
+ credential = Credential.create(value: option["value"])
- options_to_save[option['key']] = {
+ options_to_save[option["key"]] = {
credential_id: credential.id,
- encrypted: option['encrypted']
+ encrypted: option["encrypted"]
}
else
- options_to_save[option['key']] = {
- value: option['value'],
- encrypted: option['encrypted']
+ options_to_save[option["key"]] = {
+ value: option["value"],
+ encrypted: option["encrypted"]
}
end
end
@@ -38,17 +40,17 @@ class DataSourcesController < ApplicationController
options_to_save = {}
options.each do |option|
- if option['encrypted']
- credential = Credential.create(value: option['value'])
+ if option["encrypted"]
+ credential = Credential.create(value: option["value"])
- options_to_save[option['key']] = {
+ options_to_save[option["key"]] = {
credential_id: credential.id,
- encrypted: option['encrypted']
+ encrypted: option["encrypted"]
}
else
- options_to_save[option['key']] = {
- value: option['value'],
- encrypted: option['encrypted']
+ options_to_save[option["key"]] = {
+ value: option["value"],
+ encrypted: option["encrypted"]
}
end
end
@@ -67,27 +69,27 @@ class DataSourcesController < ApplicationController
render json: { status: 200 }
rescue StandardError => e
puts e
- render json: { message: e }, status: 500
+ render json: { message: e }, status: :internal_server_error
end
def authorize_oauth2
data_source = DataSource.find params[:data_source_id]
options = CredentialService.new.decrypt_options(data_source.options)
- access_token_url = options['access_token_url']
+ access_token_url = options["access_token_url"]
- custom_params = options['custom_auth_params'].to_h
+ custom_params = options["custom_auth_params"].to_h
response = HTTParty.post(access_token_url,
body: { code: params[:code],
- client_id: options['client_id'],
- client_secret: options['client_secret'],
- grant_type: options['grant_type'],
+ client_id: options["client_id"],
+ client_secret: options["client_secret"],
+ grant_type: options["grant_type"],
redirect_uri: "#{ENV.fetch('TOOLJET_HOST')}/oauth2/authorize",
**custom_params }.to_json,
- headers: { 'Content-Type' => 'application/json' })
+ headers: { "Content-Type" => "application/json" })
result = JSON.parse(response.body)
- access_token = result['access_token']
+ access_token = result["access_token"]
options = { access_token: access_token }
@@ -108,20 +110,20 @@ class DataSourcesController < ApplicationController
render json: { url: url }
end
- private
- def fetch_oauth_options(options)
+ private
+ def fetch_oauth_options(options)
# Fetch necessary access token if OAuth2 based data source
- if options.find { |option| option['key'] == 'oauth2' }
- provider = options.find { |option| option['key'] === 'provider' } ['value']
+ if options.find { |option| option["key"] == "oauth2" }
+ provider = options.find { |option| option["key"] === "provider" } ["value"]
service_class = "#{provider.capitalize}OauthService".constantize
- access_info = service_class.fetch_access_token(options.find { |option| option['key'] === 'code' } ['value'])
- options.reject! { |option| option['key'] == 'code' }
+ access_info = service_class.fetch_access_token(options.find { |option| option["key"] === "code" } ["value"])
+ options.reject! { |option| option["key"] == "code" }
access_info.each do |info|
option = {}
- option['key'] = info[0]
- option['value'] = info[1]
- option['encrypted'] = true
+ option["key"] = info[0]
+ option["value"] = info[1]
+ option["encrypted"] = true
options << option
end
end
diff --git a/app/controllers/folder_apps_controller.rb b/app/controllers/folder_apps_controller.rb
index 3a80097cf8..b36a5525ed 100644
--- a/app/controllers/folder_apps_controller.rb
+++ b/app/controllers/folder_apps_controller.rb
@@ -1,22 +1,23 @@
+# frozen_string_literal: true
+
class FolderAppsController < ApplicationController
+ def create
+ app_id = params[:app_id]
+ folder_id = params[:folder_id]
- def create
- app_id = params[:app_id]
- folder_id = params[:folder_id]
-
- @app = App.find app_id
+ @app = App.find app_id
- unless AppPolicy.new(@current_user, @app).update?
- render json: { message: 'Could not add app to folder due to insufficient permissions' }, status: 500
- return
- end
-
- folder_app = FolderApp.new(app_id: app_id, folder_id: folder_id)
-
- if folder_app.save
- render json: {}
- else
- render json: { message: 'App already in folder' }, status: 500
- end
+ unless AppPolicy.new(@current_user, @app).update?
+ render json: { message: "Could not add app to folder due to insufficient permissions" }, status: :internal_server_error
+ return
end
+
+ folder_app = FolderApp.new(app_id: app_id, folder_id: folder_id)
+
+ if folder_app.save
+ render json: {}
+ else
+ render json: { message: "App already in folder" }, status: :internal_server_error
+ end
+ end
end
diff --git a/app/controllers/folders_controller.rb b/app/controllers/folders_controller.rb
index fbb0bda159..85eac7852d 100644
--- a/app/controllers/folders_controller.rb
+++ b/app/controllers/folders_controller.rb
@@ -1,10 +1,11 @@
+# frozen_string_literal: true
+
class FoldersController < ApplicationController
+ def index
+ @folders = Folder.where(organization: @current_user.organization)
+ end
- def index
- @folders = Folder.where(organization: @current_user.organization)
- end
-
- def create
- Folder.create(name: params[:name], organization: @current_user.organization)
- end
+ def create
+ Folder.create(name: params[:name], organization: @current_user.organization)
+ end
end
diff --git a/app/controllers/organization_users_controller.rb b/app/controllers/organization_users_controller.rb
index 908c059c05..a347a37219 100644
--- a/app/controllers/organization_users_controller.rb
+++ b/app/controllers/organization_users_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class OrganizationUsersController < ApplicationController
def create
authorize OrganizationUser
diff --git a/app/controllers/organizations_controller.rb b/app/controllers/organizations_controller.rb
index 8cd6f6efe9..1efeb0ee0c 100644
--- a/app/controllers/organizations_controller.rb
+++ b/app/controllers/organizations_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class OrganizationsController < ApplicationController
def users
@org_users = OrganizationUser.where(organization: @current_user.organization).includes(:user)
diff --git a/app/controllers/probe_controller.rb b/app/controllers/probe_controller.rb
index 1188d91895..68bf165ed8 100644
--- a/app/controllers/probe_controller.rb
+++ b/app/controllers/probe_controller.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
class ProbeController < ApplicationController
skip_before_action :authenticate_request
def health_check
- render json: { works: 'yeah' }
+ render json: { works: "yeah" }
end
end
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index e7083933a6..329f56f891 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class UsersController < ApplicationController
skip_before_action :authenticate_request
@@ -6,13 +8,13 @@ class UsersController < ApplicationController
if user
user.update(first_name: params[:first_name], last_name: params[:last_name], password: params[:password], invitation_token: nil)
- user.organization_users.first.update(status: 'active')
+ user.organization_users.first.update(status: "active")
if params[:new_signup]
user.organization.update(name: params[:organization])
end
else
- render json: { message: 'Invalid Invitation Token' }, status: :bad_request
+ render json: { message: "Invalid Invitation Token" }, status: :bad_request
end
end
end
diff --git a/app/controllers/versions_controller.rb b/app/controllers/versions_controller.rb
index 8a1aca4b04..d21dc224e9 100644
--- a/app/controllers/versions_controller.rb
+++ b/app/controllers/versions_controller.rb
@@ -1,12 +1,14 @@
+# frozen_string_literal: true
+
class VersionsController < ApplicationController
def create
@app = App.find params[:app_id]
- name = params[:version]['versionName']
+ name = params[:version]["versionName"]
AppVersion.create(app: @app, name: name)
end
def index
- @versions = AppVersion.where(app_id: params['app_id']).order('created_at desc')
+ @versions = AppVersion.where(app_id: params["app_id"]).order("created_at desc")
end
def update
diff --git a/app/models/app.rb b/app/models/app.rb
index c2cb9bde14..f38e633216 100644
--- a/app/models/app.rb
+++ b/app/models/app.rb
@@ -1,8 +1,10 @@
+# frozen_string_literal: true
+
class App < ApplicationRecord
belongs_to :organization
has_many :data_queries
has_many :app_users
has_many :app_versions
- belongs_to :current_version, class_name: 'AppVersion', optional: true
+ belongs_to :current_version, class_name: "AppVersion", optional: true
belongs_to :user, optional: true
end
diff --git a/app/models/app_user.rb b/app/models/app_user.rb
index dd828bfba8..1497d84e11 100644
--- a/app/models/app_user.rb
+++ b/app/models/app_user.rb
@@ -1,17 +1,19 @@
+# frozen_string_literal: true
+
class AppUser < ApplicationRecord
belongs_to :app
belongs_to :user
- validates_uniqueness_of :app_id, scope: [:user_id]
+ validates :app_id, uniqueness: { scope: [:user_id] }
def admin?
- role == 'admin'
+ role == "admin"
end
def developer?
- role == 'developer'
+ role == "developer"
end
def viewer?
- role == 'viewer'
+ role == "viewer"
end
end
diff --git a/app/models/app_version.rb b/app/models/app_version.rb
index d7b4b28edb..e263b610e0 100644
--- a/app/models/app_version.rb
+++ b/app/models/app_version.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AppVersion < ApplicationRecord
belongs_to :app
end
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index 10a4cba84d..71fbba5b32 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
diff --git a/app/models/credential.rb b/app/models/credential.rb
index 881f480596..f605f420e0 100644
--- a/app/models/credential.rb
+++ b/app/models/credential.rb
@@ -1,5 +1,5 @@
-class Credential < ApplicationRecord
- include Encryptable
+# frozen_string_literal: true
- attr_encrypted :value
+class Credential < ApplicationRecord
+ encrypts :value
end
diff --git a/app/models/data_query.rb b/app/models/data_query.rb
index 7e0200d113..cfbe886764 100644
--- a/app/models/data_query.rb
+++ b/app/models/data_query.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
class DataQuery < ApplicationRecord
belongs_to :app
belongs_to :data_source, optional: true
- validates_uniqueness_of :name, scope: [:app_id]
+ validates :name, uniqueness: { scope: [:app_id] }
end
diff --git a/app/models/data_source.rb b/app/models/data_source.rb
index 314a180f0a..adbcb35bf0 100644
--- a/app/models/data_source.rb
+++ b/app/models/data_source.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class DataSource < ApplicationRecord
belongs_to :app
has_many :data_queries
diff --git a/app/models/data_source_user_oauth2.rb b/app/models/data_source_user_oauth2.rb
index acb0802a3d..17b25f9e61 100644
--- a/app/models/data_source_user_oauth2.rb
+++ b/app/models/data_source_user_oauth2.rb
@@ -1,8 +1,8 @@
-class DataSourceUserOauth2 < ApplicationRecord
- include Encryptable
+# frozen_string_literal: true
+class DataSourceUserOauth2 < ApplicationRecord
belongs_to :user
belongs_to :data_source
- attr_encrypted :options
+ encrypts :options
end
diff --git a/app/models/folder.rb b/app/models/folder.rb
index dcb9b34bc0..26c9cc6026 100644
--- a/app/models/folder.rb
+++ b/app/models/folder.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
class Folder < ApplicationRecord
belongs_to :organization
has_many :folder_apps
- has_many :apps, :through => :folder_apps
+ has_many :apps, through: :folder_apps
end
diff --git a/app/models/folder_app.rb b/app/models/folder_app.rb
index c11ad7e671..f40fb36c13 100644
--- a/app/models/folder_app.rb
+++ b/app/models/folder_app.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
class FolderApp < ApplicationRecord
belongs_to :folder
belongs_to :app
- validates_uniqueness_of :app_id, scope: [:folder_id]
+ validates :app_id, uniqueness: { scope: [:folder_id] }
end
diff --git a/app/models/organization.rb b/app/models/organization.rb
index 56cf9c7066..6161c412d3 100644
--- a/app/models/organization.rb
+++ b/app/models/organization.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Organization < ApplicationRecord
has_many :users
has_many :apps
diff --git a/app/models/organization_user.rb b/app/models/organization_user.rb
index d7b603fe3f..4ce4936689 100644
--- a/app/models/organization_user.rb
+++ b/app/models/organization_user.rb
@@ -1,16 +1,18 @@
+# frozen_string_literal: true
+
class OrganizationUser < ApplicationRecord
belongs_to :organization
belongs_to :user
def admin?
- role == 'admin'
+ role == "admin"
end
def developer?
- role == 'developer'
+ role == "developer"
end
def viewer?
- role == 'viewer'
+ role == "viewer"
end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 6813108df5..2e9bf3d236 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class User < ApplicationRecord
has_secure_password
has_many :organization_users
diff --git a/app/services/airtable_query_service.rb b/app/services/airtable_query_service.rb
new file mode 100644
index 0000000000..442b216880
--- /dev/null
+++ b/app/services/airtable_query_service.rb
@@ -0,0 +1,68 @@
+class AirtableQueryService
+ attr_accessor :query, :source, :options, :source_options, :current_user
+
+ def initialize(data_query, data_source, options, source_options, current_user)
+ @query = data_query
+ @source = data_source
+ @options = options
+ @source_options = source_options
+ @current_user = current_user
+ end
+
+ def process
+ operation = options['operation']
+ api_key = source_options['api_key']
+ error = false
+
+ if operation === 'list_records'
+
+ base_id = options['base_id']
+ table_name = options['table_name']
+ page_size = options['page_size']
+ offset = options['offset']
+
+ result = list_records(api_key, base_id, table_name, page_size, offset)
+
+ data = result
+ error = result.code != 200
+ end
+
+ if operation === 'retrieve_record'
+
+ base_id = options['base_id']
+ table_name = options['table_name']
+ record_id = options['record_id']
+
+ result = retrieve_record(api_key, base_id, table_name, record_id)
+
+ data = result
+ error = result.code != 200
+ end
+
+ if error
+ { status: 'error', code: 500, message: data["message"], data: data }
+ else
+ { status: 'success', data: data }
+ end
+ end
+
+ private
+
+ def list_records(api_key, base_id, table_name, page_size, offset)
+
+ result = HTTParty.get(URI.encode("https://api.airtable.com/v0/#{base_id}/#{table_name}"),
+ headers: { 'Content-Type':
+ 'application/json', "Authorization": "Bearer #{api_key}" })
+
+ result
+ end
+
+ def retrieve_record(api_key, base_id, table_name, record_id)
+
+ result = HTTParty.get(URI.encode("https://api.airtable.com/v0/#{base_id}/#{table_name}/#{record_id}"),
+ headers: { 'Content-Type':
+ 'application/json', "Authorization": "Bearer #{api_key}" })
+
+ result
+ end
+end
diff --git a/app/services/elasticsearch_query_service.rb b/app/services/elasticsearch_query_service.rb
index f332d7ff9f..e6ce06b701 100644
--- a/app/services/elasticsearch_query_service.rb
+++ b/app/services/elasticsearch_query_service.rb
@@ -13,10 +13,23 @@ class ElasticsearchQueryService
end
def self.connection options
+
+ scheme = options.dig('scheme', 'value')
+ host = options.dig('host', 'value')
+ port = options.dig('port', 'value')
+ username = options.dig('username', 'value')
+ password = options.dig('password', 'value')
+
+ unless username.blank? || password.blank?
+ url = "#{scheme}://#{username}:#{password}@#{host}:#{port}"
+ else
+ url = "#{scheme}://#{host}:#{port}"
+ end
+
client = Elasticsearch::Client.new(
- url: "#{options.dig('host', 'value')}:#{options.dig('port', 'value')}",
+ url: url,
retry_on_failure: 5,
- request_timeout: 30,
+ request_timeout: 15,
adapter: :typhoeus
)
@@ -71,10 +84,23 @@ class ElasticsearchQueryService
private
def create_connection
+
+ scheme = source_options['scheme']
+ host = source_options['host']
+ port = source_options['port']
+ username = source_options['username']
+ password = source_options['password']
+
+ unless username.blank? || password.blank?
+ url = "#{scheme}://#{username}:#{password}@#{host}:#{port}"
+ else
+ url = "#{scheme}://#{host}:#{port}"
+ end
+
connection = Elasticsearch::Client.new(
- url: "#{source_options['host']}:#{source_options['port']}",
+ url: url,
retry_on_failure: 5,
- request_timeout: 30,
+ request_timeout: 15,
adapter: :typhoeus
)
diff --git a/app/services/encryption_service.rb b/app/services/encryption_service.rb
deleted file mode 100644
index a639376769..0000000000
--- a/app/services/encryption_service.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-class EncryptionService
- KEY = ActiveSupport::KeyGenerator.new(
- ENV.fetch('SECRET_KEY_BASE')
- ).generate_key(
- ENV.fetch('ENCRYPTION_SERVICE_SALT'),
- ActiveSupport::MessageEncryptor.key_len
- ).freeze
-
- private_constant :KEY
-
- delegate :encrypt_and_sign, :decrypt_and_verify, to: :encryptor
-
- def self.encrypt(value)
- new.encrypt_and_sign(value)
- end
-
- def self.decrypt(value)
- new.decrypt_and_verify(value)
- end
-
- private
-
- def encryptor
- ActiveSupport::MessageEncryptor.new(KEY)
- end
-end
diff --git a/app/services/postgresql_query_service.rb b/app/services/postgresql_query_service.rb
index cb33c528a2..823505d8cc 100644
--- a/app/services/postgresql_query_service.rb
+++ b/app/services/postgresql_query_service.rb
@@ -39,7 +39,10 @@ class PostgresqlQueryService
result = connection.exec(query_text)
rescue StandardError => e
- reset_connection(data_source) if connection.finished?
+ if connection&.status === PG::Constants::CONNECTION_BAD
+ connection&.finish
+ reset_connection(data_source)
+ end
puts e
error = { message: e.message }
diff --git a/config/initializers/lockbox.rb b/config/initializers/lockbox.rb
new file mode 100644
index 0000000000..368ac3a2d1
--- /dev/null
+++ b/config/initializers/lockbox.rb
@@ -0,0 +1 @@
+Lockbox.master_key = ENV.fetch('LOCKBOX_MASTER_KEY')
diff --git a/db/migrate/20210529101316_add_cipher_text_to_credentials.rb b/db/migrate/20210529101316_add_cipher_text_to_credentials.rb
new file mode 100644
index 0000000000..03d213f5d0
--- /dev/null
+++ b/db/migrate/20210529101316_add_cipher_text_to_credentials.rb
@@ -0,0 +1,5 @@
+class AddCipherTextToCredentials < ActiveRecord::Migration[6.1]
+ def change
+ add_column :credentials, :value_ciphertext, :text
+ end
+end
diff --git a/db/migrate/20210529140113_add_cipher_text_to_data_source_user_oauth2.rb b/db/migrate/20210529140113_add_cipher_text_to_data_source_user_oauth2.rb
new file mode 100644
index 0000000000..e7e9984f01
--- /dev/null
+++ b/db/migrate/20210529140113_add_cipher_text_to_data_source_user_oauth2.rb
@@ -0,0 +1,5 @@
+class AddCipherTextToDataSourceUserOauth2 < ActiveRecord::Migration[6.1]
+ def change
+ add_column :data_source_user_oauth2s, :options_ciphertext, :text
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 662c957c04..f57a864cda 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,14 +10,14 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2021_05_19_084741) do
+ActiveRecord::Schema.define(version: 2021_05_29_140113) do
# These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto"
enable_extension "plpgsql"
enable_extension "uuid-ossp"
- create_table "app_users", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ create_table "app_users", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t|
t.uuid "app_id", null: false
t.uuid "user_id", null: false
t.string "role"
@@ -27,7 +27,7 @@ ActiveRecord::Schema.define(version: 2021_05_19_084741) do
t.index ["user_id"], name: "index_app_users_on_user_id"
end
- create_table "app_versions", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ create_table "app_versions", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t|
t.uuid "app_id", null: false
t.string "name"
t.json "definition"
@@ -36,7 +36,7 @@ ActiveRecord::Schema.define(version: 2021_05_19_084741) do
t.index ["app_id"], name: "index_app_versions_on_app_id"
end
- create_table "apps", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ create_table "apps", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t|
t.string "name"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
@@ -50,13 +50,14 @@ ActiveRecord::Schema.define(version: 2021_05_19_084741) do
t.index ["user_id"], name: "index_apps_on_user_id"
end
- create_table "credentials", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ create_table "credentials", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t|
t.text "encrypted_value"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
+ t.text "value_ciphertext"
end
- create_table "data_queries", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ create_table "data_queries", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t|
t.uuid "app_id", null: false
t.string "name"
t.json "options"
@@ -68,17 +69,18 @@ ActiveRecord::Schema.define(version: 2021_05_19_084741) do
t.index ["data_source_id"], name: "index_data_queries_on_data_source_id"
end
- create_table "data_source_user_oauth2s", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ create_table "data_source_user_oauth2s", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t|
t.uuid "user_id", null: false
t.uuid "data_source_id", null: false
t.text "encrypted_options"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
+ t.text "options_ciphertext"
t.index ["data_source_id"], name: "index_data_source_user_oauth2s_on_data_source_id"
t.index ["user_id"], name: "index_data_source_user_oauth2s_on_user_id"
end
- create_table "data_sources", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ create_table "data_sources", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t|
t.uuid "app_id", null: false
t.string "name"
t.json "options"
@@ -88,7 +90,7 @@ ActiveRecord::Schema.define(version: 2021_05_19_084741) do
t.index ["app_id"], name: "index_data_sources_on_app_id"
end
- create_table "endpoints", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ create_table "endpoints", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t|
t.string "identifier"
t.string "path"
t.string "method"
@@ -101,7 +103,7 @@ ActiveRecord::Schema.define(version: 2021_05_19_084741) do
t.index ["integration_id"], name: "index_endpoints_on_integration_id"
end
- create_table "folder_apps", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ create_table "folder_apps", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t|
t.uuid "folder_id", null: false
t.uuid "app_id", null: false
t.datetime "created_at", precision: 6, null: false
@@ -110,7 +112,7 @@ ActiveRecord::Schema.define(version: 2021_05_19_084741) do
t.index ["folder_id"], name: "index_folder_apps_on_folder_id"
end
- create_table "folders", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ create_table "folders", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t|
t.string "name"
t.uuid "organization_id", null: false
t.datetime "created_at", precision: 6, null: false
@@ -118,14 +120,14 @@ ActiveRecord::Schema.define(version: 2021_05_19_084741) do
t.index ["organization_id"], name: "index_folders_on_organization_id"
end
- create_table "integrations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ create_table "integrations", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t|
t.string "identifier"
t.string "name"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
- create_table "organization_users", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ create_table "organization_users", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t|
t.uuid "organization_id", null: false
t.uuid "user_id", null: false
t.string "role"
@@ -136,14 +138,14 @@ ActiveRecord::Schema.define(version: 2021_05_19_084741) do
t.index ["user_id"], name: "index_organization_users_on_user_id"
end
- create_table "organizations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ create_table "organizations", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t|
t.string "name"
t.string "domain"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
- create_table "users", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ create_table "users", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t|
t.string "first_name"
t.string "last_name"
t.string "email"
diff --git a/deploy/kubernetes/server-deployment.yaml b/deploy/kubernetes/server-deployment.yaml
index 50edc3116a..d6bd3a091e 100644
--- a/deploy/kubernetes/server-deployment.yaml
+++ b/deploy/kubernetes/server-deployment.yaml
@@ -3,7 +3,7 @@ kind: Deployment
metadata:
name: tooljet-server-deployment
spec:
- replicas: 1
+ replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
@@ -24,11 +24,11 @@ spec:
command: ["bundle", "exec", "rails", "s", "-p", "3000", "-b", "0.0.0.0"]
resources:
limits:
- memory: "1024Mi"
- cpu: "1000m"
+ memory: "3000Mi"
+ cpu: "500m"
requests:
- memory: "1024Mi"
- cpu: "1000m"
+ memory: "3000Mi"
+ cpu: "500m"
ports:
- containerPort: 3000
readinessProbe:
@@ -62,16 +62,16 @@ spec:
secretKeyRef:
name: server
key: db
+ - name: LOCKBOX_MASTER_KEY
+ valueFrom:
+ secretKeyRef:
+ name: server
+ key: lockbox_key
- name: SECRET_KEY_BASE
valueFrom:
secretKeyRef:
name: server
key: secret_key_base
- - name: ENCRYPTION_SERVICE_SALT
- valueFrom:
- secretKeyRef:
- name: server
- key: encryption_salt
- imagePullSecrets:
+ imagePullSecrets:
- name: registry-secret
\ No newline at end of file
diff --git a/docs/docs/tutorial/adding-widget.md b/docs/docs/tutorial/adding-widget.md
index 8ecff40b27..81936eaa19 100644
--- a/docs/docs/tutorial/adding-widget.md
+++ b/docs/docs/tutorial/adding-widget.md
@@ -5,8 +5,7 @@ sidebar_position: 5
# Adding a widget
To add a widget, navigate to the `insert` tab of right sidebar. It will display the list of built-in widgets that can be added to the app. Use the search functionality to quickly find the widget that you want.
-
-
+
## Drag and drop a widget
Let's add a `table` widget to the app to show the customer data from the query that we created in the previous steps.
@@ -32,4 +31,8 @@ Since we have already run the query in previous step, the data will be immedietl
-So far in this tutorial, we have connected to a PostgreSQL database and displayed the data on a table.
\ No newline at end of file
+So far in this tutorial, we have connected to a PostgreSQL database and displayed the data on a table.
+
+:::tip
+Read the widget reference of table [here](/docs/widgets/table) for more customizations such as server-side pagination, actions, editing data.
+:::
\ No newline at end of file
diff --git a/docs/docs/widgets/chart.md b/docs/docs/widgets/chart.md
new file mode 100644
index 0000000000..16fbfa56c5
--- /dev/null
+++ b/docs/docs/widgets/chart.md
@@ -0,0 +1,70 @@
+---
+sidebar_position: 1
+---
+
+# Chart
+
+Chart widget takes the chart type, data and styles to draw charts using Plotly.js.
+
+Support chart types:
+- Line charts
+- Bar charts
+- Pie charts
+
+## Line charts
+
+Data requirements:
+
+The data needs to be an array of objects and each object should have `x` and `y` keys.
+
+Example:
+
+```
+[
+ { "x": 100, "y": "Jan"},
+ { "x": 80, "y": "Feb"},
+ { "x": 40, "y": "Mar"}
+]
+```
+
+The chart will look like this:
+
+
+## Bar charts
+
+Data requirements:
+
+The data needs to be an array of objects and each object should have `x` and `y` keys.
+
+Example:
+
+```
+[
+ { "x": 100, "y": "Jan"},
+ { "x": 80, "y": "Feb"},
+ { "x": 40, "y": "Mar"}
+]
+```
+
+The chart will look like this:
+
+
+
+## Pie charts
+
+Data requirements:
+
+The data needs to be an array of objects and each object should have `label` and `value` keys.
+
+Example:
+
+```
+[
+ { "label": "Jan", "value": 100 },
+ { "label": "Feb", "value": 80 },
+ { "label": "Mar", "value": 20 }
+]
+```
+
+The chart will look like this:
+
diff --git a/docs/docs/widgets/table.md b/docs/docs/widgets/table.md
index 09c8184888..568647f7b7 100644
--- a/docs/docs/widgets/table.md
+++ b/docs/docs/widgets/table.md
@@ -3,3 +3,90 @@ sidebar_position: 1
---
# Table
+
+Tables can be used for both displaying and editing data.
+
+
+
+## Displaying Data
+
+The data object should be an array of objects. Table columns can be added, removed, rearranged from the inspector. `key` property is the accessor key used to get data from a single element of table data object. For example:
+
+If the table data is:
+
+```
+[
+ {
+ "review": {
+ "title": "An app review"
+ },
+ "user": {
+ "name": "sam",
+ "email": "sam@example.com"
+ },
+ }
+]
+```
+
+To display email column, the key for the column should be `user.email`.
+
+## Cell data types
+
+- String ( Default )
+- Text
+- Badge - can be used to display and edit predefined badges such as status of shipment.
+- Multiple badges
+- Tags - similar to badges but the values are not predefined.
+- Dropdown
+- Multiselect dropdown
+
+## Client-side pagination
+
+Client-side pagination is enabled by default. The number of records per page is 10 by default and can be changed to upto 50.
+
+## Server-side pagination
+
+Server-side pagination can be used to run a query whenever the page is changed. Go to events section of the inspector and change the action for `on page changed` event. Number of records per page needs to be handled in your query. If server-side pagination is enabled, `pageIndex` property will be exposed on the table object, this property will have the current page index. `pageIndex` can be used to query the next set of results when page is changed.
+
+## Search
+Client-side search is enabled by default and server-side search can be enabled from the events section of the inspector. Whenever the search text is changed, the `searchText` property of the table component is updated. If server-side search is enabled, `on search` event is fired after the content of `searchText` property is changed. `searchText` can be used to run a specific query to search for the records in your datasource.
+
+## Event: On row clicked
+
+This event is triggered when a table row is clicked. `selectedRow` property of the table object will have the table data of the selected row.
+
+## Actions
+Actions are buttons that will be displayed as the last column of the table. The styles of these buttons can be customised and `on click` actions can be configured. when clicked, `selectedRow` property of the table will have the table data of the row.
+
+## Property: Loading state (Boolean)
+Loading state shows a loading skeleton for the table. This property can be used to show a loading status on the table while data is being loaded. `isLoading` property of a query can be used to get the status of a query.
+
+## Saving data
+Enable `editable` property of a column to make the cells editable. If a data type is not selected, `string` is selected as the data type.
+
+If the data in a cell is changed, `changeSet` property of the table object will have the index of the row and the field that changed.
+For example, if the name field of second row of example in the 'Displaying Data' section is changed, `changeSet` will look like this:
+
+```
+{
+ 2: {
+ "name": "new name"
+ }
+}
+```
+
+Along with `changeSet`, `dataUpdates` property will also be changed when the value of a cell changes. `dataUpdates` will have the whole data of the changed index from the table data. `dataUpdates` will look like this for our example:
+
+```
+[{
+ "review": {
+ "title": "An app review"
+ },
+ "user": {
+ "name": "new name",
+ "email": "sam@example.com"
+ },
+}]
+```
+
+If the data of a cell is changed, "save changes" button will be shown at the bottom of the table. This button when clicked will trigger the `Bulk update query` event. This event can be used to run a query to update the data on your datasource.
\ No newline at end of file
diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css
index 601532f888..b08abb9302 100644
--- a/docs/src/css/custom.css
+++ b/docs/src/css/custom.css
@@ -154,3 +154,7 @@ body {
strong {
color: #3c92dc;
}
+
+.alert a {
+ color: inherit;
+}
diff --git a/docs/static/img/widgets/chart/bar.png b/docs/static/img/widgets/chart/bar.png
new file mode 100644
index 0000000000..7cc51187b7
Binary files /dev/null and b/docs/static/img/widgets/chart/bar.png differ
diff --git a/docs/static/img/widgets/chart/line.png b/docs/static/img/widgets/chart/line.png
new file mode 100644
index 0000000000..d2e5be680c
Binary files /dev/null and b/docs/static/img/widgets/chart/line.png differ
diff --git a/docs/static/img/widgets/chart/pie.png b/docs/static/img/widgets/chart/pie.png
new file mode 100644
index 0000000000..717976fb3b
Binary files /dev/null and b/docs/static/img/widgets/chart/pie.png differ
diff --git a/docs/static/img/widgets/table/adding.gif b/docs/static/img/widgets/table/adding.gif
new file mode 100644
index 0000000000..a4e3422809
Binary files /dev/null and b/docs/static/img/widgets/table/adding.gif differ
diff --git a/frontend/assets/images/icons/app-menu.svg b/frontend/assets/images/icons/app-menu.svg
new file mode 100644
index 0000000000..bcf35bcd78
--- /dev/null
+++ b/frontend/assets/images/icons/app-menu.svg
@@ -0,0 +1,47 @@
+
+
+
+
diff --git a/frontend/assets/images/icons/apps.svg b/frontend/assets/images/icons/apps.svg
new file mode 100644
index 0000000000..71042a2765
--- /dev/null
+++ b/frontend/assets/images/icons/apps.svg
@@ -0,0 +1,10 @@
+
+
\ No newline at end of file
diff --git a/frontend/assets/images/icons/copy.svg b/frontend/assets/images/icons/copy.svg
new file mode 100644
index 0000000000..9d9da36a32
--- /dev/null
+++ b/frontend/assets/images/icons/copy.svg
@@ -0,0 +1,45 @@
+
+
+
diff --git a/frontend/assets/images/icons/download.svg b/frontend/assets/images/icons/download.svg
new file mode 100644
index 0000000000..3bccdb6e60
--- /dev/null
+++ b/frontend/assets/images/icons/download.svg
@@ -0,0 +1,42 @@
+
+
+
diff --git a/frontend/assets/images/icons/edit-source.svg b/frontend/assets/images/icons/edit-source.svg
new file mode 100644
index 0000000000..3137142c4c
--- /dev/null
+++ b/frontend/assets/images/icons/edit-source.svg
@@ -0,0 +1,81 @@
+
+
+
diff --git a/frontend/assets/images/icons/edit.svg b/frontend/assets/images/icons/edit.svg
new file mode 100644
index 0000000000..3137142c4c
--- /dev/null
+++ b/frontend/assets/images/icons/edit.svg
@@ -0,0 +1,81 @@
+
+
+
diff --git a/frontend/assets/images/icons/editor/datasources/airtable.svg b/frontend/assets/images/icons/editor/datasources/airtable.svg
new file mode 100644
index 0000000000..e670715f02
--- /dev/null
+++ b/frontend/assets/images/icons/editor/datasources/airtable.svg
@@ -0,0 +1,21 @@
+
+
+
diff --git a/frontend/assets/images/icons/editor/datasources/dynamodb.svg b/frontend/assets/images/icons/editor/datasources/dynamodb.svg
new file mode 100644
index 0000000000..b8f0d359e7
--- /dev/null
+++ b/frontend/assets/images/icons/editor/datasources/dynamodb.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/assets/images/icons/editor/datasources/elasticsearch.svg b/frontend/assets/images/icons/editor/datasources/elasticsearch.svg
new file mode 100644
index 0000000000..cb5b7214c8
--- /dev/null
+++ b/frontend/assets/images/icons/editor/datasources/elasticsearch.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/assets/images/icons/editor/datasources/firestore.svg b/frontend/assets/images/icons/editor/datasources/firestore.svg
new file mode 100644
index 0000000000..c1d7dbb42f
--- /dev/null
+++ b/frontend/assets/images/icons/editor/datasources/firestore.svg
@@ -0,0 +1,13 @@
+
diff --git a/frontend/assets/images/icons/editor/datasources/googlesheets.svg b/frontend/assets/images/icons/editor/datasources/googlesheets.svg
new file mode 100644
index 0000000000..bd5d938c78
--- /dev/null
+++ b/frontend/assets/images/icons/editor/datasources/googlesheets.svg
@@ -0,0 +1,89 @@
+
+
\ No newline at end of file
diff --git a/frontend/assets/images/icons/editor/datasources/mongodb.svg b/frontend/assets/images/icons/editor/datasources/mongodb.svg
new file mode 100644
index 0000000000..764ccf588f
--- /dev/null
+++ b/frontend/assets/images/icons/editor/datasources/mongodb.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/assets/images/icons/editor/datasources/mysql.svg b/frontend/assets/images/icons/editor/datasources/mysql.svg
new file mode 100644
index 0000000000..f13b56a5d2
--- /dev/null
+++ b/frontend/assets/images/icons/editor/datasources/mysql.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/assets/images/icons/editor/datasources/postgresql.svg b/frontend/assets/images/icons/editor/datasources/postgresql.svg
new file mode 100644
index 0000000000..1fc0846bdd
--- /dev/null
+++ b/frontend/assets/images/icons/editor/datasources/postgresql.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/assets/images/icons/editor/datasources/redis.svg b/frontend/assets/images/icons/editor/datasources/redis.svg
new file mode 100644
index 0000000000..30a1498ac2
--- /dev/null
+++ b/frontend/assets/images/icons/editor/datasources/redis.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/assets/images/icons/editor/datasources/restapi.svg b/frontend/assets/images/icons/editor/datasources/restapi.svg
new file mode 100644
index 0000000000..0975234f8e
--- /dev/null
+++ b/frontend/assets/images/icons/editor/datasources/restapi.svg
@@ -0,0 +1,84 @@
+
+
+
diff --git a/frontend/assets/images/icons/editor/datasources/slack.svg b/frontend/assets/images/icons/editor/datasources/slack.svg
new file mode 100644
index 0000000000..0e925dc209
--- /dev/null
+++ b/frontend/assets/images/icons/editor/datasources/slack.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/assets/images/icons/editor/datasources/stripe.svg b/frontend/assets/images/icons/editor/datasources/stripe.svg
new file mode 100644
index 0000000000..37b894f9e8
--- /dev/null
+++ b/frontend/assets/images/icons/editor/datasources/stripe.svg
@@ -0,0 +1,24 @@
+
+
+
diff --git a/frontend/assets/images/icons/editor/rearrange.svg b/frontend/assets/images/icons/editor/rearrange.svg
new file mode 100644
index 0000000000..9e2ad27958
--- /dev/null
+++ b/frontend/assets/images/icons/editor/rearrange.svg
@@ -0,0 +1,17 @@
+
+
\ No newline at end of file
diff --git a/frontend/assets/images/icons/filter.svg b/frontend/assets/images/icons/filter.svg
new file mode 100644
index 0000000000..b58c2ccf55
--- /dev/null
+++ b/frontend/assets/images/icons/filter.svg
@@ -0,0 +1,41 @@
+
+
+
diff --git a/frontend/assets/images/icons/insert.svg b/frontend/assets/images/icons/insert.svg
new file mode 100644
index 0000000000..594ff51e5a
--- /dev/null
+++ b/frontend/assets/images/icons/insert.svg
@@ -0,0 +1,61 @@
+
+
+
diff --git a/frontend/assets/images/icons/lens.svg b/frontend/assets/images/icons/lens.svg
new file mode 100644
index 0000000000..260b0beb0c
--- /dev/null
+++ b/frontend/assets/images/icons/lens.svg
@@ -0,0 +1,9 @@
+
+
+
diff --git a/frontend/assets/images/icons/maximize.svg b/frontend/assets/images/icons/maximize.svg
new file mode 100644
index 0000000000..9cef2e5450
--- /dev/null
+++ b/frontend/assets/images/icons/maximize.svg
@@ -0,0 +1,11 @@
+
+
\ No newline at end of file
diff --git a/frontend/assets/images/icons/menu.svg b/frontend/assets/images/icons/menu.svg
new file mode 100644
index 0000000000..a9dcdac5e8
--- /dev/null
+++ b/frontend/assets/images/icons/menu.svg
@@ -0,0 +1,55 @@
+
+
+
diff --git a/frontend/assets/images/icons/minimize.svg b/frontend/assets/images/icons/minimize.svg
new file mode 100644
index 0000000000..ff8d2e51c5
--- /dev/null
+++ b/frontend/assets/images/icons/minimize.svg
@@ -0,0 +1,11 @@
+
+
\ No newline at end of file
diff --git a/frontend/assets/images/icons/padlock.svg b/frontend/assets/images/icons/padlock.svg
new file mode 100644
index 0000000000..1f4b9bf2f8
--- /dev/null
+++ b/frontend/assets/images/icons/padlock.svg
@@ -0,0 +1,52 @@
+
+
+
diff --git a/frontend/assets/images/icons/trash.svg b/frontend/assets/images/icons/trash.svg
new file mode 100644
index 0000000000..bdc0261174
--- /dev/null
+++ b/frontend/assets/images/icons/trash.svg
@@ -0,0 +1,51 @@
+
+
+
diff --git a/frontend/assets/images/icons/users.svg b/frontend/assets/images/icons/users.svg
new file mode 100644
index 0000000000..10e85199ba
--- /dev/null
+++ b/frontend/assets/images/icons/users.svg
@@ -0,0 +1,58 @@
+
+
+
diff --git a/frontend/assets/images/icons/widgets/modal.png b/frontend/assets/images/icons/widgets/modal.png
new file mode 100644
index 0000000000..8ed307ba3e
Binary files /dev/null and b/frontend/assets/images/icons/widgets/modal.png differ
diff --git a/frontend/assets/images/icons/zoom-in.svg b/frontend/assets/images/icons/zoom-in.svg
new file mode 100644
index 0000000000..6b79c40cc3
--- /dev/null
+++ b/frontend/assets/images/icons/zoom-in.svg
@@ -0,0 +1,43 @@
+
+
+
diff --git a/frontend/assets/images/icons/zoom-out.svg b/frontend/assets/images/icons/zoom-out.svg
new file mode 100644
index 0000000000..48ff0ebd75
--- /dev/null
+++ b/frontend/assets/images/icons/zoom-out.svg
@@ -0,0 +1,42 @@
+
+
+
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 465cadcdfb..5e6c90faf5 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -13710,6 +13710,23 @@
"react-draggable": "^4.0.3"
}
},
+ "react-rnd": {
+ "version": "10.3.0",
+ "resolved": "https://registry.npmjs.org/react-rnd/-/react-rnd-10.3.0.tgz",
+ "integrity": "sha512-v+0TRPIaRWY25TYv02vLQHYpACbkX+4xKvsyIrUEy4bMpq0bP1oEiaxTorp0Xn72IVv0QZV1vOnZimgTEB/skw==",
+ "requires": {
+ "re-resizable": "6.9.0",
+ "react-draggable": "4.4.3",
+ "tslib": "2.2.0"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
+ "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w=="
+ }
+ }
+ },
"react-router": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index bc48caf87d..bd1ad6bb3c 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -38,6 +38,7 @@
"react-loading-skeleton": "^2.2.0",
"react-plotly.js": "^2.5.1",
"react-resizable": "^1.11.1",
+ "react-rnd": "^10.3.0",
"react-router-dom": "^5.0.0",
"react-scripts": "3.4.3",
"react-select-search": "^3.0.5",
@@ -46,6 +47,7 @@
"react-toastify": "^7.0.3",
"react-tooltip": "^4.2.18",
"rxjs": "^6.3.3",
+ "tinycolor2": "^1.4.2",
"yup": "^0.27.0"
},
"devDependencies": {
diff --git a/frontend/src/Editor/Box.jsx b/frontend/src/Editor/Box.jsx
index 5fb22b77da..7ff034c3e9 100644
--- a/frontend/src/Editor/Box.jsx
+++ b/frontend/src/Editor/Box.jsx
@@ -56,7 +56,7 @@ export const Box = function Box({
const backgroundColor = yellow ? 'yellow' : '';
let styles = {
- cursor: mode === 'edit' ? 'move' : ''
+
};
if (inCanvas) {
@@ -85,7 +85,7 @@ export const Box = function Box({
containerProps={containerProps}
>
) : (
-