mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-22 08:28:35 +00:00
Merge branch 'main' into ai-modularisation-bug-fix
This commit is contained in:
commit
5d8b080589
144 changed files with 3361 additions and 1636 deletions
6
.github/workflows/packer-build.yml
vendored
6
.github/workflows/packer-build.yml
vendored
|
|
@ -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 }}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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`);
|
||||
|
|
|
|||
|
|
@ -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`);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 })}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
141
frontend/src/AppBuilder/_utils/async-query-handler.js
Normal file
141
frontend/src/AppBuilder/_utils/async-query-handler.js
Normal 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)
|
||||
// );
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
{t('blankPage.importApplication', 'Import an app')}
|
||||
<input
|
||||
disabled={appCreationDisabled}
|
||||
type="file"
|
||||
ref={fileInput}
|
||||
style={{ display: 'none' }}
|
||||
data-cy="import-option-input"
|
||||
/>
|
||||
</label>
|
||||
</ButtonSolid>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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">
|
||||
|
|
|
|||
|
|
@ -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={() => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
295
frontend/src/HomePage/styles/homepage.scss
Normal file
295
frontend/src/HomePage/styles/homepage.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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')}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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' }}>
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
249
frontend/src/TooljetDatabase/styles/styles.scss
Normal file
249
frontend/src/TooljetDatabase/styles/styles.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
111
frontend/src/_components/PageSearchBox.jsx
Normal file
111
frontend/src/_components/PageSearchBox.jsx
Normal 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,
|
||||
};
|
||||
|
|
@ -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(
|
||||
(
|
||||
|
|
|
|||
85
frontend/src/_components/_styles/page-search-box.scss
Normal file
85
frontend/src/_components/_styles/page-search-box.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
65
frontend/src/_components/_styles/search-box.scss
Normal file
65
frontend/src/_components/_styles/search-box.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
8
frontend/src/_stores/workflowStore.js
Normal file
8
frontend/src/_stores/workflowStore.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import create from 'zustand';
|
||||
|
||||
const useWorkflowStore = create((set) => ({
|
||||
workflowId: null,
|
||||
setWorkflowId: (id) => set({ workflowId: id }),
|
||||
}));
|
||||
|
||||
export default useWorkflowStore;
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
13
frontend/src/_styles/rocket/card.scss
Normal file
13
frontend/src/_styles/rocket/card.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -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'}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
}
|
||||
|
||||
.form-check>.form-check-input:not(:checked) {
|
||||
background-color: #ffffff;
|
||||
background-color: var(--slider-track);
|
||||
}
|
||||
.text-wrappers{
|
||||
display: flex;
|
||||
|
|
|
|||
35
frontend/src/components/ui/Card/Index.jsx
Normal file
35
frontend/src/components/ui/Card/Index.jsx
Normal 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 };
|
||||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
}
|
||||
.load.dark-loader {
|
||||
display: flex;
|
||||
background-color: #1f2936;
|
||||
background-color: #1E2226;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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%' }}>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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' }}>
|
||||
|
|
|
|||
|
|
@ -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) => ({
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ if (process.env.APM_VENDOR === 'sentry') {
|
|||
}
|
||||
|
||||
if (isDevEnv) {
|
||||
plugins.push(new ReactRefreshWebpackPlugin());
|
||||
plugins.push(new ReactRefreshWebpackPlugin({ overlay: false }));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue