Merge branch 'main' into ai-modularisation-bug-fix

This commit is contained in:
NishidhJain11 2025-07-09 12:57:28 +05:30 committed by GitHub
commit 5d8b080589
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
144 changed files with 3361 additions and 1636 deletions

View file

@ -69,7 +69,7 @@ jobs:
with: with:
command: build command: build
#The the below argument is specific for building EE AMI image #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: . target: .
working_directory: deploy/ec2/ee working_directory: deploy/ec2/ee
env: env:
@ -78,9 +78,9 @@ jobs:
- name: Send Slack Notification - name: Send Slack Notification
run: | run: |
if [[ "${{ job.status }}" == "success" ]]; then 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 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 fi
curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" ${{ secrets.SLACK_WEBHOOK_URL }} 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", baseUrl: "http://localhost:8082",
specPattern: [ specPattern: [
"cypress/e2e/happyPath/marketplace/commonTestcases/**/*.cy.js", "cypress/e2e/happyPath/marketplace/commonTestcases/**/*.cy.js",
], ]
numTestsKeptInMemory: 1, numTestsKeptInMemory: 1,
redirectionLimit: 7, redirectionLimit: 7,
experimentalRunAllSpecs: true, experimentalRunAllSpecs: true,

View file

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

View file

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

View file

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

View file

@ -118,4 +118,5 @@ npm install -g npm@10.9.2
# Building ToolJet app # Building ToolJet app
npm install -g @nestjs/cli npm install -g @nestjs/cli
export NODE_OPTIONS='--max-old-space-size=8000'
TOOLJET_EDTION=ee npm run build TOOLJET_EDTION=ee npm run build

View file

