mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-23 08:58:26 +00:00
Merge pull request #12401 from ToolJet/release/marketplace-sprint-9
Release: Marketplace Sprint 9 🚀 Features Auto install plugin based on queries on app import in #12350 by @ganesh8056 Azure Repos plugin integration in #11707 by @kushalsourav NocoDB plugin integration in #11476 by @wusuopu 🌟 Enhancements Added dynamic form validation setup for Postgres in #12292 by @parthy007 BigQuery now shows detailed errors and has documentation link in #12384 by @thesynthax DTO validation for data types in ToolJet database in #123678 by @ganesh8056 🛠️ Fixes Fixes TJDB error message text not having proper column name on bulk upload in #12346 by @manishkushare The data in the time field of the calendar is not visible when scrolling vertically in #12352 by @manishkushare Fixes unnecessary API calls on data source config page in #12368 by @ganesh8056 Exporting an application for a selected version does not export the ToolJet database schema in #12368 by @ganesh8056
This commit is contained in:
commit
0941ba4571
42 changed files with 1488 additions and 320 deletions
34
.github/workflows/render-preview-deploy.yml
vendored
34
.github/workflows/render-preview-deploy.yml
vendored
|
|
@ -13,12 +13,42 @@ permissions:
|
|||
jobs:
|
||||
|
||||
# Community Edition
|
||||
|
||||
create-ce-review-app:
|
||||
if: ${{ github.event.action == 'labeled' && (github.event.label.name == 'create-ce-review-app' || github.event.label.name == 'review-app') }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Sync repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Check if Forked Repository
|
||||
id: check_repo
|
||||
run: |
|
||||
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
|
||||
echo "is_fork=true" >> $GITHUB_ENV
|
||||
echo "FORKED_OWNER=${{ github.event.pull_request.head.repo.owner.login }}" >> $GITHUB_ENV
|
||||
else
|
||||
echo "is_fork=false" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Set Repository URL
|
||||
run: |
|
||||
if [[ "$is_fork" == "true" ]]; then
|
||||
echo "REPO_URL=https://github.com/${FORKED_OWNER}/ToolJet" >> $GITHUB_ENV
|
||||
else
|
||||
echo "REPO_URL=https://github.com/ToolJet/ToolJet" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Fetch and Checkout Forked Branch
|
||||
if: env.is_fork == 'true'
|
||||
run: |
|
||||
git fetch origin pull/${{ github.event.number }}/head:${{ env.BRANCH_NAME }}
|
||||
git checkout ${{ env.BRANCH_NAME }}
|
||||
|
||||
- name: Checkout Default Branch
|
||||
if: env.is_fork == 'false'
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Creating deployment for CE
|
||||
id: create-ce-deployment
|
||||
run: |
|
||||
|
|
@ -34,7 +64,7 @@ jobs:
|
|||
"name": "ToolJet CE PR #${{ env.PR_NUMBER }}",
|
||||
"notifyOnFail": "default",
|
||||
"ownerId": "tea-caeo4bj19n072h3dddc0",
|
||||
"repo": "https://github.com/ToolJet/ToolJet",
|
||||
"repo": "'"$REPO_URL"'",
|
||||
"slug": "tooljet-ce-pr-${{ env.PR_NUMBER }}",
|
||||
"suspended": "not_suspended",
|
||||
"suspenders": [],
|
||||
|
|
|
|||
2
.version
2
.version
|
|
@ -1 +1 @@
|
|||
3.7.0
|
||||
3.8.0
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
3.7.0
|
||||
3.8.0
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 715a830c7a8d75efc7f77106292d9e4499005b69
|
||||
Subproject commit 4b950ed3d0ba15edddf217936e9c9ae1ca3cf11a
|
||||
|
|
@ -219,4 +219,11 @@
|
|||
.react-datepicker__navigation{
|
||||
overflow: visible !important;
|
||||
height: inherit !important;
|
||||
}
|
||||
.tjdb-td-wrapper{
|
||||
.react-datepicker-time__input{
|
||||
input{
|
||||
line-height: normal !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -300,7 +300,7 @@ export const DateTimePicker = ({
|
|||
return (
|
||||
<div
|
||||
data-disabled={styles.disabledState}
|
||||
className={cx('datepicker-widget', {
|
||||
className={cx('datepicker-widget position-relative', {
|
||||
'theme-tjdb': !darkMode,
|
||||
'theme-dark': darkMode,
|
||||
})}
|
||||
|
|
@ -324,7 +324,7 @@ export const DateTimePicker = ({
|
|||
})}
|
||||
popperPlacement={'bottom-start'}
|
||||
popperClassName={cx({
|
||||
'tjdb-datepicker-reset': !isEditCell,
|
||||
// 'tjdb-datepicker-reset': !isEditCell,
|
||||
'tjdb-datepicker-celledit-reset': isEditCell,
|
||||
})}
|
||||
onInputClick={() => {
|
||||
|
|
|
|||
|
|
@ -117,6 +117,9 @@
|
|||
margin-top: 4px;
|
||||
box-shadow: 0px 8px 16px 0px #3032331A;
|
||||
}
|
||||
.react-datepicker-popper {
|
||||
z-index: 10001 !important;
|
||||
}
|
||||
|
||||
.react-datepicker-time__caption{
|
||||
margin-left:20px
|
||||
|
|
@ -234,4 +237,11 @@
|
|||
line-height: normal !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.table-schema-row{
|
||||
.react-datepicker-time__input-container{
|
||||
input{
|
||||
line-height: normal !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam
|
|||
});
|
||||
}
|
||||
|
||||
if (item.kind === 'tooljetdb' && item.options.table_id) extractedIdData.push(item.options.table_id);
|
||||
if (item.kind === 'tooljetdb' && item.options.tableId) extractedIdData.push(item.options.tableId);
|
||||
});
|
||||
const uniqueSet = new Set(extractedIdData);
|
||||
const selectedVersiontable = Array.from(uniqueSet).map((item) => ({ table_id: item }));
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
libraryAppService,
|
||||
gitSyncService,
|
||||
licenseService,
|
||||
pluginsService,
|
||||
} from '@/_services';
|
||||
import { ConfirmDialog, AppModal } from '@/_components';
|
||||
import Select from '@/_ui/Select';
|
||||
|
|
@ -113,7 +114,7 @@ class HomePageComponent extends React.Component {
|
|||
showUserGroupMigrationModal: false,
|
||||
showGroupMigrationBanner: true,
|
||||
shouldAutoImportPlugin: false,
|
||||
dependentPluginsForTemplate: [],
|
||||
dependentPlugins: [],
|
||||
dependentPluginsDetail: {},
|
||||
};
|
||||
}
|
||||
|
|
@ -310,7 +311,7 @@ class HomePageComponent extends React.Component {
|
|||
const fileReader = new FileReader();
|
||||
const fileName = file.name.replace('.json', '').substring(0, 50);
|
||||
fileReader.readAsText(file, 'UTF-8');
|
||||
fileReader.onload = (event) => {
|
||||
fileReader.onload = async (event) => {
|
||||
const result = event.target.result;
|
||||
let fileContent;
|
||||
try {
|
||||
|
|
@ -319,8 +320,26 @@ class HomePageComponent extends React.Component {
|
|||
toast.error(`Could not import: ${parseError}`);
|
||||
return;
|
||||
}
|
||||
this.setState({ fileContent, fileName, showImportAppModal: true });
|
||||
|
||||
const importedAppDef = fileContent.app || fileContent.appV2;
|
||||
const dataSourcesUsedInApps = [];
|
||||
importedAppDef.forEach((appDefinition) => {
|
||||
appDefinition?.definition?.appV2?.dataSources.forEach((dataSource) => {
|
||||
dataSourcesUsedInApps.push(dataSource);
|
||||
});
|
||||
});
|
||||
|
||||
const dependentPluginsResponse = await pluginsService.findDependentPlugins(dataSourcesUsedInApps);
|
||||
const { pluginsToBeInstalled = [], pluginsListIdToDetailsMap = {} } = dependentPluginsResponse.data;
|
||||
this.setState({
|
||||
fileContent,
|
||||
fileName,
|
||||
showImportAppModal: true,
|
||||
dependentPlugins: pluginsToBeInstalled,
|
||||
dependentPluginsDetail: { ...pluginsListIdToDetailsMap },
|
||||
});
|
||||
};
|
||||
|
||||
fileReader.onerror = (error) => {
|
||||
toast.error(`Could not import the app: ${error}`);
|
||||
return;
|
||||
|
|
@ -348,12 +367,19 @@ class HomePageComponent extends React.Component {
|
|||
importJSON.app[0].appName = appName;
|
||||
}
|
||||
const requestBody = { organization_id, ...importJSON };
|
||||
let installedPluginsInfo = [];
|
||||
try {
|
||||
if (this.state.dependentPlugins.length) {
|
||||
({ installedPluginsInfo = [] } = await pluginsService.installDependentPlugins(
|
||||
this.state.dependentPlugins,
|
||||
true
|
||||
));
|
||||
}
|
||||
|
||||
const data = await appsService.importResource(requestBody);
|
||||
toast.success('App imported successfully.');
|
||||
this.setState({
|
||||
isImportingApp: false,
|
||||
});
|
||||
this.setState({ isImportingApp: false });
|
||||
|
||||
if (!isEmpty(data.imports.app)) {
|
||||
this.props.navigate(`/${getWorkspaceId()}/apps/${data.imports.app[0].id}`, {
|
||||
state: { commitEnabled: this.state.commitEnabled },
|
||||
|
|
@ -362,12 +388,13 @@ class HomePageComponent extends React.Component {
|
|||
this.props.navigate(`/${getWorkspaceId()}/database`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.setState({
|
||||
isImportingApp: false,
|
||||
});
|
||||
if (error.statusCode === 409) {
|
||||
return false;
|
||||
if (installedPluginsInfo.length) {
|
||||
const pluginsId = installedPluginsInfo.map((pluginInfo) => pluginInfo.id);
|
||||
await pluginsService.uninstallPlugins(pluginsId);
|
||||
}
|
||||
|
||||
this.setState({ isImportingApp: false });
|
||||
if (error.statusCode === 409) return false;
|
||||
toast.error(error?.error || error?.message || 'App import failed');
|
||||
}
|
||||
};
|
||||
|
|
@ -380,7 +407,7 @@ class HomePageComponent extends React.Component {
|
|||
const data = await libraryAppService.deploy(
|
||||
id,
|
||||
appName,
|
||||
this.state.dependentPluginsForTemplate,
|
||||
this.state.dependentPlugins,
|
||||
this.state.shouldAutoImportPlugin
|
||||
);
|
||||
this.setState({ deploying: false });
|
||||
|
|
@ -732,7 +759,7 @@ class HomePageComponent extends React.Component {
|
|||
selectedTemplate: template,
|
||||
...(plugins_to_be_installed.length && {
|
||||
shouldAutoImportPlugin: true,
|
||||
dependentPluginsForTemplate: plugins_to_be_installed,
|
||||
dependentPlugins: plugins_to_be_installed,
|
||||
dependentPluginsDetail: { ...plugins_detail_by_id },
|
||||
}),
|
||||
});
|
||||
|
|
@ -750,7 +777,7 @@ class HomePageComponent extends React.Component {
|
|||
this.setState({
|
||||
showCreateAppFromTemplateModal: false,
|
||||
selectedTemplate: null,
|
||||
dependentPluginsForTemplate: [],
|
||||
dependentPlugins: [],
|
||||
dependentPluginsDetail: {},
|
||||
shouldAutoImportPlugin: false,
|
||||
});
|
||||
|
|
@ -763,6 +790,20 @@ class HomePageComponent extends React.Component {
|
|||
closeCreateAppModal = () => {
|
||||
this.setState({ showCreateAppModal: false, showCreateModuleModal: false });
|
||||
};
|
||||
|
||||
openImportAppModal = async () => {
|
||||
this.setState({ showImportAppModal: true });
|
||||
};
|
||||
|
||||
closeImportAppModal = () => {
|
||||
this.setState({
|
||||
showImportAppModal: false,
|
||||
dependentPlugins: [],
|
||||
dependentPluginsDetail: {},
|
||||
shouldAutoImportPlugin: false,
|
||||
});
|
||||
};
|
||||
|
||||
isWithinSevenDaysOfSignUp = (date) => {
|
||||
const currentDate = new Date().toISOString();
|
||||
const differenceInTime = new Date(currentDate).getTime() - new Date(date).getTime();
|
||||
|
|
@ -836,7 +877,7 @@ class HomePageComponent extends React.Component {
|
|||
workflowInstanceLevelLimit,
|
||||
showUserGroupMigrationModal,
|
||||
showGroupMigrationBanner,
|
||||
dependentPluginsForTemplate,
|
||||
dependentPlugins,
|
||||
dependentPluginsDetail,
|
||||
} = this.state;
|
||||
const modalConfigs = {
|
||||
|
|
@ -865,12 +906,14 @@ class HomePageComponent extends React.Component {
|
|||
modalType: 'import',
|
||||
closeModal: () => this.setState({ showImportAppModal: false }),
|
||||
processApp: this.importFile,
|
||||
show: () => this.setState({ showImportAppModal: true }),
|
||||
show: this.openImportAppModal,
|
||||
title: 'Import app',
|
||||
actionButton: 'Import app',
|
||||
actionLoadingButton: 'Importing',
|
||||
fileContent: fileContent,
|
||||
selectedAppName: fileName,
|
||||
dependentPluginsDetail: dependentPluginsDetail,
|
||||
dependentPlugins: dependentPlugins,
|
||||
},
|
||||
template: {
|
||||
modalType: 'template',
|
||||
|
|
@ -882,7 +925,7 @@ class HomePageComponent extends React.Component {
|
|||
actionLoadingButton: 'Creating',
|
||||
templateDetails: this.state.selectedTemplate,
|
||||
dependentPluginsDetail: dependentPluginsDetail,
|
||||
dependentPluginsForTemplate: dependentPluginsForTemplate,
|
||||
dependentPlugins: dependentPlugins,
|
||||
},
|
||||
};
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export function AppModal({
|
|||
handleCommitEnableChange,
|
||||
appType,
|
||||
dependentPluginsDetail = [],
|
||||
dependentPluginsForTemplate = [],
|
||||
dependentPlugins = [],
|
||||
}) {
|
||||
if (!selectedAppName && templateDetails) {
|
||||
selectedAppName = templateDetails?.name || '';
|
||||
|
|
@ -238,10 +238,10 @@ export function AppModal({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
{dependentPluginsForTemplate && dependentPluginsForTemplate.length >= 1 && (
|
||||
{dependentPlugins && dependentPlugins.length >= 1 && (
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<PluginsListForAppModal
|
||||
dependentPluginsForTemplate={dependentPluginsForTemplate}
|
||||
dependentPlugins={dependentPlugins}
|
||||
dependentPluginsDetail={dependentPluginsDetail}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
510
frontend/src/_components/DynamicFormV2.jsx
Normal file
510
frontend/src/_components/DynamicFormV2.jsx
Normal file
|
|
@ -0,0 +1,510 @@
|
|||
import React from 'react';
|
||||
import cx from 'classnames';
|
||||
import DataSourceSchemaManager from '@/_helpers/dataSourceSchemaManager';
|
||||
import Textarea from '@/_ui/Textarea';
|
||||
import Input from '@/_ui/Input';
|
||||
import Select from '@/_ui/Select';
|
||||
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';
|
||||
import { orgEnvironmentVariableService, orgEnvironmentConstantService } from '../_services';
|
||||
import { Constants } from '@/_helpers/utils';
|
||||
|
||||
const DynamicFormV2 = ({
|
||||
schema,
|
||||
options,
|
||||
optionchanged,
|
||||
optionsChanged,
|
||||
selectedDataSource,
|
||||
isEditMode,
|
||||
layout = 'vertical',
|
||||
onBlur,
|
||||
setDefaultOptions,
|
||||
currentAppEnvironmentId,
|
||||
isGDS,
|
||||
validationMessages,
|
||||
setValidationMessages,
|
||||
clearValidationMessages,
|
||||
}) => {
|
||||
const uiProperties = schema['tj:ui:properties'] || {};
|
||||
const dsm = React.useMemo(() => new DataSourceSchemaManager(schema), [schema]);
|
||||
const encryptedProperties = React.useMemo(() => dsm.getEncryptedProperties(), [dsm]);
|
||||
const [conditionallyRequiredProperties, setConditionallyRequiredProperties] = React.useState([]);
|
||||
const [workspaceVariables, setWorkspaceVariables] = React.useState([]);
|
||||
const [currentOrgEnvironmentConstants, setCurrentOrgEnvironmentConstants] = React.useState([]);
|
||||
const [computedProps, setComputedProps] = React.useState({});
|
||||
const [hasUserInteracted, setHasUserInteracted] = React.useState(false);
|
||||
const [interactedFields, setInteractedFields] = React.useState(new Set());
|
||||
|
||||
const isHorizontalLayout = layout === 'horizontal';
|
||||
const prevDataSourceIdRef = React.useRef(selectedDataSource?.id);
|
||||
|
||||
const globalDataSourcesStatus = useGlobalDataSourcesStatus();
|
||||
const { isEditing: isDataSourceEditing } = globalDataSourcesStatus;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isGDS) {
|
||||
orgEnvironmentConstantService.getConstantsFromEnvironment(currentAppEnvironmentId).then((data) => {
|
||||
const constants = {
|
||||
globals: {},
|
||||
secrets: {},
|
||||
};
|
||||
data.constants.forEach((constant) => {
|
||||
if (constant.type === Constants.Secret) {
|
||||
constants.secrets[constant.name] = constant.value;
|
||||
} else {
|
||||
constants.globals[constant.name] = constant.value;
|
||||
}
|
||||
});
|
||||
|
||||
setCurrentOrgEnvironmentConstants(constants);
|
||||
});
|
||||
|
||||
orgEnvironmentVariableService.getVariables().then((data) => {
|
||||
const client_variables = {};
|
||||
const server_variables = {};
|
||||
data.variables.map((variable) => {
|
||||
if (variable.variable_type === 'server') {
|
||||
server_variables[variable.variable_name] = 'HiddenEnvironmentVariable';
|
||||
} else {
|
||||
client_variables[variable.variable_name] = variable.value;
|
||||
}
|
||||
});
|
||||
|
||||
setWorkspaceVariables({ client: client_variables, server: server_variables });
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
setWorkspaceVariables([]);
|
||||
setCurrentOrgEnvironmentConstants([]);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentAppEnvironmentId]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!hasUserInteracted) return;
|
||||
const { valid, errors } = dsm.validateData(options);
|
||||
|
||||
if (valid) {
|
||||
clearValidationMessages();
|
||||
} else {
|
||||
setValidationMessages(errors, schema);
|
||||
const requiredFields = errors
|
||||
.filter((error) => error.keyword === 'required')
|
||||
.map((error) => error.params.missingProperty);
|
||||
setConditionallyRequiredProperties(requiredFields);
|
||||
}
|
||||
}, [options]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const prevDataSourceId = prevDataSourceIdRef.current;
|
||||
prevDataSourceIdRef.current = selectedDataSource?.id;
|
||||
const uiProperties = schema['tj:ui:properties'];
|
||||
if (!isEmpty(uiProperties)) {
|
||||
let fields = {};
|
||||
let encryptedFieldsProps = {};
|
||||
const flipComponentDropdown = find(uiProperties, ['widget', 'dropdown-component-flip']);
|
||||
|
||||
if (flipComponentDropdown) {
|
||||
const selector = options?.[flipComponentDropdown?.key]?.value;
|
||||
const commonFieldsFromSslCertificate = uiProperties[selector]?.ssl_certificate?.commonFields;
|
||||
fields = {
|
||||
...commonFieldsFromSslCertificate,
|
||||
...flipComponentDropdown?.commonFields,
|
||||
...uiProperties[selector],
|
||||
};
|
||||
} else {
|
||||
fields = { ...uiProperties };
|
||||
}
|
||||
|
||||
const processFields = (fieldsObject) => {
|
||||
Object.keys(fieldsObject).forEach((key) => {
|
||||
const field = fieldsObject[key];
|
||||
const { widget, encrypted, key: propertyKey } = 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)) {
|
||||
encryptedFieldsProps[propertyKey] = {
|
||||
disabled: !!selectedDataSource?.id,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// To check for nested dropdown-component-flip
|
||||
if (widget === 'dropdown-component-flip') {
|
||||
const selectedOption = options?.[field.key]?.value;
|
||||
|
||||
if (field.commonFields) {
|
||||
processFields(field.commonFields);
|
||||
}
|
||||
|
||||
if (selectedOption && fieldsObject[selectedOption]) {
|
||||
processFields(fieldsObject[selectedOption]);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
processFields(fields);
|
||||
|
||||
if (uiProperties.renderForm) {
|
||||
Object.keys(uiProperties.renderForm).forEach((sectionKey) => {
|
||||
const section = uiProperties.renderForm[sectionKey];
|
||||
const { inputs } = section;
|
||||
if (inputs) {
|
||||
processFields(inputs);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (prevDataSourceId !== selectedDataSource?.id) {
|
||||
setComputedProps({ ...encryptedFieldsProps });
|
||||
} else {
|
||||
setComputedProps({ ...computedProps, ...encryptedFieldsProps });
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedDataSource?.id, options, isDataSourceEditing]);
|
||||
|
||||
const getElement = (type) => {
|
||||
switch (type) {
|
||||
case 'password':
|
||||
case 'text':
|
||||
return Input;
|
||||
case 'password-v3':
|
||||
case 'text-v3':
|
||||
return InputV3;
|
||||
case 'textarea':
|
||||
return Textarea;
|
||||
case 'toggle':
|
||||
return Toggle;
|
||||
case 'react-component-headers':
|
||||
return Headers;
|
||||
// TODO: Move dropdown component flip logic to be handled here
|
||||
// case 'dropdown-component-flip':
|
||||
// return Select;
|
||||
default:
|
||||
return <div>Type is invalid</div>;
|
||||
}
|
||||
};
|
||||
|
||||
const getElementProps = (uiProperties) => {
|
||||
const { label, description, widget, required, width, key, help_text: helpText, list, buttonText } = uiProperties;
|
||||
|
||||
const isRequired = required || conditionallyRequiredProperties.includes(key);
|
||||
const isEncrypted = widget === 'password-v3' || encryptedProperties.includes(key);
|
||||
const currentValue = options?.[key]?.value;
|
||||
|
||||
const handleOptionChange = (key, value, flag) => {
|
||||
if (!hasUserInteracted) {
|
||||
setHasUserInteracted(true);
|
||||
}
|
||||
setInteractedFields((prev) => new Set(prev).add(key));
|
||||
optionchanged(key, value, flag);
|
||||
};
|
||||
|
||||
switch (widget) {
|
||||
case 'password':
|
||||
case 'text':
|
||||
case 'textarea': {
|
||||
return {
|
||||
key,
|
||||
widget,
|
||||
label,
|
||||
placeholder: isEncrypted ? '**************' : description,
|
||||
className: cx('form-control', {
|
||||
'dynamic-form-encrypted-field': isEncrypted,
|
||||
}),
|
||||
style: { marginBottom: '0px !important' },
|
||||
helpText: helpText,
|
||||
value: currentValue || '',
|
||||
onChange: (e) => optionchanged(key, e.target.value, true),
|
||||
isGDS: true,
|
||||
workspaceVariables: [],
|
||||
workspaceConstants: [],
|
||||
encrypted: isEncrypted,
|
||||
onBlur,
|
||||
};
|
||||
}
|
||||
case 'password-v3':
|
||||
case 'text-v3': {
|
||||
return {
|
||||
key,
|
||||
widget,
|
||||
label,
|
||||
placeholder: isEncrypted ? '**************' : description,
|
||||
className: cx('form-control', {
|
||||
'dynamic-form-encrypted-field': isEncrypted,
|
||||
}),
|
||||
style: { marginBottom: '0px !important' },
|
||||
helpText: helpText,
|
||||
value: currentValue || '',
|
||||
onChange: (e) => handleOptionChange(key, e.target.value, true),
|
||||
isGDS: true,
|
||||
workspaceVariables: [],
|
||||
workspaceConstants: [],
|
||||
encrypted: isEncrypted,
|
||||
onBlur,
|
||||
isRequired: isRequired,
|
||||
isValidatedMessages:
|
||||
!hasUserInteracted || !interactedFields.has(key)
|
||||
? { valid: null, message: '' } // skip validation for initial render and untouched elements
|
||||
: validationMessages[key]
|
||||
? { valid: false, message: validationMessages[key] }
|
||||
: isRequired && !isEncrypted
|
||||
? { valid: true, message: '' }
|
||||
: { valid: null, message: '' }, // handle optional && encrypted fields
|
||||
isDisabled: !canUpdateDataSource(selectedDataSource?.id) && !canDeleteDataSource(),
|
||||
};
|
||||
}
|
||||
case 'react-component-headers': {
|
||||
let isRenderedAsQueryEditor;
|
||||
if (isGDS) {
|
||||
isRenderedAsQueryEditor = false;
|
||||
} else {
|
||||
isRenderedAsQueryEditor = !isGDS;
|
||||
}
|
||||
return {
|
||||
getter: key,
|
||||
options: isRenderedAsQueryEditor
|
||||
? options?.[key] ?? schema?.defaults?.[key]
|
||||
: options?.[key]?.value ?? schema?.defaults?.[key]?.value,
|
||||
optionchanged,
|
||||
isRenderedAsQueryEditor,
|
||||
workspaceConstants: currentOrgEnvironmentConstants,
|
||||
isDisabled: !canUpdateDataSource(selectedDataSource?.id) && !canDeleteDataSource(),
|
||||
encrypted: isEncrypted,
|
||||
buttonText,
|
||||
width: width,
|
||||
};
|
||||
}
|
||||
case 'toggle':
|
||||
return {
|
||||
defaultChecked: currentValue,
|
||||
checked: currentValue,
|
||||
onChange: (e) => optionchanged(key, e.target.checked),
|
||||
};
|
||||
case 'dropdown':
|
||||
case 'dropdown-component-flip':
|
||||
return {
|
||||
options: list,
|
||||
value: options?.[key]?.value || options?.[key],
|
||||
onChange: (value) => optionchanged(key, value),
|
||||
width: width || '100%',
|
||||
encrypted: options?.[key]?.encrypted,
|
||||
};
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
const getLayout = (uiProperties) => {
|
||||
if (isEmpty(uiProperties)) return null;
|
||||
const flipComponentDropdown = isFlipComponentDropdown(uiProperties);
|
||||
|
||||
if (flipComponentDropdown) {
|
||||
return flipComponentDropdown;
|
||||
}
|
||||
|
||||
const handleEncryptedFieldsToggle = (event, field) => {
|
||||
if (!canUpdateDataSource(selectedDataSource?.id) && !canDeleteDataSource()) {
|
||||
return;
|
||||
}
|
||||
const isEditing = computedProps[field]['disabled'];
|
||||
if (isEditing) {
|
||||
optionchanged(field, '');
|
||||
} else {
|
||||
//Send old field value if editing mode disabled for encrypted fields
|
||||
const newOptions = { ...options };
|
||||
const oldFieldValue = selectedDataSource?.['options']?.[field];
|
||||
if (oldFieldValue) {
|
||||
optionsChanged({ ...newOptions, [field]: oldFieldValue });
|
||||
} else {
|
||||
delete newOptions[field];
|
||||
optionsChanged({ ...newOptions });
|
||||
}
|
||||
}
|
||||
setComputedProps({
|
||||
...computedProps,
|
||||
[field]: {
|
||||
...computedProps[field],
|
||||
disabled: !isEditing,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const renderLabel = (label, tooltip) => {
|
||||
const labelElement = (
|
||||
<label
|
||||
className="form-label"
|
||||
data-cy={`label-${String(label).toLowerCase().replace(/\s+/g, '-')}`}
|
||||
style={{ textDecoration: tooltip ? 'underline 2px dashed' : 'none', textDecorationColor: 'var(--slate8)' }}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
);
|
||||
|
||||
if (tooltip) {
|
||||
return (
|
||||
<OverlayTrigger
|
||||
placement="top"
|
||||
trigger="click"
|
||||
rootClose
|
||||
overlay={<Tooltip id={`tooltip-${label}`}>{tooltip}</Tooltip>}
|
||||
>
|
||||
{labelElement}
|
||||
</OverlayTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
return labelElement;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${isHorizontalLayout ? '' : 'row'}`}>
|
||||
{Object.keys(uiProperties).map((key) => {
|
||||
const { label, widget, encrypted, className, key: propertyKey } = uiProperties[key];
|
||||
const Element = getElement(widget);
|
||||
const isSpecificComponent = ['tooljetdb-operations', 'react-component-api-endpoint'].includes(widget);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx('my-2', {
|
||||
'col-md-12': !className && !isHorizontalLayout,
|
||||
[className]: !!className,
|
||||
'd-flex': isHorizontalLayout,
|
||||
'dynamic-form-row': isHorizontalLayout,
|
||||
})}
|
||||
key={key}
|
||||
>
|
||||
{!isSpecificComponent && (
|
||||
<div
|
||||
className={cx('d-flex', {
|
||||
'form-label': isHorizontalLayout,
|
||||
'align-items-center': !isHorizontalLayout,
|
||||
})}
|
||||
style={{ minWidth: '100px' }}
|
||||
>
|
||||
{label &&
|
||||
widget !== 'text-v3' &&
|
||||
widget !== 'password-v3' &&
|
||||
renderLabel(label, uiProperties[key].tooltip)}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cx(
|
||||
{
|
||||
'flex-grow-1': isHorizontalLayout && !isSpecificComponent,
|
||||
'w-100': isHorizontalLayout && widget !== 'codehinter',
|
||||
},
|
||||
'dynamic-form-element'
|
||||
)}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Element
|
||||
{...getElementProps(uiProperties[key])}
|
||||
{...computedProps[propertyKey]}
|
||||
data-cy={`${String(label).toLocaleLowerCase().replace(/\s+/g, '-')}-text-field`}
|
||||
//to be removed after whole ui is same
|
||||
isHorizontalLayout={isHorizontalLayout}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FlipComponentDropdown = (uiProperties) => {
|
||||
const flipComponentDropdowns = filter(uiProperties, ['widget', 'dropdown-component-flip']);
|
||||
|
||||
const dropdownComponents = flipComponentDropdowns.map((flipComponentDropdown) => {
|
||||
const selector = options?.[flipComponentDropdown?.key]?.value || options?.[flipComponentDropdown?.key];
|
||||
|
||||
return (
|
||||
<div key={flipComponentDropdown.key}>
|
||||
<div className={isHorizontalLayout ? '' : 'row'}>
|
||||
{flipComponentDropdown.commonFields && getLayout(flipComponentDropdown.commonFields)}
|
||||
|
||||
<div
|
||||
className={cx('my-2', {
|
||||
'col-md-12': !flipComponentDropdown.className && !isHorizontalLayout,
|
||||
'd-flex': isHorizontalLayout,
|
||||
'dynamic-form-row': isHorizontalLayout,
|
||||
[flipComponentDropdown.className]: !!flipComponentDropdown.className,
|
||||
})}
|
||||
>
|
||||
{(flipComponentDropdown.label || isHorizontalLayout) && (
|
||||
<label
|
||||
className={cx('form-label')}
|
||||
data-cy={`${String(flipComponentDropdown.label)
|
||||
.toLocaleLowerCase()
|
||||
.replace(/\s+/g, '-')}-dropdown-label`}
|
||||
>
|
||||
{flipComponentDropdown.label}
|
||||
</label>
|
||||
)}
|
||||
|
||||
<div data-cy={'query-select-dropdown'} className={cx({ 'flex-grow-1': isHorizontalLayout })}>
|
||||
<Select {...getElementProps(flipComponentDropdown)} styles={{}} useCustomStyles={false} />
|
||||
</div>
|
||||
{flipComponentDropdown.helpText && (
|
||||
<span className="flip-dropdown-help-text">{flipComponentDropdown.helpText}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{getLayout(uiProperties[selector])}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const normalComponents = Object.keys(uiProperties).map((key) => {
|
||||
const component = uiProperties[key];
|
||||
|
||||
if (component.type && component.type !== 'dropdown-component-flip') {
|
||||
return <div key={key}>{getLayout({ [key]: component })}</div>;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{normalComponents}
|
||||
{dropdownComponents}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const isFlipComponentDropdown = (uiProperties) => {
|
||||
const checkFlipComponents = filter(uiProperties, ['widget', 'dropdown-component-flip']);
|
||||
if (checkFlipComponents.length > 0) {
|
||||
return FlipComponentDropdown(uiProperties);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const flipComponentDropdown = isFlipComponentDropdown(uiProperties);
|
||||
if (flipComponentDropdown) return flipComponentDropdown;
|
||||
return getLayout(uiProperties);
|
||||
};
|
||||
|
||||
export default DynamicFormV2;
|
||||
|
|
@ -2,7 +2,7 @@ import React, { useState } from 'react';
|
|||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
import config from 'config';
|
||||
|
||||
export const PluginsListForAppModal = ({ dependentPluginsForTemplate, dependentPluginsDetail }) => {
|
||||
export const PluginsListForAppModal = ({ dependentPlugins, dependentPluginsDetail }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const toggleExpanded = () => {
|
||||
|
|
@ -29,7 +29,7 @@ export const PluginsListForAppModal = ({ dependentPluginsForTemplate, dependentP
|
|||
)}
|
||||
</span>
|
||||
|
||||
{isExpanded && dependentPluginsForTemplate && dependentPluginsForTemplate.length > 0 && (
|
||||
{isExpanded && dependentPlugins && dependentPlugins.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
minHeight: '20px',
|
||||
|
|
@ -38,7 +38,7 @@ export const PluginsListForAppModal = ({ dependentPluginsForTemplate, dependentP
|
|||
borderLeft: '1px solid var(--border-weak)',
|
||||
}}
|
||||
>
|
||||
{dependentPluginsForTemplate.map((plugin, index) => {
|
||||
{dependentPlugins.map((plugin, index) => {
|
||||
const pluginsName = dependentPluginsDetail[plugin].name || plugin;
|
||||
const iconSrc = `${config.TOOLJET_MARKETPLACE_URL}/marketplace-assets/${plugin}/lib/icon.svg`;
|
||||
return (
|
||||
|
|
|
|||
119
frontend/src/_helpers/dataSourceSchemaManager.js
Normal file
119
frontend/src/_helpers/dataSourceSchemaManager.js
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import Ajv2020 from 'ajv';
|
||||
|
||||
const ajvOptions = {
|
||||
strict: false,
|
||||
allErrors: true,
|
||||
removeAdditional: false,
|
||||
// We disable meta-schema validation to avoid the schema being validated
|
||||
// against the official 2020-12 standard in ways that conflict with custom keywords.
|
||||
validateSchema: false,
|
||||
coerceTypes: true,
|
||||
errorDataPath: 'property',
|
||||
};
|
||||
|
||||
export default class DataSourceSchemaManager {
|
||||
constructor(schema) {
|
||||
this.schema = schema;
|
||||
this.ajv = new Ajv2020(ajvOptions);
|
||||
this.validate = this.ajv.compile(this.schema);
|
||||
}
|
||||
|
||||
validateData(options) {
|
||||
const data = this._convertDataSourceOptionsToData(options);
|
||||
const valid = this.validate(data);
|
||||
if (!valid) {
|
||||
return { valid: false, errors: this.validate.errors };
|
||||
}
|
||||
return { valid: true, errors: [] };
|
||||
}
|
||||
|
||||
getDefaults(options = {}) {
|
||||
const dataWithDefaults = { ...this._convertDataSourceOptionsToData(options) };
|
||||
|
||||
// AJV does not support defaults with conditional schemas
|
||||
// https://ajv.js.org/guide/modifying-data.html#assigning-defaults
|
||||
// Create a schema without conditional properties for default value assignment
|
||||
const schemaWithoutConditionals = {
|
||||
type: this.schema.type,
|
||||
properties: { ...this.schema.properties },
|
||||
};
|
||||
|
||||
// Compile the schema without conditionals to fill in default values
|
||||
const ajvForDefaults = new Ajv2020({
|
||||
...ajvOptions,
|
||||
useDefaults: true,
|
||||
});
|
||||
const applyDefaults = ajvForDefaults.compile(schemaWithoutConditionals);
|
||||
applyDefaults(dataWithDefaults);
|
||||
|
||||
const encryptedProperties = this.getEncryptedProperties();
|
||||
|
||||
// Combine the data with defaults and set encrypted fields to null
|
||||
const combinedData = {
|
||||
...dataWithDefaults,
|
||||
...Object.fromEntries(encryptedProperties.map((key) => [key, null])),
|
||||
};
|
||||
|
||||
return Object.entries(combinedData).reduce((result, [key, value]) => {
|
||||
result[key] = {
|
||||
value: value,
|
||||
encrypted: encryptedProperties.includes(key),
|
||||
};
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
|
||||
getEncryptedProperties() {
|
||||
return this.schema['tj:encrypted'] || [];
|
||||
}
|
||||
|
||||
getSourceMetadata() {
|
||||
const { name, kind, type } = this.schema['tj:source'];
|
||||
|
||||
if (!name || !kind || !type) {
|
||||
throw new Error('Schema is missing required source metadata');
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
kind,
|
||||
type,
|
||||
options: this._getOptionsMetadata(),
|
||||
// Can remove exposed variables?
|
||||
exposedVariables: {
|
||||
isLoading: false,
|
||||
data: {},
|
||||
rawData: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_convertDataSourceOptionsToData(options) {
|
||||
return Object.entries(options).reduce((result, [key, { value }]) => {
|
||||
// Skip empty string values
|
||||
if (value !== '' && value !== null && value !== undefined) {
|
||||
result[key] = value;
|
||||
}
|
||||
|
||||
// Add a dummy value to pass validation for encrypted keys
|
||||
if (this.getEncryptedProperties().includes(key)) {
|
||||
result[key] = 'REDACTED';
|
||||
}
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
|
||||
_getOptionsMetadata() {
|
||||
const options = {};
|
||||
const properties = this.schema.properties || {};
|
||||
|
||||
for (const [key, value] of Object.entries(properties)) {
|
||||
options[key] = { type: value.type };
|
||||
if (value.encrypted) {
|
||||
options[key].encrypted = true;
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
|
@ -8,11 +8,11 @@ export const libraryAppService = {
|
|||
findDependentPluginsInTemplate,
|
||||
};
|
||||
|
||||
function deploy(identifier, appName, dependentPluginsForTemplate = [], shouldAutoImportPlugin = false) {
|
||||
function deploy(identifier, appName, dependentPlugins = [], shouldAutoImportPlugin = false) {
|
||||
const body = {
|
||||
identifier,
|
||||
appName,
|
||||
dependentPluginsForTemplate,
|
||||
dependentPlugins,
|
||||
shouldAutoImportPlugin,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import HttpClient from '@/_helpers/http-client';
|
||||
import config from 'config';
|
||||
import { authHeader, handleResponse } from '@/_helpers';
|
||||
|
||||
const adapter = new HttpClient();
|
||||
|
||||
|
|
@ -22,10 +24,47 @@ function reloadPlugin(id) {
|
|||
return adapter.post(`/plugins/${id}/reload`);
|
||||
}
|
||||
|
||||
function findDependentPlugins(dataSources) {
|
||||
return adapter.post(`/plugins/findDependentPlugins`, dataSources);
|
||||
}
|
||||
|
||||
function installDependentPlugins(dependentPlugins, shouldAutoImportPlugin) {
|
||||
const body = {
|
||||
dependentPlugins,
|
||||
shouldAutoImportPlugin,
|
||||
};
|
||||
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(body),
|
||||
};
|
||||
return fetch(`${config.apiUrl}/plugins/installDependentPlugins`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function uninstallPlugins(pluginsId) {
|
||||
const body = {
|
||||
pluginsId: pluginsId,
|
||||
};
|
||||
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(body),
|
||||
};
|
||||
|
||||
return fetch(`${config.apiUrl}/plugins/uninstallPlugins`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
export const pluginsService = {
|
||||
findAll,
|
||||
installPlugin,
|
||||
updatePlugin,
|
||||
deletePlugin,
|
||||
reloadPlugin,
|
||||
findDependentPlugins,
|
||||
installDependentPlugins,
|
||||
uninstallPlugins,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export default ({
|
|||
disabled={isDisabled}
|
||||
style={{
|
||||
flex: 1,
|
||||
width: width ? width : '300px',
|
||||
width: '316px',
|
||||
borderTopRightRadius: '0',
|
||||
borderBottomRightRadius: '0',
|
||||
borderRight: 'none',
|
||||
|
|
|
|||
85
frontend/src/_ui/Input-V3/index.js
Normal file
85
frontend/src/_ui/Input-V3/index.js
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import React, { useState } from 'react';
|
||||
import cx from 'classnames';
|
||||
import OrgConstantVariablesPreviewBox from '../../_components/OrgConstantsVariablesResolver';
|
||||
import SolidIcon from '../Icon/SolidIcons';
|
||||
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 [isFocused, setIsFocused] = useState(false);
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
|
||||
const handleCopyToClipboard = async () => {
|
||||
if (widget === 'copyToClipboard') {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
toast.success('Copied to clipboard');
|
||||
setIsCopied(true);
|
||||
setTimeout(() => {
|
||||
setIsCopied(false);
|
||||
}, 4000);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy text: ', err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="tj-app-input">
|
||||
<div
|
||||
className={cx('', {
|
||||
'tj-app-input-wrapper': widget === 'password' || widget === 'copyToClipboard' || encrypted,
|
||||
})}
|
||||
style={{ alignItems: 'flex-start' }}
|
||||
>
|
||||
{widget === 'text-v3' && (
|
||||
<InputComponent
|
||||
{...props}
|
||||
value={value}
|
||||
styles="tw-bg-transparent"
|
||||
label={props.label}
|
||||
placeholder={props.placeholder}
|
||||
required={props.isRequired}
|
||||
/>
|
||||
)}
|
||||
{(widget === 'password-v3' || encrypted) && (
|
||||
<div style={{ flex: '1' }}>
|
||||
<InputComponent
|
||||
{...props}
|
||||
type="password"
|
||||
value={value}
|
||||
styles="tw-bg-transparent"
|
||||
label={props.label}
|
||||
placeholder={props.placeholder}
|
||||
required={props.isRequired}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{widget === 'copyToClipboard' &&
|
||||
value &&
|
||||
(!isCopied ? (
|
||||
<div style={{ cursor: 'pointer' }} onClick={handleCopyToClipboard}>
|
||||
{' '}
|
||||
<SolidIcon className="copy-icon" name="copy" />
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ color: 'green' }}>
|
||||
{' '}
|
||||
<span>Copied!</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<OrgConstantVariablesPreviewBox
|
||||
workspaceVariables={workspaceVariables}
|
||||
workspaceConstants={workspaceConstants}
|
||||
isFocused={isFocused}
|
||||
value={value}
|
||||
/>
|
||||
{helpText && <small className="text-muted" dangerouslySetInnerHTML={{ __html: helpText }} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InputV3;
|
||||
|
|
@ -1,28 +1,79 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import NumberInput from './NumberInput';
|
||||
import TextInput from './TextInput';
|
||||
import { HelperMessage, InputLabel, ValidationMessage } from '../InputUtils/InputUtils';
|
||||
import { ButtonSolid } from '../../../../_components/AppButton';
|
||||
|
||||
const CommonInput = ({ label, helperText, disabled, required, ...restProps }) => {
|
||||
const InputComponentType = restProps.type === 'number' ? NumberInput : TextInput;
|
||||
const CommonInput = ({ label, helperText, disabled, required, onChange: change, ...restProps }) => {
|
||||
const { type, encrypted, validation, isValidatedMessages, isDisabled } = 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 handleChange = (e) => {
|
||||
let validateObj;
|
||||
if (restProps.validation) {
|
||||
validateObj = restProps.validation(e);
|
||||
if (validation) {
|
||||
const validateObj = validation(e);
|
||||
setIsValid(validateObj.valid);
|
||||
setMessage(validateObj.message);
|
||||
change(e, validateObj);
|
||||
} else {
|
||||
change(e);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isValidatedMessages) {
|
||||
setIsValid(isValidatedMessages.valid);
|
||||
setMessage(isValidatedMessages.message);
|
||||
}
|
||||
}, [isValidatedMessages]);
|
||||
|
||||
const toggleEditing = () => {
|
||||
if (isDisabled) return;
|
||||
|
||||
const willBeInEditMode = !isEditing;
|
||||
setIsEditing(willBeInEditMode);
|
||||
|
||||
if (willBeInEditMode) {
|
||||
change({ target: { value: '' } });
|
||||
}
|
||||
restProps.onChange(e, validateObj);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{label && <InputLabel disabled={disabled} label={label} required={required} />}
|
||||
<div className="d-flex">
|
||||
{label && <InputLabel disabled={disabled} label={label} required={required} />}
|
||||
{type === 'password' && (
|
||||
<div className="d-flex justify-content-between w-100">
|
||||
<div className="mx-1 col">
|
||||
<ButtonSolid
|
||||
className="datasource-edit-btn mb-2"
|
||||
type="a"
|
||||
variant="tertiary"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
disabled={isDisabled}
|
||||
onClick={toggleEditing}
|
||||
>
|
||||
{isEditing ? 'Cancel' : 'Edit'}
|
||||
</ButtonSolid>
|
||||
</div>
|
||||
|
||||
<div className="col-auto mb-2">
|
||||
<small className="text-green">
|
||||
<img className="mx-2 encrypted-icon" src="assets/images/icons/padlock.svg" width="12" height="12" />
|
||||
Encrypted
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<InputComponentType
|
||||
disabled={disabled}
|
||||
disabled={disabled || (isEncrypted && !isEditing)}
|
||||
required={required}
|
||||
response={isValid}
|
||||
onChange={handleChange}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,11 @@ const NumberInput = ({ size, leadingIcon, response, disabled, ...restProps }) =>
|
|||
const inputStyle = `tw-border-border-default placeholder:tw-text-text-placeholder tw-font-normal disabled:tw-bg-[#CCD1D5]/30 tw-pr-[12px] ${
|
||||
leadingIcon ? (size === 'small' ? 'tw-pl-[32px]' : 'tw-pl-[34px]') : 'tw-pl-[12px]'
|
||||
} ${
|
||||
response === true ? 'tw-border-border-success-strong' : response === false ? 'tw-border-border-danger-strong' : ''
|
||||
response === true
|
||||
? 'tw-border-border-success-strong focus-visible:!tw-ring-0 focus-visible:!tw-ring-offset-0 focus-visible:!tw-border-border-success-strong'
|
||||
: response === false
|
||||
? 'tw-border-border-danger-strong focus-visible:!tw-ring-0 focus-visible:!tw-ring-offset-0 focus-visible:!tw-border-border-success-strong'
|
||||
: ''
|
||||
}`;
|
||||
|
||||
const handleIncrement = () => {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,11 @@ const TextInput = ({
|
|||
const inputStyle = `tw-border-border-default placeholder:tw-text-text-placeholder tw-font-normal disabled:tw-bg-[#CCD1D5]/30 ${
|
||||
leadingIcon ? (size === 'small' ? 'tw-pl-[32px]' : 'tw-pl-[34px]') : 'tw-pl-[12px]'
|
||||
} ${trailingAction ? (size === 'small' ? 'tw-pr-[40px]' : 'tw-pr-[44px]') : 'tw-pr-[12px]'} ${
|
||||
response === true ? '!tw-border-border-success-strong' : response === false ? '!tw-border-border-danger-strong' : ''
|
||||
response === true
|
||||
? '!tw-border-border-success-strong focus-visible:!tw-ring-0 focus-visible:!tw-ring-offset-0 focus-visible:!tw-border-border-success-strong'
|
||||
: response === false
|
||||
? '!tw-border-border-danger-strong focus-visible:!tw-ring-0 focus-visible:!tw-ring-offset-0 focus-visible:!tw-border-border-danger-strong'
|
||||
: ''
|
||||
}`;
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ InputComponent.defaultProps = {
|
|||
size: 'medium',
|
||||
disabled: false,
|
||||
readOnly: '',
|
||||
validation: (e) => {},
|
||||
validation: null,
|
||||
label: '',
|
||||
'aria-label': '',
|
||||
required: false,
|
||||
|
|
|
|||
|
|
@ -1,18 +1,40 @@
|
|||
import * as React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { inputVariants } from './InputUtils/Variants';
|
||||
import SolidIcon from '../../../_ui/Icon/SolidIcons';
|
||||
|
||||
const Input = React.forwardRef(({ className, size, type, ...props }, ref) => {
|
||||
const [isPasswordVisible, setIsPasswordVisible] = React.useState(false);
|
||||
const isPasswordField = type === 'password';
|
||||
|
||||
const togglePasswordVisibility = () => {
|
||||
if (!props.disabled) {
|
||||
setIsPasswordVisible((prev) => !prev);
|
||||
}
|
||||
};
|
||||
|
||||
const Input = React.forwardRef(({ className, size, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
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 disabled:tw-border-transparent`,
|
||||
className
|
||||
<>
|
||||
<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 && (
|
||||
<div onClick={togglePasswordVisibility}>
|
||||
{isPasswordVisible ? (
|
||||
<SolidIcon className="eye-icon" name="eye" />
|
||||
) : (
|
||||
<SolidIcon className="eye-icon" name="eyedisable" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
Input.displayName = 'Input';
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export const ValidationMessage = ({ response, validationMessage, className }) =>
|
|||
htmlFor="validation"
|
||||
type="helper"
|
||||
size="default"
|
||||
className={`tw-font-normal ${response === true ? 'tw-text-text-success' : 'tw-text-text-warning'}`}
|
||||
className={`tw-font-normal ${response === true ? 'tw-text-text-success' : '!tw-text-text-warning'}`}
|
||||
data-cy="validation-label"
|
||||
>
|
||||
{validationMessage}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import DynamicForm from '@/_components/DynamicForm';
|
||||
import DynamicFormV2 from '@/_components/DynamicFormV2';
|
||||
import RunjsSchema from './Runjs.schema.json';
|
||||
import TooljetDbSchema from '@/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/manifest.json';
|
||||
import RunpySchema from './Runpy.schema.json';
|
||||
|
|
@ -7,12 +8,37 @@ import WorkflowsSchema from './Workflows.schema.json';
|
|||
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { allManifests } from '@tooljet/plugins/client';
|
||||
import DataSourceSchemaManager from '@/_helpers/dataSourceSchemaManager';
|
||||
|
||||
const getSchemaDetailsForRender = (schema) => {
|
||||
if (schema['tj:version']) {
|
||||
const dsm = new DataSourceSchemaManager(schema);
|
||||
const initialSourceValues = dsm.getDefaults();
|
||||
return {
|
||||
name: schema['tj:source'].name,
|
||||
kind: schema['tj:source'].kind,
|
||||
type: schema['tj:source'].type,
|
||||
options: initialSourceValues,
|
||||
};
|
||||
}
|
||||
|
||||
const _source = schema.source;
|
||||
const def = schema.defaults ?? {};
|
||||
|
||||
return { ..._source, defaults: def };
|
||||
};
|
||||
|
||||
const getSchemaMetadata = (schema, key) => {
|
||||
if (schema['tj:version']) return schema['tj:source'][key];
|
||||
// Need to depreciate old schema format
|
||||
if (key === 'type') return schema.type;
|
||||
return schema.source[key];
|
||||
};
|
||||
|
||||
//Commonly Used DS
|
||||
|
||||
export const CommonlyUsedDataSources = Object.keys(allManifests)
|
||||
.reduce((accumulator, currentValue) => {
|
||||
const sourceName = allManifests[currentValue]?.source?.name;
|
||||
const sourceName = getSchemaMetadata(allManifests[currentValue], 'name');
|
||||
if (
|
||||
sourceName === 'REST API' ||
|
||||
sourceName === 'MongoDB' ||
|
||||
|
|
@ -20,9 +46,7 @@ export const CommonlyUsedDataSources = Object.keys(allManifests)
|
|||
sourceName === 'Google Sheets' ||
|
||||
sourceName === 'PostgreSQL'
|
||||
) {
|
||||
const _source = allManifests[currentValue].source;
|
||||
const def = allManifests[currentValue]?.defaults ?? {};
|
||||
accumulator.push({ ..._source, defaults: def });
|
||||
accumulator.push(getSchemaDetailsForRender(allManifests[currentValue]));
|
||||
}
|
||||
|
||||
return accumulator;
|
||||
|
|
@ -33,31 +57,23 @@ export const CommonlyUsedDataSources = Object.keys(allManifests)
|
|||
});
|
||||
|
||||
export const DataBaseSources = Object.keys(allManifests).reduce((accumulator, currentValue) => {
|
||||
if (allManifests[currentValue].type === 'database') {
|
||||
const _source = allManifests[currentValue].source;
|
||||
const def = allManifests[currentValue]?.defaults ?? {};
|
||||
|
||||
accumulator.push({ ..._source, defaults: def });
|
||||
if (getSchemaMetadata(allManifests[currentValue], 'type') === 'database') {
|
||||
accumulator.push(getSchemaDetailsForRender(allManifests[currentValue]));
|
||||
}
|
||||
|
||||
return accumulator;
|
||||
}, []);
|
||||
export const ApiSources = Object.keys(allManifests).reduce((accumulator, currentValue) => {
|
||||
if (allManifests[currentValue].type === 'api') {
|
||||
const _source = allManifests[currentValue].source;
|
||||
const def = allManifests[currentValue]?.defaults ?? {};
|
||||
|
||||
accumulator.push({ ..._source, defaults: def });
|
||||
export const ApiSources = Object.keys(allManifests).reduce((accumulator, currentValue) => {
|
||||
if (getSchemaMetadata(allManifests[currentValue], 'type') === 'api') {
|
||||
accumulator.push(getSchemaDetailsForRender(allManifests[currentValue]));
|
||||
}
|
||||
|
||||
return accumulator;
|
||||
}, []);
|
||||
export const CloudStorageSources = Object.keys(allManifests).reduce((accumulator, currentValue) => {
|
||||
if (allManifests[currentValue].type === 'cloud-storage') {
|
||||
const _source = allManifests[currentValue].source;
|
||||
const def = allManifests[currentValue]?.defaults ?? {};
|
||||
|
||||
accumulator.push({ ..._source, defaults: def });
|
||||
if (getSchemaMetadata(allManifests[currentValue], 'type') === 'cloud-storage') {
|
||||
accumulator.push(getSchemaDetailsForRender(allManifests[currentValue]));
|
||||
}
|
||||
|
||||
return accumulator;
|
||||
|
|
@ -73,8 +89,24 @@ export const DataSourceTypes = [
|
|||
];
|
||||
|
||||
export const SourceComponents = Object.keys(allManifests).reduce((accumulator, currentValue) => {
|
||||
accumulator[currentValue] = (props) => <DynamicForm schema={allManifests[currentValue]} isGDS={true} {...props} />;
|
||||
accumulator[currentValue] = (props) => {
|
||||
const schema = allManifests[currentValue];
|
||||
|
||||
if (schema['tj:version']) {
|
||||
return <DynamicFormV2 schema={schema} isGDS={true} {...props} />;
|
||||
}
|
||||
|
||||
return <DynamicForm schema={schema} isGDS={true} {...props} />;
|
||||
};
|
||||
return accumulator;
|
||||
}, {});
|
||||
|
||||
export const SourceComponent = (props) => <DynamicForm schema={props.dataSourceSchema} isGDS={true} {...props} />;
|
||||
export const SourceComponent = (props) => {
|
||||
const schema = props.dataSourceSchema;
|
||||
|
||||
if (schema['tj:version']) {
|
||||
return <DynamicFormV2 schema={schema} isGDS={true} {...props} />;
|
||||
}
|
||||
|
||||
return <DynamicForm schema={schema} isGDS={true} {...props} />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import { LicenseTooltip } from '@/LicenseTooltip';
|
|||
import { DATA_SOURCE_TYPE } from '@/_helpers/constants';
|
||||
import './dataSourceManager.theme.scss';
|
||||
import { canUpdateDataSource } from '@/_helpers';
|
||||
import DataSourceSchemaManager from '@/_helpers/dataSourceSchemaManager';
|
||||
import MultiEnvTabs from './MultiEnvTabs';
|
||||
|
||||
class DataSourceManagerComponent extends React.Component {
|
||||
|
|
@ -81,6 +82,8 @@ class DataSourceManagerComponent extends React.Component {
|
|||
unsavedChangesModal: false,
|
||||
datasourceName,
|
||||
creatingApp: false,
|
||||
validationError: [],
|
||||
validationMessages: {},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -208,8 +211,31 @@ class DataSourceManagerComponent extends React.Component {
|
|||
};
|
||||
|
||||
createDataSource = () => {
|
||||
const { appId, options, selectedDataSource, selectedDataSourcePluginId, dataSourceMeta, dataSourceSchema } =
|
||||
this.state;
|
||||
const {
|
||||
appId,
|
||||
options,
|
||||
selectedDataSource,
|
||||
selectedDataSourcePluginId,
|
||||
dataSourceMeta,
|
||||
dataSourceSchema,
|
||||
validationMessages,
|
||||
} = this.state;
|
||||
|
||||
if (!isEmpty(validationMessages)) {
|
||||
const validationMessageArray = Object.values(validationMessages);
|
||||
this.setState({ validationError: validationMessageArray });
|
||||
toast.error(
|
||||
this.props.t(
|
||||
'editor.queryManager.dataSourceManager.toast.error.validationFailed',
|
||||
'Validation failed. Please check your inputs.'
|
||||
),
|
||||
{ position: 'top-center' }
|
||||
);
|
||||
if (validationMessageArray.length > 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const OAuthDs = ['slack', 'zendesk', 'googlesheets', 'salesforce'];
|
||||
const name = selectedDataSource.name;
|
||||
const kind = selectedDataSource?.kind;
|
||||
|
|
@ -231,6 +257,7 @@ class DataSourceManagerComponent extends React.Component {
|
|||
const value = localStorage.getItem('OAuthCode');
|
||||
parsedOptions.push({ key: 'code', value, encrypted: false });
|
||||
}
|
||||
|
||||
if (name.trim() !== '') {
|
||||
let service = scope === 'global' ? globalDatasourceService : datasourceService;
|
||||
if (selectedDataSource.id) {
|
||||
|
|
@ -335,6 +362,25 @@ class DataSourceManagerComponent extends React.Component {
|
|||
this.setState({ suggestingDatasources: true, activeDatasourceList: '#' });
|
||||
};
|
||||
|
||||
setValidationMessages = (errors, schema) => {
|
||||
const errorMap = errors.reduce((acc, error) => {
|
||||
// Get property name from either required error or dataPath
|
||||
const property =
|
||||
error.keyword === 'required'
|
||||
? error.params.missingProperty
|
||||
: error.dataPath?.replace(/^[./]/, '') || error.instancePath?.replace(/^[./]/, '');
|
||||
|
||||
if (property) {
|
||||
const propertySchema = schema.properties?.[property];
|
||||
const propertyTitle = propertySchema?.title;
|
||||
acc[property] =
|
||||
error.keyword === 'required' ? `${propertyTitle} is required` : `${propertyTitle} ${error.message}`;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
this.setState({ validationMessages: errorMap });
|
||||
};
|
||||
|
||||
renderSourceComponent = (kind, isPlugin = false) => {
|
||||
const { options, isSaving } = this.state;
|
||||
|
||||
|
|
@ -352,6 +398,9 @@ class DataSourceManagerComponent extends React.Component {
|
|||
selectedDataSource={this.state.selectedDataSource}
|
||||
isEditMode={!isEmpty(this.state.selectedDataSource)}
|
||||
currentAppEnvironmentId={this.props.currentEnvironment?.id}
|
||||
validationMessages={this.state.validationMessages}
|
||||
setValidationMessages={this.setValidationMessages}
|
||||
clearValidationMessages={() => this.setState({ validationMessages: {} })}
|
||||
setDefaultOptions={this.setDefaultOptions}
|
||||
/>
|
||||
);
|
||||
|
|
@ -851,6 +900,7 @@ class DataSourceManagerComponent extends React.Component {
|
|||
dataSourceConfirmModalProps,
|
||||
addingDataSource,
|
||||
datasourceName,
|
||||
validationError,
|
||||
} = this.state;
|
||||
const isPlugin = dataSourceSchema ? true : false;
|
||||
const createSelectedDataSource = (dataSource) => {
|
||||
|
|
@ -1056,6 +1106,18 @@ class DataSourceManagerComponent extends React.Component {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{validationError && validationError.length > 0 && (
|
||||
<div className="row w-100">
|
||||
<div className="alert alert-danger" role="alert">
|
||||
{validationError.map((error, index) => (
|
||||
<div key={index} className="text-muted" data-cy="connection-alert-text">
|
||||
{error}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="col">
|
||||
<SolidIcon name="logs" fill="#3E63DD" width="20" style={{ marginRight: '8px' }} />
|
||||
<a
|
||||
|
|
|
|||
|
|
@ -474,7 +474,7 @@ export const GlobalDataSources = ({ darkMode = false, updateSelectedDatasource }
|
|||
<div className="row gx-0">
|
||||
<Sidebar renderSidebarList={renderSidebarList} updateSelectedDatasource={updateSelectedDatasource} />
|
||||
<div ref={containerRef} className={cx('col animation-fade datasource-modal-container', {})}>
|
||||
{containerRef && containerRef?.current && (
|
||||
{containerRef && containerRef?.current && selectedDataSource && (
|
||||
<DataSourceManager
|
||||
showBackButton={selectedDataSource ? false : true}
|
||||
showDataSourceManagerModal={showDataSourceManagerModal}
|
||||
|
|
|
|||
|
|
@ -1,72 +1,15 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<rect x="445.176" y="222.066" width="15.208" height="32.606"/>
|
||||
<path d="M465.275,103.536V55.237c0-10.89-8.859-19.75-19.749-19.75H292.721c-10.89,0-19.75,8.86-19.75,19.75v48.298h-38.498
|
||||
V55.237c0-10.89-8.86-19.75-19.75-19.75H61.918c-10.891,0-19.75,8.86-19.75,19.75v48.298H0v372.977h512V103.536H465.275z
|
||||
M288.178,55.237c0-2.505,2.038-4.542,4.542-4.542h152.805c2.504,0,4.541,2.038,4.541,4.542v48.298H422.47V64.268h-15.208v39.268
|
||||
H288.178V55.237z M57.374,55.237c0-2.505,2.038-4.542,4.542-4.542h152.804c2.504,0,4.542,2.038,4.542,4.542v48.298h-29.126
|
||||
V64.268h-15.208v39.268H57.374V55.237z M496.792,461.305h-36.404V269.298H445.18v192.007H15.208V118.743h26.959h192.305h38.498
|
||||
h192.305h31.517V461.305z"/>
|
||||
<path d="M385.777,275.757c-1.028-0.078-2.067-0.118-3.092-0.118c-10.329,0-20.117,3.942-27.562,11.096
|
||||
c-0.054,0.051-0.105,0.101-0.159,0.15v-81.028h-81.027c0.05-0.054,0.099-0.105,0.149-0.158
|
||||
c7.838-8.156,11.84-19.33,10.978-30.656c-1.449-19.026-16.24-34.353-35.171-36.443c-1.494-0.164-3.008-0.248-4.5-0.248
|
||||
c-21.939,0-39.787,17.848-39.787,39.788c0,10.414,3.994,20.254,11.25,27.719h-81.031v99.334h7.604
|
||||
c6.248,0,12.237-2.492,16.431-6.835c5.122-5.303,12.213-8.006,19.621-7.435c11.548,0.878,21.213,10.196,22.481,21.673
|
||||
c0.787,7.121-1.381,13.948-6.105,19.223c-4.661,5.204-11.337,8.189-18.317,8.189c-6.631,0-12.846-2.602-17.502-7.328
|
||||
c-3.186-3.232-7.177-5.463-11.542-6.449c-0.757-0.172-1.534-0.259-2.307-0.259c-5.715,0-10.364,4.639-10.364,10.343v88.681
|
||||
h88.991c5.703,0,10.343-4.639,10.343-10.342c0-6.248-2.492-12.238-6.836-16.432c-5.285-5.103-7.994-12.256-7.435-19.622
|
||||
c0.879-11.548,10.197-21.212,21.675-22.48c0.943-0.105,1.895-0.157,2.831-0.157c13.553,0,24.58,11.027,24.58,24.58
|
||||
c0,6.606-2.585,12.805-7.281,17.456c-4.555,4.514-7.065,10.396-7.065,16.565c0,5.753,4.68,10.433,10.434,10.433h88.9v-81.031
|
||||
c7.465,7.255,17.304,11.25,27.719,11.25c11.299,0,22.105-4.83,29.646-13.251c7.537-8.415,11.142-19.728,9.893-31.037
|
||||
C420.13,291.995,404.803,277.205,385.777,275.757z M401.001,331.817c-4.661,5.205-11.338,8.19-18.317,8.19
|
||||
c-6.457,0-12.557-2.486-17.174-7c-4.843-4.737-11.287-7.344-18.148-7.344h-7.604v84.126h-67.227
|
||||
c0.262-0.342,0.551-0.671,0.868-0.987c7.6-7.528,11.785-17.564,11.785-28.259c0-21.94-17.848-39.788-39.788-39.788
|
||||
c-1.491,0-3.005,0.084-4.5,0.248c-18.931,2.093-33.722,17.419-35.17,36.442c-0.905,11.898,3.482,23.458,12.036,31.718
|
||||
c0.202,0.195,0.391,0.404,0.569,0.625h-67.295v-64.691c7.3,6.542,16.618,10.117,26.507,10.117
|
||||
c11.298,0,22.104-4.83,29.646-13.251c7.537-8.415,11.142-19.728,9.892-31.038c-2.092-18.93-17.419-33.72-36.444-35.168
|
||||
c-1.027-0.078-2.068-0.118-3.092-0.118c-9.906,0-19.212,3.571-26.508,10.116v-64.689l84.126-0.005v-7.604
|
||||
c0-6.856-2.608-13.299-7.344-18.144c-4.515-4.618-7.001-10.716-7.001-17.174c0-13.553,11.026-24.58,24.579-24.58
|
||||
c0.936,0,1.888,0.053,2.831,0.157c11.478,1.267,20.796,10.932,21.675,22.481c0.541,7.117-1.867,13.851-6.78,18.964
|
||||
c-4.831,5.026-7.491,11.525-7.491,18.3v7.604h84.126v84.126h7.604c6.775,0,13.273-2.66,18.301-7.491
|
||||
c5.061-4.863,11.901-7.314,18.963-6.78c11.548,0.878,21.213,10.196,22.481,21.675
|
||||
C407.893,319.717,405.725,326.542,401.001,331.817z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_519_49)">
|
||||
<path d="M16.9116 2.39346H7.08579C6.70857 2.39346 6.4032 2.70255 6.4032 3.08759V3.09021V5.3743H17.5967V3.09021C17.5967 2.70516 17.2939 2.39346 16.9167 2.39346C16.9141 2.39346 16.9141 2.39346 16.9116 2.39346Z" fill="#BC3618"/>
|
||||
<path d="M18.0689 3.78959H5.93102C5.387 3.78959 4.94305 4.24012 4.94305 4.79542V4.79804V5.59171H19.0594V4.79804C19.0594 4.24274 18.6181 3.78959 18.074 3.78959C18.0715 3.78959 18.0715 3.78959 18.0689 3.78959Z" fill="#D13816"/>
|
||||
<path d="M19.4932 5.21976H4.50686C3.96283 5.21976 3.51889 5.6703 3.51889 6.2256V6.22822V20.5929C3.51889 21.1508 3.96283 21.6039 4.51199 21.6066H19.488C20.0346 21.6066 20.4786 21.1534 20.4811 20.5929V6.22822C20.4811 5.67292 20.0398 5.22238 19.4932 5.21976Z" fill="#E15A18"/>
|
||||
<path d="M8.5229 6.6997C7.48361 6.6997 6.63935 7.56147 6.64191 8.62232C6.64448 9.68316 7.48618 10.5449 8.52547 10.5423C9.56476 10.5423 10.4065 9.68054 10.4065 8.6197C10.409 7.56147 9.56476 6.6997 8.5229 6.6997ZM8.5229 9.86652C7.848 9.86652 7.29885 9.30859 7.29885 8.61708C7.29885 7.92818 7.84544 7.36764 8.5229 7.36764C9.1978 7.36764 9.74695 7.92556 9.74695 8.61708C9.74695 9.30859 9.20036 9.86652 8.5229 9.86652C8.52547 9.86652 8.52547 9.86652 8.5229 9.86652Z" fill="#F7B192"/>
|
||||
<path d="M9.2311 10.3354H7.83511V14.233H9.2311V10.3354Z" fill="#F7B192"/>
|
||||
<path d="M17.4685 8.64852C17.4685 7.58767 16.6242 6.7259 15.5849 6.7259C14.5456 6.7259 13.7014 7.58767 13.7014 8.64852C13.7014 9.44481 14.1812 10.1573 14.9075 10.4428V11.7629L7.83516 14.1466H7.82233V16.5905C6.86002 16.9886 6.39812 18.1071 6.78817 19.0893C7.17823 20.0716 8.27397 20.5431 9.23628 20.1449C10.1986 19.7468 10.6605 18.6283 10.2704 17.6461C10.0831 17.172 9.71615 16.7974 9.25681 16.5983V15.0843L16.2906 12.7112L16.2752 12.6823H16.2906V10.4245C17.0014 10.1363 17.4685 9.43171 17.4685 8.64852ZM9.74694 18.3795C9.74694 19.0684 9.20035 19.6289 8.52289 19.6289C7.84542 19.6289 7.29883 19.071 7.29883 18.3795C7.29883 17.6906 7.84542 17.13 8.52289 17.13C9.20035 17.1327 9.74694 17.6906 9.74694 18.3795ZM15.5849 9.89272C14.91 9.89272 14.3609 9.33479 14.3609 8.64328C14.3609 7.95438 14.9075 7.39384 15.5849 7.39384C16.2598 7.39384 16.809 7.95176 16.809 8.64328C16.809 9.33479 16.2598 9.89272 15.5849 9.89272Z" fill="#FCDACB"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_519_49">
|
||||
<rect width="18" height="20" fill="white" transform="translate(3 2)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 2.4 KiB |
|
|
@ -104,7 +104,37 @@ export default class Bigquery implements QueryService {
|
|||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw new QueryError('Query could not be completed', error.message, {});
|
||||
const errorMessage = error.message || "An unknown error occurred.";
|
||||
let errorDetails: any = {};
|
||||
|
||||
const errorSuggestions = {
|
||||
"notFound": "Check if the table or dataset exists in the specified location.",
|
||||
"accessDenied": "Verify that the service account has the necessary permissions.",
|
||||
"invalidQuery": "Check the SQL syntax and ensure that all referenced columns exist.",
|
||||
"rateLimitExceeded": "You are making too many requests. Try again after some time.",
|
||||
"backendError": "BigQuery encountered an internal error. Retry the request after some time.",
|
||||
"quotaExceeded": "You have exceeded your quota limits. Consider upgrading your plan or reducing query size.",
|
||||
"duplicate": "A resource with this name already exists. Try using a different name.",
|
||||
"badRequest": "Check the request parameters and ensure they are correctly formatted.",
|
||||
};
|
||||
|
||||
if (error && error instanceof Error) {
|
||||
const bigqueryError = error as any;
|
||||
errorDetails.error = bigqueryError;
|
||||
|
||||
const reason = bigqueryError.response?.status?.errorResult?.reason || "unknownError";
|
||||
errorDetails.reason = reason;
|
||||
errorDetails.message = errorMessage;
|
||||
errorDetails.jobId = bigqueryError.response?.jobReference?.jobId;
|
||||
errorDetails.location = bigqueryError.response?.jobReference?.location;
|
||||
errorDetails.query = bigqueryError.response?.configuration?.query?.query;
|
||||
|
||||
|
||||
const suggestion = errorSuggestions[reason];
|
||||
errorDetails.suggestion = suggestion;
|
||||
}
|
||||
|
||||
throw new QueryError('Query could not be completed', errorMessage, errorDetails);
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,86 +1,179 @@
|
|||
{
|
||||
"$schema": "https://raw.githubusercontent.com/ToolJet/ToolJet/develop/plugins/schemas/manifest.schema.json",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "Postgresql datasource",
|
||||
"description": "A schema defining postgresql datasource",
|
||||
"type": "database",
|
||||
"source": {
|
||||
"type": "object",
|
||||
"tj:version": "1.0.0",
|
||||
"tj:source": {
|
||||
"name": "PostgreSQL",
|
||||
"kind": "postgresql",
|
||||
"options": {
|
||||
"host": {
|
||||
"type": "string"
|
||||
},
|
||||
"port": {
|
||||
"type": "string"
|
||||
},
|
||||
"database": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"encrypted": true
|
||||
},
|
||||
"ca_cert": {
|
||||
"encrypted": true
|
||||
},
|
||||
"client_key": {
|
||||
"encrypted": true
|
||||
},
|
||||
"client_cert": {
|
||||
"encrypted": true
|
||||
},
|
||||
"root_cert": {
|
||||
"encrypted": true
|
||||
},
|
||||
"connection_options": {
|
||||
"type": "array"
|
||||
},
|
||||
"connection_string": {
|
||||
"type": "string",
|
||||
"encrypted": true
|
||||
}
|
||||
},
|
||||
"exposedVariables": {
|
||||
"isLoading": false,
|
||||
"data": {},
|
||||
"rawData": {}
|
||||
}
|
||||
},
|
||||
"defaults": {
|
||||
"connection_type": {
|
||||
"value": "manual"
|
||||
},
|
||||
"host": {
|
||||
"value": "localhost"
|
||||
},
|
||||
"port": {
|
||||
"value": 5432
|
||||
},
|
||||
"database": {
|
||||
"value": ""
|
||||
},
|
||||
"username": {
|
||||
"value": ""
|
||||
},
|
||||
"password": {
|
||||
"value": ""
|
||||
},
|
||||
"ssl_enabled": {
|
||||
"value": true
|
||||
},
|
||||
"ssl_certificate": {
|
||||
"value": "none"
|
||||
}
|
||||
"type": "database"
|
||||
},
|
||||
"properties": {
|
||||
"connection_type": {
|
||||
"label": "Connection type",
|
||||
"key": "connection_type",
|
||||
"type": "dropdown-component-flip",
|
||||
"type": "string",
|
||||
"title": "Connection type",
|
||||
"description": "Single select dropdown for connection_type",
|
||||
"enum": [
|
||||
"manual",
|
||||
"string"
|
||||
],
|
||||
"default": "manual"
|
||||
},
|
||||
"host": {
|
||||
"type": "string",
|
||||
"title": "Host",
|
||||
"description": "Enter host",
|
||||
"default": "localhost"
|
||||
},
|
||||
"port": {
|
||||
"type": "number",
|
||||
"title": "Port",
|
||||
"description": "Enter port",
|
||||
"default": 5432
|
||||
},
|
||||
"database": {
|
||||
"type": "string",
|
||||
"title": "Database name",
|
||||
"description": "Name of the database"
|
||||
},
|
||||
"username": {
|
||||
"type": "string",
|
||||
"title": "Username",
|
||||
"description": "Enter username"
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"title": "Password",
|
||||
"description": "Enter password"
|
||||
},
|
||||
"ssl_enabled": {
|
||||
"type": "boolean",
|
||||
"title": "SSL",
|
||||
"description": "Toggle for ssl_enabled",
|
||||
"default": true
|
||||
},
|
||||
"ssl_certificate": {
|
||||
"type": "string",
|
||||
"title": "SSL certificate",
|
||||
"description": "Single select dropdown for choosing certificates",
|
||||
"enum": [
|
||||
"ca_certificate",
|
||||
"self_signed",
|
||||
"none"
|
||||
],
|
||||
"default": "none"
|
||||
},
|
||||
"connection_string": {
|
||||
"type": "string",
|
||||
"title": "Connection string",
|
||||
"description": "postgres://username:password@hostname:port/database?sslmode=require"
|
||||
},
|
||||
"ca_cert": {
|
||||
"type": "string",
|
||||
"title": "CA Cert",
|
||||
"description": "Enter ca certificate"
|
||||
},
|
||||
"client_key": {
|
||||
"type": "string",
|
||||
"title": "Client Key",
|
||||
"description": "Enter client key"
|
||||
},
|
||||
"client_cert": {
|
||||
"type": "string",
|
||||
"title": "Client Cert",
|
||||
"description": "Enter client certificate"
|
||||
},
|
||||
"root_cert": {
|
||||
"type": "string",
|
||||
"title": "Root Cert",
|
||||
"description": "Enter root certificate"
|
||||
}
|
||||
},
|
||||
"tj:encrypted": [
|
||||
"password",
|
||||
"ca_cert",
|
||||
"client_key",
|
||||
"client_cert",
|
||||
"root_cert",
|
||||
"connection_string"
|
||||
],
|
||||
"required": [
|
||||
"connection_type"
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"connection_type": {
|
||||
"const": "manual"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"host",
|
||||
"port",
|
||||
"username",
|
||||
"password",
|
||||
"ssl_certificate"
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"ssl_certificate": {
|
||||
"const": "ca_certificate"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"ca_cert"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"ssl_certificate": {
|
||||
"const": "self_signed"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"client_key",
|
||||
"client_cert",
|
||||
"root_cert"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"connection_type": {
|
||||
"const": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"connection_string"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"tj:ui:properties": {
|
||||
"connection_type": {
|
||||
"$ref": "#/properties/connection_type",
|
||||
"key": "connection_type",
|
||||
"label": "Connection type",
|
||||
"description": "Single select dropdown for connection_type",
|
||||
"widget": "dropdown-component-flip",
|
||||
"list": [
|
||||
{
|
||||
"name": "Manual connection",
|
||||
|
|
@ -94,10 +187,11 @@
|
|||
},
|
||||
"manual": {
|
||||
"ssl_certificate": {
|
||||
"label": "SSL certificate",
|
||||
"$ref": "#/properties/ssl_certificate",
|
||||
"key": "ssl_certificate",
|
||||
"type": "dropdown-component-flip",
|
||||
"label": "SSL certificate",
|
||||
"description": "Single select dropdown for choosing certificates",
|
||||
"widget": "dropdown-component-flip",
|
||||
"list": [
|
||||
{
|
||||
"value": "ca_certificate",
|
||||
|
|
@ -114,97 +208,104 @@
|
|||
],
|
||||
"commonFields": {
|
||||
"host": {
|
||||
"label": "Host",
|
||||
"$ref": "#/properties/host",
|
||||
"key": "host",
|
||||
"type": "text",
|
||||
"description": "Enter host"
|
||||
"label": "Host",
|
||||
"description": "Enter host",
|
||||
"widget": "text-v3",
|
||||
"required": true
|
||||
},
|
||||
"port": {
|
||||
"label": "Port",
|
||||
"$ref": "#/properties/port",
|
||||
"key": "port",
|
||||
"type": "text",
|
||||
"description": "Enter port"
|
||||
"label": "Port",
|
||||
"description": "Enter port",
|
||||
"widget": "text-v3",
|
||||
"required": true
|
||||
},
|
||||
"ssl_enabled": {
|
||||
"label": "SSL",
|
||||
"$ref": "#/properties/ssl_enabled",
|
||||
"key": "ssl_enabled",
|
||||
"type": "toggle",
|
||||
"description": "Toggle for ssl_enabled"
|
||||
"label": "SSL",
|
||||
"description": "Toggle for ssl_enabled",
|
||||
"widget": "toggle"
|
||||
},
|
||||
"database": {
|
||||
"label": "Database name",
|
||||
"$ref": "#/properties/database",
|
||||
"key": "database",
|
||||
"type": "text",
|
||||
"description": "Name of the database"
|
||||
"label": "Database name",
|
||||
"description": "Name of the database",
|
||||
"widget": "text-v3"
|
||||
},
|
||||
"username": {
|
||||
"label": "Username",
|
||||
"$ref": "#/properties/username",
|
||||
"key": "username",
|
||||
"type": "text",
|
||||
"description": "Enter username"
|
||||
"label": "Username",
|
||||
"description": "Enter username",
|
||||
"widget": "text-v3",
|
||||
"required": true
|
||||
},
|
||||
"password": {
|
||||
"label": "Password",
|
||||
"$ref": "#/properties/password",
|
||||
"key": "password",
|
||||
"type": "password",
|
||||
"description": "Enter password"
|
||||
"label": "Password",
|
||||
"description": "Enter password",
|
||||
"widget": "password-v3",
|
||||
"required": true
|
||||
},
|
||||
"connection_options": {
|
||||
"label": "Connection options",
|
||||
"$ref": "#/properties/connection_options",
|
||||
"key": "connection_options",
|
||||
"type": "react-component-headers",
|
||||
"width":"316px"
|
||||
"label": "Connection options",
|
||||
"widget": "react-component-headers",
|
||||
"width": "316px",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"ca_certificate": {
|
||||
"ca_cert": {
|
||||
"label": "CA Cert",
|
||||
"$ref": "#/properties/ca_cert",
|
||||
"key": "ca_cert",
|
||||
"type": "textarea",
|
||||
"encrypted": true,
|
||||
"description": "Enter ca certificate"
|
||||
"label": "CA Cert",
|
||||
"description": "Enter ca certificate",
|
||||
"widget": "textarea"
|
||||
}
|
||||
},
|
||||
"self_signed": {
|
||||
"client_key": {
|
||||
"label": "Client Key",
|
||||
"$ref": "#/properties/client_key",
|
||||
"key": "client_key",
|
||||
"type": "textarea",
|
||||
"encrypted": true,
|
||||
"description": "Enter client key"
|
||||
"label": "Client Key",
|
||||
"description": "Enter client key",
|
||||
"widget": "textarea"
|
||||
},
|
||||
"client_cert": {
|
||||
"label": "Client Cert",
|
||||
"$ref": "#/properties/client_cert",
|
||||
"key": "client_cert",
|
||||
"type": "textarea",
|
||||
"encrypted": true,
|
||||
"description": "Enter client certificate"
|
||||
"label": "Client Cert",
|
||||
"description": "Enter client certificate",
|
||||
"widget": "textarea"
|
||||
},
|
||||
"root_cert": {
|
||||
"label": "Root Cert",
|
||||
"$ref": "#/properties/root_cert",
|
||||
"key": "root_cert",
|
||||
"type": "textarea",
|
||||
"encrypted": true,
|
||||
"description": "Enter root certificate"
|
||||
"label": "Root Cert",
|
||||
"description": "Enter root certificate",
|
||||
"widget": "textarea",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"string": {
|
||||
"connection_string": {
|
||||
"label": "Connection string",
|
||||
"$ref": "#/properties/connection_string",
|
||||
"key": "connection_string",
|
||||
"type": "text",
|
||||
"encrypted": true,
|
||||
"description": "postgres://username:password@hostname:port/database?sslmode=require"
|
||||
"label": "Connection string",
|
||||
"description": "postgres://username:password@hostname:port/database?sslmode=require",
|
||||
"widget": "text",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"host",
|
||||
"port",
|
||||
"username",
|
||||
"database",
|
||||
"password"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +1 @@
|
|||
3.7.0
|
||||
3.8.0
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 0eefbb71a1d5288f49641af5efaaab25970f27d1
|
||||
Subproject commit 683647f83d3efeeadbe69c40b8e8dd5ba4e8ea06
|
||||
|
|
@ -193,5 +193,13 @@
|
|||
"author": "Tooljet",
|
||||
"timestamp": "Tue, 21 Jan 2025 16:55:28 GMT",
|
||||
"tags": ["AI"]
|
||||
},
|
||||
{
|
||||
"name": "azurerepos",
|
||||
"description": "api plugin from azurerepos",
|
||||
"version": "1.0.0",
|
||||
"id": "azurerepos",
|
||||
"author": "Tooljet",
|
||||
"timestamp": "Mon, 23 Dec 2024 11:57:30 GMT"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -15,10 +15,20 @@ export class FeatureAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects>
|
|||
}
|
||||
|
||||
protected defineAbilityFor(can: AbilityBuilder<FeatureAbility>['can'], UserAllPermissions: UserAllPermissions): void {
|
||||
const { superAdmin, isAdmin } = UserAllPermissions;
|
||||
if (superAdmin || isAdmin) {
|
||||
// Admin or super admin and do all operations
|
||||
can([FEATURE_KEY.INSTALL, FEATURE_KEY.UPDATE, FEATURE_KEY.DELETE], Plugin);
|
||||
const { superAdmin, isAdmin, isBuilder } = UserAllPermissions;
|
||||
if (superAdmin || isAdmin || isBuilder) {
|
||||
// Admin, super admin and Builder can do all operations
|
||||
can(
|
||||
[
|
||||
FEATURE_KEY.INSTALL,
|
||||
FEATURE_KEY.UPDATE,
|
||||
FEATURE_KEY.DELETE,
|
||||
FEATURE_KEY.INSTALL_DEPENDENT_PLUGINS,
|
||||
FEATURE_KEY.UNINSTALL_PLUGINS,
|
||||
FEATURE_KEY.DEPENDENT_PLUGINS,
|
||||
],
|
||||
Plugin
|
||||
);
|
||||
}
|
||||
// These two operations are available to all
|
||||
can([FEATURE_KEY.GET_ONE, FEATURE_KEY.RELOAD, FEATURE_KEY.GET], Plugin);
|
||||
|
|
|
|||
|
|
@ -10,5 +10,8 @@ export const FEATURES: FeaturesConfig = {
|
|||
[FEATURE_KEY.INSTALL]: {},
|
||||
[FEATURE_KEY.RELOAD]: {},
|
||||
[FEATURE_KEY.UPDATE]: {},
|
||||
[FEATURE_KEY.DEPENDENT_PLUGINS]: {},
|
||||
[FEATURE_KEY.INSTALL_DEPENDENT_PLUGINS]: {},
|
||||
[FEATURE_KEY.UNINSTALL_PLUGINS]: {},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,4 +5,7 @@ export enum FEATURE_KEY {
|
|||
GET = 'get',
|
||||
GET_ONE = 'get_one',
|
||||
RELOAD = 'reload',
|
||||
DEPENDENT_PLUGINS = 'dependent_plugins',
|
||||
INSTALL_DEPENDENT_PLUGINS = 'install_dependent_plugins',
|
||||
UNINSTALL_PLUGINS = 'uninstall_plugins',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,8 +70,24 @@ export class PluginsController implements IPluginsController {
|
|||
return this.pluginsService.reload(id);
|
||||
}
|
||||
|
||||
@Post('/findDepedentPlugins')
|
||||
@Post('findDependentPlugins')
|
||||
@InitFeature(FEATURE_KEY.DEPENDENT_PLUGINS)
|
||||
async findDependentPluginsToBeInstalledFromDataSources(@Body() dataSources) {
|
||||
return this.pluginsService.checkIfPluginsToBeInstalled(dataSources);
|
||||
}
|
||||
|
||||
@Post('installDependentPlugins')
|
||||
@InitFeature(FEATURE_KEY.INSTALL_DEPENDENT_PLUGINS)
|
||||
async installDependentPlugins(
|
||||
@Body('dependentPlugins') dependentPlugins,
|
||||
@Body('shouldAutoImportPlugin') shouldAutoImportPlugin
|
||||
) {
|
||||
return this.pluginsService.autoInstallPluginsForTemplates(dependentPlugins, shouldAutoImportPlugin);
|
||||
}
|
||||
|
||||
@Post('uninstallPlugins')
|
||||
@InitFeature(FEATURE_KEY.UNINSTALL_PLUGINS)
|
||||
async uninstallPlugins(@Body('pluginsId') pluginsId) {
|
||||
return this.pluginsService.uninstallPlugins(pluginsId);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ export class PluginsService implements IPluginsService {
|
|||
return Array.from(marketplacePluginsUsed);
|
||||
}
|
||||
|
||||
private async arePluginsInstalled(pluginsId: Array<string>): Promise<{ pluginsToBeInstalled: Array<string> }> {
|
||||
private async findPluginsToBeInstalled(pluginsId: Array<string>): Promise<{ pluginsToBeInstalled: Array<string> }> {
|
||||
const pluginsToBeInstalled = [];
|
||||
if (!pluginsId.length) return { pluginsToBeInstalled };
|
||||
|
||||
|
|
@ -154,30 +154,62 @@ export class PluginsService implements IPluginsService {
|
|||
async checkIfPluginsToBeInstalled(
|
||||
dataSources
|
||||
): Promise<{ pluginsToBeInstalled: Array<string>; pluginsListIdToDetailsMap: any }> {
|
||||
const { pluginsListIdToDetailsMap } = this.listMarketplacePlugins();
|
||||
const marketplacePluginsUsed = this.filterMarketplacePluginsFromDatasources(dataSources, pluginsListIdToDetailsMap);
|
||||
const { pluginsToBeInstalled } = await this.arePluginsInstalled(marketplacePluginsUsed);
|
||||
return { pluginsToBeInstalled, pluginsListIdToDetailsMap };
|
||||
}
|
||||
|
||||
async autoInstallPluginsForTemplates(pluginsToBeInstalled: Array<string>, shouldAutoInstall: boolean) {
|
||||
const { pluginsListIdToDetailsMap } = this.listMarketplacePlugins();
|
||||
if (shouldAutoInstall && pluginsToBeInstalled.length) {
|
||||
const installedPluginsName = [];
|
||||
for (const pluginId of pluginsToBeInstalled) {
|
||||
const pluginDetails = pluginsListIdToDetailsMap[pluginId];
|
||||
const installedPlugin = await this.install(pluginDetails);
|
||||
installedPluginsName.push(installedPlugin.name);
|
||||
}
|
||||
return installedPluginsName;
|
||||
}
|
||||
|
||||
if (!shouldAutoInstall && pluginsToBeInstalled.length) {
|
||||
throw new NotFoundException(
|
||||
`Plugins ( ${pluginsToBeInstalled
|
||||
.map((pluginToBeInstalled) => pluginsListIdToDetailsMap[pluginToBeInstalled].name || pluginToBeInstalled)
|
||||
.join(', ')} ) is not installed yet!`
|
||||
try {
|
||||
const { pluginsListIdToDetailsMap } = this.listMarketplacePlugins();
|
||||
const marketplacePluginsUsed = this.filterMarketplacePluginsFromDatasources(
|
||||
dataSources,
|
||||
pluginsListIdToDetailsMap
|
||||
);
|
||||
const { pluginsToBeInstalled } = await this.findPluginsToBeInstalled(marketplacePluginsUsed);
|
||||
return { pluginsToBeInstalled, pluginsListIdToDetailsMap };
|
||||
} catch (error) {
|
||||
throw new InternalServerErrorException(
|
||||
error,
|
||||
'An error occurred while checking whether plugins need to be installed.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async autoInstallPluginsForTemplates(pluginsToBeInstalled: Array<string>, shouldAutoInstall: boolean) {
|
||||
const installedPluginsList = [];
|
||||
const installedPluginsInfo = [];
|
||||
try {
|
||||
const { pluginsListIdToDetailsMap } = this.listMarketplacePlugins();
|
||||
if (shouldAutoInstall && pluginsToBeInstalled.length) {
|
||||
for (const pluginId of pluginsToBeInstalled) {
|
||||
const pluginDetails = pluginsListIdToDetailsMap[pluginId];
|
||||
const installedPluginInfo = await this.install(pluginDetails);
|
||||
installedPluginsList.push(installedPluginInfo.name);
|
||||
installedPluginsInfo.push(installedPluginInfo);
|
||||
}
|
||||
return { installedPluginsList, installedPluginsInfo };
|
||||
}
|
||||
|
||||
if (!shouldAutoInstall && pluginsToBeInstalled.length) {
|
||||
throw new NotFoundException(
|
||||
`Plugins ( ${pluginsToBeInstalled
|
||||
.map((pluginToBeInstalled) => pluginsListIdToDetailsMap[pluginToBeInstalled].name || pluginToBeInstalled)
|
||||
.join(', ')} ) is not installed yet!`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (installedPluginsInfo.length) {
|
||||
const pluginsId = installedPluginsInfo.map((pluginInfo) => pluginInfo.id);
|
||||
await this.uninstallPlugins(pluginsId);
|
||||
}
|
||||
throw new InternalServerErrorException(error, 'Error while installing marketplace plugins');
|
||||
}
|
||||
}
|
||||
|
||||
async uninstallPlugins(pluginsId: Array<string>) {
|
||||
try {
|
||||
if (!pluginsId.length) return;
|
||||
for (const pluginId of pluginsId) {
|
||||
await this.remove(pluginId);
|
||||
}
|
||||
return;
|
||||
} catch (error) {
|
||||
throw new InternalServerErrorException(error, 'Error while uninstalling marketplace plugins');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ interface Features {
|
|||
[FEATURE_KEY.INSTALL]: FeatureConfig;
|
||||
[FEATURE_KEY.RELOAD]: FeatureConfig;
|
||||
[FEATURE_KEY.UPDATE]: FeatureConfig;
|
||||
[FEATURE_KEY.DEPENDENT_PLUGINS]: FeatureConfig;
|
||||
[FEATURE_KEY.INSTALL_DEPENDENT_PLUGINS]: FeatureConfig;
|
||||
[FEATURE_KEY.UNINSTALL_PLUGINS]: FeatureConfig;
|
||||
}
|
||||
|
||||
export interface FeaturesConfig {
|
||||
|
|
|
|||
|
|
@ -22,14 +22,14 @@ export class TemplateAppsController {
|
|||
@User() user,
|
||||
@Body('identifier') identifier,
|
||||
@Body('appName') appName,
|
||||
@Body('dependentPluginsForTemplate') dependentPluginsForTemplate,
|
||||
@Body('dependentPlugins') dependentPlugins,
|
||||
@Body('shouldAutoImportPlugin') shouldAutoImportPlugin
|
||||
) {
|
||||
const newApp = await this.templatesService.perform(
|
||||
user,
|
||||
identifier,
|
||||
appName,
|
||||
dependentPluginsForTemplate,
|
||||
dependentPlugins,
|
||||
shouldAutoImportPlugin
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -27,12 +27,12 @@ export class TemplatesService {
|
|||
currentUser: User,
|
||||
identifier: string,
|
||||
appName: string,
|
||||
dependentPluginsForTemplate: Array<string>,
|
||||
dependentPlugins: Array<string>,
|
||||
shouldAutoImportPlugin: boolean
|
||||
) {
|
||||
const templateDefinition = this.findTemplateDefinition(identifier);
|
||||
if (dependentPluginsForTemplate.length)
|
||||
await this.pluginsService.autoInstallPluginsForTemplates(dependentPluginsForTemplate, shouldAutoImportPlugin);
|
||||
if (dependentPlugins.length)
|
||||
await this.pluginsService.autoInstallPluginsForTemplates(dependentPlugins, shouldAutoImportPlugin);
|
||||
return this.importTemplate(currentUser, templateDefinition, appName, identifier);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {
|
|||
IsIn,
|
||||
} from 'class-validator';
|
||||
import { sanitizeInput, formatTimestamp, validateDefaultValue, formatJSONB } from 'src/helpers/utils.helper';
|
||||
import { TooljetDatabaseDataTypes, TJDB } from '../types';
|
||||
|
||||
export function Match(property: string, validationOptions?: ValidationOptions) {
|
||||
return (object: any, propertyName: string) => {
|
||||
|
|
@ -189,11 +190,11 @@ export class PostgrestTableColumnDto {
|
|||
@Validate(SQLInjectionValidator, { message: 'Column name does not support special characters' })
|
||||
column_name: string;
|
||||
|
||||
@IsString()
|
||||
@IsIn(Object.values(TJDB), { message: 'Incorrect datatype.' })
|
||||
@IsNotEmpty()
|
||||
@Transform(({ value }) => sanitizeInput(value))
|
||||
@Validate(SQLInjectionValidator)
|
||||
data_type: string;
|
||||
data_type: TooljetDatabaseDataTypes;
|
||||
|
||||
@IsOptional()
|
||||
@Transform(({ value, obj }) => {
|
||||
|
|
@ -290,11 +291,11 @@ export class EditColumnTableDto {
|
|||
@Validate(SQLInjectionValidator, { message: 'Column name does not support special characters' })
|
||||
column_name: string;
|
||||
|
||||
@IsString()
|
||||
@IsIn(Object.values(TJDB), { message: 'Incorrect datatype.' })
|
||||
@IsNotEmpty()
|
||||
@Transform(({ value }) => sanitizeInput(value))
|
||||
@Validate(SQLInjectionValidator)
|
||||
data_type: string;
|
||||
data_type: TooljetDatabaseDataTypes;
|
||||
|
||||
@IsOptional()
|
||||
@Transform(({ value, obj }) => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { QueryFailedError } from 'typeorm';
|
||||
import { InternalTable } from 'src/entities/internal_table.entity';
|
||||
import { capitalize } from 'lodash';
|
||||
|
||||
export const TJDB = {
|
||||
character_varying: 'character varying' as const,
|
||||
|
|
@ -150,10 +149,11 @@ export class TooljetDatabaseError extends QueryFailedError {
|
|||
}
|
||||
|
||||
toString(): string {
|
||||
const capitalizeSentence = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
|
||||
const errorMessage =
|
||||
errorCodeMapping[this.code]?.[this.context.origin] ||
|
||||
errorCodeMapping[this.code]?.['default'] ||
|
||||
capitalize(this.message);
|
||||
capitalizeSentence(this.message);
|
||||
return this.replaceErrorPlaceholders(errorMessage);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue