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:
command: build
#The the below argument is specific for building EE AMI image
arguments: -color=false -on-error=abort -var ami_name=tooljet_${{ env.RELEASE_VERSION }}.ubuntu_focal
arguments: -color=false -on-error=abort -var ami_name=tooljet_${{ env.RELEASE_VERSION }}.ubuntu_jammy
target: .
working_directory: deploy/ec2/ee
env:
@ -78,9 +78,9 @@ jobs:
- name: Send Slack Notification
run: |
if [[ "${{ job.status }}" == "success" ]]; then
message="ToolJet enterprise AWS AMI published:\\n\`tooljet_${{ env.RELEASE_VERSION }}.ubuntu_focal\`"
message="ToolJet enterprise AWS AMI published:\\n\`tooljet_${{ env.RELEASE_VERSION }}.ubuntu-jammy\`"
else
message="ToolJet enterprise AWS AMI release failed! \\n\`tooljet_${{ env.RELEASE_VERSION }}.ubuntu_focal\`"
message="ToolJet enterprise AWS AMI release failed! \\n\`tooljet_${{ env.RELEASE_VERSION }}.ubuntu-jammy\`"
fi
curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" ${{ secrets.SLACK_WEBHOOK_URL }}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

After

Width:  |  Height:  |  Size: 310 B

View file

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

After

Width:  |  Height:  |  Size: 307 B

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -117,6 +117,9 @@ class DataSourceManagerComponent extends React.Component {
selectedDataSourceIcon: this.props.selectedDataSource?.plugin?.iconFile?.data,
connectionTestError: null,
datasourceName: this.props.selectedDataSource?.name,
validationMessages: {},
validationError: [],
showValidationErrors: false,
});
}
}
@ -146,6 +149,9 @@ class DataSourceManagerComponent extends React.Component {
dataSourceSchema: source.manifestFile?.data,
selectedDataSourcePluginId: source.id,
datasourceName: source.name,
validationMessages: {},
validationError: [],
showValidationErrors: false,
},
() => this.createDataSource()
);
@ -413,6 +419,7 @@ class DataSourceManagerComponent extends React.Component {
const ComponentToRender = isPlugin ? SourceComponent : SourceComponents[sourceComponentName] || SourceComponent;
return (
<ComponentToRender
key={this.state.selectedDataSource?.id}
dataSourceSchema={this.state.dataSourceSchema}
optionsChanged={(options = {}) => this.setState({ options })}
optionchanged={this.optionchanged}
@ -988,7 +995,7 @@ class DataSourceManagerComponent extends React.Component {
<input
type="text"
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)}
style={{ width: '160px' }}
data-cy="data-source-name-input-field"

View file

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

View file

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

View file

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

View file

@ -13,8 +13,7 @@
},
"options": {
"url": {
"type": "string",
"encrypted": false
"type": "string"
},
"apiKey": {
"type": "string",
@ -29,8 +28,7 @@
"key": "url",
"type": "text",
"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.",
"encrypted": true
"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."
},
"apiKey": {
"label": "API Key",

View file

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

View file

@ -1,6 +1,7 @@
import { QueryError } from './query.error';
import * as tls from 'tls';
import { readFileSync } from 'fs';
import crypto from 'crypto';
const CACHED_CONNECTIONS: any = {};
@ -17,8 +18,29 @@ export function cacheConnection(dataSourceId: string, connection: any): any {
CACHED_CONNECTIONS[dataSourceId] = { connection, updatedAt };
}
export function getCachedConnection(dataSourceId: string | number, dataSourceUpdatedAt: any): any {
const cachedData = CACHED_CONNECTIONS[dataSourceId];
export function generateSourceOptionsHash(sourceOptions) {
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) {
const updatedAt = new Date(dataSourceUpdatedAt || null);

View file

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

View file

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

View file

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

View file

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

View file

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

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