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. + +ToolJet - Redis connection + + +## 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. + +ToolJet - Redis connection + + +:::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" >
- + {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}% - - + {zoomLevel * 100}% +
- -
-
-
- {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.
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 && (
- +
- +
-
- Preview -
+
Preview
- {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 ( +
+
+
+ + + +
+
+
+

Forgot Password

+
+ + +
+
+ +
+
+
+
+ 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 ( +
+
+
+ + + +
+
+
+

Reset Password

+
+ + +
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+
+
+
+ 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