Resolve merge conflict by keeping local frontend/ee submodule commit

This commit is contained in:
Kavin Venkatachalam 2025-07-09 14:12:59 +05:30
commit 4f59c73f81
155 changed files with 3646 additions and 1949 deletions

View file

@ -16,11 +16,11 @@ jobs:
name: packer-ee
steps:
- name: Checkout code to lts-4.0
- name: Checkout code to lts-3.6 branch
if: contains(github.event.release.tag_name, '-ee-lts')
uses: actions/checkout@v2
with:
ref: refs/heads/lts-4.0
ref: refs/heads/lts-3.6
- name: Setting tag
if: "${{ github.event.inputs.version != '' }}"
@ -69,7 +69,7 @@ jobs:
with:
command: build
#The the below argument is specific for building EE AMI image
arguments: -color=false -on-error=abort -var ami_name=tooljet_${{ env.RELEASE_VERSION }}.ubuntu_focal
arguments: -color=false -on-error=abort -var ami_name=tooljet_${{ env.RELEASE_VERSION }}.ubuntu_jammy
target: .
working_directory: deploy/ec2/ee
env:
@ -78,9 +78,9 @@ jobs:
- name: Send Slack Notification
run: |
if [[ "${{ job.status }}" == "success" ]]; then
message="ToolJet enterprise AWS AMI published:\\n\`tooljet_${{ env.RELEASE_VERSION }}.ubuntu_focal\`"
message="ToolJet enterprise AWS AMI published:\\n\`tooljet_${{ env.RELEASE_VERSION }}.ubuntu-jammy\`"
else
message="ToolJet enterprise AWS AMI release failed! \\n\`tooljet_${{ env.RELEASE_VERSION }}.ubuntu_focal\`"
message="ToolJet enterprise AWS AMI release failed! \\n\`tooljet_${{ env.RELEASE_VERSION }}.ubuntu-jammy\`"
fi
curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" ${{ secrets.SLACK_WEBHOOK_URL }}

View file

