Merge branch 'release/v0.5.13' into main

This commit is contained in:
navaneeth 2021-07-05 20:00:24 +05:30
commit f279d732fc
130 changed files with 1384 additions and 749 deletions

View file

@ -13,7 +13,7 @@ ToolJet is an **open-source no-code framework** to build and deploy internal too
<p align="center">
<kbd>
<img src="https://user-images.githubusercontent.com/7828962/120830570-4211a000-c57c-11eb-97f5-a650b177a082.png" />
<img src="https://user-images.githubusercontent.com/7828962/124362436-6aabb900-dc52-11eb-8459-2525adfd1b3d.gif" />
</kbd>
</p>
@ -21,7 +21,8 @@ ToolJet is an **open-source no-code framework** to build and deploy internal too
## Features
- Visual app builder with widgets such as tables, charts, modals, buttons, dropdowns and more
- Mobile & desktop layouts
- Mobile 📱 & desktop layouts 🖥
- Dark mode 🌛
- Connect to databases, APIs and external services
- Deploy on-premise ( supports docker, kubernetes, heroku and more )
- Granular access control on organization level and app level

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
module ApplicationCable
class Channel < ActionCable::Channel::Base
end

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
module ApplicationCable
class Connection < ActionCable::Connection::Base
end

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
class AuthenticateUser
prepend SimpleCommand
@ -20,7 +22,7 @@ class AuthenticateUser
return user if user && user.authenticate(password) && org_user.active?
errors.add :user_authentication, 'invalid credentials'
errors.add :user_authentication, "invalid credentials"
nil
end
end

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
class AuthorizeApiRequest
prepend SimpleCommand
@ -15,12 +17,12 @@ class AuthorizeApiRequest
def user
@user ||= User.find(decoded_auth_token[:user_id]) if decoded_auth_token
@user || errors.add(:token, 'Invalid token') && nil
@user || errors.add(:token, "Invalid token") && nil
org_user = OrganizationUser.where(user: @user, organization: @user.organization)&.first
@user = nil unless org_user.active?
@user || errors.add(:token, 'Archived user') && nil
@user || errors.add(:token, "Archived user") && nil
end
def decoded_auth_token
@ -28,10 +30,10 @@ class AuthorizeApiRequest
end
def http_auth_header
if headers['Authorization'].present?
return headers['Authorization'].split(' ').last
if headers["Authorization"].present?
return headers["Authorization"].split(" ").last
else
errors.add(:token, 'Missing token')
errors.add(:token, "Missing token")
end
nil

View file

@ -15,7 +15,7 @@ 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)
@ -31,12 +31,12 @@ class AppsController < ApplicationController
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

View file

@ -2,6 +2,11 @@
class DataSourcesController < ApplicationController
def index
app = App.find_by_id params[:app_id]
unless AppPolicy.new(@current_user, app).update?
render json: { message: "Insufficient permissions" }, status: :internal_server_error
return
end
@data_sources = DataSource.where(app_id: params[:app_id])
end

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
class ForgotPasswordController < ApplicationController
skip_before_action :authenticate_request
@ -5,9 +7,9 @@ class ForgotPasswordController < ApplicationController
user = User.find_by(email: params[:_json])
if user.present?
user.send_password_reset
render json: {message: "We've sent the confirmation code to your email address"}, status: :ok
render json: { message: "We've sent the confirmation code to your email address" }, status: :ok
else
render json: {error: 'Email address is not associated with a ToolJet cloud account.'}, status: :not_found
render json: { error: "Email address is not associated with a ToolJet cloud account." }, status: :not_found
end
end
@ -15,12 +17,12 @@ class ForgotPasswordController < ApplicationController
user = User.find_by(forgot_password_token: params[:token])
if user.present? && user.forgot_password_token_valid?
if user.reset_password(params[:password])
render json: {message: 'Your password has been successfuly reset!'}, status: :ok
render json: { message: "Your password has been successfuly reset!" }, status: :ok
else
render json: {error: user.errors.full_messages}, status: :unprocessable_entity
render json: { error: user.errors.full_messages }, status: :unprocessable_entity
end
else
render json: {error: 'Link not valid or expired.'}, status: :not_found
render json: { error: "Link not valid or expired." }, status: :not_found
end
end
end

View file

@ -1,7 +1,8 @@
# frozen_string_literal: true
class MetadataController < ApplicationController
def index
unless ENV.fetch('CHECK_FOR_UPDATES', true)
unless ENV.fetch("CHECK_FOR_UPDATES", true)
return
end
@ -20,7 +21,7 @@ class MetadataController < ApplicationController
data = metadata.data
end
render json: {
render json: {
latest_version: data["latest_version"],
installed_version: installed_version,
version_ignored: data["version_ignored"],
@ -42,11 +43,10 @@ class MetadataController < ApplicationController
end
def finish_installation
name = params[:name]
email = params[:email]
response = HTTParty.post('https://hub.tooljet.io/subscribe',
response = HTTParty.post("https://hub.tooljet.io/subscribe",
verify: false,
body: { name: name, email: email, installed_version: TOOLJET_VERSION }.to_json,
headers: { "Content-Type" => "application/json" })
@ -56,23 +56,22 @@ class MetadataController < ApplicationController
Metadatum.first.update(data: data)
end
private
def check_for_updates(current_data, installed_version)
private
def check_for_updates(current_data, installed_version)
response = HTTParty.post("https://hub.tooljet.io/updates",
verify: false,
body: { installed_version: installed_version }.to_json,
headers: { "Content-Type" => "application/json" })
response = HTTParty.post('https://hub.tooljet.io/updates',
verify: false,
body: { installed_version: installed_version }.to_json,
headers: { "Content-Type" => "application/json" })
data = JSON.parse(response.body)
latest_version = data["latest_version"]
data = JSON.parse(response.body)
latest_version = data["latest_version"]
if latest_version > '0.5.3' && latest_version != current_data["ignored_version"]
current_data["latest_version"] = latest_version
current_data["version_ignored"] = false
end
current_data["last_checked"] = Time.now
Metadatum.first.update(data: current_data)
if latest_version > "0.5.3" && latest_version != current_data["ignored_version"]
current_data["latest_version"] = latest_version
current_data["version_ignored"] = false
end
current_data["last_checked"] = Time.now
Metadatum.first.update(data: current_data)
end
end

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
class ApplicationJob < ActiveJob::Base
# Automatically retry jobs that encountered a deadlock
# retry_on ActiveRecord::Deadlocked

View file

@ -1,4 +1,6 @@
# frozen_string_literal: true
class ApplicationMailer < ActionMailer::Base
default from: "ToolJet <#{ENV.fetch('DEFAULT_FROM_EMAIL', 'hello@tooljet.io')}>"
layout 'mailer'
layout "mailer"
end

View file

@ -1,15 +1,17 @@
# frozen_string_literal: true
class UserMailer < ApplicationMailer
def invitation_email
@user = params[:user]
@sender = params[:sender]
@url = "#{ENV.fetch('TOOLJET_HOST')}/invitations/#{@user.invitation_token}"
mail(to: @user.email, subject: 'ToolJet Invitation')
mail(to: @user.email, subject: "ToolJet Invitation")
end
def new_signup_email
@user = params[:user]
@url = "#{ENV.fetch('TOOLJET_HOST')}/invitations/#{@user.invitation_token}?signup=true"
mail(to: @user.email, subject: 'ToolJet Invitation')
mail(to: @user.email, subject: "ToolJet Invitation")
end
def password_reset(user)

View file

@ -1,2 +1,4 @@
# frozen_string_literal: true
class Metadatum < ApplicationRecord
end

View file

@ -53,7 +53,7 @@ class User < ApplicationRecord
private
def generate_base64_token
SecureRandom.urlsafe_base64
end
def generate_base64_token
SecureRandom.urlsafe_base64
end
end

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
class AppPolicy < ApplicationPolicy
attr_reader :user, :app

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
class AppUserPolicy < ApplicationPolicy
attr_reader :user, :app_user

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
class ApplicationPolicy
attr_reader :user, :record

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
class OrganizationUserPolicy < ApplicationPolicy
attr_reader :user, :organization_user

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
class AirtableQueryService
attr_accessor :query, :source, :options, :source_options, :current_user
@ -10,16 +12,16 @@ class AirtableQueryService
end
def process
operation = options['operation']
api_key = source_options['api_key']
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']
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)
@ -27,11 +29,11 @@ class AirtableQueryService
error = result.code != 200
end
if operation === 'retrieve_record'
base_id = options['base_id']
table_name = options['table_name']
record_id = options['record_id']
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)
@ -40,28 +42,26 @@ class AirtableQueryService
end
if error
{ status: 'error', code: 500, message: data["message"], data: data }
{ status: "error", code: 500, message: data["message"], data: data }
else
{ status: 'success', data: data }
{ 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}" })
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}" })
headers: { "Content-Type":
"application/json", "Authorization": "Bearer #{api_key}" })
result
end

View file

