mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-23 17:08:34 +00:00
Merge branch 'release/v0.5.13' into main
This commit is contained in:
commit
f279d732fc
130 changed files with 1384 additions and 749 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ApplicationCable
|
||||
class Channel < ActionCable::Channel::Base
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ApplicationCable
|
||||
class Connection < ActionCable::Connection::Base
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ApplicationJob < ActiveJob::Base
|
||||
# Automatically retry jobs that encountered a deadlock
|
||||
# retry_on ActiveRecord::Deadlocked
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,2 +1,4 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Metadatum < ApplicationRecord
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AppPolicy < ApplicationPolicy
|
||||
attr_reader :user, :app
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AppUserPolicy < ApplicationPolicy
|
||||
attr_reader :user, :app_user
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ApplicationPolicy
|
||||
attr_reader :user, :record
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class OrganizationUserPolicy < ApplicationPolicy
|
||||
attr_reader :user, :organization_user
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DataSourceConnectionService
|
||||
attr_accessor :data_source_kind, :options
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Be sure to restart your server when you modify this file.
|
||||
|
||||
# ActiveSupport::Reloader.to_prepare do
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -1 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
$connections = {}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
Rails.application.config.generators do |g|
|
||||
g.orm :active_record, primary_key_type: :uuid
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1 +1,3 @@
|
|||
Lockbox.master_key = ENV.fetch('LOCKBOX_MASTER_KEY')
|
||||
# frozen_string_literal: true
|
||||
|
||||
Lockbox.master_key = ENV.fetch("LOCKBOX_MASTER_KEY")
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
51
frontend/assets/images/icons/day.svg
Normal file
51
frontend/assets/images/icons/day.svg
Normal 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 |
57
frontend/assets/images/icons/night.svg
Normal file
57
frontend/assets/images/icons/night.svg
Normal 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 |
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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%' }}>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
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"/>
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
22
frontend/src/_components/DarkModeToggle.jsx
Normal file
22
frontend/src/_components/DarkModeToggle.jsx
Normal 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>
|
||||
}
|
||||
|
|
@ -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="#"
|
||||
|
|
|
|||
|
|
@ -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}/>;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,3 +2,4 @@ export * from './PrivateRoute';
|
|||
export * from './Pagination';
|
||||
export * from './Header';
|
||||
export * from './ConfirmDialog';
|
||||
export * from './DarkModeToggle';
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue