mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-24 01:18:23 +00:00
Merge branch 'release/v0.5.9' into main
This commit is contained in:
commit
0318427436
24 changed files with 604 additions and 215 deletions
26
app/controllers/forgot_password_controller.rb
Normal file
26
app/controllers/forgot_password_controller.rb
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
class ForgotPasswordController < ApplicationController
|
||||
skip_before_action :authenticate_request
|
||||
|
||||
def forgot
|
||||
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
|
||||
else
|
||||
render json: {error: 'Email address is not associated with a ToolJet cloud account.'}, status: :not_found
|
||||
end
|
||||
end
|
||||
|
||||
def reset
|
||||
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
|
||||
else
|
||||
render json: {error: user.errors.full_messages}, status: :unprocessable_entity
|
||||
end
|
||||
else
|
||||
render json: {error: 'Link not valid or expired.'}, status: :not_found
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -11,4 +11,9 @@ class UserMailer < ApplicationMailer
|
|||
@url = "#{ENV.fetch('TOOLJET_HOST')}/invitations/#{@user.invitation_token}?signup=true"
|
||||
mail(to: @user.email, subject: 'ToolJet Invitation')
|
||||
end
|
||||
|
||||
def password_reset(user)
|
||||
@user = user
|
||||
mail to: user.email, subject: "Password reset instructions"
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -33,4 +33,27 @@ class User < ApplicationRecord
|
|||
def app_viewer?(app)
|
||||
app_users.find_by(app_id: app.id)&.viewer?
|
||||
end
|
||||
|
||||
def send_password_reset
|
||||
self.forgot_password_token = generate_base64_token
|
||||
self.forgot_password_token_sent_at = Time.zone.now
|
||||
save!
|
||||
UserMailer.password_reset(self).deliver_now
|
||||
end
|
||||
|
||||
def forgot_password_token_valid?
|
||||
(self.forgot_password_token_sent_at + 1.hour) > Time.zone.now
|
||||
end
|
||||
|
||||
def reset_password(password)
|
||||
self.forgot_password_token = nil
|
||||
self.password = password
|
||||
save!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_base64_token
|
||||
SecureRandom.urlsafe_base64
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,6 +11,11 @@ class RestapiQueryService
|
|||
|
||||
def process
|
||||
url = options['url']
|
||||
|
||||
if data_source
|
||||
url = "#{source_options['url']}#{url}"
|
||||
end
|
||||
|
||||
method = options['method'] || 'GET'
|
||||
headers = (options['headers'] || []).reject { |header| header[0].empty? }
|
||||
headers = headers.to_h
|
||||
|
|
|
|||
2
app/views/user_mailer/password_reset.html.erb
Normal file
2
app/views/user_mailer/password_reset.html.erb
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<h1> Reset Password </h1>
|
||||
<p> Please use this code to reset your password: <%= @user.forgot_password_token %>
|
||||
|
|
@ -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.8'
|
||||
TOOLJET_VERSION = '0.5.9'
|
||||
|
||||
module ToolJet
|
||||
class Application < Rails::Application
|
||||
|
|
|
|||
|
|
@ -64,4 +64,8 @@ Rails.application.configure do
|
|||
|
||||
# Uncomment if you wish to allow Action Cable access from any origin.
|
||||
# config.action_cable.disable_request_forgery_protection = true
|
||||
config.action_mailer.raise_delivery_errors = true
|
||||
config.action_mailer.delivery_method = :test
|
||||
host = 'localhost:3000'
|
||||
config.action_mailer.default_url_options = { host: host, protocol: 'https' }
|
||||
end
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ Rails.application.routes.draw do
|
|||
post 'authenticate', to: 'authentication#authenticate'
|
||||
post 'signup', to: 'authentication#signup'
|
||||
|
||||
resources :metadata, only: [:index] do
|
||||
resources :metadata, only: [:index] do
|
||||
collection do
|
||||
post '/skip_version', to: 'metadata#skip_version'
|
||||
post '/skip_onboarding', to: 'metadata#skip_onboarding'
|
||||
|
|
@ -52,5 +52,7 @@ Rails.application.routes.draw do
|
|||
end
|
||||
|
||||
get '/health', to: 'probe#health_check'
|
||||
post 'password/forgot', to: 'forgot_password#forgot'
|
||||
post 'password/reset', to: 'forgot_password#reset'
|
||||
|
||||
end
|
||||
|
|
|
|||
6
db/migrate/20210609020556_add_forgot_password_to_user.rb
Normal file
6
db/migrate/20210609020556_add_forgot_password_to_user.rb
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
class AddForgotPasswordToUser < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
add_column :users, :forgot_password_token, :string
|
||||
add_column :users, :forgot_password_sent_at, :datetime
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
class RenameForgotPasswordToken < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
rename_column :users, :forgot_password_sent_at, :forgot_password_token_sent_at
|
||||
end
|
||||
end
|
||||
32
db/schema.rb
generated
32
db/schema.rb
generated
|
|
@ -10,14 +10,14 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2021_06_06_031036) do
|
||||
ActiveRecord::Schema.define(version: 2021_06_17_031153) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pgcrypto"
|
||||
enable_extension "plpgsql"
|
||||
enable_extension "uuid-ossp"
|
||||
|
||||
create_table "app_users", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t|
|
||||
create_table "app_users", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
t.uuid "app_id", null: false
|
||||
t.uuid "user_id", null: false
|
||||
t.string "role"
|
||||
|
|
@ -27,7 +27,7 @@ ActiveRecord::Schema.define(version: 2021_06_06_031036) do
|
|||
t.index ["user_id"], name: "index_app_users_on_user_id"
|
||||
end
|
||||
|
||||
create_table "app_versions", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t|
|
||||
create_table "app_versions", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
t.uuid "app_id", null: false
|
||||
t.string "name"
|
||||
t.json "definition"
|
||||
|
|
@ -36,7 +36,7 @@ ActiveRecord::Schema.define(version: 2021_06_06_031036) do
|
|||
t.index ["app_id"], name: "index_app_versions_on_app_id"
|
||||
end
|
||||
|
||||
create_table "apps", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t|
|
||||
create_table "apps", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
t.string "name"
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
|
|
@ -50,14 +50,14 @@ ActiveRecord::Schema.define(version: 2021_06_06_031036) do
|
|||
t.index ["user_id"], name: "index_apps_on_user_id"
|
||||
end
|
||||
|
||||
create_table "credentials", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t|
|
||||
create_table "credentials", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
t.text "encrypted_value"
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
t.text "value_ciphertext"
|
||||
end
|
||||
|
||||
create_table "data_queries", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t|
|
||||
create_table "data_queries", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
t.uuid "app_id", null: false
|
||||
t.string "name"
|
||||
t.json "options"
|
||||
|
|
@ -69,7 +69,7 @@ ActiveRecord::Schema.define(version: 2021_06_06_031036) do
|
|||
t.index ["data_source_id"], name: "index_data_queries_on_data_source_id"
|
||||
end
|
||||
|
||||
create_table "data_source_user_oauth2s", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t|
|
||||
create_table "data_source_user_oauth2s", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
t.uuid "user_id", null: false
|
||||
t.uuid "data_source_id", null: false
|
||||
t.text "encrypted_options"
|
||||
|
|
@ -80,7 +80,7 @@ ActiveRecord::Schema.define(version: 2021_06_06_031036) do
|
|||
t.index ["user_id"], name: "index_data_source_user_oauth2s_on_user_id"
|
||||
end
|
||||
|
||||
create_table "data_sources", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t|
|
||||
create_table "data_sources", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
t.uuid "app_id", null: false
|
||||
t.string "name"
|
||||
t.json "options"
|
||||
|
|
@ -90,7 +90,7 @@ ActiveRecord::Schema.define(version: 2021_06_06_031036) do
|
|||
t.index ["app_id"], name: "index_data_sources_on_app_id"
|
||||
end
|
||||
|
||||
create_table "endpoints", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t|
|
||||
create_table "endpoints", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
t.string "identifier"
|
||||
t.string "path"
|
||||
t.string "method"
|
||||
|
|
@ -103,7 +103,7 @@ ActiveRecord::Schema.define(version: 2021_06_06_031036) do
|
|||
t.index ["integration_id"], name: "index_endpoints_on_integration_id"
|
||||
end
|
||||
|
||||
create_table "folder_apps", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t|
|
||||
create_table "folder_apps", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
t.uuid "folder_id", null: false
|
||||
t.uuid "app_id", null: false
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
|
|
@ -112,7 +112,7 @@ ActiveRecord::Schema.define(version: 2021_06_06_031036) do
|
|||
t.index ["folder_id"], name: "index_folder_apps_on_folder_id"
|
||||
end
|
||||
|
||||
create_table "folders", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t|
|
||||
create_table "folders", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
t.string "name"
|
||||
t.uuid "organization_id", null: false
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
|
|
@ -120,7 +120,7 @@ ActiveRecord::Schema.define(version: 2021_06_06_031036) do
|
|||
t.index ["organization_id"], name: "index_folders_on_organization_id"
|
||||
end
|
||||
|
||||
create_table "integrations", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t|
|
||||
create_table "integrations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
t.string "identifier"
|
||||
t.string "name"
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
|
|
@ -133,7 +133,7 @@ ActiveRecord::Schema.define(version: 2021_06_06_031036) do
|
|||
t.datetime "updated_at", precision: 6, null: false
|
||||
end
|
||||
|
||||
create_table "organization_users", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t|
|
||||
create_table "organization_users", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
t.uuid "organization_id", null: false
|
||||
t.uuid "user_id", null: false
|
||||
t.string "role"
|
||||
|
|
@ -144,14 +144,14 @@ ActiveRecord::Schema.define(version: 2021_06_06_031036) do
|
|||
t.index ["user_id"], name: "index_organization_users_on_user_id"
|
||||
end
|
||||
|
||||
create_table "organizations", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t|
|
||||
create_table "organizations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
t.string "name"
|
||||
t.string "domain"
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
end
|
||||
|
||||
create_table "users", id: :uuid, default: -> { "public.gen_random_uuid()" }, force: :cascade do |t|
|
||||
create_table "users", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
t.string "first_name"
|
||||
t.string "last_name"
|
||||
t.string "email"
|
||||
|
|
@ -162,6 +162,8 @@ ActiveRecord::Schema.define(version: 2021_06_06_031036) do
|
|||
t.uuid "organization_id"
|
||||
t.text "image"
|
||||
t.string "invitation_token"
|
||||
t.string "forgot_password_token"
|
||||
t.datetime "forgot_password_token_sent_at"
|
||||
t.index ["organization_id"], name: "index_users_on_organization_id"
|
||||
end
|
||||
|
||||
|
|
|
|||
41
docs/docs/data-sources/mssql.md
Normal file
41
docs/docs/data-sources/mssql.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
# MS SQL Server / Azure SQL databases
|
||||
|
||||
|
||||
ToolJet can connect to MS SQL Server & Azure SQL databases to read and write data.
|
||||
|
||||
## Connection
|
||||
|
||||
Please make sure the host/ip of the database is accessible from your VPC if you have self-hosted ToolJet. If you are using ToolJet cloud, please whitelist our IP.
|
||||
|
||||
To add new MS SQL Server / Azure SQL database, click on the '+' button on data sources panel at the left-bottom corner of the app editor. Select `SQL Server` from the modal that pops up.
|
||||
|
||||
ToolJet requires the following to connect to your PostgreSQL database.
|
||||
|
||||
- **Host**
|
||||
- **Port**
|
||||
- **Username**
|
||||
- **Password**
|
||||
- **Azure** - Select this option if you are using Azure SQL databases.
|
||||
|
||||
It is recommended to create a new database user so that you can control the access levels of ToolJet.
|
||||
|
||||
Click on 'Test connection' button to verify if the credentials are correct and that the database is accessible to ToolJet server. Click on 'Save' button to save the datasource.
|
||||
|
||||
<img src="/img/datasource-reference/mssql/connect.gif" alt="ToolJet - Redis connection" height="420"/>
|
||||
|
||||
|
||||
## Querying SQL Server / Azure SQL databases
|
||||
Click on '+' button of the query manager at the bottom panel of the editor and select the database added in the previous step as the datasource.
|
||||
|
||||
Click on the 'run' button to run the query. NOTE: Query should be saved before running.
|
||||
|
||||
<img src="/img/datasource-reference/mssql/query.gif" alt="ToolJet - Redis connection" height="420"/>
|
||||
|
||||
|
||||
:::tip
|
||||
Query results can be transformed using transformations. Read our transformations documentation to see how: [link](/tutorial/transformations)
|
||||
:::
|
||||
BIN
docs/static/img/datasource-reference/mssql/connect.gif
vendored
Normal file
BIN
docs/static/img/datasource-reference/mssql/connect.gif
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 MiB |
BIN
docs/static/img/datasource-reference/mssql/query.gif
vendored
Normal file
BIN
docs/static/img/datasource-reference/mssql/query.gif
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.6 MiB |
|
|
@ -14,6 +14,8 @@ import { ToastContainer } from 'react-toastify';
|
|||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { ManageOrgUsers } from '@/ManageOrgUsers';
|
||||
import { OnboardingModal } from '@/Onboarding/OnboardingModal';
|
||||
import {ForgotPassword} from '@/ForgotPassword'
|
||||
import { ResetPassword } from '@/ResetPassword';
|
||||
|
||||
class App extends React.Component {
|
||||
constructor(props) {
|
||||
|
|
@ -27,7 +29,7 @@ class App extends React.Component {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
authenticationService.currentUser.subscribe((x) => {
|
||||
authenticationService.currentUser.subscribe((x) => {
|
||||
this.setState({ currentUser: x });
|
||||
});
|
||||
}
|
||||
|
|
@ -41,10 +43,10 @@ class App extends React.Component {
|
|||
const { currentUser, fetchedMetadata, updateAvailable, onboarded } = this.state;
|
||||
|
||||
if(currentUser && fetchedMetadata === false) {
|
||||
tooljetService.fetchMetaData().then((data) => {
|
||||
tooljetService.fetchMetaData().then((data) => {
|
||||
this.setState({ fetchedMetadata: true, onboarded: data.onboarded });
|
||||
|
||||
if(data.installed_version < data.latest_version && data.version_ignored === false) {
|
||||
if(data.installed_version < data.latest_version && data.version_ignored === false) {
|
||||
this.setState({ updateAvailable: true });
|
||||
}
|
||||
})
|
||||
|
|
@ -62,7 +64,7 @@ class App extends React.Component {
|
|||
</div>
|
||||
</div>}
|
||||
|
||||
{!onboarded &&
|
||||
{!onboarded &&
|
||||
<OnboardingModal />
|
||||
}
|
||||
|
||||
|
|
@ -71,6 +73,8 @@ class App extends React.Component {
|
|||
<PrivateRoute exact path="/" component={HomePage} />
|
||||
<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/:id" component={Viewer} />
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
datasourceService, dataqueryService, appService, authenticationService
|
||||
} from '@/_services';
|
||||
import { datasourceService, dataqueryService, appService, authenticationService } from '@/_services';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { Container } from './Container';
|
||||
|
|
@ -24,7 +22,7 @@ import {
|
|||
onQueryConfirm,
|
||||
onQueryCancel,
|
||||
runQuery,
|
||||
setStateAsync
|
||||
setStateAsync,
|
||||
} from '@/_helpers/appUtils';
|
||||
import { Confirm } from './Viewer/Confirm';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
|
|
@ -38,13 +36,13 @@ class Editor extends React.Component {
|
|||
const appId = this.props.match.params.id;
|
||||
|
||||
const currentUser = authenticationService.currentUserValue;
|
||||
let userVars = { };
|
||||
let userVars = {};
|
||||
|
||||
if (currentUser) {
|
||||
userVars = {
|
||||
email: currentUser.email,
|
||||
firstName: currentUser.first_name,
|
||||
lastName: currentUser.last_name
|
||||
lastName: currentUser.last_name,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -64,36 +62,38 @@ class Editor extends React.Component {
|
|||
scaleValue: 1,
|
||||
deviceWindowWidth: 450,
|
||||
appDefinition: {
|
||||
components: null
|
||||
components: null,
|
||||
},
|
||||
currentState: {
|
||||
queries: {},
|
||||
components: {},
|
||||
globals: {
|
||||
currentUser: userVars,
|
||||
urlparams: {}
|
||||
}
|
||||
}
|
||||
urlparams: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const appId = this.props.match.params.id;
|
||||
|
||||
appService.getApp(appId).then((data) => this.setState(
|
||||
{
|
||||
app: data,
|
||||
isLoading: false,
|
||||
appDefinition: { ...this.state.appDefinition, ...data.definition }
|
||||
},
|
||||
() => {
|
||||
data.data_queries.forEach((query) => {
|
||||
if (query.options.runOnPageLoad) {
|
||||
runQuery(this, query.id, query.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
));
|
||||
appService.getApp(appId).then((data) =>
|
||||
this.setState(
|
||||
{
|
||||
app: data,
|
||||
isLoading: false,
|
||||
appDefinition: { ...this.state.appDefinition, ...data.definition },
|
||||
},
|
||||
() => {
|
||||
data.data_queries.forEach((query) => {
|
||||
if (query.options.runOnPageLoad) {
|
||||
runQuery(this, query.id, query.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
this.fetchDataSources();
|
||||
this.fetchDataQueries();
|
||||
|
|
@ -101,20 +101,22 @@ class Editor extends React.Component {
|
|||
this.setState({
|
||||
appId,
|
||||
currentSidebarTab: 2,
|
||||
selectedComponent: null
|
||||
selectedComponent: null,
|
||||
});
|
||||
}
|
||||
|
||||
fetchDataSources = () => {
|
||||
this.setState(
|
||||
{
|
||||
loadingDataSources: true
|
||||
loadingDataSources: true,
|
||||
},
|
||||
() => {
|
||||
datasourceService.getAll(this.state.appId).then((data) => this.setState({
|
||||
dataSources: data.data_sources,
|
||||
loadingDataSources: false
|
||||
}));
|
||||
datasourceService.getAll(this.state.appId).then((data) =>
|
||||
this.setState({
|
||||
dataSources: data.data_sources,
|
||||
loadingDataSources: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
@ -122,7 +124,7 @@ class Editor extends React.Component {
|
|||
fetchDataQueries = () => {
|
||||
this.setState(
|
||||
{
|
||||
loadingDataQueries: true
|
||||
loadingDataQueries: true,
|
||||
},
|
||||
() => {
|
||||
dataqueryService.getAll(this.state.appId).then((data) => {
|
||||
|
|
@ -132,15 +134,15 @@ class Editor extends React.Component {
|
|||
loadingDataQueries: false,
|
||||
app: {
|
||||
...this.state.app,
|
||||
data_queries: data.data_queries
|
||||
}
|
||||
data_queries: data.data_queries,
|
||||
},
|
||||
},
|
||||
() => {
|
||||
let queryState = {};
|
||||
data.data_queries.forEach((query) => {
|
||||
queryState[query.name] = {
|
||||
queryState[query.name] = {
|
||||
...DataSourceTypes.find((source) => source.kind === query.kind).exposedVariables,
|
||||
...this.state.currentState.queries[query.name]
|
||||
...this.state.currentState.queries[query.name],
|
||||
};
|
||||
});
|
||||
|
||||
|
|
@ -162,9 +164,9 @@ class Editor extends React.Component {
|
|||
currentState: {
|
||||
...this.state.currentState,
|
||||
queries: {
|
||||
...queryState
|
||||
}
|
||||
}
|
||||
...queryState,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
@ -180,7 +182,7 @@ class Editor extends React.Component {
|
|||
const component = components[key];
|
||||
const componentMeta = componentTypes.find((comp) => component.component.component === comp.component);
|
||||
|
||||
const existingComponentName = Object.keys(currentComponents).find(comp => currentComponents[comp].id === key);
|
||||
const existingComponentName = Object.keys(currentComponents).find((comp) => currentComponents[comp].id === key);
|
||||
const existingValues = currentComponents[existingComponentName];
|
||||
|
||||
componentState[component.component.name] = { ...componentMeta.exposedVariables, id: key, ...existingValues };
|
||||
|
|
@ -190,9 +192,9 @@ class Editor extends React.Component {
|
|||
currentState: {
|
||||
...this.state.currentState,
|
||||
components: {
|
||||
...componentState
|
||||
}
|
||||
}
|
||||
...componentState,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -207,7 +209,7 @@ class Editor extends React.Component {
|
|||
|
||||
switchSidebarTab = (tabIndex) => {
|
||||
this.setState({
|
||||
currentSidebarTab: tabIndex
|
||||
currentSidebarTab: tabIndex,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -216,16 +218,15 @@ class Editor extends React.Component {
|
|||
let filteredComponents = this.state.allComponentTypes;
|
||||
|
||||
if (searchText !== '') {
|
||||
filteredComponents = this.state.allComponentTypes.filter((e) => e.name.toLowerCase() === searchText.toLowerCase());
|
||||
filteredComponents = this.state.allComponentTypes.filter(
|
||||
(e) => e.name.toLowerCase() === searchText.toLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
this.setState({ componentTypes: filteredComponents });
|
||||
};
|
||||
|
||||
appDefinitionChanged = (newDefinition) => {
|
||||
console.log('currentDefinition', this.state.appDefinition);
|
||||
console.log('newDefinition', newDefinition);
|
||||
|
||||
this.setState({ appDefinition: newDefinition });
|
||||
this.computeComponentState(newDefinition.components);
|
||||
};
|
||||
|
|
@ -244,7 +245,9 @@ class Editor extends React.Component {
|
|||
let newDefinition = this.state.appDefinition;
|
||||
|
||||
// Delete child components when parent is deleted
|
||||
const childComponents = Object.keys(newDefinition.components).filter((key) => newDefinition.components[key].parent === component.id);
|
||||
const childComponents = Object.keys(newDefinition.components).filter(
|
||||
(key) => newDefinition.components[key].parent === component.id
|
||||
);
|
||||
childComponents.forEach((componentId) => {
|
||||
delete newDefinition.components[componentId];
|
||||
});
|
||||
|
|
@ -264,10 +267,10 @@ class Editor extends React.Component {
|
|||
[newDefinition.id]: {
|
||||
...this.state.appDefinition.components[newDefinition.id],
|
||||
component: newDefinition.component,
|
||||
layouts: newDefinition.layouts
|
||||
}
|
||||
}
|
||||
}
|
||||
layouts: newDefinition.layouts,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -279,12 +282,12 @@ class Editor extends React.Component {
|
|||
...this.state.appDefinition.components,
|
||||
[newComponent.id]: {
|
||||
...this.state.appDefinition.components[newComponent.id],
|
||||
...newComponent
|
||||
}
|
||||
}
|
||||
}
|
||||
...newComponent,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
saveApp = (id, attributes, notify = false) => {
|
||||
appService.saveApp(id, attributes).then(() => {
|
||||
|
|
@ -301,12 +304,16 @@ class Editor extends React.Component {
|
|||
role="button"
|
||||
key={dataSource.name}
|
||||
onClick={() => {
|
||||
console.log('dss', dataSource);
|
||||
this.setState({ selectedDataSource: dataSource, showDataSourceManagerModal: true });
|
||||
}}
|
||||
>
|
||||
<td>
|
||||
<img src={`/assets/images/icons/editor/datasources/${sourceMeta.kind.toLowerCase()}.svg`} width="20" height="20" /> {dataSource.name}
|
||||
<img
|
||||
src={`/assets/images/icons/editor/datasources/${sourceMeta.kind.toLowerCase()}.svg`}
|
||||
width="20"
|
||||
height="20"
|
||||
/>{' '}
|
||||
{dataSource.name}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
|
@ -322,41 +329,46 @@ class Editor extends React.Component {
|
|||
|
||||
const { currentState } = this.state;
|
||||
|
||||
const isLoading = currentState.queries[dataQuery.name] ? currentState.queries[dataQuery.name].isLoading : false;
|
||||
const isLoading = currentState.queries[dataQuery.name] ? currentState.queries[dataQuery.name].isLoading : false;
|
||||
|
||||
return (
|
||||
<div
|
||||
<div
|
||||
className={'row query-row py-2 px-3' + (isSeletedQuery ? ' query-row-selected' : '')}
|
||||
key={dataQuery.name}
|
||||
onClick={() => this.setState({ editingQuery: true, selectedQuery: dataQuery })}
|
||||
role="button"
|
||||
>
|
||||
<div className="col">
|
||||
<img src={`/assets/images/icons/editor/datasources/${sourceMeta.kind.toLowerCase()}.svg`} width="20" height="20" />
|
||||
<img
|
||||
src={`/assets/images/icons/editor/datasources/${sourceMeta.kind.toLowerCase()}.svg`}
|
||||
width="20"
|
||||
height="20"
|
||||
/>
|
||||
<span className="p-3">{dataQuery.name}</span>
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
{!(isLoading === true) &&
|
||||
<button
|
||||
{!(isLoading === true) && (
|
||||
<button
|
||||
className="btn badge bg-azure-lt"
|
||||
onClick={() => {
|
||||
runQuery(this, dataQuery.id, dataQuery.name).then(() => {
|
||||
toast.info(`Query (${dataQuery.name}) completed.`, {
|
||||
hideProgressBar: true,
|
||||
position: 'bottom-center'
|
||||
position: 'bottom-center',
|
||||
});
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div><img src="/assets/images/icons/editor/play.svg" width="8" height="8" className="mx-1" /></div>
|
||||
<div>
|
||||
<img src="/assets/images/icons/editor/play.svg" width="8" height="8" className="mx-1" />
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
{isLoading === true &&
|
||||
<div
|
||||
className="px-2">
|
||||
)}
|
||||
{isLoading === true && (
|
||||
<div className="px-2">
|
||||
<div class="text-center spinner-border spinner-border-sm" role="status"></div>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -364,13 +376,13 @@ class Editor extends React.Component {
|
|||
|
||||
onNameChanged = (newName) => {
|
||||
this.setState({
|
||||
app: { ...this.state.app, name: newName }
|
||||
app: { ...this.state.app, name: newName },
|
||||
});
|
||||
};
|
||||
|
||||
toggleQueryPaneHeight = () => {
|
||||
this.setState({
|
||||
queryPaneHeight: this.state.queryPaneHeight === '30%' ? '80%' : '30%'
|
||||
queryPaneHeight: this.state.queryPaneHeight === '30%' ? '80%' : '30%',
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -385,7 +397,7 @@ class Editor extends React.Component {
|
|||
configHandleClicked = (id, component) => {
|
||||
this.switchSidebarTab(1);
|
||||
this.setState({ selectedComponent: { id, component } });
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
|
|
@ -410,7 +422,7 @@ class Editor extends React.Component {
|
|||
zoomLevel,
|
||||
currentLayout,
|
||||
deviceWindowWidth,
|
||||
scaleValue
|
||||
scaleValue,
|
||||
} = this.state;
|
||||
|
||||
const appLink = `/applications/${appId}`;
|
||||
|
|
@ -477,46 +489,36 @@ class Editor extends React.Component {
|
|||
</span>
|
||||
</div>
|
||||
<div className="canvas-buttons">
|
||||
<button
|
||||
className="btn btn-light mx-2"
|
||||
onClick={() => this.setState({ zoomLevel: zoomLevel - 0.1 })}
|
||||
disabled={zoomLevel <= 0.6}
|
||||
role="button"
|
||||
>
|
||||
<img
|
||||
src="/assets/images/icons/zoom-out.svg"
|
||||
width="12"
|
||||
height="12"
|
||||
/>
|
||||
</button>
|
||||
<small>
|
||||
{zoomLevel * 100}%
|
||||
</small>
|
||||
<button
|
||||
className="btn btn-light mx-2"
|
||||
onClick={() => this.setState({ zoomLevel: zoomLevel + 0.1 })}
|
||||
disabled={zoomLevel === 1}
|
||||
role="button"
|
||||
>
|
||||
<img
|
||||
src="/assets/images/icons/zoom-in.svg"
|
||||
width="12"
|
||||
height="12"
|
||||
/>
|
||||
<button
|
||||
className="btn btn-light mx-2"
|
||||
onClick={() => this.setState({ zoomLevel: zoomLevel - 0.1 })}
|
||||
disabled={zoomLevel <= 0.6}
|
||||
role="button"
|
||||
>
|
||||
<img src="/assets/images/icons/zoom-out.svg" width="12" height="12" />
|
||||
</button>
|
||||
<small>{zoomLevel * 100}%</small>
|
||||
<button
|
||||
className="btn btn-light mx-2"
|
||||
onClick={() => this.setState({ zoomLevel: zoomLevel + 0.1 })}
|
||||
disabled={zoomLevel === 1}
|
||||
role="button"
|
||||
>
|
||||
<img src="/assets/images/icons/zoom-in.svg" width="12" height="12" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="layout-buttons">
|
||||
<div class="btn-group" role="group" aria-label="Basic example">
|
||||
<button
|
||||
type="button"
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-light"
|
||||
onClick={() => this.setState({ currentLayout: 'desktop' })}
|
||||
disabled={currentLayout === 'desktop'}
|
||||
>
|
||||
<img src="/assets/images/icons/editor/desktop.svg" width="12" height="12" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-light"
|
||||
onClick={() => this.setState({ currentLayout: 'mobile' })}
|
||||
disabled={currentLayout === 'mobile'}
|
||||
|
|
@ -524,14 +526,9 @@ class Editor extends React.Component {
|
|||
<img src="/assets/images/icons/editor/mobile.svg" width="12" height="12" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="navbar-nav flex-row order-md-last">
|
||||
<div className="nav-item dropdown d-none d-md-flex me-3">
|
||||
{app
|
||||
&& <ManageAppUsers app={app} />
|
||||
}
|
||||
</div>
|
||||
<div className="nav-item dropdown d-none d-md-flex me-3">{app && <ManageAppUsers app={app} />}</div>
|
||||
<div className="nav-item dropdown d-none d-md-flex me-3">
|
||||
<a href={appLink} target="_blank" className="btn btn-sm" rel="noreferrer">
|
||||
Launch
|
||||
|
|
@ -555,12 +552,12 @@ class Editor extends React.Component {
|
|||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: '#f0f0f0',
|
||||
zIndex: '200'
|
||||
zIndex: '200',
|
||||
}}
|
||||
maxWidth={showLeftSidebar ? '30%' : '0%'}
|
||||
defaultSize={{
|
||||
width: '12%',
|
||||
height: '99%'
|
||||
height: '99%',
|
||||
}}
|
||||
>
|
||||
<div className="left-sidebar">
|
||||
|
|
@ -653,7 +650,8 @@ class Editor extends React.Component {
|
|||
You haven't added data sources yet. <br />
|
||||
<button
|
||||
className="btn btn-sm btn-outline-azure mt-3"
|
||||
onClick={() => this.setState({ showDataSourceManagerModal: true, selectedDataSource: null })
|
||||
onClick={() =>
|
||||
this.setState({ showDataSourceManagerModal: true, selectedDataSource: null })
|
||||
}
|
||||
>
|
||||
add datasource
|
||||
|
|
@ -668,7 +666,7 @@ class Editor extends React.Component {
|
|||
</Resizable>
|
||||
<div className="main">
|
||||
<div className="canvas-container align-items-center" style={{ transform: `scale(${zoomLevel})` }}>
|
||||
<div className="canvas-area" style={{width: currentLayout === 'desktop' ? '1292px' : '450px'}}>
|
||||
<div className="canvas-area" style={{ width: currentLayout === 'desktop' ? '1292px' : '450px' }}>
|
||||
<Container
|
||||
appDefinition={appDefinition}
|
||||
appDefinitionChanged={this.appDefinitionChanged}
|
||||
|
|
@ -681,9 +679,11 @@ class Editor extends React.Component {
|
|||
scaleValue={scaleValue}
|
||||
appLoading={isLoading}
|
||||
onEvent={(eventName, options) => onEvent(this, eventName, options)}
|
||||
onComponentOptionChanged={(component, optionName, value) => onComponentOptionChanged(this, component, optionName, value)
|
||||
onComponentOptionChanged={(component, optionName, value) =>
|
||||
onComponentOptionChanged(this, component, optionName, value)
|
||||
}
|
||||
onComponentOptionsChanged={(component, options) => onComponentOptionsChanged(this, component, options)
|
||||
onComponentOptionsChanged={(component, options) =>
|
||||
onComponentOptionsChanged(this, component, options)
|
||||
}
|
||||
currentState={this.state.currentState}
|
||||
configHandleClicked={this.configHandleClicked}
|
||||
|
|
@ -694,10 +694,7 @@ class Editor extends React.Component {
|
|||
onComponentClick(this, id, component);
|
||||
}}
|
||||
/>
|
||||
<CustomDragLayer
|
||||
snapToGrid={true}
|
||||
currentLayout={currentLayout}
|
||||
/>
|
||||
<CustomDragLayer snapToGrid={true} currentLayout={currentLayout} />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -705,7 +702,7 @@ class Editor extends React.Component {
|
|||
style={{
|
||||
height: showQueryEditor ? this.state.queryPaneHeight : '0px',
|
||||
width: !showLeftSidebar ? '85%' : '',
|
||||
left: !showLeftSidebar ? '0' : ''
|
||||
left: !showLeftSidebar ? '0' : '',
|
||||
}}
|
||||
>
|
||||
<div className="row main-row">
|
||||
|
|
@ -738,16 +735,15 @@ class Editor extends React.Component {
|
|||
</div>
|
||||
) : (
|
||||
<div className="query-list">
|
||||
<div>
|
||||
{dataQueries.map((query) => this.renderDataQuery(query))}
|
||||
</div>
|
||||
<div>{dataQueries.map((query) => this.renderDataQuery(query))}</div>
|
||||
{dataQueries.length === 0 && (
|
||||
<div className="mt-5">
|
||||
<center>
|
||||
<span className="text-muted">You haven't created queries yet.</span> <br />
|
||||
<button
|
||||
className="btn btn-sm btn-outline-azure mt-3"
|
||||
onClick={() => this.setState({ selectedQuery: {}, editingQuery: false, addingQuery: true })
|
||||
onClick={() =>
|
||||
this.setState({ selectedQuery: {}, editingQuery: false, addingQuery: true })
|
||||
}
|
||||
>
|
||||
create query
|
||||
|
|
|
|||
|
|
@ -7,15 +7,14 @@ import { changeOption } from './utils';
|
|||
class Restapi extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
options: this.props.options
|
||||
options: this.props.options,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({
|
||||
options: this.props.options
|
||||
options: this.props.options,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -24,7 +23,7 @@ class Restapi extends React.Component {
|
|||
const newOptions = { ...options, [option]: [...options[option], ['', '']] };
|
||||
|
||||
this.setState({
|
||||
options: newOptions
|
||||
options: newOptions,
|
||||
});
|
||||
this.props.optionsChanged(newOptions);
|
||||
};
|
||||
|
|
@ -48,20 +47,20 @@ class Restapi extends React.Component {
|
|||
|
||||
render() {
|
||||
const { options } = this.state;
|
||||
|
||||
const dataSourceURL = this.props.selectedDataSource?.options?.url?.value;
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-3 mt-2">
|
||||
<div className="mb-3">
|
||||
<div className="row g-2">
|
||||
<div className="col-auto" style={{ width: '120px' }} >
|
||||
<div className="col-auto" style={{ width: '120px' }}>
|
||||
<SelectSearch
|
||||
options={[
|
||||
{ name: 'GET', value: 'get' },
|
||||
{ name: 'POST', value: 'post' },
|
||||
{ name: 'PUT', value: 'put' },
|
||||
{ name: 'PATCH', value: 'patch' },
|
||||
{ name: 'DELETE', value: 'delete' }
|
||||
{ name: 'DELETE', value: 'delete' },
|
||||
]}
|
||||
value={options.method}
|
||||
search={false}
|
||||
|
|
@ -74,18 +73,41 @@ class Restapi extends React.Component {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div className="col">
|
||||
<div className="col" style={{ display: 'flex' }}>
|
||||
{dataSourceURL && (
|
||||
<span
|
||||
htmlFor=""
|
||||
style={{
|
||||
padding: '7px',
|
||||
border: '1px solid rgb(217 220 222)',
|
||||
background: 'rgb(246 247 251)',
|
||||
color: '#9ca1a6',
|
||||
marginRight: '-3px',
|
||||
borderTopLeftRadius: '3px',
|
||||
borderBottomLeftRadius: '3px',
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
{dataSourceURL}
|
||||
</span>
|
||||
)}
|
||||
<CodeHinter
|
||||
currentState={this.props.currentState}
|
||||
initialValue={options.url}
|
||||
className="codehinter-query-editor-input"
|
||||
onChange={(value) => { changeOption(this, 'url', value); }}
|
||||
onChange={(value) => {
|
||||
changeOption(this, 'url', value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{[{name: 'URL parameters', value: 'url_params'},{name: 'Headers', value: 'headers'},{name: 'Body', value: 'body'}].map((option) => (
|
||||
{[
|
||||
{ name: 'URL parameters', value: 'url_params' },
|
||||
{ name: 'Headers', value: 'headers' },
|
||||
{ name: 'Body', value: 'body' },
|
||||
].map((option) => (
|
||||
<div className="mb-3" key={option}>
|
||||
<div className="row g-2">
|
||||
<div className="col-md-2">
|
||||
|
|
@ -117,7 +139,10 @@ class Restapi extends React.Component {
|
|||
</span>
|
||||
</div>
|
||||
))}
|
||||
<button className="btn btn-sm btn-outline-azure" onClick={() => this.addNewKeyValuePair(option.value)}>
|
||||
<button
|
||||
className="btn btn-sm btn-outline-azure"
|
||||
onClick={() => this.addNewKeyValuePair(option.value)}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,9 +8,7 @@ import { allSources } from './QueryEditors';
|
|||
import { Transformation } from './Transformation';
|
||||
import { defaultOptions } from './constants';
|
||||
import ReactJson from 'react-json-view';
|
||||
import {
|
||||
previewQuery
|
||||
} from '@/_helpers/appUtils';
|
||||
import { previewQuery } from '@/_helpers/appUtils';
|
||||
|
||||
const queryNameRegex = new RegExp('^[A-Za-z0-9_-]*$');
|
||||
|
||||
|
|
@ -27,6 +25,8 @@ class QueryManager extends React.Component {
|
|||
|
||||
setStateFromProps = (props) => {
|
||||
const selectedQuery = props.selectedQuery;
|
||||
const dataSourceId = selectedQuery?.data_source_id;
|
||||
const source = props.dataSources.find((datasource) => datasource.id === dataSourceId);
|
||||
|
||||
this.setState(
|
||||
{
|
||||
|
|
@ -39,21 +39,23 @@ class QueryManager extends React.Component {
|
|||
editingQuery: props.editingQuery,
|
||||
queryPaneHeight: props.queryPaneHeight,
|
||||
currentState: props.currentState,
|
||||
selectedSource: source,
|
||||
},
|
||||
() => {
|
||||
if (this.props.mode === 'edit') {
|
||||
let source = props.dataSources.find((datasource) => datasource.id === selectedQuery.data_source_id);
|
||||
if(selectedQuery.kind === 'restapi') source = { kind: 'restapi' };
|
||||
|
||||
if (selectedQuery.kind === 'restapi') source = { kind: 'restapi' };
|
||||
//
|
||||
this.setState({
|
||||
options: selectedQuery.options,
|
||||
selectedDataSource: source,
|
||||
selectedQuery,
|
||||
queryName: selectedQuery.name
|
||||
queryName: selectedQuery.name,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
selectedQuery: null
|
||||
options: {},
|
||||
selectedQuery: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -70,23 +72,23 @@ class QueryManager extends React.Component {
|
|||
|
||||
changeDataSource = (sourceId) => {
|
||||
const source = [...this.state.dataSources, ...staticDataSources].find((datasource) => datasource.id === sourceId);
|
||||
|
||||
this.setState({
|
||||
selectedDataSource: source,
|
||||
selectedSource: source,
|
||||
options: defaultOptions[source.kind],
|
||||
queryName: this.computeQueryName(source.kind)
|
||||
queryName: this.computeQueryName(source.kind),
|
||||
});
|
||||
};
|
||||
|
||||
switchCurrentTab = (tab) => {
|
||||
this.setState({
|
||||
currentTab: tab
|
||||
currentTab: tab,
|
||||
});
|
||||
};
|
||||
|
||||
validateQueryName = () => {
|
||||
const {
|
||||
queryName, dataQueries, mode, selectedQuery
|
||||
} = this.state;
|
||||
const { queryName, dataQueries, mode, selectedQuery } = this.state;
|
||||
|
||||
if (mode === 'create') {
|
||||
return dataQueries.find((query) => query.name === queryName) === undefined && queryNameRegex.test(queryName);
|
||||
|
|
@ -117,9 +119,7 @@ class QueryManager extends React.Component {
|
|||
};
|
||||
|
||||
createOrUpdateDataQuery = () => {
|
||||
const {
|
||||
appId, options, selectedDataSource, mode, queryName
|
||||
} = this.state;
|
||||
const { appId, options, selectedDataSource, mode, queryName } = this.state;
|
||||
const kind = selectedDataSource.kind;
|
||||
const dataSourceId = selectedDataSource.id;
|
||||
|
||||
|
|
@ -127,31 +127,37 @@ class QueryManager extends React.Component {
|
|||
if (!isQueryNameValid) {
|
||||
toast.error('Invalid query name. Should be unique and only include letters, numbers and underscore.', {
|
||||
hideProgressBar: true,
|
||||
position: 'bottom-center'
|
||||
position: 'bottom-center',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode === 'edit') {
|
||||
this.setState({ isUpdating: true });
|
||||
dataqueryService.update(this.state.selectedQuery.id, queryName, options).then(() => {
|
||||
toast.success('Query Updated', { hideProgressBar: true, position: 'bottom-center' });
|
||||
this.setState({ isUpdating: false });
|
||||
this.props.dataQueriesChanged();
|
||||
}).catch(( { error }) => {
|
||||
this.setState({ isUpdating: false });
|
||||
toast.error(error, { hideProgressBar: true, position: 'bottom-center' });
|
||||
});
|
||||
dataqueryService
|
||||
.update(this.state.selectedQuery.id, queryName, options)
|
||||
.then(() => {
|
||||
toast.success('Query Updated', { hideProgressBar: true, position: 'bottom-center' });
|
||||
this.setState({ isUpdating: false });
|
||||
this.props.dataQueriesChanged();
|
||||
})
|
||||
.catch(({ error }) => {
|
||||
this.setState({ isUpdating: false });
|
||||
toast.error(error, { hideProgressBar: true, position: 'bottom-center' });
|
||||
});
|
||||
} else {
|
||||
this.setState({ isCreating: true });
|
||||
dataqueryService.create(appId, queryName, kind, options, dataSourceId).then(() => {
|
||||
toast.success('Query Added', { hideProgressBar: true, position: 'bottom-center' });
|
||||
this.setState({ isCreating: false });
|
||||
this.props.dataQueriesChanged();
|
||||
}).catch(({ error }) => {
|
||||
this.setState({ isCreating: false });
|
||||
toast.error(error, { hideProgressBar: true, position: 'bottom-center' });
|
||||
});
|
||||
dataqueryService
|
||||
.create(appId, queryName, kind, options, dataSourceId)
|
||||
.then(() => {
|
||||
toast.success('Query Added', { hideProgressBar: true, position: 'bottom-center' });
|
||||
this.setState({ isCreating: false });
|
||||
this.props.dataQueriesChanged();
|
||||
})
|
||||
.catch(({ error }) => {
|
||||
this.setState({ isCreating: false });
|
||||
toast.error(error, { hideProgressBar: true, position: 'bottom-center' });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -196,7 +202,7 @@ class QueryManager extends React.Component {
|
|||
currentState,
|
||||
queryName,
|
||||
previewLoading,
|
||||
queryPreviewData
|
||||
queryPreviewData,
|
||||
} = this.state;
|
||||
|
||||
let ElementToRender = '';
|
||||
|
|
@ -237,7 +243,7 @@ class QueryManager extends React.Component {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
{((addingQuery || editingQuery) && selectedDataSource) && (
|
||||
{(addingQuery || editingQuery) && selectedDataSource && (
|
||||
<div className="col query-name-field">
|
||||
<div className="input-icon" style={{ width: '160px' }}>
|
||||
<input
|
||||
|
|
@ -257,21 +263,25 @@ class QueryManager extends React.Component {
|
|||
<div className="col-auto">
|
||||
{(addingQuery || editingQuery) && (
|
||||
<span
|
||||
onClick={() => {
|
||||
const query = { data_source_id: selectedDataSource.id, options: options, kind: selectedDataSource.kind };
|
||||
previewQuery(this, query).then(() => {
|
||||
toast.info(`Query completed.`, {
|
||||
hideProgressBar: true,
|
||||
position: 'bottom-center'
|
||||
onClick={() => {
|
||||
const query = {
|
||||
data_source_id: selectedDataSource.id,
|
||||
options: options,
|
||||
kind: selectedDataSource.kind,
|
||||
};
|
||||
previewQuery(this, query)
|
||||
.then(() => {
|
||||
toast.info(`Query completed.`, {
|
||||
hideProgressBar: true,
|
||||
position: 'bottom-center',
|
||||
});
|
||||
this.previewPanelRef.current.scrollIntoView();
|
||||
})
|
||||
.catch(({ error, data }) => {
|
||||
debugger;
|
||||
});
|
||||
this.previewPanelRef.current.scrollIntoView();
|
||||
}).catch(( { error, data } ) => {
|
||||
debugger
|
||||
});
|
||||
}}
|
||||
className={`btn btn-secondary m-1 float-right1 ${
|
||||
previewLoading ? ' btn-loading' : ''
|
||||
}`}
|
||||
className={`btn btn-secondary m-1 float-right1 ${previewLoading ? ' btn-loading' : ''}`}
|
||||
>
|
||||
Preview
|
||||
</span>
|
||||
|
|
@ -319,7 +329,7 @@ class QueryManager extends React.Component {
|
|||
}),
|
||||
...staticDataSources.map((source) => {
|
||||
return { name: source.name, value: source.id };
|
||||
})
|
||||
}),
|
||||
]}
|
||||
value={selectedDataSource ? selectedDataSource.id : ''}
|
||||
search={true}
|
||||
|
|
@ -333,19 +343,30 @@ class QueryManager extends React.Component {
|
|||
|
||||
{selectedDataSource && (
|
||||
<div>
|
||||
<ElementToRender options={this.state.options} optionsChanged={this.optionsChanged} currentState={currentState}/>
|
||||
<ElementToRender
|
||||
selectedDataSource={this.state.selectedSource}
|
||||
options={this.state.options}
|
||||
optionsChanged={this.optionsChanged}
|
||||
currentState={currentState}
|
||||
/>
|
||||
<hr></hr>
|
||||
<div className="mb-3 mt-2">
|
||||
<Transformation changeOption={this.optionchanged} options={this.state.options} currentState={currentState}/>
|
||||
<Transformation
|
||||
changeOption={this.optionchanged}
|
||||
options={this.state.options}
|
||||
currentState={currentState}
|
||||
/>
|
||||
</div>
|
||||
<div className="row preview-header border-top" ref={this.previewPanelRef}>
|
||||
<div className="py-2">
|
||||
Preview
|
||||
</div>
|
||||
<div className="py-2">Preview</div>
|
||||
</div>
|
||||
<div className="mb-3 mt-2">
|
||||
{previewLoading && <center><div class="spinner-border text-azure mt-5" role="status"></div></center>}
|
||||
{previewLoading === false &&
|
||||
{previewLoading && (
|
||||
<center>
|
||||
<div class="spinner-border text-azure mt-5" role="status"></div>
|
||||
</center>
|
||||
)}
|
||||
{previewLoading === false && (
|
||||
<div>
|
||||
<ReactJson
|
||||
name={false}
|
||||
|
|
@ -360,7 +381,7 @@ class QueryManager extends React.Component {
|
|||
indentWidth={1}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
86
frontend/src/ForgotPassword/ForgotPasswordPage.jsx
Normal file
86
frontend/src/ForgotPassword/ForgotPasswordPage.jsx
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
import config from 'config';
|
||||
|
||||
class ForgotPassword extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isLoading: false,
|
||||
email: '',
|
||||
};
|
||||
}
|
||||
|
||||
handleChange = (event) => {
|
||||
this.setState({ [event.target.name]: event.target.value });
|
||||
};
|
||||
|
||||
handleClick = (event) => {
|
||||
event.preventDefault();
|
||||
fetch(`${config.apiUrl}/password/forgot`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(this.state.email),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
if (res.error) {
|
||||
toast.error(res.error);
|
||||
} else {
|
||||
toast.success(res.message);
|
||||
this.props.history.push('/reset-password');
|
||||
}
|
||||
})
|
||||
.catch(console.log);
|
||||
};
|
||||
render() {
|
||||
const { isLoading } = this.state;
|
||||
|
||||
return (
|
||||
<div className="page page-center">
|
||||
<div className="container-tight py-2">
|
||||
<div className="text-center mb-4">
|
||||
<a href=".">
|
||||
<img src="/assets/images/logo-text.svg" height="30" alt="" />
|
||||
</a>
|
||||
</div>
|
||||
<form className="card card-md" action="." method="get" autoComplete="off">
|
||||
<div className="card-body">
|
||||
<h2 className="card-title text-center mb-4">Forgot Password</h2>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Email address</label>
|
||||
<input
|
||||
onChange={this.handleChange}
|
||||
name="email"
|
||||
type="email"
|
||||
className="form-control"
|
||||
placeholder="Enter email"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-footer">
|
||||
<button
|
||||
className={`btn btn-primary w-100 ${isLoading ? 'btn-loading' : ''}`}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div className="text-center text-muted mt-3">
|
||||
Don't have account yet?
|
||||
<Link to={'/signup'} tabIndex="-1">
|
||||
Sign up
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { ForgotPassword };
|
||||
1
frontend/src/ForgotPassword/index.js
Normal file
1
frontend/src/ForgotPassword/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './ForgotPasswordPage';
|
||||
|
|
@ -69,7 +69,9 @@ class LoginPage extends React.Component {
|
|||
<label className="form-label">
|
||||
Password
|
||||
<span className="form-label-description">
|
||||
<a tabIndex="-1" href="/forgot-password">Forgot password</a>
|
||||
<Link to={'/forgot-password'} tabIndex="-1">
|
||||
Forgot password
|
||||
</Link>
|
||||
</span>
|
||||
</label>
|
||||
<div className="input-group input-group-flat">
|
||||
|
|
|
|||
125
frontend/src/ResetPassword/ResetPasswordPage.jsx
Normal file
125
frontend/src/ResetPassword/ResetPasswordPage.jsx
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
import config from 'config';
|
||||
|
||||
class ResetPassword extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isLoading: false,
|
||||
token: '',
|
||||
email: '',
|
||||
password: '',
|
||||
};
|
||||
}
|
||||
|
||||
handleChange = (event) => {
|
||||
this.setState({ [event.target.name]: event.target.value });
|
||||
};
|
||||
|
||||
handleClick = (event) => {
|
||||
event.preventDefault();
|
||||
const { password, password_confirmation } = this.state;
|
||||
if (password !== password_confirmation) {
|
||||
toast.error("Password don't match");
|
||||
this.setState({
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
});
|
||||
} else {
|
||||
fetch(`${config.apiUrl}/password/reset`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(this.state),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
if (res.error) {
|
||||
toast.error(res.error);
|
||||
} else {
|
||||
toast.success(res.message);
|
||||
this.props.history.push('/login');
|
||||
}
|
||||
})
|
||||
.catch(console.log);
|
||||
}
|
||||
};
|
||||
render() {
|
||||
const { isLoading } = this.state;
|
||||
|
||||
return (
|
||||
<div className="page page-center">
|
||||
<div className="container-tight py-2">
|
||||
<div className="text-center mb-4">
|
||||
<a href=".">
|
||||
<img src="/assets/images/logo-text.svg" height="30" alt="" />
|
||||
</a>
|
||||
</div>
|
||||
<form className="card card-md" action="." method="get" autoComplete="off">
|
||||
<div className="card-body">
|
||||
<h2 className="card-title text-center mb-4">Reset Password</h2>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Token</label>
|
||||
<input
|
||||
onChange={this.handleChange}
|
||||
name="token"
|
||||
type="token"
|
||||
className="form-control"
|
||||
placeholder="Enter token"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<label className="form-label">New Password</label>
|
||||
<div className="input-group input-group-flat">
|
||||
<input
|
||||
onChange={this.handleChange}
|
||||
name="password"
|
||||
type="password"
|
||||
className="form-control"
|
||||
placeholder="Password"
|
||||
autoComplete="off"
|
||||
/>
|
||||
<span className="input-group-text"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<label className="form-label">Password Confirmation</label>
|
||||
<div className="input-group input-group-flat">
|
||||
<input
|
||||
onChange={this.handleChange}
|
||||
name="password_confirmation"
|
||||
type="password"
|
||||
className="form-control"
|
||||
placeholder="Password Confirmation"
|
||||
autoComplete="off"
|
||||
/>
|
||||
<span className="input-group-text"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-footer">
|
||||
<button
|
||||
className={`btn btn-primary w-100 ${isLoading ? 'btn-loading' : ''}`}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div className="text-center text-muted mt-3">
|
||||
Don't have account yet?
|
||||
<Link to={'/signup'} tabIndex="-1">
|
||||
Sign up
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { ResetPassword };
|
||||
1
frontend/src/ResetPassword/index.js
Normal file
1
frontend/src/ResetPassword/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './ResetPasswordPage';
|
||||
7
test/controllers/forgot_password_controller_test.rb
Normal file
7
test/controllers/forgot_password_controller_test.rb
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
require "test_helper"
|
||||
|
||||
class ForgotPasswordControllerTest < ActionDispatch::IntegrationTest
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
||||
Loading…
Reference in a new issue