Merge branch 'develop' into main
13
.env.example
|
|
@ -34,9 +34,12 @@ SMTP_PASSWORD=
|
|||
SMTP_DOMAIN=
|
||||
SMTP_PORT=
|
||||
|
||||
# DISABLE USER SIGNUPS (true or false). Default: true
|
||||
# DISABLE USER SIGNUPS (true or false). only applicable if MULTI_ORGANIZATION=true
|
||||
DISABLE_SIGNUPS=
|
||||
|
||||
# Enables all multi organization features
|
||||
MULTI_ORGANIZATION=
|
||||
|
||||
# OBSERVABILITY
|
||||
APM_VENDOR=
|
||||
SENTRY_DNS=
|
||||
|
|
@ -44,10 +47,4 @@ SENTRY_DEBUG=
|
|||
|
||||
# FEATURE TOGGLE
|
||||
COMMENT_FEATURE_ENABLE=
|
||||
|
||||
#SSO
|
||||
SSO_DISABLE_SIGNUP=
|
||||
SSO_RESTRICTED_DOMAIN=
|
||||
SSO_GOOGLE_OAUTH2_CLIENT_ID=
|
||||
SSO_GIT_OAUTH2_CLIENT_ID=
|
||||
SSO_GIT_OAUTH2_CLIENT_SECRET=
|
||||
ENABLE_MULTIPLAYER_EDITING=true
|
||||
|
|
|
|||
10
app.json
|
|
@ -32,6 +32,14 @@
|
|||
"NODE_OPTIONS": {
|
||||
"description": "Node options configured to increase node memory to support app build",
|
||||
"value": "--max-old-space-size=4096"
|
||||
},
|
||||
"DISABLE_SIGNUPS": {
|
||||
"description": "Disable sign up in login page only applicable if MULTI_ORGANIZATION=true",
|
||||
"value": "false"
|
||||
},
|
||||
"MULTI_ORGANIZATION": {
|
||||
"description": "Enables multi organization feature",
|
||||
"value": "false"
|
||||
}
|
||||
},
|
||||
"formation": {
|
||||
|
|
@ -61,4 +69,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -41,6 +41,7 @@ SENTRY_DEBUG=
|
|||
|
||||
# FEATURE TOGGLE
|
||||
COMMENT_FEATURE_ENABLE=
|
||||
ENABLE_MULTIPLAYER_EDITING=true
|
||||
|
||||
#SSO
|
||||
SSO_DISABLE_SIGNUP=
|
||||
|
|
|
|||
5
docs/docs/password-login/_category_.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"label": "Password Login",
|
||||
"position": 10,
|
||||
"collapsed": true
|
||||
}
|
||||
24
docs/docs/password-login/password-login.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
sidebar_position: 1
|
||||
sidebar_label: Password Login
|
||||
---
|
||||
|
||||
# Password Login
|
||||
|
||||
Password login is enabled by default for all organizations. User with admin privilege can enable/disable it.
|
||||
|
||||
Select `Manage SSO` from organization options
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Select `Password Login`. You can enable/disable it
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
|
@ -57,6 +57,14 @@ Use this environment variable to enable/disable the feature that allows you to a
|
|||
| -------- | ---------------------- |
|
||||
| COMMENT_FEATURE_ENABLE | `true` or `false` |
|
||||
|
||||
#### Multiplayer feature enable ( optional )
|
||||
|
||||
Use this environment variable to enable/disable the feature that allows users to collaboratively work on the canvas.
|
||||
|
||||
| variable | value |
|
||||
| -------- | ---------------------- |
|
||||
| ENABLE_MULTIPLAYER_EDITING | `true` or `false` |
|
||||
|
||||
#### Server Host ( optional )
|
||||
|
||||
You can specify a different server for backend if it is hosted on another server.
|
||||
|
|
@ -65,22 +73,19 @@ You can specify a different server for backend if it is hosted on another server
|
|||
| -------- | ---------------------- |
|
||||
| SERVER_HOST | Configure a hostname for the server as a proxy pass. If no value is set, it defaults to `server`. |
|
||||
|
||||
#### Enable multiple organizations ( optional )
|
||||
|
||||
If you want to enable multiple environments, set the environment variable `MULTI_ORGANIZATION` to `true`.
|
||||
|
||||
|
||||
#### Disabling signups ( optional )
|
||||
|
||||
If you want to restrict the signups and allow new users only by invitations, set the environment variable `DISABLE_SIGNUPS` to `true`.
|
||||
Sign up is enabled only for multiple organization environment. If you want to restrict the signups and allow new users only by invitations, set the environment variable `DISABLE_SIGNUPS` to `true`.
|
||||
|
||||
:::tip
|
||||
You will still be able to see the signup page but won't be able to successfully submit the form.
|
||||
:::
|
||||
|
||||
#### Disable login and signup using username and password
|
||||
|
||||
:::info
|
||||
Use this feature only if you have configured other methods of authentication, such as SSO.
|
||||
:::
|
||||
|
||||
If you want to restrict users from logging in using regular username and password, set the environment variable `DISABLE_PASSWORD_LOGIN` to `true`.
|
||||
|
||||
#### Serve client as a server end-point ( optional )
|
||||
|
||||
By default, the `SERVE_CLIENT` variable will be set to `false` and the server won't serve the client at its `/` end-point.
|
||||
|
|
@ -146,12 +151,6 @@ Prints logs for sentry.
|
|||
| ---------- | ----------------------------------------- |
|
||||
| SENTRY_DEBUG | `true` or `false`. Default value is `false` |
|
||||
|
||||
#### SSO ( optional )
|
||||
|
||||
:::info
|
||||
We currently support GitHub and Google SSO. Check out docs for **[GitHub SSO](/docs/sso/github)** and **[Google SSO](/docs/sso/google)** for more information on respective environment variables.
|
||||
:::
|
||||
|
||||
#### Server URL ( optional)
|
||||
|
||||
This is used to set up for CSP headers and put trace info to be used with APM vendors.
|
||||
|
|
|
|||
30
docs/docs/sso/general-settings.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
sidebar_position: 6
|
||||
sidebar_label: General Settings
|
||||
---
|
||||
|
||||
# Single Sign-On General Settings
|
||||
|
||||
Select `Manage SSO` from organization options
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Select `General Settings`
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## Enable Signup
|
||||
|
||||
You can enable/disable `Enable signup`. If it is enabled, new account will be created for user's first time SSO sign in else only existing users will be allowed to sign in via SSO.
|
||||
|
||||
## Allowed domain
|
||||
|
||||
You can set allowed domains for SSO login, can add multiple domains comma separated. Allowed all domains by default.
|
||||
|
|
@ -5,7 +5,31 @@ title: GitHub
|
|||
|
||||
# GitHub Single Sign-on
|
||||
|
||||
- Go to the [GitHub Developer settings](https://github.com/settings/developers) and navigate to `OAuth Apps` and create a project. `Authorization callback URL` should be `<Your Domain>/sso/git`
|
||||
Select `Manage SSO` from organization options
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Select `Git`, Git login will be disabled by default
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Enable Git. You can see `Redirect URL` generated
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Go to [GitHub Developer settings](https://github.com/settings/developers) and navigate to `OAuth Apps` and create a project. `Authorization callback URL` should be the generated `Redirect URL` in Git manage SSO page.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||
|
|
@ -29,14 +53,6 @@ title: GitHub
|
|||
|
||||
</div>
|
||||
|
||||
- Lastly, supply the environment variables `SSO_GIT_OAUTH2_CLIENT_ID` which is client id and `SSO_GIT_OAUTH2_CLIENT_SECRET` is client secret to your deployment.
|
||||
Lastly, enter `Client Id` and `Client Secret` in Git manage SSO page and save.
|
||||
|
||||
:::info
|
||||
|
||||
### Restrict signup via SSO
|
||||
|
||||
Set the environment variable `SSO_DISABLE_SIGNUP` to `true` to ensure that users can only log in and not sign up via SSO. If this variable is set to `true`, only those users who have already signed up, or the ones that are invited, can access ToolJet via SSO.
|
||||
:::
|
||||
|
||||
<br />
|
||||
The GitHub sign-in button will now be available in your ToolJet login screen.
|
||||
The GitHub sign-in button will now be available in your ToolJet login screen if you have not enabled multiple organization.
|
||||
|
|
|
|||
|
|
@ -5,7 +5,31 @@ title: Google
|
|||
|
||||
# Google Single Sign-on
|
||||
|
||||
- Go to the [Google cloud console](https://console.cloud.google.com/) and create a project.
|
||||
Select `Manage SSO` from organization options
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Select `Google`, Google login will be disabled by default
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Enable Google. You can see `Redirect URL` generated
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Go to [Google cloud console](https://console.cloud.google.com/) and create a project.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||
|
|
@ -48,20 +72,14 @@ user who is signing in
|
|||
|
||||
</div>
|
||||
|
||||
- Lastly, supply the environment variable `SSO_GOOGLE_OAUTH2_CLIENT_ID` to your deployment. This value will be available from your [Google cloud console credentials page](https://console.cloud.google.com/apis/credentials)
|
||||
Set the `Redirect URL` generated at manage SSO `Google` page under Authorised redirect URIs
|
||||
|
||||
:::info
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||
### Restrict to your domains
|
||||
Set the environment variable `SSO_RESTRICTED_DOMAIN` to ensure that ToolJet verifies the domain of the user who signs in via SSO, on the server side.
|
||||
If you're setting this environment variable, please make sure that the value does not contain any protocols, subdomains or slashes. It should
|
||||
simply be `yourdomain.com`. Add multiple domians separated by coma example : `yourdomain.com,yourotherdomain.com`
|
||||
:::
|
||||

|
||||
|
||||
:::info
|
||||
### Restrict signup via SSO
|
||||
Set the environment variable `SSO_DISABLE_SIGNUP` to `true` to ensure that users can only log in and not sign up via SSO. If this variable is set to `true`, only those users who have already signed up, or the ones that are invited, can access ToolJet via SSO.
|
||||
:::
|
||||
</div>
|
||||
|
||||
<br />
|
||||
The Google sign-in button will now be available in your ToolJet login screen.
|
||||
Lastly, set the `client id` in google manage SSO page. This value will be available from your [Google cloud console credentials page](https://console.cloud.google.com/apis/credentials)
|
||||
|
||||
The Google sign-in button will now be available in your ToolJet login screen, if you are not enabled multiple organization.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
const isProd = process.env.NODE_ENV === 'production';
|
||||
|
||||
/** @type {import('@docusaurus/types').DocusaurusConfig} */
|
||||
module.exports = {
|
||||
title: 'ToolJet - Documentation',
|
||||
|
|
@ -33,7 +35,7 @@ module.exports = {
|
|||
},
|
||||
navbar: {
|
||||
logo: {
|
||||
href: '/docs/intro',
|
||||
href: '/docs',
|
||||
alt: 'ToolJet Logo',
|
||||
src: 'img/logo.svg',
|
||||
width: 90
|
||||
|
|
@ -112,11 +114,13 @@ module.exports = {
|
|||
customCss: require.resolve('./src/css/custom.css'),
|
||||
},
|
||||
sitemap: {},
|
||||
gtag: {
|
||||
trackingID: process.env.GA_MID,
|
||||
// Optional fields.
|
||||
anonymizeIP: true, // Should IPs be anonymized?
|
||||
},
|
||||
gtag: isProd
|
||||
? {
|
||||
trackingID: process.env.GA_MID,
|
||||
// Optional fields.
|
||||
anonymizeIP: true, // Should IPs be anonymized?
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
],
|
||||
],
|
||||
|
|
|
|||
BIN
docs/static/img/password-login/organization-menu.png
vendored
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
docs/static/img/password-login/password-login.png
vendored
Normal file
|
After Width: | Height: | Size: 369 KiB |
BIN
docs/static/img/sso/general/general-settings.png
vendored
Normal file
|
After Width: | Height: | Size: 488 KiB |
BIN
docs/static/img/sso/git/manage-sso-1.png
vendored
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
docs/static/img/sso/git/manage-sso-2.png
vendored
Normal file
|
After Width: | Height: | Size: 585 KiB |
BIN
docs/static/img/sso/google/authorized-redirect-urls.png
vendored
Normal file
|
After Width: | Height: | Size: 169 KiB |
BIN
docs/static/img/sso/google/manage-sso-1.png
vendored
Normal file
|
After Width: | Height: | Size: 389 KiB |
BIN
docs/static/img/sso/google/manage-sso-2.png
vendored
Normal file
|
After Width: | Height: | Size: 493 KiB |
|
|
@ -0,0 +1,5 @@
|
|||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 2H3C2.44772 2 2 2.44772 2 3V3.5C2 4.05228 2.44772 4.5 3 4.5H4C4.55228 4.5 5 4.05228 5 3.5V3C5 2.44772 4.55228 2 4 2Z" stroke="#2C3E50" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4 6.5H3C2.44772 6.5 2 6.94772 2 7.5V9C2 9.55228 2.44772 10 3 10H4C4.55228 10 5 9.55228 5 9V7.5C5 6.94772 4.55228 6.5 4 6.5Z" stroke="#2C3E50" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9 2H8C7.44772 2 7 2.44772 7 3V9C7 9.55229 7.44772 10 8 10H9C9.55228 10 10 9.55229 10 9V3C10 2.44772 9.55228 2 9 2Z" stroke="#2C3E50" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 692 B |
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.5 3.5L5 6L2.5 8.5" stroke="#2C3E50" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.5 8.5H9.5" stroke="#2C3E50" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 289 B |
|
|
@ -0,0 +1,7 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-pinned-off" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.85" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<line x1="3" y1="3" x2="21" y2="21"></line>
|
||||
<path d="M15 4.5l-3.249 3.249m-2.57 1.433l-2.181 .818l-1.5 1.5l7 7l1.5 -1.5l.82 -2.186m1.43 -2.563l3.25 -3.251"></path>
|
||||
<line x1="9" y1="15" x2="4.5" y2="19.5"></line>
|
||||
<line x1="14.5" y1="4" x2="20" y2="9.5"></line>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 572 B |
|
|
@ -0,0 +1,8 @@
|
|||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 4C3.55228 4 4 3.55228 4 3C4 2.44772 3.55228 2 3 2C2.44772 2 2 2.44772 2 3C2 3.55228 2.44772 4 3 4Z" stroke="#2C3E50" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9 10C9.55228 10 10 9.55228 10 9C10 8.44772 9.55228 8 9 8C8.44772 8 8 8.44772 8 9C8 9.55228 8.44772 10 9 10Z" stroke="#2C3E50" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5.5 3H8C8.26522 3 8.51957 3.10536 8.70711 3.29289C8.89464 3.48043 9 3.73478 9 4V8" stroke="#2C3E50" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7 4.5L5.5 3L7 1.5" stroke="#2C3E50" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.5 9H4C3.73478 9 3.48043 8.89464 3.29289 8.70711C3.10536 8.51957 3 8.26522 3 8V4" stroke="#2C3E50" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5 7.5L6.5 9L5 10.5" stroke="#2C3E50" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 975 B |
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.5 2C1.25 4.5 1.25 7 2.5 10M9.5 2C10.75 4.5 10.75 7 9.5 10M4.5 4.5H5C5.5 4.5 5.5 5 6.008 6.2635C6.5 7.5 6.5 8 7 8H7.5" stroke="#2C3E50" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4 8C4.75 8 5.5 7 6 6.25C6.5 5.5 7.25 4.5 8 4.5" stroke="#2C3E50" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 423 B |
5
frontend/assets/images/icons/widgets/verticaldivider.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="17px" height="17px" viewBox="0 0 17 17" version="1.1" >
|
||||
<g>
|
||||
</g>
|
||||
<path d="M2.207 8h3.772v1h-3.772l1.646 1.646-0.707 0.707-2.853-2.853 2.854-2.854 0.707 0.707-1.647 1.647zM13.854 5.646l-0.707 0.707 1.646 1.647h-3.772v1h3.772l-1.646 1.646 0.707 0.707 2.853-2.853-2.853-2.854zM8 17h1v-17h-1v17z" stroke="#61656F" stroke-width="0"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 426 B |
|
|
@ -1 +1,7 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M11 17a2.5 2.5 0 0 0 2 0" /><path d="M12 3c-4.664 0 -7.396 2.331 -7.862 5.595a11.816 11.816 0 0 0 2 8.592a10.777 10.777 0 0 0 3.199 3.064c1.666 1 3.664 1 5.33 0a10.777 10.777 0 0 0 3.199 -3.064a11.89 11.89 0 0 0 2 -8.592c-.466 -3.265 -3.198 -5.595 -7.862 -5.595z" /><line x1="8" y1="11" x2="10" y2="13" /><line x1="16" y1="11" x2="14" y2="13" /></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-user-exclamation" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<circle cx="9" cy="7" r="4"></circle>
|
||||
<path d="M3 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2"></path>
|
||||
<line x1="19" y1="7" x2="19" y2="10"></line>
|
||||
<line x1="19" y1="14" x2="19" y2="14.01"></line>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 605 B After Width: | Height: | Size: 507 B |
|
|
@ -1,11 +1,13 @@
|
|||
import React from 'react';
|
||||
import { buildURLWithQuery } from '@/_helpers/utils';
|
||||
|
||||
export default function GitSSOLoginButton() {
|
||||
const clientId = window.public_config.SSO_GIT_OAUTH2_CLIENT_ID;
|
||||
export default function GitSSOLoginButton({ configs }) {
|
||||
const gitLogin = (e) => {
|
||||
e.preventDefault();
|
||||
window.location.href = buildURLWithQuery('https://github.com/login/oauth/authorize', { client_id: clientId });
|
||||
window.location.href = buildURLWithQuery('https://github.com/login/oauth/authorize', {
|
||||
client_id: configs?.client_id,
|
||||
scope: 'user:email',
|
||||
});
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -1,24 +1,15 @@
|
|||
import React from 'react';
|
||||
import GoogleLogin from 'react-google-login';
|
||||
import { authenticationService } from '@/_services';
|
||||
|
||||
export default function GoogleSSOLoginButton(props) {
|
||||
const googleSSOSuccessHandler = (googleUser) => {
|
||||
const idToken = googleUser.getAuthResponse().id_token;
|
||||
authenticationService
|
||||
.signInViaOAuth({ token: idToken, origin: 'google' })
|
||||
.then(props.authSuccessHandler)
|
||||
.catch(props.authFailureHandler);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-2">
|
||||
<GoogleLogin
|
||||
clientId={window.public_config?.SSO_GOOGLE_OAUTH2_CLIENT_ID}
|
||||
clientId={props.configs?.client_id}
|
||||
buttonText="Login"
|
||||
onSuccess={googleSSOSuccessHandler}
|
||||
onFailure={props.authFailureHandler}
|
||||
cookiePolicy={'single_host_origin'}
|
||||
uxMode="redirect"
|
||||
redirectUri={`${window.location.protocol}//${window.location.host}/sso/google/${props.configId}`}
|
||||
render={(renderProps) => (
|
||||
<div>
|
||||
<button {...renderProps} className="btn border-0 rounded-2">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react';
|
||||
import config from 'config';
|
||||
import { Router, Route, Redirect } from 'react-router-dom';
|
||||
import { history } from '@/_helpers';
|
||||
import { authenticationService, tooljetService } from '@/_services';
|
||||
|
|
@ -6,7 +7,7 @@ import { PrivateRoute } from '@/_components';
|
|||
import { HomePage } from '@/HomePage';
|
||||
import { LoginPage } from '@/LoginPage';
|
||||
import { SignupPage } from '@/SignupPage';
|
||||
import { ConfirmationPage } from '@/ConfirmationPage';
|
||||
import { ConfirmationPage, OrganizationInvitationPage } from '@/ConfirmationPage';
|
||||
import { Authorize } from '@/Oauth2';
|
||||
import { Authorize as Oauth } from '@/Oauth';
|
||||
import { Viewer } from '@/Editor';
|
||||
|
|
@ -17,9 +18,11 @@ import { SettingsPage } from '../SettingsPage/SettingsPage';
|
|||
import { OnboardingModal } from '@/Onboarding/OnboardingModal';
|
||||
import { ForgotPassword } from '@/ForgotPassword';
|
||||
import { ResetPassword } from '@/ResetPassword';
|
||||
import { ManageSSO } from '@/ManageSSO';
|
||||
import { lt } from 'semver';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import { RealtimeEditor } from '@/Editor/RealtimeEditor';
|
||||
import { Editor } from '@/Editor/Editor';
|
||||
|
||||
import '@/_styles/theme.scss';
|
||||
import 'emoji-mart/css/emoji-mart.css';
|
||||
|
|
@ -119,8 +122,9 @@ class App extends React.Component {
|
|||
switchDarkMode={this.switchDarkMode}
|
||||
darkMode={darkMode}
|
||||
/>
|
||||
<Route path="/login" component={LoginPage} />
|
||||
<Route path="/sso/:origin" component={Oauth} />
|
||||
<Route path="/login/:organisationId" exact component={LoginPage} />
|
||||
<Route path="/login" exact component={LoginPage} />
|
||||
<Route path="/sso/:origin/:configId" component={Oauth} />
|
||||
<Route path="/signup" component={SignupPage} />
|
||||
<Route path="/forgot-password" component={ForgotPassword} />
|
||||
<Route path="/reset-password" component={ResetPassword} />
|
||||
|
|
@ -132,17 +136,30 @@ class App extends React.Component {
|
|||
pathname: '/confirm',
|
||||
state: {
|
||||
token: props.match.params.token,
|
||||
search: props.location.search,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route path="/confirm" component={ConfirmationPage} />
|
||||
<Route
|
||||
path="/organization-invitations/:token"
|
||||
render={(props) => (
|
||||
<Redirect
|
||||
to={{
|
||||
pathname: '/confirm-invite',
|
||||
state: {
|
||||
token: props.match.params.token,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route path="/confirm-invite" component={OrganizationInvitationPage} />
|
||||
<PrivateRoute
|
||||
exact
|
||||
path="/apps/:id"
|
||||
component={RealtimeEditor}
|
||||
component={config.ENABLE_MULTIPLAYER_EDITING ? RealtimeEditor : Editor}
|
||||
switchDarkMode={this.switchDarkMode}
|
||||
darkMode={darkMode}
|
||||
/>
|
||||
|
|
@ -174,6 +191,13 @@ class App extends React.Component {
|
|||
switchDarkMode={this.switchDarkMode}
|
||||
darkMode={darkMode}
|
||||
/>
|
||||
<PrivateRoute
|
||||
exact
|
||||
path="/manage-sso"
|
||||
component={ManageSSO}
|
||||
switchDarkMode={this.switchDarkMode}
|
||||
darkMode={darkMode}
|
||||
/>
|
||||
<PrivateRoute
|
||||
exact
|
||||
path="/groups"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
import { userService } from '@/_services';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import queryString from 'query-string';
|
||||
|
||||
class ConfirmationPage extends React.Component {
|
||||
constructor(props) {
|
||||
|
|
@ -9,7 +8,6 @@ class ConfirmationPage extends React.Component {
|
|||
|
||||
this.state = {
|
||||
isLoading: false,
|
||||
newSignup: !!queryString.parse(props.location.state.search).signup,
|
||||
};
|
||||
this.formRef = React.createRef(null);
|
||||
}
|
||||
|
|
@ -26,7 +24,7 @@ class ConfirmationPage extends React.Component {
|
|||
setPassword = (e) => {
|
||||
e.preventDefault();
|
||||
const token = this.props.location.state.token;
|
||||
const { password, organization, role, newSignup, firstName, lastName, password_confirmation } = this.state;
|
||||
const { password, organization, role, firstName, lastName, password_confirmation } = this.state;
|
||||
this.setState({ isLoading: true });
|
||||
|
||||
if (!password || !password_confirmation || !password.trim() || !password_confirmation.trim()) {
|
||||
|
|
@ -51,13 +49,12 @@ class ConfirmationPage extends React.Component {
|
|||
password,
|
||||
organization,
|
||||
role,
|
||||
newSignup,
|
||||
firstName,
|
||||
lastName,
|
||||
})
|
||||
.then(() => {
|
||||
this.setState({ isLoading: false });
|
||||
toast.success('Password has been set successfully.', {
|
||||
toast.success('Account has been setup successfully.', {
|
||||
position: 'top-center',
|
||||
});
|
||||
this.props.history.push('/login');
|
||||
|
|
@ -69,7 +66,7 @@ class ConfirmationPage extends React.Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
const { isLoading, newSignup } = this.state;
|
||||
const { isLoading } = this.state;
|
||||
const roles = [
|
||||
'CTO/CIO',
|
||||
'Founder/CEO',
|
||||
|
|
@ -102,59 +99,54 @@ class ConfirmationPage extends React.Component {
|
|||
<form className="card card-md" action="." method="get" autoComplete="off">
|
||||
<div className="card-body">
|
||||
<h2 className="card-title text-center mb-4">Set up your account</h2>
|
||||
{newSignup && (
|
||||
<>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">First name</label>
|
||||
<div className="input-group input-group-flat">
|
||||
<input
|
||||
onChange={this.handleChange}
|
||||
name="firstName"
|
||||
type="text"
|
||||
className="form-control"
|
||||
autoComplete="off"
|
||||
/>
|
||||
<span className="input-group-text"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Last name</label>
|
||||
<div className="input-group input-group-flat">
|
||||
<input
|
||||
onChange={this.handleChange}
|
||||
name="lastName"
|
||||
type="text"
|
||||
className="form-control"
|
||||
autoComplete="off"
|
||||
/>
|
||||
<span className="input-group-text"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Organization</label>
|
||||
<div className="input-group input-group-flat">
|
||||
<input
|
||||
onChange={this.handleChange}
|
||||
name="organization"
|
||||
type="text"
|
||||
className="form-control"
|
||||
autoComplete="off"
|
||||
/>
|
||||
<span className="input-group-text"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<div className="form-label">Role</div>
|
||||
<select className="form-select" name="role" defaultValue="" onChange={this.handleChange}>
|
||||
<option value="" disabled>
|
||||
Please select
|
||||
</option>
|
||||
{roleOptions}
|
||||
</select>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="mb-3">
|
||||
<label className="form-label">First name</label>
|
||||
<div className="input-group input-group-flat">
|
||||
<input
|
||||
onChange={this.handleChange}
|
||||
name="firstName"
|
||||
type="text"
|
||||
className="form-control"
|
||||
autoComplete="off"
|
||||
/>
|
||||
<span className="input-group-text"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Last name</label>
|
||||
<div className="input-group input-group-flat">
|
||||
<input
|
||||
onChange={this.handleChange}
|
||||
name="lastName"
|
||||
type="text"
|
||||
className="form-control"
|
||||
autoComplete="off"
|
||||
/>
|
||||
<span className="input-group-text"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Organization</label>
|
||||
<div className="input-group input-group-flat">
|
||||
<input
|
||||
onChange={this.handleChange}
|
||||
name="organization"
|
||||
type="text"
|
||||
className="form-control"
|
||||
autoComplete="off"
|
||||
/>
|
||||
<span className="input-group-text"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<div className="form-label">Role</div>
|
||||
<select className="form-select" name="role" defaultValue="" onChange={this.handleChange}>
|
||||
<option value="" disabled>
|
||||
Please select
|
||||
</option>
|
||||
{roleOptions}
|
||||
</select>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Password</label>
|
||||
<div className="input-group input-group-flat">
|
||||
|
|
|
|||
143
frontend/src/ConfirmationPage/OrganizationInvitationPage.jsx
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
import React from 'react';
|
||||
import { userService } from '@/_services';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
class OrganizationInvitationPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isLoading: false,
|
||||
};
|
||||
this.formRef = React.createRef(null);
|
||||
this.single_organization = window.public_config?.MULTI_ORGANIZATION !== 'true';
|
||||
}
|
||||
|
||||
handleChange = (event) => {
|
||||
this.setState({ [event.target.name]: event.target.value });
|
||||
};
|
||||
|
||||
acceptInvite = (e, isSetPassword) => {
|
||||
e.preventDefault();
|
||||
|
||||
const token = this.props.location.state.token;
|
||||
const { password, password_confirmation } = this.state;
|
||||
this.setState({ isLoading: true });
|
||||
|
||||
if (isSetPassword) {
|
||||
if (!password || !password_confirmation || !password.trim() || !password_confirmation.trim()) {
|
||||
this.setState({ isLoading: false });
|
||||
toast.error("Password shouldn't be empty or contain white space(s)", {
|
||||
position: 'top-center',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (password !== password_confirmation) {
|
||||
this.setState({ isLoading: false });
|
||||
toast.error("Passwords don't match", {
|
||||
position: 'top-center',
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
userService
|
||||
.acceptInvite({
|
||||
token,
|
||||
password,
|
||||
})
|
||||
.then(() => {
|
||||
this.setState({ isLoading: false });
|
||||
toast.success(`Added to the organization${isSetPassword ? ' and password has been set ' : ' '}successfully.`, {
|
||||
position: 'top-center',
|
||||
});
|
||||
this.props.history.push('/login');
|
||||
})
|
||||
.catch(({ error }) => {
|
||||
this.setState({ isLoading: false });
|
||||
toast.error(error, { position: 'top-center' });
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isLoading } = this.state;
|
||||
|
||||
return (
|
||||
<div className="page page-center" ref={this.formRef}>
|
||||
<div className="container-tight py-2 invitation-page">
|
||||
<div className="text-center mb-4">
|
||||
<a href=".">
|
||||
<img src="/assets/images/logo-color.svg" height="30" alt="" />
|
||||
</a>
|
||||
</div>
|
||||
<form className="card card-md" action="." method="get" autoComplete="off">
|
||||
<div className="card-body">
|
||||
{!this.single_organization && (
|
||||
<>
|
||||
<h2 className="card-title text-center mb-2">Already have an account?</h2>
|
||||
<div className="mb-3">
|
||||
<button
|
||||
className={`btn mt-2 btn-primary w-100 ${isLoading ? ' btn-loading' : ''}`}
|
||||
onClick={(e) => this.acceptInvite(e)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Accept invite
|
||||
</button>
|
||||
</div>
|
||||
<div className="org-invite-or">
|
||||
<h2>
|
||||
<span>OR</span>
|
||||
</h2>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<h2 className="card-title text-center mb-4">Set up your account</h2>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Password</label>
|
||||
<div className="input-group input-group-flat">
|
||||
<input
|
||||
onChange={this.handleChange}
|
||||
name="password"
|
||||
type="password"
|
||||
className="form-control"
|
||||
autoComplete="off"
|
||||
/>
|
||||
<span className="input-group-text"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Confirm Password</label>
|
||||
<div className="input-group input-group-flat">
|
||||
<input
|
||||
onChange={this.handleChange}
|
||||
name="password_confirmation"
|
||||
type="password"
|
||||
className="form-control"
|
||||
autoComplete="off"
|
||||
/>
|
||||
<span className="input-group-text"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-footer">
|
||||
<p>
|
||||
By clicking the button below, you agree to our{' '}
|
||||
<a href="https://tooljet.io/terms">Terms and Conditions</a>.
|
||||
</p>
|
||||
<button
|
||||
className={`btn mt-2 btn-primary w-100 ${isLoading ? ' btn-loading' : ''}`}
|
||||
onClick={(e) => this.acceptInvite(e, true)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Finish account setup and accept invite
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { OrganizationInvitationPage };
|
||||
|
|
@ -1 +1,2 @@
|
|||
export * from './ConfirmationPage';
|
||||
export * from './OrganizationInvitationPage';
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import { renderTooltip } from '@/_helpers/appUtils';
|
|||
import { RangeSlider } from './Components/RangeSlider';
|
||||
import { Timeline } from './Components/Timeline';
|
||||
import { SvgImage } from './Components/SvgImage';
|
||||
import { VerticalDivider } from './Components/verticalDivider';
|
||||
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
|
||||
import '@/_styles/custom.scss';
|
||||
import { resolveProperties, resolveStyles } from './component-properties-resolution';
|
||||
|
|
@ -82,6 +83,7 @@ const AllComponents = {
|
|||
RangeSlider,
|
||||
Timeline,
|
||||
SvgImage,
|
||||
VerticalDivider,
|
||||
};
|
||||
|
||||
export const Box = function Box({
|
||||
|
|
|
|||
|
|
@ -12,10 +12,12 @@ export const DropDown = function DropDown({
|
|||
onComponentClick,
|
||||
id,
|
||||
component,
|
||||
exposedVariables,
|
||||
}) {
|
||||
let { label, value, display_values, values } = properties;
|
||||
const { selectedTextColor, borderRadius, visibility, disabledState, justifyContent } = styles;
|
||||
const [currentValue, setCurrentValue] = useState(() => value);
|
||||
const { value: exposedValue } = exposedVariables;
|
||||
|
||||
if (!_.isArray(values)) {
|
||||
values = [];
|
||||
|
|
@ -46,12 +48,13 @@ export const DropDown = function DropDown({
|
|||
if (values?.includes(value)) {
|
||||
newValue = value;
|
||||
}
|
||||
setExposedVariable('value', value);
|
||||
setCurrentValue(newValue);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
setExposedVariable('value', currentValue);
|
||||
if (exposedValue !== currentValue) setExposedVariable('value', currentValue);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentValue]);
|
||||
|
||||
|
|
|
|||
|
|
@ -916,7 +916,7 @@ export const componentTypes = [
|
|||
justifyContent: { type: 'alignButtons', displayName: 'Align Text' },
|
||||
},
|
||||
exposedVariables: {
|
||||
value: null,
|
||||
value: 2,
|
||||
searchText: '',
|
||||
},
|
||||
definition: {
|
||||
|
|
@ -2023,4 +2023,39 @@ export const componentTypes = [
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VerticalDivider',
|
||||
displayName: 'Vertical Divider',
|
||||
description: 'Vertical Separator between components',
|
||||
component: 'VerticalDivider',
|
||||
defaultSize: {
|
||||
width: 2,
|
||||
height: 100,
|
||||
},
|
||||
others: {
|
||||
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
|
||||
showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
|
||||
},
|
||||
properties: {},
|
||||
events: {},
|
||||
styles: {
|
||||
dividerColor: { type: 'color', displayName: 'Divider Color' },
|
||||
visibility: { type: 'toggle', displayName: 'Visibility' },
|
||||
},
|
||||
exposedVariables: {
|
||||
value: {},
|
||||
},
|
||||
definition: {
|
||||
others: {
|
||||
showOnDesktop: { value: '{{true}}' },
|
||||
showOnMobile: { value: '{{false}}' },
|
||||
},
|
||||
properties: {},
|
||||
events: [],
|
||||
styles: {
|
||||
visibility: { value: '{{true}}' },
|
||||
dividerColor: { value: '#E7E8EA' },
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
|||
16
frontend/src/Editor/Components/verticalDivider.jsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
|
||||
export const VerticalDivider = function Divider({ styles, height, width }) {
|
||||
const { visibility, dividerColor } = styles;
|
||||
const color = dividerColor ?? '#E7E8EA';
|
||||
|
||||
return (
|
||||
<div className="row" style={{ display: visibility ? 'flex' : 'none', padding: '0 8px', width, height }}>
|
||||
<div className="col-6"></div>
|
||||
<div
|
||||
className="col-6 border-right"
|
||||
style={{ height, width: '1px', backgroundColor: color, padding: '0rem', marginLeft: '0.5rem' }}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
/* eslint-disable import/no-named-as-default */
|
||||
import React, { createRef } from 'react';
|
||||
import cx from 'classnames';
|
||||
import { datasourceService, dataqueryService, appService, authenticationService, appVersionService } from '@/_services';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
|
|
@ -154,11 +155,13 @@ class Editor extends React.Component {
|
|||
* current appDef is equal to the newAppDef then we do not trigger a realtimeSave
|
||||
*/
|
||||
initRealtimeSave = () => {
|
||||
this.props.ymap.observe(() => {
|
||||
if (!isEqual(this.state.editingVersion?.id, this.props.ymap.get('appDef').editingVersionId)) return;
|
||||
if (isEqual(this.state.appDefinition, this.props.ymap.get('appDef').newDefinition)) return;
|
||||
if (!config.ENABLE_MULTIPLAYER_EDITING) return null;
|
||||
|
||||
this.realtimeSave(this.props.ymap.get('appDef').newDefinition, { skipAutoSave: true, skipYmapUpdate: true });
|
||||
this.props.ymap?.observe(() => {
|
||||
if (!isEqual(this.state.editingVersion?.id, this.props.ymap?.get('appDef').editingVersionId)) return;
|
||||
if (isEqual(this.state.appDefinition, this.props.ymap?.get('appDef').newDefinition)) return;
|
||||
|
||||
this.realtimeSave(this.props.ymap?.get('appDef').newDefinition, { skipAutoSave: true, skipYmapUpdate: true });
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -283,6 +286,7 @@ class Editor extends React.Component {
|
|||
data.data_queries.forEach((query) => {
|
||||
queryState[query.name] = {
|
||||
...DataSourceTypes.find((source) => source.kind === query.kind).exposedVariables,
|
||||
kind: DataSourceTypes.find((source) => source.kind === query.kind).kind,
|
||||
...this.state.currentState.queries[query.name],
|
||||
};
|
||||
});
|
||||
|
|
@ -469,9 +473,17 @@ class Editor extends React.Component {
|
|||
this.canRedo = true;
|
||||
|
||||
if (!appDefinition) return;
|
||||
this.setState({
|
||||
appDefinition,
|
||||
});
|
||||
this.setState(
|
||||
{
|
||||
appDefinition,
|
||||
},
|
||||
() => {
|
||||
this.props.ymap?.set('appDef', {
|
||||
newDefinition: appDefinition,
|
||||
editingVersionId: this.state.editingVersion?.id,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -486,16 +498,24 @@ class Editor extends React.Component {
|
|||
this.canRedo = this.currentVersionChanges.hasOwnProperty(this.currentVersion + 1);
|
||||
|
||||
if (!appDefinition) return;
|
||||
this.setState({
|
||||
appDefinition,
|
||||
});
|
||||
this.setState(
|
||||
{
|
||||
appDefinition,
|
||||
},
|
||||
() => {
|
||||
this.props.ymap?.set('appDef', {
|
||||
newDefinition: appDefinition,
|
||||
editingVersionId: this.state.editingVersion?.id,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
appDefinitionChanged = (newDefinition, opts = {}) => {
|
||||
if (isEqual(this.state.appDefinition, newDefinition)) return;
|
||||
if (!opts.skipYmapUpdate) {
|
||||
this.props.ymap.set('appDef', { newDefinition, editingVersionId: this.state.editingVersion?.id });
|
||||
if (config.ENABLE_MULTIPLAYER_EDITING && !opts.skipYmapUpdate) {
|
||||
this.props.ymap?.set('appDef', { newDefinition, editingVersionId: this.state.editingVersion?.id });
|
||||
}
|
||||
|
||||
produce(
|
||||
|
|
@ -576,7 +596,7 @@ class Editor extends React.Component {
|
|||
setStateAsync(_self, newDefinition).then(() => {
|
||||
computeComponentState(_self, _self.state.appDefinition.components);
|
||||
this.autoSave();
|
||||
this.props.ymap.set('appDef', {
|
||||
this.props.ymap?.set('appDef', {
|
||||
newDefinition: newDefinition.appDefinition,
|
||||
editingVersionId: this.state.editingVersion?.id,
|
||||
});
|
||||
|
|
@ -600,7 +620,7 @@ class Editor extends React.Component {
|
|||
appDefinition,
|
||||
},
|
||||
() => {
|
||||
this.props.ymap.set('appDef', {
|
||||
this.props.ymap?.set('appDef', {
|
||||
newDefinition: appDefinition,
|
||||
editingVersionId: this.state.editingVersion?.id,
|
||||
});
|
||||
|
|
@ -1014,11 +1034,13 @@ class Editor extends React.Component {
|
|||
</span>
|
||||
</div>
|
||||
)}
|
||||
<RealtimeAvatars
|
||||
updatePresence={this.props.updatePresence}
|
||||
editingVersionId={this.state?.editingVersion?.id}
|
||||
self={this.props.self}
|
||||
/>
|
||||
{config.ENABLE_MULTIPLAYER_EDITING && (
|
||||
<RealtimeAvatars
|
||||
updatePresence={this.props.updatePresence}
|
||||
editingVersionId={this.state?.editingVersion?.id}
|
||||
self={this.props.self}
|
||||
/>
|
||||
)}
|
||||
{editingVersion && (
|
||||
<AppVersionsManager
|
||||
appId={appId}
|
||||
|
|
@ -1029,8 +1051,6 @@ class Editor extends React.Component {
|
|||
closeCreateVersionModalPrompt={this.closeCreateVersionModalPrompt}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="layout-buttons cursor-pointer">{this.renderLayoutIcon(currentLayout === 'desktop')}</div>
|
||||
<div className="navbar-nav flex-row order-md-last release-buttons">
|
||||
<div className="nav-item dropdown d-none d-md-flex me-2">
|
||||
<a
|
||||
|
|
@ -1086,6 +1106,14 @@ class Editor extends React.Component {
|
|||
globalSettingsChanged={this.globalSettingsChanged}
|
||||
globalSettings={appDefinition.globalSettings}
|
||||
currentState={currentState}
|
||||
appDefinition={{
|
||||
components: appDefinition.components,
|
||||
queries: dataQueries,
|
||||
selectedComponent: this.state?.selectedComponent,
|
||||
}}
|
||||
setSelectedComponent={this.setSelectedComponent}
|
||||
removeComponent={this.removeComponent}
|
||||
runQuery={(queryId, queryName) => runQuery(this, queryId, queryName)}
|
||||
toggleAppMaintenance={this.toggleAppMaintenance}
|
||||
is_maintenance_on={this.state.app.is_maintenance_on}
|
||||
/>
|
||||
|
|
@ -1107,7 +1135,7 @@ class Editor extends React.Component {
|
|||
backgroundColor: this.state.appDefinition.globalSettings.canvasBackgroundColor,
|
||||
}}
|
||||
>
|
||||
{this.props.othersOnSameVersion.map(({ id, presence }) => {
|
||||
{this.props?.othersOnSameVersion?.map(({ id, presence }) => {
|
||||
if (!presence) return null;
|
||||
return (
|
||||
<Cursor key={id} name={presence.firstName} color={presence.color} x={presence.x} y={presence.y} />
|
||||
|
|
@ -1317,10 +1345,56 @@ class Editor extends React.Component {
|
|||
</div>
|
||||
</div>
|
||||
<div className="editor-sidebar">
|
||||
<div className="col-md-12">
|
||||
<div></div>
|
||||
<div className="editor-actions col-md-12">
|
||||
<div className="m-auto undo-redo-buttons">
|
||||
<svg
|
||||
onClick={this.handleUndo}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={cx('cursor-pointer icon icon-tabler icon-tabler-arrow-back-up', {
|
||||
disabled: !this.canUndo,
|
||||
})}
|
||||
width="44"
|
||||
height="44"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="1.5"
|
||||
stroke={this.props.darkMode ? '#fff' : '#2c3e50'}
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none">
|
||||
<title>undo</title>
|
||||
</path>
|
||||
<path d="M9 13l-4 -4l4 -4m-4 4h11a4 4 0 0 1 0 8h-1" fill="none">
|
||||
<title>undo</title>
|
||||
</path>
|
||||
</svg>
|
||||
<svg
|
||||
title="redo"
|
||||
onClick={this.handleRedo}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={cx('cursor-pointer icon icon-tabler icon-tabler-arrow-forward-up', {
|
||||
disabled: !this.canRedo,
|
||||
})}
|
||||
width="44"
|
||||
height="44"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="1.5"
|
||||
stroke={this.props.darkMode ? '#fff' : '#2c3e50'}
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none">
|
||||
<title>redo</title>
|
||||
</path>
|
||||
<path d="M15 13l4 -4l-4 -4m4 4h-11a4 4 0 0 0 0 8h1" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="layout-buttons cursor-pointer">
|
||||
{this.renderLayoutIcon(currentLayout === 'desktop')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{currentSidebarTab === 1 && (
|
||||
<div className="pages-container">
|
||||
{selectedComponent &&
|
||||
|
|
|
|||
|
|
@ -5,15 +5,12 @@
|
|||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect x="1" y="1" width="36" height="26" rx="1" fill='#EEF3F9' />
|
||||
<rect x="37" y="1" width="36" height="26" rx="1" fill='#FFFFFC' />
|
||||
<g clipPath="url(#clip0_392_5486)">
|
||||
<path
|
||||
d="M58.375 8H53.125C52.5039 8 52 8.50391 52 9.125V18.875C52 19.4961 52.5039 20 53.125 20H58.375C58.9961 20 59.5 19.4961 59.5 18.875V9.125C59.5 8.50391 58.9961 8 58.375 8ZM55.75 19.25C55.3352 19.25 55 18.9148 55 18.5C55 18.0852 55.3352 17.75 55.75 17.75C56.1648 17.75 56.5 18.0852 56.5 18.5C56.5 18.9148 56.1648 19.25 55.75 19.25ZM58.375 16.7188C58.375 16.8734 58.2484 17 58.0938 17H53.4062C53.2516 17 53.125 16.8734 53.125 16.7188V9.40625C53.125 9.25156 53.2516 9.125 53.4062 9.125H58.0938C58.2484 9.125 58.375 9.25156 58.375 9.40625V16.7188Z"
|
||||
fill='#8092AC'
|
||||
/>
|
||||
</g>
|
||||
<rect x="0.5" y="0.5" width="73" height="27" rx="3.5" stroke="#D2DDEC" />
|
||||
<path d="M37 1L37 27" stroke="#D2DDEC" strokeLinecap="round" />
|
||||
<path
|
||||
d="M17.2 18.2H14.2C13.8817 18.2 13.5765 18.0736 13.3515 17.8485C13.1264 17.6235 13 17.3183 13 17V9.2C13 8.54 13.54 8 14.2 8H23.8C24.1183 8 24.4235 8.12643 24.6485 8.35147C24.8736 8.57652 25 8.88174 25 9.2V17C25 17.3183 24.8736 17.6235 24.6485 17.8485C24.4235 18.0736 24.1183 18.2 23.8 18.2H20.8L23.2 19.4V20H14.8V19.4L17.2 18.2ZM14.2 9.2V15.8H23.8V9.2H14.2Z"
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.3 KiB |
|
|
@ -5,15 +5,12 @@
|
|||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect x="1" y="1" width="36" height="26" rx="1" fill='#FFFFFC'/>
|
||||
<rect x="37" y="1" width="36" height="26" rx="1" fill='#EEF3F9' />
|
||||
<g clipPath="url(#clip0_392_5486)">
|
||||
<path
|
||||
d="M58.375 8H53.125C52.5039 8 52 8.50391 52 9.125V18.875C52 19.4961 52.5039 20 53.125 20H58.375C58.9961 20 59.5 19.4961 59.5 18.875V9.125C59.5 8.50391 58.9961 8 58.375 8ZM55.75 19.25C55.3352 19.25 55 18.9148 55 18.5C55 18.0852 55.3352 17.75 55.75 17.75C56.1648 17.75 56.5 18.0852 56.5 18.5C56.5 18.9148 56.1648 19.25 55.75 19.25ZM58.375 16.7188C58.375 16.8734 58.2484 17 58.0938 17H53.4062C53.2516 17 53.125 16.8734 53.125 16.7188V9.40625C53.125 9.25156 53.2516 9.125 53.4062 9.125H58.0938C58.2484 9.125 58.375 9.25156 58.375 9.40625V16.7188Z"
|
||||
fill='#4D72FA'
|
||||
/>
|
||||
</g>
|
||||
<rect x="0.5" y="0.5" width="73" height="27" rx="3.5" stroke="#D2DDEC" />
|
||||
<path d="M37 1L37 27" stroke="#D2DDEC" strokeLinecap="round" />
|
||||
<path
|
||||
d="M17.2 18.2H14.2C13.8817 18.2 13.5765 18.0736 13.3515 17.8485C13.1264 17.6235 13 17.3183 13 17V9.2C13 8.54 13.54 8 14.2 8H23.8C24.1183 8 24.4235 8.12643 24.6485 8.35147C24.8736 8.57652 25 8.88174 25 9.2V17C25 17.3183 24.8736 17.6235 24.6485 17.8485C24.4235 18.0736 24.1183 18.2 23.8 18.2H20.8L23.2 19.4V20H14.8V19.4L17.2 18.2ZM14.2 9.2V15.8H23.8V9.2H14.2Z"
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.3 KiB |
|
|
@ -1,12 +1,128 @@
|
|||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import usePinnedPopover from '@/_hooks/usePinnedPopover';
|
||||
import { LeftSidebarItem } from './SidebarItem';
|
||||
import { SidebarPinnedButton } from './SidebarPinnedButton';
|
||||
import ReactJson from 'react-json-view';
|
||||
import JSONTreeViewer from '@/_ui/JSONTreeViewer';
|
||||
import _ from 'lodash';
|
||||
import { allSvgs } from '@tooljet/plugins/client';
|
||||
import RunjsIcon from '../Icons/runjs.svg';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
export const LeftSidebarInspector = ({ darkMode, currentState }) => {
|
||||
export const LeftSidebarInspector = ({
|
||||
darkMode,
|
||||
currentState,
|
||||
appDefinition,
|
||||
setSelectedComponent,
|
||||
removeComponent,
|
||||
runQuery,
|
||||
}) => {
|
||||
const [open, trigger, content, popoverPinned, updatePopoverPinnedState] = usePinnedPopover(false);
|
||||
const jsonData = Object.entries(currentState).filter(([key]) => key !== 'errors');
|
||||
|
||||
const componentDefinitions = JSON.parse(JSON.stringify(appDefinition))['components'];
|
||||
const queryDefinitions = appDefinition['queries'];
|
||||
const selectedComponent = React.useMemo(() => {
|
||||
return {
|
||||
id: appDefinition['selectedComponent']?.id,
|
||||
component: appDefinition['selectedComponent']?.component?.name,
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [appDefinition['selectedComponent']]);
|
||||
|
||||
const queries = {};
|
||||
|
||||
if (!_.isEmpty(queryDefinitions)) {
|
||||
queryDefinitions.forEach((query) => {
|
||||
queries[query.name] = { id: query.id };
|
||||
});
|
||||
}
|
||||
|
||||
const memoizedJSONData = React.useMemo(() => {
|
||||
const data = _.merge(currentState, { queries });
|
||||
const jsontreeData = { ...data };
|
||||
delete jsontreeData.errors;
|
||||
return jsontreeData;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentState]);
|
||||
|
||||
const queryIcons = Object.entries(currentState['queries']).map(([key, value]) => {
|
||||
if (value.kind === 'runjs') {
|
||||
return { iconName: key, jsx: () => <RunjsIcon style={{ height: 16, width: 16, marginRight: 12 }} /> };
|
||||
}
|
||||
const Icon = allSvgs[value.kind];
|
||||
return { iconName: key, jsx: () => <Icon style={{ height: 16, width: 16, marginRight: 12 }} /> };
|
||||
});
|
||||
|
||||
const componentIcons = Object.entries(currentState['components']).map(([key, value]) => {
|
||||
const component = componentDefinitions[value.id]?.component ?? {};
|
||||
|
||||
if (!_.isEmpty(component) && component.name === key) {
|
||||
return {
|
||||
iconName: key,
|
||||
iconPath: `/assets/images/icons/widgets/${
|
||||
component.component.toLowerCase() === 'radiobutton' ? 'radio-button' : component.component.toLowerCase()
|
||||
}.svg`,
|
||||
className: 'component-icon',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const iconsList = useMemo(() => [...queryIcons, ...componentIcons], [queryIcons, componentIcons]);
|
||||
|
||||
const handleRemoveComponent = (component) => {
|
||||
removeComponent(component);
|
||||
};
|
||||
|
||||
const handleSelectComponentOnEditor = (component) => {
|
||||
setSelectedComponent(component.id, component);
|
||||
};
|
||||
|
||||
const handleRunQuery = (query, currentNode) => {
|
||||
runQuery(query.id, currentNode);
|
||||
};
|
||||
|
||||
const copyToClipboard = (data) => {
|
||||
const stringified = JSON.stringify(data, null, 2);
|
||||
navigator.clipboard.writeText(stringified);
|
||||
return toast.success('Copied to the clipboard', { position: 'top-center' });
|
||||
};
|
||||
|
||||
const updatePinnedParentState = () => {
|
||||
if (!popoverPinned) {
|
||||
updatePopoverPinnedState();
|
||||
}
|
||||
};
|
||||
|
||||
const callbackActions = [
|
||||
{
|
||||
for: 'queries',
|
||||
actions: [
|
||||
{
|
||||
name: 'Run Query',
|
||||
dispatchAction: handleRunQuery,
|
||||
icon: true,
|
||||
src: '/assets/images/icons/editor/play.svg',
|
||||
width: 8,
|
||||
height: 8,
|
||||
},
|
||||
],
|
||||
enableForAllChildren: false,
|
||||
enableFor1stLevelChildren: true,
|
||||
},
|
||||
{
|
||||
for: 'components',
|
||||
actions: [
|
||||
{ name: 'Select Widget', dispatchAction: handleSelectComponentOnEditor, icon: false, onSelect: true },
|
||||
{ name: 'Delete Widget', dispatchAction: handleRemoveComponent, icon: true, iconName: 'trash' },
|
||||
],
|
||||
enableForAllChildren: false,
|
||||
enableFor1stLevelChildren: true,
|
||||
},
|
||||
{
|
||||
for: 'all',
|
||||
actions: [{ name: 'Copy value', dispatchAction: copyToClipboard, icon: false }],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<LeftSidebarItem
|
||||
|
|
@ -19,7 +135,7 @@ export const LeftSidebarInspector = ({ darkMode, currentState }) => {
|
|||
<div
|
||||
{...content}
|
||||
className={`card popover ${open || popoverPinned ? 'show' : 'hide'}`}
|
||||
style={{ resize: 'horizontal', maxWidth: '50%' }}
|
||||
style={{ resize: 'horizontal', maxWidth: '60%', minWidth: '312px' }}
|
||||
>
|
||||
<SidebarPinnedButton
|
||||
darkMode={darkMode}
|
||||
|
|
@ -28,22 +144,22 @@ export const LeftSidebarInspector = ({ darkMode, currentState }) => {
|
|||
updateState={updatePopoverPinnedState}
|
||||
/>
|
||||
<div style={{ marginTop: '1rem' }} className="card-body">
|
||||
{jsonData.map((data) => (
|
||||
<ReactJson
|
||||
key={data[0]}
|
||||
src={data[1]}
|
||||
theme={darkMode ? 'shapeshifter' : 'rjv-default'}
|
||||
name={data[0]}
|
||||
style={{ fontSize: '0.7rem' }}
|
||||
enableClipboard={false}
|
||||
displayDataTypes={false}
|
||||
collapsed={true}
|
||||
displayObjectSize={false}
|
||||
quotesOnKeys={false}
|
||||
sortKeys={true}
|
||||
collapseStringsAfterLength={1000}
|
||||
/>
|
||||
))}
|
||||
<JSONTreeViewer
|
||||
data={memoizedJSONData}
|
||||
useIcons={true}
|
||||
iconsList={iconsList}
|
||||
useIndentedBlock={true}
|
||||
enableCopyToClipboard={true}
|
||||
useActions={true}
|
||||
actionsList={callbackActions}
|
||||
currentState={appDefinition}
|
||||
actionIdentifier="id"
|
||||
expandWithLabels={false}
|
||||
selectedComponent={selectedComponent}
|
||||
treeType="inspector"
|
||||
parentPopoverState={popoverPinned}
|
||||
updateParentState={updatePinnedParentState}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import Tooltip from 'react-bootstrap/Tooltip';
|
|||
|
||||
export const SidebarPinnedButton = ({ state, component, updateState, darkMode }) => {
|
||||
const tooltipMsg = state ? `Unpin ${component}` : `Pin ${component}`;
|
||||
const pinnedIcon = !state ? 'pinned' : 'pinnedoff';
|
||||
|
||||
return (
|
||||
<SidebarPinnedButton.OverlayContainer tip={tooltipMsg}>
|
||||
|
|
@ -13,7 +14,12 @@ export const SidebarPinnedButton = ({ state, component, updateState, darkMode })
|
|||
}`}
|
||||
onClick={updateState}
|
||||
>
|
||||
<img className="svg-icon" src={`/assets/images/icons/editor/left-sidebar/pinned.svg`} width="16" height="16" />
|
||||
<img
|
||||
className="svg-icon"
|
||||
src={`/assets/images/icons/editor/left-sidebar/${pinnedIcon}.svg`}
|
||||
width="16"
|
||||
height="16"
|
||||
/>
|
||||
</div>
|
||||
</SidebarPinnedButton.OverlayContainer>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@ export const LeftSidebar = ({
|
|||
globalSettingsChanged,
|
||||
globalSettings,
|
||||
currentState,
|
||||
appDefinition,
|
||||
setSelectedComponent,
|
||||
removeComponent,
|
||||
runQuery,
|
||||
toggleAppMaintenance,
|
||||
is_maintenance_on,
|
||||
}) => {
|
||||
|
|
@ -33,7 +37,14 @@ export const LeftSidebar = ({
|
|||
const [showLeaveDialog, setShowLeaveDialog] = useState(false);
|
||||
return (
|
||||
<div className="left-sidebar">
|
||||
<LeftSidebarInspector darkMode={darkMode} currentState={currentState} />
|
||||
<LeftSidebarInspector
|
||||
darkMode={darkMode}
|
||||
currentState={currentState}
|
||||
appDefinition={appDefinition}
|
||||
setSelectedComponent={setSelectedComponent}
|
||||
removeComponent={removeComponent}
|
||||
runQuery={runQuery}
|
||||
/>
|
||||
<LeftSidebarDataSources
|
||||
darkMode={darkMode}
|
||||
appId={appId}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
/* eslint-disable import/no-unresolved */
|
||||
import React from 'react';
|
||||
import config from 'config';
|
||||
import Avatar from '@/_ui/Avatar';
|
||||
import { useOthers } from 'y-presence';
|
||||
|
||||
|
|
|
|||
|
|
@ -121,11 +121,11 @@ export const WidgetManager = function WidgetManager({ componentTypes, zoomLevel,
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="components-container mx-3 mt-3">
|
||||
<div className="components-container mx-3">
|
||||
<div className="input-icon">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control mb-2"
|
||||
className="form-control mt-3 mb-2"
|
||||
placeholder="Search…"
|
||||
value={searchQuery}
|
||||
onChange={(e) => handleSearchQueryChange(e)}
|
||||
|
|
|
|||
|
|
@ -10,19 +10,45 @@ import { validateEmail } from '../_helpers/utils';
|
|||
class LoginPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
// redirect to home if already logged in
|
||||
if (authenticationService.currentUserValue) {
|
||||
this.props.history.push('/');
|
||||
}
|
||||
|
||||
this.state = {
|
||||
isLoading: false,
|
||||
showPassword: false,
|
||||
isGettingConfigs: true,
|
||||
configs: undefined,
|
||||
};
|
||||
this.single_organization = window.public_config?.MULTI_ORGANIZATION !== 'true';
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const organizationId = this.props.match.params.organisationId;
|
||||
if (
|
||||
(!organizationId && authenticationService.currentUserValue) ||
|
||||
(organizationId && authenticationService?.currentUserValue?.organization_id === organizationId)
|
||||
) {
|
||||
// redirect to home if already logged in
|
||||
return this.props.history.push('/');
|
||||
}
|
||||
if (organizationId || this.single_organization) {
|
||||
authenticationService.getOrganizationConfigs(organizationId).then(
|
||||
(configs) => {
|
||||
this.setState({ isGettingConfigs: false, configs });
|
||||
},
|
||||
() => this.props.history.push({ pathname: '/', state: { errorMessage: 'Error while login, please try again' } })
|
||||
);
|
||||
} else {
|
||||
// Not single organization login page and not an organization login page => Multi organization common login page
|
||||
// Only form login is allowed
|
||||
this.setState({
|
||||
isGettingConfigs: false,
|
||||
configs: {
|
||||
form: {
|
||||
enable_sign_up: window.public_config?.DISABLE_SIGNUPS !== 'true',
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.props.location?.state?.errorMessage &&
|
||||
toast.error(this.props.location.state.errorMessage, {
|
||||
id: 'toast-login-auth-error',
|
||||
|
|
@ -54,7 +80,9 @@ class LoginPage extends React.Component {
|
|||
return;
|
||||
}
|
||||
|
||||
authenticationService.login(email, password).then(this.authSuccessHandler, this.authFailureHandler);
|
||||
authenticationService
|
||||
.login(email, password, this.props.match.params.organisationId)
|
||||
.then(this.authSuccessHandler, this.authFailureHandler);
|
||||
};
|
||||
|
||||
authSuccessHandler = () => {
|
||||
|
|
@ -65,11 +93,7 @@ class LoginPage extends React.Component {
|
|||
this.setState({ isLoading: false });
|
||||
};
|
||||
|
||||
authFailureHandler = (error) => {
|
||||
if (error?.error === 'idpiframe_initialization_failed') {
|
||||
//Error thrown by google on load
|
||||
return this.setState({ isLoading: false });
|
||||
}
|
||||
authFailureHandler = () => {
|
||||
toast.error('Invalid email or password', {
|
||||
id: 'toast-login-auth-error',
|
||||
position: 'top-center',
|
||||
|
|
@ -77,9 +101,24 @@ class LoginPage extends React.Component {
|
|||
this.setState({ isLoading: false });
|
||||
};
|
||||
|
||||
showLoading = () => {
|
||||
return (
|
||||
<div className="card-body">
|
||||
<div className="skeleton-heading"></div>
|
||||
<div className="skeleton-line"></div>
|
||||
<div className="skeleton-line"></div>
|
||||
<div className="skeleton-line"></div>
|
||||
<div className="mb-2"></div>
|
||||
<div className="skeleton-heading"></div>
|
||||
<div className="skeleton-line"></div>
|
||||
<div className="skeleton-line"></div>
|
||||
<div className="skeleton-line"></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isLoading } = this.state;
|
||||
const passwordLoginDisabled = window.public_config?.DISABLE_PASSWORD_LOGIN === 'true';
|
||||
const { isLoading, configs, isGettingConfigs } = this.state;
|
||||
return (
|
||||
<div className="page page-center">
|
||||
<div className="container-tight py-2">
|
||||
|
|
@ -89,93 +128,108 @@ class LoginPage extends React.Component {
|
|||
</a>
|
||||
</div>
|
||||
<form className="card card-md" action="." method="get" autoComplete="off">
|
||||
<div className="card-body">
|
||||
{!passwordLoginDisabled && (
|
||||
<div>
|
||||
<h2 className="card-title text-center mb-4" data-cy="login-page-header">
|
||||
Login to your account
|
||||
</h2>
|
||||
<div className="mb-3">
|
||||
<label className="form-label" data-cy="email-label">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
onChange={this.handleChange}
|
||||
name="email"
|
||||
type="email"
|
||||
className="form-control"
|
||||
placeholder="Email"
|
||||
data-testid="emailField"
|
||||
data-cy="email-text-field"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<label className="form-label" data-cy="password-label">
|
||||
Password
|
||||
<span className="form-label-description">
|
||||
<Link to={'/forgot-password'} tabIndex="-1" data-cy="forgot-password-link">
|
||||
Forgot password
|
||||
</Link>
|
||||
</span>
|
||||
</label>
|
||||
<div className="input-group input-group-flat">
|
||||
{isGettingConfigs ? (
|
||||
this.showLoading()
|
||||
) : (
|
||||
<div className="card-body">
|
||||
{!configs && <div className="text-center">No login methods enabled for this organization</div>}
|
||||
{configs?.form?.enabled && (
|
||||
<div>
|
||||
<h2 className="card-title text-center mb-4" data-cy="login-page-header">
|
||||
Login to {this.single_organization ? 'your account' : configs?.name || 'your account'}
|
||||
</h2>
|
||||
<div className="mb-3">
|
||||
<label className="form-label" data-cy="email-label">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
onChange={this.handleChange}
|
||||
name="password"
|
||||
type={this.state.showPassword ? 'text' : 'password'}
|
||||
name="email"
|
||||
type="email"
|
||||
className="form-control"
|
||||
placeholder="Password"
|
||||
autoComplete="off"
|
||||
data-testid="passwordField"
|
||||
data-cy="password-text-field"
|
||||
placeholder="Email"
|
||||
data-testid="emailField"
|
||||
data-cy="email-text-field"
|
||||
/>
|
||||
<span className="input-group-text"></span>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<label className="form-label" data-cy="password-label">
|
||||
Password
|
||||
<span className="form-label-description">
|
||||
<Link to={'/forgot-password'} tabIndex="-1" data-cy="forgot-password-link">
|
||||
Forgot password
|
||||
</Link>
|
||||
</span>
|
||||
</label>
|
||||
<div className="input-group input-group-flat">
|
||||
<input
|
||||
onChange={this.handleChange}
|
||||
name="password"
|
||||
type={this.state.showPassword ? 'text' : 'password'}
|
||||
className="form-control"
|
||||
placeholder="Password"
|
||||
autoComplete="off"
|
||||
data-testid="passwordField"
|
||||
data-cy="password-text-field"
|
||||
/>
|
||||
<span className="input-group-text"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
id="check-input"
|
||||
name="check-input"
|
||||
onChange={this.handleOnCheck}
|
||||
data-cy="checkbox-input"
|
||||
/>
|
||||
<label className="form-check-label" htmlFor="check-input" data-cy="show-password-label">
|
||||
show password
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
id="check-input"
|
||||
name="check-input"
|
||||
onChange={this.handleOnCheck}
|
||||
data-cy="checkbox-input"
|
||||
)}
|
||||
<div
|
||||
className={`form-footer d-flex flex-column align-items-center ${
|
||||
!configs?.form?.enabled ? 'mt-0' : ''
|
||||
}`}
|
||||
>
|
||||
{configs?.form?.enabled && (
|
||||
<button
|
||||
data-testid="loginButton"
|
||||
className={`btn btn-primary w-100 ${isLoading ? 'btn-loading' : ''}`}
|
||||
onClick={this.authUser}
|
||||
data-cy="login-button"
|
||||
>
|
||||
Sign in
|
||||
</button>
|
||||
)}
|
||||
{this.state.configs?.google?.enabled && (
|
||||
<GoogleSSOLoginButton
|
||||
configs={this.state.configs?.google?.configs}
|
||||
configId={this.state.configs?.google?.config_id}
|
||||
/>
|
||||
<label className="form-check-label" htmlFor="check-input" data-cy="show-password-label">
|
||||
show password
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
{this.state.configs?.git?.enabled && <GitSSOLoginButton configs={this.state.configs?.git?.configs} />}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`form-footer d-flex flex-column align-items-center ${passwordLoginDisabled ? 'mt-0' : ''}`}
|
||||
>
|
||||
{!passwordLoginDisabled && (
|
||||
<button
|
||||
data-testid="loginButton"
|
||||
className={`btn btn-primary w-100 ${isLoading ? 'btn-loading' : ''}`}
|
||||
onClick={this.authUser}
|
||||
data-cy="login-button"
|
||||
>
|
||||
Sign in
|
||||
</button>
|
||||
)}
|
||||
{window.public_config?.SSO_GOOGLE_OAUTH2_CLIENT_ID && (
|
||||
<GoogleSSOLoginButton
|
||||
authSuccessHandler={this.authSuccessHandler}
|
||||
authFailureHandler={this.authFailureHandler}
|
||||
/>
|
||||
)}
|
||||
{window.public_config?.SSO_GIT_OAUTH2_CLIENT_ID && <GitSSOLoginButton />}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
{!passwordLoginDisabled && (
|
||||
<div className="text-center text-secondary mt-3" data-cy="sign-up-message">
|
||||
Don't have account yet?
|
||||
<Link to={'/signup'} tabIndex="-1" data-cy="sign-up-link">
|
||||
Sign up
|
||||
</Link>
|
||||
{!this.props.match.params.organisationId &&
|
||||
!this.single_organization &&
|
||||
configs?.form?.enabled &&
|
||||
configs?.form?.enable_sign_up && (
|
||||
<div className="text-center text-secondary mt-3" data-cy="sign-up-message">
|
||||
Don't have account yet?
|
||||
<Link to={'/signup'} tabIndex="-1" data-cy="sign-up-link">
|
||||
Sign up
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{authenticationService?.currentUserValue?.organization && (
|
||||
<div className="text-center mt-3">
|
||||
back to <a href="/">{authenticationService?.currentUserValue?.organization}</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ class ManageOrgUsers extends React.Component {
|
|||
history.push('/login');
|
||||
};
|
||||
|
||||
generateInvitationURL = (user) => window.location.origin + '/invitations/' + user.invitation_token;
|
||||
generateInvitationURL = (user) => window.location.origin + '/organization-invitations/' + user.invitation_token;
|
||||
|
||||
invitationLinkCopyHandler = () => {
|
||||
toast.success('Invitation URL copied', {
|
||||
|
|
|
|||
43
frontend/src/ManageSSO/Form.jsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import React, { useState } from 'react';
|
||||
import { organizationService } from '@/_services';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
export function Form({ settings, updateData }) {
|
||||
const [enabled, setEnabled] = useState(settings?.enabled || false);
|
||||
|
||||
const changeStatus = () => {
|
||||
organizationService.editOrganizationConfigs({ type: 'form', enabled: !enabled }).then(
|
||||
(data) => {
|
||||
const enabled_tmp = !enabled;
|
||||
setEnabled(enabled_tmp);
|
||||
updateData('form', { id: data.id, enabled: enabled_tmp });
|
||||
toast.success(`${enabled_tmp ? 'Enabled' : 'Disabled'} Form login`, {
|
||||
position: 'top-center',
|
||||
});
|
||||
},
|
||||
() => {
|
||||
toast.error('Error saving sso configurations', {
|
||||
position: 'top-center',
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<div className="d-flex justify-content-between title-with-toggle">
|
||||
<div className="card-title">
|
||||
Password Login
|
||||
<span className={`badge bg-${enabled ? 'green' : 'grey'} ms-1`}>{enabled ? 'Enabled' : 'Disabled'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<label className="form-check form-switch">
|
||||
<input className="form-check-input" type="checkbox" checked={enabled} onChange={changeStatus} />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
85
frontend/src/ManageSSO/GeneralSettings.jsx
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import React, { useState } from 'react';
|
||||
import { organizationService } from '@/_services';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
export function GeneralSettings({ settings, updateData }) {
|
||||
const [enableSignUp, setEnableSignUp] = useState(settings?.enable_sign_up || false);
|
||||
const [domain, setDomain] = useState(settings?.domain || '');
|
||||
const [isSaving, setSaving] = useState(false);
|
||||
|
||||
const reset = () => {
|
||||
setEnableSignUp(settings?.enable_sign_up || false);
|
||||
setDomain(settings?.domain || '');
|
||||
};
|
||||
|
||||
const saveSettings = () => {
|
||||
setSaving(true);
|
||||
organizationService.editOrganization({ enableSignUp, domain }).then(
|
||||
() => {
|
||||
setSaving(false);
|
||||
updateData('general', { enable_sign_up: enableSignUp, domain });
|
||||
toast.success('updated sso configurations', {
|
||||
position: 'top-center',
|
||||
});
|
||||
},
|
||||
() => {
|
||||
setSaving(false);
|
||||
toast.error('Error saving sso configurations', {
|
||||
position: 'top-center',
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<div className="card-title">General Settings</div>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<form noValidate>
|
||||
<div className="form-group mb-3">
|
||||
<label className="form-check form-switch">
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
onChange={() => setEnableSignUp((enableSignUp) => !enableSignUp)}
|
||||
checked={enableSignUp}
|
||||
/>
|
||||
<span className="form-check-label">Enable signup</span>
|
||||
</label>
|
||||
<div className="help-text">
|
||||
<div>New account will be created for user's first time sso sign in</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group mb-3">
|
||||
<label className="form-label">Allowed domain</label>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="Enter Domains"
|
||||
name="domain"
|
||||
value={domain}
|
||||
onChange={(e) => setDomain(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-footer">
|
||||
<button type="button" className="btn btn-light mr-2" onClick={reset}>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn mx-2 btn-primary ${isSaving ? 'btn-loading' : ''}`}
|
||||
disabled={isSaving}
|
||||
onClick={saveSettings}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
129
frontend/src/ManageSSO/Git.jsx
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import React, { useState } from 'react';
|
||||
import { organizationService } from '@/_services';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
export function Git({ settings, updateData }) {
|
||||
const [enabled, setEnabled] = useState(settings?.enabled || false);
|
||||
const [clientId, setClientId] = useState(settings?.configs?.client_id || '');
|
||||
const [clientSecret, setClientSecret] = useState(settings?.configs?.client_secret || '');
|
||||
const [isSaving, setSaving] = useState(false);
|
||||
const [configId, setConfigId] = useState(settings?.id);
|
||||
|
||||
const reset = () => {
|
||||
setClientId(settings?.configs?.client_id || '');
|
||||
setClientSecret(settings?.configs?.client_secret || '');
|
||||
};
|
||||
|
||||
const saveSettings = () => {
|
||||
setSaving(true);
|
||||
organizationService.editOrganizationConfigs({ type: 'git', configs: { clientId, clientSecret } }).then(
|
||||
(data) => {
|
||||
setSaving(false);
|
||||
data.id && setConfigId(data.id);
|
||||
updateData('git', { id: data.id, configs: { client_id: clientId, client_secret: clientSecret } });
|
||||
toast.success('updated SSO configurations', {
|
||||
position: 'top-center',
|
||||
});
|
||||
},
|
||||
() => {
|
||||
setSaving(false);
|
||||
toast.error('Error saving sso configurations', {
|
||||
position: 'top-center',
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const changeStatus = () => {
|
||||
setSaving(true);
|
||||
organizationService.editOrganizationConfigs({ type: 'git', enabled: !enabled }).then(
|
||||
(data) => {
|
||||
setSaving(false);
|
||||
const enabled_tmp = !enabled;
|
||||
setEnabled(enabled_tmp);
|
||||
data.id && setConfigId(data.id);
|
||||
updateData('git', { id: data.id, enabled: enabled_tmp });
|
||||
toast.success(`${enabled_tmp ? 'Enabled' : 'Disabled'} Git SSO`, {
|
||||
position: 'top-center',
|
||||
});
|
||||
},
|
||||
() => {
|
||||
setSaving(false);
|
||||
toast.error('Error saving sso configurations', {
|
||||
position: 'top-center',
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<div className="d-flex justify-content-between title-with-toggle">
|
||||
<div className="card-title">
|
||||
Git
|
||||
<span className={`badge bg-${enabled ? 'green' : 'grey'} ms-1`}>{enabled ? 'Enabled' : 'Disabled'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<label className="form-check form-switch">
|
||||
<input className="form-check-input" type="checkbox" checked={enabled} onChange={changeStatus} />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<form noValidate>
|
||||
<div className="form-group mb-3">
|
||||
<label className="form-label">Client Id</label>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="Enter Client Id"
|
||||
value={clientId}
|
||||
onChange={(e) => setClientId(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group mb-3">
|
||||
<label className="form-label">
|
||||
Client Secret
|
||||
<small className="text-green mx-2">
|
||||
<img className="mx-2 encrypted-icon" src="/assets/images/icons/padlock.svg" width="12" height="12" />
|
||||
Encrypted
|
||||
</small>
|
||||
</label>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="Enter Client Secret"
|
||||
value={clientSecret}
|
||||
onChange={(e) => setClientSecret(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{configId && (
|
||||
<div className="form-group mb-3">
|
||||
<label className="form-label">Redirect URL</label>
|
||||
<div>{`${window.location.protocol}//${window.location.host}/sso/git/${configId}`}</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="form-footer">
|
||||
<button type="button" className="btn btn-light mr-2" onClick={reset}>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn mx-2 btn-primary ${isSaving ? 'btn-loading' : ''}`}
|
||||
disabled={isSaving}
|
||||
onClick={saveSettings}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
109
frontend/src/ManageSSO/Google.jsx
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import React, { useState } from 'react';
|
||||
import { organizationService } from '@/_services';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
export function Google({ settings, updateData }) {
|
||||
const [enabled, setEnabled] = useState(settings?.enabled || false);
|
||||
const [clientId, setClientId] = useState(settings?.configs?.client_id || '');
|
||||
const [isSaving, setSaving] = useState(false);
|
||||
const [configId, setConfigId] = useState(settings?.id);
|
||||
|
||||
const reset = () => {
|
||||
setClientId(settings?.configs?.client_id || '');
|
||||
};
|
||||
|
||||
const saveSettings = () => {
|
||||
setSaving(true);
|
||||
organizationService.editOrganizationConfigs({ type: 'google', configs: { clientId } }).then(
|
||||
(data) => {
|
||||
setSaving(false);
|
||||
data.id && setConfigId(data.id);
|
||||
updateData('google', { id: data.id, configs: { client_id: clientId } });
|
||||
toast.success('updated SSO configurations', {
|
||||
position: 'top-center',
|
||||
});
|
||||
},
|
||||
() => {
|
||||
setSaving(false);
|
||||
toast.error('Error saving sso configurations', {
|
||||
position: 'top-center',
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const changeStatus = () => {
|
||||
setSaving(true);
|
||||
organizationService.editOrganizationConfigs({ type: 'google', enabled: !enabled }).then(
|
||||
(data) => {
|
||||
setSaving(false);
|
||||
const enabled_tmp = !enabled;
|
||||
setEnabled(enabled_tmp);
|
||||
data.id && setConfigId(data.id);
|
||||
updateData('google', { id: data.id, enabled: enabled_tmp });
|
||||
toast.success(`${enabled_tmp ? 'Enabled' : 'Disabled'} Google SSO`, {
|
||||
position: 'top-center',
|
||||
});
|
||||
},
|
||||
() => {
|
||||
setSaving(false);
|
||||
toast.error('Error saving sso configurations', {
|
||||
position: 'top-center',
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<div className="d-flex justify-content-between title-with-toggle">
|
||||
<div className="card-title">
|
||||
Google
|
||||
<span className={`badge bg-${enabled ? 'green' : 'grey'} ms-1`}>{enabled ? 'Enabled' : 'Disabled'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<label className="form-check form-switch">
|
||||
<input className="form-check-input" type="checkbox" checked={enabled} onChange={changeStatus} />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<form noValidate>
|
||||
<div className="form-group mb-3">
|
||||
<label className="form-label">Client Id</label>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="Enter Client Secret"
|
||||
value={clientId}
|
||||
onChange={(e) => setClientId(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{configId && (
|
||||
<div className="form-group mb-3">
|
||||
<label className="form-label">Redirect URL</label>
|
||||
<div>{`${window.location.protocol}//${window.location.host}/sso/google/${configId}`}</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="form-footer">
|
||||
<button type="button" className="btn btn-light mr-2" onClick={reset}>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn mx-2 btn-primary ${isSaving ? 'btn-loading' : ''}`}
|
||||
disabled={isSaving}
|
||||
onClick={saveSettings}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
19
frontend/src/ManageSSO/Loader.jsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
|
||||
export function Loader() {
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<div className="card-title">
|
||||
<div className="skeleton-avatar"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="skeleton-line"></div>
|
||||
<div className="skeleton-line"></div>
|
||||
<div className="skeleton-line"></div>
|
||||
<div className="skeleton-line"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
127
frontend/src/ManageSSO/ManageSSO.jsx
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { organizationService } from '@/_services';
|
||||
import { Header, Menu } from '@/_components';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import { GeneralSettings } from './GeneralSettings';
|
||||
import { Google } from './Google';
|
||||
import { Loader } from './Loader';
|
||||
import { Git } from './Git';
|
||||
import { Form } from './Form';
|
||||
|
||||
export function ManageSSO({ switchDarkMode, darkMode }) {
|
||||
const menuItems = [
|
||||
{ id: 'general-settings', label: 'General Settings' },
|
||||
{ id: 'google', label: 'Google' },
|
||||
{ id: 'git', label: 'Git' },
|
||||
{ id: 'form', label: 'Password Login' },
|
||||
];
|
||||
const changePage = useCallback(
|
||||
(page) => {
|
||||
setCurrentPage(page);
|
||||
},
|
||||
[setCurrentPage]
|
||||
);
|
||||
const [currentPage, setCurrentPage] = useState('');
|
||||
const [isLoading, setIsloading] = useState(true);
|
||||
const [ssoData, setSsoData] = useState({});
|
||||
|
||||
const showPage = () => {
|
||||
switch (currentPage) {
|
||||
case 'general-settings':
|
||||
return <GeneralSettings updateData={updateData} settings={ssoData} />;
|
||||
case 'google':
|
||||
return <Google updateData={updateData} settings={ssoData?.sso_configs?.find((obj) => obj.sso === 'google')} />;
|
||||
case 'git':
|
||||
return <Git updateData={updateData} settings={ssoData?.sso_configs?.find((obj) => obj.sso === 'git')} />;
|
||||
case 'form':
|
||||
return <Form updateData={updateData} settings={ssoData?.sso_configs?.find((obj) => obj.sso === 'form')} />;
|
||||
default:
|
||||
return <Loader />;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
organizationService.getSSODetails().then((data) => {
|
||||
setSsoData(data.organization_details);
|
||||
setIsloading(false);
|
||||
setCurrentPage('general-settings');
|
||||
});
|
||||
}, []);
|
||||
|
||||
const updateData = useCallback(
|
||||
(type, data) => {
|
||||
const ssoData_tmp = ssoData;
|
||||
let configs = ssoData_tmp.sso_configs.find((obj) => obj.sso === type);
|
||||
|
||||
switch (type) {
|
||||
case 'general':
|
||||
return setSsoData({ ...ssoData, ...data });
|
||||
default:
|
||||
if (!configs) {
|
||||
// Enable/Disable
|
||||
ssoData_tmp.sso_configs.push({ ...data, sso: type });
|
||||
} else {
|
||||
// Change configs
|
||||
if (data.id !== undefined) {
|
||||
configs.id = data.id;
|
||||
}
|
||||
if (data.enabled !== undefined) {
|
||||
configs.enabled = data.enabled;
|
||||
}
|
||||
if (data.configs !== undefined) {
|
||||
configs.configs = data.configs;
|
||||
}
|
||||
}
|
||||
return setSsoData(ssoData_tmp);
|
||||
}
|
||||
},
|
||||
[ssoData]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="wrapper manage-sso">
|
||||
<Header switchDarkMode={switchDarkMode} darkMode={darkMode} />
|
||||
<ReactTooltip type="dark" effect="solid" delayShow={250} />
|
||||
|
||||
<div className="page-wrapper">
|
||||
<div className="container-xl">
|
||||
<div className="page-header d-print-none">
|
||||
<div className="row align-items-center">
|
||||
<div className="col">
|
||||
<div className="page-pretitle"></div>
|
||||
<h2 className="page-title">Manage SSO</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="page-body">
|
||||
<div className="container-xl">
|
||||
<div className="row">
|
||||
<div className="col-3">
|
||||
<div>
|
||||
{isLoading ? (
|
||||
<div className="row">
|
||||
<div className="row">
|
||||
<div className="skeleton-line"></div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="skeleton-line"></div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="skeleton-line"></div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Menu items={menuItems} onChange={changePage} selected={currentPage} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-9">{showPage()}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
1
frontend/src/ManageSSO/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './ManageSSO';
|
||||
|
|
@ -10,6 +10,7 @@ export function Authorize() {
|
|||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
authenticationService.clearUser();
|
||||
const errorMessage = router.query.error_description || router.query.error;
|
||||
|
||||
if (errorMessage) {
|
||||
|
|
@ -21,15 +22,26 @@ export function Authorize() {
|
|||
}
|
||||
|
||||
const configs = Configs[router.query.origin];
|
||||
let authParams = {};
|
||||
|
||||
if (configs.responseType === 'hash') {
|
||||
if (!window.location.hash) {
|
||||
return setError('Login failed');
|
||||
}
|
||||
const params = new Proxy(new URLSearchParams(window.location.hash.substr(1)), {
|
||||
get: (searchParams, prop) => searchParams.get(prop),
|
||||
});
|
||||
authParams.token = params[configs.params.token];
|
||||
authParams.state = params[configs.params.state];
|
||||
} else {
|
||||
authParams.token = router.query[configs.params.token];
|
||||
authParams.state = router.query[configs.params.state];
|
||||
}
|
||||
|
||||
authenticationService
|
||||
.signInViaOAuth({
|
||||
token: router.query[configs.params.token],
|
||||
origin: router.query.origin,
|
||||
state: router.query[configs.params.state],
|
||||
})
|
||||
.signInViaOAuth(router.query.configId, authParams)
|
||||
.then(() => setSuccess(true))
|
||||
.catch(() => setError(`${configs.name} login failed`));
|
||||
.catch((err) => setError(`${configs.name} login failed - ${err?.error ? err?.error : ''}`));
|
||||
// Disabled for useEffect not being called for updation
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
|
@ -77,7 +89,7 @@ export function Authorize() {
|
|||
<Redirect
|
||||
to={{
|
||||
pathname: '/login',
|
||||
state: { errorMessage: success ? '' : error },
|
||||
state: { errorMessage: error && error },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,16 @@
|
|||
{
|
||||
"git": {
|
||||
"name": "Github",
|
||||
"responseType": "query",
|
||||
"params": {
|
||||
"token": "code"
|
||||
}
|
||||
},
|
||||
"google": {
|
||||
"name": "Google",
|
||||
"responseType": "hash",
|
||||
"params": {
|
||||
"token": "id_token"
|
||||
}
|
||||
}
|
||||
}
|
||||
39
frontend/src/_components/CopyToClipboard/CopyToClipboard.jsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import React from 'react';
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { ToolTip } from '@/_components/ToolTip';
|
||||
|
||||
export const CopyToClipboardComponent = ({ data, callback }) => {
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
const dataToCopy = callback(data);
|
||||
const message = 'Path copied to clipboard';
|
||||
const tip = 'Copy path to clipboard';
|
||||
|
||||
//clears the clipboard after 2 seconds
|
||||
React.useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 2000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [copied]);
|
||||
|
||||
if (copied) {
|
||||
return <center>Copied</center>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ToolTip message={tip}>
|
||||
<CopyToClipboard
|
||||
text={dataToCopy}
|
||||
onCopy={() => {
|
||||
setCopied(true);
|
||||
toast.success(message, { position: 'top-center' });
|
||||
}}
|
||||
>
|
||||
<span style={{ height: '13px', width: '13px', marginBottom: '2px' }} className="mx-1 copy-to-clipboard">
|
||||
<img src={`/assets/images/icons/copy.svg`} width="12" height="12" />
|
||||
</span>
|
||||
</CopyToClipboard>
|
||||
</ToolTip>
|
||||
);
|
||||
};
|
||||
3
frontend/src/_components/CopyToClipboard/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { CopyToClipboardComponent } from './CopyToClipboard';
|
||||
|
||||
export default CopyToClipboardComponent;
|
||||
|
|
@ -5,6 +5,7 @@ import { history } from '@/_helpers';
|
|||
import { DarkModeToggle } from './DarkModeToggle';
|
||||
|
||||
import LogoIcon from '../Editor/Icons/logo.svg';
|
||||
import { Organization } from './Organization';
|
||||
|
||||
export const Header = function Header({ switchDarkMode, darkMode }) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
|
|
@ -42,6 +43,9 @@ export const Header = function Header({ switchDarkMode, darkMode }) {
|
|||
<div className="p-1 m-1 d-flex align-items-center">
|
||||
<DarkModeToggle switchDarkMode={switchDarkMode} darkMode={darkMode} />
|
||||
</div>
|
||||
<div>
|
||||
<Organization admin={admin} />
|
||||
</div>
|
||||
<div className="nav-item dropdown ms-2 user-avatar-nav-item">
|
||||
<a
|
||||
href="#"
|
||||
|
|
@ -58,16 +62,6 @@ export const Header = function Header({ switchDarkMode, darkMode }) {
|
|||
</div>
|
||||
</a>
|
||||
<div className="dropdown-menu dropdown-menu-end dropdown-menu-arrow end-0">
|
||||
{admin && (
|
||||
<Link data-testid="settingsBtn" to="/users" className="dropdown-item">
|
||||
Manage Users
|
||||
</Link>
|
||||
)}
|
||||
{admin && (
|
||||
<Link data-tesid="settingsBtn" to="/groups" className="dropdown-item">
|
||||
Manage Groups
|
||||
</Link>
|
||||
)}
|
||||
<Link data-testid="settingsBtn" to="#" onClick={openSettings} className="dropdown-item">
|
||||
Profile
|
||||
</Link>
|
||||
|
|
|
|||
16
frontend/src/_components/Menu.jsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
|
||||
export function Menu({ onChange, items, selected }) {
|
||||
return (
|
||||
<div className="left-menu">
|
||||
<ul>
|
||||
{items &&
|
||||
items.map((item) => (
|
||||
<li key={item.id} onClick={() => onChange(item.id)} className={selected === item.id ? 'active' : ''}>
|
||||
{item.label}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
349
frontend/src/_components/Organization.jsx
Normal file
|
|
@ -0,0 +1,349 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { authenticationService, organizationService } from '@/_services';
|
||||
import Modal from '../HomePage/Modal';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { SearchBox } from './SearchBox';
|
||||
|
||||
export const Organization = function Organization() {
|
||||
const isSingleOrganization = window.public_config?.MULTI_ORGANIZATION !== 'true';
|
||||
const { admin, organization_id } = authenticationService.currentUserValue;
|
||||
const [organization, setOrganization] = useState(authenticationService.currentUserValue?.organization);
|
||||
const [showCreateOrg, setShowCreateOrg] = useState(false);
|
||||
const [showEditOrg, setShowEditOrg] = useState(false);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [organizationList, setOrganizationList] = useState([]);
|
||||
const [getOrgStatus, setGetOrgStatus] = useState('loading');
|
||||
const [isListOrganizations, setIsListOrganizations] = useState(false);
|
||||
const [newOrgName, setNewOrgName] = useState('');
|
||||
|
||||
const getAvatar = (organization) => {
|
||||
if (!organization) return;
|
||||
|
||||
const orgName = organization.split(' ');
|
||||
if (orgName.length > 1) {
|
||||
return `${orgName[0]?.[0]}${orgName[1]?.[0]}`;
|
||||
} else {
|
||||
return `${organization[0]}${organization[1]}`;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
!isSingleOrganization && getOrganizations();
|
||||
}, [isSingleOrganization]);
|
||||
|
||||
const getOrganizations = () => {
|
||||
setGetOrgStatus('loading');
|
||||
organizationService.getOrganizations().then(
|
||||
(data) => {
|
||||
setOrganizationList(data.organizations);
|
||||
setGetOrgStatus('success');
|
||||
},
|
||||
() => {
|
||||
setGetOrgStatus('failure');
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const showEditModal = () => {
|
||||
setNewOrgName(organization);
|
||||
setShowEditOrg(true);
|
||||
};
|
||||
|
||||
const showCreateModal = () => {
|
||||
setNewOrgName('');
|
||||
setShowCreateOrg(true);
|
||||
};
|
||||
|
||||
const createOrganization = () => {
|
||||
if (!(newOrgName && newOrgName.trim())) {
|
||||
toast.error("organization name can't be empty.", {
|
||||
position: 'top-center',
|
||||
});
|
||||
return;
|
||||
}
|
||||
setIsCreating(true);
|
||||
organizationService.createOrganization(newOrgName).then(
|
||||
(data) => {
|
||||
authenticationService.updateCurrentUserDetails(data);
|
||||
window.location.href = '/';
|
||||
},
|
||||
() => {
|
||||
toast.error('Error while creating organization', {
|
||||
position: 'top-center',
|
||||
});
|
||||
}
|
||||
);
|
||||
setIsCreating(false);
|
||||
};
|
||||
|
||||
const editOrganization = () => {
|
||||
if (!(newOrgName && newOrgName.trim())) {
|
||||
toast.error("organization name can't be empty.", {
|
||||
position: 'top-center',
|
||||
});
|
||||
return;
|
||||
}
|
||||
setIsCreating(true);
|
||||
organizationService.editOrganization({ name: newOrgName }).then(
|
||||
() => {
|
||||
authenticationService.updateCurrentUserDetails({ organization: newOrgName });
|
||||
toast.success('Organization updated', {
|
||||
position: 'top-center',
|
||||
});
|
||||
setOrganization(newOrgName);
|
||||
},
|
||||
() => {
|
||||
toast.error('Error while editing organization', {
|
||||
position: 'top-center',
|
||||
});
|
||||
}
|
||||
);
|
||||
setIsCreating(false);
|
||||
setShowEditOrg(false);
|
||||
};
|
||||
|
||||
const switchOrganization = (orgId) => {
|
||||
organizationService.switchOrganization(orgId).then((response) => {
|
||||
response.text().then((text) => {
|
||||
if (!response.ok) {
|
||||
return (window.location.href = `/login/${orgId}`);
|
||||
}
|
||||
const data = text && JSON.parse(text);
|
||||
authenticationService.updateCurrentUserDetails(data);
|
||||
window.location.href = '/';
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const listOrganization = () => {
|
||||
return (
|
||||
organizationList &&
|
||||
organizationList
|
||||
.filter((org) => org.name.toLowerCase().includes(searchText ? searchText.toLowerCase() : ''))
|
||||
.map((org) => {
|
||||
return (
|
||||
<div
|
||||
key={org.id}
|
||||
onClick={organization_id === org.id ? undefined : () => switchOrganization(org.id)}
|
||||
className="dropdown-item org-list-item"
|
||||
>
|
||||
<div className="col-3">
|
||||
<span className="avatar bg-secondary-lt">{getAvatar(org.name)}</span>
|
||||
</div>
|
||||
<div className="col-8">
|
||||
<div className="org-name">{org.name}</div>
|
||||
</div>
|
||||
<div className="col-1">
|
||||
{organization_id === org.id && (
|
||||
<div className="tick-ico">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="icon icon-tabler icon-tabler-check"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M5 12l5 5l10 -10"></path>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const searchOrganizations = (text) => {
|
||||
setSearchText(text);
|
||||
};
|
||||
|
||||
const getListOrganizations = () => {
|
||||
return (
|
||||
<div className="organization-switchlist">
|
||||
<div className="dd-item-padding">
|
||||
<div className="d-flex">
|
||||
<div className="back-ico" onClick={() => setIsListOrganizations(false)}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="icon icon-tabler icon-tabler-chevron-left"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<polyline points="15 6 9 12 15 18"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="back-btn" onClick={() => setIsListOrganizations(false)}>
|
||||
Back
|
||||
</div>
|
||||
</div>
|
||||
<div className="search-box">
|
||||
<SearchBox onSubmit={searchOrganizations} debounceDelay={100} width="14rem" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="org-list">
|
||||
{getOrgStatus === 'success' ? (
|
||||
listOrganization()
|
||||
) : (
|
||||
<div className="text-center">
|
||||
<a
|
||||
onClick={getOrganizations}
|
||||
href="#"
|
||||
className={`btn btn-primary mb-2 ${getOrgStatus === 'loading' ? 'btn-loading' : ''}`}
|
||||
>
|
||||
Load Organizations
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getOrganizationMenu = () => {
|
||||
return (
|
||||
<div>
|
||||
<div className="dropdown-item org-avatar">
|
||||
<div className="row">
|
||||
<div className="col-3">
|
||||
<span className="avatar bg-secondary-lt">{getAvatar(organization)}</span>
|
||||
</div>
|
||||
<div className={`col-${isSingleOrganization ? '9' : '7'}`}>
|
||||
<div className="org-name" style={{ padding: `${admin ? '0px' : '0.6rem'} 0px` }}>
|
||||
{organization}
|
||||
</div>
|
||||
{admin && (
|
||||
<div className="org-edit">
|
||||
<span onClick={showEditModal}>Edit</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isSingleOrganization && (
|
||||
<div className="col-2">
|
||||
<div className="arrow-container" onClick={() => setIsListOrganizations(true)}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="icon icon-tabler icon-tabler-chevron-right"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<polyline points="9 6 15 12 9 18"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!isSingleOrganization && (
|
||||
<div className="dropdown-item org-actions">
|
||||
<div onClick={showCreateModal}>Add Organizations</div>
|
||||
</div>
|
||||
)}
|
||||
{admin && (
|
||||
<>
|
||||
<div className="dropdown-divider"></div>
|
||||
<Link data-testid="settingsBtn" to="/users" className="dropdown-item">
|
||||
Manage Users
|
||||
</Link>
|
||||
<Link data-tesid="settingsBtn" to="/groups" className="dropdown-item">
|
||||
Manage Groups
|
||||
</Link>
|
||||
<Link data-tesid="settingsBtn" to="/manage-sso" className="dropdown-item">
|
||||
Manage SSO
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="dropdown organization-list" onMouseEnter={() => setIsListOrganizations(false)}>
|
||||
<a href="#" className={`btn ${!isSingleOrganization || admin ? 'dropdown-toggle' : ''}`}>
|
||||
<div>{organization}</div>
|
||||
</a>
|
||||
{(!isSingleOrganization || admin) && (
|
||||
<div className="dropdown-menu end-0">
|
||||
{isListOrganizations ? getListOrganizations() : getOrganizationMenu()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Modal show={showCreateOrg} closeModal={() => setShowCreateOrg(false)} title="Create organization">
|
||||
<div className="row">
|
||||
<div className="col modal-main">
|
||||
<input
|
||||
type="text"
|
||||
onChange={(e) => setNewOrgName(e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="organization name"
|
||||
disabled={isCreating}
|
||||
maxLength={25}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col d-flex modal-footer-btn">
|
||||
<button className="btn btn-light" onClick={() => setShowCreateOrg(false)}>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
disabled={isCreating}
|
||||
className={`btn btn-primary ${isCreating ? 'btn-loading' : ''}`}
|
||||
onClick={createOrganization}
|
||||
>
|
||||
Create organization
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<Modal show={showEditOrg} closeModal={() => setShowEditOrg(false)} title="Edit organization">
|
||||
<div className="row">
|
||||
<div className="col modal-main">
|
||||
<input
|
||||
type="text"
|
||||
onChange={(e) => setNewOrgName(e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="organization name"
|
||||
disabled={isCreating}
|
||||
value={newOrgName}
|
||||
maxLength={25}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col d-flex modal-footer-btn">
|
||||
<button className="btn btn-light" onClick={() => setShowEditOrg(false)}>
|
||||
Cancel
|
||||
</button>
|
||||
<button className={`btn btn-primary ${isCreating ? 'btn-loading' : ''}`} onClick={editOrganization}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import useDebounce from '@/_hooks/useDebounce';
|
||||
|
||||
export function SearchBox({ onSubmit, debounceDelay = 300 }) {
|
||||
export function SearchBox({ width = '200px', onSubmit, debounceDelay = 300 }) {
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const debouncedSearchTerm = useDebounce(searchText, debounceDelay);
|
||||
const [isFocused, setFocussed] = useState(false);
|
||||
|
|
@ -16,7 +16,6 @@ export function SearchBox({ onSubmit, debounceDelay = 300 }) {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
console.log(debouncedSearchTerm);
|
||||
onSubmit(debouncedSearchTerm);
|
||||
}, [debouncedSearchTerm, onSubmit]);
|
||||
|
||||
|
|
@ -44,6 +43,7 @@ export function SearchBox({ onSubmit, debounceDelay = 300 }) {
|
|||
</span>
|
||||
)}
|
||||
<input
|
||||
style={{ width }}
|
||||
type="text"
|
||||
value={searchText}
|
||||
onChange={handleChange}
|
||||
|
|
@ -81,4 +81,5 @@ export function SearchBox({ onSubmit, debounceDelay = 300 }) {
|
|||
SearchBox.propTypes = {
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
debounceDelay: PropTypes.number,
|
||||
width: PropTypes.string,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,3 +6,4 @@ export * from './DarkModeToggle';
|
|||
export * from './SearchBox';
|
||||
export * from './ToolTip';
|
||||
export * from './ImageWithSpinner';
|
||||
export * from './Menu';
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ jest.mock(
|
|||
apiUrl: `http://localhost:3000/api`,
|
||||
SERVER_IP: process.env.SERVER_IP,
|
||||
COMMENT_FEATURE_ENABLE: true,
|
||||
ENABLE_MULTIPLAYER_EDITING: true,
|
||||
};
|
||||
},
|
||||
{ virtual: true }
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@ const currentUserSubject = new BehaviorSubject(JSON.parse(localStorage.getItem('
|
|||
|
||||
export const authenticationService = {
|
||||
login,
|
||||
getOrganizationConfigs,
|
||||
logout,
|
||||
clearUser,
|
||||
signup,
|
||||
updateCurrentUserDetails,
|
||||
currentUser: currentUserSubject.asObservable(),
|
||||
|
|
@ -17,29 +19,40 @@ export const authenticationService = {
|
|||
resetPassword,
|
||||
};
|
||||
|
||||
function login(email, password) {
|
||||
function login(email, password, organizationId) {
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
};
|
||||
|
||||
return fetch(`${config.apiUrl}/authenticate`, requestOptions)
|
||||
return fetch(`${config.apiUrl}/authenticate${organizationId ? `/${organizationId}` : ''}`, requestOptions)
|
||||
.then(handleResponse)
|
||||
.then((user) => {
|
||||
// store user details and jwt token in local storage to keep user logged in between page refreshes
|
||||
localStorage.setItem('currentUser', JSON.stringify(user));
|
||||
currentUserSubject.next(user);
|
||||
|
||||
updateUser(user);
|
||||
return user;
|
||||
});
|
||||
}
|
||||
|
||||
function getOrganizationConfigs(organizationId) {
|
||||
const requestOptions = {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
};
|
||||
|
||||
return fetch(
|
||||
`${config.apiUrl}/organizations/${organizationId ? `${organizationId}/` : ''}public-configs`,
|
||||
requestOptions
|
||||
)
|
||||
.then(handleResponse)
|
||||
.then((configs) => configs?.sso_configs);
|
||||
}
|
||||
|
||||
function updateCurrentUserDetails(details) {
|
||||
const currentUserDetails = JSON.parse(localStorage.getItem('currentUser'));
|
||||
const updatedUserDetails = Object.assign({}, currentUserDetails, details);
|
||||
localStorage.setItem('currentUser', JSON.stringify(updatedUserDetails));
|
||||
currentUserSubject.next(updatedUserDetails);
|
||||
updateUser(updatedUserDetails);
|
||||
}
|
||||
|
||||
function signup(email) {
|
||||
|
|
@ -70,25 +83,40 @@ function resetPassword(params) {
|
|||
}
|
||||
|
||||
function logout() {
|
||||
clearUser();
|
||||
history.push(`/login?redirectTo=${window.location.pathname?.startsWith('/sso/') ? '/' : window.location.pathname}`);
|
||||
}
|
||||
|
||||
function clearUser() {
|
||||
// remove user from local storage to log user out
|
||||
localStorage.removeItem('currentUser');
|
||||
currentUserSubject.next(null);
|
||||
history.push(`/login?redirectTo=${window.location.pathname}`);
|
||||
}
|
||||
|
||||
function signInViaOAuth(ssoResponse) {
|
||||
function signInViaOAuth(configId, ssoResponse) {
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(ssoResponse),
|
||||
};
|
||||
|
||||
return fetch(`${config.apiUrl}/oauth/sign-in`, requestOptions)
|
||||
.then(handleResponse)
|
||||
return fetch(`${config.apiUrl}/oauth/sign-in/${configId}`, requestOptions)
|
||||
.then((response) => {
|
||||
return response.text().then((text) => {
|
||||
const data = text && JSON.parse(text);
|
||||
if (!response.ok) {
|
||||
const error = (data && data.message) || response.statusText;
|
||||
return Promise.reject({ error, data });
|
||||
}
|
||||
return data;
|
||||
});
|
||||
})
|
||||
.then((user) => {
|
||||
localStorage.setItem('currentUser', JSON.stringify(user));
|
||||
currentUserSubject.next(user);
|
||||
|
||||
updateUser(user);
|
||||
return user;
|
||||
});
|
||||
}
|
||||
function updateUser(user) {
|
||||
localStorage.setItem('currentUser', JSON.stringify(user));
|
||||
currentUserSubject.next(user);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,15 +9,15 @@ function getThreads(appId, appVersionsId) {
|
|||
}
|
||||
|
||||
function createThread(data) {
|
||||
return adapter.post(`/threads/create`, data);
|
||||
return adapter.post(`/threads`, data);
|
||||
}
|
||||
|
||||
function updateThread(threadId, data) {
|
||||
return adapter.patch(`/threads/edit/${threadId}`, data);
|
||||
return adapter.patch(`/threads/${threadId}`, data);
|
||||
}
|
||||
|
||||
function deleteThread(threadId) {
|
||||
return adapter.delete(`/threads/delete/${threadId}`);
|
||||
return adapter.delete(`/threads/${threadId}`);
|
||||
}
|
||||
|
||||
function getComments(threadId, appVersionsId) {
|
||||
|
|
@ -25,15 +25,15 @@ function getComments(threadId, appVersionsId) {
|
|||
}
|
||||
|
||||
function createComment(data) {
|
||||
return adapter.post(`/comments/create`, data);
|
||||
return adapter.post(`/comments`, data);
|
||||
}
|
||||
|
||||
function updateComment(commentId, data) {
|
||||
return adapter.patch(`/comments/edit/${commentId}`, data);
|
||||
return adapter.patch(`/comments/${commentId}`, data);
|
||||
}
|
||||
|
||||
function deleteComment(commentId) {
|
||||
return adapter.delete(`/comments/delete/${commentId}`);
|
||||
return adapter.delete(`/comments/${commentId}`);
|
||||
}
|
||||
|
||||
function getNotifications(appId, isResolved, appVersionsId) {
|
||||
|
|
|
|||
|
|
@ -3,9 +3,45 @@ import { authHeader, handleResponse } from '@/_helpers';
|
|||
|
||||
export const organizationService = {
|
||||
getUsers,
|
||||
createOrganization,
|
||||
editOrganization,
|
||||
getOrganizations,
|
||||
switchOrganization,
|
||||
getSSODetails,
|
||||
editOrganizationConfigs,
|
||||
};
|
||||
|
||||
function getUsers() {
|
||||
const requestOptions = { method: 'GET', headers: authHeader() };
|
||||
return fetch(`${config.apiUrl}/organizations/users`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function createOrganization(name) {
|
||||
const requestOptions = { method: 'POST', headers: authHeader(), body: JSON.stringify({ name }) };
|
||||
return fetch(`${config.apiUrl}/organizations`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function editOrganization(params) {
|
||||
const requestOptions = { method: 'PATCH', headers: authHeader(), body: JSON.stringify(params) };
|
||||
return fetch(`${config.apiUrl}/organizations/`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function getOrganizations() {
|
||||
const requestOptions = { method: 'GET', headers: authHeader() };
|
||||
return fetch(`${config.apiUrl}/organizations`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function switchOrganization(organizationId) {
|
||||
const requestOptions = { method: 'GET', headers: authHeader() };
|
||||
return fetch(`${config.apiUrl}/switch/${organizationId}`, requestOptions);
|
||||
}
|
||||
|
||||
function getSSODetails() {
|
||||
const requestOptions = { method: 'GET', headers: authHeader() };
|
||||
return fetch(`${config.apiUrl}/organizations/configs`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function editOrganizationConfigs(params) {
|
||||
const requestOptions = { method: 'PATCH', headers: authHeader(), body: JSON.stringify(params) };
|
||||
return fetch(`${config.apiUrl}/organizations/configs`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export const userService = {
|
|||
setPasswordFromToken,
|
||||
updateCurrentUser,
|
||||
changePassword,
|
||||
acceptInvite,
|
||||
};
|
||||
|
||||
function getAll() {
|
||||
|
|
@ -32,13 +33,12 @@ function deleteUser(id) {
|
|||
return fetch(`${config.apiUrl}/users/${id}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function setPasswordFromToken({ token, password, organization, role, newSignup, firstName, lastName }) {
|
||||
function setPasswordFromToken({ token, password, organization, role, firstName, lastName }) {
|
||||
const body = {
|
||||
token,
|
||||
password,
|
||||
organization,
|
||||
role,
|
||||
new_signup: newSignup,
|
||||
first_name: firstName,
|
||||
last_name: lastName,
|
||||
};
|
||||
|
|
@ -47,6 +47,16 @@ function setPasswordFromToken({ token, password, organization, role, newSignup,
|
|||
return fetch(`${config.apiUrl}/users/set_password_from_token`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function acceptInvite({ token, password }) {
|
||||
const body = {
|
||||
token,
|
||||
password,
|
||||
};
|
||||
|
||||
const requestOptions = { method: 'POST', headers: authHeader(), body: JSON.stringify(body) };
|
||||
return fetch(`${config.apiUrl}/users/accept-invite`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function updateCurrentUser(firstName, lastName) {
|
||||
const body = { first_name: firstName, last_name: lastName };
|
||||
const requestOptions = { method: 'PATCH', headers: authHeader(), body: JSON.stringify(body) };
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ $dark-background: #1f2936;
|
|||
$bg-light: #EEF3F9;
|
||||
$bg-dark: #22272E;
|
||||
$bg-dark-light: #232e3c;
|
||||
$primary-light: #7A95FB;
|
||||
|
||||
.color-primary {
|
||||
color: $primary !important;
|
||||
|
|
|
|||
|
|
@ -731,7 +731,7 @@ button {
|
|||
|
||||
.page-body,
|
||||
.homepage-body {
|
||||
height: 100vh;
|
||||
height: 90.6vh;
|
||||
|
||||
.list-group.list-group-transparent.dark .all-apps-link,
|
||||
.list-group-item-action.dark.active {
|
||||
|
|
@ -1219,6 +1219,17 @@ button {
|
|||
filter: invert(89%) sepia(2%) saturate(127%) hue-rotate(175deg) brightness(99%) contrast(96%);
|
||||
}
|
||||
}
|
||||
.organization-list {
|
||||
margin-top: 5px;
|
||||
.btn {
|
||||
border: 0px;
|
||||
}
|
||||
.dropdown-toggle div {
|
||||
max-width: 200px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pagination {
|
||||
|
|
@ -3227,6 +3238,12 @@ input:focus-visible {
|
|||
.app-version-name.form-select {
|
||||
border-color: $border-grey-dark;
|
||||
}
|
||||
.organization-list {
|
||||
.btn {
|
||||
background-color: #273342;
|
||||
color: #656d77;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
|
|
@ -3914,7 +3931,7 @@ input[type="text"] {
|
|||
}
|
||||
.close-icon {
|
||||
position: fixed;
|
||||
top: 45px;
|
||||
top: 84px;
|
||||
right: 0;
|
||||
width: 60px;
|
||||
height: 22;
|
||||
|
|
@ -3973,9 +3990,9 @@ input[type="text"] {
|
|||
color: #36af8b;
|
||||
}
|
||||
|
||||
.layout-buttons {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
.undo-redo-buttons {
|
||||
flex: 1;
|
||||
padding-left: .5rem;
|
||||
}
|
||||
|
||||
.app-version-menu {
|
||||
|
|
@ -4716,11 +4733,351 @@ div#driver-page-overlay {
|
|||
#transformation-popover-container {
|
||||
margin-left: 80px !important;
|
||||
margin-bottom: -2px !important;
|
||||
// top: -10px !important;
|
||||
// left: 100px !important;
|
||||
// background-color: #0565ff;
|
||||
}
|
||||
|
||||
.organization-list {
|
||||
margin-top: 5px;
|
||||
.btn {
|
||||
border: 0px;
|
||||
}
|
||||
.dropdown-toggle div {
|
||||
max-width: 200px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
.org-name {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
font-weight: bold;
|
||||
}
|
||||
.org-actions div {
|
||||
color: #0565ff;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
.dropdown-menu {
|
||||
min-width: 14rem;
|
||||
}
|
||||
.org-avatar {
|
||||
display: block;
|
||||
}
|
||||
.org-avatar:hover {
|
||||
.avatar {
|
||||
background: #fcfcfc no-repeat center/cover;
|
||||
}
|
||||
.arrow-container {
|
||||
svg {
|
||||
filter: invert(48%) sepia(6%) saturate(6%) hue-rotate(315deg) brightness(103%) contrast(96%);
|
||||
}
|
||||
}
|
||||
}
|
||||
.arrow-container {
|
||||
padding: 5px 0px;
|
||||
}
|
||||
.arrow-container {
|
||||
svg {
|
||||
cursor: pointer;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
padding: 0px 0px;
|
||||
filter: invert(84%) sepia(13%) saturate(11%) hue-rotate(352deg) brightness(90%) contrast(91%);
|
||||
}
|
||||
}
|
||||
.org-edit {
|
||||
span {
|
||||
color: #0565ff;
|
||||
cursor: pointer;
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
.organization-switchlist {
|
||||
.back-btn {
|
||||
font-size: 12px;
|
||||
padding: 2px 0px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.back-ico {
|
||||
cursor: pointer;
|
||||
svg {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
filter: invert(84%) sepia(13%) saturate(11%) hue-rotate(352deg) brightness(90%) contrast(91%);
|
||||
}
|
||||
}
|
||||
.dd-item-padding {
|
||||
padding: 0.5rem 0.75rem 0rem 0.75rem;
|
||||
}
|
||||
.search-box {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.org-list {
|
||||
max-height: 60vh;
|
||||
overflow: auto;
|
||||
}
|
||||
.tick-ico {
|
||||
filter: invert(50%) sepia(13%) saturate(208%) hue-rotate(153deg) brightness(99%) contrast(86%);
|
||||
}
|
||||
.org-list-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
.org-list-item:hover {
|
||||
.avatar {
|
||||
background: #fcfcfc no-repeat center/cover;
|
||||
}
|
||||
.tick-ico {
|
||||
filter: invert(35%) sepia(17%) saturate(238%) hue-rotate(153deg) brightness(94%) contrast(89%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Left Menu
|
||||
.left-menu {
|
||||
background-color: #e8ebf4;
|
||||
padding: 1rem 0.5rem;
|
||||
border-radius: 5px;
|
||||
ul {
|
||||
overflow: auto;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
li {
|
||||
float: left;
|
||||
list-style: none;
|
||||
width: 100%;
|
||||
padding: 5px 10px;
|
||||
font-weight: bold;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
margin: 3px 0px;
|
||||
}
|
||||
li.active {
|
||||
background-color: $primary;
|
||||
color: $white;
|
||||
}
|
||||
li:not(.active):hover {
|
||||
background-color: #d2daf0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.manage-sso {
|
||||
.title-with-toggle {
|
||||
width: 100%;
|
||||
input[type=checkbox] {
|
||||
/* Double-sized Checkboxes */
|
||||
-ms-transform: scale(1.5); /* IE */
|
||||
-moz-transform: scale(1.5); /* FF */
|
||||
-webkit-transform: scale(1.5); /* Safari and Chrome */
|
||||
-o-transform: scale(1.5); /* Opera */
|
||||
transform: scale(1.5);
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.help-text {
|
||||
overflow: auto;
|
||||
div {
|
||||
margin: 0px 0px 5px 0px;
|
||||
background-color: #eaeaea;
|
||||
float: left;
|
||||
padding: 2px 10px;
|
||||
font-size: 11px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #e1e1e1;
|
||||
}
|
||||
}
|
||||
.org-invite-or {
|
||||
padding: 1rem 0rem;
|
||||
h2 {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #000;
|
||||
line-height: 0.1em;
|
||||
margin: 10px 0 20px;
|
||||
}
|
||||
|
||||
h2 span {
|
||||
background:#fff;
|
||||
padding:0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.theme-dark .json-tree-container {
|
||||
.json-tree-node-icon {
|
||||
svg {
|
||||
filter: invert(89%) sepia(2%) saturate(127%) hue-rotate(175deg) brightness(99%) contrast(96%);
|
||||
}
|
||||
}
|
||||
.json-tree-svg-icon.component-icon {
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
|
||||
.node-key-outline {
|
||||
height: 1rem!important;
|
||||
border: 1px solid transparent!important;
|
||||
color: #ccd4df;
|
||||
}
|
||||
|
||||
.selected-node {
|
||||
border-color: $primary-light !important;
|
||||
}
|
||||
.json-tree-icon-container .selected-node > svg:first-child {
|
||||
filter: invert(65%) sepia(62%) saturate(4331%) hue-rotate(204deg) brightness(106%) contrast(97%);
|
||||
}
|
||||
.node-length-color {
|
||||
color: #B8C7FD;
|
||||
}
|
||||
.node-type {
|
||||
color: #8a96a6;
|
||||
}
|
||||
.group-border {
|
||||
border-color: rgb(97, 101, 111);
|
||||
}
|
||||
|
||||
.action-icons-group {
|
||||
img, svg {
|
||||
filter: invert(89%) sepia(2%) saturate(127%) hue-rotate(175deg) brightness(99%) contrast(96%);
|
||||
}
|
||||
}
|
||||
|
||||
.hovered-node.node-key.badge {
|
||||
color: #8092AB !important;
|
||||
border-color: #8092AB !important;
|
||||
}
|
||||
}
|
||||
|
||||
.json-tree-container {
|
||||
.json-tree-svg-icon.component-icon {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
.json-tree-icon-container {
|
||||
max-width: 20px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.node-type {
|
||||
color: #A6B6CC;
|
||||
padding-top: 2px;
|
||||
}
|
||||
.json-tree-valuetype {
|
||||
font-size: 10px;
|
||||
padding-top: 2px;
|
||||
}
|
||||
.node-length-color {
|
||||
color: #3650AF;
|
||||
padding-top: 3px;
|
||||
}
|
||||
.json-tree-node-value {
|
||||
font-size: 11px;
|
||||
}
|
||||
.json-tree-node-string {
|
||||
color: #F6820C;
|
||||
}
|
||||
.json-tree-node-boolean {
|
||||
color: #3EB25F;
|
||||
}
|
||||
.json-tree-node-number {
|
||||
color: #F4B2B0;
|
||||
}
|
||||
.json-tree-node-null {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.group-border {
|
||||
border-left: 0.5px solid #dadcde;
|
||||
margin-top: 16px;
|
||||
margin-left: -12px;
|
||||
}
|
||||
|
||||
.selected-node {
|
||||
border-color: #4D72FA !important;
|
||||
}
|
||||
|
||||
.selected-node .group-object-container .badge {
|
||||
font-weight: 400 !important;
|
||||
height: 1rem !important;
|
||||
}
|
||||
|
||||
.group-object-container {
|
||||
margin-left: 0.72rem;
|
||||
margin-top: -16px;
|
||||
}
|
||||
|
||||
.json-node-element {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hide-show-icon {
|
||||
cursor: pointer;
|
||||
margin-left: 1rem;
|
||||
&:hover {
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
.action-icons-group {
|
||||
margin-right: 4rem !important;
|
||||
margin-left: 2rem !important;
|
||||
}
|
||||
|
||||
.action-icons-group {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hovered-node {
|
||||
font-weight: 400 !important;
|
||||
height: 1rem !important;
|
||||
color:#8092AB;
|
||||
}
|
||||
|
||||
.node-key {
|
||||
font-weight: 400!important;
|
||||
margin-left: -0.25rem!important;
|
||||
}
|
||||
|
||||
.node-key-outline {
|
||||
height: 1rem!important;
|
||||
border: 1px solid transparent!important;
|
||||
color: #3e525b;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.popover-more-actions {
|
||||
font-weight: 400!important;
|
||||
|
||||
&:hover {
|
||||
background: #d2ddec !important;
|
||||
}
|
||||
}
|
||||
|
||||
.popover-dark-themed .popover-more-actions {
|
||||
color: #ccd4df;
|
||||
|
||||
&:hover {
|
||||
background-color: #324156 !important;
|
||||
}
|
||||
}
|
||||
|
||||
#json-tree-popover {
|
||||
padding: 0.25rem !important;
|
||||
}
|
||||
|
||||
// Font sizes
|
||||
.fs-9 {
|
||||
font-size: 9px !important;
|
||||
}
|
||||
.fs-10 {
|
||||
font-size: 10px !important;
|
||||
}
|
||||
|
||||
.fs-12 {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
|
||||
.realtime-avatars {
|
||||
position: absolute;
|
||||
left: 35%;
|
||||
|
|
@ -4752,3 +5109,9 @@ div#driver-page-overlay {
|
|||
.list-timeline:not(.list-timeline-simple) .list-timeline-time {
|
||||
top: auto;
|
||||
}
|
||||
.editor-actions {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding: 5px;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
362
frontend/src/_ui/JSONTreeViewer/JSONNode.jsx
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import cx from 'classnames';
|
||||
import { ToolTip } from '@/_components/ToolTip';
|
||||
import CopyToClipboardComponent from '@/_components/CopyToClipboard';
|
||||
import { Popover } from 'react-bootstrap';
|
||||
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
|
||||
import JSONNodeObject from './JSONNodeObject';
|
||||
import JSONNodeArray from './JSONNodeArray';
|
||||
import JSONNodeValue from './JSONNodeValue';
|
||||
import JSONNodeIndicator from './JSONNodeIndicator';
|
||||
|
||||
export const JSONNode = ({ data, ...restProps }) => {
|
||||
const {
|
||||
path,
|
||||
shouldExpandNode,
|
||||
currentNode,
|
||||
selectedNode,
|
||||
hoveredNode,
|
||||
getCurrentPath,
|
||||
getCurrentNodeType,
|
||||
getLength,
|
||||
toUseNodeIcons,
|
||||
renderNodeIcons,
|
||||
useIndentedBlock,
|
||||
updateSelectedNode,
|
||||
updateHoveredNode,
|
||||
useActions,
|
||||
enableCopyToClipboard,
|
||||
getNodeShowHideComponents,
|
||||
getOnSelectLabelDispatchActions,
|
||||
expandWithLabels,
|
||||
getAbsoluteNodePath,
|
||||
actionsList,
|
||||
updateParentState = () => null,
|
||||
} = restProps;
|
||||
|
||||
const [expandable, set] = React.useState(() =>
|
||||
typeof shouldExpandNode === 'function' ? shouldExpandNode(path, data) : shouldExpandNode
|
||||
);
|
||||
|
||||
const [showHiddenOptionsForNode, setShowHiddenOptionsForNode] = React.useState(false);
|
||||
const [showHiddenOptionButtons, setShowHiddenOptionButtons] = React.useState([]);
|
||||
const [onSelectDispatchActions, setOnSelectDispatchActions] = React.useState([]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (showHiddenOptionButtons) {
|
||||
setShowHiddenOptionButtons(() => getNodeShowHideComponents(currentNode, path));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (useActions && currentNode) {
|
||||
const onSelectDispatchActions = getOnSelectLabelDispatchActions(currentNode, path).filter(
|
||||
(action) => action.onSelect
|
||||
);
|
||||
if (onSelectDispatchActions.length > 0) {
|
||||
setOnSelectDispatchActions(onSelectDispatchActions);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedNode]);
|
||||
|
||||
const toggleExpandNode = (node) => {
|
||||
if (expandable) {
|
||||
updateSelectedNode(null);
|
||||
} else {
|
||||
updateSelectedNode(node, path);
|
||||
}
|
||||
|
||||
set((prev) => !prev);
|
||||
};
|
||||
|
||||
const onSelect = (data, currentNode, path) => {
|
||||
const actions = onSelectDispatchActions;
|
||||
actions.forEach((action) => action.dispatchAction(data, currentNode));
|
||||
|
||||
if (!expandWithLabels) {
|
||||
updateSelectedNode(currentNode, path);
|
||||
set(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnClickLabels = (data, currentNode, path) => {
|
||||
if (expandWithLabels) {
|
||||
toggleExpandNode(currentNode);
|
||||
}
|
||||
|
||||
if (useActions) {
|
||||
onSelect(data, currentNode, path);
|
||||
}
|
||||
};
|
||||
|
||||
const typeofCurrentNode = getCurrentNodeType(data);
|
||||
const currentNodePath = getCurrentPath(path, currentNode);
|
||||
const toExpandNode = (data instanceof Array || data instanceof Object) && !_.isEmpty(data);
|
||||
const toShowNodeIndicator = (data instanceof Array || data instanceof Object) && typeofCurrentNode !== 'Function';
|
||||
const numberOfEntries = getLength(typeofCurrentNode, data);
|
||||
const toRenderSelector = (typeofCurrentNode === 'Object' || typeofCurrentNode === 'Array') && numberOfEntries > 0;
|
||||
|
||||
let $VALUE = null;
|
||||
let $NODEType = null;
|
||||
let $NODEIcon = null;
|
||||
|
||||
const checkSelectedNode = (_selectedNode, _currentNode, parent, toExpand) => {
|
||||
if (selectedNode?.parent && parent) {
|
||||
return _selectedNode.parent === parent && _selectedNode?.node === _currentNode && toExpand;
|
||||
}
|
||||
|
||||
return toExpand && _selectedNode?.node === _currentNode;
|
||||
};
|
||||
|
||||
const parent = path && typeof path?.length === 'number' ? path[path.length - 2] : null;
|
||||
|
||||
const applySelectedNodeStyles = toExpandNode
|
||||
? checkSelectedNode(selectedNode, currentNode, parent, expandable)
|
||||
: false;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!expandable) {
|
||||
updateSelectedNode(null);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [expandable]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (selectedNode?.node === currentNode) {
|
||||
set(true);
|
||||
}
|
||||
}, [selectedNode, currentNode]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (hoveredNode?.node === currentNode && hoveredNode?.parent === parent) {
|
||||
setShowHiddenOptionsForNode(true);
|
||||
}
|
||||
|
||||
return () => {
|
||||
setShowHiddenOptionsForNode(false);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [hoveredNode]);
|
||||
|
||||
if (toUseNodeIcons && currentNode) {
|
||||
$NODEIcon = renderNodeIcons(currentNode);
|
||||
}
|
||||
|
||||
switch (typeofCurrentNode) {
|
||||
case 'String':
|
||||
case 'Boolean':
|
||||
case 'Number':
|
||||
case 'Null':
|
||||
case 'Undefined':
|
||||
case 'Function':
|
||||
$VALUE = <JSONNodeValue data={data} type={typeofCurrentNode} />;
|
||||
$NODEType = <JSONNode.DisplayNodeLabel type={typeofCurrentNode} />;
|
||||
break;
|
||||
|
||||
case 'Object':
|
||||
$VALUE = <JSONNodeObject data={data} path={currentNodePath} {...restProps} />;
|
||||
$NODEType = (
|
||||
<JSONNode.DisplayNodeLabel type={'Object'}>
|
||||
<span className="mx-1 fs-9 node-length-color">
|
||||
{`${numberOfEntries} ${numberOfEntries > 1 ? 'entries' : 'entry'}`}{' '}
|
||||
</span>
|
||||
</JSONNode.DisplayNodeLabel>
|
||||
);
|
||||
break;
|
||||
|
||||
case 'Array':
|
||||
$VALUE = <JSONNodeArray data={data} path={currentNodePath} {...restProps} />;
|
||||
$NODEType = (
|
||||
<JSONNode.DisplayNodeLabel type={'Array'}>
|
||||
<span className="mx-1 fs-9 node-length-color">
|
||||
{`${numberOfEntries} ${numberOfEntries > 1 ? 'items' : 'item'}`}{' '}
|
||||
</span>
|
||||
</JSONNode.DisplayNodeLabel>
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
$VALUE = <span>{String(data)}</span>;
|
||||
$NODEType = typeofCurrentNode;
|
||||
}
|
||||
|
||||
let $key = (
|
||||
<span
|
||||
onClick={() => toExpandNode && handleOnClickLabels(data, currentNode, path)}
|
||||
style={{ marginTop: '1px', cursor: 'pointer', textTransform: 'none' }}
|
||||
className={cx('node-key fs-12 mx-0 badge badge-outline', {
|
||||
'color-primary': applySelectedNodeStyles && !showHiddenOptionsForNode,
|
||||
'hovered-node': showHiddenOptionsForNode,
|
||||
'node-key-outline': !applySelectedNodeStyles && !showHiddenOptionsForNode,
|
||||
})}
|
||||
>
|
||||
{String(currentNode)}
|
||||
</span>
|
||||
);
|
||||
|
||||
if (!currentNode) {
|
||||
return $VALUE;
|
||||
}
|
||||
|
||||
const shouldDisplayIntendedBlock =
|
||||
useIndentedBlock && expandable && (typeofCurrentNode === 'Object' || typeofCurrentNode === 'Array');
|
||||
|
||||
function moreActionsPopover(actions) {
|
||||
//Todo: For adding more actions to the menu popover!
|
||||
const darkMode = localStorage.getItem('darkMode') === 'true';
|
||||
|
||||
return (
|
||||
<Popover
|
||||
id="popover-basic popover-positioned-right json-tree-popover"
|
||||
style={{ maxWidth: '350px', padding: '0px' }}
|
||||
className={`shadow ${darkMode && 'popover-dark-themed theme-dark'}`}
|
||||
>
|
||||
<div className="list-group">
|
||||
{actions?.map((action, index) => (
|
||||
<span
|
||||
key={index}
|
||||
type="button"
|
||||
className="list-group-item list-group-item-action popover-more-actions"
|
||||
aria-current="true"
|
||||
onClick={() => {
|
||||
action.dispatchAction(data, currentNode);
|
||||
updateParentState();
|
||||
}}
|
||||
>
|
||||
{action.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
const renderHiddenOptionsForNode = () => {
|
||||
const moreActions = actionsList.filter((action) => action.for === 'all')[0];
|
||||
|
||||
const renderOptions = () => {
|
||||
if (!useActions || showHiddenOptionButtons?.length === 0) return null;
|
||||
|
||||
return showHiddenOptionButtons?.map((actionOption, index) => {
|
||||
const { name, icon, src, iconName, dispatchAction, width = 12, height = 12 } = actionOption;
|
||||
if (icon) {
|
||||
return (
|
||||
<ToolTip key={`${name}-${index}`} message={`${name} ${currentNode}`}>
|
||||
<span
|
||||
style={{ height: '13px', width: '13px' }}
|
||||
className="mx-1"
|
||||
onClick={() => dispatchAction(data, currentNode)}
|
||||
>
|
||||
<img src={src ?? `/assets/images/icons/${iconName}.svg`} width={width} height={height} />
|
||||
</span>
|
||||
</ToolTip>
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ fontSize: '9px', marginTop: '0px' }} className="d-flex end-0 position-absolute">
|
||||
{enableCopyToClipboard && (
|
||||
<CopyToClipboardComponent data={currentNodePath} path={true} callback={getAbsoluteNodePath} />
|
||||
)}
|
||||
{renderOptions()}
|
||||
|
||||
{moreActions.actions?.length > 0 && (
|
||||
<OverlayTrigger
|
||||
rootClose={true}
|
||||
rootCloseEvent="mousedown"
|
||||
trigger="click"
|
||||
placement={'right'}
|
||||
overlay={moreActionsPopover(moreActions?.actions)}
|
||||
>
|
||||
<span>
|
||||
<ToolTip message={'More actions'}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="icon-tabler icon-tabler-dots-vertical"
|
||||
width="13"
|
||||
height="13"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="2"
|
||||
stroke="#2c3e50"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<circle cx="12" cy="12" r="1" />
|
||||
<circle cx="12" cy="19" r="1" />
|
||||
<circle cx="12" cy="5" r="1" />
|
||||
</svg>
|
||||
</ToolTip>
|
||||
</span>
|
||||
</OverlayTrigger>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx('d-flex row-flex mt-1 font-monospace container-fluid px-1', {
|
||||
'json-node-element': !expandable,
|
||||
})}
|
||||
>
|
||||
<div className={`json-tree-icon-container mx-2 ${applySelectedNodeStyles && 'selected-node'}`}>
|
||||
<JSONNodeIndicator
|
||||
toExpand={expandable}
|
||||
toShowNodeIndicator={toShowNodeIndicator}
|
||||
handleToggle={toggleExpandNode}
|
||||
typeofCurrentNode={typeofCurrentNode}
|
||||
currentNode={currentNode}
|
||||
isSelected={selectedNode?.node === currentNode}
|
||||
toExpandNode={toExpandNode}
|
||||
data={data}
|
||||
path={currentNodePath}
|
||||
toExpandWithLabels={expandWithLabels}
|
||||
toggleWithLabels={handleOnClickLabels}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{ width: 'inherit' }}
|
||||
className={`${shouldDisplayIntendedBlock && 'group-border'} ${applySelectedNodeStyles && 'selected-node'}`}
|
||||
>
|
||||
<div
|
||||
onMouseEnter={() => updateHoveredNode(currentNode, currentNodePath)}
|
||||
onMouseLeave={() => updateHoveredNode(null)}
|
||||
className={cx('d-flex', {
|
||||
'group-object-container': shouldDisplayIntendedBlock,
|
||||
'mx-2': typeofCurrentNode !== 'Object' && typeofCurrentNode !== 'Array',
|
||||
})}
|
||||
>
|
||||
{$NODEIcon && <div className="json-tree-icon-container">{$NODEIcon}</div>}
|
||||
{$key} {$NODEType}
|
||||
{!toExpandNode && !expandable && !toRenderSelector ? $VALUE : null}
|
||||
<div className="action-icons-group">{showHiddenOptionsForNode && renderHiddenOptionsForNode()}</div>
|
||||
</div>
|
||||
{toRenderSelector && (toExpandNode && !expandable ? null : $VALUE)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DisplayNodeLabel = ({ type = '', children }) => {
|
||||
if (type === 'Null' || type === 'Undefined') {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<span className="mx-1 fs-10 node-type">{type}</span>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
JSONNode.DisplayNodeLabel = DisplayNodeLabel;
|
||||
21
frontend/src/_ui/JSONTreeViewer/JSONNodeArray.jsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react';
|
||||
import { JSONNode } from './JSONNode';
|
||||
|
||||
const JSONTreeArrayNode = ({ data, path, ...restProps }) => {
|
||||
const keys = [];
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
keys.push(String(i));
|
||||
}
|
||||
|
||||
return keys.map((key, index) => {
|
||||
const currentPath = [...path, key];
|
||||
const _currentNode = key;
|
||||
const props = { ...restProps };
|
||||
props.currentNode = _currentNode;
|
||||
|
||||
return <JSONNode key={`arr-${key}/${index}`} data={data[Number(key)]} path={currentPath} {...props} />;
|
||||
});
|
||||
};
|
||||
|
||||
export default JSONTreeArrayNode;
|
||||
53
frontend/src/_ui/JSONTreeViewer/JSONNodeIndicator.jsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import React from 'react';
|
||||
|
||||
const JSONTreeNodeIndicator = ({ toExpand, toShowNodeIndicator, handleToggle, ...restProps }) => {
|
||||
const {
|
||||
renderCustomIndicator,
|
||||
typeofCurrentNode,
|
||||
currentNode,
|
||||
isSelected,
|
||||
toExpandNode,
|
||||
data,
|
||||
path,
|
||||
toExpandWithLabels,
|
||||
toggleWithLabels,
|
||||
} = restProps;
|
||||
|
||||
const defaultStyles = {
|
||||
transform: toExpandNode && toExpand ? 'rotate(90deg)' : 'rotate(0deg)',
|
||||
transition: '0.2s all',
|
||||
display: 'inline-block',
|
||||
cursor: 'pointer',
|
||||
};
|
||||
|
||||
const handleToggleForNode = () => {
|
||||
if (toExpandWithLabels) {
|
||||
return toggleWithLabels(data, currentNode, path);
|
||||
}
|
||||
|
||||
return handleToggle(currentNode);
|
||||
};
|
||||
|
||||
const renderDefaultIndicator = () => (
|
||||
<svg width="6" height="10" viewBox="0 0 6 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M1.02063 1L5.01032 5.01028L1.00003 8.99997"
|
||||
stroke={`${toExpand && isSelected ? '#4D72FA' : '#61656F'}`}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
if (!toShowNodeIndicator && (typeofCurrentNode !== 'Object' || typeofCurrentNode !== 'Array')) return null;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<span className="json-tree-node-icon" onClick={handleToggleForNode} style={defaultStyles}>
|
||||
{renderCustomIndicator ? renderCustomIndicator() : renderDefaultIndicator()}
|
||||
</span>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default JSONTreeNodeIndicator;
|
||||
17
frontend/src/_ui/JSONTreeViewer/JSONNodeObject.jsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import React from 'react';
|
||||
import { JSONNode } from './JSONNode';
|
||||
|
||||
const JSONTreeObjectNode = ({ data, path, ...restProps }) => {
|
||||
const nodeKeys = Object.keys(data);
|
||||
|
||||
return nodeKeys.map((key, index) => {
|
||||
const currentPath = [...path, key];
|
||||
const _currentNode = key;
|
||||
const props = { ...restProps };
|
||||
props.currentNode = _currentNode;
|
||||
|
||||
return <JSONNode key={`obj-${key}/${index}`} data={data[key]} path={currentPath} {...props} />;
|
||||
});
|
||||
};
|
||||
|
||||
export default JSONTreeObjectNode;
|
||||
31
frontend/src/_ui/JSONTreeViewer/JSONNodeValue.jsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import React from 'react';
|
||||
|
||||
const JSONTreeValueNode = ({ data, type }) => {
|
||||
if (type === 'Function') {
|
||||
const functionString = `${data.toString().split('{')[0].trim()}{...}`;
|
||||
return (
|
||||
<React.Fragment>
|
||||
<span
|
||||
className={`text-secondary node-value-${type}`}
|
||||
style={{ fontSize: '12px', fontFamily: 'monospace', textTransform: 'none' }}
|
||||
>
|
||||
{functionString}
|
||||
</span>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
const value = type === 'String' ? `"${data}"` : String(data);
|
||||
const clsForUndefinedOrNull = (type === 'Undefined' || type === 'Null') && 'badge badge-secondary';
|
||||
return (
|
||||
<span
|
||||
className={`mx-2 json-tree-valuetype json-tree-node-${String(
|
||||
type
|
||||
).toLowerCase()} text-break ${clsForUndefinedOrNull}`}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default JSONTreeValueNode;
|
||||
229
frontend/src/_ui/JSONTreeViewer/JSONTreeViewer.jsx
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { JSONNode } from './JSONNode';
|
||||
import ErrorBoundary from '@/Editor/ErrorBoundary';
|
||||
|
||||
export class JSONTreeViewer extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
data: this.props.data,
|
||||
shouldExpandNode: false,
|
||||
currentNode: 'Root',
|
||||
selectedNode: null,
|
||||
hoveredNode: null,
|
||||
darkTheme: false,
|
||||
showHideActions: false,
|
||||
enableCopyToClipboard: false,
|
||||
actionsList: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (!_.isEqual(prevProps, this.props)) {
|
||||
this.setState({
|
||||
data: this.props.data,
|
||||
shouldExpandNode: this.props.shouldExpandNode,
|
||||
...this.props,
|
||||
});
|
||||
}
|
||||
|
||||
if (prevState.selectedComponent !== this.state.selectedComponent && this.props.treeType === 'inspector') {
|
||||
if (this.getCurrentNodeType(this.state.data) === 'Object') {
|
||||
const matchedWidget = Object.keys(this.state.data.components).filter(
|
||||
(component) => this.state.data.components[component].id === this.state.selectedComponent.id
|
||||
)[0];
|
||||
|
||||
if (matchedWidget) {
|
||||
this.setState(
|
||||
{
|
||||
selectedWidget: matchedWidget,
|
||||
},
|
||||
() => {
|
||||
this.updateSelectedNode(matchedWidget);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentNodePath(path, node) {
|
||||
let currentPath = path ?? [];
|
||||
if (node) {
|
||||
if (!currentPath[currentPath.length - 1] === node) {
|
||||
currentPath = [...currentPath, node];
|
||||
}
|
||||
}
|
||||
return currentPath;
|
||||
}
|
||||
|
||||
getCurrentNodeType(node) {
|
||||
const typeofCurrentNode = Object.prototype.toString.call(node).slice(8, -1);
|
||||
//Todo: Handle more types (Custom type or Iterable type)
|
||||
|
||||
return typeofCurrentNode;
|
||||
}
|
||||
|
||||
getLength(type, collection) {
|
||||
if (!collection) return 0;
|
||||
if (type === 'Object') {
|
||||
return Object.keys(collection).length;
|
||||
} else if (type === 'Array') {
|
||||
return collection.length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
renderNodeIcons = (node) => {
|
||||
const icon = this.props.iconsList.filter((icon) => icon?.iconName === node)[0];
|
||||
|
||||
if (icon && icon.iconPath) {
|
||||
return (
|
||||
<img
|
||||
style={{ maxWidth: 'none', padding: '2px' }}
|
||||
className={`json-tree-svg-icon ${icon.className}`}
|
||||
src={icon.iconPath}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (icon && icon.jsx) {
|
||||
return icon.jsx();
|
||||
}
|
||||
};
|
||||
|
||||
updateSelectedNode = (node, path) => {
|
||||
if (node) {
|
||||
this.setState({
|
||||
selectedNode: { node: node, parent: path?.length ? path[path.length - 2] : null },
|
||||
});
|
||||
}
|
||||
};
|
||||
updateHoveredNode = (node, path) => {
|
||||
this.setState({
|
||||
hoveredNode: { node: node, parent: path?.length ? path[path.length - 2] : null },
|
||||
});
|
||||
};
|
||||
|
||||
getDispatchActionsForNode = (node) => {
|
||||
if (!node) return null;
|
||||
return this.state.actionsList.filter((action) => action.for === node)[0];
|
||||
};
|
||||
|
||||
getNodeShowHideComponents = (currentNode, path) => {
|
||||
const showHideComponents = [];
|
||||
const parent = path ? path[path.length - 2] : 'root';
|
||||
const dispatchActionForCurrentNode = this.getDispatchActionsForNode(parent);
|
||||
|
||||
if (currentNode === parent) return;
|
||||
|
||||
if (dispatchActionForCurrentNode && dispatchActionForCurrentNode['enableFor1stLevelChildren']) {
|
||||
dispatchActionForCurrentNode['actions'].map((action) => showHideComponents.push(action));
|
||||
}
|
||||
|
||||
return showHideComponents;
|
||||
//Todo: if actions should be available for all children
|
||||
};
|
||||
|
||||
getOnSelectLabelDispatchActions = (currentNode, path) => {
|
||||
const actions = [];
|
||||
const parent = path ? path[path.length - 2] : 'root';
|
||||
const dispatchActionForCurrentNode = this.getDispatchActionsForNode(parent);
|
||||
if (currentNode === parent) return;
|
||||
|
||||
if (dispatchActionForCurrentNode && dispatchActionForCurrentNode['enableFor1stLevelChildren']) {
|
||||
dispatchActionForCurrentNode['actions'].map((action) => actions.push(action));
|
||||
}
|
||||
//Todo: if actions should be available for all children
|
||||
return actions;
|
||||
};
|
||||
|
||||
getAbsoluteNodePath = (path) => {
|
||||
const data = this.state.data;
|
||||
if (!data || _.isEmpty(data)) return null;
|
||||
const map = new Map();
|
||||
|
||||
// loop through the data and build the map
|
||||
const buildMap = (data, path = '') => {
|
||||
const keys = Object.keys(data);
|
||||
keys.forEach((key) => {
|
||||
const value = data[key];
|
||||
const _type = Object.prototype.toString.call(value).slice(8, -1);
|
||||
let newPath = '';
|
||||
if (path === '') {
|
||||
newPath = key;
|
||||
} else {
|
||||
newPath = `${path}.${key}`;
|
||||
}
|
||||
|
||||
if (_.isObject(value) || _.isArray(value)) {
|
||||
buildMap(value, newPath);
|
||||
} else if (_.isFunction(value)) {
|
||||
map.set(newPath, { type: _type });
|
||||
} else {
|
||||
map.set(newPath, { type: _type });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const computeAbsolutePath = (path) => {
|
||||
let prevPath, prevType, prevRelPath, currentPath, abs;
|
||||
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
prevType = map.get(prevRelPath)?.type;
|
||||
const node = path[i];
|
||||
|
||||
currentPath = prevRelPath ? `${prevRelPath}.${node}` : node;
|
||||
|
||||
if (prevType === 'Object') {
|
||||
abs = `${prevPath}.${node}`;
|
||||
} else if (prevType === 'Array') {
|
||||
abs = `${prevPath}[${node}]`;
|
||||
} else {
|
||||
abs = currentPath;
|
||||
}
|
||||
prevPath = abs;
|
||||
prevRelPath = currentPath;
|
||||
}
|
||||
|
||||
return abs;
|
||||
};
|
||||
|
||||
buildMap(data);
|
||||
|
||||
return computeAbsolutePath(path);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="json-tree-container row-flex container-fluid p-0">
|
||||
<ErrorBoundary showFallback={true}>
|
||||
<JSONNode
|
||||
data={this.state.data}
|
||||
shouldExpandNode={false}
|
||||
getCurrentPath={this.getCurrentNodePath}
|
||||
getCurrentNodeType={this.getCurrentNodeType}
|
||||
toUseNodeIcons={this.props.useIcons ?? false}
|
||||
getLength={this.getLength}
|
||||
renderNodeIcons={this.renderNodeIcons}
|
||||
useIndentedBlock={this.props.useIndentedBlock ?? false}
|
||||
selectedNode={this.state.selectedNode}
|
||||
hoveredNode={this.state.hoveredNode}
|
||||
selectedWidget={this.state.selectedWidget ?? null}
|
||||
updateSelectedNode={this.updateSelectedNode}
|
||||
updateHoveredNode={this.updateHoveredNode}
|
||||
useActions={this.state.useActions}
|
||||
actionsList={this.state.actionsList}
|
||||
enableCopyToClipboard={this.state.enableCopyToClipboard}
|
||||
getNodeShowHideComponents={this.getNodeShowHideComponents}
|
||||
getOnSelectLabelDispatchActions={this.getOnSelectLabelDispatchActions}
|
||||
expandWithLabels={this.props.expandWithLabels ?? false} //expand and collapse: onclick of label
|
||||
getAbsoluteNodePath={this.getAbsoluteNodePath}
|
||||
updateParentState={this.state.updateParentState}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
3
frontend/src/_ui/JSONTreeViewer/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { JSONTreeViewer } from './JSONTreeViewer';
|
||||
|
||||
export default JSONTreeViewer;
|
||||
|
|
@ -110,6 +110,7 @@ module.exports = {
|
|||
apiUrl: `${API_URL[environment] || ''}/api`,
|
||||
SERVER_IP: process.env.SERVER_IP,
|
||||
COMMENT_FEATURE_ENABLE: true,
|
||||
ENABLE_MULTIPLAYER_EDITING: true,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -56,4 +56,4 @@
|
|||
"cy:open": "cypress open --env db.name=$TEST_PG_DB,db.user=$TEST_PG_USERNAME,db.password=$TEST_PG_PASSWORD",
|
||||
"prepare": "husky install"
|
||||
}
|
||||
}
|
||||
}
|
||||
381
plugins/package-lock.json
generated
|
|
@ -27,6 +27,7 @@
|
|||
"@tooljet-plugins/mssql": "file:packages/mssql",
|
||||
"@tooljet-plugins/mysql": "file:packages/mysql",
|
||||
"@tooljet-plugins/n8n": "file:packages/n8n",
|
||||
"@tooljet-plugins/notion": "file:packages/notion",
|
||||
"@tooljet-plugins/openapi": "file:packages/openapi",
|
||||
"@tooljet-plugins/oracledb": "file:packages/oracledb",
|
||||
"@tooljet-plugins/postgresql": "file:packages/postgresql",
|
||||
|
|
@ -4068,6 +4069,18 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@notionhq/client": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@notionhq/client/-/client-1.0.4.tgz",
|
||||
"integrity": "sha512-m7zZ5l3RUktayf1lRBV1XMb8HSKsmWTv/LZPqP7UGC1NMzOlc+bbTOPNQ4CP/c1P4cP61VWLb/zBq7a3c0nMaw==",
|
||||
"dependencies": {
|
||||
"@types/node-fetch": "^2.5.10",
|
||||
"node-fetch": "^2.6.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@npmcli/ci-detect": {
|
||||
"version": "1.4.0",
|
||||
"dev": true,
|
||||
|
|
@ -4459,8 +4472,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@sindresorhus/is": {
|
||||
"version": "4.2.0",
|
||||
"license": "MIT",
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
|
||||
"integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
|
|
@ -4570,6 +4584,10 @@
|
|||
"resolved": "packages/n8n",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@tooljet-plugins/notion": {
|
||||
"resolved": "packages/notion",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@tooljet-plugins/openapi": {
|
||||
"resolved": "packages/openapi",
|
||||
"link": true
|
||||
|
|
@ -8416,6 +8434,11 @@
|
|||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data-encoder": {
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.1.tgz",
|
||||
"integrity": "sha512-EFRDrsMm/kyqbTQocNvRXMLjc7Es2Vk+IQFx/YW7hkUH1eBl4J1fqiP34l74Yt0pFLCNpc06fkbVk00008mzjg=="
|
||||
},
|
||||
"node_modules/fs-constants": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
|
|
@ -10696,11 +10719,9 @@
|
|||
"license": "ISC"
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.5"
|
||||
},
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
|
||||
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
|
||||
"bin": {
|
||||
"json5": "lib/cli.js"
|
||||
},
|
||||
|
|
@ -16529,9 +16550,98 @@
|
|||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@tooljet-plugins/common": "file:../common",
|
||||
"got": "^12.0.3",
|
||||
"json5": "^2.2.1",
|
||||
"react": "^17.0.2"
|
||||
}
|
||||
},
|
||||
"packages/baserow/node_modules/@szmarczak/http-timer": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz",
|
||||
"integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==",
|
||||
"dependencies": {
|
||||
"defer-to-connect": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
}
|
||||
},
|
||||
"packages/baserow/node_modules/cacheable-lookup": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-6.0.4.tgz",
|
||||
"integrity": "sha512-mbcDEZCkv2CZF4G01kr8eBd/5agkt9oCqz75tJMSIsquvRZ2sL6Hi5zGVKi/0OSC9oO1GHfJ2AV0ZIOY9vye0A==",
|
||||
"engines": {
|
||||
"node": ">=10.6.0"
|
||||
}
|
||||
},
|
||||
"packages/baserow/node_modules/got": {
|
||||
"version": "12.0.4",
|
||||
"resolved": "https://registry.npmjs.org/got/-/got-12.0.4.tgz",
|
||||
"integrity": "sha512-2Eyz4iU/ktq7wtMFXxzK7g5p35uNYLLdiZarZ5/Yn3IJlNEpBd5+dCgcAyxN8/8guZLszffwe3wVyw+DEVrpBg==",
|
||||
"dependencies": {
|
||||
"@sindresorhus/is": "^4.6.0",
|
||||
"@szmarczak/http-timer": "^5.0.1",
|
||||
"@types/cacheable-request": "^6.0.2",
|
||||
"@types/responselike": "^1.0.0",
|
||||
"cacheable-lookup": "^6.0.4",
|
||||
"cacheable-request": "^7.0.2",
|
||||
"decompress-response": "^6.0.0",
|
||||
"form-data-encoder": "1.7.1",
|
||||
"get-stream": "^6.0.1",
|
||||
"http2-wrapper": "^2.1.10",
|
||||
"lowercase-keys": "^3.0.0",
|
||||
"p-cancelable": "^3.0.0",
|
||||
"responselike": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/got?sponsor=1"
|
||||
}
|
||||
},
|
||||
"packages/baserow/node_modules/http2-wrapper": {
|
||||
"version": "2.1.11",
|
||||
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.1.11.tgz",
|
||||
"integrity": "sha512-aNAk5JzLturWEUiuhAN73Jcbq96R7rTitAoXV54FYMatvihnpD2+6PUgU4ce3D/m5VDbw+F5CsyKSF176ptitQ==",
|
||||
"dependencies": {
|
||||
"quick-lru": "^5.1.1",
|
||||
"resolve-alpn": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.19.0"
|
||||
}
|
||||
},
|
||||
"packages/baserow/node_modules/lowercase-keys": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz",
|
||||
"integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"packages/baserow/node_modules/p-cancelable": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz",
|
||||
"integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==",
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
}
|
||||
},
|
||||
"packages/baserow/node_modules/quick-lru": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
|
||||
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"packages/bigquery": {
|
||||
"name": "@tooljet-plugins/bigquery",
|
||||
"version": "1.0.0",
|
||||
|
|
@ -16707,11 +16817,109 @@
|
|||
"react": "^17.0.2"
|
||||
}
|
||||
},
|
||||
"packages/notion": {
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@notionhq/client": "^1.0.4",
|
||||
"@tooljet-plugins/common": "file:../common",
|
||||
"react": "^17.0.2"
|
||||
}
|
||||
},
|
||||
"packages/openapi": {
|
||||
"name": "@tooljet-plugins/openapi",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@tooljet-plugins/common": "file:../common",
|
||||
"react": "^17.0.2"
|
||||
"got": "^12.0.3",
|
||||
"react": "^17.0.2",
|
||||
"tough-cookie": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"packages/openapi/node_modules/@szmarczak/http-timer": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz",
|
||||
"integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==",
|
||||
"dependencies": {
|
||||
"defer-to-connect": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
}
|
||||
},
|
||||
"packages/openapi/node_modules/cacheable-lookup": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-6.0.4.tgz",
|
||||
"integrity": "sha512-mbcDEZCkv2CZF4G01kr8eBd/5agkt9oCqz75tJMSIsquvRZ2sL6Hi5zGVKi/0OSC9oO1GHfJ2AV0ZIOY9vye0A==",
|
||||
"engines": {
|
||||
"node": ">=10.6.0"
|
||||
}
|
||||
},
|
||||
"packages/openapi/node_modules/got": {
|
||||
"version": "12.0.4",
|
||||
"resolved": "https://registry.npmjs.org/got/-/got-12.0.4.tgz",
|
||||
"integrity": "sha512-2Eyz4iU/ktq7wtMFXxzK7g5p35uNYLLdiZarZ5/Yn3IJlNEpBd5+dCgcAyxN8/8guZLszffwe3wVyw+DEVrpBg==",
|
||||
"dependencies": {
|
||||
"@sindresorhus/is": "^4.6.0",
|
||||
"@szmarczak/http-timer": "^5.0.1",
|
||||
"@types/cacheable-request": "^6.0.2",
|
||||
"@types/responselike": "^1.0.0",
|
||||
"cacheable-lookup": "^6.0.4",
|
||||
"cacheable-request": "^7.0.2",
|
||||
"decompress-response": "^6.0.0",
|
||||
"form-data-encoder": "1.7.1",
|
||||
"get-stream": "^6.0.1",
|
||||
"http2-wrapper": "^2.1.10",
|
||||
"lowercase-keys": "^3.0.0",
|
||||
"p-cancelable": "^3.0.0",
|
||||
"responselike": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/got?sponsor=1"
|
||||
}
|
||||
},
|
||||
"packages/openapi/node_modules/http2-wrapper": {
|
||||
"version": "2.1.11",
|
||||
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.1.11.tgz",
|
||||
"integrity": "sha512-aNAk5JzLturWEUiuhAN73Jcbq96R7rTitAoXV54FYMatvihnpD2+6PUgU4ce3D/m5VDbw+F5CsyKSF176ptitQ==",
|
||||
"dependencies": {
|
||||
"quick-lru": "^5.1.1",
|
||||
"resolve-alpn": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.19.0"
|
||||
}
|
||||
},
|
||||
"packages/openapi/node_modules/lowercase-keys": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz",
|
||||
"integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"packages/openapi/node_modules/p-cancelable": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz",
|
||||
"integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==",
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
}
|
||||
},
|
||||
"packages/openapi/node_modules/quick-lru": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
|
||||
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"packages/oracledb": {
|
||||
|
|
@ -19982,6 +20190,15 @@
|
|||
"fastq": "^1.6.0"
|
||||
}
|
||||
},
|
||||
"@notionhq/client": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@notionhq/client/-/client-1.0.4.tgz",
|
||||
"integrity": "sha512-m7zZ5l3RUktayf1lRBV1XMb8HSKsmWTv/LZPqP7UGC1NMzOlc+bbTOPNQ4CP/c1P4cP61VWLb/zBq7a3c0nMaw==",
|
||||
"requires": {
|
||||
"@types/node-fetch": "^2.5.10",
|
||||
"node-fetch": "^2.6.1"
|
||||
}
|
||||
},
|
||||
"@npmcli/ci-detect": {
|
||||
"version": "1.4.0",
|
||||
"dev": true
|
||||
|
|
@ -20277,7 +20494,9 @@
|
|||
}
|
||||
},
|
||||
"@sindresorhus/is": {
|
||||
"version": "4.2.0"
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
|
||||
"integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="
|
||||
},
|
||||
"@sinonjs/commons": {
|
||||
"version": "1.8.3",
|
||||
|
|
@ -20321,7 +20540,68 @@
|
|||
"version": "file:packages/baserow",
|
||||
"requires": {
|
||||
"@tooljet-plugins/common": "file:../common",
|
||||
"got": "^12.0.3",
|
||||
"json5": "^2.2.1",
|
||||
"react": "^17.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@szmarczak/http-timer": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz",
|
||||
"integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==",
|
||||
"requires": {
|
||||
"defer-to-connect": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"cacheable-lookup": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-6.0.4.tgz",
|
||||
"integrity": "sha512-mbcDEZCkv2CZF4G01kr8eBd/5agkt9oCqz75tJMSIsquvRZ2sL6Hi5zGVKi/0OSC9oO1GHfJ2AV0ZIOY9vye0A=="
|
||||
},
|
||||
"got": {
|
||||
"version": "12.0.4",
|
||||
"resolved": "https://registry.npmjs.org/got/-/got-12.0.4.tgz",
|
||||
"integrity": "sha512-2Eyz4iU/ktq7wtMFXxzK7g5p35uNYLLdiZarZ5/Yn3IJlNEpBd5+dCgcAyxN8/8guZLszffwe3wVyw+DEVrpBg==",
|
||||
"requires": {
|
||||
"@sindresorhus/is": "^4.6.0",
|
||||
"@szmarczak/http-timer": "^5.0.1",
|
||||
"@types/cacheable-request": "^6.0.2",
|
||||
"@types/responselike": "^1.0.0",
|
||||
"cacheable-lookup": "^6.0.4",
|
||||
"cacheable-request": "^7.0.2",
|
||||
"decompress-response": "^6.0.0",
|
||||
"form-data-encoder": "1.7.1",
|
||||
"get-stream": "^6.0.1",
|
||||
"http2-wrapper": "^2.1.10",
|
||||
"lowercase-keys": "^3.0.0",
|
||||
"p-cancelable": "^3.0.0",
|
||||
"responselike": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"http2-wrapper": {
|
||||
"version": "2.1.11",
|
||||
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.1.11.tgz",
|
||||
"integrity": "sha512-aNAk5JzLturWEUiuhAN73Jcbq96R7rTitAoXV54FYMatvihnpD2+6PUgU4ce3D/m5VDbw+F5CsyKSF176ptitQ==",
|
||||
"requires": {
|
||||
"quick-lru": "^5.1.1",
|
||||
"resolve-alpn": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"lowercase-keys": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz",
|
||||
"integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ=="
|
||||
},
|
||||
"p-cancelable": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz",
|
||||
"integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw=="
|
||||
},
|
||||
"quick-lru": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
|
||||
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@tooljet-plugins/bigquery": {
|
||||
|
|
@ -20461,11 +20741,80 @@
|
|||
"react": "^17.0.2"
|
||||
}
|
||||
},
|
||||
"@tooljet-plugins/notion": {
|
||||
"version": "file:packages/notion",
|
||||
"requires": {
|
||||
"@notionhq/client": "^1.0.4",
|
||||
"@tooljet-plugins/common": "file:../common",
|
||||
"react": "^17.0.2"
|
||||
}
|
||||
},
|
||||
"@tooljet-plugins/openapi": {
|
||||
"version": "file:packages/openapi",
|
||||
"requires": {
|
||||
"@tooljet-plugins/common": "file:../common",
|
||||
"react": "^17.0.2"
|
||||
"got": "^12.0.3",
|
||||
"react": "^17.0.2",
|
||||
"tough-cookie": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@szmarczak/http-timer": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz",
|
||||
"integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==",
|
||||
"requires": {
|
||||
"defer-to-connect": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"cacheable-lookup": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-6.0.4.tgz",
|
||||
"integrity": "sha512-mbcDEZCkv2CZF4G01kr8eBd/5agkt9oCqz75tJMSIsquvRZ2sL6Hi5zGVKi/0OSC9oO1GHfJ2AV0ZIOY9vye0A=="
|
||||
},
|
||||
"got": {
|
||||
"version": "12.0.4",
|
||||
"resolved": "https://registry.npmjs.org/got/-/got-12.0.4.tgz",
|
||||
"integrity": "sha512-2Eyz4iU/ktq7wtMFXxzK7g5p35uNYLLdiZarZ5/Yn3IJlNEpBd5+dCgcAyxN8/8guZLszffwe3wVyw+DEVrpBg==",
|
||||
"requires": {
|
||||
"@sindresorhus/is": "^4.6.0",
|
||||
"@szmarczak/http-timer": "^5.0.1",
|
||||
"@types/cacheable-request": "^6.0.2",
|
||||
"@types/responselike": "^1.0.0",
|
||||
"cacheable-lookup": "^6.0.4",
|
||||
"cacheable-request": "^7.0.2",
|
||||
"decompress-response": "^6.0.0",
|
||||
"form-data-encoder": "1.7.1",
|
||||
"get-stream": "^6.0.1",
|
||||
"http2-wrapper": "^2.1.10",
|
||||
"lowercase-keys": "^3.0.0",
|
||||
"p-cancelable": "^3.0.0",
|
||||
"responselike": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"http2-wrapper": {
|
||||
"version": "2.1.11",
|
||||
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.1.11.tgz",
|
||||
"integrity": "sha512-aNAk5JzLturWEUiuhAN73Jcbq96R7rTitAoXV54FYMatvihnpD2+6PUgU4ce3D/m5VDbw+F5CsyKSF176ptitQ==",
|
||||
"requires": {
|
||||
"quick-lru": "^5.1.1",
|
||||
"resolve-alpn": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"lowercase-keys": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz",
|
||||
"integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ=="
|
||||
},
|
||||
"p-cancelable": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz",
|
||||
"integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw=="
|
||||
},
|
||||
"quick-lru": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
|
||||
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@tooljet-plugins/oracledb": {
|
||||
|
|
@ -23219,6 +23568,11 @@
|
|||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"form-data-encoder": {
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.1.tgz",
|
||||
"integrity": "sha512-EFRDrsMm/kyqbTQocNvRXMLjc7Es2Vk+IQFx/YW7hkUH1eBl4J1fqiP34l74Yt0pFLCNpc06fkbVk00008mzjg=="
|
||||
},
|
||||
"fs-constants": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
|
|
@ -24718,10 +25072,9 @@
|
|||
"version": "5.0.1"
|
||||
},
|
||||
"json5": {
|
||||
"version": "2.2.0",
|
||||
"requires": {
|
||||
"minimist": "^1.2.5"
|
||||
}
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
|
||||
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA=="
|
||||
},
|
||||
"jsonfile": {
|
||||
"version": "6.1.0",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ConnectionTestResult, QueryService, QueryResult } from '@tooljet-plugins/common';
|
||||
import { ConnectionTestResult, QueryService, QueryResult, QueryError } from '@tooljet-plugins/common';
|
||||
import { getDocument, updateDocument } from './operations';
|
||||
import { indexDocument, search } from './operations';
|
||||
import { Client } from '@opensearch-project/opensearch';
|
||||
|
|
@ -27,6 +27,7 @@ export default class ElasticsearchService implements QueryService {
|
|||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
throw new QueryError('Query could not be completed', err.message, {});
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { Body, Controller, Post, Request } from '@nestjs/common';
|
||||
import { Body, Controller, Param, Post } from '@nestjs/common';
|
||||
import { OauthService } from '../services/oauth/oauth.service';
|
||||
|
||||
@Controller('oauth')
|
||||
export class OauthController {
|
||||
constructor(private oauthService: OauthService) {}
|
||||
|
||||
@Post('sign-in')
|
||||
async create(@Request() req, @Body() body) {
|
||||
const result = await this.oauthService.signIn(body);
|
||||
@Post('sign-in/:configId')
|
||||
async create(@Param('configId') configId, @Body() body) {
|
||||
const result = await this.oauthService.signIn(body, configId);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,35 +5,47 @@ import UserResponse from './models/user_response';
|
|||
|
||||
@Injectable()
|
||||
export class GitOAuthService {
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
this.clientId = this.configService.get<string>('SSO_GIT_OAUTH2_CLIENT_ID');
|
||||
this.clientSecret = this.configService.get<string>('SSO_GIT_OAUTH2_CLIENT_SECRET');
|
||||
}
|
||||
private readonly clientId: string;
|
||||
private readonly clientSecret: string;
|
||||
constructor(private readonly configService: ConfigService) {}
|
||||
private readonly authUrl = 'https://github.com/login/oauth/access_token';
|
||||
private readonly getUserUrl = 'https://api.github.com/user';
|
||||
private readonly getUserEmailUrl = 'https://api.github.com/user/emails';
|
||||
|
||||
async #getUserDetails({ access_token }: AuthResponse): Promise<UserResponse> {
|
||||
const response: any = await got(this.getUserUrl, {
|
||||
method: 'get',
|
||||
headers: { Accept: 'application/json', Authorization: `token ${access_token}` },
|
||||
}).json();
|
||||
const { name, email } = response;
|
||||
|
||||
const { name } = response;
|
||||
let { email } = response;
|
||||
const words = name?.split(' ');
|
||||
const firstName = words?.[0] || '';
|
||||
const lastName = words?.length > 1 ? words[words.length - 1] : '';
|
||||
|
||||
if (!email) {
|
||||
// email visibility not set to public
|
||||
email = await this.#getEmailId(access_token);
|
||||
}
|
||||
|
||||
return { userSSOId: access_token, firstName, lastName, email, sso: 'git' };
|
||||
}
|
||||
|
||||
async signIn(code: string): Promise<any> {
|
||||
async #getEmailId(access_token: string) {
|
||||
const response: any = await got(this.getUserEmailUrl, {
|
||||
method: 'get',
|
||||
headers: { Accept: 'application/json', Authorization: `token ${access_token}` },
|
||||
}).json();
|
||||
|
||||
return response?.find((emails) => emails.primary)?.email;
|
||||
}
|
||||
|
||||
async signIn(code: string, configs: any): Promise<any> {
|
||||
const response: any = await got(this.authUrl, {
|
||||
method: 'post',
|
||||
headers: { Accept: 'application/json' },
|
||||
json: { client_id: this.clientId, client_secret: this.clientSecret, code },
|
||||
json: { client_id: configs.clientId, client_secret: configs.clientSecret, code },
|
||||
}).json();
|
||||
|
||||
return await this.#getUserDetails(response);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,32 +1,27 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { OAuth2Client, TokenPayload } from 'google-auth-library';
|
||||
import UserResponse from './models/user_response';
|
||||
|
||||
@Injectable()
|
||||
export class GoogleOAuthService {
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
this.clientId = this.configService.get<string>('SSO_GOOGLE_OAUTH2_CLIENT_ID');
|
||||
this.client = new OAuth2Client(this.clientId);
|
||||
}
|
||||
private readonly client: OAuth2Client;
|
||||
private readonly clientId: string;
|
||||
constructor() {}
|
||||
|
||||
#extractDetailsFromPayload(payload: TokenPayload): UserResponse {
|
||||
const email = payload.email;
|
||||
const userSSOId = payload.sub;
|
||||
const domain = payload.hd;
|
||||
|
||||
const words = payload.name?.split(' ');
|
||||
const firstName = words?.[0] || '';
|
||||
const lastName = words?.length > 1 ? words[words.length - 1] : '';
|
||||
return { userSSOId, firstName, lastName, email, domain, sso: 'google' };
|
||||
|
||||
return { userSSOId, firstName, lastName, email, sso: 'google' };
|
||||
}
|
||||
|
||||
async signIn(token: string): Promise<UserResponse> {
|
||||
const ticket = await this.client.verifyIdToken({
|
||||
async signIn(token: string, configs: any): Promise<UserResponse> {
|
||||
const client: OAuth2Client = new OAuth2Client(configs.clientId);
|
||||
const ticket = await client.verifyIdToken({
|
||||
idToken: token,
|
||||
audience: this.clientId,
|
||||
audience: configs.clientId,
|
||||
});
|
||||
const payload = ticket.getPayload();
|
||||
return this.#extractDetailsFromPayload(payload);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,5 @@ export default interface UserResponse {
|
|||
firstName?: string;
|
||||
lastName?: string;
|
||||
email: string;
|
||||
domain?: string;
|
||||
sso: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { User } from 'src/entities/user.entity';
|
||||
import { OrganizationsService } from '@services/organizations.service';
|
||||
import { OrganizationUsersService } from '@services/organization_users.service';
|
||||
|
|
@ -9,6 +8,9 @@ import { GoogleOAuthService } from './google_oauth.service';
|
|||
import { decamelizeKeys } from 'humps';
|
||||
import { GitOAuthService } from './git_oauth.service';
|
||||
import UserResponse from './models/user_response';
|
||||
import { OrganizationUser } from 'src/entities/organization_user.entity';
|
||||
import { Organization } from 'src/entities/organization.entity';
|
||||
import { SSOConfigs } from 'src/entities/sso_config.entity';
|
||||
|
||||
@Injectable()
|
||||
export class OauthService {
|
||||
|
|
@ -18,12 +20,14 @@ export class OauthService {
|
|||
private readonly jwtService: JwtService,
|
||||
private readonly organizationUsersService: OrganizationUsersService,
|
||||
private readonly googleOAuthService: GoogleOAuthService,
|
||||
private readonly gitOAuthService: GitOAuthService,
|
||||
private readonly configService: ConfigService
|
||||
private readonly gitOAuthService: GitOAuthService
|
||||
) {}
|
||||
|
||||
#isValidDomain(domain: string): boolean {
|
||||
const restrictedDomain = this.configService.get<string>('SSO_RESTRICTED_DOMAIN');
|
||||
#isValidDomain(email: string, restrictedDomain: string): boolean {
|
||||
if (!email) {
|
||||
return false;
|
||||
}
|
||||
const domain = email.substring(email.lastIndexOf('@') + 1);
|
||||
|
||||
if (!restrictedDomain) {
|
||||
return true;
|
||||
|
|
@ -34,6 +38,7 @@ export class OauthService {
|
|||
if (
|
||||
!restrictedDomain
|
||||
.split(',')
|
||||
.map((e) => e && e.trim())
|
||||
.filter((e) => !!e)
|
||||
.includes(domain)
|
||||
) {
|
||||
|
|
@ -42,63 +47,71 @@ export class OauthService {
|
|||
return true;
|
||||
}
|
||||
|
||||
async #findOrCreateUser({ userSSOId, firstName, lastName, email, sso }: UserResponse): Promise<User> {
|
||||
const organization = await this.organizationService.findFirst();
|
||||
async #findOrCreateUser({ firstName, lastName, email }: UserResponse, organization: Organization): Promise<User> {
|
||||
const { user, newUserCreated } = await this.usersService.findOrCreateByEmail(
|
||||
{ firstName, lastName, email, ssoId: userSSOId, sso },
|
||||
organization
|
||||
{ firstName, lastName, email },
|
||||
organization.id
|
||||
);
|
||||
|
||||
if (newUserCreated) {
|
||||
const organizationUser = await this.organizationUsersService.create(user, organization);
|
||||
await this.organizationUsersService.activate(organizationUser);
|
||||
} else if (userSSOId) {
|
||||
await this.usersService.updateSSODetails(user, { userSSOId, sso });
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
async #findAndActivateUser(email: string): Promise<User> {
|
||||
const user = await this.usersService.findByEmail(email);
|
||||
async #findAndActivateUser(email: string, organizationId: string): Promise<User> {
|
||||
const user = await this.usersService.findByEmail(email, organizationId);
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('Invalid credentials');
|
||||
throw new UnauthorizedException('User not exist in the organization');
|
||||
}
|
||||
const organizationUser: OrganizationUser = user.organizationUsers?.[0];
|
||||
|
||||
if (!organizationUser) {
|
||||
throw new UnauthorizedException('User not exist in the organization');
|
||||
}
|
||||
const organizationUser = user.organizationUsers[0];
|
||||
if (organizationUser.status != 'active') await this.organizationUsersService.activate(organizationUser);
|
||||
return user;
|
||||
}
|
||||
|
||||
async #generateLoginResultPayload(user: User): Promise<any> {
|
||||
const JWTPayload: JWTPayload = { username: user.id, sub: user.email, ssoId: user.ssoId, sso: user.sso };
|
||||
async #generateLoginResultPayload(user: User, organization: Organization): Promise<any> {
|
||||
const JWTPayload: JWTPayload = { username: user.id, sub: user.email, organizationId: organization.id };
|
||||
user.organizationId = organization.id;
|
||||
|
||||
return decamelizeKeys({
|
||||
id: user.id,
|
||||
auth_token: this.jwtService.sign(JWTPayload),
|
||||
email: user.email,
|
||||
first_name: user.firstName,
|
||||
last_name: user.lastName,
|
||||
organizationId: organization.id,
|
||||
organization: organization.name,
|
||||
admin: await this.usersService.hasGroup(user, 'admin'),
|
||||
group_permissions: await this.usersService.groupPermissions(user),
|
||||
app_group_permissions: await this.usersService.appGroupPermissions(user),
|
||||
});
|
||||
}
|
||||
|
||||
async signIn(ssoResponse: SSOResponse): Promise<any> {
|
||||
const ssoSignUpDisabled =
|
||||
this.configService.get<string>('SSO_DISABLE_SIGNUP') &&
|
||||
this.configService.get<string>('SSO_DISABLE_SIGNUP') === 'true';
|
||||
async signIn(ssoResponse: SSOResponse, configId: string): Promise<any> {
|
||||
const ssoConfigs: SSOConfigs = await this.organizationService.getConfigs(configId);
|
||||
|
||||
const { token, origin } = ssoResponse;
|
||||
if (!(ssoConfigs && ssoConfigs?.organization)) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
const organization = ssoConfigs.organization;
|
||||
|
||||
const { enableSignUp, domain } = ssoConfigs.organization;
|
||||
const { sso, configs } = ssoConfigs;
|
||||
const { token } = ssoResponse;
|
||||
|
||||
let userResponse: UserResponse;
|
||||
switch (origin) {
|
||||
switch (sso) {
|
||||
case 'google':
|
||||
userResponse = await this.googleOAuthService.signIn(token);
|
||||
if (!this.#isValidDomain(userResponse.domain))
|
||||
throw new UnauthorizedException(`You cannot sign in using a ${userResponse.domain} id`);
|
||||
userResponse = await this.googleOAuthService.signIn(token, configs);
|
||||
break;
|
||||
|
||||
case 'git':
|
||||
userResponse = await this.gitOAuthService.signIn(token);
|
||||
userResponse = await this.gitOAuthService.signIn(token, configs);
|
||||
break;
|
||||
|
||||
default:
|
||||
|
|
@ -108,28 +121,33 @@ export class OauthService {
|
|||
if (!(userResponse.userSSOId && userResponse.email)) {
|
||||
throw new UnauthorizedException('Invalid credentials');
|
||||
}
|
||||
const user: User = await (ssoSignUpDisabled
|
||||
? this.#findAndActivateUser(userResponse.email)
|
||||
: this.#findOrCreateUser(userResponse));
|
||||
if (!this.#isValidDomain(userResponse.email, domain)) {
|
||||
throw new UnauthorizedException(`You cannot sign in using the mail id - Domain verification failed`);
|
||||
}
|
||||
|
||||
// If name not found
|
||||
if (!(userResponse.firstName && userResponse.lastName)) {
|
||||
userResponse.firstName = userResponse.email?.split('@')?.[0];
|
||||
}
|
||||
const user: User = await (!enableSignUp
|
||||
? this.#findAndActivateUser(userResponse.email, organization.id)
|
||||
: this.#findOrCreateUser(userResponse, organization));
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedException(`Email id ${userResponse.email} is not registered`);
|
||||
}
|
||||
|
||||
return await this.#generateLoginResultPayload(user);
|
||||
return await this.#generateLoginResultPayload(user, organization);
|
||||
}
|
||||
}
|
||||
|
||||
interface SSOResponse {
|
||||
token: string;
|
||||
origin: 'google' | 'git';
|
||||
state?: string;
|
||||
redirectUri?: string;
|
||||
}
|
||||
|
||||
interface JWTPayload {
|
||||
username: string;
|
||||
sub: string;
|
||||
ssoId: string;
|
||||
sso: string;
|
||||
organizationId: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export class PopulateUserGroupsFromOrganizationRoles1632468258787 implements Mig
|
|||
const entityManager = queryRunner.manager;
|
||||
const OrganizationRepository = entityManager.getRepository(Organization);
|
||||
|
||||
const organizations = await OrganizationRepository.find();
|
||||
const organizations = await OrganizationRepository.find({ select: ['id'] });
|
||||
|
||||
for (const organization of organizations) {
|
||||
const groupPermissions = await setupInitialGroupPermissions(entityManager, organization);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ import { cloneDeep } from 'lodash';
|
|||
export class BackfillDataSourcesAndQueriesForAppVersions1639734070615 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
const entityManager = queryRunner.manager;
|
||||
const organizations = await entityManager.find(Organization);
|
||||
const organizations = await entityManager.find(Organization, {
|
||||
select: ['id', 'name'],
|
||||
});
|
||||
const nestApp = await NestFactory.createApplicationContext(AppModule);
|
||||
const dataSourcesService = nestApp.get(DataSourcesService);
|
||||
|
||||
|
|
|
|||
18
server/migrations/1645864719155-MultiOrganization.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
|
||||
|
||||
export class MultiOrganization1645864719155 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.addColumn(
|
||||
'organization_users',
|
||||
new TableColumn({
|
||||
name: 'invitation_token',
|
||||
type: 'varchar',
|
||||
isNullable: true,
|
||||
})
|
||||
);
|
||||
await queryRunner.dropColumn('users', 'sso');
|
||||
await queryRunner.dropColumn('users', 'sso_id');
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {}
|
||||
}
|
||||
65
server/migrations/1646823984673-OrganizationConfigs.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
|
||||
|
||||
export class OrganizationConfigs1646823984673 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
name: 'sso_configs',
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'uuid',
|
||||
isGenerated: true,
|
||||
default: 'gen_random_uuid()',
|
||||
isPrimary: true,
|
||||
},
|
||||
{
|
||||
name: 'organization_id',
|
||||
type: 'uuid',
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
name: 'sso',
|
||||
type: 'varchar',
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
name: 'configs',
|
||||
type: 'json',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
name: 'enabled',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
name: 'created_at',
|
||||
type: 'timestamp',
|
||||
isNullable: true,
|
||||
default: 'now()',
|
||||
},
|
||||
{
|
||||
name: 'updated_at',
|
||||
type: 'timestamp',
|
||||
isNullable: true,
|
||||
default: 'now()',
|
||||
},
|
||||
],
|
||||
}),
|
||||
true
|
||||
);
|
||||
|
||||
await queryRunner.createForeignKey(
|
||||
'sso_configs',
|
||||
new TableForeignKey({
|
||||
columnNames: ['organization_id'],
|
||||
referencedColumnNames: ['id'],
|
||||
referencedTableName: 'organizations',
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {}
|
||||
}
|
||||
15
server/migrations/1650455299630-OrganizationEnableSignup.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
|
||||
|
||||
export class OrganizationEnableSignup1650455299630 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.addColumns('organizations', [
|
||||
new TableColumn({
|
||||
name: 'enable_sign_up',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {}
|
||||
}
|
||||
105
server/migrations/1650485473528-PopulateSSOConfigs.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import { Organization } from 'src/entities/organization.entity';
|
||||
import { SSOConfigs } from 'src/entities/sso_config.entity';
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
import { EncryptionService } from 'src/services/encryption.service';
|
||||
|
||||
export class PopulateSSOConfigs1650485473528 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
const entityManager = queryRunner.manager;
|
||||
const encryptionService = new EncryptionService();
|
||||
const OrganizationRepository = entityManager.getRepository(Organization);
|
||||
|
||||
const isSingleOrganization = process.env.MULTI_ORGANIZATION !== 'true';
|
||||
const enableSignUp = process.env.SSO_DISABLE_SIGNUP !== 'true';
|
||||
const domain = process.env.SSO_RESTRICTED_DOMAIN;
|
||||
|
||||
const googleEnabled = !!process.env.SSO_GOOGLE_OAUTH2_CLIENT_ID;
|
||||
const googleConfigs = {
|
||||
clientId: process.env.SSO_GOOGLE_OAUTH2_CLIENT_ID,
|
||||
};
|
||||
|
||||
const gitEnabled = !!process.env.SSO_GIT_OAUTH2_CLIENT_ID;
|
||||
|
||||
const gitConfigs = {
|
||||
clientId: process.env.SSO_GIT_OAUTH2_CLIENT_ID,
|
||||
clientSecret:
|
||||
process.env.SSO_GIT_OAUTH2_CLIENT_SECRET &&
|
||||
(await encryptionService.encryptColumnValue(
|
||||
'ssoConfigs',
|
||||
'clientSecret',
|
||||
process.env.SSO_GIT_OAUTH2_CLIENT_SECRET
|
||||
)),
|
||||
};
|
||||
|
||||
const passwordEnabled = process.env.DISABLE_PASSWORD_LOGIN !== 'true';
|
||||
|
||||
const organizations: Organization[] = await OrganizationRepository.find({
|
||||
relations: ['ssoConfigs'],
|
||||
select: ['ssoConfigs', 'id'],
|
||||
});
|
||||
|
||||
if (organizations && organizations.length > 0) {
|
||||
for (const organization of organizations) {
|
||||
await OrganizationRepository.update({ id: organization.id }, { enableSignUp, ...(domain ? { domain } : {}) });
|
||||
// adding form configs for organizations which does not have any
|
||||
if (
|
||||
!organization.ssoConfigs?.some((og) => {
|
||||
og?.sso === 'form';
|
||||
})
|
||||
) {
|
||||
await entityManager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(SSOConfigs, ['organizationId', 'sso', 'enabled'])
|
||||
.values({
|
||||
organizationId: organization.id,
|
||||
sso: 'form',
|
||||
enabled: !isSingleOrganization ? true : passwordEnabled,
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
if (
|
||||
isSingleOrganization &&
|
||||
googleEnabled &&
|
||||
!organization.ssoConfigs?.some((og) => {
|
||||
og?.sso === 'google';
|
||||
})
|
||||
) {
|
||||
await entityManager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(SSOConfigs, ['organizationId', 'sso', 'enabled', 'configs'])
|
||||
.values({
|
||||
organizationId: organization.id,
|
||||
sso: 'google',
|
||||
enabled: googleEnabled,
|
||||
configs: googleConfigs,
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
if (
|
||||
isSingleOrganization &&
|
||||
gitEnabled &&
|
||||
!organization.ssoConfigs?.some((og) => {
|
||||
og?.sso === 'git';
|
||||
})
|
||||
) {
|
||||
await entityManager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(SSOConfigs, ['organizationId', 'sso', 'enabled', 'configs'])
|
||||
.values({
|
||||
organizationId: organization.id,
|
||||
sso: 'git',
|
||||
enabled: gitEnabled,
|
||||
configs: gitConfigs,
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {}
|
||||
}
|
||||
|
|
@ -77,6 +77,7 @@ const imports = [
|
|||
MetaModule,
|
||||
LibraryAppModule,
|
||||
GroupPermissionsModule,
|
||||
EventsModule,
|
||||
];
|
||||
|
||||
if (process.env.SERVE_CLIENT !== 'false') {
|
||||
|
|
@ -98,7 +99,7 @@ if (process.env.APM_VENDOR == 'sentry') {
|
|||
}
|
||||
|
||||
if (process.env.COMMENT_FEATURE_ENABLE !== 'false') {
|
||||
imports.unshift(CommentModule, ThreadModule, EventsModule);
|
||||
imports.unshift(CommentModule, ThreadModule);
|
||||
}
|
||||
|
||||
@Module({
|
||||
|
|
|
|||
|
|
@ -1,22 +1,30 @@
|
|||
import { Controller, Get, Request, Post, UseGuards, Body, Param, BadRequestException } from '@nestjs/common';
|
||||
import { User } from 'src/decorators/user.decorator';
|
||||
import { JwtAuthGuard } from '../../src/modules/auth/jwt-auth.guard';
|
||||
import { AppAuthenticationDto, AppForgotPasswordDto, AppPasswordResetDto } from '@dto/app-authentication.dto';
|
||||
import { Controller, Get, Request, Post, UseGuards, Body } from '@nestjs/common';
|
||||
import { PasswordLoginDisabledGuard } from 'src/modules/auth/password-login-disabled.guard';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(private authService: AuthService) {}
|
||||
|
||||
@UseGuards(PasswordLoginDisabledGuard)
|
||||
@Post('authenticate')
|
||||
async login(@Body() appAuthDto: AppAuthenticationDto) {
|
||||
return this.authService.login(appAuthDto);
|
||||
@Post(['authenticate', 'authenticate/:organizationId'])
|
||||
async login(@Body() appAuthDto: AppAuthenticationDto, @Param('organizationId') organizationId) {
|
||||
return this.authService.login(appAuthDto.email, appAuthDto.password, organizationId);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get('switch/:organizationId')
|
||||
async switch(@Param('organizationId') organizationId, @User() user) {
|
||||
if (!organizationId) {
|
||||
throw new BadRequestException();
|
||||
}
|
||||
return await this.authService.switchOrganization(organizationId, user);
|
||||
}
|
||||
|
||||
@UseGuards(PasswordLoginDisabledGuard)
|
||||
@Post('signup')
|
||||
async signup(@Body() appAuthDto: AppAuthenticationDto) {
|
||||
return this.authService.signup(appAuthDto);
|
||||
return this.authService.signup(appAuthDto.email);
|
||||
}
|
||||
|
||||
@Post('/forgot_password')
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export class AppUsersController {
|
|||
const { role } = params;
|
||||
|
||||
const app = await this.appsService.find(appId);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, { id: appId });
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, appId);
|
||||
|
||||
if (!ability.can('createUsers', app)) {
|
||||
throw new ForbiddenException('you do not have permissions to perform this action');
|
||||
|
|
|
|||
|
|
@ -1,16 +1,4 @@
|
|||
import {
|
||||
Controller,
|
||||
ForbiddenException,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Query,
|
||||
Request,
|
||||
UseGuards,
|
||||
Body,
|
||||
} from '@nestjs/common';
|
||||
import { Controller, ForbiddenException, Get, Param, Post, Put, Delete, Query, UseGuards, Body } from '@nestjs/common';
|
||||
import { JwtAuthGuard } from '../../src/modules/auth/jwt-auth.guard';
|
||||
import { AppsService } from '../services/apps.service';
|
||||
import { camelizeKeys, decamelizeKeys } from 'humps';
|
||||
|
|
@ -19,6 +7,7 @@ import { AppAuthGuard } from 'src/modules/auth/app-auth.guard';
|
|||
import { FoldersService } from '@services/folders.service';
|
||||
import { App } from 'src/entities/app.entity';
|
||||
import { AppImportExportService } from '@services/app_import_export.service';
|
||||
import { User } from 'src/decorators/user.decorator';
|
||||
import { AppUpdateDto } from '@dto/app-update.dto';
|
||||
import { VersionCreateDto } from '@dto/version-create.dto';
|
||||
|
||||
|
|
@ -33,26 +22,26 @@ export class AppsController {
|
|||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post()
|
||||
async create(@Request() req) {
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {});
|
||||
async create(@User() user) {
|
||||
const ability = await this.appsAbilityFactory.appsActions(user);
|
||||
|
||||
if (!ability.can('createApp', App)) {
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
}
|
||||
const app = await this.appsService.create(req.user);
|
||||
const app = await this.appsService.create(user);
|
||||
|
||||
const appUpdateDto = new AppUpdateDto();
|
||||
appUpdateDto.slug = app.id;
|
||||
await this.appsService.update(req.user, app.id, appUpdateDto);
|
||||
await this.appsService.update(user, app.id, appUpdateDto);
|
||||
|
||||
return decamelizeKeys(app);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get(':id')
|
||||
async show(@Request() req, @Param() params) {
|
||||
const app = await this.appsService.find(params.id);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, params);
|
||||
async show(@User() user, @Param('id') id) {
|
||||
const app = await this.appsService.find(id);
|
||||
const ability = await this.appsAbilityFactory.appsActions(user, id);
|
||||
|
||||
if (!ability.can('viewApp', app)) {
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
|
|
@ -86,19 +75,17 @@ export class AppsController {
|
|||
|
||||
@UseGuards(AppAuthGuard) // This guard will allow access for unauthenticated user if the app is public
|
||||
@Get('slugs/:slug')
|
||||
async appFromSlug(@Request() req, @Param() params) {
|
||||
if (req.user) {
|
||||
const app = await this.appsService.findBySlug(params.slug);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {
|
||||
id: app.id,
|
||||
});
|
||||
async appFromSlug(@User() user, @Param('slug') slug) {
|
||||
if (user) {
|
||||
const app = await this.appsService.findBySlug(slug);
|
||||
const ability = await this.appsAbilityFactory.appsActions(user, app.id);
|
||||
|
||||
if (!ability.can('viewApp', app)) {
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
}
|
||||
}
|
||||
|
||||
const app = await this.appsService.findBySlug(params.slug);
|
||||
const app = await this.appsService.findBySlug(slug);
|
||||
const versionToLoad = app.currentVersionId
|
||||
? await this.appsService.findVersion(app.currentVersionId)
|
||||
: await this.appsService.findVersion(app.editingVersion?.id);
|
||||
|
|
@ -117,15 +104,15 @@ export class AppsController {
|
|||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Put(':id')
|
||||
async update(@Request() req, @Param() params, @Body('app') appUpdateDto: AppUpdateDto) {
|
||||
const app = await this.appsService.find(params.id);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, params);
|
||||
async update(@User() user, @Param('id') id, @Body('app') appUpdateDto: AppUpdateDto) {
|
||||
const app = await this.appsService.find(id);
|
||||
const ability = await this.appsAbilityFactory.appsActions(user, id);
|
||||
|
||||
if (!ability.can('updateParams', app)) {
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
}
|
||||
|
||||
const result = await this.appsService.update(req.user, params.id, appUpdateDto);
|
||||
const result = await this.appsService.update(user, id, appUpdateDto);
|
||||
const response = decamelizeKeys(result);
|
||||
|
||||
return response;
|
||||
|
|
@ -133,15 +120,15 @@ export class AppsController {
|
|||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post(':id/clone')
|
||||
async clone(@Request() req, @Param() params) {
|
||||
const existingApp = await this.appsService.find(params.id);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, params);
|
||||
async clone(@User() user, @Param('id') id) {
|
||||
const existingApp = await this.appsService.find(id);
|
||||
const ability = await this.appsAbilityFactory.appsActions(user, id);
|
||||
|
||||
if (!ability.can('cloneApp', existingApp)) {
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
}
|
||||
|
||||
const result = await this.appsService.clone(existingApp, req.user);
|
||||
const result = await this.appsService.clone(existingApp, user);
|
||||
const response = decamelizeKeys(result);
|
||||
|
||||
return response;
|
||||
|
|
@ -149,15 +136,15 @@ export class AppsController {
|
|||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get(':id/export')
|
||||
async export(@Request() req, @Param() params) {
|
||||
const appToExport = await this.appsService.find(params.id);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, params);
|
||||
async export(@User() user, @Param('id') id) {
|
||||
const appToExport = await this.appsService.find(id);
|
||||
const ability = await this.appsAbilityFactory.appsActions(user, id);
|
||||
|
||||
if (!ability.can('viewApp', appToExport)) {
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
}
|
||||
|
||||
const app = await this.appImportExportService.export(req.user, params.id);
|
||||
const app = await this.appImportExportService.export(user, id);
|
||||
return {
|
||||
...app,
|
||||
tooljetVersion: globalThis.TOOLJET_VERSION,
|
||||
|
|
@ -166,28 +153,28 @@ export class AppsController {
|
|||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post('/import')
|
||||
async import(@Request() req, @Body() body) {
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {});
|
||||
async import(@User() user, @Body() body) {
|
||||
const ability = await this.appsAbilityFactory.appsActions(user);
|
||||
|
||||
if (!ability.can('createApp', App)) {
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
}
|
||||
await this.appImportExportService.import(req.user, body);
|
||||
await this.appImportExportService.import(user, body);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Delete(':id')
|
||||
async delete(@Request() req, @Param() params) {
|
||||
const app = await this.appsService.find(params.id);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, params);
|
||||
async delete(@User() user, @Param('id') id) {
|
||||
const app = await this.appsService.find(id);
|
||||
const ability = await this.appsAbilityFactory.appsActions(user, id);
|
||||
|
||||
if (!ability.can('deleteApp', app)) {
|
||||
throw new ForbiddenException('Only administrators are allowed to delete apps.');
|
||||
}
|
||||
|
||||
const result = await this.appsService.delete(params.id);
|
||||
const result = await this.appsService.delete(id);
|
||||
const response = decamelizeKeys(result);
|
||||
|
||||
return response;
|
||||
|
|
@ -195,7 +182,7 @@ export class AppsController {
|
|||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get()
|
||||
async index(@Request() req, @Query() query) {
|
||||
async index(@User() user, @Query() query) {
|
||||
const page = query.page;
|
||||
const folderId = query.folder;
|
||||
const searchKey = query.searchKey || '';
|
||||
|
|
@ -205,15 +192,15 @@ export class AppsController {
|
|||
|
||||
if (folderId) {
|
||||
const folder = await this.foldersService.findOne(folderId);
|
||||
apps = await this.foldersService.getAppsFor(req.user, folder, page, searchKey);
|
||||
totalFolderCount = await this.foldersService.userAppCount(req.user, folder, searchKey);
|
||||
apps = await this.foldersService.getAppsFor(user, folder, page, searchKey);
|
||||
totalFolderCount = await this.foldersService.userAppCount(user, folder, searchKey);
|
||||
} else {
|
||||
apps = await this.appsService.all(req.user, page, searchKey);
|
||||
apps = await this.appsService.all(user, page, searchKey);
|
||||
}
|
||||
//remove password from user info
|
||||
apps.forEach((app) => (app.user.password = undefined));
|
||||
|
||||
const totalCount = await this.appsService.count(req.user, searchKey);
|
||||
const totalCount = await this.appsService.count(user, searchKey);
|
||||
|
||||
const totalPageCount = folderId ? totalFolderCount : totalCount;
|
||||
|
||||
|
|
@ -235,44 +222,44 @@ export class AppsController {
|
|||
// deprecated
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get(':id/users')
|
||||
async fetchUsers(@Request() req, @Param() params) {
|
||||
const app = await this.appsService.find(params.id);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, params);
|
||||
async fetchUsers(@User() user, @Param('id') id) {
|
||||
const app = await this.appsService.find(id);
|
||||
const ability = await this.appsAbilityFactory.appsActions(user, id);
|
||||
|
||||
if (!ability.can('fetchUsers', app)) {
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
}
|
||||
|
||||
const result = await this.appsService.fetchUsers(req.user, params.id);
|
||||
const result = await this.appsService.fetchUsers(user, id);
|
||||
return decamelizeKeys({ users: result });
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get(':id/versions')
|
||||
async fetchVersions(@Request() req, @Param() params) {
|
||||
const app = await this.appsService.find(params.id);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, params);
|
||||
async fetchVersions(@User() user, @Param('id') id) {
|
||||
const app = await this.appsService.find(id);
|
||||
const ability = await this.appsAbilityFactory.appsActions(user, id);
|
||||
|
||||
if (!ability.can('fetchVersions', app)) {
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
}
|
||||
|
||||
const result = await this.appsService.fetchVersions(req.user, params.id);
|
||||
const result = await this.appsService.fetchVersions(user, id);
|
||||
return { versions: result };
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post(':id/versions')
|
||||
async createVersion(@Request() req, @Param() params, @Body() versionCreateDto: VersionCreateDto) {
|
||||
const app = await this.appsService.find(params.id);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, params);
|
||||
async createVersion(@User() user, @Param('id') id, @Body() versionCreateDto: VersionCreateDto) {
|
||||
const app = await this.appsService.find(id);
|
||||
const ability = await this.appsAbilityFactory.appsActions(user, id);
|
||||
|
||||
if (!ability.can('createVersions', app)) {
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
}
|
||||
|
||||
const appUser = await this.appsService.createVersion(
|
||||
req.user,
|
||||
user,
|
||||
app,
|
||||
versionCreateDto.versionName,
|
||||
versionCreateDto.versionFromId
|
||||
|
|
@ -282,38 +269,38 @@ export class AppsController {
|
|||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get(':id/versions/:versionId')
|
||||
async version(@Request() req, @Param() params) {
|
||||
const app = await this.appsService.find(params.id);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, params);
|
||||
async version(@User() user, @Param('id') id, @Param('versionId') versionId) {
|
||||
const app = await this.appsService.find(id);
|
||||
const ability = await this.appsAbilityFactory.appsActions(user, id);
|
||||
|
||||
if (!ability.can('fetchVersions', app)) {
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
}
|
||||
|
||||
const appVersion = await this.appsService.findVersion(params.versionId);
|
||||
const appVersion = await this.appsService.findVersion(versionId);
|
||||
|
||||
return { ...appVersion, data_queries: appVersion.dataQueries };
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Put(':id/versions/:versionId')
|
||||
async updateVersion(@Request() req, @Param() params, @Body('definition') definition) {
|
||||
const version = await this.appsService.findVersion(params.versionId);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, params);
|
||||
async updateVersion(@User() user, @Param('id') id, @Param('versionId') versionId, @Body('definition') definition) {
|
||||
const version = await this.appsService.findVersion(versionId);
|
||||
const ability = await this.appsAbilityFactory.appsActions(user, id);
|
||||
|
||||
if (!ability.can('updateVersions', version.app)) {
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
}
|
||||
|
||||
const appUser = await this.appsService.updateVersion(req.user, version, definition);
|
||||
const appUser = await this.appsService.updateVersion(user, version, definition);
|
||||
return decamelizeKeys(appUser);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Delete(':id/versions/:versionId')
|
||||
async deleteVersion(@Request() req, @Param() params) {
|
||||
const version = await this.appsService.findVersion(params.versionId);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, params);
|
||||
async deleteVersion(@User() user, @Param('id') id, @Param('versionId') versionId) {
|
||||
const version = await this.appsService.findVersion(versionId);
|
||||
const ability = await this.appsAbilityFactory.appsActions(user, id);
|
||||
|
||||
if (!version || !ability.can('deleteVersions', version.app)) {
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
|
|
@ -324,9 +311,9 @@ export class AppsController {
|
|||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Put(':id/icons')
|
||||
async updateIcon(@Request() req, @Param() params, @Body('icon') icon) {
|
||||
const app = await this.appsService.find(params.id);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, params);
|
||||
async updateIcon(@User() user, @Param('id') id, @Body('icon') icon) {
|
||||
const app = await this.appsService.find(id);
|
||||
const ability = await this.appsAbilityFactory.appsActions(user, id);
|
||||
|
||||
if (!ability.can('updateIcon', app)) {
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
|
|
@ -334,7 +321,7 @@ export class AppsController {
|
|||
|
||||
const appUpdateDto = new AppUpdateDto();
|
||||
appUpdateDto.icon = icon;
|
||||
const appUser = await this.appsService.update(req.user, params.id, appUpdateDto);
|
||||
const appUser = await this.appsService.update(user, id, appUpdateDto);
|
||||
return decamelizeKeys(appUser);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import {
|
||||
Controller,
|
||||
Request,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
|
|
@ -17,34 +16,35 @@ import { Comment } from '../entities/comment.entity';
|
|||
import { Thread } from '../entities/thread.entity';
|
||||
import { JwtAuthGuard } from '../../src/modules/auth/jwt-auth.guard';
|
||||
import { CommentsAbilityFactory } from 'src/modules/casl/abilities/comments-ability.factory';
|
||||
import { User } from 'src/decorators/user.decorator';
|
||||
|
||||
@Controller('comments')
|
||||
export class CommentController {
|
||||
constructor(private commentService: CommentService, private commentsAbilityFactory: CommentsAbilityFactory) {}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post('create')
|
||||
public async createComment(@Request() req, @Body() createCommentDto: CreateCommentDto): Promise<Comment> {
|
||||
@Post()
|
||||
public async createComment(@User() user, @Body() createCommentDto: CreateCommentDto): Promise<Comment> {
|
||||
const _response = await Thread.findOne({
|
||||
where: { id: createCommentDto.threadId },
|
||||
});
|
||||
const ability = await this.commentsAbilityFactory.appsActions(req.user, { id: _response.appId });
|
||||
const ability = await this.commentsAbilityFactory.appsActions(user, { id: _response.appId });
|
||||
|
||||
if (!ability.can('createComment', Comment)) {
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
}
|
||||
|
||||
const comment = await this.commentService.createComment(createCommentDto, req.user.id, req.user.organization.id);
|
||||
const comment = await this.commentService.createComment(createCommentDto, user.id, user.organizationId);
|
||||
return comment;
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get('/:threadId/all')
|
||||
public async getComments(@Request() req, @Param('threadId') threadId: string, @Query() query): Promise<Comment[]> {
|
||||
public async getComments(@User() user, @Param('threadId') threadId: string, @Query() query): Promise<Comment[]> {
|
||||
const _response = await Thread.findOne({
|
||||
where: { id: threadId },
|
||||
});
|
||||
const ability = await this.commentsAbilityFactory.appsActions(req.user, { id: _response.appId });
|
||||
const ability = await this.commentsAbilityFactory.appsActions(user, { id: _response.appId });
|
||||
|
||||
if (!ability.can('fetchComments', Comment)) {
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
|
|
@ -56,18 +56,13 @@ export class CommentController {
|
|||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get('/:appId/notifications')
|
||||
public async getNotifications(@Request() req, @Param('appId') appId: string, @Query() query): Promise<Comment[]> {
|
||||
const ability = await this.commentsAbilityFactory.appsActions(req.user, { id: appId });
|
||||
public async getNotifications(@User() user, @Param('appId') appId: string, @Query() query): Promise<Comment[]> {
|
||||
const ability = await this.commentsAbilityFactory.appsActions(user, { id: appId });
|
||||
|
||||
if (!ability.can('fetchComments', Comment)) {
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
}
|
||||
const comments = await this.commentService.getNotifications(
|
||||
appId,
|
||||
req.user.id,
|
||||
query.isResolved,
|
||||
query.appVersionsId
|
||||
);
|
||||
const comments = await this.commentService.getNotifications(appId, user.id, query.isResolved, query.appVersionsId);
|
||||
return comments;
|
||||
}
|
||||
|
||||
|
|
@ -79,9 +74,9 @@ export class CommentController {
|
|||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Patch('/edit/:commentId')
|
||||
@Patch('/:commentId')
|
||||
public async editComment(
|
||||
@Request() req,
|
||||
@User() user,
|
||||
@Body() updateCommentDto: UpdateCommentDto,
|
||||
@Param('commentId') commentId: string
|
||||
): Promise<Comment> {
|
||||
|
|
@ -89,7 +84,7 @@ export class CommentController {
|
|||
where: { id: commentId },
|
||||
relations: ['thread'],
|
||||
});
|
||||
const ability = await this.commentsAbilityFactory.appsActions(req.user, { id: _response.thread.appId });
|
||||
const ability = await this.commentsAbilityFactory.appsActions(user, { id: _response.thread.appId });
|
||||
|
||||
if (!ability.can('updateComment', Comment)) {
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
|
|
@ -99,13 +94,13 @@ export class CommentController {
|
|||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Delete('/delete/:commentId')
|
||||
public async deleteComment(@Request() req, @Param('commentId') commentId: string) {
|
||||
@Delete('/:commentId')
|
||||
public async deleteComment(@User() user, @Param('commentId') commentId: string) {
|
||||
const _response = await Comment.findOne({
|
||||
where: { id: commentId },
|
||||
relations: ['thread'],
|
||||
});
|
||||
const ability = await this.commentsAbilityFactory.appsActions(req.user, { id: _response.thread.appId });
|
||||
const ability = await this.commentsAbilityFactory.appsActions(user, { id: _response.thread.appId });
|
||||
|
||||
if (!ability.can('deleteComment', Comment)) {
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import {
|
|||
Patch,
|
||||
Delete,
|
||||
Query,
|
||||
Request,
|
||||
UseGuards,
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
|
|
@ -19,6 +18,7 @@ import { QueryAuthGuard } from 'src/modules/auth/query-auth.guard';
|
|||
import { AppsAbilityFactory } from 'src/modules/casl/abilities/apps-ability.factory';
|
||||
import { AppsService } from '@services/apps.service';
|
||||
import { CreateDataQueryDto, UpdateDataQueryDto } from '@dto/data-query.dto';
|
||||
import { User } from 'src/decorators/user.decorator';
|
||||
|
||||
@Controller('data_queries')
|
||||
export class DataQueriesController {
|
||||
|
|
@ -31,17 +31,15 @@ export class DataQueriesController {
|
|||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get()
|
||||
async index(@Request() req, @Query() query) {
|
||||
async index(@User() user, @Query() query) {
|
||||
const app = await this.appsService.find(query.app_id);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {
|
||||
id: query.app_id,
|
||||
});
|
||||
const ability = await this.appsAbilityFactory.appsActions(user, query.app_id);
|
||||
|
||||
if (!ability.can('getQueries', app)) {
|
||||
throw new ForbiddenException('you do not have permissions to perform this action');
|
||||
}
|
||||
|
||||
const queries = await this.dataQueriesService.all(req.user, query);
|
||||
const queries = await this.dataQueriesService.all(user, query);
|
||||
const seralizedQueries = [];
|
||||
|
||||
// serialize
|
||||
|
|
@ -59,16 +57,14 @@ export class DataQueriesController {
|
|||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post()
|
||||
async create(@Request() req, @Body() dataQueryDto: CreateDataQueryDto): Promise<object> {
|
||||
async create(@User() user, @Body() dataQueryDto: CreateDataQueryDto): Promise<object> {
|
||||
const { kind, name, options, app_id, app_version_id, data_source_id } = dataQueryDto;
|
||||
const appId = app_id;
|
||||
const appVersionId = app_version_id;
|
||||
const dataSourceId = data_source_id;
|
||||
|
||||
const app = await this.appsService.find(appId);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {
|
||||
id: appId,
|
||||
});
|
||||
const ability = await this.appsAbilityFactory.appsActions(user, appId);
|
||||
|
||||
if (!ability.can('createQuery', app)) {
|
||||
throw new ForbiddenException('you do not have permissions to perform this action');
|
||||
|
|
@ -83,7 +79,7 @@ export class DataQueriesController {
|
|||
}
|
||||
|
||||
const dataQuery = await this.dataQueriesService.create(
|
||||
req.user,
|
||||
user,
|
||||
name,
|
||||
kind,
|
||||
options,
|
||||
|
|
@ -96,32 +92,28 @@ export class DataQueriesController {
|
|||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Patch(':id')
|
||||
async update(@Request() req, @Param() params, @Body() updateDataQueryDto: UpdateDataQueryDto) {
|
||||
async update(@User() user, @Param() params, @Body() updateDataQueryDto: UpdateDataQueryDto) {
|
||||
const { name, options } = updateDataQueryDto;
|
||||
const dataQueryId = params.id;
|
||||
|
||||
const dataQuery = await this.dataQueriesService.findOne(dataQueryId);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {
|
||||
id: dataQuery.appId,
|
||||
});
|
||||
const ability = await this.appsAbilityFactory.appsActions(user, dataQuery.appId);
|
||||
|
||||
if (!ability.can('updateQuery', dataQuery.app)) {
|
||||
throw new ForbiddenException('you do not have permissions to perform this action');
|
||||
}
|
||||
|
||||
const result = await this.dataQueriesService.update(req.user, dataQueryId, name, options);
|
||||
const result = await this.dataQueriesService.update(user, dataQueryId, name, options);
|
||||
return decamelizeKeys(result);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Delete(':id')
|
||||
async delete(@Request() req, @Param() params) {
|
||||
async delete(@User() user, @Param() params) {
|
||||
const dataQueryId = params.id;
|
||||
|
||||
const dataQuery = await this.dataQueriesService.findOne(dataQueryId);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {
|
||||
id: dataQuery.appId,
|
||||
});
|
||||
const ability = await this.appsAbilityFactory.appsActions(user, dataQuery.appId);
|
||||
|
||||
if (!ability.can('deleteQuery', dataQuery.app)) {
|
||||
throw new ForbiddenException('you do not have permissions to perform this action');
|
||||
|
|
@ -133,16 +125,13 @@ export class DataQueriesController {
|
|||
|
||||
@UseGuards(QueryAuthGuard)
|
||||
@Post(':id/run')
|
||||
async runQuery(@Request() req, @Param() params, @Body() updateDataQueryDto: UpdateDataQueryDto) {
|
||||
const dataQueryId = params.id;
|
||||
async runQuery(@User() user, @Param('id') dataQueryId, @Body() updateDataQueryDto: UpdateDataQueryDto) {
|
||||
const { options } = updateDataQueryDto;
|
||||
|
||||
const dataQuery = await this.dataQueriesService.findOne(dataQueryId);
|
||||
|
||||
if (req.user) {
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {
|
||||
id: dataQuery.appId,
|
||||
});
|
||||
if (user) {
|
||||
const ability = await this.appsAbilityFactory.appsActions(user, dataQuery.appId);
|
||||
|
||||
if (!ability.can('runQuery', dataQuery.app)) {
|
||||
throw new ForbiddenException('you do not have permissions to perform this action');
|
||||
|
|
@ -152,7 +141,7 @@ export class DataQueriesController {
|
|||
let result = {};
|
||||
|
||||
try {
|
||||
result = await this.dataQueriesService.runQuery(req.user, dataQuery, options);
|
||||
result = await this.dataQueriesService.runQuery(user, dataQuery, options);
|
||||
} catch (error) {
|
||||
if (error.constructor.name === 'QueryError') {
|
||||
result = {
|
||||
|
|
@ -177,7 +166,7 @@ export class DataQueriesController {
|
|||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post('/preview')
|
||||
async previewQuery(@Request() req, @Body() updateDataQueryDto: UpdateDataQueryDto) {
|
||||
async previewQuery(@User() user, @Body() updateDataQueryDto: UpdateDataQueryDto) {
|
||||
const { options, query } = updateDataQueryDto;
|
||||
const dataQueryEntity = {
|
||||
...query,
|
||||
|
|
@ -185,9 +174,7 @@ export class DataQueriesController {
|
|||
};
|
||||
|
||||
if (dataQueryEntity.dataSource) {
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {
|
||||
id: dataQueryEntity.dataSource.appId,
|
||||
});
|
||||
const ability = await this.appsAbilityFactory.appsActions(user, dataQueryEntity.dataSource.appId);
|
||||
|
||||
if (!ability.can('previewQuery', dataQueryEntity.dataSource.app)) {
|
||||
throw new ForbiddenException('you do not have permissions to perform this action');
|
||||
|
|
@ -197,7 +184,7 @@ export class DataQueriesController {
|
|||
let result = {};
|
||||
|
||||
try {
|
||||
result = await this.dataQueriesService.runQuery(req.user, dataQueryEntity, options);
|
||||
result = await this.dataQueriesService.runQuery(user, dataQueryEntity, options);
|
||||
} catch (error) {
|
||||
if (error.constructor.name === 'QueryError') {
|
||||
result = {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import {
|
|||
Delete,
|
||||
Put,
|
||||
Query,
|
||||
Request,
|
||||
UseGuards,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
|
|
@ -25,6 +24,7 @@ import {
|
|||
TestDataSourceDto,
|
||||
UpdateDataSourceDto,
|
||||
} from '@dto/data-source.dto';
|
||||
import { User } from 'src/decorators/user.decorator';
|
||||
|
||||
@Controller('data_sources')
|
||||
export class DataSourcesController {
|
||||
|
|
@ -37,17 +37,15 @@ export class DataSourcesController {
|
|||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get()
|
||||
async index(@Request() req, @Query() query) {
|
||||
async index(@User() user, @Query() query) {
|
||||
const app = await this.appsService.find(query.app_id);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {
|
||||
id: app.id,
|
||||
});
|
||||
const ability = await this.appsAbilityFactory.appsActions(user, app.id);
|
||||
|
||||
if (!ability.can('getDataSources', app)) {
|
||||
throw new ForbiddenException('you do not have permissions to perform this action');
|
||||
}
|
||||
|
||||
const dataSources = await this.dataSourcesService.all(req.user, query);
|
||||
const dataSources = await this.dataSourcesService.all(user, query);
|
||||
const response = decamelizeKeys({ data_sources: dataSources });
|
||||
|
||||
return response;
|
||||
|
|
@ -55,15 +53,13 @@ export class DataSourcesController {
|
|||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post()
|
||||
async create(@Request() req, @Body() createDataSourceDto: CreateDataSourceDto) {
|
||||
async create(@User() user, @Body() createDataSourceDto: CreateDataSourceDto) {
|
||||
const { kind, name, options, app_id, app_version_id } = createDataSourceDto;
|
||||
const appId = app_id;
|
||||
const appVersionId = app_version_id;
|
||||
|
||||
const app = await this.appsService.find(appId);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {
|
||||
id: appId,
|
||||
});
|
||||
const ability = await this.appsAbilityFactory.appsActions(user, appId);
|
||||
|
||||
if (!ability.can('createDataSource', app)) {
|
||||
throw new ForbiddenException('you do not have permissions to perform this action');
|
||||
|
|
@ -75,16 +71,13 @@ export class DataSourcesController {
|
|||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Put(':id')
|
||||
async update(@Request() req, @Param() params, @Body() updateDataSourceDto: UpdateDataSourceDto) {
|
||||
const dataSourceId = params.id;
|
||||
async update(@User() user, @Param('id') dataSourceId, @Body() updateDataSourceDto: UpdateDataSourceDto) {
|
||||
const { name, options } = updateDataSourceDto;
|
||||
|
||||
const dataSource = await this.dataSourcesService.findOne(dataSourceId);
|
||||
|
||||
const app = await this.appsService.find(dataSource.appId);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {
|
||||
id: app.id,
|
||||
});
|
||||
const ability = await this.appsAbilityFactory.appsActions(user, app.id);
|
||||
|
||||
if (!ability.can('updateDataSource', app)) {
|
||||
throw new ForbiddenException('you do not have permissions to perform this action');
|
||||
|
|
@ -96,21 +89,17 @@ export class DataSourcesController {
|
|||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Delete(':id')
|
||||
async delete(@Request() req, @Param() params) {
|
||||
const dataSourceId = params.id;
|
||||
|
||||
async delete(@User() user, @Param('id') dataSourceId) {
|
||||
const dataSource = await this.dataSourcesService.findOne(dataSourceId);
|
||||
|
||||
const app = await this.appsService.find(dataSource.appId);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {
|
||||
id: app.id,
|
||||
});
|
||||
const ability = await this.appsAbilityFactory.appsActions(user, app.id);
|
||||
|
||||
if (!ability.can('deleteDataSource', dataSource.app)) {
|
||||
throw new ForbiddenException('you do not have permissions to perform this action');
|
||||
}
|
||||
|
||||
const result = await this.dataSourcesService.delete(params.id);
|
||||
const result = await this.dataSourcesService.delete(dataSourceId);
|
||||
if (result.affected == 1) {
|
||||
return;
|
||||
} else {
|
||||
|
|
@ -120,14 +109,14 @@ export class DataSourcesController {
|
|||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post('test_connection')
|
||||
async testConnection(@Request() req, @Body() testDataSourceDto: TestDataSourceDto) {
|
||||
const { kind, options } = req.body;
|
||||
async testConnection(@User() user, @Body() testDataSourceDto: TestDataSourceDto) {
|
||||
const { kind, options } = testDataSourceDto;
|
||||
return await this.dataSourcesService.testConnection(kind, options);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post('fetch_oauth2_base_url')
|
||||
async getAuthUrl(@Request() req, @Body() getDataSourceOauthUrlDto: GetDataSourceOauthUrlDto) {
|
||||
async getAuthUrl(@User() user, @Body() getDataSourceOauthUrlDto: GetDataSourceOauthUrlDto) {
|
||||
const { provider } = getDataSourceOauthUrlDto;
|
||||
return await this.dataSourcesService.getAuthUrl(provider);
|
||||
}
|
||||
|
|
@ -135,7 +124,7 @@ export class DataSourcesController {
|
|||
@UseGuards(JwtAuthGuard)
|
||||
@Post(':id/authorize_oauth2')
|
||||
async authorizeOauth2(
|
||||
@Request() req,
|
||||
@User() user,
|
||||
@Param() params,
|
||||
@Body() authorizeDataSourceOauthDto: AuthorizeDataSourceOauthDto
|
||||
) {
|
||||
|
|
@ -145,9 +134,7 @@ export class DataSourcesController {
|
|||
const dataSource = await this.dataSourcesService.findOne(dataSourceId);
|
||||
|
||||
const app = await this.appsService.find(dataSource.appId);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {
|
||||
id: app.id,
|
||||
});
|
||||
const ability = await this.appsAbilityFactory.appsActions(user, app.id);
|
||||
|
||||
if (!ability.can('authorizeOauthForSource', app)) {
|
||||
throw new ForbiddenException('you do not have permissions to perform this action');
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { Controller, Body, Post, Get, Put, Delete, Request, UseGuards, Param } from '@nestjs/common';
|
||||
import { Controller, Body, Post, Get, Put, Delete, UseGuards, Param } from '@nestjs/common';
|
||||
import { decamelizeKeys } from 'humps';
|
||||
import { JwtAuthGuard } from '../../src/modules/auth/jwt-auth.guard';
|
||||
import { GroupPermissionsService } from '../services/group_permissions.service';
|
||||
import { PoliciesGuard } from 'src/modules/casl/policies.guard';
|
||||
import { CheckPolicies } from 'src/modules/casl/check_policies.decorator';
|
||||
import { AppAbility } from 'src/modules/casl/casl-ability.factory';
|
||||
import { User } from 'src/entities/user.entity';
|
||||
import { User } from 'src/decorators/user.decorator';
|
||||
import { User as UserEntity } from 'src/entities/user.entity';
|
||||
import { CreateGroupPermissionDto, UpdateGroupPermissionDto } from '@dto/group-permission.dto';
|
||||
|
||||
@Controller('group_permissions')
|
||||
|
|
@ -13,35 +14,35 @@ export class GroupPermissionsController {
|
|||
constructor(private groupPermissionsService: GroupPermissionsService) {}
|
||||
|
||||
@UseGuards(JwtAuthGuard, PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', UserEntity))
|
||||
@Post()
|
||||
async create(@Request() req, @Body() createGroupPermissionDto: CreateGroupPermissionDto) {
|
||||
const groupPermission = await this.groupPermissionsService.create(req.user, createGroupPermissionDto.group);
|
||||
|
||||
async create(@User() user, @Body() createGroupPermissionDto: CreateGroupPermissionDto) {
|
||||
const groupPermission = await this.groupPermissionsService.create(user, createGroupPermissionDto.group);
|
||||
return decamelizeKeys(groupPermission);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard, PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', UserEntity))
|
||||
@Get(':id')
|
||||
async show(@Request() req, @Param() params) {
|
||||
const groupPermission = await this.groupPermissionsService.findOne(req.user, params.id);
|
||||
async show(@User() user, @Param('id') id: string) {
|
||||
const groupPermission = await this.groupPermissionsService.findOne(user, id);
|
||||
|
||||
return decamelizeKeys(groupPermission);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard, PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', UserEntity))
|
||||
@Put(':id/app_group_permissions/:appGroupPermissionId')
|
||||
async updateAppGroupPermission(
|
||||
@Request() req,
|
||||
@Param() params,
|
||||
@Body() updateGroupPermissionDto: UpdateGroupPermissionDto
|
||||
@Body() updateGroupPermissionDto: UpdateGroupPermissionDto,
|
||||
@User() user,
|
||||
@Param('id') id: string,
|
||||
@Param('appGroupPermissionId') appGroupPermissionId: string
|
||||
) {
|
||||
const groupPermission = await this.groupPermissionsService.updateAppGroupPermission(
|
||||
req.user,
|
||||
params.id,
|
||||
params.appGroupPermissionId,
|
||||
user,
|
||||
id,
|
||||
appGroupPermissionId,
|
||||
updateGroupPermissionDto.actions
|
||||
);
|
||||
|
||||
|
|
@ -49,64 +50,64 @@ export class GroupPermissionsController {
|
|||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard, PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', UserEntity))
|
||||
@Put(':id')
|
||||
async update(@Request() req, @Param() params) {
|
||||
const groupPermission = await this.groupPermissionsService.update(req.user, params.id, req.body);
|
||||
async update(@User() user, @Param('id') id, @Body() body) {
|
||||
const groupPermission = await this.groupPermissionsService.update(user, id, body);
|
||||
|
||||
return decamelizeKeys(groupPermission);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard, PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', UserEntity))
|
||||
@Get()
|
||||
async index(@Request() req) {
|
||||
const groupPermissions = await this.groupPermissionsService.findAll(req.user);
|
||||
async index(@User() user) {
|
||||
const groupPermissions = await this.groupPermissionsService.findAll(user);
|
||||
|
||||
return decamelizeKeys({ groupPermissions });
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard, PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', UserEntity))
|
||||
@Delete(':id')
|
||||
async destroy(@Request() req, @Param() params) {
|
||||
const groupPermission = await this.groupPermissionsService.destroy(req.user, params.id);
|
||||
async destroy(@User() user, @Param('id') id) {
|
||||
const groupPermission = await this.groupPermissionsService.destroy(user, id);
|
||||
|
||||
return decamelizeKeys(groupPermission);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard, PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', UserEntity))
|
||||
@Get(':id/apps')
|
||||
async apps(@Request() req, @Param() params) {
|
||||
const apps = await this.groupPermissionsService.findApps(req.user, params.id);
|
||||
async apps(@User() user, @Param('id') id) {
|
||||
const apps = await this.groupPermissionsService.findApps(user, id);
|
||||
|
||||
return decamelizeKeys({ apps });
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard, PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', UserEntity))
|
||||
@Get(':id/addable_apps')
|
||||
async addableApps(@Request() req, @Param() params) {
|
||||
const apps = await this.groupPermissionsService.findAddableApps(req.user, params.id);
|
||||
async addableApps(@User() user, @Param('id') id) {
|
||||
const apps = await this.groupPermissionsService.findAddableApps(user, id);
|
||||
|
||||
return decamelizeKeys({ apps });
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard, PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', UserEntity))
|
||||
@Get(':id/users')
|
||||
async users(@Request() req, @Param() params) {
|
||||
const users = await this.groupPermissionsService.findUsers(req.user, params.id);
|
||||
async users(@User() user, @Param('id') id) {
|
||||
const users = await this.groupPermissionsService.findUsers(user, id);
|
||||
|
||||
return decamelizeKeys({ users });
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard, PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', UserEntity))
|
||||
@Get(':id/addable_users')
|
||||
async addableUsers(@Request() req, @Param() params) {
|
||||
const users = await this.groupPermissionsService.findAddableUsers(req.user, params.id);
|
||||
async addableUsers(@User() user, @Param('id') id) {
|
||||
const users = await this.groupPermissionsService.findAddableUsers(user, id);
|
||||
|
||||
return decamelizeKeys({ users });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Controller, Post, Request, Param, UseGuards, Get, ForbiddenException } from '@nestjs/common';
|
||||
import { Controller, Post, UseGuards, Get, ForbiddenException, Body } from '@nestjs/common';
|
||||
import { LibraryAppCreationService } from '@services/library_app_creation.service';
|
||||
import { User } from 'src/decorators/user.decorator';
|
||||
import { App } from 'src/entities/app.entity';
|
||||
import { AppsAbilityFactory } from 'src/modules/casl/abilities/apps-ability.factory';
|
||||
import { JwtAuthGuard } from '../../src/modules/auth/jwt-auth.guard';
|
||||
|
|
@ -14,21 +15,20 @@ export class LibraryAppsController {
|
|||
|
||||
@Post()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
async create(@Request() req, @Param() _params) {
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {});
|
||||
async create(@User() user, @Body('identifier') identifier) {
|
||||
const ability = await this.appsAbilityFactory.appsActions(user);
|
||||
|
||||
if (!ability.can('createApp', App)) {
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
}
|
||||
const { identifier } = req.body;
|
||||
const newApp = await this.libraryAppCreationService.perform(req.user, identifier);
|
||||
const newApp = await this.libraryAppCreationService.perform(user, identifier);
|
||||
|
||||
return newApp;
|
||||
}
|
||||
|
||||
@Get()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
async index(@Request() _req, @Param() _params) {
|
||||
async index() {
|
||||
return { template_app_manifests: TemplateAppManifests };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { Controller, Param, Post, Request, UseGuards, Body } from '@nestjs/common';
|
||||
import { Controller, Param, Post, UseGuards, Body } from '@nestjs/common';
|
||||
import { OrganizationUsersService } from 'src/services/organization_users.service';
|
||||
import { decamelizeKeys } from 'humps';
|
||||
import { JwtAuthGuard } from '../../src/modules/auth/jwt-auth.guard';
|
||||
import { AppAbility } from 'src/modules/casl/casl-ability.factory';
|
||||
import { PoliciesGuard } from 'src/modules/casl/policies.guard';
|
||||
import { CheckPolicies } from 'src/modules/casl/check_policies.decorator';
|
||||
import { User as UserEntity } from 'src/entities/user.entity';
|
||||
import { User } from 'src/decorators/user.decorator';
|
||||
import { InviteNewUserDto } from '../dto/invite-new-user.dto';
|
||||
import { User } from 'src/entities/user.entity';
|
||||
|
||||
@Controller('organization_users')
|
||||
export class OrganizationUsersController {
|
||||
|
|
@ -14,35 +15,35 @@ export class OrganizationUsersController {
|
|||
|
||||
// Endpoint for inviting new organization users
|
||||
@UseGuards(JwtAuthGuard, PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('inviteUser', User))
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('inviteUser', UserEntity))
|
||||
@Post()
|
||||
async create(@Request() req, @Body() inviteNewUserDto: InviteNewUserDto) {
|
||||
const result = await this.organizationUsersService.inviteNewUser(req.user, inviteNewUserDto);
|
||||
async create(@User() user, @Body() inviteNewUserDto: InviteNewUserDto) {
|
||||
const result = await this.organizationUsersService.inviteNewUser(user, inviteNewUserDto);
|
||||
return decamelizeKeys({ users: result });
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard, PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('archiveUser', User))
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('archiveUser', UserEntity))
|
||||
@Post(':id/archive')
|
||||
async archive(@Request() req, @Param() params) {
|
||||
const result = await this.organizationUsersService.archive(params.id);
|
||||
async archive(@Param('id') id: string) {
|
||||
const result = await this.organizationUsersService.archive(id);
|
||||
return decamelizeKeys({ result });
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard, PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('archiveUser', User))
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('archiveUser', UserEntity))
|
||||
@Post(':id/unarchive')
|
||||
async unarchive(@Request() req, @Param() params) {
|
||||
const result = await this.organizationUsersService.unarchive(req.user, params.id);
|
||||
async unarchive(@User() user, @Param('id') id: string) {
|
||||
const result = await this.organizationUsersService.unarchive(user, id);
|
||||
return decamelizeKeys({ result });
|
||||
}
|
||||
|
||||
// Deprecated
|
||||
@UseGuards(JwtAuthGuard, PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('changeRole', User))
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('changeRole', UserEntity))
|
||||
@Post(':id/change_role')
|
||||
async changeRole(@Request() req, @Param() params) {
|
||||
const result = await this.organizationUsersService.changeRole(req.user, params.id, req.body.role);
|
||||
async changeRole(@Param('id') id, @Body('role') role) {
|
||||
const result = await this.organizationUsersService.changeRole(id, role);
|
||||
return decamelizeKeys({ result });
|
||||
}
|
||||
}
|
||||
|
|
|
|||