@ -16,7 +16,7 @@ source "amazon-ebs" "ubuntu" {
source_ami_filter { source_ami_filter {
filters = { 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" root-device-type = "ebs"
virtualization-type = "hvm" virtualization-type = "hvm"
} }
@ -30,7 +30,7 @@ source "amazon-ebs" "ubuntu" {
launch_block_device_mappings { launch_block_device_mappings {
device_name = "/dev/sda1" device_name = "/dev/sda1"
volume_size = 10 volume_size = 30
delete_on_termination = true delete_on_termination = true
} }
@ -47,7 +47,7 @@ build {
} }
provisioner "file" { provisioner "file" {
source = "../../frontend/config/nginx.conf.template" source = "../../../frontend/config/nginx.conf.template"
destination = "/tmp/nginx.conf" destination = "/tmp/nginx.conf"
} }

View file

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

@ -1 +1 @@
Subproject commit 3297d4303806594bd3f5b614df9057c8ceaa92b3 Subproject commit aed7af0149525ff745650c21ee40bf9caf2892dc

View file

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

View file

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

View file

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

View file

@ -9,6 +9,7 @@ import { BaseUrl } from './BaseUrl';
import { queryManagerSelectComponentStyle } from '@/_ui/Select/styles'; import { queryManagerSelectComponentStyle } from '@/_ui/Select/styles';
import CodeHinter from '@/AppBuilder/CodeEditor'; import CodeHinter from '@/AppBuilder/CodeEditor';
import { deepClone } from '@/_helpers/utilities/utils.helpers'; import { deepClone } from '@/_helpers/utilities/utils.helpers';
import './styles.css';
class Restapi extends React.Component { class Restapi extends React.Component {
constructor(props) { constructor(props) {
@ -287,14 +288,15 @@ class Restapi extends React.Component {
const { options } = this.state; const { options } = this.state;
const dataSourceURL = this.props.selectedDataSource?.options?.url?.value; const dataSourceURL = this.props.selectedDataSource?.options?.url?.value;
const queryName = this.props.queryName; const queryName = this.props.queryName;
const isWorkflowNode = queryName === 'workflowNode';
const currentValue = { label: options.method?.toUpperCase(), value: options.method }; const currentValue = { label: options.method?.toUpperCase(), value: options.method };
return ( 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>}{' '} {this.props.selectedDataSource?.scope == 'global' && <div className="form-label flex-shrink-0"></div>}{' '}
<div className="flex-grow-1 overflow-hidden"> <div className="flex-grow-1 overflow-hidden">
<div className="rest-api-methods-select-element-container"> <div className={`rest-api-methods-select-element-container ${isWorkflowNode ? 'workflow-rest-api' : ''}`}>
<div className="d-flex"> <div className={`d-flex ${isWorkflowNode ? 'mb-2' : ''}`}>
<p <p
className="text-placeholder font-weight-medium" className="text-placeholder font-weight-medium"
style={{ width: '100px', marginRight: '16px', marginBottom: '0px' }} style={{ width: '100px', marginRight: '16px', marginBottom: '0px' }}
@ -303,8 +305,11 @@ class Restapi extends React.Component {
</p> </p>
</div> </div>
<div className="d-flex flex-column w-100"> <div className="d-flex flex-column w-100">
<div className="d-flex flex-row"> <div className={`${isWorkflowNode ? '' : 'd-flex'} flex-row`}>
<div className={`me-2`} style={{ width: '90px', height: '32px' }}> <div
className={`me-2 ${isWorkflowNode ? 'mb-2' : ''}`}
style={{ width: isWorkflowNode ? '150px' : '90px', height: '32px' }}
>
<label className="font-weight-medium color-slate12">Method</label> <label className="font-weight-medium color-slate12">Method</label>
<Select <Select
options={[ options={[
@ -320,9 +325,9 @@ class Restapi extends React.Component {
value={currentValue} value={currentValue}
defaultValue={{ label: 'GET', value: 'get' }} defaultValue={{ label: 'GET', value: 'get' }}
placeholder="Method" placeholder="Method"
width={100} width={isWorkflowNode ? 150 : 100}
height={32} height={32}
styles={this.customSelectStyles(this.props.darkMode, 91)} styles={this.customSelectStyles(this.props.darkMode, isWorkflowNode ? 150 : 91)}
useCustomStyles={true} useCustomStyles={true}
customClassPrefix="restapi-method-select" customClassPrefix="restapi-method-select"
onMenuOpen={() => { onMenuOpen={() => {
@ -335,7 +340,7 @@ class Restapi extends React.Component {
</div> </div>
<div <div
className={`field rest-methods-url ${dataSourceURL && 'data-source-exists'}`} 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="font-weight-medium color-slate12">URL</div>
<div className="d-flex h-100 w-100"> <div className="d-flex h-100 w-100">
@ -371,7 +376,7 @@ class Restapi extends React.Component {
</div> </div>
</div> </div>
</div> </div>
<div className={`query-pane-restapi-tabs`}> <div className={`query-pane-restapi-tabs`} data-workflow={isWorkflowNode ? 'true' : 'false'}>
<Tabs <Tabs
theme={this.props.darkMode ? 'monokai' : 'default'} theme={this.props.darkMode ? 'monokai' : 'default'}
options={this.state.options} options={this.state.options}
@ -384,6 +389,7 @@ class Restapi extends React.Component {
bodyToggle={this.state.options.body_toggle} bodyToggle={this.state.options.body_toggle}
setBodyToggle={this.onBodyToggleChanged} setBodyToggle={this.onBodyToggleChanged}
onInputChange={this.handleInputChange} onInputChange={this.handleInputChange}
isWorkflow={isWorkflowNode}
/> />
</div> </div>
</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 { deepClone } from '@/_helpers/utilities/utils.helpers';
import { BulkUploadPrimaryKey } from './BulkUploadPrimaryKey'; import { BulkUploadPrimaryKey } from './BulkUploadPrimaryKey';
import BulkUpsertPrimaryKey from './BulkUpsertPrimaryKey'; import BulkUpsertPrimaryKey from './BulkUpsertPrimaryKey';
import { fetchEdition } from '@/modules/common/helpers/utils';
import config from 'config';
import './styles.scss'; import './styles.scss';
import CodeHinter from '@/AppBuilder/CodeEditor'; 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 [bulkUpdatePrimaryKey, setBulkUpdatePrimaryKey] = useState(() => options['bulk_update_with_primary_key'] || {});
const [bulkUpsertPrimaryKey, setBulkUpsertPrimaryKey] = useState(() => options['bulk_upsert_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'] || [ const joinOptions = options['join_table']?.['joins'] || [
{ conditions: { conditionsList: [{ leftField: { table: selectedTableId } }] } }, { conditions: { conditionsList: [{ leftField: { table: selectedTableId } }] } },
]; ];
@ -557,7 +574,7 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
<TooljetDatabaseContext.Provider value={value}> <TooljetDatabaseContext.Provider value={value}>
{/* table name dropdown */} {/* table name dropdown */}
{window.public_config?.TJDB_SQL_MODE_DISABLE !== 'true' && ( {!isSqlModeDisabled() && (
<div <div
className={cx({ 'col-4': !isHorizontalLayout, 'd-flex tooljetdb-worflow-operations': isHorizontalLayout })} 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 useStore from '@/AppBuilder/_stores/store';
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
import usePopoverObserver from '@/AppBuilder/_hooks/usePopoverObserver'; import usePopoverObserver from '@/AppBuilder/_hooks/usePopoverObserver';
import useWorkflowStore from '@/_stores/workflowStore';
export function Workflows({ options, optionsChanged, currentState }) { export function Workflows({ options, optionsChanged, currentState }) {
const { moduleId } = useModuleContext(); const { moduleId } = useModuleContext();
@ -15,7 +16,9 @@ export function Workflows({ options, optionsChanged, currentState }) {
const [_selectedWorkflowId, setSelectedWorkflowId] = useState(undefined); const [_selectedWorkflowId, setSelectedWorkflowId] = useState(undefined);
const [params, setParams] = useState([...(options.params ?? [{ key: '', value: '' }])]); 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( usePopoverObserver(
document.getElementsByClassName('query-details')[0], 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 _, { isEmpty } from 'lodash';
import { resolveReferences, loadPyodide, hasCircularDependency } from '@/_helpers/utils'; import { resolveReferences, loadPyodide, hasCircularDependency } from '@/_helpers/utils';
import { fetchOAuthToken, fetchOauthTokenForSlackAndGSheet } from '@/AppBuilder/_utils/auth'; import { fetchOAuthToken, fetchOauthTokenForSlackAndGSheet } from '@/AppBuilder/_utils/auth';
@ -7,7 +9,7 @@ import axios from 'axios';
import { validateMultilineCode } from '@/_helpers/utility'; import { validateMultilineCode } from '@/_helpers/utility';
import { convertMapSet, getQueryVariables } from '@/AppBuilder/_utils/queryPanel'; import { convertMapSet, getQueryVariables } from '@/AppBuilder/_utils/queryPanel';
import { deepClone } from '@/_helpers/utilities/utils.helpers'; import { deepClone } from '@/_helpers/utilities/utils.helpers';
import toast from 'react-hot-toast';
const queryManagerPreferences = JSON.parse(localStorage.getItem('queryManagerPreferences')) ?? {}; const queryManagerPreferences = JSON.parse(localStorage.getItem('queryManagerPreferences')) ?? {};
const initialState = { const initialState = {
@ -168,6 +170,19 @@ export const createQueryPanelSlice = (set, get) => ({
'setLoadingDataQueries' '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') => { onQueryConfirmOrCancel: (queryConfirmationData, isConfirm = false, mode = 'edit', moduleId = 'canvas') => {
const { queryPanel, dataQuery, setResolvedQuery } = get(); const { queryPanel, dataQuery, setResolvedQuery } = get();
const { runQuery } = queryPanel; 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: ( runQuery: (
queryId, queryId,
queryName, queryName,
@ -238,7 +316,7 @@ export const createQueryPanelSlice = (set, get) => ({
setPreviewPanelExpanded, setPreviewPanelExpanded,
executeRunPycode, executeRunPycode,
runTransformation, runTransformation,
executeWorkflow, triggerWorkflow,
executeMultilineJS, executeMultilineJS,
} = queryPanel; } = queryPanel;
const queryUpdatePromise = dataQuerySlice.queryUpdates[queryId]; 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 // eslint-disable-next-line no-unused-vars
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
if (shouldSetPreviewData) { if (shouldSetPreviewData) {
@ -363,9 +555,8 @@ export const createQueryPanelSlice = (set, get) => ({
} else if (query.kind === 'runpy') { } else if (query.kind === 'runpy') {
queryExecutionPromise = executeRunPycode(query.options?.code, query, false, mode, queryState, moduleId); queryExecutionPromise = executeRunPycode(query.options?.code, query, false, mode, queryState, moduleId);
} else if (query.kind === 'workflows') { } else if (query.kind === 'workflows') {
queryExecutionPromise = executeWorkflow( queryExecutionPromise = triggerWorkflow(
moduleId, moduleId,
query,
query.options?.workflowId, query.options?.workflowId,
query.options?.blocking, query.options?.blocking,
query.options?.params, query.options?.params,
@ -395,6 +586,38 @@ export const createQueryPanelSlice = (set, get) => ({
fetchOAuthToken(url, dataQuery['data_source_id'] || dataQuery['dataSourceId']); 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; let queryStatusCode = data?.status ?? null;
const promiseStatus = query.kind === 'runpy' ? data?.data?.status ?? 'ok' : data.status; const promiseStatus = query.kind === 'runpy' ? data?.data?.status ?? 'ok' : data.status;
// Note: Need to move away from statusText -> statusCode // Note: Need to move away from statusText -> statusCode
@ -429,120 +652,22 @@ export const createQueryPanelSlice = (set, get) => ({
errorData = data; errorData = data;
break; break;
} }
if (shouldSetPreviewData) {
setPreviewLoading(false);
setPreviewData(errorData);
}
errorData = query.kind === 'runpy' || query.kind === 'runjs' ? data?.data : data; errorData = query.kind === 'runpy' || query.kind === 'runjs' ? data?.data : data;
get().debugger.log({ const result = handleFailure(errorData);
logLevel: 'error', resolve(result);
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);
return; return;
} else { } else {
let rawData = data.data; const rawData = data.data;
let finalData = data.data; const result = await processQueryResults(data.data, rawData);
if (dataQuery.options.enableTransformation) { resolve(result);
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);
} }
}) })
.catch((e) => { .catch((e) => {
const { error } = e; const { error } = e;
if (mode !== 'view') toast.error(error ?? 'Unknown error'); const errorMessage = typeof error === 'string' ? error : error?.message || 'Unknown error';
resolve({ status: 'failed', message: error }); if (mode !== 'view') toast.error(errorMessage);
resolve({ status: 'failed', message: errorMessage });
}); });
}); });
}, },
@ -556,7 +681,7 @@ export const createQueryPanelSlice = (set, get) => ({
setPreviewPanelExpanded, setPreviewPanelExpanded,
executeRunPycode, executeRunPycode,
runTransformation, runTransformation,
executeWorkflow, triggerWorkflow,
executeMultilineJS, executeMultilineJS,
setIsPreviewQueryLoading, setIsPreviewQueryLoading,
} = queryPanel; } = queryPanel;
@ -616,7 +741,7 @@ export const createQueryPanelSlice = (set, get) => ({
} else if (query.kind === 'runpy') { } else if (query.kind === 'runpy') {
queryExecutionPromise = executeRunPycode(query.options.code, query, true, 'edit', queryState); queryExecutionPromise = executeRunPycode(query.options.code, query, true, 'edit', queryState);
} else if (query.kind === 'workflows') { } else if (query.kind === 'workflows') {
queryExecutionPromise = executeWorkflow( queryExecutionPromise = triggerWorkflow(
moduleId, moduleId,
query.options.workflowId, query.options.workflowId,
query.options.blocking, query.options.blocking,
@ -629,11 +754,73 @@ export const createQueryPanelSlice = (set, get) => ({
queryExecutionPromise queryExecutionPromise
.then(async (data) => { .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 finalData = data.data;
let queryStatusCode = data?.status ?? null; let queryStatusCode = data?.status ?? null;
const queryStatus = query.kind === 'runpy' ? data?.data?.status ?? 'ok' : data.status; const queryStatus = query.kind === 'runpy' ? data?.data?.status ?? 'ok' : data.status;
switch (true) { switch (true) {
// Note: Need to move away from statusText -> statusCode
case queryStatus === 'Bad Request' || case queryStatus === 'Bad Request' ||
queryStatus === 'Not Found' || queryStatus === 'Not Found' ||
queryStatus === 'Unprocessable Entity' || queryStatus === 'Unprocessable Entity' ||
@ -665,9 +852,7 @@ export const createQueryPanelSlice = (set, get) => ({
} }
onEvent('onDataQueryFailure', queryEvents); onEvent('onDataQueryFailure', queryEvents);
if (!calledFromQuery) setPreviewData(errorData); if (!calledFromQuery) setPreviewData(errorData);
break; break;
} }
case queryStatus === 'needs_oauth': { 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 { const {
queryPanel: { evaluatePythonCode }, queryPanel: { evaluatePythonCode },
} = get(); } = get();
@ -950,7 +1135,13 @@ export const createQueryPanelSlice = (set, get) => ({
const { const {
queryPanel: { evaluatePythonCode }, queryPanel: { evaluatePythonCode },
} = get(); } = get();
return await evaluatePythonCode({ queryResult, code, query, mode, currentState }); return await evaluatePythonCode({
queryResult,
code,
query,
mode,
currentState,
});
}, },
updateQuerySuggestions: (oldName, newName) => { updateQuerySuggestions: (oldName, newName) => {
@ -971,7 +1162,7 @@ export const createQueryPanelSlice = (set, get) => ({
delete updatedQueries[oldName]; 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); // useResolveStore.getState().actions.removeAppSuggestions(oldSuggestions);
// useCurrentStateStore.getState().actions.setCurrentState({ // useCurrentStateStore.getState().actions.setCurrentState({
@ -1013,10 +1204,20 @@ export const createQueryPanelSlice = (set, get) => ({
return { data: undefined, status: 'failed' }; 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 = '') => { createProxy: (obj, path = '') => {
const { queryPanel } = get();
const { createProxy } = queryPanel;
return new Proxy(obj, { return new Proxy(obj, {
get(target, prop) { get(target, prop) {
@ -1027,7 +1228,7 @@ export const createQueryPanelSlice = (set, get) => ({
} }
const value = target[prop]; 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) => { isQuerySelected: (queryId) => {
return get().queryPanel.selectedQuery?.id === 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: () => { runQueryOnShortcut: () => {
const { queryPanel } = get(); const { queryPanel } = get();
const { runQuery, selectedQuery } = queryPanel; 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 { Tooltip } from 'react-tooltip';
import { canCreateDataSource } from '@/_helpers'; import { canCreateDataSource } from '@/_helpers';
import SolidIcon from '@/_ui/Icon/SolidIcons'; import SolidIcon from '@/_ui/Icon/SolidIcons';
import { isWorkflowsFeatureEnabled } from '@/modules/common/helpers/utils';
import '../queryManager.theme.scss'; import '../queryManager.theme.scss';
function DataSourcePicker({ dataSources, sampleDataSource, staticDataSources, darkMode, globalDataSources }) { function DataSourcePicker({ dataSources, sampleDataSource, staticDataSources, darkMode, globalDataSources }) {
@ -50,7 +51,7 @@ function DataSourcePicker({ dataSources, sampleDataSource, staticDataSources, da
navigate(`/${workspaceId}/data-sources`); navigate(`/${workspaceId}/data-sources`);
}; };
const workflowsEnabled = window.public_config?.ENABLE_WORKFLOWS_FEATURE == 'true'; const workflowsEnabled = isWorkflowsFeatureEnabled();
return ( return (
<> <>

View file

@ -14,8 +14,17 @@ import { DataBaseSources, ApiSources, CloudStorageSources } from '@/modules/comm
import { canCreateDataSource } from '@/_helpers'; import { canCreateDataSource } from '@/_helpers';
import './../queryManager.theme.scss'; import './../queryManager.theme.scss';
import { DATA_SOURCE_TYPE } from '@/_helpers/constants'; 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 dataSources = useDataSources();
const globalDataSources = useGlobalDataSources(); const globalDataSources = useGlobalDataSources();
const sampleDataSource = useSampleDataSource(); const sampleDataSource = useSampleDataSource();
@ -32,6 +41,10 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSourc
closePopup(); closePopup();
}; };
function cleanWord(word) {
return word.replace(/default/g, '');
}
useEffect(() => { useEffect(() => {
const shouldAddSampleDataSource = !!sampleDataSource; const shouldAddSampleDataSource = !!sampleDataSource;
const allDataSources = [...dataSources, ...globalDataSources, shouldAddSampleDataSource && sampleDataSource].filter( const allDataSources = [...dataSources, ...globalDataSources, shouldAddSampleDataSource && sampleDataSource].filter(
@ -132,6 +145,37 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSourc
...userDefinedSourcesOpts, ...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 = [ const dataSourcesAvailable = [
{ {
label: ( label: (
@ -146,7 +190,7 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSourc
label: ( label: (
<div> <div>
<DataSourceIcon source={source} height={16} />{' '} <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> </div>
), ),
value: source.name, value: source.name,
@ -154,6 +198,22 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSourc
})), })),
}, },
...userDefinedSourcesOpts, ...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; const dataSourceList = workflowDataSources && workflowDataSources ? dataSourcesAvailable : DataSourceOptions;

View file

@ -106,3 +106,10 @@ export const defaultSources = {
runpy: { kind: 'runpy', id: 'runpy', name: 'Run Python code' }, runpy: { kind: 'runpy', id: 'runpy', name: 'Run Python code' },
workflows: { kind: 'workflows', id: 'null', name: 'Run Workflow' }, 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" placement="bottom"
show={appType === 'module' && props.basicPlan} show={appType === 'module' && props.basicPlan}
> >
<div className="card homepage-app-card" ref={cardRef}> <div className="card homepage-app-card card--clickable" ref={cardRef}>
<div <div
className={appType === 'module' && props.basicPlan ? 'disabled-module' : ''} className={appType === 'module' && props.basicPlan ? 'disabled-module' : ''}
key={app?.id} key={app?.id}

View file

@ -26,8 +26,8 @@ const AppList = (props) => {
</> </>
)} )}
{!props.isLoading && props.meta.total_count > 0 && ( {!props.isLoading && props.meta.total_count > 0 && (
<div className="container px-0"> <div className="">
<div className="row homepage-app-card-list-item-wrap"> <div className="homepage-app-card-list-item-wrap">
{props.apps.map((app) => { {props.apps.map((app) => {
return ( return (
<div className="homepage-app-card-list-item" key={app.id}> <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' && ( {canUpdateApp && canCreateApp && appType !== 'workflow' && (
<> <>
{appType !== 'workflow' && (
<Field
text={t('homePage.appCard.cloneApp', 'Clone app')}
onClick={() => openAppActionModal('clone-app')}
/>
)}
<Field <Field
text={ text={
appType === 'workflow' ? 'Clone workflow' : appType === 'module' ? 'Clone module' : 'Clone app' appType === 'workflow' ? 'Clone workflow' : appType === 'module' ? 'Clone module' : 'Clone app'
@ -113,7 +119,7 @@ export const AppMenu = function AppMenu({
</div> </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"> <svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path <path
fillRule="evenodd" fillRule="evenodd"

View file

@ -147,34 +147,38 @@ export const BlankPage = function BlankPage({
Create new {appType !== 'workflow' ? 'application' : 'workflow'} Create new {appType !== 'workflow' ? 'application' : 'workflow'}
</ButtonSolid> </ButtonSolid>
</div> </div>
{appType !== 'workflow' && ( <div className="col-6">
<div className="col-6"> <ButtonSolid
<ButtonSolid disabled={appType !== 'workflow' ? appCreationDisabled : workflowsCreationDisabled}
disabled={appCreationDisabled} leftIcon="folderdownload"
leftIcon="folderdownload" onChange={readAndImport}
onChange={readAndImport} isLoading={isImportingApp}
isLoading={isImportingApp} data-cy={appType !== 'workflow' ? 'button-import-an-app' : 'button-import-a-workflow'}
data-cy="button-import-an-app" className="col"
className="col" variant="tertiary"
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 &nbsp;
className={cx('', { 'cursor-pointer': !appCreationDisabled })} {appType !== 'workflow'
style={{ visibility: isImportingApp ? 'hidden' : 'visible' }} ? t('blankPage.importApplication', 'Import an app')
data-cy="import-an-application" : t('blankPage.importWorkflow', 'Import a workflow')}
> <input
&nbsp;{t('blankPage.importApplication', 'Import an app')} disabled={appType !== 'workflow' ? appCreationDisabled : workflowsCreationDisabled}
<input type="file"
disabled={appCreationDisabled} ref={fileInput}
type="file" style={{ display: 'none' }}
ref={fileInput} data-cy="import-option-input"
style={{ display: 'none' }} />
data-cy="import-option-input" </label>
/> </ButtonSolid>
</label> </div>
</ButtonSolid>
</div>
)}
</div> </div>
</div> </div>
<div className="col-5 empty-home-page-image" data-cy="empty-home-page-image"> <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 { validateName, handleHttpErrorMessages, getWorkspaceId } from '@/_helpers/utils';
import { useNavigate, useLocation } from 'react-router-dom'; import { useNavigate, useLocation } from 'react-router-dom';
import FolderSkeleton from '@/_ui/FolderSkeleton/FolderSkeleton'; import FolderSkeleton from '@/_ui/FolderSkeleton/FolderSkeleton';
import { Button } from '@/components/ui/Button/Button';
export const Folders = function Folders({ export const Folders = function Folders({
folders, folders,
foldersLoading, foldersLoading,
@ -246,24 +248,36 @@ export const Folders = function Folders({
<div className="d-flex folder-header-icons-wrap"> <div className="d-flex folder-header-icons-wrap">
{canCreateFolder && ( {canCreateFolder && (
<> <>
<div <Button
className="folder-create-btn" size="medium"
variant="ghost"
iconOnly
ariaLabel="Create new folder"
onClick={() => { onClick={() => {
setNewFolderName(''); setNewFolderName('');
setShowForm(true); setShowForm(true);
}} }}
data-cy="create-new-folder-button" data-cy="create-new-folder-button"
> >
<SolidIcon name="plus" width="14" fill={darkMode ? '#ECEDEE' : '#11181C'} /> <SolidIcon name="plus" width="14" fill={darkMode ? '#CFD3D8E6' : '#6A727C'} />
</div> </Button>
<div <Button
className="folder-create-btn" size="medium"
variant="ghost"
iconOnly
ariaLabel="Search for folders"
onClick={() => { onClick={() => {
setShowInput(true); setShowInput(true);
}} }}
data-cy="create-new-folder-button"
> >
<SolidIcon name="search" width="14" fill={darkMode ? '#ECEDEE' : '#11181C'} /> <SolidIcon
</div> name="search"
width="14"
fill={darkMode ? '#CFD3D8E6' : '#6A727C'}
className="tw-relative tw-top-[2px]"
/>
</Button>
</> </>
)} )}
</div> </div>
@ -287,8 +301,7 @@ export const Folders = function Folders({
className={cx( className={cx(
`list-group-item border-0 list-group-item-action d-flex align-items-center all-apps-link tj-text-xsm`, `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, 'tw-bg-interactive-default': _.isEmpty(activeFolder),
'bg-dark-indigo': _.isEmpty(activeFolder) && darkMode,
} }
)} )}
style={{ height: '32px' }} style={{ height: '32px' }}
@ -314,8 +327,7 @@ export const Folders = function Folders({
className={cx( 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`, `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, 'tw-bg-interactive-default': activeFolder.id === folder.id,
'bg-dark-indigo': activeFolder.id === folder.id && darkMode,
} }
)} )}
onClick={() => { onClick={() => {

View file

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

View file

@ -12,7 +12,7 @@ import {
} from '@/_services'; } from '@/_services';
import { ConfirmDialog, AppModal, ToolTip } from '@/_components'; import { ConfirmDialog, AppModal, ToolTip } from '@/_components';
import Select from '@/_ui/Select'; import Select from '@/_ui/Select';
import _, { sample, isEmpty, capitalize } from 'lodash'; import _, { sample, isEmpty, capitalize, has } from 'lodash';
import { Folders } from './Folders'; import { Folders } from './Folders';
import { BlankPage } from './BlankPage'; import { BlankPage } from './BlankPage';
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
@ -48,6 +48,7 @@ import {
} from '@/modules/dashboard/components'; } from '@/modules/dashboard/components';
import CreateAppWithPrompt from '@/modules/AiBuilder/components/CreateAppWithPrompt'; import CreateAppWithPrompt from '@/modules/AiBuilder/components/CreateAppWithPrompt';
import SolidIcon from '@/_ui/Icon/SolidIcons'; import SolidIcon from '@/_ui/Icon/SolidIcons';
import { isWorkflowsFeatureEnabled } from '@/modules/common/helpers/utils';
import EmptyModuleSvg from '../../assets/images/icons/empty-modules.svg'; import EmptyModuleSvg from '../../assets/images/icons/empty-modules.svg';
const { iconList, defaultIcon } = configs; const { iconList, defaultIcon } = configs;
@ -256,7 +257,11 @@ class HomePageComponent extends React.Component {
}; };
getAppType = () => { 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) => { createApp = async (appName, type, prompt) => {
@ -339,6 +344,66 @@ class HomePageComponent extends React.Component {
this.setState({ isExportingApp: true, app: app }); 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) => { readAndImport = (event) => {
try { try {
const file = event.target.files[0]; const file = event.target.files[0];
@ -413,7 +478,7 @@ class HomePageComponent extends React.Component {
let installedPluginsInfo = []; let installedPluginsInfo = [];
try { try {
if (this.state.dependentPlugins.length) { if (this.state.dependentPlugins.length) {
({ installedPluginsInfo = [] } = await pluginsService.installDependentPlugins( ({ installedPluginsInfo =[] } = await pluginsService.installDependentPlugins(
this.state.dependentPlugins, this.state.dependentPlugins,
true true
)); ));
@ -421,8 +486,7 @@ class HomePageComponent extends React.Component {
if (importJSON.app[0].definition.appV2.type !== this.props.appType) { if (importJSON.app[0].definition.appV2.type !== this.props.appType) {
toast.error( toast.error(
`${this.props.appType === 'module' ? 'App' : 'Module'} could not be imported in ${ `${this.props.appType === 'module' ? 'App' : 'Module'} could not be imported in ${this.props.appType === 'module' ? 'modules' : 'apps'
this.props.appType === 'module' ? 'modules' : 'apps'
} section. Switch to ${this.props.appType === 'module' ? 'apps' : 'modules'} section and try again.`, } section. Switch to ${this.props.appType === 'module' ? 'apps' : 'modules'} section and try again.`,
{ style: { maxWidth: '425px' } } { style: { maxWidth: '425px' } }
); );
@ -453,7 +517,7 @@ class HomePageComponent extends React.Component {
this.setState({ isImportingApp: false }); this.setState({ isImportingApp: false });
if (error.statusCode === 409) return 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 = () => { canViewWorkflow = () => {
return this.canUserPerform(this.state.currentUser, 'view'); return this.canUserPerform(this.state.currentUser, 'view') && isWorkflowsFeatureEnabled();
}; };
canUserPerform(user, action, app) { canUserPerform(user, action, app) {
@ -953,6 +1017,53 @@ class HomePageComponent extends React.Component {
importingGitAppOperations: validationMessage, 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() { render() {
const { const {
apps, apps,
@ -1012,7 +1123,7 @@ class HomePageComponent extends React.Component {
} else if (this.props.appType === 'front-end') { } else if (this.props.appType === 'front-end') {
return appsLimit?.percentage >= 100; return appsLimit?.percentage >= 100;
} else { } else {
return workflowInstanceLevelLimit.percentage >= 100 || workflowWorkspaceLevelLimit.percentage >= 100; return this.hasWorkflowLimitReached();
} }
}; };
const modalConfigs = { const modalConfigs = {
@ -1113,9 +1224,8 @@ class HomePageComponent extends React.Component {
<div className="groups-list"> <div className="groups-list">
<div <div
className={`border rounded text-sm container ${ className={`border rounded text-sm container ${missingGroupsExpanded ? 'max-h-48 overflow-y-auto' : ''
missingGroupsExpanded ? 'max-h-48 overflow-y-auto' : '' }`}
}`}
> >
<div style={{ color: 'var(--text-placeholder)' }} className="tj-text-xsm font-weight-500"> <div style={{ color: 'var(--text-placeholder)' }} className="tj-text-xsm font-weight-500">
User groups User groups
@ -1191,8 +1301,8 @@ class HomePageComponent extends React.Component {
this.props.appType === 'workflow' this.props.appType === 'workflow'
? 'homePage.deleteWorkflowAndData' ? 'homePage.deleteWorkflowAndData'
: this.props.appType === 'front-end' : this.props.appType === 'front-end'
? 'homePage.deleteAppAndData' ? 'homePage.deleteAppAndData'
: deleteModuleText, : deleteModuleText,
{ {
appName: appToBeDeleted?.name, appName: appToBeDeleted?.name,
} }
@ -1457,22 +1567,18 @@ class HomePageComponent extends React.Component {
{this.props.appType === 'module' {this.props.appType === 'module'
? 'Create new module' ? 'Create new module'
: this.props.t( : this.props.t(
`${ `${this.props.appType === 'workflow' ? 'workflowsDashboard' : 'homePage'
this.props.appType === 'workflow' ? 'workflowsDashboard' : 'homePage' }.header.createNewApplication`,
}.header.createNewApplication`, 'Create new app'
'Create new app' )}
)}
</> </>
</Button> </Button>
<Dropdown.Toggle
{this.props.appType !== 'workflow' && ( disabled={getDisabledState()}
<Dropdown.Toggle split
disabled={getDisabledState()} className="d-inline"
split data-cy="import-dropdown-menu"
className="d-inline" />
data-cy="import-dropdown-menu"
/>
)}
<ImportAppMenu <ImportAppMenu
darkMode={this.props.darkMode} darkMode={this.props.darkMode}
showTemplateLibraryModal={ showTemplateLibraryModal={
@ -1525,8 +1631,8 @@ class HomePageComponent extends React.Component {
classes="mb-3 small" classes="mb-3 small"
limits={ limits={
workflowInstanceLevelLimit.current >= workflowInstanceLevelLimit.total || workflowInstanceLevelLimit.current >= workflowInstanceLevelLimit.total ||
100 > workflowInstanceLevelLimit.percentage >= 90 || 100 > workflowInstanceLevelLimit.percentage >= 90 ||
workflowInstanceLevelLimit.current === workflowInstanceLevelLimit.total - 1 workflowInstanceLevelLimit.current === workflowInstanceLevelLimit.total - 1
? workflowInstanceLevelLimit ? workflowInstanceLevelLimit
: workflowWorkspaceLevelLimit : workflowWorkspaceLevelLimit
} }
@ -1545,12 +1651,7 @@ class HomePageComponent extends React.Component {
<OrganizationList customStyle={{ marginBottom: isAdmin || isBuilder ? '' : '0px' }} /> <OrganizationList customStyle={{ marginBottom: isAdmin || isBuilder ? '' : '0px' }} />
</div> </div>
<div <div className={cx('col home-page-content')} data-cy="home-page-content">
className={cx('col home-page-content', {
'bg-light-gray': !this.props.darkMode,
})}
data-cy="home-page-content"
>
<div className="w-100 mb-5 container home-page-content-container"> <div className="w-100 mb-5 container home-page-content-container">
{featuresLoaded && !isLoading ? ( {featuresLoaded && !isLoading ? (
<> <>
@ -1577,15 +1678,12 @@ class HomePageComponent extends React.Component {
{(meta?.total_count > 0 || appSearchKey) && ( {(meta?.total_count > 0 || appSearchKey) && (
<> <>
{!(isLoading && !appSearchKey) && ( {!(isLoading && !appSearchKey) && (
<> <HomeHeader
<HomeHeader onSearchSubmit={this.onSearchSubmit}
onSearchSubmit={this.onSearchSubmit} darkMode={this.props.darkMode}
darkMode={this.props.darkMode} appType={this.props.appType}
appType={this.props.appType} disabled={this.props.appType === 'module' && invalidLicense}
disabled={this.props.appType === 'module' && invalidLicense} />
/>
<div className="liner"></div>
</>
)} )}
<div className="filter-container"> <div className="filter-container">
<span>{currentFolder?.count ?? meta?.total_count} APPS</span> <span>{currentFolder?.count ?? meta?.total_count} APPS</span>
@ -1633,8 +1731,8 @@ class HomePageComponent extends React.Component {
appType={this.props.appType} appType={this.props.appType}
workflowsLimit={ workflowsLimit={
workflowInstanceLevelLimit.current >= workflowInstanceLevelLimit.total || workflowInstanceLevelLimit.current >= workflowInstanceLevelLimit.total ||
100 > workflowInstanceLevelLimit.percentage >= 90 || 100 > workflowInstanceLevelLimit.percentage >= 90 ||
workflowInstanceLevelLimit.current === workflowInstanceLevelLimit.total - 1 workflowInstanceLevelLimit.current === workflowInstanceLevelLimit.total - 1
? workflowInstanceLevelLimit ? workflowInstanceLevelLimit
: workflowWorkspaceLevelLimit : workflowWorkspaceLevelLimit
} }
@ -1679,7 +1777,7 @@ class HomePageComponent extends React.Component {
canUpdateApp={this.canUpdateApp} canUpdateApp={this.canUpdateApp}
deleteApp={this.deleteApp} deleteApp={this.deleteApp}
cloneApp={this.cloneApp} cloneApp={this.cloneApp}
exportApp={this.exportApp} exportApp={this.props.appType === 'workflow' ? this.exportAppDirectly : this.exportApp}
meta={meta} meta={meta}
currentFolder={currentFolder} currentFolder={currentFolder}
isLoading={isLoading || !featuresLoaded} 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 ( return (
<div className="col-sm-6 col-lg-4"> <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="card-body card-body-alignment">
<div className="row align-items-center"> <div className="row align-items-center">
<div className="col-auto"> <div className="col-auto">

View file

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

View file

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

View file

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

View file

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

View file

@ -97,8 +97,8 @@
z-index: 1; z-index: 1;
position: sticky; position: sticky;
left: 66px; left: 66px;
border-right: 2px solid var(--light-slate-08, #C1C8CD); border-right: 2px solid var(--border-weak);
background-color: white; background-color: var(--surfaces-surface-01);
} }
th { th {
@ -145,14 +145,14 @@
th:nth-child(2) { th:nth-child(2) {
z-index: 2; z-index: 2;
left: 66px; left: 66px;
border-right: 2px solid var(--light-slate-08, #C1C8CD); border-right: 2px solid var(--border-weak);
} }
.dark-background { .dark-background {
td:nth-child(1), td:nth-child(1),
td:nth-child(2) { td:nth-child(2) {
background-color: #2B394A; background-color: var(--surfaces-surface-01);
} }
} }
@ -283,26 +283,6 @@
background-color: #2B2F30 !important; 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 { .keyPress-actions {

View file

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

View file

@ -7,6 +7,7 @@ import { BreadCrumbContext } from '@/App/App';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { pageTitles, fetchAndSetWindowTitle } from '@white-label/whiteLabelling'; import { pageTitles, fetchAndSetWindowTitle } from '@white-label/whiteLabelling';
import { hasBuilderRole } from '@/_helpers/utils'; import { hasBuilderRole } from '@/_helpers/utils';
import './styles/styles.scss';
export const TooljetDatabaseContext = createContext({ export const TooljetDatabaseContext = createContext({
organizationId: null, 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 source = schema?.source?.kind;
const darkMode = localStorage.getItem('darkMode') === 'true'; const darkMode = localStorage.getItem('darkMode') === 'true';
const workspaceConstant = options?.[key]?.workspace_constant;
const isWorkspaceConstant = !!workspaceConstant;
if (!options) return; if (!options) return;
@ -264,7 +266,7 @@ const DynamicForm = ({
(options?.[key]?.encrypted !== undefined ? options?.[key].encrypted : encrypted) || type === 'password'; (options?.[key]?.encrypted !== undefined ? options?.[key].encrypted : encrypted) || type === 'password';
return { return {
type, type,
placeholder: useEncrypted ? '**************' : description, placeholder: workspaceConstant ? workspaceConstant : useEncrypted ? '**************' : description,
className: `form-control${handleToggle(controller)} ${useEncrypted && 'dynamic-form-encrypted-field'}`, className: `form-control${handleToggle(controller)} ${useEncrypted && 'dynamic-form-encrypted-field'}`,
style: { marginBottom: '0px !important' }, style: { marginBottom: '0px !important' },
value: options?.[key]?.value || '', value: options?.[key]?.value || '',
@ -276,6 +278,7 @@ const DynamicForm = ({
workspaceVariables, workspaceVariables,
workspaceConstants: currentOrgEnvironmentConstants, workspaceConstants: currentOrgEnvironmentConstants,
encrypted: useEncrypted, encrypted: useEncrypted,
isWorkspaceConstant: isWorkspaceConstant,
}; };
} }
case 'toggle': case 'toggle':
@ -509,10 +512,16 @@ const DynamicForm = ({
return; return;
} }
const isEditing = computedProps[field]['disabled']; const isEditing = computedProps[field]['disabled'];
const workspaceConstant = options?.[field]?.workspace_constant;
const isWorkspaceConstant = !!workspaceConstant;
if (isEditing) { if (isEditing) {
optionchanged(field, ''); if (isWorkspaceConstant) {
optionchanged(field, workspaceConstant);
} else {
optionchanged(field, '');
}
} else { } else {
//Send old field value if editing mode disabled for encrypted fields
const newOptions = { ...options }; const newOptions = { ...options };
const oldFieldValue = selectedDataSource?.['options']?.[field]; const oldFieldValue = selectedDataSource?.['options']?.[field];
if (oldFieldValue) { if (oldFieldValue) {

View file

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

View file

@ -38,7 +38,7 @@ export const NotificationCenter = ({ darkMode }) => {
const overlay = ( const overlay = (
<div <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' darkMode && 'dark-theme'
}`} }`}
data-bs-popper="static" 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 useDebounce from '@/_hooks/useDebounce';
import { useMounted } from '@/_hooks/use-mount'; import { useMounted } from '@/_hooks/use-mount';
import SolidIcon from '@/_ui/Icon/SolidIcons'; import SolidIcon from '@/_ui/Icon/SolidIcons';
import './_styles/search-box.scss';
export const SearchBox = forwardRef( 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 = { export const aiService = {
generateApp, generateApp,
createComponent,
createQuery, createQuery,
updateComponent, updateComponent,
createEvent, createEvent,
@ -60,14 +59,6 @@ function generateApp(prompt) {
return fetch(`${config.apiUrl}/ai/generateApp`, requestOptions).then(handleResponse); 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) { function createQuery(prompt) {
const body = { const body = {
prompt, prompt,

View file

@ -11,6 +11,7 @@ export const dataqueryService = {
changeQueryDataSource, changeQueryDataSource,
updateStatus, updateStatus,
bulkUpdateQueryOptions, bulkUpdateQueryOptions,
createWorkflowQuery,
}; };
function getAll(appVersionId, mode) { function getAll(appVersionId, mode) {
@ -36,6 +37,21 @@ function create(app_id, app_version_id, name, kind, options, data_source_id, plu
).then(handleResponse); ).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) { function update(id, versionId, name, options, dataSourceId) {
const body = { const body = {
options, options,

View file

@ -10,11 +10,15 @@ export const workflowExecutionsService = {
all, all,
enableWebhook, enableWebhook,
previewQueryNode, previewQueryNode,
getPaginatedExecutions,
getPaginatedNodes,
trigger,
streamSSE,
}; };
function previewQueryNode(queryId, appVersionId, nodeId) { function previewQueryNode(queryId, appVersionId, nodeId, state = {}) {
const currentSession = authenticationService.currentSessionValue; 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' }; const requestOptions = { method: 'POST', headers: authHeader(), body: JSON.stringify(body), credentials: 'include' };
return fetch(`${config.apiUrl}/workflow_executions/previewQueryNode`, requestOptions).then(handleResponse); 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' }; const requestOptions = { method: 'PATCH', headers: authHeader(), body: JSON.stringify(body), credentials: 'include' };
return fetch(`${config.apiUrl}/v2/webhooks/workflows/${appId}`, requestOptions).then(handleResponse); 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
--upgrade-default: #FFAF41; --upgrade-default: #FFAF41;
--upgrade-weak: #FFAF4140; --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 { .dark-theme {
@ -222,4 +233,15 @@
//upgrade //upgrade
--upgrade-default: #FFAF41; --upgrade-default: #FFAF41;
--upgrade-weak: #FFAF4140; --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-overlays-column-resize: #1B1F244D;
//interactive //interactive
--interactive-default: #CCD1D54D; --interactive-default: #88909914;
--interactive-hover: #ACB2B959; --interactive-hover: #ACB2B959;
@ -211,7 +211,7 @@
--interactive-overlays-column-resize: #FFFFFF80; --interactive-overlays-column-resize: #FFFFFF80;
//interactive //interactive
--interactive-default: #A1A7AE1F; --interactive-default: #858C940D;
--interactive-hover: #A1A7AE29; --interactive-hover: #A1A7AE29;

View file

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

View file

@ -1,80 +1,94 @@
// for selects and dropdowns across app dashboard // for selects and dropdowns across app dashboard
.react-select__control { .react-select__control {
background-color: var(--base) !important; background-color: var(--surfaces-surface-01) !important;
border: 1px solid var(--slate7) !important; border: 1px solid var(--border-weak) !important;
&:active { &:active {
border: 1px solid var(--indigo9); border: 1px solid var(--indigo9);
} }
} }
.react-select__menu-portal { .react-select__menu-portal {
z-index: 100 !important; z-index: 100 !important;
.react-select__option { .react-select__option {
color: var(--slate12); color: var(--text-default);
z-index: 100; height: 32px;
z-index: 100;
} padding: 4px 8px;
}
} }
.react-select__single-value { .react-select__single-value {
color: var(--slate12) ; color: var(--text-default);
} }
.react-select__menu { .react-select__menu {
background-color: var(--base) !important; background-color: var(--surfaces-surface-01) !important;
border: 1px solid var(--slate3) !important; border: 1px solid var(--border-weak) !important;
box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03) !important; box-shadow: var(--elevation-00-box-shadow) !important;
margin: 0px !important; margin: 0px !important;
z-index: 100; z-index: 100;
.react-select__menu-list { .react-select__menu-list {
background-color: var(--base) !important; background-color: var(--surfaces-surface-01) !important;
overflow-y: auto; padding: 4px;
overflow-y: auto;
.react-select__option { .react-select__option {
background-color: var(--base) !important; 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 { .org-select-container {
height: 52px; height: 52px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-top: 1px solid var(--slate5); border-top: 1px solid var(--border-weak);
margin-bottom: var(--dynamic-margin, 0px); //please Remove after Basicplan banner is removed.. margin-bottom: var(
--dynamic-margin,
0px
); //please Remove after Basicplan banner is removed..
} }
.tj-org-select { .tj-org-select {
.react-select__control { .react-select__control {
width: 262px; width: 262px;
height: 32px; height: 32px;
border: none !important; border: none !important;
background-color: var(--page-default) !important; background-color: var(--surfaces-surface-01) !important;
&:hover { &:hover {
background: var(--slate2) !important; background: var(--slate2) !important;
}
&:active {
background: var(--slate3) !important;
}
} }
.tj-text-xsm { &:active {
white-space: nowrap; background: var(--slate3) !important;
overflow: hidden;
text-overflow: ellipsis;
width: 200px;
} }
}
.tj-text-xsm {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 200px;
}
} }
.users-filter-dropdown, .users-filter-dropdown,
@ -85,59 +99,58 @@
.select-order-field, .select-order-field,
.select-column-field, .select-column-field,
.records-dropdown-field { .records-dropdown-field {
.react-select__control { .react-select__control {
border: 1px solid var(--slate7) !important; border: 1px solid var(--border-default) !important;
} }
} }
.css-1ms6gku-MenuPortal, .css-1ms6gku-MenuPortal,
.css-169zxdi-MenuList { .css-169zxdi-MenuList {
.react-select__option { .react-select__option {
border-radius: 6px; border-radius: 6px;
} }
} }
.css-nw08ma-menu { .css-nw08ma-menu {
box-shadow: none !important; box-shadow: none !important;
} }
.react-select__menu-portal { .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 // 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{ .table-select-custom-menu-list {
.react-select__menu-list{ .react-select__menu-list {
padding: 2px; padding: 2px;
// this is needed otherwise :active state doesn't look nice, gap is required // this is needed otherwise :active state doesn't look nice, gap is required
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4px !important; gap: 4px !important;
background-color: var(--base) !important; background-color: var(--surfaces-surface-01) !important;
overflow-y: auto; 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{ &:active {
display: flex; background: var(--surfaces-surface-01) !important;
justify-content: space-between; box-shadow: 0px 0px 0px 4px var(--slate6);
padding: 8px 12px; color: var(--slate12) !important;
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;
}
} }
}
} }

View file

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

View file

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

View file

@ -1,16 +1,14 @@
.instance-logout-wrapper{ .instance-logout-wrapper{
background: var(--base); background: var(--page-weak);
.instance-logout-header{ .instance-logout-header{
padding: 24px 24px; padding: 24px 24px;
gap: 12px; gap: 12px;
height: 72px; height: 72px;
border-top-left-radius: 6px; border-top-left-radius: 6px;
border-top-right-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; padding-bottom: 1rem;
&.dark-mode {
border-bottom: 1px solid rgb(43, 47, 49) !important;
}
.instance-logout-title{ .instance-logout-title{
font-size: 18px; font-size: 18px;
line-height: 28px; line-height: 28px;

View file

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

View file

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

View file

@ -1,6 +1,7 @@
.apps-modules-tabs { .apps-modules-tabs.nav-tabs {
.nav-link { .nav-link,
background-color: var(--page-default); ul > li.nav-link.active {
background-color: var(--page-weak);
} }
.nav-link.active { .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 { @media not print {
.theme-dark { .theme-dark {
color: #f4f6fa; color: #f4f6fa;
background-color: #1f2936 background-color: #1E2226;
} }
.theme-dark .card, .theme-dark .card,

File diff suppressed because it is too large Load diff

View file

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

View file

@ -5,7 +5,7 @@
line-height: 20px; line-height: 20px;
display: flex; display: flex;
align-items: center; align-items: center;
color: var(--slate12); color: var(--text-default);
min-height: 32px; min-height: 32px;
cursor: pointer; cursor: pointer;
padding: 6px 8px; padding: 6px 8px;
@ -71,5 +71,5 @@
} }
.tj-list-item-selected { .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"> <div className="row w-100 gx-0">
{!collapseSidebar && ( {!collapseSidebar && (
<div className="tj-dashboard-section-header" data-name={pathname}> <div className="tj-dashboard-section-header" data-name={pathname}>
<div className="row"> <div className="row tw-w-full">
<div className="col-9 d-flex"> <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} {pathname}
</p> </p>
{routesWithTags(pathname) && ( {routesWithTags(pathname) && (
@ -117,7 +117,7 @@ function Header({
</div> </div>
)} )}
<div className="col tj-dashboard-header-wrap"> <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 && ( {enableCollapsibleSidebar && collapseSidebar && (
<ToolTip message="Open sidebar" placement="bottom" delay={{ show: 0, hide: 100 }}> <ToolTip message="Open sidebar" placement="bottom" delay={{ show: 0, hide: 100 }}>
<div className="pe-3"> <div className="pe-3">

View file

@ -1,22 +1,22 @@
import React from 'react'; import React from 'react';
const AppLimitSvg = () => ( const AppLimitSvg = ({ fill }) => (
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 25 25" fill="none"> <svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 25 25" fill={fill}>
<path <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" 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 <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" 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 <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" 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 <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" 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> </svg>
); );

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,56 +2,65 @@ import * as React from 'react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { inputVariants } from './InputUtils/Variants'; import { inputVariants } from './InputUtils/Variants';
import SolidIcon from '../../../_ui/Icon/SolidIcons'; import SolidIcon from '../../../_ui/Icon/SolidIcons';
import { useEffect } from 'react';
const Input = React.forwardRef(({ className, size, type, multiline, response, rows = 3, ...props }, ref) => { const Input = React.forwardRef(
const [isPasswordVisible, setIsPasswordVisible] = React.useState(false); ({ className, size, type, multiline, response, isWorkspaceConstant, rows = 3, ...props }, ref) => {
const isPasswordField = type === 'password'; const [isPasswordVisible, setIsPasswordVisible] = React.useState(false);
const isPasswordField = type === 'password';
const togglePasswordVisibility = () => { const togglePasswordVisibility = () => {
if (!props.disabled) { if (!props.disabled) {
setIsPasswordVisible((prev) => !prev); setIsPasswordVisible((prev) => !prev);
} }
}; };
const validationClass = response === true ? 'valid-textarea' : response === false ? 'invalid-textarea' : ''; useEffect(() => {
if (isWorkspaceConstant) {
setIsPasswordVisible(true);
}
}, [isWorkspaceConstant]);
return ( const validationClass = response === true ? 'valid-textarea' : response === false ? 'invalid-textarea' : '';
<div className="design-component-inputs">
{multiline ? ( return (
<textarea <div className="design-component-inputs">
className={cn( {multiline ? (
`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}`, <textarea
className, className={cn(
validationClass `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,
rows={rows} validationClass
ref={ref} )}
{...props} rows={rows}
/> ref={ref}
) : ( {...props}
<input />
type={isPasswordField && isPasswordVisible ? 'text' : type} ) : (
className={cn( <input
inputVariants({ size }), type={isPasswordField && isPasswordVisible ? 'text' : type}
`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={cn(
className 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}`,
ref={ref} className
{...props} )}
/> ref={ref}
)} {...props}
{isPasswordField && !multiline && ( />
<div onClick={togglePasswordVisibility}> )}
{isPasswordVisible ? ( {isPasswordField && !multiline && (
<SolidIcon className="eye-icon" name="eye" /> <div onClick={togglePasswordVisibility}>
) : ( {isPasswordVisible ? (
<SolidIcon className="eye-icon" name="eyedisable" /> <SolidIcon className="eye-icon" name="eye" />
)} ) : (
</div> <SolidIcon className="eye-icon" name="eyedisable" />
)} )}
</div> </div>
); )}
}); </div>
);
}
);
Input.displayName = 'Input'; Input.displayName = 'Input';
export { Input }; export { Input };

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -473,10 +473,8 @@ const BaseManageOrgConstants = ({
featureAceess={featureAccess} featureAceess={featureAccess}
licenseType={featureAccess?.licenseStatus?.licenseType} licenseType={featureAccess?.licenseStatus?.licenseType}
/> />
<div style={{ marginTop: '850px' }}>
<OrganizationList />
</div>
</div> </div>
<OrganizationList />
</div> </div>
<div className="page-wrapper mt-4"> <div className="page-wrapper mt-4">
<div className="container-xl" style={{ width: '880px' }}> <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. 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 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 { current_organization_id, admin } = authenticationService.currentSessionValue;
const { fetchOrganizations, organizationList, isGettingOrganizations } = useCurrentSessionStore( const { fetchOrganizations, organizationList, isGettingOrganizations } = useCurrentSessionStore(
(state) => ({ (state) => ({

View file

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

View file

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

View file

@ -117,6 +117,9 @@ class DataSourceManagerComponent extends React.Component {
selectedDataSourceIcon: this.props.selectedDataSource?.plugin?.iconFile?.data, selectedDataSourceIcon: this.props.selectedDataSource?.plugin?.iconFile?.data,
connectionTestError: null, connectionTestError: null,
datasourceName: this.props.selectedDataSource?.name, datasourceName: this.props.selectedDataSource?.name,
validationMessages: {},
validationError: [],
showValidationErrors: false,
}); });
} }
} }
@ -146,6 +149,9 @@ class DataSourceManagerComponent extends React.Component {
dataSourceSchema: source.manifestFile?.data, dataSourceSchema: source.manifestFile?.data,
selectedDataSourcePluginId: source.id, selectedDataSourcePluginId: source.id,
datasourceName: source.name, datasourceName: source.name,
validationMessages: {},
validationError: [],
showValidationErrors: false,
}, },
() => this.createDataSource() () => this.createDataSource()
); );
@ -413,6 +419,7 @@ class DataSourceManagerComponent extends React.Component {
const ComponentToRender = isPlugin ? SourceComponent : SourceComponents[sourceComponentName] || SourceComponent; const ComponentToRender = isPlugin ? SourceComponent : SourceComponents[sourceComponentName] || SourceComponent;
return ( return (
<ComponentToRender <ComponentToRender
key={this.state.selectedDataSource?.id}
dataSourceSchema={this.state.dataSourceSchema} dataSourceSchema={this.state.dataSourceSchema}
optionsChanged={(options = {}) => this.setState({ options })} optionsChanged={(options = {}) => this.setState({ options })}
optionchanged={this.optionchanged} optionchanged={this.optionchanged}
@ -988,7 +995,7 @@ class DataSourceManagerComponent extends React.Component {
<input <input
type="text" type="text"
onChange={(e) => this.onNameChanged(e.target.value)} onChange={(e) => this.onNameChanged(e.target.value)}
className="form-control-plaintext form-control-plaintext-sm color-slate12" className="form-control-plaintext form-control-plaintext-sm color-slate12 tw-border-x tw-border-y"
value={decodeEntities(selectedDataSource.name)} value={decodeEntities(selectedDataSource.name)}
style={{ width: '160px' }} style={{ width: '160px' }}
data-cy="data-source-name-input-field" data-cy="data-source-name-input-field"

View file

@ -249,7 +249,7 @@ export const GlobalDataSources = ({ darkMode = false, updateSelectedDatasource }
<div className="datasource-search-holder"> <div className="datasource-search-holder">
<SearchBox <SearchBox
dataCy={`home-page`} dataCy={`home-page`}
className="border-0 homepage-search" className="border-0"
darkMode={darkMode} darkMode={darkMode}
placeholder={`Search data sources`} placeholder={`Search data sources`}
initialValue={queryString} initialValue={queryString}
@ -260,7 +260,6 @@ export const GlobalDataSources = ({ darkMode = false, updateSelectedDatasource }
setSuggestingDataSource(false); setSuggestingDataSource(false);
}} }}
/> />
<div className="liner mb-4"></div>
</div> </div>
{suggestingDataSource ? ( {suggestingDataSource ? (
<center className="empty-ds-container"> <center className="empty-ds-container">
@ -307,7 +306,7 @@ export const GlobalDataSources = ({ darkMode = false, updateSelectedDatasource }
}, 100); }, 100);
}; };
return ( return (
<div> <div className="tw-pt-4">
<SegregatedList <SegregatedList
handleOnSelect={handleOnSelect} handleOnSelect={handleOnSelect}
activeDatasourceList={activeDatasourceList} activeDatasourceList={activeDatasourceList}
@ -375,6 +374,7 @@ export const GlobalDataSources = ({ darkMode = false, updateSelectedDatasource }
width={'35px'} width={'35px'}
actionButton={addDataSourceBtn(item)} actionButton={addDataSourceBtn(item)}
className="datasource-card" className="datasource-card"
cardClassName="card--clickable"
titleClassName={'datasource-card-title'} titleClassName={'datasource-card-title'}
tags={tags} tags={tags}
/> />

View file

@ -10,6 +10,7 @@ import { SearchBox } from '@/_components/SearchBox';
import { DATA_SOURCE_TYPE } from '@/_helpers/constants'; import { DATA_SOURCE_TYPE } from '@/_helpers/constants';
import FolderSkeleton from '@/_ui/FolderSkeleton/FolderSkeleton'; import FolderSkeleton from '@/_ui/FolderSkeleton/FolderSkeleton';
import Modal from '@/HomePage/Modal'; import Modal from '@/HomePage/Modal';
import { Button } from '@/components/ui/Button/Button';
export const List = ({ updateSelectedDatasource }) => { export const List = ({ updateSelectedDatasource }) => {
const { const {
@ -141,15 +142,18 @@ export const List = ({ updateSelectedDatasource }) => {
Data sources added{' '} Data sources added{' '}
{!isLoading && filteredData && filteredData.length > 0 && `(${filteredData.length})`} {!isLoading && filteredData && filteredData.length > 0 && `(${filteredData.length})`}
</div> </div>
<div <Button
className="datasources-search-btn" size="medium"
variant="ghost"
iconOnly
ariaLabel="Search for folders"
onClick={() => { onClick={() => {
setShowInput(true); setShowInput(true);
}} }}
data-cy="added-ds-search-icon" data-cy="create-new-folder-button"
> >
<SolidIcon name="search" width="14" fill={darkMode ? '#ECEDEE' : '#11181C'} /> <SolidIcon name="search" width="14" fill={darkMode ? '#CFD3D8E6' : '#6A727C'} />
</div> </Button>
</> </>
) : ( ) : (
<SearchBox <SearchBox

View file

@ -79,7 +79,7 @@ if (process.env.APM_VENDOR === 'sentry') {
} }
if (isDevEnv) { if (isDevEnv) {
plugins.push(new ReactRefreshWebpackPlugin()); plugins.push(new ReactRefreshWebpackPlugin({ overlay: false }));
} }
module.exports = { module.exports = {

View file

@ -13,8 +13,7 @@
}, },
"options": { "options": {
"url": { "url": {
"type": "string", "type": "string"
"encrypted": false
}, },
"apiKey": { "apiKey": {
"type": "string", "type": "string",
@ -29,8 +28,7 @@
"key": "url", "key": "url",
"type": "text", "type": "text",
"description": "Enter your Qdrant URL.", "description": "Enter your Qdrant URL.",
"helpText": "<a href='https://qdrant.tech/documentation/quickstart-cloud/#authenticate-via-sdks' target='_blank' rel='noreferrer'>REST URL</a> to authenticate the requests of the Qdrant instance.", "helpText": "<a href='https://qdrant.tech/documentation/quickstart-cloud/#authenticate-via-sdks' target='_blank' rel='noreferrer'>REST URL</a> to authenticate the requests of the Qdrant instance."
"encrypted": true
}, },
"apiKey": { "apiKey": {
"label": "API Key", "label": "API Key",

View file

@ -6,6 +6,8 @@ import { QueryService } from './query_service.interface';
import { import {
isEmpty, isEmpty,
cacheConnection, cacheConnection,
cacheConnectionWithConfiguration,
generateSourceOptionsHash,
getCachedConnection, getCachedConnection,
parseJson, parseJson,
cleanSensitiveData, cleanSensitiveData,
@ -37,6 +39,8 @@ export {
User, User,
App, App,
cacheConnection, cacheConnection,
generateSourceOptionsHash,
cacheConnectionWithConfiguration,
getCachedConnection, getCachedConnection,
parseJson, parseJson,
isEmpty, isEmpty,

View file

@ -1,6 +1,7 @@
import { QueryError } from './query.error'; import { QueryError } from './query.error';
import * as tls from 'tls'; import * as tls from 'tls';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import crypto from 'crypto';
const CACHED_CONNECTIONS: any = {}; const CACHED_CONNECTIONS: any = {};
@ -17,8 +18,29 @@ export function cacheConnection(dataSourceId: string, connection: any): any {
CACHED_CONNECTIONS[dataSourceId] = { connection, updatedAt }; CACHED_CONNECTIONS[dataSourceId] = { connection, updatedAt };
} }
export function getCachedConnection(dataSourceId: string | number, dataSourceUpdatedAt: any): any { export function generateSourceOptionsHash(sourceOptions) {
const cachedData = CACHED_CONNECTIONS[dataSourceId]; const sortedEntries = Object.entries(sourceOptions)
.filter(([_, value]) => value !== undefined && value !== null && value !== '')
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, value]) => `${key}:${value}`)
.join('|');
return crypto.createHash('sha256').update(sortedEntries).digest('hex').substring(0, 16);
}
export function cacheConnectionWithConfiguration(dataSourceId: string, enhancedCacheKey: string, connection: any): any {
const updatedAt = new Date();
const allKeys = Object.keys(CACHED_CONNECTIONS);
const oldKeysForThisDatasource = allKeys.filter(
(key) => key.startsWith(`${dataSourceId}_`) && key !== enhancedCacheKey
);
oldKeysForThisDatasource.forEach((oldKey) => delete CACHED_CONNECTIONS[oldKey]);
CACHED_CONNECTIONS[enhancedCacheKey] = { connection, updatedAt };
}
export function getCachedConnection(cacheKey: string | number, dataSourceUpdatedAt: any): any {
const cachedData = CACHED_CONNECTIONS[cacheKey];
if (cachedData) { if (cachedData) {
const updatedAt = new Date(dataSourceUpdatedAt || null); const updatedAt = new Date(dataSourceUpdatedAt || null);

View file

@ -4,7 +4,8 @@ import {
QueryError, QueryError,
QueryResult, QueryResult,
QueryService, QueryService,
cacheConnection, cacheConnectionWithConfiguration,
generateSourceOptionsHash,
getCachedConnection, getCachedConnection,
} from '@tooljet-plugins/common'; } from '@tooljet-plugins/common';
import { SourceOptions, QueryOptions } from './types'; import { SourceOptions, QueryOptions } from './types';
@ -143,13 +144,15 @@ export default class MssqlQueryService implements QueryService {
dataSourceUpdatedAt?: string dataSourceUpdatedAt?: string
): Promise<Knex> { ): Promise<Knex> {
if (checkCache) { if (checkCache) {
let connection = await getCachedConnection(dataSourceId, dataSourceUpdatedAt); const optionsHash = generateSourceOptionsHash(sourceOptions);
const enhancedCacheKey = `${dataSourceId}_${optionsHash}`;
let connection = await getCachedConnection(enhancedCacheKey, dataSourceUpdatedAt);
if (connection) { if (connection) {
return connection; return connection;
} else { } else {
connection = await this.buildConnection(sourceOptions); connection = await this.buildConnection(sourceOptions);
dataSourceId && cacheConnection(dataSourceId, connection); cacheConnectionWithConfiguration(dataSourceId, enhancedCacheKey, connection);
return connection; return connection;
} }
} else { } else {

View file

@ -1,6 +1,7 @@
import knex, { Knex } from 'knex'; import knex, { Knex } from 'knex';
import { import {
cacheConnection, cacheConnectionWithConfiguration,
generateSourceOptionsHash,
getCachedConnection, getCachedConnection,
ConnectionTestResult, ConnectionTestResult,
QueryService, QueryService,
@ -145,13 +146,17 @@ export default class MysqlQueryService implements QueryService {
dataSourceUpdatedAt?: string dataSourceUpdatedAt?: string
): Promise<Knex> { ): Promise<Knex> {
if (checkCache) { if (checkCache) {
const cachedConnection = await getCachedConnection(dataSourceId, dataSourceUpdatedAt); const optionsHash = generateSourceOptionsHash(sourceOptions);
const enhancedCacheKey = `${dataSourceId}_${optionsHash}`;
const cachedConnection = await getCachedConnection(enhancedCacheKey, dataSourceUpdatedAt);
if (cachedConnection) return cachedConnection; if (cachedConnection) return cachedConnection;
const connection = await this.buildConnection(sourceOptions);
cacheConnectionWithConfiguration(dataSourceId, enhancedCacheKey, connection);
return connection;
} }
const connection = await this.buildConnection(sourceOptions); return await this.buildConnection(sourceOptions);
if (checkCache && dataSourceId) cacheConnection(dataSourceId, connection);
return connection;
} }
buildBulkUpdateQuery(queryOptions: QueryOptions): string { buildBulkUpdateQuery(queryOptions: QueryOptions): string {

View file

@ -1,7 +1,8 @@
import { Knex, knex } from 'knex'; import { Knex, knex } from 'knex';
import oracledb from 'oracledb'; import oracledb from 'oracledb';
import { import {
cacheConnection, cacheConnectionWithConfiguration,
generateSourceOptionsHash,
getCachedConnection, getCachedConnection,
ConnectionTestResult, ConnectionTestResult,
QueryService, QueryService,
@ -118,13 +119,15 @@ export default class OracledbQueryService implements QueryService {
dataSourceUpdatedAt?: string dataSourceUpdatedAt?: string
): Promise<any> { ): Promise<any> {
if (checkCache) { if (checkCache) {
let connection = await getCachedConnection(dataSourceId, dataSourceUpdatedAt); const optionsHash = generateSourceOptionsHash(sourceOptions);
const enhancedCacheKey = `${dataSourceId}_${optionsHash}`;
let connection = await getCachedConnection(enhancedCacheKey, dataSourceUpdatedAt);
if (connection) { if (connection) {
return connection; return connection;
} else { } else {
connection = await this.buildConnection(sourceOptions); connection = await this.buildConnection(sourceOptions);
dataSourceId && cacheConnection(dataSourceId, connection); cacheConnectionWithConfiguration(dataSourceId, enhancedCacheKey, connection);
return connection; return connection;
} }
} else { } else {

View file

@ -1,6 +1,7 @@
import { import {
ConnectionTestResult, ConnectionTestResult,
cacheConnection, cacheConnectionWithConfiguration,
generateSourceOptionsHash,
getCachedConnection, getCachedConnection,
QueryService, QueryService,
QueryResult, QueryResult,
@ -145,13 +146,17 @@ export default class PostgresqlQueryService implements QueryService {
dataSourceUpdatedAt?: string dataSourceUpdatedAt?: string
): Promise<Knex> { ): Promise<Knex> {
if (checkCache) { if (checkCache) {
const cachedConnection = await getCachedConnection(dataSourceId, dataSourceUpdatedAt); const optionsHash = generateSourceOptionsHash(sourceOptions);
const enhancedCacheKey = `${dataSourceId}_${optionsHash}`;
const cachedConnection = await getCachedConnection(enhancedCacheKey, dataSourceUpdatedAt);
if (cachedConnection) return cachedConnection; if (cachedConnection) return cachedConnection;
const connection = await this.buildConnection(sourceOptions);
cacheConnectionWithConfiguration(dataSourceId, enhancedCacheKey, connection);
return connection;
} }
const connection = await this.buildConnection(sourceOptions); return await this.buildConnection(sourceOptions);
if (checkCache && dataSourceId) cacheConnection(dataSourceId, connection);
return connection;
} }
buildBulkUpdateQuery(queryOptions: QueryOptions): string { buildBulkUpdateQuery(queryOptions: QueryOptions): string {

View file

@ -3,7 +3,8 @@ import {
QueryResult, QueryResult,
QueryService, QueryService,
ConnectionTestResult, ConnectionTestResult,
cacheConnection, cacheConnectionWithConfiguration,
generateSourceOptionsHash,
getCachedConnection, getCachedConnection,
} from '@tooljet-plugins/common'; } from '@tooljet-plugins/common';
import { SourceOptions, QueryOptions } from './types'; import { SourceOptions, QueryOptions } from './types';
@ -93,13 +94,15 @@ export default class Snowflake implements QueryService {
dataSourceUpdatedAt?: string dataSourceUpdatedAt?: string
): Promise<any> { ): Promise<any> {
if (checkCache) { if (checkCache) {
let connection = await getCachedConnection(dataSourceId, dataSourceUpdatedAt); const optionsHash = generateSourceOptionsHash(sourceOptions);
const enhancedCacheKey = `${dataSourceId}_${optionsHash}`;
let connection = await getCachedConnection(enhancedCacheKey, dataSourceUpdatedAt);
if (connection && (await connection.isValidAsync())) { if (connection && (await connection.isValidAsync())) {
return connection; return connection;
} else { } else {
connection = await this.buildConnection(sourceOptions); connection = await this.buildConnection(sourceOptions);
await cacheConnection(dataSourceId, connection); cacheConnectionWithConfiguration(dataSourceId, enhancedCacheKey, connection);
return connection; return connection;
} }
} else { } else {

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