@ -1,8 +1,9 @@
# frozen_string_literal: true
module DatasourceUtils
extend ActiveSupport::Concern
def get_cached_connection(data_source)
connection = nil
if $connections.include? data_source.id
data = $connections[data_source.id]
@ -10,8 +11,7 @@ module DatasourceUtils
connection = $connections[data_source.id][:connection]
end
end
connection
connection
end
def cache_connection(data_source, connection)
@ -21,6 +21,4 @@ module DatasourceUtils
def reset_connection(data_source)
$connections.delete @data_source.id
end
end

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
class CredentialService
def initialize; end
@ -6,10 +8,10 @@ class CredentialService
options.keys.each do |key|
option = options[key]
parsed_options[key] = if option['encrypted']
Credential.find(option['credential_id']).value
parsed_options[key] = if option["encrypted"]
Credential.find(option["credential_id"]).value
else
option['value']
option["value"]
end
end

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
class DataSourceConnectionService
attr_accessor :data_source_kind, :options

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
class DynamodbQueryService
attr_accessor :data_query, :data_source, :options, :source_options, :current_user
@ -9,16 +11,15 @@ class DynamodbQueryService
@current_user = current_user
end
def self.connection options
region = options.dig('region', 'value')
access_key = options.dig('access_key', 'value')
secret_key = options.dig('secret_key', 'value')
def self.connection(options)
region = options.dig("region", "value")
access_key = options.dig("access_key", "value")
secret_key = options.dig("secret_key", "value")
credentials = Aws::Credentials.new(access_key, secret_key)
dynamodb = Aws::DynamoDB::Client.new(region: region, credentials: credentials)
dynamodb.list_tables
dynamodb.list_tables
end
def process
@ -36,17 +37,17 @@ class DynamodbQueryService
error = e.message
end
{ status: error ? 'failed' : 'success', data: data, error: { message: error } }
{ status: error ? "failed" : "success", data: data, error: { message: error } }
end
private
private
def get_connection
if $connections.include? data_source.id
connection = $connections[data_source.id][:connection]
else
region = source_options['region']
access_key = source_options['access_key']
secret_key = source_options['secret_key']
region = source_options["region"]
access_key = source_options["access_key"]
secret_key = source_options["secret_key"]
credentials = Aws::Credentials.new(access_key, secret_key)
connection = Aws::DynamoDB::Client.new(region: region, credentials: credentials)
@ -58,7 +59,7 @@ class DynamodbQueryService
end
def exec_list_tables(connection, options)
tables = connection.list_tables
tables = connection.list_tables
tables.to_h
end
@ -70,7 +71,7 @@ class DynamodbQueryService
table_name: table,
key: key
}
connection.get_item(item).to_h
end

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true
class ElasticsearchQueryService
include DatasourceUtils
require 'elasticsearch'
require "elasticsearch"
attr_accessor :data_query, :data_source, :options, :source_options, :current_user
@ -12,27 +14,26 @@ class ElasticsearchQueryService
@data_source = data_source
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')
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: url,
retry_on_failure: 5,
request_timeout: 15,
adapter: :typhoeus
)
client.info # Try to fetch cluster info
end
@ -44,32 +45,32 @@ class ElasticsearchQueryService
connection = create_connection unless connection
begin
operation = options['operation']
operation = options["operation"]
if operation == 'search'
index = options['index']
query = JSON.parse(options['query'])
if operation == "search"
index = options["index"]
query = JSON.parse(options["query"])
data = connection.search(index: index, body: query)
end
if operation == 'index_document'
index = options['index']
body = options['body']
if operation == "index_document"
index = options["index"]
body = options["body"]
data = connection.index(index: index, body: body)
end
if operation == 'get'
index = options['index']
id = options['id']
if operation == "get"
index = options["index"]
id = options["id"]
data = connection.get(index: index, id: id)
end
if operation == 'update'
index = options['index']
id = options['id']
body = options['body']
if operation == "update"
index = options["index"]
id = options["id"]
body = options["body"]
data = connection.update(index: index, id: id, body: body)
end
@ -82,14 +83,13 @@ class ElasticsearchQueryService
{ data: data, error: error }
end
private
private
def create_connection
scheme = source_options['scheme']
host = source_options['host']
port = source_options['port']
username = source_options['username']
password = source_options['password']
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}"

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
class FirestoreQueryService
require 'google/cloud/firestore'
require "google/cloud/firestore"
include DatasourceUtils
attr_accessor :data_query, :options, :source_options, :current_user, :data_source
@ -12,8 +14,8 @@ class FirestoreQueryService
@current_user = current_user
end
def self.connection options
gcp_key = JSON.parse(options.dig('gcp_key', 'value'))
def self.connection(options)
gcp_key = JSON.parse(options.dig("gcp_key", "value"))
Google::Cloud::Firestore.configure do |config|
config.credentials = gcp_key
@ -33,14 +35,14 @@ class FirestoreQueryService
firestore = get_cached_connection(data_source)
firestore = create_connection unless firestore
operation = options['operation']
operation = options["operation"]
update_document(options['path'], options['body'].as_json, firestore) if operation == 'update_document'
update_document(options["path"], options["body"].as_json, firestore) if operation == "update_document"
if operation == 'bulk_update'
records = options['records']
collection = options['collection']
doc_key_id = options['document_id_key']
if operation == "bulk_update"
records = options["records"]
collection = options["collection"]
doc_key_id = options["document_id_key"]
records.each do |record|
path = "#{collection}/#{record[doc_key_id]}"
@ -49,50 +51,50 @@ class FirestoreQueryService
end
end
if operation == 'get_document'
path = options['path']
if operation == "get_document"
path = options["path"]
doc_ref = firestore.doc path
snapshot = doc_ref.get
data = snapshot.data
end
if operation == 'set_document'
path = options['path']
body = JSON.parse(options['body'])
if operation == "set_document"
path = options["path"]
body = JSON.parse(options["body"])
doc_ref = firestore.doc path
doc_ref.set body
end
if operation == 'add_document'
path = options['path']
body = JSON.parse(options['body'])
if operation == "add_document"
path = options["path"]
body = JSON.parse(options["body"])
col_ref = firestore.col path
col_ref.add body
end
if operation == 'delete_document'
path = options['path']
body = JSON.parse(options['body'])
if operation == "delete_document"
path = options["path"]
body = JSON.parse(options["body"])
doc_ref = firestore.doc path
doc_ref.delete
end
if operation == 'query_collection'
path = options['path']
if operation == "query_collection"
path = options["path"]
doc_ref = firestore.col path
# execute where condition
if options['where_field']
doc_ref = doc_ref.where options['where_field'], options['where_operation'], options['where_value']
if options["where_field"]
doc_ref = doc_ref.where options["where_field"], options["where_operation"], options["where_value"]
end
if options['order']
doc_ref = doc_ref.order(options['order'], 'desc')
if options["order"]
doc_ref = doc_ref.order(options["order"], "desc")
end
if options['limit']
doc_ref = doc_ref.limit(options['limit'].to_i)
if options["limit"]
doc_ref = doc_ref.limit(options["limit"].to_i)
end
data = []
@ -116,7 +118,7 @@ class FirestoreQueryService
end
def create_connection
credential_json = JSON.parse(source_options['gcp_key'])
credential_json = JSON.parse(source_options["gcp_key"])
Google::Cloud::Firestore.configure do |config|
config.credentials = credential_json
end

View file

@ -1,14 +1,16 @@
# frozen_string_literal: true
class GoogleOauthService
def self.generate_base_auth_url
client_id = ENV.fetch('GOOGLE_CLIENT_ID', '')
client_id = ENV.fetch("GOOGLE_CLIENT_ID", "")
"https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=#{client_id}&redirect_uri=#{ENV.fetch('TOOLJET_HOST')}/oauth2/authorize"
end
def self.fetch_access_token(code)
access_token_url = 'https://oauth2.googleapis.com/token'
client_id = ENV.fetch('GOOGLE_CLIENT_ID', '')
client_secret = ENV.fetch('GOOGLE_CLIENT_SECRET', '')
grant_type = 'authorization_code'
access_token_url = "https://oauth2.googleapis.com/token"
client_id = ENV.fetch("GOOGLE_CLIENT_ID", "")
client_secret = ENV.fetch("GOOGLE_CLIENT_SECRET", "")
grant_type = "authorization_code"
custom_params = [
%w[prompt consent],
@ -22,21 +24,21 @@ class GoogleOauthService
grant_type: 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']
refresh_token = result['refresh_token']
access_token = result["access_token"]
refresh_token = result["refresh_token"]
[['access_token', access_token], ['refresh_token', refresh_token]]
[["access_token", access_token], ["refresh_token", refresh_token]]
end
def self.refresh_access_token(refresh_token, data_source)
access_token_url = 'https://oauth2.googleapis.com/token'
client_id = ENV.fetch('GOOGLE_CLIENT_ID')
client_secret = ENV.fetch('GOOGLE_CLIENT_SECRET')
grant_type = 'refresh_token'
access_token_url = "https://oauth2.googleapis.com/token"
client_id = ENV.fetch("GOOGLE_CLIENT_ID")
client_secret = ENV.fetch("GOOGLE_CLIENT_SECRET")
grant_type = "refresh_token"
response = HTTParty.post(access_token_url,
body: { refresh_token: refresh_token,
@ -45,11 +47,11 @@ class GoogleOauthService
grant_type: grant_type,
redirect_uri: "#{ENV.fetch('TOOLJET_HOST')}/oauth2/authorize"
}.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"]
credential_id = data_source.options["access_token"]["credential_id"]
credential = Credential.find(credential_id)

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
class GooglesheetsQueryService
attr_accessor :query, :source, :options, :source_options, :current_user
@ -10,12 +12,12 @@ class GooglesheetsQueryService
end
def process
operation = options['operation']
access_token = source_options['access_token']
operation = options["operation"]
access_token = source_options["access_token"]
error = false
if operation === 'info'
spreadsheet_id = options['spreadsheet_id']
if operation === "info"
spreadsheet_id = options["spreadsheet_id"]
result = get_spreadsheet_info(spreadsheet_id, access_token)
if result.code === 401
@ -27,11 +29,11 @@ class GooglesheetsQueryService
error = result.code != 200
end
if operation === 'append'
if operation === "append"
spreadsheet_id = options['spreadsheet_id']
sheet = options['sheet']
rows = options['rows']
spreadsheet_id = options["spreadsheet_id"]
sheet = options["sheet"]
rows = options["rows"]
result = append_data_to_sheet(spreadsheet_id, sheet, rows, access_token)
@ -60,7 +62,7 @@ class GooglesheetsQueryService
error = result.code != 200
end
if operation === 'read'
if operation === "read"
result = read_data(access_token)
if result.code === 401
@ -72,9 +74,9 @@ class GooglesheetsQueryService
headers = []
values = []
if result['values']
headers = result['values'][0] if
values = result['values'][1..] if result['values'].size > 1
if result["values"]
headers = result["values"][0] if
values = result["values"][1..] if result["values"].size > 1
end
data = []
@ -85,45 +87,44 @@ class GooglesheetsQueryService
end
data << row
end
else
else
error = true
data = result["error"]
end
end
if error
{ status: 'error', code: 500, message: data["message"], data: data }
{ status: "error", code: 500, message: data["message"], data: data }
else
{ status: 'success', data: data }
{ status: "success", data: data }
end
end
private
def read_data_from_sheet(spreadsheet_id, sheet, access_token, range)
result = HTTParty.get("https://sheets.googleapis.com/v4/spreadsheets/#{spreadsheet_id}/values/#{sheet}!#{range}",
headers: { 'Content-Type':
'application/json', "Authorization": "Bearer #{access_token}" })
headers: { "Content-Type":
"application/json", "Authorization": "Bearer #{access_token}" })
result
end
def read_data(access_token)
spreadsheet_id = options['spreadsheet_id']
sheet = options['sheet']
spreadsheet_id = options["spreadsheet_id"]
sheet = options["sheet"]
read_data_from_sheet(spreadsheet_id, sheet, access_token, 'A1:V101')
read_data_from_sheet(spreadsheet_id, sheet, access_token, "A1:V101")
end
def append_data_to_sheet(spreadsheet_id, sheet, rows, access_token)
data = read_data_from_sheet(spreadsheet_id, sheet, access_token, 'A1:V1')
headers = data['values'][0]
data = read_data_from_sheet(spreadsheet_id, sheet, access_token, "A1:V1")
headers = data["values"][0]
parsed_data = JSON.parse(rows)
data_to_append = []
parsed_data.each do |row|
row_data = []
headers.each_with_index do |header, index|
@ -136,8 +137,8 @@ class GooglesheetsQueryService
"values": data_to_append
}.to_json
result = HTTParty.post("https://sheets.googleapis.com/v4/spreadsheets/#{spreadsheet_id}/values/#{sheet}!A:V:append?valueInputOption=USER_ENTERED", body: data, headers: { 'Content-Type':
'application/json', "Authorization": "Bearer #{access_token}" })
result = HTTParty.post("https://sheets.googleapis.com/v4/spreadsheets/#{spreadsheet_id}/values/#{sheet}!A:V:append?valueInputOption=USER_ENTERED", body: data, headers: { "Content-Type":
"application/json", "Authorization": "Bearer #{access_token}" })
end
def delete_row_from_sheet(spreadsheet_id, sheet, row_index, access_token)
@ -165,15 +166,14 @@ class GooglesheetsQueryService
end
def get_spreadsheet_info(spreadsheet_id, access_token)
result = HTTParty.get("https://sheets.googleapis.com/v4/spreadsheets/#{spreadsheet_id}",
headers: { 'Content-Type':
'application/json', "Authorization": "Bearer #{access_token}" })
headers: { "Content-Type":
"application/json", "Authorization": "Bearer #{access_token}" })
result
end
def refresh_access_token
GoogleOauthService.refresh_access_token(source_options['refresh_token'], @source )
GoogleOauthService.refresh_access_token(source_options["refresh_token"], @source)
end
end

View file