@ -77,7 +77,7 @@ module.exports = defineConfig({
baseUrl: "http://localhost:8082",
specPattern: [
"cypress/e2e/happyPath/marketplace/commonTestcases/**/*.cy.js",
],
]
numTestsKeptInMemory: 1,
redirectionLimit: 7,
experimentalRunAllSpecs: true,

View file

@ -239,9 +239,9 @@ Cypress.Commands.add(
.invoke("text")
.then((text) => {
cy.wrap(subject).realType(createBackspaceText(text)),
{
delay: 0,
};
{
delay: 0,
};
});
}
);
@ -561,7 +561,7 @@ Cypress.Commands.add("installMarketplacePlugin", (pluginName) => {
}
});
function installPlugin (pluginName) {
function installPlugin(pluginName) {
cy.get('[data-cy="-list-item"]').eq(1).click();
cy.wait(1000);
@ -621,6 +621,7 @@ Cypress.Commands.add("uninstallMarketplacePlugin", (pluginName) => {
Cypress.Commands.add(
"verifyRequiredFieldValidation",
(fieldName, expectedColor) => {
cy.get(commonSelectors.textField(fieldName)).type("some text").clear();
cy.get(commonSelectors.textField(fieldName)).should(
"have.css",
"border-color",

View file

@ -202,10 +202,10 @@ describe("Data source Airtable", () => {
);
cy.get(dataSourceSelector.queryPreviewButton).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
`Query (${data.dsName}) completed.`
);
// cy.verifyToastMessage(
// commonSelectors.toastMessage,
// `Query (${data.dsName}) completed.`
// );
// Verfiy Retrieve record operation
@ -225,10 +225,10 @@ describe("Data source Airtable", () => {
);
cy.get(dataSourceSelector.queryPreviewButton).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
`Query (${data.dsName}) completed.`
);
// cy.verifyToastMessage(
// commonSelectors.toastMessage,
// `Query (${data.dsName}) completed.`
// );
// Verfiy Create record operation
@ -251,10 +251,10 @@ describe("Data source Airtable", () => {
.realType('": {}', { force: true, delay: 0 });
cy.get(dataSourceSelector.queryPreviewButton).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
`Query (${data.dsName}) completed.`
);
// cy.verifyToastMessage(
// commonSelectors.toastMessage,
// `Query (${data.dsName}) completed.`
// );
// Verfiy Update record operation
@ -285,10 +285,10 @@ describe("Data source Airtable", () => {
.realType('"Phone Number": "555_98"', { force: true, delay: 0 });
cy.get(dataSourceSelector.queryPreviewButton).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
`Query (${data.queryName}) completed.`
);
// cy.verifyToastMessage(
// commonSelectors.toastMessage,
// `Query (${data.queryName}) completed.`
// );
// Verify Delete record operation
@ -337,10 +337,10 @@ describe("Data source Airtable", () => {
);
cy.get(dataSourceSelector.queryPreviewButton).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
`Query (${data.queryName}) completed.`
);
// cy.verifyToastMessage(
// commonSelectors.toastMessage,
// `Query (${data.queryName}) completed.`
// );
cy.apiDeleteApp(`${data.dsName}-airtable-app`);
cy.apiDeleteGDS(`cypress-${data.dsName}-airtable`);

View file

@ -254,7 +254,7 @@ describe("Data sources", () => {
.and("be.disabled");
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
"have.text",
"connect ECONNREFUSED 127.0.0.1:5432"
postgreSqlText.serverNotSuppotSsl
);
cy.apiDeleteGDS(`cypress-${data.dataSourceName}-postgresql`);

View file

@ -1,60 +0,0 @@
# https://docs.tooljet.io/docs/setup/env-vars
TOOLJET_HOST=__required__
LOCKBOX_MASTER_KEY=__required__
SECRET_KEY_BASE=__required__
PG_USER=__required__
PG_HOST=__required__
PG_PASS=__required__
PG_DB=tooljet_prod
ORM_LOGGING=true
NODE_ENV=production
DEPLOYMENT_PLATFORM=ec2
# ToolJet Database
TOOLJET_DB=tooljet_db
TOOLJET_DB_USER=
TOOLJET_DB_HOST=
TOOLJET_DB_PASS=
PGRST_HOST=localhost:3001
PGRST_SERVER_PORT=3001
PGRST_JWT_SECRET=
PGRST_DB_URI=
PGRST_DB_PRE_CONFIG=postgrest.pre_config
# Checks every 24 hours to see if a new version of ToolJet is available
# (Enabled by default. Set 0 to disable)
CHECK_FOR_UPDATES=
# Checks every 24 hours to update app telemetry data to ToolJet hub.
# (Telemetry is enabled by default. Set value to true to disable.)
# DISABLE_APP_TELEMETRY=false
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# EMAIL CONFIGURATION
DEFAULT_FROM_EMAIL=hello@tooljet.io
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_DOMAIN=
SMTP_PORT=
# DISABLE USER SIGNUPS (true or false). Default: true
DISABLE_SIGNUPS=
# OBSERVABILITY
APM_VENDOR=
SENTRY_DNS=
SENTRY_DEBUG=
# FEATURE TOGGLE
COMMENT_FEATURE_ENABLE=
ENABLE_MULTIPLAYER_EDITING=true
#SSO
SSO_DISABLE_SIGNUP=
SSO_RESTRICTED_DOMAIN=
SSO_GOOGLE_OAUTH2_CLIENT_ID=
SSO_GIT_OAUTH2_CLIENT_ID=
SSO_GIT_OAUTH2_CLIENT_SECRET=
SSO_GIT_OAUTH2_HOST=

View file

@ -1,17 +0,0 @@
[Unit]
Description=Nest Server
After=network.target
[Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/app
Environment="NODE_ENV=production"
EnvironmentFile=/home/ubuntu/app/.env
RestartSec=1
ExecStart=/usr/bin/npm --prefix /home/ubuntu/app run start:prod
Restart=always
[Install]
WantedBy=multi-user.target

View file

@ -1,16 +0,0 @@
[Unit]
Description=PostgREST Server
After=network.target
[Service]
Type=simple
User=ubuntu
WorkingDirectory=/bin
EnvironmentFile=/home/ubuntu/app/.env
RestartSec=1
ExecStart=/bin/postgrest
Restart=always
[Install]
WantedBy=multi-user.target

View file

@ -1,46 +0,0 @@
#!/bin/bash
if grep __required__ .env
then
echo "Please set the required values within the .env file"
exit 1
fi
export $(grep -v '^#' .env | xargs)
if psql -d postgresql://$PG_USER:$PG_PASS@$PG_HOST/postgres -c 'select now()' > /dev/null 2>&1
then
echo "Successfully pinged the database!";
else
echo "Can't connect to the database. Kindly check the credenials provided in the .env file!"
exit 1
fi
if sudo systemctl start openresty
then
echo "Successfully started reverse proxy!"
else
echo "Failed to start reverse proxy"
exit 1
fi
if $ENABLE_TOOLJET_DB == "true"
then
if sudo systemctl start postgrest
then
echo "Successfully started PostgREST server!"
else
echo "Failed to start PostgREST server"
exit 1
fi
fi
TOOLJET_EDTION=ce npm --prefix server run db:setup:prod
if sudo systemctl start nest
then
echo "The app will be served at ${TOOLJET_HOST}"
else
echo "Failed to start the server!"
exit 1
fi

View file

@ -1,83 +0,0 @@
#!/bin/bash
set -e
# Setup prerequisite dependencies
sudo apt-get update
sudo apt-get -y install --no-install-recommends wget gnupg ca-certificates apt-utils git curl postgresql-client
curl https://raw.githubusercontent.com/creationix/nvm/master/install.sh | bash
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
nvm install 22.15.1
sudo ln -s "$(which node)" /usr/bin/node
sudo ln -s "$(which npm)" /usr/bin/npm
sudo npm i -g npm@10.9.2
# Setup openresty
wget -O - https://openresty.org/package/pubkey.gpg | sudo apt-key add -
echo "deb http://openresty.org/package/ubuntu bionic main" > openresty.list
sudo mv openresty.list /etc/apt/sources.list.d/
sudo apt-get update
sudo apt-get -y install --no-install-recommends openresty
sudo apt-get install -y curl g++ gcc autoconf automake bison libc6-dev \
libffi-dev libgdbm-dev libncurses5-dev libsqlite3-dev libtool \
libyaml-dev make pkg-config sqlite3 zlib1g-dev libgmp-dev \
libreadline-dev libssl-dev libmysqlclient-dev build-essential \
freetds-dev libpq-dev
sudo apt-get install -y luarocks
sudo luarocks install lua-resty-auto-ssl
sudo mkdir /etc/resty-auto-ssl /var/log/openresty /etc/fallback-certs
sudo chown -R www-data:www-data /etc/resty-auto-ssl
# Oracle db client library setup
sudo apt install -y libaio1
curl -o instantclient-basiclite.zip https://download.oracle.com/otn_software/linux/instantclient/instantclient-basiclite-linuxx64.zip -SL && \
curl -o instantclient-basiclite-11.zip https://tooljet-plugins-production.s3.us-east-2.amazonaws.com/marketplace-assets/oracledb/instantclients/instantclient-basiclite-linux.x64-11.2.0.4.0.zip -SL && \
unzip instantclient-basiclite.zip && \
unzip instantclient-basiclite-11.zip && \
sudo mkdir -p /usr/lib/instantclient && sudo mv instantclient*/ /usr/lib/instantclient && \
rm instantclient-basiclite.zip && \
rm instantclient-basiclite-11.zip && \
echo /usr/lib/instantclient/* | sudo tee /etc/ld.so.conf.d/oracle-instantclient.conf > /dev/null && sudo ldconfig
# Set the Instant Client library paths
export LD_LIBRARY_PATH="/usr/lib/instantclient/instantclient_11_2:/usr/lib/instantclient/instantclient_21_10${LD_LIBRARY_PATH}"
# Gen fallback certs
sudo openssl rand -out /home/ubuntu/.rnd -hex 256
sudo chown www-data:www-data /home/ubuntu/.rnd
sudo openssl req -new -newkey rsa:2048 -days 3650 -nodes -x509 \
-subj '/CN=sni-support-required-for-valid-ssl' \
-keyout /etc/fallback-certs/resty-auto-ssl-fallback.key \
-out /etc/fallback-certs/resty-auto-ssl-fallback.crt
# Setup nginx config
export SERVER_HOST="${SERVER_HOST:=localhost}"
export SERVER_USER="${SERVER_USER:=www-data}"
VARS_TO_SUBSTITUTE='$SERVER_HOST:$SERVER_USER'
envsubst "${VARS_TO_SUBSTITUTE}" < /tmp/nginx.conf > /tmp/nginx-substituted.conf
sudo cp /tmp/nginx-substituted.conf /usr/local/openresty/nginx/conf/nginx.conf
# Download and setup postgrest binary
curl -OL https://github.com/PostgREST/postgrest/releases/download/v12.2.0/postgrest-v12.2.0-linux-static-x64.tar.xz
tar xJf postgrest-v12.2.0-linux-static-x64.tar.xz
sudo mv ./postgrest /bin/postgrest
sudo rm postgrest-v12.2.0-linux-static-x64.tar.xz
# Setup app and postgrest as systemd service
sudo cp /tmp/nest.service /lib/systemd/system/nest.service
sudo cp /tmp/postgrest.service /lib/systemd/system/postgrest.service
# Setup app directory
mkdir -p ~/app
git clone -b main https://github.com/ToolJet/ToolJet.git ~/app && cd ~/app
mv /tmp/.env ~/app/.env
mv /tmp/setup_app ~/app/setup_app
sudo chmod +x ~/app/setup_app
npm install -g npm@10.9.2
# Building ToolJet app
npm install -g @nestjs/cli
TOOLJET_EDTION=ce npm run build

View file

@ -1,63 +0,0 @@
packer {
required_plugins {
amazon = {
version = ">= 0.0.1"
source = "github.com/hashicorp/amazon"
}
}
}
source "amazon-ebs" "ubuntu" {
ami_name = "${var.ami_name}"
instance_type = "${var.instance_type}"
region = "${var.ami_region}"
ami_regions = "${var.ami_regions}"
ami_groups = "${var.ami_groups}"
source_ami_filter {
filters = {
name = "ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"
root-device-type = "ebs"
virtualization-type = "hvm"
}
most_recent = true
owners = ["099720109477"]
}
ssh_username = "ubuntu"
ssh_clear_authorized_keys = "true"
}
build {
sources = [
"source.amazon-ebs.ubuntu"
]
provisioner "file" {
source = "nest.service"
destination = "/tmp/nest.service"
}
provisioner "file" {
source = "../../frontend/config/nginx.conf.template"
destination = "/tmp/nginx.conf"
}
provisioner "file" {
source = ".env"
destination = "/tmp/.env"
}
provisioner "file" {
source = "setup_app"
destination = "/tmp/setup_app"
}
provisioner "file" {
source = "postgrest.service"
destination = "/tmp/postgrest.service"
}
provisioner "shell" {
script = "setup_machine.sh"
}
}

View file

@ -1,23 +0,0 @@
variable "ami_name" {
type = string
}
variable "instance_type" {
type = string
default = "t2.medium"
}
variable "ami_region" {
type = string
default = "us-west-1"
}
variable "ami_groups" {
type = list(string)
default = ["all"]
}
variable "ami_regions" {
type = list(string)
default = ["us-west-1", "us-east-1", "us-east-2", "eu-west-2", "eu-central-1", "ap-northeast-1", "ap-southeast-1","ap-northeast-3", "ap-south-1", "ap-northeast-2", "ap-southeast-2", "ca-central-1", "eu-west-1", "eu-north-1", "sa-east-1", "ap-east-1"]
}

View file

@ -161,6 +161,21 @@ else
exit 1
fi
if [[ "$WORKFLOW_WORKER" == "true" ]]; then
echo "WORKER is true. Running the worker..."
npm run worker:prod &
else
echo "WORKER is not true. Skipping the worker execution."
fi
if sudo systemctl start neo4j && sudo systemctl enable neo4j
then
echo "Successfully started Neo4j!"
else
echo "Failed to start and enable Neo4j"
exit 1
fi
TOOLJET_EDTION=ee npm --prefix server run db:setup:prod
if sudo -E systemctl start nest
@ -172,4 +187,4 @@ else
fi
sudo systemctl restart nest
sudo -E systemctl restart postgrest
sudo -E systemctl restart postgrest

View file

@ -78,6 +78,28 @@ sudo cp /tmp/redis-server.service /lib/systemd/system/redis-server.service
# Start and enable Redis service
sudo systemctl daemon-reload
# Setup Neo4j with APOC plugin
wget -O - https://debian.neo4j.com/neotechnology.gpg.key | sudo apt-key add -
echo "deb https://debian.neo4j.com stable 5" | sudo tee /etc/apt/sources.list.d/neo4j.list
sudo apt-get update
sudo apt-get install -y neo4j=1:5.26.6
sudo apt-mark hold neo4j
# Setup APOC plugin
sudo mkdir -p /var/lib/neo4j/plugins
sudo wget -P /var/lib/neo4j/plugins https://github.com/neo4j/apoc/releases/download/5.26.6/apoc-5.26.6-core.jar
# Update Neo4j config
echo "dbms.security.procedures.unrestricted=apoc.*" | sudo tee -a /etc/neo4j/neo4j.conf
echo "dbms.security.procedures.allowlist=apoc.*,algo.*,gds.*" | sudo tee -a /etc/neo4j/neo4j.conf
echo "dbms.directories.plugins=/var/lib/neo4j/plugins" | sudo tee -a /etc/neo4j/neo4j.conf
echo "dbms.security.auth_enabled=true" | sudo tee -a /etc/neo4j/neo4j.conf
# Clean up APT cache
sudo apt-get clean
sudo rm -rf /var/lib/apt/lists/*
# Setup app directory
mkdir -p ~/app
@ -96,4 +118,5 @@ npm install -g npm@10.9.2
# Building ToolJet app
npm install -g @nestjs/cli
TOOLJET_EDTION=ee npm run build
export NODE_OPTIONS='--max-old-space-size=8000'
TOOLJET_EDTION=ee npm run build

View file

@ -16,7 +16,7 @@ source "amazon-ebs" "ubuntu" {
source_ami_filter {
filters = {
name = "ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"
name = "ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"
root-device-type = "ebs"
virtualization-type = "hvm"
}
@ -30,7 +30,7 @@ source "amazon-ebs" "ubuntu" {
launch_block_device_mappings {
device_name = "/dev/sda1"
volume_size = 10
volume_size = 30
delete_on_termination = true
}
@ -47,7 +47,7 @@ build {
}
provisioner "file" {
source = "../../frontend/config/nginx.conf.template"
source = "../../../frontend/config/nginx.conf.template"
destination = "/tmp/nginx.conf"
}

View file

@ -4,7 +4,7 @@ variable "ami_name" {
variable "instance_type" {
type = string
default = "t2.medium"
default = "t2.large"
}
variable "ami_region" {

View file

@ -3,6 +3,116 @@ set -e
echo "🚀 Starting Try ToolJet container initialization..."
# Neo4j configuration
# ----------------------------------
# Default Neo4j environment values
# ----------------------------------
export NEO4J_USER=${NEO4J_USER:-"neo4j"}
export NEO4J_PASSWORD=${NEO4J_PASSWORD:-"appaqvyvRLbeukhFE"}
export NEO4J_AUTH=${NEO4J_AUTH:-"neo4j/appaqvyvRLbeukhFE"}
export NEO4J_URI=${NEO4J_URI:-"bolt://localhost:7687"}
export NEO4J_PLUGINS=${NEO4J_PLUGINS:-'["apoc"]'}
export NEO4J_AUTH
# Extract username and password from NEO4J_AUTH if set
if [ -n "$NEO4J_AUTH" ]; then
# Extract username and password from NEO4J_AUTH (format: username/password)
NEO4J_USERNAME=$(echo "$NEO4J_AUTH" | cut -d'/' -f1)
NEO4J_PASSWORD=$(echo "$NEO4J_AUTH" | cut -d'/' -f2)
# Export these for application use
export NEO4J_USERNAME
export NEO4J_PASSWORD
echo "Neo4j authentication configured with username: $NEO4J_USERNAME" >/dev/null 2>&1
else
echo "NEO4J_AUTH not set, using default authentication" >/dev/null 2>&1
fi
# Check if Neo4j is already initialized and set password if necessary
if [ "$NEO4J_AUTH" != "none" ] && [ -n "$NEO4J_PASSWORD" ]; then
echo "Setting Neo4j initial password..." >/dev/null 2>&1
# Ensure Neo4j is not running before setting the initial password
neo4j stop || true
# Set the initial password using the correct command format for Neo4j 5.x
NEO4J_ADMIN_CMD=$(which neo4j-admin)
NEO4J_VERSION=$(neo4j --version | grep -o "[0-9]\+\.[0-9]\+\.[0-9]\+" | head -n 1)
echo "Detected Neo4j version: $NEO4J_VERSION" >/dev/null 2>&1
# Use version-specific command format
MAJOR_VERSION=$(echo $NEO4J_VERSION | cut -d. -f1)
if [ "$MAJOR_VERSION" -ge "5" ]; then
# For Neo4j 5.x and higher
echo "Using Neo4j 5.x+ password command format" >/dev/null 2>&1
$NEO4J_ADMIN_CMD dbms set-initial-password "$NEO4J_PASSWORD" --require-password-change=false >/dev/null 2>&1 || {
echo "Warning: Could not set Neo4j password, it may already be set" >/dev/null 2>&1
}
else
# For Neo4j 4.x and lower
echo "Using Neo4j 4.x password command format" >/dev/null 2>&1
$NEO4J_ADMIN_CMD set-initial-password "$NEO4J_PASSWORD" >/dev/null 2>&1 || {
echo "Warning: Could not set Neo4j password, it may already be set" >/dev/null 2>&1
}
fi
fi
# Update Neo4j configuration
echo "Configuring Neo4j..." >/dev/null 2>&1
cat > /etc/neo4j/neo4j.conf << EOF
# Neo4j configuration
dbms.security.auth_enabled=true
server.bolt.enabled=true
server.bolt.listen_address=0.0.0.0:7687
server.directories.data=/var/lib/neo4j/data
server.directories.logs=/var/log/neo4j
initial.dbms.default_database=neo4j
server.directories.plugins=/var/lib/neo4j/plugins
server.directories.import=/var/lib/neo4j/import
# APOC Settings
dbms.security.procedures.unrestricted=apoc.*
dbms.security.procedures.allowlist=apoc.*,algo.*,gds.*
EOF
if [ -w "$NEO4J_LOG_DIR" ]; then
chmod -R 770 "$NEO4J_LOG_DIR" || echo "Warning: Could not set log directory permissions" >/dev/null 2>&1
fi
# Start Neo4j
echo "Starting Neo4j service..."
neo4j console >/dev/null 2>&1 &
# Add a wait for Neo4j to be ready with more robust checking
echo "Waiting for Neo4j to be ready..." >/dev/null 2>&1
NEO4J_READY=false
for i in {1..60}; do
# First try standard status check
if neo4j status >/dev/null 2>&1; then
echo "Neo4j is ready 🚀"
NEO4J_READY=true
break
fi
# Also try connecting to the bolt port as a fallback
if command -v nc >/dev/null 2>&1; then
if nc -z localhost 7687 >/dev/null 2>&1; then
echo "Neo4j is ready (port 7687 is open)"
NEO4J_READY=true
break
fi
fi
echo "Waiting for Neo4j to start... ($i/60)" >/dev/null 2>&1
sleep 2
done
if [ "$NEO4J_READY" = false ]; then
echo "WARNING: Neo4j may not be fully started yet, but continuing..."
fi
# Configure PostgreSQL authentication
echo "🔧 Configuring PostgreSQL authentication..."
sed -i 's/^local\s\+all\s\+postgres\s\+\(peer\|md5\)/local all postgres trust/' /etc/postgresql/13/main/pg_hba.conf >/dev/null 2>&1

View file

@ -3,6 +3,115 @@ set -e
echo "🚀 Starting Try ToolJet container initialization..."
# Neo4j configuration
# ----------------------------------
# Default Neo4j environment values
# ----------------------------------
export NEO4J_USER=${NEO4J_USER:-"neo4j"}
export NEO4J_PASSWORD=${NEO4J_PASSWORD:-"appaqvyvRLbeukhFE"}
export NEO4J_AUTH=${NEO4J_AUTH:-"neo4j/appaqvyvRLbeukhFE"}
export NEO4J_URI=${NEO4J_URI:-"bolt://localhost:7687"}
export NEO4J_PLUGINS=${NEO4J_PLUGINS:-'["apoc"]'}
export NEO4J_AUTH
# Extract username and password from NEO4J_AUTH if set
if [ -n "$NEO4J_AUTH" ]; then
# Extract username and password from NEO4J_AUTH (format: username/password)
NEO4J_USERNAME=$(echo "$NEO4J_AUTH" | cut -d'/' -f1)
NEO4J_PASSWORD=$(echo "$NEO4J_AUTH" | cut -d'/' -f2)
# Export these for application use
export NEO4J_USERNAME
export NEO4J_PASSWORD
echo "Neo4j authentication configured with username: $NEO4J_USERNAME" >/dev/null 2>&1
else
echo "NEO4J_AUTH not set, using default authentication" >/dev/null 2>&1
fi
# Check if Neo4j is already initialized and set password if necessary
if [ "$NEO4J_AUTH" != "none" ] && [ -n "$NEO4J_PASSWORD" ]; then
echo "Setting Neo4j initial password..." >/dev/null 2>&1
# Ensure Neo4j is not running before setting the initial password
neo4j stop || true
# Set the initial password using the correct command format for Neo4j 5.x
NEO4J_ADMIN_CMD=$(which neo4j-admin)
NEO4J_VERSION=$(neo4j --version | grep -o "[0-9]\+\.[0-9]\+\.[0-9]\+" | head -n 1)
echo "Detected Neo4j version: $NEO4J_VERSION" >/dev/null 2>&1
# Use version-specific command format
MAJOR_VERSION=$(echo $NEO4J_VERSION | cut -d. -f1)
if [ "$MAJOR_VERSION" -ge "5" ]; then
# For Neo4j 5.x and higher
echo "Using Neo4j 5.x+ password command format" >/dev/null 2>&1
$NEO4J_ADMIN_CMD dbms set-initial-password "$NEO4J_PASSWORD" --require-password-change=false >/dev/null 2>&1 || {
echo "Warning: Could not set Neo4j password, it may already be set" >/dev/null 2>&1
}
else
# For Neo4j 4.x and lower
echo "Using Neo4j 4.x password command format" >/dev/null 2>&1
$NEO4J_ADMIN_CMD set-initial-password "$NEO4J_PASSWORD" >/dev/null 2>&1 || {
echo "Warning: Could not set Neo4j password, it may already be set" >/dev/null 2>&1
}
fi
fi
# Update Neo4j configuration
echo "Configuring Neo4j..." >/dev/null 2>&1
cat > /etc/neo4j/neo4j.conf << EOF
# Neo4j configuration
dbms.security.auth_enabled=true
server.bolt.enabled=true
server.bolt.listen_address=0.0.0.0:7687
server.directories.data=/var/lib/neo4j/data
server.directories.logs=/var/log/neo4j
initial.dbms.default_database=neo4j
server.directories.plugins=/var/lib/neo4j/plugins
server.directories.import=/var/lib/neo4j/import
# APOC Settings
dbms.security.procedures.unrestricted=apoc.*
dbms.security.procedures.allowlist=apoc.*,algo.*,gds.*
EOF
if [ -w "$NEO4J_LOG_DIR" ]; then
chmod -R 770 "$NEO4J_LOG_DIR" || echo "Warning: Could not set log directory permissions" >/dev/null 2>&1
fi
# Start Neo4j
echo "Starting Neo4j service..."
neo4j console >/dev/null 2>&1 &
# Add a wait for Neo4j to be ready with more robust checking
echo "Waiting for Neo4j to be ready..." >/dev/null 2>&1
NEO4J_READY=false
for i in {1..60}; do
# First try standard status check
if neo4j status >/dev/null 2>&1; then
echo "Neo4j is ready 🚀"
NEO4J_READY=true
break
fi
# Also try connecting to the bolt port as a fallback
if command -v nc >/dev/null 2>&1; then
if nc -z localhost 7687 >/dev/null 2>&1; then
echo "Neo4j is ready (port 7687 is open)"
NEO4J_READY=true
break
fi
fi
echo "Waiting for Neo4j to start... ($i/60)" >/dev/null 2>&1
sleep 2
done
if [ "$NEO4J_READY" = false ]; then
echo "WARNING: Neo4j may not be fully started yet, but continuing..."
fi
# Configure PostgreSQL authentication
echo "🔧 Configuring PostgreSQL authentication..."
sed -i 's/^local\s\+all\s\+postgres\s\+\(peer\|md5\)/local all postgres trust/' /etc/postgresql/13/main/pg_hba.conf >/dev/null 2>&1

View file

@ -6,7 +6,7 @@ COPY --from=postgrest/postgrest:v12.2.0 /bin/postgrest /bin
# Install Postgres
USER root
RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ bookworm-pgdg main" | tee /etc/apt/sources.list.d/pgdg.list
RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ bullseye-pgdg main" | tee /etc/apt/sources.list.d/pgdg.list
RUN echo "deb http://deb.debian.org/debian"
RUN apt update && apt -y install postgresql-13 postgresql-client-13 supervisor
USER postgres
@ -52,6 +52,18 @@ RUN apt update && apt install -y gettext-base curl \
COPY ./docker/ee/temporal-server.yaml /etc/temporal/temporal-server.template.yaml
COPY ./docker/ee/temporal-ui-server.yaml /etc/temporal/temporal-ui-server.yaml
# Install Neo4j + APOC
RUN wget -O - https://debian.neo4j.com/neotechnology.gpg.key | apt-key add - && \
echo "deb https://debian.neo4j.com stable 5" > /etc/apt/sources.list.d/neo4j.list && \
apt-get update && apt-get install -y neo4j=1:5.26.6 && apt-mark hold neo4j && \
mkdir -p /var/lib/neo4j/plugins && \
wget -P /var/lib/neo4j/plugins https://github.com/neo4j/apoc/releases/download/5.26.6/apoc-5.26.6-core.jar && \
echo "dbms.security.procedures.unrestricted=apoc.*" >> /etc/neo4j/neo4j.conf && \
echo "dbms.security.procedures.allowlist=apoc.*,algo.*,gds.*" >> /etc/neo4j/neo4j.conf && \
echo "dbms.directories.plugins=/var/lib/neo4j/plugins" >> /etc/neo4j/neo4j.conf && \
echo "dbms.security.auth_enabled=true" >> /etc/neo4j/neo4j.conf && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# Configure Supervisor to manage PostgREST, ToolJet, and Redis
RUN echo "[supervisord] \n" \
"nodaemon=true \n" \

View file

@ -6,7 +6,7 @@ COPY --from=postgrest/postgrest:v12.2.0 /bin/postgrest /bin
# Install Postgres
USER root
RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ bookworm-pgdg main" | tee /etc/apt/sources.list.d/pgdg.list
RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ bullseye-pgdg main" | tee /etc/apt/sources.list.d/pgdg.list
RUN echo "deb http://deb.debian.org/debian"
RUN apt update && apt -y install postgresql-13 postgresql-client-13 supervisor
USER postgres
@ -52,6 +52,18 @@ RUN apt update && apt install -y gettext-base curl \
COPY ./docker/ee/temporal-server.yaml /etc/temporal/temporal-server.template.yaml
COPY ./docker/ee/temporal-ui-server.yaml /etc/temporal/temporal-ui-server.yaml
# Install Neo4j + APOC
RUN wget -O - https://debian.neo4j.com/neotechnology.gpg.key | apt-key add - && \
echo "deb https://debian.neo4j.com stable 5" > /etc/apt/sources.list.d/neo4j.list && \
apt-get update && apt-get install -y neo4j=1:5.26.6 && apt-mark hold neo4j && \
mkdir -p /var/lib/neo4j/plugins && \
wget -P /var/lib/neo4j/plugins https://github.com/neo4j/apoc/releases/download/5.26.6/apoc-5.26.6-core.jar && \
echo "dbms.security.procedures.unrestricted=apoc.*" >> /etc/neo4j/neo4j.conf && \
echo "dbms.security.procedures.allowlist=apoc.*,algo.*,gds.*" >> /etc/neo4j/neo4j.conf && \
echo "dbms.directories.plugins=/var/lib/neo4j/plugins" >> /etc/neo4j/neo4j.conf && \
echo "dbms.security.auth_enabled=true" >> /etc/neo4j/neo4j.conf && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# Configure Supervisor to manage PostgREST, ToolJet, and Redis
RUN echo "[supervisord] \n" \
"nodaemon=true \n" \

View file

@ -38,6 +38,7 @@ import {
getDataSourcesRoutes,
getAuditLogsRoutes,
} from '@/modules';
import { isWorkflowsFeatureEnabled } from '@/modules/common/helpers/utils';
import { shallow } from 'zustand/shallow';
import useStore from '@/AppBuilder/_stores/store';
import { checkIfToolJetCloud } from '@/_helpers/utils';
@ -112,6 +113,7 @@ class AppComponent extends React.Component {
const featureAccess = await licenseService.getFeatureAccess();
const isBasicPlan = !featureAccess?.licenseStatus?.isLicenseValid || featureAccess?.licenseStatus?.isExpired;
this.setState({ showBanner: isBasicPlan });
this.updateColorScheme();
}
// check if its getting routed from editor
checkPreviousRoute = (route) => {
@ -121,7 +123,7 @@ class AppComponent extends React.Component {
return false;
};
componentDidUpdate(prevProps) {
componentDidUpdate(prevProps, prevState) {
// Check if the current location is the dashboard (homepage)
if (
this.props.location.pathname === `/${getWorkspaceIdOrSlugFromURL()}` &&
@ -134,18 +136,24 @@ class AppComponent extends React.Component {
}
// Update margin when showBanner changes
this.updateMargin();
// Update color scheme if darkMode changed
if (prevState.darkMode !== this.state.darkMode) {
this.updateColorScheme();
}
}
switchDarkMode = (newMode) => {
this.setState({ darkMode: newMode });
this.props.updateIsTJDarkMode(newMode);
localStorage.setItem('darkMode', newMode);
this.updateColorScheme(newMode);
};
isEditorOrViewerFromPath = () => {
const pathname = this.props.location.pathname;
if (pathname.includes('/apps/')) {
return 'editor';
} else if (pathname.includes('/applications/') || pathname.includes('/embed-apps/')) {
}
if (pathname.includes('/applications/') || pathname.includes('/embed-apps/')) {
return 'viewer';
}
return '';
@ -156,6 +164,14 @@ class AppComponent extends React.Component {
isExistingPlanUser = (date) => {
return new Date(date) < new Date('2025-04-24'); //show banner if user created before 2 april (24 for testing)
};
updateColorScheme = (darkModeValue) => {
const isDark = darkModeValue !== undefined ? darkModeValue : this.state.darkMode;
if (isDark) {
document.documentElement.style.setProperty('color-scheme', 'dark');
} else {
document.documentElement.style.removeProperty('color-scheme');
}
};
render() {
const { updateAvailable, darkMode, isEditorOrViewer, showBanner } = this.state;
const mergedProps = {
@ -278,7 +294,7 @@ class AppComponent extends React.Component {
</PrivateRoute>
}
/>
{window.public_config?.ENABLE_WORKFLOWS_FEATURE === 'true' && (
{isWorkflowsFeatureEnabled() && (
<Route
exact
path="/:workspaceId/workflows/*"
@ -289,17 +305,19 @@ class AppComponent extends React.Component {
}
/>
)}
<Route
path="/:workspaceId/workspace-settings/*"
element={<WorkspaceSettings {...mergedProps} />}
></Route>
<Route path="/:workspaceId/workspace-settings/*" element={<WorkspaceSettings {...mergedProps} />} />
<Route
path="settings/*"
element={
<InstanceSettings switchDarkMode={this.switchDarkMode} darkMode={darkMode} {...this.props} />
}
></Route>
<Route path="/:workspaceId/settings/*" element={<InstanceSettings {...this.props} darkMode={darkMode} switchDarkMode={this.switchDarkMode} />}></Route>
/>
<Route
path="/:workspaceId/settings/*"
element={
<InstanceSettings {...this.props} darkMode={darkMode} switchDarkMode={this.switchDarkMode} />
}
/>
<Route
exact
path="/:workspaceId/modules"
@ -422,7 +440,7 @@ class AppComponent extends React.Component {
/>
</Routes>
</BreadCrumbContext.Provider>
<div id="modal-div"></div>
<div id="modal-div" />
</div>
<Toast toastOptions={toastOptions} />

View file

@ -348,7 +348,7 @@ const MultiLineCodeEditor = (props) => {
view={editorView}
isPanelOpen={isSearchPanelOpen}
renderCopilot={() =>
renderCopilot({
renderCopilot?.({
darkMode,
language: lang,
editorRef,

View file

@ -14,6 +14,7 @@ import { useQueryPanelActions } from '@/_stores/queryPanelStore';
import { Tooltip } from 'react-tooltip';
import { canCreateDataSource } from '@/_helpers';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import { isWorkflowsFeatureEnabled } from '@/modules/common/helpers/utils';
import '../queryManager.theme.scss';
import useStore from '@/AppBuilder/_stores/store';
import { staticDataSources } from '../constants';
@ -80,7 +81,7 @@ function DataSourcePicker({ darkMode }) {
navigate(`/${workspaceId}/data-sources`);
};
const workflowsEnabled = window.public_config?.ENABLE_WORKFLOWS_FEATURE == 'true';
const workflowsEnabled = isWorkflowsFeatureEnabled();
return (
<>

View file

@ -15,6 +15,7 @@ import { DataBaseSources, ApiSources, CloudStorageSources } from '@/modules/comm
import { canCreateDataSource } from '@/_helpers';
import './../queryManager.theme.scss';
import { DATA_SOURCE_TYPE } from '@/_helpers/constants';
import { isWorkflowsFeatureEnabled } from '@/modules/common/helpers/utils';
import useStore from '@/AppBuilder/_stores/store';
function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSources, onNewNode, defaultDataSources }) {
@ -39,7 +40,7 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSourc
closePopup();
};
const workflowsEnabled = window.public_config?.ENABLE_WORKFLOWS_FEATURE == 'true';
const workflowsEnabled = isWorkflowsFeatureEnabled();
const staticDataSources = workflowsEnabled
? staticDatasources
: staticDatasources.filter((ds) => ds?.kind !== 'workflows');

View file

@ -9,6 +9,7 @@ import { BaseUrl } from './BaseUrl';
import { queryManagerSelectComponentStyle } from '@/_ui/Select/styles';
import CodeHinter from '@/AppBuilder/CodeEditor';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
import './styles.css';
class Restapi extends React.Component {
constructor(props) {
@ -287,14 +288,15 @@ class Restapi extends React.Component {
const { options } = this.state;
const dataSourceURL = this.props.selectedDataSource?.options?.url?.value;
const queryName = this.props.queryName;
const isWorkflowNode = queryName === 'workflowNode';
const currentValue = { label: options.method?.toUpperCase(), value: options.method };
return (
<div className={`${this.props?.queryName !== 'workflowNode' && 'd-flex'} flex-column`}>
<div className={`${!isWorkflowNode && 'd-flex'} flex-column`}>
{this.props.selectedDataSource?.scope == 'global' && <div className="form-label flex-shrink-0"></div>}{' '}
<div className="flex-grow-1 overflow-hidden">
<div className="rest-api-methods-select-element-container">
<div className="d-flex">
<div className={`rest-api-methods-select-element-container ${isWorkflowNode ? 'workflow-rest-api' : ''}`}>
<div className={`d-flex ${isWorkflowNode ? 'mb-2' : ''}`}>
<p
className="text-placeholder font-weight-medium"
style={{ width: '100px', marginRight: '16px', marginBottom: '0px' }}
@ -303,8 +305,11 @@ class Restapi extends React.Component {
</p>
</div>
<div className="d-flex flex-column w-100">
<div className="d-flex flex-row">
<div className={`me-2`} style={{ width: '90px', height: '32px' }}>
<div className={`${isWorkflowNode ? '' : 'd-flex'} flex-row`}>
<div
className={`me-2 ${isWorkflowNode ? 'mb-2' : ''}`}
style={{ width: isWorkflowNode ? '150px' : '90px', height: '32px' }}
>
<label className="font-weight-medium color-slate12">Method</label>
<Select
options={[
@ -320,9 +325,9 @@ class Restapi extends React.Component {
value={currentValue}
defaultValue={{ label: 'GET', value: 'get' }}
placeholder="Method"
width={100}
width={isWorkflowNode ? 150 : 100}
height={32}
styles={this.customSelectStyles(this.props.darkMode, 91)}
styles={this.customSelectStyles(this.props.darkMode, isWorkflowNode ? 150 : 91)}
useCustomStyles={true}
customClassPrefix="restapi-method-select"
onMenuOpen={() => {
@ -335,7 +340,7 @@ class Restapi extends React.Component {
</div>
<div
className={`field rest-methods-url ${dataSourceURL && 'data-source-exists'}`}
style={{ width: 'calc(100% - 214px)' }}
style={{ width: isWorkflowNode ? '100%' : 'calc(100% - 214px)' }}
>
<div className="font-weight-medium color-slate12">URL</div>
<div className="d-flex h-100 w-100">
@ -371,7 +376,7 @@ class Restapi extends React.Component {
</div>
</div>
</div>
<div className={`query-pane-restapi-tabs`}>
<div className={`query-pane-restapi-tabs`} data-workflow={isWorkflowNode ? 'true' : 'false'}>
<Tabs
theme={this.props.darkMode ? 'monokai' : 'default'}
options={this.state.options}
@ -384,6 +389,7 @@ class Restapi extends React.Component {
bodyToggle={this.state.options.body_toggle}
setBodyToggle={this.onBodyToggleChanged}
onInputChange={this.handleInputChange}
isWorkflow={isWorkflowNode}
/>
</div>
</div>

View file

@ -0,0 +1,45 @@
/* Specific styling for workflow modal */
.workflow-rest-api {
display: flex;
flex-direction: column;
}
/* Ensure method and URL fields have full width in workflow node */
.workflow-rest-api .me-2 {
width: 100% !important;
margin-bottom: 16px; /* Increased spacing to avoid label overlap */
}
/* Ensure URL label doesn't overlap with Method dropdown */
.workflow-rest-api .field .font-weight-medium {
margin-bottom: 4px;
display: block;
padding-top: 4px; /* Add space above URL label */
}
/* Fix the method dropdown width and height for workflow */
.workflow-rest-api .me-2 {
width: 150px !important; /* Wider to accommodate "DELETE" and other long options */
height: auto !important;
min-height: 32px;
}
/* Fix Add more button to fit text properly */
.add-params-btn {
width: 100px !important;
padding: 4px 8px;
}
.add-params-btn p {
display: flex;
align-items: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Button fix for workflow */
.workflow-rest-api ~ .query-pane-restapi-tabs .add-params-btn {
width: auto !important;
min-width: 100px;
}

View file

@ -17,6 +17,8 @@ import { useNavigate } from 'react-router-dom';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
import { BulkUploadPrimaryKey } from './BulkUploadPrimaryKey';
import BulkUpsertPrimaryKey from './BulkUpsertPrimaryKey';
import { fetchEdition } from '@/modules/common/helpers/utils';
import config from 'config';
import './styles.scss';
import CodeHinter from '@/AppBuilder/CodeEditor';
@ -49,6 +51,21 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
const [bulkUpdatePrimaryKey, setBulkUpdatePrimaryKey] = useState(() => options['bulk_update_with_primary_key'] || {});
const [bulkUpsertPrimaryKey, setBulkUpsertPrimaryKey] = useState(() => options['bulk_upsert_with_primary_key'] || {});
// Check if SQL mode should be disabled
const isSqlModeDisabled = () => {
// Check legacy environment variable for backward compatibility
if (window.public_config?.TJDB_SQL_MODE_DISABLE === 'true') {
return true;
}
const edition = fetchEdition(config);
if (edition === 'cloud') {
return true;
}
return false;
};
const joinOptions = options['join_table']?.['joins'] || [
{ conditions: { conditionsList: [{ leftField: { table: selectedTableId } }] } },
];
@ -557,7 +574,7 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
<TooljetDatabaseContext.Provider value={value}>
{/* table name dropdown */}
{window.public_config?.TJDB_SQL_MODE_DISABLE !== 'true' && (
{!isSqlModeDisabled() && (
<div
className={cx({ 'col-4': !isHorizontalLayout, 'd-flex tooljetdb-worflow-operations': isHorizontalLayout })}
>

View file

@ -7,6 +7,7 @@ import { v4 as uuidv4 } from 'uuid';
import useStore from '@/AppBuilder/_stores/store';
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
import usePopoverObserver from '@/AppBuilder/_hooks/usePopoverObserver';
import useWorkflowStore from '@/_stores/workflowStore';
export function Workflows({ options, optionsChanged, currentState }) {
const { moduleId } = useModuleContext();
@ -15,7 +16,9 @@ export function Workflows({ options, optionsChanged, currentState }) {
const [_selectedWorkflowId, setSelectedWorkflowId] = useState(undefined);
const [params, setParams] = useState([...(options.params ?? [{ key: '', value: '' }])]);
const appId = useStore((state) => state.appStore.modules[moduleId].app.appId);
const workflowIdFromStore = useWorkflowStore((state) => state.workflowId);
const appIdFromStore = useStore((state) => state.appStore.modules[moduleId].app.appId);
const appId = workflowIdFromStore || appIdFromStore;
usePopoverObserver(
document.getElementsByClassName('query-details')[0],

View file

@ -1,3 +1,5 @@
import { toast } from 'react-hot-toast';
import { AsyncQueryHandler } from '@/AppBuilder/_utils/async-query-handler';
import _, { isEmpty } from 'lodash';
import { resolveReferences, loadPyodide, hasCircularDependency } from '@/_helpers/utils';
import { fetchOAuthToken, fetchOauthTokenForSlackAndGSheet } from '@/AppBuilder/_utils/auth';
@ -7,7 +9,7 @@ import axios from 'axios';
import { validateMultilineCode } from '@/_helpers/utility';
import { convertMapSet, getQueryVariables } from '@/AppBuilder/_utils/queryPanel';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
import toast from 'react-hot-toast';
const queryManagerPreferences = JSON.parse(localStorage.getItem('queryManagerPreferences')) ?? {};
const initialState = {
@ -168,6 +170,19 @@ export const createQueryPanelSlice = (set, get) => ({
'setLoadingDataQueries'
),
setAsyncQueryRuns: (updater) =>
set(
(state) => {
if (typeof updater === 'function') {
state.queryPanel.asyncQueryRuns = updater(state.queryPanel.asyncQueryRuns);
} else {
state.queryPanel.asyncQueryRuns = updater;
}
},
false,
'setAsyncQueryRuns'
),
onQueryConfirmOrCancel: (queryConfirmationData, isConfirm = false, mode = 'edit', moduleId = 'canvas') => {
const { queryPanel, dataQuery, setResolvedQuery } = get();
const { runQuery } = queryPanel;
@ -208,6 +223,69 @@ export const createQueryPanelSlice = (set, get) => ({
);
},
createWorkflowAsyncHandler: ({
executionId,
queryId,
processQueryResults,
handleFailure,
shouldSetPreviewData,
setPreviewData,
setResolvedQuery,
}) => {
const asyncHandler = new AsyncQueryHandler({
streamSSE: (jobId) => {
return workflowExecutionsService.streamSSE(jobId);
},
extractJobId: () => executionId,
classifyEventStatus: (eventData) => {
// hardcoded for workflows
if (eventData.type === 'workflow_connection_close') {
return { status: 'CLOSE', data: eventData };
} else if (eventData.type === 'workflow_execution_completed') {
return { status: 'COMPLETE', result: eventData.result, data: eventData };
} else if (eventData.type === 'workflow_execution_error') {
return { status: 'ERROR', data: eventData };
} else {
return { status: 'PROGRESS', data: eventData };
}
},
callbacks: {
onProgress: (progressData) => {
// Update UI with progress information
if (shouldSetPreviewData) {
setPreviewData({ ...progressData });
}
setResolvedQuery(queryId, {
isLoading: true,
progress: progressData.progress,
currentData: progressData.partialData || [],
});
},
onComplete: async (result) => {
await processQueryResults(result);
// Remove the AsyncQueryHandler instance from asyncQueryRuns on completion
get().queryPanel.setAsyncQueryRuns((currentRuns) =>
currentRuns.filter((handler) => handler.jobId !== asyncHandler.jobId)
);
},
onError: (e) => {
handleFailure({
status: 'failed',
message: e?.error?.message || 'Error running workflow',
description: e?.error?.description || null,
data: typeof e?.error === 'object' ? { ...e.error } : e?.error,
});
// Remove the AsyncQueryHandler instance from asyncQueryRuns on error
get().queryPanel.setAsyncQueryRuns((currentRuns) =>
currentRuns.filter((handler) => handler.jobId !== asyncHandler.jobId)
);
},
},
});
return asyncHandler;
},
runQuery: (
queryId,
queryName,
@ -238,7 +316,7 @@ export const createQueryPanelSlice = (set, get) => ({
setPreviewPanelExpanded,
executeRunPycode,
runTransformation,
executeWorkflow,
triggerWorkflow,
executeMultilineJS,
} = queryPanel;
const queryUpdatePromise = dataQuerySlice.queryUpdates[queryId];
@ -339,6 +417,120 @@ export const createQueryPanelSlice = (set, get) => ({
}
}
// Handler for transformation and completion of query results
const processQueryResults = async (data, rawData = null) => {
let finalData = data;
rawData = rawData || data;
if (dataQuery.options.enableTransformation) {
finalData = await runTransformation(
finalData,
query.options.transformation,
query.options.transformationLanguage,
query,
mode,
moduleId
);
if (finalData.status === 'failed') {
handleFailure(finalData);
return finalData;
}
}
if (shouldSetPreviewData) {
setPreviewLoading(false);
setPreviewData(finalData);
}
if (dataQuery.options.showSuccessNotification) {
const notificationDuration = dataQuery.options.notificationDuration * 1000 || 5000;
toast.success(dataQuery.options.successMessage, {
duration: notificationDuration,
});
}
get().debugger.log({
logLevel: 'success',
type: 'query',
kind: query.kind,
key: query.name,
message: 'Query executed successfully',
isQuerySuccessLog: true,
errorTarget: 'Queries',
});
setResolvedQuery(
queryId,
{
isLoading: false,
data: finalData,
rawData,
metadata: data?.metadata,
request: data?.metadata?.request,
response: data?.metadata?.response,
},
moduleId
);
onEvent('onDataQuerySuccess', queryEvents, mode);
return { status: 'ok', data: finalData };
};
// Handler for query failures
const handleFailure = (errorData) => {
if (shouldSetPreviewData) {
setPreviewLoading(false);
setPreviewData(errorData);
}
get().debugger.log({
logLevel: 'error',
type: 'query',
kind: query.kind,
key: query.name,
message: errorData?.description,
errorTarget: 'Queries',
error:
query.kind === 'restapi'
? {
substitutedVariables: options,
request: errorData?.requestObject,
response: errorData?.responseObject,
}
: errorData,
isQuerySuccessLog: false,
});
setResolvedQuery(
queryId,
{
isLoading: false,
...(query.kind === 'restapi' || errorData?.type === 'tj-401'
? {
metadata: errorData?.metadata,
request: errorData?.requestObject,
response: errorData?.responseObject,
responseHeaders: errorData?.responseHeaders,
}
: {}),
},
moduleId
);
setResolvedQuery(
queryId,
{
isLoading: false,
error: errorData,
},
moduleId
);
onEvent('onDataQueryFailure', queryEvents);
return errorData;
};
// eslint-disable-next-line no-unused-vars
return new Promise(function (resolve, reject) {
if (shouldSetPreviewData) {
@ -363,9 +555,8 @@ export const createQueryPanelSlice = (set, get) => ({
} else if (query.kind === 'runpy') {
queryExecutionPromise = executeRunPycode(query.options?.code, query, false, mode, queryState, moduleId);
} else if (query.kind === 'workflows') {
queryExecutionPromise = executeWorkflow(
queryExecutionPromise = triggerWorkflow(
moduleId,
query,
query.options?.workflowId,
query.options?.blocking,
query.options?.params,
@ -395,6 +586,38 @@ export const createQueryPanelSlice = (set, get) => ({
fetchOAuthToken(url, dataQuery['data_source_id'] || dataQuery['dataSourceId']);
}
// Asynchronous query execution
// Currently async query resolution is applicable only to workflows
// Change this conditional to async query type check for other
// async queries in the future
if (query.kind === 'workflows') {
const { error, completionPromise } = get().queryPanel.setupAsyncWorkflowHandler({
data,
queryId,
processQueryResults,
handleFailure,
shouldSetPreviewData,
setPreviewData,
setResolvedQuery,
});
if (error) {
resolve({ status: 'failed', message: error });
return;
}
if (!error && completionPromise) {
// This early resolution pattern is temporary - once the UI fully supports
// tracking individual async queries through their lifecycle, we can refactor
// this to rely on the completion promise concurrently
const result = await completionPromise;
resolve(result);
}
return;
}
// Handle synchronous queries (original code)
let queryStatusCode = data?.status ?? null;
const promiseStatus = query.kind === 'runpy' ? data?.data?.status ?? 'ok' : data.status;
// Note: Need to move away from statusText -> statusCode
@ -429,120 +652,22 @@ export const createQueryPanelSlice = (set, get) => ({
errorData = data;
break;
}
if (shouldSetPreviewData) {
setPreviewLoading(false);
setPreviewData(errorData);
}
errorData = query.kind === 'runpy' || query.kind === 'runjs' ? data?.data : data;
get().debugger.log({
logLevel: 'error',
type: 'query',
kind: query.kind,
key: query.name,
message: errorData?.description,
errorTarget: 'Queries',
error:
query.kind === 'restapi'
? {
substitutedVariables: options,
request: data?.data?.requestObject,
response: data?.data?.responseObject,
}
: errorData,
isQuerySuccessLog: false,
});
setResolvedQuery(
queryId,
{
isLoading: false,
...(query.kind === 'restapi' || data.data.type === 'tj-401'
? {
metadata: data.metadata,
request: data.data.requestObject,
response: data.data.responseObject,
responseHeaders: data.data.responseHeaders,
}
: {}),
},
moduleId
);
resolve(data);
onEvent('onDataQueryFailure', queryEvents);
const result = handleFailure(errorData);
resolve(result);
return;
} else {
let rawData = data.data;
let finalData = data.data;
if (dataQuery.options.enableTransformation) {
finalData = await runTransformation(
finalData,
query.options.transformation,
query.options.transformationLanguage,
query,
'edit',
moduleId
);
if (finalData.status === 'failed') {
setResolvedQuery(
queryId,
{
isLoading: false,
},
moduleId
);
resolve(finalData);
onEvent('onDataQueryFailure', queryEvents);
setPreviewLoading(false);
if (shouldSetPreviewData) setPreviewData(finalData);
return;
}
}
if (shouldSetPreviewData) {
setPreviewLoading(false);
setPreviewData(finalData);
}
if (dataQuery.options.showSuccessNotification) {
const notificationDuration = dataQuery.options.notificationDuration * 1000 || 5000;
toast.success(dataQuery.options.successMessage, {
duration: notificationDuration,
});
}
get().debugger.log({
logLevel: 'success',
type: 'query',
kind: query.kind,
key: query.name,
message: 'Query executed successfully',
isQuerySuccessLog: true,
errorTarget: 'Queries',
});
setResolvedQuery(
queryId,
{
isLoading: false,
data: finalData,
rawData,
metadata: data?.metadata,
request: data?.metadata?.request,
response: data?.metadata?.response,
},
moduleId
);
resolve({ status: 'ok', data: finalData });
onEvent('onDataQuerySuccess', queryEvents, mode);
const rawData = data.data;
const result = await processQueryResults(data.data, rawData);
resolve(result);
}
})
.catch((e) => {
const { error } = e;
if (mode !== 'view') toast.error(error ?? 'Unknown error');
resolve({ status: 'failed', message: error });
const errorMessage = typeof error === 'string' ? error : error?.message || 'Unknown error';
if (mode !== 'view') toast.error(errorMessage);
resolve({ status: 'failed', message: errorMessage });
});
});
},
@ -556,7 +681,7 @@ export const createQueryPanelSlice = (set, get) => ({
setPreviewPanelExpanded,
executeRunPycode,
runTransformation,
executeWorkflow,
triggerWorkflow,
executeMultilineJS,
setIsPreviewQueryLoading,
} = queryPanel;
@ -616,7 +741,7 @@ export const createQueryPanelSlice = (set, get) => ({
} else if (query.kind === 'runpy') {
queryExecutionPromise = executeRunPycode(query.options.code, query, true, 'edit', queryState);
} else if (query.kind === 'workflows') {
queryExecutionPromise = executeWorkflow(
queryExecutionPromise = triggerWorkflow(
moduleId,
query.options.workflowId,
query.options.blocking,
@ -629,11 +754,73 @@ export const createQueryPanelSlice = (set, get) => ({
queryExecutionPromise
.then(async (data) => {
// Asynchronous query execution
// Currently async query resolution is applicable only to workflows
// Change this conditional to async query type check for other
// async queries in the future
if (query.kind === 'workflows') {
const processQueryResultsPreview = async (result) => {
let finalData = result;
if (query.options.enableTransformation) {
finalData = await runTransformation(
finalData,
query.options.transformation,
query.options.transformationLanguage,
query,
'edit',
moduleId
);
if (finalData.status === 'failed') {
setPreviewLoading(false);
setIsPreviewQueryLoading(false);
if (!calledFromQuery) setPreviewData(finalData);
return { status: 'failed', data: finalData };
}
}
setPreviewLoading(false);
setIsPreviewQueryLoading(false);
if (!calledFromQuery) setPreviewData(finalData);
return { status: 'ok', data: finalData };
};
const handleFailurePreview = (errorData) => {
setPreviewLoading(false);
setIsPreviewQueryLoading(false);
if (!calledFromQuery) setPreviewData(errorData);
return { status: 'failed', data: errorData };
};
const { error, completionPromise } = get().queryPanel.setupAsyncWorkflowHandler({
data,
queryId: query.id,
processQueryResults: processQueryResultsPreview,
handleFailure: handleFailurePreview,
shouldSetPreviewData: true,
setPreviewData,
setResolvedQuery: () => {}, // No resolvedQuery for preview
resolve,
});
if (!error && completionPromise) {
try {
// This early resolution pattern is temporary - once the UI fully supports
// tracking individual async queries through their lifecycle, we can refactor
// this to rely on the completion promise concurrently
const result = await completionPromise;
resolve(result);
} catch (error) {
toast.error('Async operation failed:', error);
setPreviewLoading(false);
setIsPreviewQueryLoading(false);
resolve({ status: 'failed', message: error?.message || 'Unknown error' });
}
}
return;
}
let finalData = data.data;
let queryStatusCode = data?.status ?? null;
const queryStatus = query.kind === 'runpy' ? data?.data?.status ?? 'ok' : data.status;
switch (true) {
// Note: Need to move away from statusText -> statusCode
case queryStatus === 'Bad Request' ||
queryStatus === 'Not Found' ||
queryStatus === 'Unprocessable Entity' ||
@ -665,9 +852,7 @@ export const createQueryPanelSlice = (set, get) => ({
}
onEvent('onDataQueryFailure', queryEvents);
if (!calledFromQuery) setPreviewData(errorData);
break;
}
case queryStatus === 'needs_oauth': {
@ -730,7 +915,7 @@ export const createQueryPanelSlice = (set, get) => ({
});
},
executeRunPycode: async (code, query, isPreview, mode, currentState) => {
executeRunPycode: async (code, query, isPreview, mode, currentState, _moduleId = 'canvas') => {
const {
queryPanel: { evaluatePythonCode },
} = get();
@ -950,7 +1135,13 @@ export const createQueryPanelSlice = (set, get) => ({
const {
queryPanel: { evaluatePythonCode },
} = get();
return await evaluatePythonCode({ queryResult, code, query, mode, currentState });
return await evaluatePythonCode({
queryResult,
code,
query,
mode,
currentState,
});
},
updateQuerySuggestions: (oldName, newName) => {
@ -971,7 +1162,7 @@ export const createQueryPanelSlice = (set, get) => ({
delete updatedQueries[oldName];
const oldSuggestions = Object.keys(queries[oldName]).map((key) => `queries.${oldName}.${key}`);
const _oldSuggestions = Object.keys(queries[oldName]).map((key) => `queries.${oldName}.${key}`);
// useResolveStore.getState().actions.removeAppSuggestions(oldSuggestions);
// useCurrentStateStore.getState().actions.setCurrentState({
@ -1013,10 +1204,20 @@ export const createQueryPanelSlice = (set, get) => ({
return { data: undefined, status: 'failed' };
}
},
triggerWorkflow: async (moduleId, workflowAppId, _blocking = false, params = {}, appEnvId) => {
const { getAllExposedValues } = get();
const currentState = getAllExposedValues();
const resolvedParams = get().resolveReferences(moduleId, params, currentState, {}, {});
try {
const executionResponse = await workflowExecutionsService.trigger(workflowAppId, resolvedParams, appEnvId);
return { data: executionResponse.result, status: 'ok' };
} catch (e) {
return { data: e?.message, status: 'failed' };
}
},
createProxy: (obj, path = '') => {
const { queryPanel } = get();
const { createProxy } = queryPanel;
return new Proxy(obj, {
get(target, prop) {
@ -1027,7 +1228,7 @@ export const createQueryPanelSlice = (set, get) => ({
}
const value = target[prop];
return typeof value === 'object' && value !== null ? createProxy(value, fullPath) : value;
return value;
},
});
},
@ -1209,6 +1410,48 @@ export const createQueryPanelSlice = (set, get) => ({
isQuerySelected: (queryId) => {
return get().queryPanel.selectedQuery?.id === queryId;
},
setupAsyncWorkflowHandler: ({
data,
queryId,
processQueryResults,
handleFailure,
shouldSetPreviewData,
setPreviewData,
setResolvedQuery,
}) => {
try {
const asyncHandler = get().queryPanel.createWorkflowAsyncHandler({
executionId: data.data.executionId,
queryId,
processQueryResults,
handleFailure,
shouldSetPreviewData,
setPreviewData,
setResolvedQuery,
});
// Process initial response and start SSE monitoring
const { __asyncCompletionPromise } = asyncHandler.processInitialResponse(data.data);
// Add the AsyncQueryHandler instance to asyncQueryRuns
get().queryPanel.setAsyncQueryRuns((currentRuns) => [...currentRuns, asyncHandler]);
if (setResolvedQuery) {
setResolvedQuery(queryId, {
isLoading: true,
jobId: asyncHandler.jobId,
});
}
return {
handler: asyncHandler,
completionPromise: __asyncCompletionPromise,
};
} catch (error) {
return { error };
}
},
runQueryOnShortcut: () => {
const { queryPanel } = get();
const { runQuery, selectedQuery } = queryPanel;

View file

@ -0,0 +1,141 @@
// AsyncQueryHandler manages long-running operations via server-sent events (SSE).
export class AsyncQueryHandler {
/**
* Creates a new AsyncQueryHandler
* @param {Object} options - Configuration options
* @param {Function} options.streamSSE - Function that returns an EventSource for SSE status updates
* @param {Function} options.extractJobId - Function to extract job ID from response
* @param {Function} options.classifyEventStatus - Function to classify SSE events into status categories
* @param {Object} options.callbacks - Event callbacks
* @param {Function} options.callbacks.onProgress - Progress update handler
* @param {Function} options.callbacks.onComplete - Completion handler
* @param {Function} options.callbacks.onError - Error handler
* @param {Function} options.callbacks.onClose - Close handler
*/
constructor(options = {}) {
this.config = {
streamSSE: () => {},
extractJobId: (response) => response.data?.id,
// Default implementation that doesn't make assumptions about specific status/type fields
classifyEventStatus: (data) => {
return {
// Default to treating all messages as progress updates
status: 'PROGRESS',
result: data.result || data,
// Return data for callback handlers
data,
};
},
callbacks: {
onProgress: () => {},
onComplete: () => {},
onError: () => {},
onClose: () => {},
},
...options,
};
this.eventSource = null;
this.jobId = null;
}
/**
* Processes the initial query response and starts SSE monitoring
* @param {Object} response - The initial query response
* @returns {{ __jobId: string, __cancel: Function, __asyncCompletionPromise: Promise<any> }} Status object with jobId, control methods, and completion promise
*/
processInitialResponse(response) {
const jobId = this.config.extractJobId(response);
if (!jobId) throw new Error('Could not extract job ID for async query');
this.jobId = jobId;
this.eventSource = this.startSSE(jobId);
// Return the reserved async completion promise for consumers
this.__asyncCompletionPromise =
this.__asyncCompletionPromise ||
new Promise((resolve, reject) => {
this.resolveCompletion = resolve;
this.rejectCompletion = reject;
});
return { __jobId: jobId, __cancel: () => this.cancel(), __asyncCompletionPromise: this.__asyncCompletionPromise };
}
/**
* Opens an SSE connection to receive real-time updates for the given job.
* @private
* @param {string} jobId - Identifier for the async job
* @returns {EventSource} SSE event source for updates
*/
startSSE(jobId) {
const eventSource = this.config.streamSSE(jobId);
eventSource.onmessage = (event) => this.handleMessage(event, eventSource);
eventSource.onerror = (error) => this.handleError(error, eventSource);
return eventSource;
}
/**
* Processes incoming SSE messages and delegates to the appropriate callback.
* @private
* @param {MessageEvent} event - Incoming SSE message
* @param {EventSource} eventSource - EventSource instance for the SSE connection
*/
handleMessage(event, eventSource) {
try {
const payload = JSON.parse(event.data);
const { status, result, data } = this.config.classifyEventStatus(payload);
switch (status) {
case 'PROGRESS':
this.config.callbacks.onProgress(data);
break;
case 'COMPLETE':
eventSource.close();
this.config.callbacks.onComplete(result);
this.resolveCompletion(result);
break;
case 'ERROR':
eventSource.close();
this.config.callbacks.onError(data);
this.rejectCompletion(data);
break;
case 'CLOSE':
eventSource.close();
this.config.callbacks.onClose(data);
break;
default:
this.config.callbacks.onProgress(data);
}
} catch (err) {
console.error('Error parsing SSE message:', err);
eventSource.close();
this.config.callbacks.onError({ message: 'Invalid server message', error: err });
}
}
/**
* Handles SSE connection errors and notifies onError if closed.
* @private
* @param {any} error - Error event or object
* @param {EventSource} eventSource - EventSource instance for the SSE connection
*/
handleError(error, eventSource) {
if (eventSource.readyState === EventSource.CLOSED) {
this.config.callbacks.onError({ message: 'SSE connection closed', error });
}
}
/**
* Cancels the ongoing async operation and cleans up resources.
*/
cancel() {
if (this.eventSource) {
this.eventSource.close();
}
// Notify backend to cancel the job if jobId exists
// if (this.jobId) {
// fetch(`${this.config.endpoint}/${this.jobId}/cancel`, { method: 'POST' }).catch((e) =>
// console.error('Failed to cancel async job', e)
// );
// }
}
}

View file

@ -14,6 +14,7 @@ import { useQueryPanelActions } from '@/_stores/queryPanelStore';
import { Tooltip } from 'react-tooltip';
import { canCreateDataSource } from '@/_helpers';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import { isWorkflowsFeatureEnabled } from '@/modules/common/helpers/utils';
import '../queryManager.theme.scss';
function DataSourcePicker({ dataSources, sampleDataSource, staticDataSources, darkMode, globalDataSources }) {
@ -50,7 +51,7 @@ function DataSourcePicker({ dataSources, sampleDataSource, staticDataSources, da
navigate(`/${workspaceId}/data-sources`);
};
const workflowsEnabled = window.public_config?.ENABLE_WORKFLOWS_FEATURE == 'true';
const workflowsEnabled = isWorkflowsFeatureEnabled();
return (
<>

View file

@ -14,8 +14,17 @@ import { DataBaseSources, ApiSources, CloudStorageSources } from '@/modules/comm
import { canCreateDataSource } from '@/_helpers';
import './../queryManager.theme.scss';
import { DATA_SOURCE_TYPE } from '@/_helpers/constants';
import { workflowDefaultSources } from '../constants';
function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSources, onNewNode, staticDataSources }) {
function DataSourceSelect({
isDisabled,
selectRef,
closePopup,
workflowDataSources,
onNewNode,
staticDataSources,
sampleDataSources = [],
}) {
const dataSources = useDataSources();
const globalDataSources = useGlobalDataSources();
const sampleDataSource = useSampleDataSource();
@ -32,6 +41,10 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSourc
closePopup();
};
function cleanWord(word) {
return word.replace(/default/g, '');
}
useEffect(() => {
const shouldAddSampleDataSource = !!sampleDataSource;
const allDataSources = [...dataSources, ...globalDataSources, shouldAddSampleDataSource && sampleDataSource].filter(
@ -132,6 +145,37 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSourc
...userDefinedSourcesOpts,
];
// Group sample data sources by kind
const groupedSampleDataSources =
sampleDataSources && sampleDataSources.length > 0
? Object.entries(groupBy(sampleDataSources, 'kind')).map(([kind, sources]) => ({
label: (
<div>
<DataSourceIcon source={sources[0]} height={16} />
<span className="ms-1 small">{dataSourcesKinds.find((dsk) => dsk.kind === kind)?.name || kind}</span>
</div>
),
options: sources.map((source) => ({
label: (
<div
key={source.id}
className="py-2 px-2 rounded option-nested-datasource-selector small text-truncate"
style={{ fontSize: '13px' }}
data-tooltip-id="tooltip-for-add-query-dd-option"
data-tooltip-content={decodeEntities(source.name)}
data-cy={`ds-${source.name.toLowerCase()}`}
>
{decodeEntities(source.name)}
<Tooltip id="tooltip-for-add-query-dd-option" className="tooltip query-manager-ds-select-tooltip" />
</div>
),
value: source.id,
isNested: true,
source,
})),
}))
: [];
const dataSourcesAvailable = [
{
label: (
@ -146,7 +190,7 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSourc
label: (
<div>
<DataSourceIcon source={source} height={16} />{' '}
<span className="ms-1 small">{source?.name ?? source.kind}</span>
<span className="ms-1 small"> {workflowDefaultSources[cleanWord(source.name)]?.name}</span>
</div>
),
value: source.name,
@ -154,6 +198,22 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSourc
})),
},
...userDefinedSourcesOpts,
// Sample data sources group header
...(groupedSampleDataSources.length > 0
? [
{
label: (
<div>
<span className="color-slate9" style={{ fontWeight: 500 }}>
Sample data sources
</span>
</div>
),
isDisabled: true,
},
...groupedSampleDataSources,
]
: []),
];
const dataSourceList = workflowDataSources && workflowDataSources ? dataSourcesAvailable : DataSourceOptions;

View file

@ -106,3 +106,10 @@ export const defaultSources = {
runpy: { kind: 'runpy', id: 'runpy', name: 'Run Python code' },
workflows: { kind: 'workflows', id: 'null', name: 'Run Workflow' },
};
export const workflowDefaultSources = {
...defaultSources,
'If condition': { kind: 'if', id: 'if', name: 'If condition' },
Response: { kind: 'response', id: 'response', name: 'Response' },
Loop: { kind: 'loop', id: 'loop', name: 'Loop' },
};

View file

@ -202,7 +202,7 @@ export default function AppCard({
placement="bottom"
show={appType === 'module' && props.basicPlan}
>
<div className="card homepage-app-card" ref={cardRef}>
<div className="card homepage-app-card card--clickable" ref={cardRef}>
<div
className={appType === 'module' && props.basicPlan ? 'disabled-module' : ''}
key={app?.id}

View file

@ -26,8 +26,8 @@ const AppList = (props) => {
</>
)}
{!props.isLoading && props.meta.total_count > 0 && (
<div className="container px-0">
<div className="row homepage-app-card-list-item-wrap">
<div className="">
<div className="homepage-app-card-list-item-wrap">
{props.apps.map((app) => {
return (
<div className="homepage-app-card-list-item" key={app.id}>

View file

@ -85,6 +85,12 @@ export const AppMenu = function AppMenu({
)}
{canUpdateApp && canCreateApp && appType !== 'workflow' && (
<>
{appType !== 'workflow' && (
<Field
text={t('homePage.appCard.cloneApp', 'Clone app')}
onClick={() => openAppActionModal('clone-app')}
/>
)}
<Field
text={
appType === 'workflow' ? 'Clone workflow' : appType === 'module' ? 'Clone module' : 'Clone app'
@ -113,7 +119,7 @@ export const AppMenu = function AppMenu({
</div>
}
>
<div className={'cursor-pointer menu-ico'} data-cy={`app-card-menu-icon`}>
<div className={'cursor-pointer menu-ico menu-icon--trigger'} data-cy={`app-card-menu-icon`}>
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"

View file

@ -147,34 +147,38 @@ export const BlankPage = function BlankPage({
Create new {appType !== 'workflow' ? 'application' : 'workflow'}
</ButtonSolid>
</div>
{appType !== 'workflow' && (
<div className="col-6">
<ButtonSolid
disabled={appCreationDisabled}
leftIcon="folderdownload"
onChange={readAndImport}
isLoading={isImportingApp}
data-cy="button-import-an-app"
className="col"
variant="tertiary"
<div className="col-6">
<ButtonSolid
disabled={appType !== 'workflow' ? appCreationDisabled : workflowsCreationDisabled}
leftIcon="folderdownload"
onChange={readAndImport}
isLoading={isImportingApp}
data-cy={appType !== 'workflow' ? 'button-import-an-app' : 'button-import-a-workflow'}
className="col"
variant="tertiary"
>
<label
className={cx('', {
'cursor-pointer':
appType !== 'workflow' ? !appCreationDisabled : !workflowsCreationDisabled,
})}
style={{ visibility: isImportingApp ? 'hidden' : 'visible' }}
data-cy={appType !== 'workflow' ? 'import-an-application' : 'import-a-workflow'}
>
<label
className={cx('', { 'cursor-pointer': !appCreationDisabled })}
style={{ visibility: isImportingApp ? 'hidden' : 'visible' }}
data-cy="import-an-application"
>
&nbsp;{t('blankPage.importApplication', 'Import an app')}
<input
disabled={appCreationDisabled}
type="file"
ref={fileInput}
style={{ display: 'none' }}
data-cy="import-option-input"
/>
</label>
</ButtonSolid>
</div>
)}
&nbsp;
{appType !== 'workflow'
? t('blankPage.importApplication', 'Import an app')
: t('blankPage.importWorkflow', 'Import a workflow')}
<input
disabled={appType !== 'workflow' ? appCreationDisabled : workflowsCreationDisabled}
type="file"
ref={fileInput}
style={{ display: 'none' }}
data-cy="import-option-input"
/>
</label>
</ButtonSolid>
</div>
</div>
</div>
<div className="col-5 empty-home-page-image" data-cy="empty-home-page-image">

View file

@ -14,6 +14,8 @@ import _ from 'lodash';
import { validateName, handleHttpErrorMessages, getWorkspaceId } from '@/_helpers/utils';
import { useNavigate, useLocation } from 'react-router-dom';
import FolderSkeleton from '@/_ui/FolderSkeleton/FolderSkeleton';
import { Button } from '@/components/ui/Button/Button';
export const Folders = function Folders({
folders,
foldersLoading,
@ -246,24 +248,36 @@ export const Folders = function Folders({
<div className="d-flex folder-header-icons-wrap">
{canCreateFolder && (
<>
<div
className="folder-create-btn"
<Button
size="medium"
variant="ghost"
iconOnly
ariaLabel="Create new folder"
onClick={() => {
setNewFolderName('');
setShowForm(true);
}}
data-cy="create-new-folder-button"
>
<SolidIcon name="plus" width="14" fill={darkMode ? '#ECEDEE' : '#11181C'} />
</div>
<div
className="folder-create-btn"
<SolidIcon name="plus" width="14" fill={darkMode ? '#CFD3D8E6' : '#6A727C'} />
</Button>
<Button
size="medium"
variant="ghost"
iconOnly
ariaLabel="Search for folders"
onClick={() => {
setShowInput(true);
}}
data-cy="create-new-folder-button"
>
<SolidIcon name="search" width="14" fill={darkMode ? '#ECEDEE' : '#11181C'} />
</div>
<SolidIcon
name="search"
width="14"
fill={darkMode ? '#CFD3D8E6' : '#6A727C'}
className="tw-relative tw-top-[2px]"
/>
</Button>
</>
)}
</div>
@ -287,8 +301,7 @@ export const Folders = function Folders({
className={cx(
`list-group-item border-0 list-group-item-action d-flex align-items-center all-apps-link tj-text-xsm`,
{
'bg-light-indigo': _.isEmpty(activeFolder) && !darkMode,
'bg-dark-indigo': _.isEmpty(activeFolder) && darkMode,
'tw-bg-interactive-default': _.isEmpty(activeFolder),
}
)}
style={{ height: '32px' }}
@ -314,8 +327,7 @@ export const Folders = function Folders({
className={cx(
`folder-list-group-item rounded-2 list-group-item h-4 mb-1 list-group-item-action no-border d-flex align-items-center`,
{
'bg-light-indigo': activeFolder.id === folder.id && !darkMode,
'bg-dark-indigo': activeFolder.id === folder.id && darkMode,
'tw-bg-interactive-default': activeFolder.id === folder.id,
}
)}
onClick={() => {

View file

@ -1,5 +1,5 @@
import React from 'react';
import { SearchBox } from '@/_components/SearchBox';
import { SearchBox } from '@/_components/PageSearchBox';
import { useTranslation } from 'react-i18next';
export default function HomeHeader({ onSearchSubmit, darkMode, appType }) {
@ -14,17 +14,15 @@ export default function HomeHeader({ onSearchSubmit, darkMode, appType }) {
: t('globals.workflowsSearchItem', 'Search workflows in this workspace');
return (
<div className="row">
<div className="home-search-holder">
<SearchBox
dataCy={`home-page`}
className="border-0 homepage-search"
onSubmit={onSearchSubmit}
darkMode={darkMode}
placeholder={placeholderText}
width={'100%'}
/>
</div>
<div className="home-search-holder">
<SearchBox
dataCy={'home-page'}
className="border-0 homepage-search"
onSubmit={onSearchSubmit}
darkMode={darkMode}
placeholder={placeholderText}
width={'100%'}
/>
</div>
);
}

View file

@ -12,7 +12,7 @@ import {
} from '@/_services';
import { ConfirmDialog, AppModal, ToolTip } from '@/_components';
import Select from '@/_ui/Select';
import _, { sample, isEmpty, capitalize } from 'lodash';
import _, { sample, isEmpty, capitalize, has } from 'lodash';
import { Folders } from './Folders';
import { BlankPage } from './BlankPage';
import { toast } from 'react-hot-toast';
@ -48,6 +48,7 @@ import {
} from '@/modules/dashboard/components';
import CreateAppWithPrompt from '@/modules/AiBuilder/components/CreateAppWithPrompt';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import { isWorkflowsFeatureEnabled } from '@/modules/common/helpers/utils';
import EmptyModuleSvg from '../../assets/images/icons/empty-modules.svg';
const { iconList, defaultIcon } = configs;
@ -256,7 +257,11 @@ class HomePageComponent extends React.Component {
};
getAppType = () => {
return this.props.appType === 'module' ? 'Module' : this.props.appType === 'workflow' ? 'Workflow' : 'App';
const { appType } = this.props;
if (appType === 'front-end') return 'App';
if (appType === 'workflow') return 'Workflow';
if (appType === 'module') return 'Module';
return 'app';
};
createApp = async (appName, type, prompt) => {
@ -339,6 +344,66 @@ class HomePageComponent extends React.Component {
this.setState({ isExportingApp: true, app: app });
};
exportAppDirectly = async (app) => {
try {
const fetchVersions = await appsService.getVersions(app.id);
const { versions } = fetchVersions;
const currentEditingVersion = versions?.filter((version) => version?.isCurrentEditingVersion)[0];
if (!currentEditingVersion) {
toast.error('Could not find current editing version.', {
position: 'top-center',
});
return;
}
// Export all TJDB tables used by default
const fetchTables = await appsService.getTables(app.id);
const { tables: allTables } = fetchTables;
const versionId = currentEditingVersion.id;
const exportTjDb = true;
const exportTables = allTables;
const appOpts = {
app: [
{
id: app.id,
search_params: { version_id: versionId },
},
],
};
const requestBody = {
...appOpts,
...(exportTjDb && { tooljet_database: exportTables }),
organization_id: app.organization_id,
};
const data = await appsService.exportResource(requestBody);
const appName = app.name.replace(/\s+/g, '-').toLowerCase();
const fileName = `${appName}-export-${new Date().getTime()}`;
const json = JSON.stringify(data, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const href = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = href;
link.download = fileName + '.json';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
toast.success('Workflow exported successfully!', {
position: 'top-center',
});
} catch (error) {
toast.error(`Could not export workflow: ${error?.data?.message || error.message}`, {
position: 'top-center',
});
}
};
readAndImport = (event) => {
try {
const file = event.target.files[0];
@ -413,7 +478,7 @@ class HomePageComponent extends React.Component {
let installedPluginsInfo = [];
try {
if (this.state.dependentPlugins.length) {
({ installedPluginsInfo = [] } = await pluginsService.installDependentPlugins(
({ installedPluginsInfo =[] } = await pluginsService.installDependentPlugins(
this.state.dependentPlugins,
true
));
@ -421,8 +486,7 @@ class HomePageComponent extends React.Component {
if (importJSON.app[0].definition.appV2.type !== this.props.appType) {
toast.error(
`${this.props.appType === 'module' ? 'App' : 'Module'} could not be imported in ${
this.props.appType === 'module' ? 'modules' : 'apps'
`${this.props.appType === 'module' ? 'App' : 'Module'} could not be imported in ${this.props.appType === 'module' ? 'modules' : 'apps'
} section. Switch to ${this.props.appType === 'module' ? 'apps' : 'modules'} section and try again.`,
{ style: { maxWidth: '425px' } }
);
@ -453,7 +517,7 @@ class HomePageComponent extends React.Component {
this.setState({ isImportingApp: false });
if (error.statusCode === 409) return false;
toast.error(error?.error || error?.message || 'App import failed');
toast.error(error?.error || error?.message || `${capitalize(this.getAppType())} import failed`);
}
};
@ -485,7 +549,7 @@ class HomePageComponent extends React.Component {
};
canViewWorkflow = () => {
return this.canUserPerform(this.state.currentUser, 'view');
return this.canUserPerform(this.state.currentUser, 'view') && isWorkflowsFeatureEnabled();
};
canUserPerform(user, action, app) {
@ -953,6 +1017,53 @@ class HomePageComponent extends React.Component {
importingGitAppOperations: validationMessage,
});
};
// Helper functions for workflow limit checks
hasWorkflowLimitReached = () => {
const { workflowInstanceLevelLimit, workflowWorkspaceLevelLimit } = this.state;
const instanceLimitReached =
workflowInstanceLevelLimit.total === 0 || workflowInstanceLevelLimit.current >= workflowInstanceLevelLimit.total;
const workspaceLimitReached =
workflowWorkspaceLevelLimit.total === 0 ||
workflowWorkspaceLevelLimit.current >= workflowWorkspaceLevelLimit.total;
return instanceLimitReached || workspaceLimitReached;
};
hasWorkflowLimitWarning = () => {
const { workflowInstanceLevelLimit, workflowWorkspaceLevelLimit } = this.state;
return this.hasInstanceLimitWarning() || this.hasWorkspaceLimitWarning();
};
hasInstanceLimitWarning = () => {
const { workflowInstanceLevelLimit } = this.state;
const percentage = workflowInstanceLevelLimit.percentage;
return (
workflowInstanceLevelLimit.current >= workflowInstanceLevelLimit.total ||
(percentage >= 90 && percentage < 100) ||
workflowInstanceLevelLimit.current === workflowInstanceLevelLimit.total - 1
);
};
hasWorkspaceLimitWarning = () => {
const { workflowWorkspaceLevelLimit } = this.state;
const percentage = workflowWorkspaceLevelLimit.percentage;
return (
workflowWorkspaceLevelLimit.current >= workflowWorkspaceLevelLimit.total ||
(percentage >= 90 && percentage < 100) ||
workflowWorkspaceLevelLimit.current === workflowWorkspaceLevelLimit.total - 1
);
};
getWorkflowLimit = () => {
return this.hasInstanceLimitWarning()
? this.state.workflowInstanceLevelLimit
: this.state.workflowWorkspaceLevelLimit;
};
render() {
const {
apps,
@ -1012,7 +1123,7 @@ class HomePageComponent extends React.Component {
} else if (this.props.appType === 'front-end') {
return appsLimit?.percentage >= 100;
} else {
return workflowInstanceLevelLimit.percentage >= 100 || workflowWorkspaceLevelLimit.percentage >= 100;
return this.hasWorkflowLimitReached();
}
};
const modalConfigs = {
@ -1113,9 +1224,8 @@ class HomePageComponent extends React.Component {
<div className="groups-list">
<div
className={`border rounded text-sm container ${
missingGroupsExpanded ? 'max-h-48 overflow-y-auto' : ''
}`}
className={`border rounded text-sm container ${missingGroupsExpanded ? 'max-h-48 overflow-y-auto' : ''
}`}
>
<div style={{ color: 'var(--text-placeholder)' }} className="tj-text-xsm font-weight-500">
User groups
@ -1191,8 +1301,8 @@ class HomePageComponent extends React.Component {
this.props.appType === 'workflow'
? 'homePage.deleteWorkflowAndData'
: this.props.appType === 'front-end'
? 'homePage.deleteAppAndData'
: deleteModuleText,
? 'homePage.deleteAppAndData'
: deleteModuleText,
{
appName: appToBeDeleted?.name,
}
@ -1457,22 +1567,18 @@ class HomePageComponent extends React.Component {
{this.props.appType === 'module'
? 'Create new module'
: this.props.t(
`${
this.props.appType === 'workflow' ? 'workflowsDashboard' : 'homePage'
}.header.createNewApplication`,
'Create new app'
)}
`${this.props.appType === 'workflow' ? 'workflowsDashboard' : 'homePage'
}.header.createNewApplication`,
'Create new app'
)}
</>
</Button>
{this.props.appType !== 'workflow' && (
<Dropdown.Toggle
disabled={getDisabledState()}
split
className="d-inline"
data-cy="import-dropdown-menu"
/>
)}
<Dropdown.Toggle
disabled={getDisabledState()}
split
className="d-inline"
data-cy="import-dropdown-menu"
/>
<ImportAppMenu
darkMode={this.props.darkMode}
showTemplateLibraryModal={
@ -1525,8 +1631,8 @@ class HomePageComponent extends React.Component {
classes="mb-3 small"
limits={
workflowInstanceLevelLimit.current >= workflowInstanceLevelLimit.total ||
100 > workflowInstanceLevelLimit.percentage >= 90 ||
workflowInstanceLevelLimit.current === workflowInstanceLevelLimit.total - 1
100 > workflowInstanceLevelLimit.percentage >= 90 ||
workflowInstanceLevelLimit.current === workflowInstanceLevelLimit.total - 1
? workflowInstanceLevelLimit
: workflowWorkspaceLevelLimit
}
@ -1545,12 +1651,7 @@ class HomePageComponent extends React.Component {
<OrganizationList customStyle={{ marginBottom: isAdmin || isBuilder ? '' : '0px' }} />
</div>
<div
className={cx('col home-page-content', {
'bg-light-gray': !this.props.darkMode,
})}
data-cy="home-page-content"
>
<div className={cx('col home-page-content')} data-cy="home-page-content">
<div className="w-100 mb-5 container home-page-content-container">
{featuresLoaded && !isLoading ? (
<>
@ -1577,15 +1678,12 @@ class HomePageComponent extends React.Component {
{(meta?.total_count > 0 || appSearchKey) && (
<>
{!(isLoading && !appSearchKey) && (
<>
<HomeHeader
onSearchSubmit={this.onSearchSubmit}
darkMode={this.props.darkMode}
appType={this.props.appType}
disabled={this.props.appType === 'module' && invalidLicense}
/>
<div className="liner"></div>
</>
<HomeHeader
onSearchSubmit={this.onSearchSubmit}
darkMode={this.props.darkMode}
appType={this.props.appType}
disabled={this.props.appType === 'module' && invalidLicense}
/>
)}
<div className="filter-container">
<span>{currentFolder?.count ?? meta?.total_count} APPS</span>
@ -1633,8 +1731,8 @@ class HomePageComponent extends React.Component {
appType={this.props.appType}
workflowsLimit={
workflowInstanceLevelLimit.current >= workflowInstanceLevelLimit.total ||
100 > workflowInstanceLevelLimit.percentage >= 90 ||
workflowInstanceLevelLimit.current === workflowInstanceLevelLimit.total - 1
100 > workflowInstanceLevelLimit.percentage >= 90 ||
workflowInstanceLevelLimit.current === workflowInstanceLevelLimit.total - 1
? workflowInstanceLevelLimit
: workflowWorkspaceLevelLimit
}
@ -1679,7 +1777,7 @@ class HomePageComponent extends React.Component {
canUpdateApp={this.canUpdateApp}
deleteApp={this.deleteApp}
cloneApp={this.cloneApp}
exportApp={this.exportApp}
exportApp={this.props.appType === 'workflow' ? this.exportAppDirectly : this.exportApp}
meta={meta}
currentFolder={currentFolder}
isLoading={isLoading || !featuresLoaded}

View file

@ -0,0 +1,295 @@
.home-page-sidebar {
height: calc(100vh - 48px) !important; //64 is navbar height
.folder-list-user {
height: calc(100vh - 116px) !important; //64 is navbar height + 52 px footer
}
}
.app-list {
margin: 24px 0;
}
.home-search-holder {
height: 48px;
width: 100%;
margin-top: 32px;
}
.homepage-app-card-list-item-wrap {
column-gap: 24px;
row-gap: 24px;
flex-wrap: wrap;
display: flex;
}
.homepage-app-card-list-item {
max-width: 272px;
flex-basis: 33%;
padding: 0 !important;
}
.homepage-dropdown-style {
min-width: 11rem;
display: block;
align-items: center;
margin: 0;
line-height: 1.4285714;
width: 100%;
padding: 0.5rem 0.75rem !important;
font-weight: 400;
white-space: nowrap;
border: 0;
cursor: pointer;
font-size: 12px;
}
.homepage-dropdown-style:hover {
background: rgba(101, 109, 119, 0.06);
}
.menu-icon--trigger {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 10px;
background-color: var(--background-surface-layer-01);
box-shadow: none;
transition: all 0.15s ease-in-out;
will-change: background-color, box-shadow;
&:hover {
background-color: var(--background-surface-layer-02);
box-shadow: var(--elevation-000-box-shadow);
}
}
.home-app-card-header {
margin-bottom: 32px;
}
.homepage-app-card {
height: 160px;
padding: 16px;
.app-icon-main {
background: var(--indigo3) !important;
border-radius: 6px !important;
display: flex;
justify-content: center;
align-items: center;
width: 48px;
height: 48px;
will-change: height, width;
transition: all 0.15s ease-in-out;
}
.appcard-buttons-wrap {
visibility: hidden;
opacity: 0;
height: 0;
}
.home-app-card-header {
.menu-ico {
visibility: hidden !important;
}
}
&:hover {
.home-app-card-header {
margin-bottom: 12px;
.menu-ico {
visibility: visible !important;
}
}
.app-creation-time-container {
margin-bottom: 0px;
}
.app-card-name {
margin-bottom: 0px;
}
.app-creation-time {
// display: none;
}
.appcard-buttons-wrap {
display: flex;
visibility: visible;
opacity: 1;
padding: 0px;
gap: 12px;
width: 240px;
height: 28px;
flex-direction: row;
transition: all 0.15s ease-in-out;
will-change: opacity, visibility;
div {
a {
text-decoration: none;
}
}
}
.app-icon-main {
width: 36px;
height: 36px;
}
}
}
.home-page-content-container {
max-width: 880px;
@media only screen and (max-width: 768px) {
margin-bottom: 0rem !important;
.liner {
width: unset !important;
}
.app-list {
overflow-y: auto;
height: calc(100vh - 26rem);
.skeleton-container {
display: flex;
flex-direction: column;
.col {
display: flex;
justify-content: center;
margin-bottom: 1rem;
}
.card-skeleton-container {
width: 304px;
}
}
}
.menu-ico {
display: none !important;
}
}
}
.home-page-footer {
height: 52px;
background-color: var(--page-weak) !important;
border-top: 1px solid var(--border-weak) !important;
width: calc(100% - 336px) !important;
@media only screen and (max-width: 768px) {
position: unset;
width: 100%;
.col-4,
.col-5 {
display: none;
}
.pagination-container {
display: flex !important;
align-items: center;
justify-content: center;
}
}
}
@media only screen and (min-width: 1728px) {
.homepage-app-card-list-item {
// max-width: 304px;
max-width: calc(33.3% - 16px);
.edit-button,
.launch-button {
width: 129px !important;
}
}
.home-page-content-container {
max-width: 976px;
}
.liner {
width: 976px;
}
}
@media only screen and (min-width: 1584px) and (max-width: 1727px) {
.homepage-app-card-list-item {
max-width: calc(33.3% - 16px);
}
.edit-button,
.launch-button {
width: 113px !important;
}
}
@media only screen and (min-width: 1312px) and (max-width: 1583px) {
.homepage-app-card-list-item {
// max-width: 264px;
max-width: calc(33.3% - 16px);
.edit-button,
.launch-button {
width: 109px !important;
}
}
}
@media only screen and (min-width: 993px) and (max-width: 1311px) {
.home-page-content-container {
max-width: 568px;
}
.homepage-app-card-list-item-wrap {
row-gap: 20px;
}
.homepage-app-card-list-item {
// max-width: 269px;
max-width: calc(50% - 12px);
flex-basis: 50%;
flex-grow: 1;
flex-shrink: 0;
.edit-button,
.launch-button {
width: 111.5px !important;
}
}
.liner {
width: 568px;
}
}
@media only screen and (max-width: 992px) {
.homepage-app-card-list-item-wrap {
display: flex;
justify-content: center;
width: 100%;
gap: 24px;
}
.homepage-app-card-list-item {
// max-width: 304px !important;
max-width: calc(50% - 12px);
flex-basis: 100%;
.edit-button,
.launch-button {
width: 129px !important;
}
}
}

View file

@ -52,7 +52,7 @@ export const MarketplaceCard = ({ id, name, repo, description, version, isInstal
return (
<div className="col-sm-6 col-lg-4">
<div className="plugins-card card-borderless">
<div className="card plugins-card card-borderless">
<div className="card-body card-body-alignment">
<div className="row align-items-center">
<div className="col-auto">

View file

@ -169,7 +169,7 @@ function SettingsPage(props) {
<div className="page-wrapper profile-page-content-wrap">
<div style={{ height: `calc(100vh - 2.5rem - 64px)` }}>
<div className="container-xl">
<div className="profile-page-card">
<div className="card profile-page-card">
<div className="card-header">
<h3 className="card-title" data-cy="card-title-profile">
{t('header.profileSettingPage.profile', 'Profile')}
@ -244,8 +244,7 @@ function SettingsPage(props) {
<div></div>
</div>
</div>
<br />
<div className="profile-page-card">
<div className="card profile-page-card tw-mt-16">
<div className="card-header">
<h3 className="card-title" data-cy="card-title-change-password">
{t('header.profileSettingPage.changePassword', 'Change password')}

View file

@ -368,6 +368,12 @@ function TableSchema({
isDisabled={
isEditMode && columnDetails[index]?.constraints_type?.is_primary_key === true ? true : false
}
classNames={{
control: (state) => cx({
'!tw-border-border-default': true,
}),
}}
/>
</div>
</ToolTip>

View file

@ -548,7 +548,7 @@
}
.empty-foreignkey-container {
border: 1px dashed #d7dbdf;
border: 1px dashed var(--border-default);
height: 40px;
width: 270px !important;
border-radius: 100px !important;

View file

@ -148,8 +148,8 @@ const Header = ({
return (
<>
<div className="database-table-header-wrapper">
<div className="card border-0">
<div className="card-body tj-db-operations-header">
<div className="border-0">
<div className="tj-db-operations-header">
<div className="row align-items-center">
<div className="col-8 align-items-center gap-1" style={{ padding: '0 16px' }}>
<>

View file

@ -97,8 +97,8 @@
z-index: 1;
position: sticky;
left: 66px;
border-right: 2px solid var(--light-slate-08, #C1C8CD);
background-color: white;
border-right: 2px solid var(--border-weak);
background-color: var(--surfaces-surface-01);
}
th {
@ -145,14 +145,14 @@
th:nth-child(2) {
z-index: 2;
left: 66px;
border-right: 2px solid var(--light-slate-08, #C1C8CD);
border-right: 2px solid var(--border-weak);
}
.dark-background {
td:nth-child(1),
td:nth-child(2) {
background-color: #2B394A;
background-color: var(--surfaces-surface-01);
}
}
@ -283,26 +283,6 @@
background-color: #2B2F30 !important;
}
.empty-table-description {
font-size: 14px !important;
line-height: 20px !important;
margin-top: 5px !important;
}
.empty-table-container {
display: flex;
align-items: center;
justify-content: center;
height: calc(100% - 95px);
}
.tjdb-create-new-table {
width: 180px !important;
margin: 0px auto !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
.keyPress-actions {

View file

@ -8,6 +8,7 @@ import { ListItem } from '../TableListItem';
import { BreadCrumbContext } from '../../App/App';
import Search from '../Search';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import { Button } from '@/components/ui/Button/Button';
const List = () => {
const {
@ -83,15 +84,23 @@ const List = () => {
<>
<span>All tables ({filteredTables.length})</span>
<div
className="folder-create-btn search-icon-wrap"
<Button
size="medium"
variant="ghost"
iconOnly
ariaLabel="Search for folders"
onClick={() => {
setShowInput(true);
}}
data-cy="create-new-folder-button"
>
<SolidIcon name="search" width="14" fill={darkMode ? '#ECEDEE' : '#11181C'} />
</div>
<SolidIcon
name="search"
width="14"
fill={darkMode ? '#CFD3D8E6' : '#6A727C'}
className="tw-relative tw-top-[2px]"
/>
</Button>
</>
) : (
<Search

View file

@ -7,6 +7,7 @@ import { BreadCrumbContext } from '@/App/App';
import { useNavigate } from 'react-router-dom';
import { pageTitles, fetchAndSetWindowTitle } from '@white-label/whiteLabelling';
import { hasBuilderRole } from '@/_helpers/utils';
import './styles/styles.scss';
export const TooljetDatabaseContext = createContext({
organizationId: null,

View file

@ -0,0 +1,249 @@
.layout-header .tj-dashboard-header-wrap {
display: flex;
align-items: center;
justify-content: space-between;
background-color: var(--page-weak);
padding-top: 8px;
padding-bottom: 8px;
padding-left: 40px;
height: 48px;
border-bottom: 1px solid var(--border-weak);
@media only screen and (max-width: 768px) {
border-bottom: none;
}
}
.tooljet-database {
.search-icon-wrap svg {
position: relative;
top: 1px;
left: 1px;
}
.create-new-table-btn {
width: 248px;
button {
height: 40px !important;
}
}
.tooljet-database-sidebar {
max-width: 288px;
background: var(--page-weak);
border-right: 1px solid var(--border-weak);
height: calc(100vh - 48px) !important;
.sidebar-container {
height: 40px !important;
margin: 12px auto 0;
display: flex;
justify-content: center;
}
.sidebar-container-with-banner {
height: 40px !important;
padding-top: 1px !important;
margin: 0 auto;
display: flex;
justify-content: center;
}
}
// TABLE
.table-left-sidebar {
display: flex;
flex-direction: column;
overflow-y: auto;
}
.toojet-db-table-footer {
height: 52px;
background: var(--page-weak) !important;
width: calc(100vw - 336px);
}
.toojet-db-table-footer-collapse {
height: 52px;
background: var(--page-weak) !important;
width: calc(100vw - 48px);
}
.toojet-db-table-footer-collapse {
height: 52px;
background: var(--page-weak) !important;
width: calc(100vw - 48px);
}
.database-page-content-wrap {
background-color: var(--page-weak);
height: calc(100vh - 64px) !important;
}
.instance-settings-wrapper {
}
.database-page-content-wrap {
height: calc(100vh - 64px) !important;
}
.empty-table-description {
font-size: 14px !important;
line-height: 20px !important;
margin-top: 5px !important;
}
.empty-table-container {
display: flex;
align-items: center;
justify-content: center;
height: calc(100% - 95px);
}
.tjdb-create-new-table {
width: 180px !important;
margin: 0px auto !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
.tj-db-operations-header {
height: 56px;
padding: 0 !important;
display: flex;
align-items: center;
background-color: var(--page-weak);
.row {
margin-left: 0px;
width: 98%;
}
.col-8 {
padding-left: 0px;
display: flex;
gap: 12px;
align-items: center;
}
}
.add-new-column-btn {
margin-left: 16px;
height: 28px;
border-radius: 6px;
padding: 0 !important;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
color: var(--slate12);
border: none;
}
.tj-db-filter-btn {
width: 100%;
height: 28px;
display: flex;
border-radius: 6px;
background: transparent;
color: var(--slate12);
border: none;
display: flex;
align-items: center;
justify-content: center;
}
.tj-db-filter-btn-applied,
.tj-db-sort-btn-applied {
display: flex !important;
flex-direction: row !important;
justify-content: center !important;
align-items: center !important;
width: 100% !important;
height: 28px !important;
background: var(--grass2) !important;
border-radius: 6px !important;
}
.tj-db-filter-btn-applied,
.tj-db-filter-clear-icon {
background-color: var(--indigo4) !important;
color: var(--indigo9) !important;
&:hover {
background-color: var(--button-secondary-pressed) !important;
}
}
.tj-db-filter-clear-icon {
border-radius: 0px 6px 6px 0px;
}
.tj-db-filter-btn-active,
.tj-db-sort-btn-active {
display: flex !important;
flex-direction: row !important;
justify-content: center !important;
align-items: center !important;
width: 100% !important;
height: 28px !important;
border-radius: 6px !important;
background: var(--indigo4) !important;
color: var(--indigo9) !important;
}
.tj-db-filter-btn-active {
background: var(--button-outline-pressed) !important;
color: var(--text-default) !important;
}
.tj-db-filter-btn-active-filter {
display: flex !important;
flex-direction: row !important;
justify-content: center !important;
align-items: center !important;
width: 100% !important;
height: 28px !important;
border-radius: 6px !important;
background: var(--button-secondary-pressed) !important;
color: var(--text-brand) !important;
}
.tj-db-header-add-new-row-btn {
height: 28px;
background: transparent;
border-radius: 6px !important;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
gap: 6px;
border: none;
padding: span {
}
}
.tj-db-sort-btn {
width: 100%;
height: 28px;
background: transparent;
color: var(--slate12);
border: none;
display: flex;
align-items: center;
justify-content: center;
margin: 0;
}
.edit-row-btn {
background: transparent;
color: var(--slate12);
border: none;
display: flex;
align-items: center;
justify-content: center;
}
}

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.3884 6L7.61156 6C6.45387 6 5.73256 7.25582 6.31589 8.25581L10.7043 15.7789C11.2832 16.7711 12.7169 16.7711 13.2957 15.7789L17.6841 8.25581C18.2674 7.25582 17.5461 6 16.3884 6Z" fill="#6A727C"/>
</svg>

After

Width:  |  Height:  |  Size: 310 B

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 16.3885V7.61157C7 6.45389 8.25582 5.73258 9.25581 6.3159L16.7789 10.7043C17.7711 11.2832 17.7711 12.7169 16.7789 13.2957L9.25581 17.6841C8.25582 18.2675 7 17.5461 7 16.3885Z" fill="#6A727C"/>
</svg>

After

Width:  |  Height:  |  Size: 307 B

View file

@ -253,6 +253,8 @@ const DynamicForm = ({
}) => {
const source = schema?.source?.kind;
const darkMode = localStorage.getItem('darkMode') === 'true';
const workspaceConstant = options?.[key]?.workspace_constant;
const isWorkspaceConstant = !!workspaceConstant;
if (!options) return;
@ -264,7 +266,7 @@ const DynamicForm = ({
(options?.[key]?.encrypted !== undefined ? options?.[key].encrypted : encrypted) || type === 'password';
return {
type,
placeholder: useEncrypted ? '**************' : description,
placeholder: workspaceConstant ? workspaceConstant : useEncrypted ? '**************' : description,
className: `form-control${handleToggle(controller)} ${useEncrypted && 'dynamic-form-encrypted-field'}`,
style: { marginBottom: '0px !important' },
value: options?.[key]?.value || '',
@ -276,6 +278,7 @@ const DynamicForm = ({
workspaceVariables,
workspaceConstants: currentOrgEnvironmentConstants,
encrypted: useEncrypted,
isWorkspaceConstant: isWorkspaceConstant,
};
}
case 'toggle':
@ -509,10 +512,16 @@ const DynamicForm = ({
return;
}
const isEditing = computedProps[field]['disabled'];
const workspaceConstant = options?.[field]?.workspace_constant;
const isWorkspaceConstant = !!workspaceConstant;
if (isEditing) {
optionchanged(field, '');
if (isWorkspaceConstant) {
optionchanged(field, workspaceConstant);
} else {
optionchanged(field, '');
}
} else {
//Send old field value if editing mode disabled for encrypted fields
const newOptions = { ...options };
const oldFieldValue = selectedDataSource?.['options']?.[field];
if (oldFieldValue) {

View file

@ -8,7 +8,6 @@ import Headers from '@/_ui/HttpHeaders';
import Toggle from '@/_ui/Toggle';
import InputV3 from '@/_ui/Input-V3';
import { filter, find, isEmpty } from 'lodash';
import { ButtonSolid } from './AppButton';
import { useGlobalDataSourcesStatus } from '@/_stores/dataSourcesStore';
import { canDeleteDataSource, canUpdateDataSource } from '@/_helpers';
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
@ -206,39 +205,63 @@ const DynamicFormV2 = ({
}
const processFields = (fieldsObject) => {
Object.keys(fieldsObject).forEach((key) => {
const field = fieldsObject[key];
const { widget, encrypted, key: propertyKey } = field;
const processNestedField = (field, propertyKey) => {
const { widget, encrypted } = field;
if (!canUpdateDataSource(selectedDataSource?.id) && !canDeleteDataSource()) {
encryptedFieldsProps[propertyKey] = {
disabled: !!selectedDataSource?.id,
};
} else if (!isDataSourceEditing) {
if (widget === 'password' || encrypted) {
encryptedFieldsProps[propertyKey] = {
disabled: true,
};
}
} else {
if ((widget === 'password' || encrypted) && !(propertyKey in computedProps)) {
const isEncryptedField =
widget === 'password-v3' ||
widget === 'password-v3-textarea' ||
widget === 'password' ||
encrypted ||
encryptedProperties.includes(propertyKey);
if (isEncryptedField) {
if (computedProps[propertyKey] !== undefined && computedProps[propertyKey].disabled === false) {
encryptedFieldsProps[propertyKey] = { disabled: false };
} else if (!isDataSourceEditing) {
encryptedFieldsProps[propertyKey] = { disabled: true };
} else if (!(propertyKey in computedProps)) {
encryptedFieldsProps[propertyKey] = {
disabled: !!selectedDataSource?.id,
};
}
}
};
// To check for nested dropdown-component-flip
if (widget === 'dropdown-component-flip') {
const selectedOption = options?.[field.key]?.value;
Object.keys(fieldsObject).forEach((key) => {
const field = fieldsObject[key];
if (field.commonFields) {
processFields(field.commonFields);
if (field.key) {
processNestedField(field, field.key);
}
// Check for nested structures and recursively process them
if (typeof field === 'object') {
if (field.widget === 'dropdown-component-flip') {
const selectedOption = options?.[field.key]?.value;
if (field.commonFields) {
Object.keys(field.commonFields).forEach((commonKey) => {
const commonField = field.commonFields[commonKey];
processNestedField(commonField, commonField.key);
});
}
if (selectedOption && fieldsObject[selectedOption]) {
processFields(fieldsObject[selectedOption]);
}
}
if (selectedOption && fieldsObject[selectedOption]) {
processFields(fieldsObject[selectedOption]);
}
// For other nested objects, recursively process them
Object.keys(field).forEach((subKey) => {
if (typeof field[subKey] === 'object' && field[subKey] !== null) {
if (field[subKey].widget || field[subKey].key) {
processNestedField(field[subKey], field[subKey].key);
} else {
processFields({ [subKey]: field[subKey] });
}
}
});
}
});
};
@ -264,6 +287,11 @@ const DynamicFormV2 = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedDataSource?.id, options, isDataSourceEditing]);
React.useEffect(() => {
const requiredFields = processAllOfConditions(schema, options);
setConditionallyRequiredProperties(requiredFields);
}, [options, processAllOfConditions, schema, selectedDataSource.id]);
const getElement = (type) => {
switch (type) {
case 'password':
@ -295,6 +323,8 @@ const DynamicFormV2 = ({
const currentValue = options?.[key]?.value;
const skipValidation =
(!hasUserInteracted && !showValidationErrors) || (!interactedFields.has(key) && !showValidationErrors);
const workspaceConstant = options?.[key]?.workspace_constant;
const isEditing = computedProps[key] && computedProps[key].disabled === false;
const handleOptionChange = (key, value, flag = true) => {
if (!hasUserInteracted) {
@ -309,10 +339,10 @@ const DynamicFormV2 = ({
case 'text':
case 'textarea': {
return {
key,
propertyKey: key,
widget,
label,
placeholder: isEncrypted ? '**************' : description,
placeholder: workspaceConstant ? workspaceConstant : isEncrypted ? '**************' : description,
className: cx('form-control', {
'dynamic-form-encrypted-field': isEncrypted,
}),
@ -321,20 +351,20 @@ const DynamicFormV2 = ({
value: currentValue || '',
onChange: (e) => optionchanged(key, e.target.value, true),
isGDS: true,
workspaceVariables: [],
workspaceConstants: [],
encrypted: isEncrypted,
onBlur,
workspaceVariables,
workspaceConstants: currentOrgEnvironmentConstants,
};
}
case 'password-v3':
case 'password-v3-textarea':
case 'text-v3': {
return {
key,
propertyKey: key,
widget,
label,
placeholder: isEncrypted ? '**************' : description,
placeholder: workspaceConstant ? workspaceConstant : isEncrypted ? '**************' : description,
className: cx('form-control', {
'dynamic-form-encrypted-field': isEncrypted,
}),
@ -343,8 +373,6 @@ const DynamicFormV2 = ({
value: currentValue || '',
onChange: (e) => handleOptionChange(key, e.target.value, true),
isGDS: true,
workspaceVariables: [],
workspaceConstants: [],
encrypted: isEncrypted,
onBlur,
isRequired: isRequired,
@ -356,6 +384,10 @@ const DynamicFormV2 = ({
? { valid: true, message: '' }
: { valid: null, message: '' }, // handle optional && encrypted fields
isDisabled: !canUpdateDataSource(selectedDataSource?.id) && !canDeleteDataSource(),
workspaceVariables,
workspaceConstants: currentOrgEnvironmentConstants,
isEditing: isEditing,
labelDisabled: false,
};
}
case 'react-component-headers': {
@ -411,11 +443,18 @@ const DynamicFormV2 = ({
if (!canUpdateDataSource(selectedDataSource?.id) && !canDeleteDataSource()) {
return;
}
const isEditing = computedProps[field]['disabled'];
const workspaceConstant = options?.[field]?.workspace_constant;
const isWorkspaceConstant = !!workspaceConstant;
if (isEditing) {
optionchanged(field, '');
if (isWorkspaceConstant) {
optionchanged(field, workspaceConstant);
} else {
optionchanged(field, '');
}
} else {
//Send old field value if editing mode disabled for encrypted fields
const newOptions = { ...options };
const oldFieldValue = selectedDataSource?.['options']?.[field];
if (oldFieldValue) {
@ -425,6 +464,7 @@ const DynamicFormV2 = ({
optionsChanged({ ...newOptions });
}
}
setComputedProps({
...computedProps,
[field]: {
@ -511,6 +551,7 @@ const DynamicFormV2 = ({
dataCy={uiProperties[key].key.replace(/_/g, '-')}
//to be removed after whole ui is same
isHorizontalLayout={isHorizontalLayout}
handleEncryptedFieldsToggle={handleEncryptedFieldsToggle}
/>
</div>
</div>

View file

@ -38,7 +38,7 @@ export const NotificationCenter = ({ darkMode }) => {
const overlay = (
<div
className={`notification-center dropdown-menu dropdown-menu-arrow dropdown-menu-end dropdown-menu-card ${
className={`notification-center dropdown-menu dropdown-menu-arrow dropdown-menu-end !tw-rounded-lg dropdown-menu-card ${
darkMode && 'dark-theme'
}`}
data-bs-popper="static"

View file

@ -0,0 +1,111 @@
import React, { useState, useEffect, forwardRef } from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import useDebounce from '@/_hooks/useDebounce';
import { useMounted } from '@/_hooks/use-mount';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import './_styles/page-search-box.scss';
export const SearchBox = forwardRef(
(
{
width = '200px',
onSubmit,
className,
debounceDelay = 300,
darkMode = false,
placeholder = 'Search',
customClass = '',
dataCy = '',
callBack,
onClearCallback,
autoFocus = false,
showClearButton,
initialValue = '',
clearTextOnBlur = true,
},
ref
) => {
const [searchText, setSearchText] = useState('');
const debouncedSearchTerm = useDebounce(searchText, debounceDelay);
const [isFocused, setFocussed] = useState(false);
const handleChange = (e) => {
setSearchText(e.target.value);
callBack?.(e);
};
const clearSearchText = () => {
setSearchText('');
onClearCallback?.();
};
const handleClickOutside = (event) => {
if (ref?.current && !ref.current.contains(event.target) && clearTextOnBlur) {
clearSearchText();
// Your function to be triggered
}
};
const mounted = useMounted();
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside);
if (mounted) {
onSubmit?.(debouncedSearchTerm);
}
return () => {
// Cleanup event listener on component unmount
document.removeEventListener('mousedown', handleClickOutside);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedSearchTerm, onSubmit]);
useEffect(() => {
initialValue !== undefined && setSearchText(initialValue);
}, [initialValue]);
return (
<div className={`ghost-search-box-wrapper ${customClass}`}>
<div className="input-icon">
{!isFocused && (
<span className="input-icon-addon">
<SolidIcon name="search" width="14" />
</span>
)}
<input
style={{ width }}
type="text"
value={searchText}
onChange={handleChange}
className={cx('form-control ghost-search', {
'dark-theme-placeholder': darkMode,
[className]: !!className,
})}
placeholder={placeholder}
onFocus={() => setFocussed(true)}
onBlur={() => setFocussed(false)}
data-cy={`${dataCy}-search-bar`}
autoFocus={autoFocus}
ref={ref}
/>
{searchText.length >= 0 ? (
<span className="input-icon-addon end" onMouseDown={clearSearchText}>
<div className="d-flex tj-common-search-input-clear-icon" title="clear">
<SolidIcon name="remove" />
</div>
</span>
) : (
''
)}
</div>
</div>
);
}
);
SearchBox.propTypes = {
onSubmit: PropTypes.func.isRequired,
debounceDelay: PropTypes.number,
width: PropTypes.string,
};

View file

@ -4,6 +4,7 @@ import cx from 'classnames';
import useDebounce from '@/_hooks/useDebounce';
import { useMounted } from '@/_hooks/use-mount';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import './_styles/search-box.scss';
export const SearchBox = forwardRef(
(

View file

@ -0,0 +1,85 @@
.ghost-search-box-wrapper {
.form-control.ghost-search {
background: none !important;
color: var(--slate12);
height: 48px;
border: none !important;
border-radius: 0 !important;
border-bottom: 1px solid var(--border-weak) !important;
transition: border-bottom 0.2s ease-in-out;
&:hover {
background: none !important;
border-bottom: 1px solid var(--border-accent-weak) !important;
color: var(--slate12);
}
&:focus {
background: none !important;
border: none !important;
border-bottom: 1px solid var(--border-accent-strong) !important;
}
}
.input-icon {
.input-icon-addon {
padding-right: 6px;
display: flex;
}
}
}
/**
* Search Box
*/
.ghost-search-box-wrapper {
input {
width: 200px;
border-radius: 5px !important;
color: var(--slate12);
background-color: var(--base);
}
.input-icon .form-control:not(:first-child),
.input-icon .form-select:not(:last-child) {
padding-left: 28px !important;
}
input:focus {
width: 200px;
background-color: var(--base);
}
.input-icon .input-icon-addon {
display: flex;
}
.input-icon .input-icon-addon.end {
pointer-events: auto;
.tj-common-search-input-clear-icon {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 4px;
width: 20px;
height: 20px;
background: var(--indigo3) !important;
border-radius: 4px;
}
div {
border-radius: 12px;
color: #ffffff;
padding: 1px;
cursor: pointer;
svg {
height: 14px;
width: 14px;
}
}
}
}

View file

@ -0,0 +1,65 @@
.search-box-wrapper {
input {
width: 200px;
border-radius: 5px !important;
color: var(--text-primary);
background-color: var(--surfaces-surface-01) !important;
border: 1px solid var(--border-weak) !important;
}
.input-icon .form-control:not(:first-child),
.input-icon .form-select:not(:last-child) {
padding-left: 28px !important;
}
input:focus {
width: 200px;
background-color: var(--surfaces-surface-02);
}
.input-icon .input-icon-addon {
display: flex;
}
.input-icon .input-icon-addon.end {
pointer-events: auto;
.tj-common-search-input-clear-icon {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 4px;
width: 20px;
height: 20px;
background: var(--indigo3) !important;
border-radius: 4px;
}
div {
border-radius: 12px;
color: #ffffff;
padding: 1px;
cursor: pointer;
svg {
height: 14px;
width: 14px;
}
}
}
}
.searchbox-wrapper {
margin-top: 0 !important;
.search-icon {
margin: 0.30rem
}
input {
border-radius: 8px !important;
padding-left: 1.75rem !important;
border-radius: 8px !important;
}
}

View file

@ -4,7 +4,6 @@ import { fetchEventSource } from '@microsoft/fetch-event-source';
export const aiService = {
generateApp,
createComponent,
createQuery,
updateComponent,
createEvent,
@ -60,14 +59,6 @@ function generateApp(prompt) {
return fetch(`${config.apiUrl}/ai/generateApp`, requestOptions).then(handleResponse);
}
function createComponent(prompt) {
const body = {
prompt,
};
const requestOptions = { method: 'POST', headers: authHeader(), credentials: 'include', body: JSON.stringify(body) };
return fetch(`${config.apiUrl}/agents/create-components`, requestOptions).then(handleResponse);
}
function createQuery(prompt) {
const body = {
prompt,

View file

@ -11,6 +11,7 @@ export const dataqueryService = {
changeQueryDataSource,
updateStatus,
bulkUpdateQueryOptions,
createWorkflowQuery,
};
function getAll(appVersionId, mode) {
@ -36,6 +37,21 @@ function create(app_id, app_version_id, name, kind, options, data_source_id, plu
).then(handleResponse);
}
function createWorkflowQuery(app_id, app_version_id, name, kind, options, data_source_id, plugin_id) {
const body = {
app_id,
app_version_id,
name,
kind,
options,
data_source_id,
plugin_id,
};
const requestOptions = { method: 'POST', headers: authHeader(), credentials: 'include', body: JSON.stringify(body) };
return fetch(`${config.apiUrl}/data-queries/workflow-node`, requestOptions).then(handleResponse);
}
function update(id, versionId, name, options, dataSourceId) {
const body = {
options,

View file

@ -10,11 +10,15 @@ export const workflowExecutionsService = {
all,
enableWebhook,
previewQueryNode,
getPaginatedExecutions,
getPaginatedNodes,
trigger,
streamSSE,
};
function previewQueryNode(queryId, appVersionId, nodeId) {
function previewQueryNode(queryId, appVersionId, nodeId, state = {}) {
const currentSession = authenticationService.currentSessionValue;
const body = { appVersionId, userId: currentSession.current_user?.id, queryId, nodeId };
const body = { appVersionId, userId: currentSession.current_user?.id, queryId, nodeId, state };
const requestOptions = { method: 'POST', headers: authHeader(), body: JSON.stringify(body), credentials: 'include' };
return fetch(`${config.apiUrl}/workflow_executions/previewQueryNode`, requestOptions).then(handleResponse);
}
@ -70,3 +74,40 @@ function enableWebhook(appId, value) {
const requestOptions = { method: 'PATCH', headers: authHeader(), body: JSON.stringify(body), credentials: 'include' };
return fetch(`${config.apiUrl}/v2/webhooks/workflows/${appId}`, requestOptions).then(handleResponse);
}
function getPaginatedExecutions(appVersionId, page = 1, perPage = 10) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
return fetch(
`${config.apiUrl}/workflow_executions?appVersionId=${appVersionId}&page=${page}&per_page=${perPage}`,
requestOptions
).then(handleResponse);
}
function getPaginatedNodes(executionId, page = 1, perPage = 20) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
return fetch(
`${config.apiUrl}/workflow_executions/${executionId}/nodes?page=${page}&per_page=${perPage}`,
requestOptions
).then(handleResponse);
}
function trigger(workflowAppId, params, environmentId) {
const currentSession = authenticationService.currentSessionValue;
const body = {
appId: workflowAppId,
userId: currentSession.current_user?.id,
executeUsing: 'app',
params: Array.isArray(params)
? Object.fromEntries(params.filter((param) => param.key !== '').map((param) => [param.key, param.value]))
: params || {},
environmentId,
};
const requestOptions = { method: 'POST', headers: authHeader(), body: JSON.stringify(body), credentials: 'include' };
return fetch(`${config.apiUrl}/workflow_executions/${workflowAppId}/trigger`, requestOptions).then(handleResponse);
}
function streamSSE(workflowExecutionId) {
return new EventSource(`${config.apiUrl}/workflow_executions/${workflowExecutionId}/stream`, {
withCredentials: true,
});
}

View file

@ -0,0 +1,8 @@
import create from 'zustand';
const useWorkflowStore = create((set) => ({
workflowId: null,
setWorkflowId: (id) => set({ workflowId: id }),
}));
export default useWorkflowStore;

View file

@ -109,6 +109,17 @@
//upgrade
--upgrade-default: #FFAF41;
--upgrade-weak: #FFAF4140;
// Shadows
--elevation-000-box-shadow: 0px 1px 0px 0px rgba(0, 0, 0, 0.10);
--elevation-200-box-shadow: 0px 2px 4px 0px rgba(48, 50, 51, 0.10), 0px 0px 1px 0px rgba(48, 50, 51, 0.05);
--elevation-300-box-shadow: 0px 4px 8px 0px rgba(48, 50, 51, 0.10), 0px 0px 1px 0px rgba(48, 50, 51, 0.05);
--elevation-400-box-shadow: 0px 8px 16px 0px rgba(48, 50, 51, 0.10), 0px 0px 1px 0px rgba(48, 50, 51, 0.05);
--elevation-500-box-shadow: 0px 16px 24px 0px rgba(48, 50, 51, 0.09), 0px 0px 1px 0px rgba(48, 50, 51, 0.05);
--elevation-600-box-shadow: 0px 24px 40px 0px rgba(48, 50, 51, 0.08), 0px 0px 1px 0px rgba(48, 50, 51, 0.05);
--elevation-700-box-shadow: 0px 32px 50px 0px rgba(48, 50, 51, 0.08), 0px 0px 1px 0px rgba(48, 50, 51, 0.05);
--elevation-100-box-shadow: 0px 1px 1px 0px rgba(48, 50, 51, 0.10), 0px 0px 1px 0px rgba(48, 50, 51, 0.05);
}
.dark-theme {
@ -222,4 +233,15 @@
//upgrade
--upgrade-default: #FFAF41;
--upgrade-weak: #FFAF4140;
//box-shadow
--elevation-000-box-shadow: 0px 1px 0px 0px rgba(0, 0, 0, 0.40);
--elevation-100-box-shadow: 0px 1px 1px 0px #000, 0px 0px 1px 0px rgba(0, 0, 0, 0.90);
--elevation-200-box-shadow: 0px 2px 4px 0px #000, 0px 0px 1px 0px rgba(0, 0, 0, 0.90);
--elevation-300-box-shadow: 0px 4px 8px 0px #000, 0px 0px 1px 0px rgba(0, 0, 0, 0.90);
--elevation-400-box-shadow: 0px 8px 16px 0px #000, 0px 0px 1px 0px rgba(0, 0, 0, 0.90);
--elevation-500-box-shadow: 0px 16px 24px 0px rgba(0, 0, 0, 0.99), 0px 0px 1px 0px rgba(0, 0, 0, 0.90);
--elevation-600-box-shadow: 0px 24px 40px 0px rgba(0, 0, 0, 0.98), 0px 0px 1px 0px rgba(0, 0, 0, 0.90);
--elevation-700-box-shadow: 0px 32px 50px 0px rgba(0, 0, 0, 0.98), 0px 0px 1px 0px rgba(0, 0, 0, 0.90);
}

View file

@ -120,7 +120,7 @@
--interactive-overlays-column-resize: #1B1F244D;
//interactive
--interactive-default: #CCD1D54D;
--interactive-default: #88909914;
--interactive-hover: #ACB2B959;
@ -211,7 +211,7 @@
--interactive-overlays-column-resize: #FFFFFF80;
//interactive
--interactive-default: #A1A7AE1F;
--interactive-default: #858C940D;
--interactive-hover: #A1A7AE29;

View file

@ -3,15 +3,14 @@
}
.drawer {
background: var(--base);
background: var(--surfaces-surface-01);
width: 540px;
height: 100%;
position: fixed;
border: 1px solid var(--slate5);
box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);
border: 1px solid var(--border-weak);
box-shadow: var(--elevation-400-box-shadow);
transition: transform var(--transition-speed) ease;
z-index: 1000;
background: var(--base);
overflow-y: auto;
&.left {

View file

@ -1,80 +1,94 @@
// for selects and dropdowns across app dashboard
.react-select__control {
background-color: var(--base) !important;
border: 1px solid var(--slate7) !important;
background-color: var(--surfaces-surface-01) !important;
border: 1px solid var(--border-weak) !important;
&:active {
border: 1px solid var(--indigo9);
}
&:active {
border: 1px solid var(--indigo9);
}
}
.react-select__menu-portal {
z-index: 100 !important;
z-index: 100 !important;
.react-select__option {
color: var(--slate12);
z-index: 100;
}
.react-select__option {
color: var(--text-default);
height: 32px;
z-index: 100;
padding: 4px 8px;
}
}
.react-select__single-value {
color: var(--slate12) ;
color: var(--text-default);
}
.react-select__menu {
background-color: var(--base) !important;
border: 1px solid var(--slate3) !important;
box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03) !important;
margin: 0px !important;
z-index: 100;
background-color: var(--surfaces-surface-01) !important;
border: 1px solid var(--border-weak) !important;
box-shadow: var(--elevation-00-box-shadow) !important;
margin: 0px !important;
z-index: 100;
.react-select__menu-list {
background-color: var(--base) !important;
overflow-y: auto;
.react-select__menu-list {
background-color: var(--surfaces-surface-01) !important;
padding: 4px;
overflow-y: auto;
.react-select__option {
background-color: var(--base) !important;
.react-select__option {
background-color: var(--surfaces-surface-01) !important;
border-radius: 6px;
&:hover {
background-color: var(--slate3) !important;
}
> div {
color: var(--text-default) !important;
background-color: transparent !important;
}
&:hover {
background-color: var(--interactive-hover) !important;
> div {
background-color: transparent !important;
}
}
}
}
}
.org-select-container {
height: 52px;
display: flex;
align-items: center;
justify-content: center;
border-top: 1px solid var(--slate5);
margin-bottom: var(--dynamic-margin, 0px); //please Remove after Basicplan banner is removed..
height: 52px;
display: flex;
align-items: center;
justify-content: center;
border-top: 1px solid var(--border-weak);
margin-bottom: var(
--dynamic-margin,
0px
); //please Remove after Basicplan banner is removed..
}
.tj-org-select {
.react-select__control {
width: 262px;
height: 32px;
border: none !important;
background-color: var(--page-default) !important;
.react-select__control {
width: 262px;
height: 32px;
border: none !important;
background-color: var(--surfaces-surface-01) !important;
&:hover {
background: var(--slate2) !important;
}
&:active {
background: var(--slate3) !important;
}
&:hover {
background: var(--slate2) !important;
}
.tj-text-xsm {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 200px;
&:active {
background: var(--slate3) !important;
}
}
.tj-text-xsm {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 200px;
}
}
.users-filter-dropdown,
@ -85,59 +99,58 @@
.select-order-field,
.select-column-field,
.records-dropdown-field {
.react-select__control {
border: 1px solid var(--slate7) !important;
}
.react-select__control {
border: 1px solid var(--border-default) !important;
}
}
.css-1ms6gku-MenuPortal,
.css-169zxdi-MenuList {
.react-select__option {
border-radius: 6px;
}
.react-select__option {
border-radius: 6px;
}
}
.css-nw08ma-menu {
box-shadow: none !important;
box-shadow: none !important;
}
.react-select__menu-portal {
z-index: 9999 !important;
z-index: 9999 !important;
}
// following is the styles for table select column type menu list and options styles. If its same for all the select elements in the editor, then we can make it common and not specific for table select
.table-select-custom-menu-list{
.react-select__menu-list{
padding: 2px;
// this is needed otherwise :active state doesn't look nice, gap is required
display: flex;
flex-direction: column;
gap: 4px !important;
background-color: var(--base) !important;
overflow-y: auto;
.table-select-custom-menu-list {
.react-select__menu-list {
padding: 2px;
// this is needed otherwise :active state doesn't look nice, gap is required
display: flex;
flex-direction: column;
gap: 4px !important;
background-color: var(--surfaces-surface-01) !important;
overflow-y: auto;
}
.react-select__option {
display: flex;
justify-content: space-between;
padding: 8px 12px;
align-self: stretch;
align-items: center;
color: var(--slate12) !important;
border-radius: 6px;
/* Paragraph/Extrasmall/Regular */
font-family: "IBM Plex Sans";
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 166.667% */
&.react-select__option--is-selected {
color: var(--indigo9) !important;
}
.react-select__option{
display: flex;
justify-content: space-between;
padding: 8px 12px;
align-self: stretch;
align-items: center;
color: var(--slate12) !important;
border-radius: 6px;
/* Paragraph/Extrasmall/Regular */
font-family: 'IBM Plex Sans';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 166.667% */
&.react-select__option--is-selected{
color: var(--indigo9) !important;
}
&:active{
background: var(--base) !important;
box-shadow: 0px 0px 0px 4px var(--slate6);
color : var(--slate12) !important;
}
&:active {
background: var(--surfaces-surface-01) !important;
box-shadow: 0px 0px 0px 4px var(--slate6);
color: var(--slate12) !important;
}
}
}

View file

@ -20,10 +20,10 @@
}
.select-search-container {
--select-search-background: var(--base);
--select-search-border: var(--slate7);
--select-search-background: var(--surfaces-surface-01);
--select-search-border: var(--border-weak);
--select-search-selected: #dadcde;
--select-search-text: var(--slate12);
--select-search-text: var(--text-default);
--select-search-subtle-text: #6c6f85;
--select-search-inverted-text: var(--select-search-background);
--select-search-highlight: var(--indigo3);

View file

@ -2,12 +2,13 @@
@import "./designtheme.scss";
.global-datasources-sidebar {
height: calc(100vh - 64px);
height: calc(100vh - 48px);
max-width: 288px;
background: var(--page-default);
background: var(--page-weak);
display: grid;
grid-template-rows: auto 1fr auto;
border-right: 1px solid var(--slate5);
border-right: 1px solid var(--border-weak);
gap: 30px;
.add-datasource-btn {
height: 40px;
@ -28,7 +29,7 @@
padding: 6px 15px;
width: 248px;
height: 32px;
margin-bottom: 10px;
margin-bottom: 8px;
&:focus-visible {
box-shadow: 0px 0px 0px 4px #dfe3e6;
@ -69,7 +70,8 @@
}
.datasources-list-item {
background-color: var(--indigo3);
background-color: var(--interactive-default);
color: var(--text-default);
}
}
@ -109,7 +111,7 @@
.datasource-modal-container {
position: relative;
background: var(--page-default);
background: var(--page-weak);
.modal-header {
background-color: var(--slate3) !important;
@ -118,12 +120,12 @@
.modal {
position: absolute;
z-index: 1050;
background: var(--slate2);
background: var(--page-weak);
}
.modal-content {
border: 1px solid var(--slate5);
background-color: var(--base) !important;
border: 1px solid var(--border-weak);
background-color: var(--page-weak) !important;
.input-icon {
&:hover {
@ -165,6 +167,12 @@
display: flex;
justify-content: center;
align-items: center;
svg {
top: 1px;
left: 1px;
position: relative;
}
}
}
@ -184,26 +192,24 @@
.datasource-list-container {
overflow-y: auto;
padding-left: 20px;
max-height: calc(100vh - 64px);
border-left: 1px solid var(--slate5);
max-height: calc(100vh - 48px);
.datasource-list {
width: 976px;
margin: 0 auto;
max-height: calc(100vh - 70px);
padding-bottom: 48px;
.datasource-search-holder {
width: 100%;
margin-top: 22px;
margin-top: 24px;
margin-bottom: 24px;
}
.liner {
margin-top: 5px;
width: 100% !important;
}
input {
background: none !important;
border: none !important;
}
.ghost-search-box-wrapper .form-control.ghost-search {
padding-top: 16px;
padding-bottom: 16px;
height: 64px;
}
}

View file

@ -1,16 +1,14 @@
.instance-logout-wrapper{
background: var(--base);
background: var(--page-weak);
.instance-logout-header{
padding: 24px 24px;
gap: 12px;
height: 72px;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
border-bottom: 1px solid rgb(230, 232, 235); /* Light gray border */
border-bottom: 1px solid var(--border-weak);
padding-bottom: 1rem;
&.dark-mode {
border-bottom: 1px solid rgb(43, 47, 49) !important;
}
.instance-logout-title{
font-size: 18px;
line-height: 28px;

View file

@ -1,7 +1,7 @@
@import "./colors.scss";
@import "./designtheme.scss";
.left-sidebar {
background: var(--page-default) !important;
background: var(--page-weak) !important;
display: flex;
gap: 16px;
@ -785,7 +785,7 @@
align-items: center;
padding-top: 0px;
width: 48px;
border-right: 1px solid var(--slate5);
border-right: 1px solid var(--border-weak);
}
.tj-leftsidebar-icon-wrap {

View file

@ -6,16 +6,17 @@
width: 880px;
margin: auto;
border-radius: 6px;
border: 1px solid var(--border-weak);
.body-wrapper {
border: 1px solid var(--slate5);
height: 100%;
min-height: 620px;
}
.license-page-sidebar {
max-width: 220px;
background-color: var(--base);
border-right: 1px solid var(--slate5) !important;
background-color: var(--surfaces-surface-01);
border-right: 1px solid var(--border-weak) !important;
display: grid !important;
grid-template-rows: auto 1fr auto !important;
@ -29,7 +30,7 @@
}
.license-content-wrapper {
background-color: var(--base);
background-color: var(--surfaces-surface-01);
.groups-sub-header-wrap {
width: 100%;
@ -253,11 +254,10 @@
.license-header-wrap {
display: flex;
justify-content: space-between;
padding-right: 40px;
padding-left: 20px;
padding: 24px 40px 16px;
align-items: center;
height: unset !important;
background-color: var(--base);
background-color: var(--surfaces-surface-01);
.status-container {
border-radius: 20px;
@ -599,9 +599,9 @@
align-items: center;
align-self: stretch;
border-radius: 8px;
background-color: #FFFFFF;
border: 1px solid var(--upgrade-weak, #FFAF4140);
box-shadow: 0px 0px 1px 0px var(--dropshadow-100700-layer-1, rgba(48, 50, 51, 0.05)), 0px 1px 1px 0px var(--dropshadow-100400-layer-2, rgba(48, 50, 51, 0.10));
background-color: var(--surfaces-surface-01);
border: 1px solid var(--border-weak, #FFAF4140);
box-shadow: var(--elevation-000-box-shadow);
.license-loader {
justify-content: center;
@ -796,7 +796,7 @@
}
.license-error-modal {
background-color: var(--base);
background-color: var(--surfaces-surface-01);
.modal-header {
background-color: var(--slate3) !important;
@ -859,7 +859,7 @@
width: 100%;
height: 88px;
border-top: 1px solid var(--slate5) !important;
background: var(--base);
background: var(--surfaces-surface-01);
margin-top: 0px !important;
}

View file

@ -1,6 +1,7 @@
.apps-modules-tabs {
.nav-link {
background-color: var(--page-default);
.apps-modules-tabs.nav-tabs {
.nav-link,
ul > li.nav-link.active {
background-color: var(--page-weak);
}
.nav-link.active {

View file

@ -0,0 +1,13 @@
// Card
.card {
border: 0 !important;
outline: 1px solid var(--border-weak);
box-shadow: var(--elevation-100-box-shadow);
border-radius: 8px;
background-color: var(--background-surface-layer-01) !important;
&.card--clickable:hover {
box-shadow: var(--elevation-200-box-shadow);
}
}

View file

@ -18972,7 +18972,7 @@ img {
@media not print {
.theme-dark {
color: #f4f6fa;
background-color: #1f2936
background-color: #1E2226;
}
.theme-dark .card,

File diff suppressed because it is too large Load diff

View file

@ -11,6 +11,7 @@ const Card = ({
width = 50,
usePluginIcon = false,
className,
cardClassName,
titleClassName,
actionButton,
darkMode,
@ -37,7 +38,7 @@ const Card = ({
return (
<div style={{ height: '112px', width: '164px' }} className={`col-md-2 mb-4 ${className}`}>
<div
className="card"
className={`card ${cardClassName}`}
role="button"
onClick={(e) => {
e.preventDefault();

View file

@ -5,7 +5,7 @@
line-height: 20px;
display: flex;
align-items: center;
color: var(--slate12);
color: var(--text-default);
min-height: 32px;
cursor: pointer;
padding: 6px 8px;
@ -71,5 +71,5 @@
}
.tj-list-item-selected {
background-color: var(--slate5);;
background-color: var(--interactive-default);
}

View file

@ -71,9 +71,9 @@ function Header({
<div className="row w-100 gx-0">
{!collapseSidebar && (
<div className="tj-dashboard-section-header" data-name={pathname}>
<div className="row">
<div className="row tw-w-full">
<div className="col-9 d-flex">
<p className="tj-text-md font-weight-500" data-cy="dashboard-section-header">
<p className="tj-text-md font-weight-500 text-black-000" data-cy="dashboard-section-header">
{pathname}
</p>
{routesWithTags(pathname) && (
@ -117,7 +117,7 @@ function Header({
</div>
)}
<div className="col tj-dashboard-header-wrap">
<div className="d-flex justify-content-sm-between">
<div className="d-flex justify-content-sm-between tw-w-full">
{enableCollapsibleSidebar && collapseSidebar && (
<ToolTip message="Open sidebar" placement="bottom" delay={{ show: 0, hide: 100 }}>
<div className="pe-3">

View file

@ -1,22 +1,22 @@
import React from 'react';
const AppLimitSvg = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 25 25" fill="none">
const AppLimitSvg = ({ fill }) => (
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 25 25" fill={fill}>
<path
d="M2.5 4.64844C2.5 3.26773 3.61929 2.14844 5 2.14844H7.5C8.88071 2.14844 10 3.26773 10 4.64844V7.14844C10 8.52915 8.88071 9.64844 7.5 9.64844H5C3.61929 9.64844 2.5 8.52915 2.5 7.14844V4.64844Z"
fill="#CCD1D5"
fill={fill}
/>
<path
d="M17.5 2.14844C16.1193 2.14844 15 3.26773 15 4.64844V7.14844C15 8.52915 16.1193 9.64844 17.5 9.64844H20C21.3807 9.64844 22.5 8.52915 22.5 7.14844V4.64844C22.5 3.26773 21.3807 2.14844 20 2.14844H17.5Z"
fill="#CCD1D5"
fill={fill}
/>
<path
d="M18.75 22.1484C20.8211 22.1484 22.5 20.4695 22.5 18.3984C22.5 16.3274 20.8211 14.6484 18.75 14.6484C16.6789 14.6484 15 16.3274 15 18.3984C15 20.4695 16.6789 22.1484 18.75 22.1484Z"
fill="#CCD1D5"
fill={fill}
/>
<path
d="M5 14.6484C3.61929 14.6484 2.5 15.7677 2.5 17.1484V19.6484C2.5 21.0291 3.61929 22.1484 5 22.1484H7.5C8.88071 22.1484 10 21.0291 10 19.6484V17.1484C10 15.7677 8.88071 14.6484 7.5 14.6484H5Z"
fill="#CCD1D5"
fill={fill}
/>
</svg>
);

View file

@ -5,7 +5,7 @@ const Plus = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 2
width={width}
height={width}
viewBox={viewBox}
fill="none"
fill={fill}
xmlns="http://www.w3.org/2000/svg"
className={className}
data-cy={dataCy}

View file

@ -5,7 +5,7 @@ const Search = ({ fill = '#C1C8CD', width = '24', className = '', viewBox = '0 0
width={width}
height={width}
viewBox={viewBox}
fill="none"
fill={fill}
xmlns="http://www.w3.org/2000/svg"
className={className}
style={style}

View file

@ -6,7 +6,7 @@ import { toast } from 'react-hot-toast';
import InputComponent from '@/components/ui/Input/Index';
const InputV3 = ({ helpText, ...props }) => {
const { workspaceVariables, workspaceConstants, value, widget, disabled, encrypted } = props;
const { workspaceVariables, workspaceConstants, value, widget, encrypted, onBlur } = props;
const [isFocused, setIsFocused] = useState(false);
const [isCopied, setIsCopied] = useState(false);
@ -37,6 +37,11 @@ const InputV3 = ({ helpText, ...props }) => {
<InputComponent
{...props}
value={value}
onFocus={() => setIsFocused(true)}
onBlur={(event) => {
setIsFocused(false);
onBlur(event);
}}
styles="tw-bg-transparent"
label={props.label}
placeholder={props.placeholder}
@ -49,6 +54,11 @@ const InputV3 = ({ helpText, ...props }) => {
{...props}
type="password"
value={value}
onFocus={() => setIsFocused(true)}
onBlur={(event) => {
setIsFocused(false);
onBlur(event);
}}
styles="tw-bg-transparent"
label={props.label}
placeholder={props.placeholder}

View file

@ -5,20 +5,21 @@ import SolidIcon from '../Icon/SolidIcons';
import { toast } from 'react-hot-toast';
const Input = ({ helpText, onBlur, ...props }) => {
const { workspaceVariables, workspaceConstants, value, type, disabled, encrypted } = props;
const { workspaceVariables, workspaceConstants, value, type, disabled, encrypted, isWorkspaceConstant } = props;
const [isFocused, setIsFocused] = useState(false);
const [isCopied, setIsCopied] = useState(false);
const [showPasswordProps, setShowPasswordProps] = useState({
inputType: type,
iconType: 'eyedisable',
});
const [showPassword, setShowPassword] = useState(false);
const inputType = type === 'password' || encrypted ? (showPassword ? 'text' : 'password') : type;
const iconType = showPassword ? 'eye' : 'eyedisable';
useEffect(() => {
if (isWorkspaceConstant) {
setShowPassword(true);
}
}, [isWorkspaceConstant]);
const toggleShowPassword = () => {
if (inputType !== 'text') {
setShowPasswordProps({ inputType: 'text', iconType: 'eye' });
} else {
setShowPasswordProps({ inputType: 'password', iconType: 'eyedisable' });
}
setShowPassword(!showPassword);
};
const handleCopyToClipboard = async () => {
@ -36,12 +37,6 @@ const Input = ({ helpText, onBlur, ...props }) => {
}
};
useEffect(() => {
if (disabled && encrypted) setShowPasswordProps({ inputType: 'password', iconType: 'eyedisable' });
}, [disabled]);
const { inputType, iconType } = showPasswordProps;
return (
<div className="tj-app-input">
<div
@ -57,8 +52,10 @@ const Input = ({ helpText, onBlur, ...props }) => {
}}
/>
{(type === 'password' || encrypted) && (
<div onClick={!disabled && toggleShowPassword}>
{' '}
<div
onClick={!disabled ? toggleShowPassword : undefined}
style={{ cursor: !disabled ? 'pointer' : 'default' }}
>
<SolidIcon className="eye-icon" name={iconType} />
</div>
)}
@ -66,12 +63,10 @@ const Input = ({ helpText, onBlur, ...props }) => {
value &&
(!isCopied ? (
<div style={{ cursor: 'pointer' }} onClick={handleCopyToClipboard}>
{' '}
<SolidIcon className="copy-icon" name="copy" />
</div>
) : (
<div style={{ color: 'green' }}>
{' '}
<span>Copied!</span>
</div>
))}

View file

@ -148,7 +148,7 @@ function Layout({
collapseSidebar={collapseSidebar}
toggleCollapsibleSidebar={toggleCollapsibleSidebar}
/>
<div style={{ paddingTop: 64 }}>{children}</div>
<div style={{ paddingTop: 48 }}>{children}</div>
</div>
<ConfirmDialog
title={'Unsaved Changes'}

View file

@ -16,7 +16,7 @@
}
.form-check>.form-check-input:not(:checked) {
background-color: #ffffff;
background-color: var(--slider-track);
}
.text-wrappers{
display: flex;

View file

@ -0,0 +1,35 @@
import * as React from 'react';
import { cn } from '@/lib/utils';
const Card = React.forwardRef(({ className, ...props }, ref) => (
<div ref={ref} className={cn('rounded-lg border bg-card text-card-foreground shadow-sm', className)} {...props} />
));
Card.displayName = 'Card';
const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
));
CardHeader.displayName = 'CardHeader';
const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
<div ref={ref} className={cn('text-2xl font-semibold leading-none tracking-tight', className)} {...props} />
));
CardTitle.displayName = 'CardTitle';
const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
<div ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
));
CardDescription.displayName = 'CardDescription';
const CardContent = React.forwardRef(({ className, ...props }, ref) => (
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
));
CardContent.displayName = 'CardContent';
const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
));
CardFooter.displayName = 'CardFooter';
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };

View file

@ -6,14 +6,26 @@ import { ButtonSolid } from '../../../../_components/AppButton';
import { generateCypressDataCy } from '../../../../modules/common/helpers/cypressHelpers.js';
const CommonInput = ({ label, helperText, disabled, required, onChange: change, ...restProps }) => {
const { type, encrypted, validation, isValidatedMessages, isDisabled } = restProps;
const {
propertyKey,
type,
encrypted,
validation,
isValidatedMessages,
isDisabled,
isEditing,
handleEncryptedFieldsToggle,
labelDisabled,
} = restProps;
const InputComponentType = type === 'number' ? NumberInput : TextInput;
const [isValid, setIsValid] = useState(null);
const [message, setMessage] = useState('');
const [isEditing, setIsEditing] = useState(false);
const isEncrypted = type === 'password' || encrypted;
const isWorkspaceConstant =
restProps.placeholder &&
(restProps.placeholder.includes('{{constants') || restProps.placeholder.includes('{{secrets'));
const handleChange = (e) => {
if (validation) {
@ -39,20 +51,12 @@ const CommonInput = ({ label, helperText, disabled, required, onChange: change,
}
}, [isValid, isValidatedMessages]);
const toggleEditing = () => {
if (isDisabled) return;
const willBeInEditMode = !isEditing;
setIsEditing(willBeInEditMode);
change({ target: { value: '' } });
};
return (
<div>
<div className="d-flex">
{label && (
<div className="tw-flex-shrink-0">
<InputLabel disabled={disabled} label={label} required={required} />
<InputLabel disabled={labelDisabled ?? disabled} label={label} required={required} />
</div>
)}
{type === 'password' && (
@ -65,7 +69,7 @@ const CommonInput = ({ label, helperText, disabled, required, onChange: change,
target="_blank"
rel="noreferrer"
disabled={isDisabled}
onClick={toggleEditing}
onClick={(e) => handleEncryptedFieldsToggle(e, propertyKey)}
data-cy={`button-${generateCypressDataCy(isEditing ? 'Cancel' : 'Edit')}`}
>
{isEditing ? 'Cancel' : 'Edit'}
@ -86,6 +90,7 @@ const CommonInput = ({ label, helperText, disabled, required, onChange: change,
required={required}
response={isValid}
onChange={handleChange}
isWorkspaceConstant={isWorkspaceConstant}
{...restProps}
/>
{helperText && (

View file

@ -2,56 +2,65 @@ import * as React from 'react';
import { cn } from '@/lib/utils';
import { inputVariants } from './InputUtils/Variants';
import SolidIcon from '../../../_ui/Icon/SolidIcons';
import { useEffect } from 'react';
const Input = React.forwardRef(({ className, size, type, multiline, response, rows = 3, ...props }, ref) => {
const [isPasswordVisible, setIsPasswordVisible] = React.useState(false);
const isPasswordField = type === 'password';
const Input = React.forwardRef(
({ className, size, type, multiline, response, isWorkspaceConstant, rows = 3, ...props }, ref) => {
const [isPasswordVisible, setIsPasswordVisible] = React.useState(false);
const isPasswordField = type === 'password';
const togglePasswordVisibility = () => {
if (!props.disabled) {
setIsPasswordVisible((prev) => !prev);
}
};
const togglePasswordVisibility = () => {
if (!props.disabled) {
setIsPasswordVisible((prev) => !prev);
}
};
const validationClass = response === true ? 'valid-textarea' : response === false ? 'invalid-textarea' : '';
useEffect(() => {
if (isWorkspaceConstant) {
setIsPasswordVisible(true);
}
}, [isWorkspaceConstant]);
return (
<div className="design-component-inputs">
{multiline ? (
<textarea
className={cn(
`tw-relative tw-peer tw-flex tw-text-[12px]/[18px] tw-w-full tw-rounded-[8px] tw-border-[1px] tw-border-solid tw-bg-background-surface-layer-01 tw-py-[7px] tw-text-text-default focus-visible:tw-ring-[1px] focus-visible:tw-ring-offset-[1px] focus-visible:tw-ring-border-accent-strong focus-visible:tw-ring-offset-border-accent-strong focus-visible:tw-border-transparent disabled:tw-cursor-not-allowed ${props.styles}`,
className,
validationClass
)}
rows={rows}
ref={ref}
{...props}
/>
) : (
<input
type={isPasswordField && isPasswordVisible ? 'text' : type}
className={cn(
inputVariants({ size }),
`tw-peer tw-flex tw-text-[12px]/[18px] tw-w-full tw-rounded-[8px] tw-border-[1px] tw-border-solid tw-bg-background-surface-layer-01 tw-py-[7px] tw-text-text-default focus-visible:tw-ring-[1px] focus-visible:tw-ring-offset-[1px] focus-visible:tw-ring-border-accent-strong focus-visible:tw-ring-offset-border-accent-strong focus-visible:tw-border-transparent disabled:tw-cursor-not-allowed ${props.styles}`,
className
)}
ref={ref}
{...props}
/>
)}
{isPasswordField && !multiline && (
<div onClick={togglePasswordVisibility}>
{isPasswordVisible ? (
<SolidIcon className="eye-icon" name="eye" />
) : (
<SolidIcon className="eye-icon" name="eyedisable" />
)}
</div>
)}
</div>
);
});
const validationClass = response === true ? 'valid-textarea' : response === false ? 'invalid-textarea' : '';
return (
<div className="design-component-inputs">
{multiline ? (
<textarea
className={cn(
`tw-relative tw-peer tw-flex tw-text-[12px]/[18px] tw-w-full tw-rounded-[8px] tw-border-[1px] tw-border-solid tw-bg-background-surface-layer-01 tw-py-[7px] tw-text-text-default focus-visible:tw-ring-[1px] focus-visible:tw-ring-offset-[1px] focus-visible:tw-ring-border-accent-strong focus-visible:tw-ring-offset-border-accent-strong focus-visible:tw-border-transparent disabled:tw-cursor-not-allowed ${props.styles}`,
className,
validationClass
)}
rows={rows}
ref={ref}
{...props}
/>
) : (
<input
type={isPasswordField && isPasswordVisible ? 'text' : type}
className={cn(
inputVariants({ size }),
`tw-relative tw-peer tw-flex tw-text-[12px]/[18px] tw-w-full tw-rounded-[8px] tw-border-[1px] tw-border-solid tw-bg-background-surface-layer-01 tw-py-[7px] tw-text-text-default focus-visible:tw-ring-[1px] focus-visible:tw-ring-offset-[1px] focus-visible:tw-ring-border-accent-strong focus-visible:tw-ring-offset-border-accent-strong focus-visible:tw-border-transparent disabled:tw-cursor-not-allowed ${props.styles}`,
className
)}
ref={ref}
{...props}
/>
)}
{isPasswordField && !multiline && (
<div onClick={togglePasswordVisibility}>
{isPasswordVisible ? (
<SolidIcon className="eye-icon" name="eye" />
) : (
<SolidIcon className="eye-icon" name="eyedisable" />
)}
</div>
)}
</div>
);
}
);
Input.displayName = 'Input';
export { Input };

View file

@ -28,7 +28,7 @@
}
.load.dark-loader {
display: flex;
background-color: #1f2936;
background-color: #1E2226;
margin: 0;
}

View file

@ -6,6 +6,7 @@ import { Dropdown } from 'react-bootstrap';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import { LicenseTooltip } from '@/LicenseTooltip';
import { DefaultSSOList, DefaultSSOModal } from '@/modules/common/components';
import { Button } from '@/components/ui/Button/Button';
class BaseSSOConfigurationList extends React.Component {
protectedSSO = ['openid', 'ldap', 'saml'];
constructor(props) {
@ -304,7 +305,8 @@ class BaseSSOConfigurationList extends React.Component {
noTooltipIfValid={true}
placement="left"
>
<div
<Button
variant="outline"
className="sso-option"
key={key}
onClick={isFeatureAvailable ? () => this.openModal(key) : (e) => e.preventDefault()}
@ -345,7 +347,7 @@ class BaseSSOConfigurationList extends React.Component {
/>
<span className="slider round"></span>
</label>
</div>
</Button>
</LicenseTooltip>
);
};
@ -381,12 +383,13 @@ class BaseSSOConfigurationList extends React.Component {
bsPrefix="no-caret-dropdown-toggle"
data-cy="dropdown-custom-toggle"
>
<div
<Button
variant="outline"
className="sso-option-label"
style={{
paddingLeft: '12px',
width: '270px',
paddingRight: '220px',
paddingRight: '160px',
paddingTop: '6px',
paddingBottom: '6px',
height: '34px',
@ -395,7 +398,7 @@ class BaseSSOConfigurationList extends React.Component {
>
Instance SSO {defaultSSO ? `(${this.state.inheritedInstanceSSO})` : ''}
<SolidIcon className="option-icon" name={showDropdown ? 'cheveronup' : 'cheverondown'} fill={'grey'} />
</div>
</Button>
</Dropdown.Toggle>
<Dropdown.Menu style={{ width: '100%' }}>

View file

@ -20,8 +20,7 @@
padding-top: 6px;
padding-bottom: 6px;
margin-bottom: 10px;
background-color: #f9f9f9;
border: 1px solid #e1e1e1;
width: 100%;
border-radius: 8px;
transition: background-color 0.1s;
cursor: pointer;
@ -236,12 +235,9 @@ input:checked+.slider:before {
.workspace-settings-page {
width: 880px;
margin: 0 auto;
background: var(--base);
background: var(--page-weak);
.card {
background: var(--base);
border: 1px solid var(--slate7) !important;
box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05) !important;
width: 880px;
.card-header {
@ -273,8 +269,8 @@ input:checked+.slider:before {
align-items: center;
padding: 24px 32px;
gap: 8px;
border-top: 1px solid var(--slate5) !important;
background: var(--base);
border-top: 1px solid var(--border-weak) !important;
background: var(--surfaces-surface-01);
margin-top: 0px !important;
align-Self: 'stretch';
height: 88px;
@ -303,6 +299,11 @@ input:checked+.slider:before {
.theme-dark {
.form-control {
background-color: unset !important;
input {
color: var(--text-default) !important;
border-color: var(--border-default) !important;
}
}
.react-tel-input .form-control {

View file

@ -42,14 +42,13 @@
}
.constant-wrapper {
background-color: #f8f9fa;
background-color: var(--page-weak);
padding: 0px;
}
.constant-page-wrapper {
background-color: #ffffff;
border: 1px solid #e9ecef;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
background-color: var(--page-weak);
border: 1px solid var(--border-weak);
overflow: hidden;
width: 920px;
height: 620px;
@ -257,17 +256,11 @@
color: #adb5bd;
}
/* Dark Theme Styles */
.dark-theme .constant-wrapper,
.theme-dark .constant-wrapper {
background-color: var(--slate2);
}
.dark-theme .constant-page-wrapper,
.theme-dark .constant-page-wrapper {
background-color: var(--base);
border: 1px solid #6c757d;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.3);
background-color: var(--page-weak);
border: 1px solid var(--border-weak);
}
.dark-theme .workspace-constant-header .tj-text-sm,

View file

@ -44,16 +44,18 @@ const ConstantTable = ({
return (
<div>
<div className="card constant-table-card" style={{ border: 'none' }}>
<div className="constant-table-card" style={{ border: 'none' }}>
<div
className="fixedHeader table-responsive px-2"
className="fixedHeader table-responsive"
ref={tableRef}
style={{ maxHeight: tableRef.current && calculateOffset() }}
>
<table className="table table-vcenter mt-2" disabled={true}>
<thead>
<tr>
<th data-cy="workspace-variable-table-name-header">Name</th>
<th className="!tw-pl-4" data-cy="workspace-variable-table-name-header">
Name
</th>
<th data-cy="workspace-variable-table-value-header">Value</th>
{canUpdateDeleteConstant && (
<th className="w-1" style={{ paddingRight: '16px' }}>
@ -99,7 +101,7 @@ const ConstantTable = ({
{constants.map((constant) => {
return (
<tr key={constant.id}>
<td className="p-3-constants">
<td className="p-3-constants !tw-pl-4">
<span
data-cy={`${constant.name.toLowerCase().replace(/\s+/g, '-')}-workspace-constant-name`}
data-tooltip-id="tooltip-for-org-constant-cell"

View file

@ -10,13 +10,14 @@ const BaseImportAppMenu = ({
showCloudMenuItems = false,
CloudMenuComponent = () => null,
darkMode = false,
appType = 'front-end',
...props
}) => {
const fileInput = React.createRef();
const { t } = useTranslation();
return (
<Dropdown.Menu className="import-lg-position new-app-dropdown">
{props.appType !== 'module' && (
{appType !== 'wzorkflow' && appType !== 'module' && (
<Dropdown.Item
className="homepage-dropdown-style tj-text tj-text-xsm"
onClick={showTemplateLibraryModal}

View file

@ -6,11 +6,12 @@ import { getPrivateRoute, redirectToDashboard, redirectToWorkflows } from '@/_he
import SolidIcon from '@/_ui/Icon/SolidIcons';
import AppLogo from '@/_components/AppLogo';
import { hasBuilderRole } from '@/_helpers/utils';
import { isWorkflowsFeatureEnabled } from '@/modules/common/helpers/utils';
const BaseLogoNavDropdown = ({ darkMode, showWorkflows = false, type = 'apps' }) => {
const { admin } = authenticationService?.currentSessionValue ?? {};
const isWorkflows = type === 'workflows';
const workflowsEnabled = admin && window.public_config?.ENABLE_WORKFLOWS_FEATURE == 'true';
const workflowsEnabled = admin && isWorkflowsFeatureEnabled();
const handleBackClick = (e) => {
e.preventDefault();

View file

@ -473,10 +473,8 @@ const BaseManageOrgConstants = ({
featureAceess={featureAccess}
licenseType={featureAccess?.licenseStatus?.licenseType}
/>
<div style={{ marginTop: '850px' }}>
<OrganizationList />
</div>
</div>
<OrganizationList />
</div>
<div className="page-wrapper mt-4">
<div className="container-xl" style={{ width: '880px' }}>

View file

@ -13,7 +13,7 @@ import { WorkspaceDropDown } from '@/modules/dashboard/components';
each workspace related component has organizations list component which can be moved to a single wrapper.
otherwise this component will intiate everytime we switch between pages
*/
const BaseOrganizationList = function ({ workspacesLimit = null, LicenseBadge = () => null, ...props }) {
const BaseOrganizationList = ({ workspacesLimit = null, LicenseBadge = () => null, ...props }) => {
const { current_organization_id, admin } = authenticationService.currentSessionValue;
const { fetchOrganizations, organizationList, isGettingOrganizations } = useCurrentSessionStore(
(state) => ({

View file

@ -68,7 +68,7 @@ const UsersTable = ({
/>
<div style={customStyles} className="tj-user-table-wrapper">
<div className="card-table fixedHeader table-responsive">
<table data-testid="usersTable" className="users-table table table-vcenter h-100">
<table data-testid="usersTable" className="users-table table table-vcenter h-100 mx-0">
<thead>
<tr>
<th data-cy="users-table-name-column-header" data-name="name-header">
@ -106,9 +106,7 @@ const UsersTable = ({
{translator('header.organization.menus.manageUsers.workspaces', 'Workspaces')}
</th>
)}
<th className="w-1"></th>
<th className="w-1"></th>
<th className="w-1"></th>
<th className="w-1 !tw-w-16 !tw-max-w-16 !tw-min-w-16"></th>
</tr>
</thead>
{isLoading ? (
@ -128,7 +126,7 @@ const UsersTable = ({
users.length > 0 &&
users.map((user) => (
<tr key={user.id} data-cy={`${user.name.toLowerCase().replace(/\s+/g, '-')}-user-row`}>
<td>
<td data-name="name-header">
<Avatar
avatarId={user.avatar_id}
text={`${user.first_name ? user.first_name[0] : ''}${
@ -161,7 +159,7 @@ const UsersTable = ({
</td>
)}
{isLoadingAllUsers && (
<td className="text-muted">
<td className="text-muted !tw-w-[230px] tw-max-w-[230px]">
<span
className="text-muted user-type"
data-cy={`${user.name.toLowerCase().replace(/\s+/g, '-')}-user-type`}
@ -176,7 +174,7 @@ const UsersTable = ({
{!isLoadingAllUsers && <GroupChipTD groups={user.groups.map((group) => group.name)} />}
{user.status && (
<td
className="text-muted"
className="text-muted !tw-w-[230px] tw-max-w-[230px]"
data-name={wsSettings ? 'status-header' : ''}
style={{ marginRight: wsSettings ? '6px' : '0px' }}
>
@ -223,7 +221,7 @@ const UsersTable = ({
</td>
)}
{isLoadingAllUsers && (
<td className="text-muted">
<td className="text-muted !tw-w-[230px] tw-max-w-[230px]">
<a
className="px-2 text-muted workspaces"
onClick={
@ -239,7 +237,7 @@ const UsersTable = ({
</a>
</td>
)}
<td className="user-actions-button">
<td className="user-actions-button tw-w-16 tw-max-w-16">
<UsersActionMenu
archivingUser={archivingUser}
user={user}
@ -336,7 +334,9 @@ const GroupChipTD = ({ groups = [], isRole = false }) => {
onClick={(e) => {
orderedArray.length > 2 && toggleAllGroupsList(e);
}}
className={cx('text-muted groups-name-cell', { 'groups-hover': orderedArray.length > 2 })}
className={cx('text-muted groups-name-cell !tw-w-[230px] tw-max-w-[230px]', {
'groups-hover': orderedArray.length > 2,
})}
>
<div className="groups-name-container tj-text-sm font-weight-500">
{orderedArray.length === 0 ? (

View file

@ -18,4 +18,9 @@ const fetchEdition = () => {
return config.TOOLJET_EDITION?.toLowerCase() || 'ce';
};
export { processErrorMessage, clearPageHistory, fetchEdition };
const isWorkflowsFeatureEnabled = () => {
if (fetchEdition() === 'ee') return true;
return false;
};
export { processErrorMessage, clearPageHistory, fetchEdition, isWorkflowsFeatureEnabled };

Some files were not shown because too many files have changed in this diff Show more