diff --git a/app/controllers/forgot_password_controller.rb b/app/controllers/forgot_password_controller.rb
new file mode 100644
index 0000000000..ddad4390df
--- /dev/null
+++ b/app/controllers/forgot_password_controller.rb
@@ -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
diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb
index 2f09019527..69713ddbd0 100644
--- a/app/mailers/user_mailer.rb
+++ b/app/mailers/user_mailer.rb
@@ -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
diff --git a/app/models/user.rb b/app/models/user.rb
index 2e9bf3d236..13695cf6ba 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -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
diff --git a/app/services/restapi_query_service.rb b/app/services/restapi_query_service.rb
index 7272c345e1..44f3fbf235 100644
--- a/app/services/restapi_query_service.rb
+++ b/app/services/restapi_query_service.rb
@@ -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
diff --git a/app/views/user_mailer/password_reset.html.erb b/app/views/user_mailer/password_reset.html.erb
new file mode 100644
index 0000000000..bdd5b6c06d
--- /dev/null
+++ b/app/views/user_mailer/password_reset.html.erb
@@ -0,0 +1,2 @@
+
Reset Password
+ Please use this code to reset your password: <%= @user.forgot_password_token %>
diff --git a/config/application.rb b/config/application.rb
index 0a9d8b9d85..4045a089bd 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -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
diff --git a/config/environments/development.rb b/config/environments/development.rb
index b7b8d31864..c388949fae 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -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
diff --git a/config/routes.rb b/config/routes.rb
index 4e680cdc3e..291ea936da 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -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
diff --git a/db/migrate/20210609020556_add_forgot_password_to_user.rb b/db/migrate/20210609020556_add_forgot_password_to_user.rb
new file mode 100644
index 0000000000..1f0f206287
--- /dev/null
+++ b/db/migrate/20210609020556_add_forgot_password_to_user.rb
@@ -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
diff --git a/db/migrate/20210617031153_rename_forgot_password_token.rb b/db/migrate/20210617031153_rename_forgot_password_token.rb
new file mode 100644
index 0000000000..60bf57fe38
--- /dev/null
+++ b/db/migrate/20210617031153_rename_forgot_password_token.rb
@@ -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
diff --git a/db/schema.rb b/db/schema.rb
index 028448f28d..49adde58d7 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,14 +10,14 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2021_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
diff --git a/docs/docs/data-sources/mssql.md b/docs/docs/data-sources/mssql.md
new file mode 100644
index 0000000000..c4b0fdaf27
--- /dev/null
+++ b/docs/docs/data-sources/mssql.md
@@ -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.
+
+
+
+
+## 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.
+
+
+
+
+:::tip
+Query results can be transformed using transformations. Read our transformations documentation to see how: [link](/tutorial/transformations)
+:::
\ No newline at end of file
diff --git a/docs/static/img/datasource-reference/mssql/connect.gif b/docs/static/img/datasource-reference/mssql/connect.gif
new file mode 100644
index 0000000000..dc39f22a28
Binary files /dev/null and b/docs/static/img/datasource-reference/mssql/connect.gif differ
diff --git a/docs/static/img/datasource-reference/mssql/query.gif b/docs/static/img/datasource-reference/mssql/query.gif
new file mode 100644
index 0000000000..1dd498e9ef
Binary files /dev/null and b/docs/static/img/datasource-reference/mssql/query.gif differ
diff --git a/frontend/src/App/App.jsx b/frontend/src/App/App.jsx
index b1b22b0eb9..6441c2578e 100644
--- a/frontend/src/App/App.jsx
+++ b/frontend/src/App/App.jsx
@@ -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 {
}
- {!onboarded &&
+ {!onboarded &&
}
@@ -71,6 +73,8 @@ class App extends React.Component {
+
+
diff --git a/frontend/src/Editor/Editor.jsx b/frontend/src/Editor/Editor.jsx
index 53137eeeb4..03e1ac74d3 100644
--- a/frontend/src/Editor/Editor.jsx
+++ b/frontend/src/Editor/Editor.jsx
@@ -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 });
}}
>
- {dataSource.name}
+ {' '}
+ {dataSource.name}
|
);
@@ -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 (
- this.setState({ editingQuery: true, selectedQuery: dataQuery })}
role="button"
>
-
}.svg`})
+
{dataQuery.name}
- {!(isLoading === true) &&
-
- }
- {isLoading === true &&
-
+ )}
+ {isLoading === true && (
+
- }
+ )}
);
@@ -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 {
-
-
- {zoomLevel * 100}%
-
-
-
this.setState({ currentLayout: 'desktop' })}
disabled={currentLayout === 'desktop'}
>
-
this.setState({ currentLayout: 'mobile' })}
disabled={currentLayout === 'mobile'}
@@ -524,14 +526,9 @@ class Editor extends React.Component {
-
-
- {app
- &&
- }
-
+
{app && }
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%',
}}
>
@@ -653,7 +650,8 @@ class Editor extends React.Component {
You haven't added data sources yet.
this.setState({ showDataSourceManagerModal: true, selectedDataSource: null })
+ onClick={() =>
+ this.setState({ showDataSourceManagerModal: true, selectedDataSource: null })
}
>
add datasource
@@ -668,7 +666,7 @@ class Editor extends React.Component {
-
+
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);
}}
/>
-
+
@@ -738,16 +735,15 @@ class Editor extends React.Component {
) : (
-
- {dataQueries.map((query) => this.renderDataQuery(query))}
-
+
{dataQueries.map((query) => this.renderDataQuery(query))}
{dataQueries.length === 0 && (
You haven't created queries yet.
this.setState({ selectedQuery: {}, editingQuery: false, addingQuery: true })
+ onClick={() =>
+ this.setState({ selectedQuery: {}, editingQuery: false, addingQuery: true })
}
>
create query
diff --git a/frontend/src/Editor/QueryManager/QueryEditors/Restapi.jsx b/frontend/src/Editor/QueryManager/QueryEditors/Restapi.jsx
index 899c8324bd..709fa3f10a 100644
--- a/frontend/src/Editor/QueryManager/QueryEditors/Restapi.jsx
+++ b/frontend/src/Editor/QueryManager/QueryEditors/Restapi.jsx
@@ -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 (
-
+
-
+
+ {dataSourceURL && (
+
+ {dataSourceURL}
+
+ )}
{ changeOption(this, 'url', value); }}
+ onChange={(value) => {
+ changeOption(this, 'url', value);
+ }}
/>
- {[{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) => (
@@ -117,7 +139,10 @@ class Restapi extends React.Component {
))}
-
this.addNewKeyValuePair(option.value)}>
+ this.addNewKeyValuePair(option.value)}
+ >
+
diff --git a/frontend/src/Editor/QueryManager/QueryManager.jsx b/frontend/src/Editor/QueryManager/QueryManager.jsx
index 4603bbe8bc..bb3f45ea6d 100644
--- a/frontend/src/Editor/QueryManager/QueryManager.jsx
+++ b/frontend/src/Editor/QueryManager/QueryManager.jsx
@@ -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 {
)}
- {((addingQuery || editingQuery) && selectedDataSource) && (
+ {(addingQuery || editingQuery) && selectedDataSource && (
{(addingQuery || editingQuery) && (
{
- 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
@@ -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 && (
-
+
-
+
- {previewLoading &&
}
- {previewLoading === false &&
+ {previewLoading && (
+
+
+
+ )}
+ {previewLoading === false && (
- }
+ )}
)}
diff --git a/frontend/src/ForgotPassword/ForgotPasswordPage.jsx b/frontend/src/ForgotPassword/ForgotPasswordPage.jsx
new file mode 100644
index 0000000000..7d98befb08
--- /dev/null
+++ b/frontend/src/ForgotPassword/ForgotPasswordPage.jsx
@@ -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 (
+
+
+
+
+
+ Don't have account yet?
+
+ Sign up
+
+
+
+
+ );
+ }
+}
+
+export { ForgotPassword };
diff --git a/frontend/src/ForgotPassword/index.js b/frontend/src/ForgotPassword/index.js
new file mode 100644
index 0000000000..750fa59a71
--- /dev/null
+++ b/frontend/src/ForgotPassword/index.js
@@ -0,0 +1 @@
+export * from './ForgotPasswordPage';
diff --git a/frontend/src/LoginPage/LoginPage.jsx b/frontend/src/LoginPage/LoginPage.jsx
index caf55e74ab..cd68a2714e 100644
--- a/frontend/src/LoginPage/LoginPage.jsx
+++ b/frontend/src/LoginPage/LoginPage.jsx
@@ -69,7 +69,9 @@ class LoginPage extends React.Component {
diff --git a/frontend/src/ResetPassword/ResetPasswordPage.jsx b/frontend/src/ResetPassword/ResetPasswordPage.jsx
new file mode 100644
index 0000000000..506668eac1
--- /dev/null
+++ b/frontend/src/ResetPassword/ResetPasswordPage.jsx
@@ -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 (
+
+
+
+
+
+ Don't have account yet?
+
+ Sign up
+
+
+
+
+ );
+ }
+}
+
+export { ResetPassword };
diff --git a/frontend/src/ResetPassword/index.js b/frontend/src/ResetPassword/index.js
new file mode 100644
index 0000000000..2d7f2efefd
--- /dev/null
+++ b/frontend/src/ResetPassword/index.js
@@ -0,0 +1 @@
+export * from './ResetPasswordPage';
diff --git a/test/controllers/forgot_password_controller_test.rb b/test/controllers/forgot_password_controller_test.rb
new file mode 100644
index 0000000000..5c67e21620
--- /dev/null
+++ b/test/controllers/forgot_password_controller_test.rb
@@ -0,0 +1,7 @@
+require "test_helper"
+
+class ForgotPasswordControllerTest < ActionDispatch::IntegrationTest
+ # test "the truth" do
+ # assert true
+ # end
+end