@ -1,5 +1,6 @@
class GraphqlQueryService
# frozen_string_literal: true
class GraphqlQueryService
attr_accessor :data_query, :options, :source_options, :current_user, :data_source
def initialize(data_query, data_source, options, source_options, current_user)
@ -11,12 +12,12 @@ class GraphqlQueryService
end
def process
url = source_options['url']
method = options['method'] || 'GET'
source_headers = (source_options['headers'] || []).reject { |header| header[0].empty? }.to_h
url_params = source_options['url_params']
url = source_options["url"]
method = options["method"] || "GET"
source_headers = (source_options["headers"] || []).reject { |header| header[0].empty? }.to_h
url_params = source_options["url_params"]
encoded_url = url_encoded_with_params(url, url_params)
query = options['query']
query = options["query"]
client = Graphlient::Client.new(encoded_url, headers: source_headers)
result = client.query(query)
if result.errors.present?
@ -33,7 +34,7 @@ def url_encoded_with_params(original_url, url_params)
original_url
else
uri = URI.parse(original_url)
params = URI.decode_www_form(uri.query || '') + url_params
params = URI.decode_www_form(uri.query || "") + url_params
uri.query = URI.encode_www_form(params)
uri.to_s
end

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
class MongodbQueryService
attr_accessor :data_query, :data_source, :options, :source_options, :current_user
@ -9,17 +11,16 @@ class MongodbQueryService
@current_user = current_user
end
def self.connection options
connection_type = options.dig('connection_type', 'value')
def self.connection(options)
connection_type = options.dig("connection_type", "value")
if connection_type === "manual"
host = options.dig('host', 'value')
port = options.dig('port', 'value')
user = options.dig('username', 'value')
password = options.dig('password', 'value')
database = options.dig('database', 'value')
host = options.dig("host", "value")
port = options.dig("port", "value")
user = options.dig("username", "value")
password = options.dig("password", "value")
database = options.dig("database", "value")
user = nil if user.blank?
password = nil if password.blank?
@ -32,9 +33,9 @@ class MongodbQueryService
password: password
)
else
connection_string = options.dig('connection_string', 'value')
connection_string = options.dig("connection_string", "value")
connection = Mongo::Client.new(connection_string, server_selection_timeout: 5)
end
end
connection.collections
end
@ -42,22 +43,22 @@ class MongodbQueryService
def process
error = nil
data = []
operation = options['operation']
operation = options["operation"]
begin
if $connections.include? data_source.id
connection = $connections[data_source.id][:connection]
else
if source_options['connection_type'] === 'manual'
password = source_options['password']
if source_options["connection_type"] === "manual"
password = source_options["password"]
password = nil if password.blank?
user = source_options['username']
user = source_options["username"]
user = nil if user.blank?
host = source_options['host']
port = source_options['port']
database = source_options['database']
host = source_options["host"]
port = source_options["port"]
database = source_options["database"]
connection = Mongo::Client.new(
[ "#{host}:#{port}" ],
@ -67,24 +68,24 @@ class MongodbQueryService
password: password
)
else
connection_string = source_options['connection_string']
connection_string = source_options["connection_string"]
connection = Mongo::Client.new(connection_string, server_selection_timeout: 5)
end
$connections[data_source.id] = { connection: connection }
end
if operation === 'list_collections'
if operation === "list_collections"
connection.collections.each { |coll| data << { name: coll.name } }
end
if operation === 'insert_one'
if operation === "insert_one"
collection = connection[options["collection"]]
doc = JSON.parse(options["document"])
result = collection.insert_one(doc)
end
if operation === 'insert_many'
if operation === "insert_many"
collection = connection[options["collection"]]
docs = JSON.parse(options["documents"])
result = collection.insert_many(docs)
@ -95,6 +96,6 @@ class MongodbQueryService
error = e.message
end
{ status: error ? 'failed' : 'success', data: data, error: { message: error } }
{ status: error ? "failed" : "success", data: data, error: { message: error } }
end
end

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
class MssqlQueryService
include DatasourceUtils
attr_accessor :data_query, :data_source, :options, :source_options, :current_user
@ -12,13 +14,13 @@ class MssqlQueryService
def self.connection(options)
TinyTds::Client.new(
database: options.dig('database', 'value'),
username: options.dig('username', 'value'),
password: options.dig('password', 'value'),
host: options.dig('host', 'value'),
port: options.dig('port', 'value'),
database: options.dig("database", "value"),
username: options.dig("username", "value"),
password: options.dig("password", "value"),
host: options.dig("host", "value"),
port: options.dig("port", "value"),
azure: ActiveModel::Type::Boolean.new.cast(
options.dig('azure', 'value')
options.dig("azure", "value")
) || false
)
end
@ -26,10 +28,10 @@ class MssqlQueryService
def process
connection = get_cached_connection(data_source)
connection ||= create_connection
query_text = options['query']
query_text = options["query"]
results = connection.execute(query_text)
{ status: 'success', data: results.to_a }
{ status: "success", data: results.to_a }
rescue StandardError => e
if connection&.active?
connection&.close
@ -43,13 +45,13 @@ class MssqlQueryService
def create_connection
connection = TinyTds::Client.new(
database: source_options['database'],
username: source_options['username'],
password: source_options['password'],
host: source_options['host'],
port: source_options['port'],
database: source_options["database"],
username: source_options["username"],
password: source_options["password"],
host: source_options["host"],
port: source_options["port"],
azure: ActiveModel::Type::Boolean.new.cast(
source_options['azure']
source_options["azure"]
) || false
)

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
class MysqlQueryService
include DatasourceUtils
attr_accessor :data_query, :data_source, :options, :source_options, :current_user
@ -12,11 +14,11 @@ class MysqlQueryService
def self.connection options
connection = Mysql2::Client.new(
database: options.dig('database', 'value'),
user: options.dig('username', 'value'),
password: options.dig('password', 'value'),
host: options.dig('host', 'value'),
port: options.dig('port', 'value'),
database: options.dig("database", "value"),
user: options.dig("username", "value"),
password: options.dig("password", "value"),
host: options.dig("host", "value"),
port: options.dig("port", "value"),
)
end
@ -25,21 +27,21 @@ class MysqlQueryService
connection = get_cached_connection(data_source)
connection = create_connection unless connection
query_text = options['query']
query_text = options["query"]
results = connection.query(query_text)
{ status: 'success', data: results.to_a }
{ status: "success", data: results.to_a }
end
private
def create_connection
connection = Mysql2::Client.new(
host: source_options['host'],
username: source_options['username'],
password: source_options['password'],
port: source_options['port'],
database: source_options['database']
host: source_options["host"],
username: source_options["username"],
password: source_options["password"],
port: source_options["port"],
database: source_options["database"]
)
cache_connection(data_source, connection)

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
class PostgresqlQueryService
include DatasourceUtils
attr_accessor :data_query, :data_source, :options, :source_options, :current_user
@ -10,18 +12,17 @@ class PostgresqlQueryService
@current_user = current_user
end
def self.connection options
def self.connection(options)
PG.connect(
dbname: options.dig('database', 'value'),
user: options.dig('username', 'value'),
password: options.dig('password', 'value'),
host: options.dig('host', 'value'),
port: options.dig('port', 'value'),
dbname: options.dig("database", "value"),
user: options.dig("username", "value"),
password: options.dig("password", "value"),
host: options.dig("host", "value"),
port: options.dig("port", "value"),
)
end
def process
error = false
begin
@ -29,11 +30,11 @@ class PostgresqlQueryService
connection = create_connection unless connection
query_text = ''
query_text = if options['mode'] === 'gui'
query_text = ""
query_text = if options["mode"] === "gui"
send("generate_#{options['operation']}_query", options)
else
options['query']
options["query"]
end
result = connection.exec(query_text)
@ -45,24 +46,24 @@ class PostgresqlQueryService
end
puts e
error = { message: e.message }
error = { message: e.message }
end
if error
{ status: 'error', code: 500, message: error[:message] }
{ status: "error", code: 500, message: error[:message] }
else
{ status: 'success', data: result.to_a }
{ status: "success", data: result.to_a }
end
end
private
def generate_bulk_update_pkey_query(options)
query_text = ''
query_text = ""
table_name = options['table']
primary_key = options['primary_key_column']
records = options['records']
table_name = options["table"]
primary_key = options["primary_key_column"]
records = options["records"]
records.each do |record|
query_text = "#{query_text} UPDATE #{table_name} SET"
@ -78,17 +79,17 @@ class PostgresqlQueryService
query_text
end
def create_connection
def create_connection
connection = PG.connect(
dbname: source_options['database'],
user: source_options['username'],
password: source_options['password'],
host: source_options['host'],
port: source_options['port']
dbname: source_options["database"],
user: source_options["username"],
password: source_options["password"],
host: source_options["host"],
port: source_options["port"]
)
connection.type_map_for_results = PG::BasicTypeMapForResults.new connection
cache_connection(data_source, connection)
connection

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
class QueryService
attr_accessor :data_query, :options, :current_user
@ -14,15 +16,15 @@ class QueryService
data_source_options.keys.each do |key|
option = data_source_options[key]
parsed_options[key] = if option['encrypted']
Credential.find(option['credential_id']).value
parsed_options[key] = if option["encrypted"]
Credential.find(option["credential_id"]).value
else
option['value']
option["value"]
end
end if data_source
query_options = data_query[:options]
if query_options.class.name === 'Hash'
if query_options.class.name === "Hash"
parsed_query_options = get_query_options(query_options)
else
parsed_query_options = get_query_options(query_options.permit!.to_h)
@ -35,7 +37,6 @@ class QueryService
private
def get_query_options(object)
if object.is_a?(Hash)
object.keys.each do |key|
@ -43,7 +44,7 @@ class QueryService
end
elsif object.class.name === "String"
if object.start_with?('{{') && object.end_with?('}}')
if object.start_with?("{{") && object.end_with?("}}")
object = options[object]
else
variables = object.scan(/\{\{(.*?)\}\}/).to_a

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
class RedisQueryService
require 'redis'
require "redis"
attr_accessor :data_query, :data_source, :options, :source_options, :current_user
@ -11,15 +13,14 @@ class RedisQueryService
@current_user = current_user
end
def self.connection options
password = options.dig('password', 'value')
def self.connection(options)
password = options.dig("password", "value")
password = nil if password.blank?
connection = Redis.new(
host: options.dig('host', 'value'),
port: options.dig('port', 'value'),
user: options.dig('username', 'value'),
host: options.dig("host", "value"),
port: options.dig("port", "value"),
user: options.dig("username", "value"),
password: password
)
@ -28,7 +29,7 @@ class RedisQueryService
def process
error = nil
password = source_options['password']
password = source_options["password"]
password = nil if password.blank?
begin
@ -36,23 +37,23 @@ class RedisQueryService
connection = $connections[data_source.id][:connection]
else
connection = Redis.new(
host: source_options['host'],
port: source_options['port'],
user: source_options['username'],
host: source_options["host"],
port: source_options["port"],
user: source_options["username"],
password: password
)
$connections[data_source.id] = { connection: connection }
end
query_text = options['query']
query_text = options["query"]
result = connection.call(query_text.split(' '))
result = connection.call(query_text.split(" "))
rescue StandardError => e
puts e
error = e.message
end
{ status: error ? 'failed' : 'success', data: result, error: { message: error } }
{ status: error ? "failed" : "success", data: result, error: { message: error } }
end
end

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
class RestapiQueryService
attr_accessor :data_query, :options, :source_options, :current_user, :data_source
@ -10,31 +12,31 @@ class RestapiQueryService
end
def process
url = options['url']
url = options["url"]
if data_source
url = "#{source_options['url']}#{url}"
end
method = options['method'] || 'GET'
headers = (options['headers'] || []).reject { |header| header[0].empty? }
method = options["method"] || "GET"
headers = (options["headers"] || []).reject { |header| header[0].empty? }
headers = headers.to_h
body = options['body']
url_params = options['url_params']
body = options["body"]
url_params = options["url_params"]
if source_options['auth_type'] === 'oauth2'
if source_options["auth_type"] === "oauth2"
oauth_tokens = DataSourceUserOauth2.where(user: current_user,
data_source: data_source).order('created_at desc')
data_source: data_source).order("created_at desc")
if oauth_tokens.size == 0
auth_url = "#{source_options['auth_url']}?response_type=code&client_id=#{source_options['client_id']}&redirect_uri=#{ENV.fetch('TOOLJET_HOST')}/oauth2/authorize&scope=#{source_options['scopes']}"
return { error: { message: 'needs authorization', code: 'oauth2_needs_auth',
return { error: { message: "needs authorization", code: "oauth2_needs_auth",
data: { auth_url: auth_url } } }
else
token = JSON.parse(oauth_tokens.first.options)['access_token']
token = JSON.parse(oauth_tokens.first.options)["access_token"]
end
if source_options['add_token_to'] === 'header'
if source_options["add_token_to"] === "header"
headers = {
**headers,
'Authorization': "Bearer #{token}"
@ -42,7 +44,7 @@ class RestapiQueryService
end
end
response = if method.downcase === 'get'
response = if method.downcase === "get"
HTTParty.send(method.downcase,
url,
headers: headers,
@ -57,7 +59,7 @@ class RestapiQueryService
if response.code == 401
auth_url = "#{source_options['auth_url']}?response_type=code&client_id=#{source_options['client_id']}&redirect_uri=#{ENV.fetch('TOOLJET_HOST')}/oauth2/authorize&scope=#{source_options['scopes']}"
return { error: { message: 'needs authorization', code: 'oauth2_needs_auth',
return { error: { message: "needs authorization", code: "oauth2_needs_auth",
data: { auth_url: auth_url } } }
end

View file

@ -1,13 +1,15 @@
# frozen_string_literal: true
class SlackOauthService
def self.generate_base_auth_url
client_id = ENV.fetch('SLACK_CLIENT_ID')
client_id = ENV.fetch("SLACK_CLIENT_ID")
"https://slack.com/oauth/v2/authorize?response_type=code&client_id=#{client_id}&redirect_uri=#{ENV.fetch('TOOLJET_HOST')}/oauth2/authorize"
end
def self.fetch_access_token(code)
access_token_url = "https://slack.com/api/oauth.v2.access"
client_id = ENV.fetch('SLACK_CLIENT_ID')
client_secret = ENV.fetch('SLACK_CLIENT_SECRET')
client_id = ENV.fetch("SLACK_CLIENT_ID")
client_secret = ENV.fetch("SLACK_CLIENT_SECRET")
data = { code: code,
client_id: client_id,
@ -20,9 +22,9 @@ class SlackOauthService
result = JSON.parse(response.body)
access_token = result['access_token']
refresh_token = result['refresh_token']
access_token = result["access_token"]
refresh_token = result["refresh_token"]
[['access_token', access_token], ['refresh_token', refresh_token]]
[["access_token", access_token], ["refresh_token", refresh_token]]
end
end

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
class SlackQueryService
attr_accessor :query, :ource, :options, :source_options, :current_user
@ -10,34 +12,33 @@ class SlackQueryService
end
def process
operation = options['operation']
access_token = source_options['access_token']
operation = options["operation"]
access_token = source_options["access_token"]
data = []
if operation === 'list_users'
if operation === "list_users"
result = HTTParty.get("https://slack.com/api/users.list",
headers: { "Authorization": "Bearer #{access_token}" })
data = JSON.parse(result.body)
end
if operation === 'send_message'
if operation === "send_message"
body = {
channel: options["channel"],
text: options["message"],
as_user: options["sendAsUser"]
}.to_json
result = HTTParty.post("https://slack.com/api/chat.postMessage",
body: body,
headers: { "Content-Type": "application/json", "Authorization": "Bearer #{access_token}" }
)
data = JSON.parse(result.body)
data = JSON.parse(result.body)
end
{ status: 'success', data: data }
{ status: "success", data: data }
end
end

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
class StripeQueryService
attr_accessor :data_query, :options, :data_source, :source_options, :current_user
@ -18,22 +20,22 @@ class StripeQueryService
end
def process
stripe_api_key = source_options['api_key']
api_base_url = 'https://api.stripe.com'
operation = options['operation']
path = options['path']
stripe_api_key = source_options["api_key"]
api_base_url = "https://api.stripe.com"
operation = options["operation"]
path = options["path"]
url = "#{api_base_url}#{path}"
# Replace path params in url with their values
path_params = options['params']['path']
query_params = options['params']['query']
body_params = options['params']['request']
path_params = options["params"]["path"]
query_params = options["params"]["query"]
body_params = options["params"]["request"]
url = replace_path_params(url, path_params)
headers = {
'Authorization': "Bearer #{stripe_api_key}"
"Authorization": "Bearer #{stripe_api_key}"
}
response = HTTParty.send(

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true
# This file is used by Rack-based servers to start the application.
require_relative 'config/environment'
require_relative "config/environment"
run Rails.application
Rails.application.load_server

View file

@ -19,7 +19,7 @@ require 'rails/test_unit/railtie'
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
TOOLJET_VERSION = '0.5.12'
TOOLJET_VERSION = '0.5.13'
module ToolJet
class Application < Rails::Application

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Be sure to restart your server when you modify this file.
# ActiveSupport::Reloader.to_prepare do

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Be sure to restart your server when you modify this file.
# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
@ -5,4 +7,4 @@
# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code
# by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'".
Rails.backtrace_cleaner.remove_silencers! if ENV['BACKTRACE']
Rails.backtrace_cleaner.remove_silencers! if ENV["BACKTRACE"]

View file

@ -1 +1,3 @@
# frozen_string_literal: true
$connections = {}

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Be sure to restart your server when you modify this file.
# Avoid CORS issues when API is called from the frontend app.

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Be sure to restart your server when you modify this file.
# Configure sensitive parameters which will be filtered from the log file.

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
Rails.application.config.generators do |g|
g.orm :active_record, primary_key_type: :uuid
end

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Be sure to restart your server when you modify this file.
# Add new inflection rules using the following format. Inflections

View file

@ -1 +1,3 @@
Lockbox.master_key = ENV.fetch('LOCKBOX_MASTER_KEY')
# frozen_string_literal: true
Lockbox.master_key = ENV.fetch("LOCKBOX_MASTER_KEY")

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Be sure to restart your server when you modify this file.
# Add new mime types for use in respond_to blocks:

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Be sure to restart your server when you modify this file.
# This file contains settings for ActionController::ParamsWrapper which

View file

@ -1,28 +1,30 @@
# frozen_string_literal: true
# Puma can serve each request in a thread from an internal thread pool.
# The `threads` method setting takes two numbers: a minimum and maximum.
# Any libraries that use thread pools should be configured to match
# the maximum value specified for Puma. Default is set to 5 threads for minimum
# and maximum; this matches the default thread size of Active Record.
#
max_threads_count = ENV.fetch('RAILS_MAX_THREADS') { 5 }
min_threads_count = ENV.fetch('RAILS_MIN_THREADS') { max_threads_count }
max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
threads min_threads_count, max_threads_count
# Specifies the `worker_timeout` threshold that Puma will use to wait before
# terminating a worker in development environments.
#
worker_timeout 3600 if ENV.fetch('RAILS_ENV', 'development') == 'development'
worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development"
# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
#
port ENV.fetch('PORT') { 3000 }
port ENV.fetch("PORT") { 3000 }
# Specifies the `environment` that Puma will run in.
#
environment ENV.fetch('RAILS_ENV') { 'development' }
environment ENV.fetch("RAILS_ENV") { "development" }
# Specifies the `pidfile` that Puma will use.
pidfile ENV.fetch('PIDFILE') { 'tmp/pids/server.pid' }
pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
# Specifies the number of `workers` to boot in clustered mode.
# Workers are forked web server processes. If using threads and workers together

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true
Spring.watch(
'.ruby-version',
'.rbenv-vars',
'tmp/restart.txt',
'tmp/caching-dev.txt'
".ruby-version",
".rbenv-vars",
"tmp/restart.txt",
"tmp/caching-dev.txt"
)

View file

@ -1,4 +1,5 @@
# frozen_string_literal: true
org = Organization.create(name: 'My organization')
user = User.create(first_name: 'The', last_name: 'Developer', email: 'dev@tooljet.io', password: 'password', organization: org)
OrganizationUser.create(user: user, organization: org, role: 'admin', status: 'active')
org = Organization.create(name: "My organization")
user = User.create(first_name: "The", last_name: "Developer", email: "dev@tooljet.io", password: "password", organization: org)
OrganizationUser.create(user: user, organization: org, role: "admin", status: "active")

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
#!/usr/bin/env ruby
def ensure_db_connectivity(user = ENV.fetch("PG_USER"), pass = ENV.fetch("PG_PASS"), host = ENV.fetch("PG_HOST"))
@ -17,7 +19,7 @@ def install_script_deps
end
def load_env
require 'dotenv'
require "dotenv"
Dir.chdir "/home/ubuntu/app"
Dotenv.load!
Dotenv.require_keys("TOOLJET_HOST", "LOCKBOX_MASTER_KEY", "SECRET_KEY_BASE", "PG_DB", "PG_USER", "PG_HOST", "PG_PASS")

View file

@ -2935,15 +2935,6 @@ cli-boxes@^2.2.1:
resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f"
integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==
clipboard@^2.0.0:
version "2.0.8"
resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.8.tgz#ffc6c103dd2967a83005f3f61976aa4655a4cdba"
integrity sha512-Y6WO0unAIQp5bLmk1zdThRhgJt/x3ks6f30s3oE3H1mgIEU33XyQjEf8gsf6DxC7NPX8Y1SsNWjUjL/ywLnnbQ==
dependencies:
good-listener "^1.2.2"
select "^1.1.2"
tiny-emitter "^2.0.0"
cliui@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5"
@ -3643,11 +3634,6 @@ del@^6.0.0:
rimraf "^3.0.2"
slash "^3.0.0"
delegate@^3.1.2:
version "3.2.0"
resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166"
integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==
depd@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
@ -4605,13 +4591,6 @@ globby@^6.1.0:
pify "^2.0.0"
pinkie-promise "^2.0.0"
good-listener@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50"
integrity sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=
dependencies:
delegate "^3.1.2"
got@^9.6.0:
version "9.6.0"
resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85"
@ -7366,11 +7345,9 @@ prism-react-renderer@^1.1.1:
integrity sha512-GHqzxLYImx1iKN1jJURcuRoA/0ygCcNhfGw1IT8nPIMzarmKQ3Nc+JcG0gi8JXQzuh0C5ShE4npMIoqNin40hg==
prismjs@^1.23.0:
version "1.23.0"
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.23.0.tgz#d3b3967f7d72440690497652a9d40ff046067f33"
integrity sha512-c29LVsqOaLbBHuIbsTxaKENh1N2EQBOHaWv7gkHN4dgRbxSREqDnDbtFJYdpPauS4YCplMSNCABQ6Eeor69bAA==
optionalDependencies:
clipboard "^2.0.0"
version "1.24.1"
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.24.1.tgz#c4d7895c4d6500289482fa8936d9cdd192684036"
integrity sha512-mNPsedLuk90RVJioIky8ANZEwYm5w9LcvCXrxHlwf4fNVSn8jEipMybMkWUyyF0JhnC+C4VcOVSBuHRKs1L5Ow==
process-nextick-args@~2.0.0:
version "2.0.1"
@ -8209,11 +8186,6 @@ select-hose@^2.0.0:
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=
select@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=
selfsigned@^1.10.8:
version "1.10.8"
resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.8.tgz#0d17208b7d12c33f8eac85c41835f27fc3d81a30"
@ -8919,11 +8891,6 @@ timsort@^0.3.0:
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
tiny-emitter@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"
integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==
tiny-invariant@^1.0.2:
version "1.1.0"
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"

View file

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 456.54 456.54" style="enable-background:new 0 0 456.54 456.54;" xml:space="preserve">
<g>
<rect x="215.27" y="379.3" style="fill:#FFDE55;" width="26" height="77.24"/>
<rect x="215.27" style="fill:#FFDE55;" width="26" height="77.24"/>
<rect x="81.169" y="323.75" transform="matrix(-0.7071 -0.7071 0.7071 -0.7071 -95.4776 685.1913)" style="fill:#FCEBA2;" width="26" height="77.239"/>
<rect x="349.372" y="55.544" transform="matrix(-0.7071 -0.7071 0.7071 -0.7071 552.0227 416.9835)" style="fill:#FCEBA2;" width="26" height="77.239"/>
<rect y="215.27" style="fill:#FFDE55;" width="77.24" height="26"/>
<rect x="379.3" y="215.27" style="fill:#FFDE55;" width="77.24" height="26"/>
<rect x="81.169" y="55.548" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -39.005 94.1686)" style="fill:#FCEBA2;" width="26" height="77.239"/>
<rect x="349.378" y="323.753" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -150.0985 362.3763)" style="fill:#FCEBA2;" width="26" height="77.239"/>
<circle style="fill:#FCEBA2;" cx="228.267" cy="228.271" r="124.003"/>
<circle style="fill:#FFDE55;" cx="228.267" cy="228.271" r="95.142"/>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 496.158 496.158" style="enable-background:new 0 0 496.158 496.158;" xml:space="preserve">
<path style="fill:#334D5C;" d="M248.082,0.003C111.07,0.003,0,111.063,0,248.085c0,137.001,111.07,248.07,248.082,248.07
c137.006,0,248.076-111.069,248.076-248.07C496.158,111.062,385.088,0.003,248.082,0.003z"/>
<path style="fill:#F2C900;" d="M322.377,80.781c10.1,22.706,15.721,47.844,15.721,74.298c0,101.079-81.94,183.019-183.019,183.019
c-26.454,0-51.591-5.622-74.298-15.721c28.49,64.053,92.674,108.721,167.298,108.721c101.078,0,183.019-81.94,183.019-183.019
C431.098,173.454,386.43,109.27,322.377,80.781z"/>
<g>
<path style="fill:#DBBB00;" d="M155.079,338.098c-26.454,0-51.591-5.622-74.298-15.721
c28.49,64.053,92.674,108.721,167.298,108.721c101.078,0,183.019-81.94,183.019-183.019
C431.098,248.079,256.157,338.098,155.079,338.098z"/>
<polygon style="fill:#DBBB00;" points="236.634,188.671 209.692,187.721 201.228,162.948 192.765,187.721 165.823,188.671
187.533,203.82 179.347,230.293 201.228,213.77 223.109,230.293 214.924,203.82 "/>
</g>
<polygon style="fill:#F2C900;" points="209.824,194.758 214.465,177.15 199.799,187.941 183.881,178.107 190.219,195.371
175.339,207.49 194.323,206.778 200.644,224.693 206.441,206.399 224.826,205.941 "/>
<polygon style="fill:#DBBB00;" points="101.521,229.699 82.134,229.015 76.043,211.187 69.952,229.015 50.564,229.699 66.187,240.6
60.296,259.651 76.043,247.761 91.789,259.651 85.898,240.6 "/>
<polygon style="fill:#F2C900;" points="82.228,234.079 85.568,221.408 75.014,229.174 63.558,222.096 68.12,234.52 57.412,243.241
71.074,242.729 75.623,255.621 79.795,242.456 93.024,242.127 "/>
<polygon style="fill:#DBBB00;" points="278.639,68.596 255.368,67.775 248.058,46.377 240.747,67.775 217.476,68.596
236.228,81.682 229.156,104.548 248.058,90.276 266.958,104.548 259.887,81.682 "/>
<polygon style="fill:#F2C900;" points="255.482,73.853 259.491,58.644 246.822,67.966 233.072,59.471 238.549,74.382 225.694,84.85
242.093,84.236 247.554,99.71 252.561,83.909 268.44,83.513 "/>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -24,7 +24,8 @@ class App extends React.Component {
this.state = {
currentUser: null,
fetchedMetadata: false,
onboarded: true
onboarded: true,
darkMode: localStorage.getItem('darkMode') === 'true'
};
}
@ -39,8 +40,13 @@ class App extends React.Component {
history.push('/login');
}
switchDarkMode = (newMode) => {
this.setState({ darkMode: newMode });
localStorage.setItem('darkMode', newMode);
}
render() {
const { currentUser, fetchedMetadata, updateAvailable, onboarded } = this.state;
const { currentUser, fetchedMetadata, updateAvailable, onboarded, darkMode } = this.state;
if(currentUser && fetchedMetadata === false) {
tooljetService.fetchMetaData().then((data) => {
@ -54,7 +60,7 @@ class App extends React.Component {
return (
<Router history={history}>
<div>
<div className={`main-wrapper ${darkMode ? 'theme-dark' : ''}`}>
{updateAvailable && <div className="alert alert-info alert-dismissible" role="alert">
<h3 className="mb-1">Update available</h3>
<p>A new version of ToolJet has been released.</p>
@ -70,16 +76,16 @@ class App extends React.Component {
<ToastContainer />
<PrivateRoute exact path="/" component={HomePage} />
<Route path="/login" component={LoginPage} />
<PrivateRoute exact path="/" component={HomePage} switchDarkMode={this.switchDarkMode} darkMode={darkMode}/>
<Route path="/login" component={LoginPage}/>
<Route path="/signup" component={SignupPage} />
<Route path = "/forgot-password" component ={ForgotPassword} />
<Route path = "/reset-password" component ={ResetPassword} />
<Route path="/invitations/:token" component={InvitationPage} />
<PrivateRoute exact path="/apps/:id" component={Editor} />
<PrivateRoute exact path="/applications/:slug" component={Viewer} />
<PrivateRoute exact path="/oauth2/authorize" component={Authorize} />
<PrivateRoute exact path="/users" component={ManageOrgUsers} />
<PrivateRoute exact path="/apps/:id" component={Editor} switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
<PrivateRoute exact path="/applications/:slug" component={Viewer} switchDarkMode={this.switchDarkMode} darkMode={darkMode}/>
<PrivateRoute exact path="/oauth2/authorize" component={Authorize} switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
<PrivateRoute exact path="/users" component={ManageOrgUsers} switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
</div>
</Router>
);

View file

@ -54,6 +54,7 @@ export const Box = function Box({
paramUpdated,
changeCanDrag,
containerProps,
darkMode
}) {
const backgroundColor = yellow ? 'yellow' : '';
@ -91,6 +92,7 @@ export const Box = function Box({
height={height}
component={component}
containerProps={containerProps}
darkMode={darkMode}
></ComponentToRender>
) : (
<div className="m-1" style={{ height: '100%' }}>

View file

@ -8,7 +8,8 @@ import 'codemirror/addon/display/placeholder';
import 'codemirror/addon/search/match-highlighter';
import 'codemirror/addon/hint/show-hint.css';
import 'codemirror/theme/base16-light.css';
import 'codemirror/theme/duotone-light.css';
import 'codemirror/theme/duotone-light.css'
import 'codemirror/theme/monokai.css';
import { getSuggestionKeys, onBeforeChange, handleChange } from './utils';
import { resolveReferences } from '@/_helpers/utils';
@ -25,6 +26,7 @@ export function CodeHinter({
enablePreview,
height
}) {
console.log('theme', theme)
const options = {
lineNumbers: lineNumbers,
singleLine: true,

View file

@ -9,7 +9,7 @@ const Plot = createPlotlyComponent(Plotly)
import Skeleton from 'react-loading-skeleton';
export const Chart = function Chart({
id, width, height, component, onComponentClick, currentState
id, width, height, component, onComponentClick, currentState, darkMode
}) {
console.log('currentState', currentState);
@ -27,7 +27,7 @@ export const Chart = function Chart({
const computedStyles = {
width,
height,
backgroundColor: 'white'
background: darkMode ? '#1f2936' : 'white'
};
const dataProperty = component.definition.properties.data;
@ -48,14 +48,23 @@ export const Chart = function Chart({
const layout = {
width,
height,
title,
plot_bgcolor: darkMode ? '#1f2936' : null,
paper_bgcolor: darkMode ? '#1f2936' : null,
title: {
text: title,
font: {
color: darkMode ? '#c3c3c3' : null
}
},
xaxis: {
showgrid: showGridLines,
showline: true
showline: true,
color: darkMode ? '#c3c3c3' : null
},
yaxis: {
showgrid: showGridLines,
showline: true
showline: true,
color: darkMode ? '#c3c3c3' : null
}
}
@ -101,7 +110,9 @@ export const Chart = function Chart({
>
{loadingState === true ?
<div style={{ width: '100%' }} className="p-2">
<Skeleton count={5} />
<center>
<div className="spinner-border mt-5" role="status"></div>
</center>
</div>
:
<Plot

View file

@ -31,7 +31,8 @@ export function Table({
paramUpdated,
changeCanDrag,
onComponentOptionChanged,
onComponentOptionsChanged
onComponentOptionsChanged,
darkMode
}) {
const color = component.definition.styles.textColor.value;
const actions = component.definition.properties.actions || { value: [] };
@ -472,6 +473,7 @@ export function Table({
<div className="ms-2 d-inline-block">
Search:{' '}
<input
className="global-search-field"
defaultValue={value || ''}
onBlur={(e) => {
handleSearchTextChange(e.target.value)
@ -494,7 +496,7 @@ export function Table({
return (
<div
className="card"
className="card jet-table"
style={{ width: `${width}px`, height: `${height}px` }}
onClick={() => onComponentClick(id, component)}
>
@ -587,7 +589,9 @@ export function Table({
</table>
{loadingState === true && (
<div style={{ width: '100%' }} className="p-2">
<Skeleton count={5} />
<center>
<div className="spinner-border mt-5" role="status"></div>
</center>
</div>
)}
</div>

View file

@ -45,7 +45,7 @@ export const Text = function Text({
{!loadingState && <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(data) }} />}
{loadingState === true && (
<div>
<Skeleton count={1} />
<div className="skeleton-line w-10"></div>
</div>
)}
</div>

View file

@ -30,7 +30,8 @@ export const Container = ({
removeComponent,
deviceWindowWidth,
scaleValue,
selectedComponent
selectedComponent,
darkMode
}) => {
const styles = {
@ -278,6 +279,7 @@ export const Container = ({
scaleValue={scaleValue}
deviceWindowWidth={deviceWindowWidth}
isSelectedComponent={selectedComponent? selectedComponent.id === key : false}
darkMode={darkMode}
containerProps={{
mode,
snapToGrid,
@ -295,7 +297,8 @@ export const Container = ({
currentLayout,
scaleValue,
deviceWindowWidth,
selectedComponent
selectedComponent,
darkMode
}}
/>
}

View file

@ -140,8 +140,8 @@ class DataSourceManager extends React.Component {
size={selectedDataSource ? 'lg' : 'xl'}
onEscapeKeyDown={this.hideModal}
className="mt-5"
contentClassName={this.props.darkMode ? 'theme-dark' : ''}
animation={false}
backdrop="static"
>
<Modal.Header>
<Modal.Title>
@ -175,7 +175,7 @@ class DataSourceManager extends React.Component {
</span>
)}
</Modal.Title>
<Button variant="light" size="sm" onClick={() => this.hideModal()}>
<Button variant={this.props.darkMode ? 'secondary' : 'light'} size="sm" onClick={() => this.hideModal()}>
x
</Button>
</Modal.Header>
@ -235,7 +235,7 @@ class DataSourceManager extends React.Component {
<div className="alert alert-info" role="alert">
<div className="text-muted">
Please white-list our IP address if your datasource is not publicly accessible.
IP: <span className="bg-light px-2 py-1">{config.SERVER_IP}</span>
IP: <span className="px-2 py-1">{config.SERVER_IP}</span>
<CopyToClipboard
text={config.SERVER_IP}
onCopy={() => toast.success('IP copied to clipboard', {
@ -244,7 +244,7 @@ class DataSourceManager extends React.Component {
})
}
>
<img src="/assets/images/icons/copy.svg" className="mx-1" width="14" height="14" role="button"/>
<img src="/assets/images/icons/copy.svg" className="mx-1 svg-icon" width="14" height="14" role="button"/>
</CopyToClipboard>
</div>
</div>

View file

@ -84,6 +84,7 @@ export const DraggableBox = function DraggableBox({
scaleValue,
deviceWindowWidth,
isSelectedComponent,
darkMode
}) {
const [isResizing, setResizing] = useState(false);
const [canDrag, setCanDrag] = useState(true);
@ -230,6 +231,7 @@ export const DraggableBox = function DraggableBox({
onComponentClick={onComponentClick}
currentState={currentState}
containerProps={containerProps}
darkMode={darkMode}
/>
</div>
</Rnd>
@ -247,6 +249,7 @@ export const DraggableBox = function DraggableBox({
onComponentOptionsChanged={onComponentOptionsChanged}
onComponentClick={onComponentClick}
currentState={currentState}
darkMode={darkMode}
/>
</div>
)}

View file

@ -2,6 +2,7 @@ import React from 'react';
import {
datasourceService, dataqueryService, appService, authenticationService
} from '@/_services';
import { DarkModeToggle } from '@/_components/DarkModeToggle';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { Container } from './Container';
@ -571,11 +572,18 @@ class Editor extends React.Component {
</div>
</div>
<div className="navbar-nav flex-row order-md-last">
<div className="mx-3" style={{ marginTop: '7px'}}>
<DarkModeToggle
switchDarkMode={this.props.switchDarkMode}
darkMode={this.props.darkMode}
/>
</div>
<div className="nav-item dropdown d-none d-md-flex me-3">
{app.id
&& <ManageAppUsers
app={app}
slug={slug}
darkMode={this.props.darkMode}
handleSlugChange={this.handleSlugChange} />}
</div>
<div className="nav-item dropdown d-none d-md-flex me-3">
@ -590,6 +598,7 @@ class Editor extends React.Component {
appName={app.name}
appDefinition={appDefinition}
app={app}
darkMode={this.props.darkMode}
onVersionDeploy={this.onVersionDeploy}
/>
)}
@ -606,7 +615,6 @@ class Editor extends React.Component {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: '#f0f0f0',
zIndex: '200'
}}
maxWidth={showLeftSidebar ? '30%' : '0%'}
@ -621,6 +629,7 @@ class Editor extends React.Component {
<div className="mb-2">
<ReactJson
style={{ fontSize: '0.7rem' }}
theme={this.props.darkMode ? 'shapeshifter' : 'rjv-default'}
enableClipboard={false}
src={currentState.globals}
name={'globals'}
@ -636,6 +645,7 @@ class Editor extends React.Component {
<div className="mb-2">
<ReactJson
src={currentState.components}
theme={this.props.darkMode ? 'shapeshifter' : 'rjv-default'}
name={'components'}
style={{ fontSize: '0.7rem' }}
enableClipboard={false}
@ -651,6 +661,7 @@ class Editor extends React.Component {
<div className="mb-2">
<ReactJson
src={currentState.queries}
theme={this.props.darkMode ? 'shapeshifter' : 'rjv-default'}
name={'queries'}
style={{ fontSize: '0.7rem' }}
enableClipboard={false}
@ -680,6 +691,7 @@ class Editor extends React.Component {
{this.state.showDataSourceManagerModal && (
<DataSourceManager
appId={appId}
darkMode={this.props.darkMode}
hideModal={() => this.setState({ showDataSourceManagerModal: false })}
dataSourcesChanged={this.dataSourcesChanged}
showDataSourceManagerModal={this.state.showDataSourceManagerModal}
@ -725,6 +737,7 @@ class Editor extends React.Component {
appDefinition={appDefinition}
appDefinitionChanged={this.appDefinitionChanged}
snapToGrid={true}
darkMode={this.props.darkMode}
mode={'edit'}
zoomLevel={zoomLevel}
currentLayout={currentLayout}
@ -838,6 +851,7 @@ class Editor extends React.Component {
editingQuery={editingQuery}
queryPaneHeight={queryPaneHeight}
currentState={currentState}
darkMode={this.props.darkMode}
/>
</div>
</div>
@ -849,33 +863,7 @@ class Editor extends React.Component {
<div className="editor-sidebar">
<div className="col-md-12">
<div>
<ul className="nav nav-tabs" data-bs-toggle="tabs">
<li className="nav-item col-md-6">
<a
onClick={() => this.switchSidebarTab(1)}
className={currentSidebarTab === 1 ? 'nav-link active' : 'nav-link'}
data-bs-toggle="tab"
>
<img
src="/assets/images/icons/lens.svg"
width="16"
height="16"
className="d-md-none d-lg-block"
/>
&nbsp; Properties
</a>
</li>
<li className="nav-item col-md-6">
<a
onClick={() => this.switchSidebarTab(2)}
className={currentSidebarTab === 2 ? 'nav-link active' : 'nav-link'}
data-bs-toggle="tab"
>
<img src="/assets/images/icons/insert.svg" width="16" height="16" className="d-md-none d-lg-block"/>
&nbsp; Widgets
</a>
</li>
</ul>
</div>
</div>
@ -891,7 +879,9 @@ class Editor extends React.Component {
currentState={currentState}
allComponents={appDefinition.components}
key={selectedComponent.id}
switchSidebarTab={this.switchSidebarTab}
apps={apps}
darkMode={this.props.darkMode}
></Inspector>
) : (
<div className="mt-5 p-2">Please select a component to inspect</div>

View file

@ -72,7 +72,7 @@ class Chart extends React.Component {
<CodeHinter
currentState={this.props.currentState}
initialValue={data.value}
theme="duotone-light"
theme={this.props.darkMode ? 'monokai' : 'duotone-light'}
mode= "javascript"
lineNumbers={false}
className="chart-input pr-2"

View file

@ -25,7 +25,7 @@ class Table extends React.Component {
eventUpdated,
eventOptionUpdated,
components,
currentState
currentState,
};
}
@ -239,8 +239,8 @@ class Table extends React.Component {
actionButton(action, index) {
return (
<OverlayTrigger trigger="click" placement="left" rootClose overlay={this.actionPopOver(action, index)}>
<div className="card p-2 bg-light" role="button">
<div className="row bg-light">
<div className={`card p-2 ${this.props.darkMode ? 'bg-secondary' : 'bg-light'}`} role="button">
<div className={`row ${this.props.darkMode ? '' : 'bg-light'}`}>
<div className="col-auto">
<div className="text">{action.buttonText}</div>
</div>
@ -355,13 +355,14 @@ class Table extends React.Component {
<div>
<SortableList onSortEnd={this.onSortEnd} className="w-100" draggedItemClassName="dragged">
{columns.value.map((item, index) => (
<div className="card p-2 bg-light column-sort-row" key={index}>
<div className={`card p-2 column-sort-row ${this.props.darkMode ? '' : 'bg-light'}`} key={index}>
<OverlayTrigger trigger="click" placement="left" rootClose overlay={this.columnPopover(item, index)}>
<div className="row bg-light" role="button">
<div className={`row ${this.props.darkMode ? '' : 'bg-light'}`} role="button">
<div className="col-auto">
<SortableItem key={item.name}>
<img
style={{ cursor: 'move' }}
className="svg-icon"
src="/assets/images/icons/editor/rearrange.svg"
width="10"
height="10"

View file

@ -3,7 +3,7 @@ import { CodeHinter } from '../../CodeBuilder/CodeHinter';
import { ToolTip } from './Components/ToolTip';
export const Code = ({
param, definition, onChange, paramType, dataQueries, components, componentMeta, currentState
param, definition, onChange, paramType, dataQueries, components, componentMeta, currentState, darkMode
}) => {
const initialValue = definition ? definition.value : '';
const paramMeta = componentMeta[paramType][param.name];
@ -22,7 +22,7 @@ export const Code = ({
currentState={currentState}
initialValue={initialValue}
mode={options.mode}
theme={options.theme}
theme={darkMode? 'monokai' : options.theme}
className={options.className}
onChange={(value) => handleCodeChanged(value)}
/>

View file

@ -17,7 +17,9 @@ export const Inspector = ({
allComponents,
componentChanged,
currentState,
apps
apps,
darkMode,
switchSidebarTab
}) => {
const selectedComponent = { id: selectedComponentId, component: allComponents[selectedComponentId].component, layouts: allComponents[selectedComponentId].layouts}
@ -153,7 +155,7 @@ export const Inspector = ({
return (
<div className="inspector">
<div className="header p-2 row">
<div className="header px-2 py-1 row">
<div className="col-auto">
<div className="input-icon">
<input
@ -167,33 +169,13 @@ export const Inspector = ({
</span>
</div>
</div>
<div className="col pt-2">
<OverlayTrigger
trigger="click"
placement="left"
overlay={
<Popover id="popover-basic">
{/* <Popover.Title as="h3">brrr</Popover.Title> */}
<Popover.Content>
<div className="field mb-2">
<button className="btn btn-light btn-sm mb-2">Duplicate</button>
<br></br>
<button className="btn btn-danger btn-sm" onClick={() => removeComponent(component)}>
Remove
</button>
</div>
</Popover.Content>
</Popover>
}
>
<img
role="button"
className="component-action-button"
src="/assets/images/icons/app-menu.svg"
width="15"
height="15"
/>
</OverlayTrigger>
<div className="col py-1">
<button
className="btn btn-sm component-action-button btn-light"
onClick={() => switchSidebarTab(2)}
>
x
</button>
</div>
</div>
@ -208,6 +190,7 @@ export const Inspector = ({
eventOptionUpdated={eventOptionUpdated}
components={components}
currentState={currentState}
darkMode={darkMode}
/>
}
@ -221,19 +204,19 @@ export const Inspector = ({
eventOptionUpdated={eventOptionUpdated}
components={components}
currentState={currentState}
darkMode={darkMode}
/>
}
{!['Table', 'Chart'].includes(componentMeta.component) &&
<div className="properties-container p-2">
{Object.keys(componentMeta.properties).map((property) => renderElement(component, componentMeta, paramUpdated, dataQueries, property, 'properties', currentState, components))}
<div className="hr-text">Style</div>
{Object.keys(componentMeta.properties).map((property) => renderElement(component, componentMeta, paramUpdated, dataQueries, property, 'properties', currentState, components, darkMode))}
{Object.keys(componentMeta.styles).length > 0 && <div className="hr-text">Style</div>}
{Object.keys(componentMeta.styles).map((style) => renderElement(component, componentMeta, paramUpdated, dataQueries, style, 'styles', currentState, components))}
<div className="hr-text">Events</div>
{Object.keys(componentMeta.events).map((eventName) => renderEvent(component, eventUpdated, dataQueries, eventOptionUpdated, eventName, componentMeta.events[eventName], currentState, components, apps))}
{Object.keys(componentMeta.events).length > 0 && <div className="hr-text">Events</div>}
{Object.keys(componentMeta.events).map((eventName) => renderEvent(component, eventUpdated, dataQueries, eventOptionUpdated, eventName, componentMeta.events[eventName], currentState, components, apps))}
</div>
}

View file

@ -31,7 +31,7 @@ export function renderQuerySelector(component, dataQueries, eventOptionUpdated,
/>)
}
export function renderElement(component, componentMeta, paramUpdated, dataQueries, param, paramType, currentState, components = {}) {
export function renderElement(component, componentMeta, paramUpdated, dataQueries, param, paramType, currentState, components = {}, darkMode = false) {
const definition = component.component.definition[paramType][param];
const meta = componentMeta[paramType][param];
console.log('definition', definition);
@ -47,6 +47,7 @@ export function renderElement(component, componentMeta, paramUpdated, dataQuerie
components={components}
componentMeta={componentMeta}
currentState={currentState}
darkMode={darkMode}
/>
);
}

View file

@ -147,11 +147,21 @@ class ManageAppUsers extends React.Component {
Share
</button>
<Modal show={this.state.showModal} size="lg" backdrop="static" centered={true} keyboard={true} onEscapeKeyDown={this.hideModal} className="app-sharing-modal">
<Modal
show={this.state.showModal}
size="lg"
backdrop="static"
centered={true}
keyboard={true}
animation={false}
onEscapeKeyDown={this.hideModal}
className="app-sharing-modal"
contentClassName={this.props.darkMode ? 'theme-dark' : ''}
>
<Modal.Header>
<Modal.Title>Users and permissions</Modal.Title>
<div>
<Button variant="light" size="sm" onClick={() => this.hideModal()}>
<Button variant={this.props.darkMode ? 'secondary' : 'light'} size="sm" onClick={() => this.hideModal()}>
x
</Button>
</div>
@ -203,7 +213,7 @@ class ManageAppUsers extends React.Component {
})
}
>
<button className="btn btn-light btn-sm">Copy</button>
<button className="btn btn-secondary btn-sm">Copy</button>
</CopyToClipboard>
</span>
<div className="invalid-feedback">{slugError}</div>
@ -257,7 +267,7 @@ class ManageAppUsers extends React.Component {
</div>
</div>
<div className="table-responsive">
<table className="table table-vcenter">
<table className="table table-vcenter app-users-list">
<thead>
<tr>
<th>Name</th>

View file

@ -65,6 +65,7 @@ class Airtable extends React.Component {
currentState={this.props.currentState}
initialValue={this.state.options.base_id}
className="codehinter-query-editor-input"
theme={this.props.darkMode ? 'monokai' : 'default'}
onChange={(value) => changeOption(this, 'base_id', value)}
/>
</div>
@ -74,6 +75,7 @@ class Airtable extends React.Component {
currentState={this.props.currentState}
initialValue={this.state.options.table_name}
className="codehinter-query-editor-input"
theme={this.props.darkMode ? 'monokai' : 'default'}
onChange={(value) => changeOption(this, 'table_name', value)}
/>
</div>
@ -83,6 +85,7 @@ class Airtable extends React.Component {
currentState={this.props.currentState}
initialValue={this.state.options.page_size}
className="codehinter-query-editor-input"
theme={this.props.darkMode ? 'monokai' : 'default'}
onChange={(value) => changeOption(this, 'page_size', value)}
/>
</div>
@ -92,6 +95,7 @@ class Airtable extends React.Component {
currentState={this.props.currentState}
initialValue={this.state.options.offset}
className="codehinter-query-editor-input"
theme={this.props.darkMode ? 'monokai' : 'default'}
onChange={(value) => changeOption(this, 'offset', value)}
/>
</div>
@ -106,6 +110,7 @@ class Airtable extends React.Component {
currentState={this.props.currentState}
initialValue={this.state.options.base_id}
className="codehinter-query-editor-input"
theme={this.props.darkMode ? 'monokai' : 'default'}
onChange={(value) => changeOption(this, 'base_id', value)}
/>
</div>
@ -115,6 +120,7 @@ class Airtable extends React.Component {
currentState={this.props.currentState}
initialValue={this.state.options.table_name}
className="codehinter-query-editor-input"
theme={this.props.darkMode ? 'monokai' : 'default'}
onChange={(value) => changeOption(this, 'table_name', value)}
/>
</div>
@ -124,6 +130,7 @@ class Airtable extends React.Component {
currentState={this.props.currentState}
initialValue={this.state.options.record_id}
className="codehinter-query-editor-input"
theme={this.props.darkMode ? 'monokai' : 'default'}
onChange={(value) => changeOption(this, 'record_id', value)}
/>
</div>

View file

@ -67,9 +67,9 @@ class Dynamodb extends React.Component {
<CodeHinter
currentState={this.props.currentState}
initialValue={typeof this.state.options.query_condition === 'string' ? this.state.options.query_condition : JSON.stringify(this.state.options.query_condition )}
theme="duotone-light"
mode="javascript"
lineNumbers={true}
theme={this.props.darkMode ? 'monokai' : 'duotone-light'}
className="query-hinter"
onChange={(value) => changeOption(this, 'query_condition', value)}
/>
@ -84,9 +84,9 @@ class Dynamodb extends React.Component {
<CodeHinter
currentState={this.props.currentState}
initialValue={typeof this.state.options.scan_condition === 'string' ? this.state.options.scan_condition : JSON.stringify(this.state.options.scan_condition )}
theme="duotone-light"
mode="javascript"
lineNumbers={true}
theme={this.props.darkMode ? 'monokai' : 'duotone-light'}
className="query-hinter"
onChange={(value) => changeOption(this, 'scan_condition', value)}
/>
@ -101,6 +101,7 @@ class Dynamodb extends React.Component {
<CodeHinter
currentState={this.props.currentState}
initialValue={this.state.options.table}
theme={this.props.darkMode ? 'monokai' : 'default'}
className="codehinter-query-editor-input"
onChange={(value) => changeOption(this, 'table', value)}
/>
@ -110,7 +111,7 @@ class Dynamodb extends React.Component {
<CodeHinter
currentState={this.props.currentState}
initialValue={typeof this.state.options.key === 'string' ? this.state.options.key : JSON.stringify(this.state.options.key )}
theme="duotone-light"
theme={this.props.darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={true}
className="query-hinter"

View file

@ -67,6 +67,7 @@ class Elasticsearch extends React.Component {
<CodeHinter
currentState={this.props.currentState}
initialValue={this.state.options.index}
theme={this.props.darkMode ? 'monokai' : 'default'}
onChange={(value) => changeOption(this, 'index', value)}
/>
</div>
@ -75,6 +76,7 @@ class Elasticsearch extends React.Component {
<CodeHinter
currentState={this.props.currentState}
initialValue={this.state.options.id}
theme={this.props.darkMode ? 'monokai' : 'default'}
onChange={(value) => changeOption(this, 'id', value)}
/>
</div>
@ -85,7 +87,7 @@ class Elasticsearch extends React.Component {
initialValue={options.body}
mode="javascript"
placeholder={'{ doc: { page_count: 225 } }'}
theme="duotone-light"
theme={this.props.darkMode ? 'monokai' : 'duotone-light'}
lineNumbers={true}
className="query-hinter"
onChange={(value) => changeOption(this, 'body', value)}
@ -101,6 +103,7 @@ class Elasticsearch extends React.Component {
<CodeHinter
currentState={this.props.currentState}
initialValue={this.state.options.index}
theme={this.props.darkMode ? 'monokai' : 'default'}
onChange={(value) => changeOption(this, 'index', value)}
/>
</div>
@ -109,6 +112,7 @@ class Elasticsearch extends React.Component {
<CodeHinter
currentState={this.props.currentState}
initialValue={this.state.options.id}
theme={this.props.darkMode ? 'monokai' : 'default'}
onChange={(value) => changeOption(this, 'id', value)}
/>
</div>
@ -122,6 +126,7 @@ class Elasticsearch extends React.Component {
<CodeHinter
currentState={this.props.currentState}
initialValue={this.state.options.index}
theme={this.props.darkMode ? 'monokai' : 'default'}
onChange={(value) => changeOption(this, 'index', value)}
/>
</div>
@ -132,7 +137,7 @@ class Elasticsearch extends React.Component {
initialValue={options.body}
mode="javascript"
placeholder={'{ "name": "The Hitchhikers Guide to the Galaxy" }'}
theme="duotone-light"
theme={this.props.darkMode ? 'monokai' : 'duotone-light'}
lineNumbers={true}
className="query-hinter"
onChange={(value) => changeOption(this, 'body', value)}
@ -147,6 +152,7 @@ class Elasticsearch extends React.Component {
<CodeHinter
currentState={this.props.currentState}
initialValue={this.state.options.index}
theme={this.props.darkMode ? 'monokai' : 'default'}
onChange={(value) => changeOption(this, 'index', value)}
/>
@ -158,7 +164,7 @@ class Elasticsearch extends React.Component {
initialValue={options.query}
mode="sql"
placeholder={'{ "name": "" }'}
theme="duotone-light"
theme={this.props.darkMode ? 'monokai' : 'duotone-light'}
lineNumbers={true}
className="query-hinter"
onChange={(value) => changeOption(this, 'query', value)}

View file

@ -75,13 +75,14 @@ class Firestore extends React.Component {
placeholder="Select.."
/>
</div>
{this.state.options.operation === 'get_document' || this.state.options.operation === 'delete_document' && (
{(this.state.options.operation === 'get_document' || this.state.options.operation === 'delete_document') && (
<div>
<div className="mb-3 mt-2">
<label className="form-label">Path</label>
<CodeHinter
currentState={this.props.currentState}
initialValue={this.state.options.path}
theme={this.props.darkMode ? 'monokai' : 'default'}
onChange={(value) => changeOption(this, 'path', value)}
/>
</div>
@ -95,6 +96,7 @@ class Firestore extends React.Component {
<CodeHinter
currentState={this.props.currentState}
initialValue={this.state.options.path}
theme={this.props.darkMode ? 'monokai' : 'default'}
onChange={(value) => changeOption(this, 'path', value)}
/>
</div>
@ -106,6 +108,7 @@ class Firestore extends React.Component {
theme="duotone-light"
lineNumbers={true}
className="query-hinter"
theme={this.props.darkMode ? 'monokai' : 'default'}
onChange={(value) => changeOption(this, 'body', value)}
/>
</div>
@ -118,6 +121,7 @@ class Firestore extends React.Component {
<CodeHinter
currentState={this.props.currentState}
initialValue={this.state.options.collection}
theme={this.props.darkMode ? 'monokai' : 'default'}
onChange={(value) => changeOption(this, 'collection', value)}
/>
</div>
@ -126,6 +130,7 @@ class Firestore extends React.Component {
<CodeHinter
currentState={this.props.currentState}
initialValue={this.state.options.document_id_key}
theme={this.props.darkMode ? 'monokai' : 'default'}
onChange={(value) => changeOption(this, 'document_id_key', value)}
/>
</div>
@ -138,7 +143,7 @@ class Firestore extends React.Component {
onChange={(instance) => changeOption(this, 'records', instance.getValue())}
placeholder="{ }"
options={{
theme: 'duotone-light',
theme: this.props.darkMode ? 'monokai' : 'default',
mode: 'javascript',
lineWrapping: true,
scrollbarStyle: null
@ -154,6 +159,7 @@ class Firestore extends React.Component {
<CodeHinter
currentState={this.props.currentState}
initialValue={this.state.options.path}
theme={this.props.darkMode ? 'monokai' : 'default'}
className="codehinter-query-editor-input"
onChange={(value) => changeOption(this, 'path', value)}
/>
@ -164,6 +170,7 @@ class Firestore extends React.Component {
currentState={this.props.currentState}
initialValue={this.state.options.order}
className="codehinter-query-editor-input"
theme={this.props.darkMode ? 'monokai' : 'default'}
onChange={(value) => changeOption(this, 'order', value)}
/>
</div>
@ -173,6 +180,7 @@ class Firestore extends React.Component {
currentState={this.props.currentState}
initialValue={this.state.options.limit}
className="codehinter-query-editor-input"
theme={this.props.darkMode ? 'monokai' : 'default'}
onChange={(value) => changeOption(this, 'limit', value)}
/>
</div>
@ -183,6 +191,7 @@ class Firestore extends React.Component {
<CodeHinter
currentState={this.props.currentState}
className="codehinter-query-editor-input"
theme={this.props.darkMode ? 'monokai' : 'default'}
initialValue={this.state.options.where_field}
onChange={(value) => changeOption(this, 'where_field', value)}
/>
@ -214,6 +223,7 @@ class Firestore extends React.Component {
<CodeHinter
currentState={this.props.currentState}
className="codehinter-query-editor-input"
theme={this.props.darkMode ? 'monokai' : 'default'}
initialValue={this.state.options.where_value}
onChange={(value) => changeOption(this, 'where_value', value)}
/>

View file

@ -110,7 +110,7 @@ class Googlesheets extends React.Component {
<CodeHinter
currentState={this.props.currentState}
initialValue={options.rows}
theme="duotone-light"
theme={this.props.darkMode ? 'monokai' : 'duotone-light'}
lineNumbers={true}
className="query-hinter"
onChange={(value) => changeOption(this, 'rows', value)}

View file

@ -26,7 +26,7 @@ class Graphql extends React.Component {
currentState={this.props.currentState}
initialValue={options.query}
mode="sql"
theme="duotone-light"
theme={this.props.darkMode ? 'monokai' : 'duotone-light'}
lineNumbers={true}
className="query-hinter"
onChange={(value) => changeOption(this, 'query', value)}

View file

@ -80,6 +80,7 @@ class Mongodb extends React.Component {
<CodeHinter
currentState={this.props.currentState}
initialValue={this.state.options.collection}
theme={this.props.darkMode ? 'monokai' : 'default'}
onChange={(value) => changeOption(this, 'collection', value)}
/>
</div>
@ -89,7 +90,7 @@ class Mongodb extends React.Component {
<CodeHinter
currentState={this.props.currentState}
initialValue={this.state.options.document}
theme="duotone-light"
theme={this.props.darkMode ? 'monokai' : 'duotone-light'}
mode="javascript"
lineNumbers={true}
placeholder={placeholders['mongodb']['insert_one']}
@ -107,6 +108,7 @@ class Mongodb extends React.Component {
<CodeHinter
currentState={this.props.currentState}
initialValue={this.state.options.collection}
theme={this.props.darkMode ? 'monokai' : 'default'}
onChange={(value) => changeOption(this, 'collection', value)}
/>
</div>
@ -116,7 +118,7 @@ class Mongodb extends React.Component {
<CodeHinter
currentState={this.props.currentState}
initialValue={this.state.options.documents}
theme="duotone-light"
theme={this.props.darkMode ? 'monokai' : 'duotone-light'}
mode="javascript"
lineNumbers={true}
className="query-hinter"

View file

@ -26,7 +26,7 @@ class Mssql extends React.Component {
currentState={this.props.currentState}
initialValue={options.query}
mode="sql"
theme="duotone-light"
theme={this.props.darkMode ? 'monokai' : 'duotone-light'}
lineNumbers={true}
className="query-hinter"
onChange={(value) => changeOption(this, 'query', value)}

View file

@ -28,7 +28,7 @@ class Mysql extends React.Component {
currentState={this.props.currentState}
initialValue={options.query}
mode="sql"
theme="duotone-light"
theme={this.props.darkMode ? 'monokai' : 'duotone-light'}
lineNumbers={true}
className="query-hinter"
onChange={(value) => changeOption(this, 'query', value)}

View file

@ -49,7 +49,7 @@ class Postgresql extends React.Component {
currentState={this.props.currentState}
initialValue={options.query}
mode="sql"
theme="duotone-light"
theme={this.props.darkMode ? 'monokai' : 'duotone-light'}
lineNumbers={true}
className="query-hinter"
enablePreview

View file

@ -33,7 +33,7 @@ class Redis extends React.Component {
onChange={(instance) => changeOption(this, 'query', instance.getValue())}
placeholder="PING"
options={{
theme: 'duotone-light',
theme: this.props.darkMode ? 'monokai' : 'duotone-light',
mode: 'sql',
lineWrapping: true,
scrollbarStyle: null

View file

@ -95,6 +95,7 @@ class Restapi extends React.Component {
currentState={this.props.currentState}
initialValue={options.url}
className="codehinter-query-editor-input"
theme={this.props.darkMode ? 'monokai' : 'default'}
onChange={(value) => {
changeOption(this, 'url', value);
}}
@ -119,6 +120,7 @@ class Restapi extends React.Component {
<CodeHinter
currentState={this.props.currentState}
initialValue={pair[0]}
theme={this.props.darkMode ? 'monokai' : 'default'}
className="form-control codehinter-query-editor-input"
onChange={(value) => this.keyValuePairValueChanged(value, 0, option.value, index)}
/>
@ -126,6 +128,7 @@ class Restapi extends React.Component {
currentState={this.props.currentState}
className="form-control codehinter-query-editor-input"
initialValue={pair[1]}
theme={this.props.darkMode ? 'monokai' : 'default'}
onChange={(value) => this.keyValuePairValueChanged(value, 1, option.value, index)}
/>
<span

View file

@ -80,6 +80,7 @@ class Slack extends React.Component {
currentState={this.props.currentState}
initialValue={this.state.options.channel}
className="codehinter-query-editor-input"
theme={this.props.darkMode ? 'monokai' : 'default'}
onChange={(value) => changeOption(this, 'channel', value)}
/>
</div>
@ -91,6 +92,7 @@ class Slack extends React.Component {
currentState={this.props.currentState}
initialValue={options.message}
className="codehinter-query-editor-input"
theme={this.props.darkMode ? 'monokai' : 'default'}
onChange={(value) => changeOption(this, 'message', value)}
/>
</div>

View file

@ -255,7 +255,7 @@ class QueryManager extends React.Component {
autoFocus={false}
/>
<span className="input-icon-addon">
<img src="/assets/images/icons/edit.svg" width="12" height="12" />
<img className="svg-icon" src="/assets/images/icons/edit.svg" width="12" height="12" />
</span>
</div>
</div>
@ -278,7 +278,7 @@ class QueryManager extends React.Component {
this.previewPanelRef.current.scrollIntoView();
})
.catch(({ error, data }) => {
debugger;
});
}}
className={`btn btn-secondary m-1 float-right1 ${previewLoading ? ' btn-loading' : ''}`}
@ -348,6 +348,7 @@ class QueryManager extends React.Component {
options={this.state.options}
optionsChanged={this.optionsChanged}
currentState={currentState}
darkMode={this.props.darkMode}
/>
<hr></hr>
<div className="mb-3 mt-2">
@ -355,6 +356,7 @@ class QueryManager extends React.Component {
changeOption={this.optionchanged}
options={this.state.options}
currentState={currentState}
darkMode={this.props.darkMode}
/>
</div>
<div className="row preview-header border-top" ref={this.previewPanelRef}>
@ -373,6 +375,7 @@ class QueryManager extends React.Component {
style={{ fontSize: '0.7rem' }}
enableClipboard={false}
src={queryPreviewData}
theme={this.props.darkMode ? 'shapeshifter' : 'rjv-default'}
displayDataTypes={true}
collapsed={false}
displayObjectSize={true}

View file

@ -8,7 +8,7 @@ import 'codemirror/addon/search/match-highlighter';
import 'codemirror/addon/hint/show-hint.css';
import { CodeHinter } from '../CodeBuilder/CodeHinter';
export const Transformation = ({ changeOption, options, currentState }) => {
export const Transformation = ({ changeOption, options, currentState, darkMode }) => {
const defaultValue = options.transformation
|| `// write your code here
// return value will be set as data and the original data will be available as rawData
@ -51,7 +51,7 @@ return data.filter(row => row.amount > 1000);`;
currentState={currentState}
initialValue={value}
mode="javascript"
theme="base16-light"
theme={darkMode? 'monokai' : 'base16-light'}
lineNumbers={true}
className="query-hinter"
ignoreBraces={true}

View file

@ -1,15 +1,19 @@
export const defaultOptions = {
postgresql: {},
postgresql: {
mode: 'sql'
},
redis: {
query: 'PING'
},
mysql: {},
graphql: {},
firestore: {
path: ''
path: '',
operation: 'get_document'
},
elasticsearch: {
query: ''
query: '',
operation: 'search'
},
restapi: {
method: 'GET',

View file

@ -91,6 +91,7 @@ class SaveAndPreview extends React.Component {
enforceFocus={false}
animation={false}
onEscapeKeyDown={() => this.hideModal()}
contentClassName={this.props.darkMode ? 'theme-dark' : ''}
>
<Modal.Header>
<Modal.Title>Versions and deployments</Modal.Title>
@ -101,7 +102,7 @@ class SaveAndPreview extends React.Component {
</button>
)}
<Button variant="light" size="sm" onClick={() => this.hideModal()}>
<Button variant={this.props.darkMode ? 'secondary' : 'light'}size="sm" onClick={() => this.hideModal()}>
x
</Button>
</div>

View file

@ -15,6 +15,7 @@ import {
runQuery
} from '@/_helpers/appUtils';
import queryString from 'query-string';
import { DarkModeToggle } from '@/_components/DarkModeToggle';
class Viewer extends React.Component {
constructor(props) {
@ -137,7 +138,12 @@ class Viewer extends React.Component {
</a>
</h1>
{this.state.app && <span>{this.state.app.name}</span>}
<div className="navbar-nav flex-row order-md-last"></div>
<div className="navbar-nav flex-row order-md-last">
<DarkModeToggle
switchDarkMode={this.props.switchDarkMode}
darkMode={this.props.darkMode}
/>
</div>
</div>
</header>
</div>
@ -150,6 +156,7 @@ class Viewer extends React.Component {
appDefinitionChanged={() => false} // function not relevant in viewer
snapToGrid={true}
appLoading={isLoading}
darkMode={this.props.darkMode}
onEvent={(eventName, options) => onEvent(this, eventName, options, 'view')}
mode="view"
scaleValue={scaleValue}

View file

@ -89,7 +89,7 @@ export const AppMenu = function AppMenu({
}
>
<span className="badge bg-blue-lt mx-2" role="button">
<img src="/assets/images/icons/app-menu.svg" width="12" height="12" />
<img className="svg-icon" src="/assets/images/icons/app-menu.svg" width="12" height="12" />
</span>
</OverlayTrigger>
}

View file

@ -36,7 +36,7 @@ export const Folders = function Folders({
folderChanged(folder);
}
return (<div className="w-100 mt-4 px-3 card">
return (<div className="w-100 mt-4 px-3 card folder-list">
{isLoading && (
<div className="px-1 py-2" style={{minHeight: '200px'}}>
{[1,2,3,4, 5].map(element => {

View file

@ -125,7 +125,8 @@ class HomePage extends React.Component {
/>
<Header
switchDarkMode={this.props.switchDarkMode}
darkMode={this.props.darkMode}
/>
{!isLoading && meta.total_count === 0 &&
<BlankPage
@ -162,10 +163,10 @@ class HomePage extends React.Component {
</div>
</div>
<div className={currentFolder.count == 0 ? 'table-responsive bg-white w-100 apps-table mt-3 d-flex align-items-center' : 'table-responsive bg-white w-100 apps-table mt-3'} style={{minHeight: '600px'}}>
<div className={currentFolder.count == 0 ? 'table-responsive w-100 apps-table mt-3 d-flex align-items-center' : 'table-responsive w-100 apps-table mt-3'} style={{minHeight: '600px'}}>
<table
data-testid="appsTable"
className="table table-vcenter">
className={`table table-vcenter ${this.props.darkMode ? 'bg-dark' : 'bg-white' }`}>
<tbody>
{isLoading && (
<>
@ -196,7 +197,7 @@ class HomePage extends React.Component {
<tr className="row">
<td className="col p-3">
<span className="app-title mb-3">{app.name}</span> <br />
<small className="pt-2">created {app.created_at} ago by {app.user.first_name} {app.user.last_name} </small>
<small className="pt-2 app-description">created {app.created_at} ago by {app.user.first_name} {app.user.last_name} </small>
</td>
<td className="text-muted col-auto pt-4">
<Link

View file

@ -109,7 +109,10 @@ class ManageOrgUsers extends React.Component {
return (
<div className="wrapper org-users-page">
<Header />
<Header
switchDarkMode={this.props.switchDarkMode}
darkMode={this.props.darkMode}
/>
<div className="page-wrapper">
<div className="container-xl">
@ -269,7 +272,7 @@ class ManageOrgUsers extends React.Component {
</span>
</td>
<td className="text-muted">
<a href="#" className="text-reset">
<a href="#" className="text-reset user-email">
{user.email}
</a>
</td>
@ -295,7 +298,7 @@ class ManageOrgUsers extends React.Component {
<span
className={`badge bg-${user.status === 'invited' ? 'warning' : 'success'} me-1 m-1`}
></span>
<small>{user.status}</small>
<small className="user-status">{user.status}</small>
</td>
<td>
{archivingUser === null && (

View file

@ -0,0 +1,22 @@
import React, { useState, useEffect } from 'react';
export const DarkModeToggle = function DarkModeToggle({
darkMode, switchDarkMode
}) {
const [darkModeEnabled, setMode] = useState(darkMode);
const icon = darkModeEnabled ? 'night.svg' : 'day.svg';
return <div>
<label className="form-check form-switch my-2">
<img src={`/assets/images/icons/${icon}`} width="16" height="16" />
<input
className="form-check-input"
type="checkbox"
onClick={() => { switchDarkMode(!darkModeEnabled); setMode(!darkModeEnabled); } }
checked={darkModeEnabled}
/>
</label>
</div>
}

View file

@ -2,9 +2,10 @@ import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { authenticationService } from '@/_services';
import { history } from '@/_helpers';
import { DarkModeToggle } from './DarkModeToggle';
export const Header = function Header({
switchDarkMode, darkMode
}) {
const [pahtName, setPathName] = useState(document.location.pathname);
@ -35,7 +36,7 @@ export const Header = function Header({
<li className={`nav-item mx-3 ${pahtName === '/' ? 'active' : ''}`}>
<Link to={'/'} className="nav-link">
<span className="nav-link-icon d-md-none d-lg-inline-block">
<img src="/assets/images/icons/apps.svg" width="15" height="15" />
<img className="svg-icon" src="/assets/images/icons/apps.svg" width="15" height="15" />
</span>
<span className="nav-link-title">
Apps
@ -46,7 +47,7 @@ export const Header = function Header({
<li className={`nav-item ${pahtName === '/users' ? 'active' : ''}`}>
<Link to={'/users'} className="nav-link">
<span className="nav-link-icon d-md-none d-lg-inline-block">
<img src="/assets/images/icons/users.svg" width="15" height="15" />
<img className="svg-icon" src="/assets/images/icons/users.svg" width="15" height="15" />
</span>
<span className="nav-link-title">
Users
@ -55,6 +56,12 @@ export const Header = function Header({
</li>
</ul>
<div className="navbar-nav flex-row order-md-last">
<div className="p-1">
<DarkModeToggle
switchDarkMode={switchDarkMode}
darkMode={darkMode}
/>
</div>
<div className="nav-item dropdown">
<a
href="#"

View file

@ -3,7 +3,7 @@ import { Route, Redirect } from 'react-router-dom';
import { authenticationService } from '@/_services';
export const PrivateRoute = ({ component: Component, ...rest }) => (
export const PrivateRoute = ({ component: Component, switchDarkMode, darkMode, ...rest }) => (
<Route
{...rest}
render={(props) => {
@ -14,7 +14,7 @@ export const PrivateRoute = ({ component: Component, ...rest }) => (
}
// authorised so return component
return <Component {...props} />;
return <Component {...props} switchDarkMode={switchDarkMode} darkMode={darkMode}/>;
}}
/>
);

View file

@ -2,3 +2,4 @@ export * from './PrivateRoute';
export * from './Pagination';
export * from './Header';
export * from './ConfirmDialog';
export * from './DarkModeToggle';

View file

@ -16,7 +16,7 @@ export const authenticationService = {
function login(email, password) {
const requestOptions = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password })
};
@ -34,7 +34,7 @@ function login(email, password) {
function signup(email) {
const requestOptions = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email })
};

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