Merge branch 'release/v0.5.9' into main

This commit is contained in:
navaneeth 2021-06-17 18:17:45 +05:30
commit 0318427436
24 changed files with 604 additions and 215 deletions

View 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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,2 @@
<h1> Reset Password </h1>
<p> Please use this code to reset your password: <%= @user.forgot_password_token %>

View file

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

View file

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

View file

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

View 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

View file

@ -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
View file

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

View 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)
:::

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 MiB

View file

@ -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} />

View file

@ -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&apos;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&apos;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

View file

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

View file

@ -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>
)}

View 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&apos;t have account yet? &nbsp;
<Link to={'/signup'} tabIndex="-1">
Sign up
</Link>
</div>
</div>
</div>
);
}
}
export { ForgotPassword };

View file

@ -0,0 +1 @@
export * from './ForgotPasswordPage';

View file

@ -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">

View 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&apos;t have account yet? &nbsp;
<Link to={'/signup'} tabIndex="-1">
Sign up
</Link>
</div>
</div>
</div>
);
}
}
export { ResetPassword };

View file

@ -0,0 +1 @@
export * from './ResetPasswordPage';

View file

@ -0,0 +1,7 @@
require "test_helper"
class ForgotPasswordControllerTest < ActionDispatch::IntegrationTest
# test "the truth" do
# assert true
# end
end