ToolJet/frontend/src/_components/DynamicForm.jsx
Arpit 10668546d9
Improvement - adds ability to resolve workspace variables in GDS connection forms (#6824)
* adds ability to resolveworkspace variables in GDS connection forms

* clean up

* resolves workpace variables in GDS connection form UI (client-side)

* fixes: encrypted source options resolver helper

* add workspace vars support for textarea components used in gds

* updates warning banner

* update textarea with the new UI

* Revert "Merge branch 'develop' into improvements/add-support-workspace-vars-datasource-forms"

This reverts commit f3b42005aa, reversing
changes made to 68334ca281.

* Revert "Revert "Merge branch 'develop' into improvements/add-support-workspace-vars-datasource-forms""

This reverts commit 9b19e5b530.

* fixes crash when trting to resolve variables

* remove comments
2023-07-04 15:32:45 +05:30

454 lines
15 KiB
JavaScript

import React from 'react';
import cx from 'classnames';
import Input from '@/_ui/Input';
import Textarea from '@/_ui/Textarea';
import Select from '@/_ui/Select';
import Headers from '@/_ui/HttpHeaders';
import OAuth from '@/_ui/OAuth';
import Toggle from '@/_ui/Toggle';
import OpenApi from '@/_ui/OpenAPI';
import { Checkbox, CheckboxGroup } from '@/_ui/CheckBox';
import { CodeHinter } from '@/Editor/CodeBuilder/CodeHinter';
import GoogleSheets from '@/_components/Googlesheets';
import Slack from '@/_components/Slack';
import Zendesk from '@/_components/Zendesk';
import ToolJetDbOperations from '@/Editor/QueryManager/QueryEditors/TooljetDatabase/ToolJetDbOperations';
import { orgEnvironmentVariableService } from '../_services';
import { find, isEmpty } from 'lodash';
import { ButtonSolid } from './AppButton';
const DynamicForm = ({
schema,
isGDS,
optionchanged,
createDataSource,
options,
isSaving,
selectedDataSource,
currentState,
isEditMode,
optionsChanged,
queryName,
computeSelectStyles = false,
}) => {
const [computedProps, setComputedProps] = React.useState({});
const [workspaceVariables, setWorkspaceVariables] = React.useState([]);
// if(schema.properties) todo add empty check
React.useLayoutEffect(() => {
if (!isEditMode || isEmpty(options)) {
optionsChanged(schema?.defaults ?? {});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
React.useEffect(() => {
if (isGDS) {
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([]);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
React.useEffect(() => {
const { properties } = schema;
if (!isEmpty(properties)) {
let fields = {};
let encrpytedFieldsProps = {};
const flipComponentDropdown = find(properties, ['type', 'dropdown-component-flip']);
if (flipComponentDropdown) {
const selector = options?.[flipComponentDropdown?.key]?.value;
fields = { ...flipComponentDropdown?.commonFields, ...properties[selector] };
} else {
fields = { ...properties };
}
Object.keys(fields).length > 0 &&
Object.keys(fields).map((key) => {
const { type, encrypted } = fields[key];
if ((type === 'password' || encrypted) && !(key in computedProps)) {
//Editable encrypted fields only if datasource doesn't exists
encrpytedFieldsProps[key] = {
disabled: !!selectedDataSource?.id,
};
}
});
setComputedProps({ ...computedProps, ...encrpytedFieldsProps });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [options]);
const getElement = (type) => {
switch (type) {
case 'password':
case 'text':
return Input;
case 'textarea':
return Textarea;
case 'dropdown':
return Select;
case 'toggle':
return Toggle;
case 'checkbox':
return Checkbox;
case 'checkbox-group':
return CheckboxGroup;
case 'tooljetdb-operations':
return ToolJetDbOperations;
case 'react-component-headers':
return Headers;
case 'react-component-oauth-authentication':
return OAuth;
case 'react-component-google-sheets':
return GoogleSheets;
case 'react-component-slack':
return Slack;
case 'codehinter':
return CodeHinter;
case 'react-component-openapi-validator':
return OpenApi;
case 'react-component-zendesk':
return Zendesk;
default:
return <div>Type is invalid</div>;
}
};
const handleToggle = (controller) => {
if (controller) {
return !options?.[controller]?.value ? ' d-none' : '';
} else {
return '';
}
};
const getElementProps = ({
key,
list,
rows = 5,
helpText,
description,
type,
placeholder = '',
mode = 'sql',
lineNumbers = true,
initialValue,
height = 'auto',
width,
ignoreBraces = false,
className,
controller,
}) => {
const source = schema?.source?.kind;
const darkMode = localStorage.getItem('darkMode') === 'true';
if (!options) return;
switch (type) {
case 'password':
case 'text':
case 'textarea':
return {
type,
placeholder: options?.[key]?.encrypted ? '**************' : description,
className: `form-control${handleToggle(controller)}`,
value: options?.[key]?.value,
...(type === 'textarea' && { rows: rows }),
...(helpText && { helpText }),
onChange: (e) => optionchanged(key, e.target.value),
isGDS,
workspaceVariables,
};
case 'toggle':
return {
defaultChecked: options?.[key],
checked: options?.[key]?.value,
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%',
useMenuPortal: queryName ? true : false,
styles: computeSelectStyles ? computeSelectStyles('100%') : {},
useCustomStyles: computeSelectStyles ? true : false,
};
case 'checkbox-group':
return {
options: list,
values: options?.[key] ?? [],
onChange: (value) => {
optionchanged(key, [...value]);
},
};
case 'react-component-headers': {
const isRenderedAsQueryEditor = currentState != null;
return {
getter: key,
options: isRenderedAsQueryEditor
? options?.[key] ?? schema?.defaults?.[key]
: options?.[key]?.value ?? schema?.defaults?.[key]?.value,
optionchanged,
currentState,
isRenderedAsQueryEditor,
};
}
case 'react-component-oauth-authentication':
return {
isGrpc: source === 'grpc',
grant_type: options?.grant_type?.value,
auth_type: options?.auth_type?.value,
add_token_to: options?.add_token_to?.value,
header_prefix: options?.header_prefix?.value,
access_token_url: options?.access_token_url?.value,
access_token_custom_headers: options?.access_token_custom_headers?.value,
client_id: options?.client_id?.value,
client_secret: options?.client_secret?.value,
client_auth: options?.client_auth?.value,
scopes: options?.scopes?.value,
username: options?.username?.value,
password: options?.password?.value,
grpc_apiKey_key: options?.grpc_apikey_key?.value,
grpc_apiKey_value: options?.grpc_apikey_value?.value,
bearer_token: options?.bearer_token?.value,
auth_url: options?.auth_url?.value,
auth_key: options?.auth_key?.value,
custom_auth_params: options?.custom_auth_params?.value,
custom_query_params: options?.custom_query_params?.value,
multiple_auth_enabled: options?.multiple_auth_enabled?.value,
optionchanged,
};
case 'react-component-google-sheets':
case 'react-component-slack':
case 'react-component-zendesk':
return {
optionchanged,
createDataSource,
options,
isSaving,
selectedDataSource,
};
case 'tooljetdb-operations':
return {
currentState,
optionchanged,
createDataSource,
options,
isSaving,
selectedDataSource,
darkMode,
};
case 'codehinter':
return {
currentState,
initialValue: options[key]
? typeof options[key] === 'string'
? options[key]
: JSON.stringify(options[key])
: initialValue,
mode,
lineNumbers,
className: className ? className : lineNumbers ? 'query-hinter' : 'codehinter-query-editor-input',
onChange: (value) => optionchanged(key, value),
theme: darkMode ? 'monokai' : lineNumbers ? 'duotone-light' : 'default',
placeholder,
height,
width,
componentName: queryName ? `${queryName}::${key ?? ''}` : null,
ignoreBraces,
cyLabel: key ? `${String(key).toLocaleLowerCase().replace(/\s+/g, '-')}` : '',
};
case 'react-component-openapi-validator':
return {
format: options.format?.value,
definition: options.definition?.value,
auth_type: options.auth_type?.value,
auth_key: options.auth_key?.value,
username: options.username?.value,
password: options.password?.value,
bearer_token: options.bearer_token?.value,
api_keys: options.api_keys?.value,
optionchanged,
grant_type: options.grant_type?.value,
add_token_to: options.add_token_to?.value,
header_prefix: options.header_prefix?.value,
access_token_url: options.access_token_url?.value,
client_id: options.client_id?.value,
client_secret: options.client_secret?.value,
client_auth: options.client_auth?.value,
scopes: options.scopes?.value,
auth_url: options.auth_url?.value,
custom_auth_params: options.custom_auth_params?.value,
custom_query_params: options.custom_query_params?.value,
spec: options.spec?.value,
};
default:
return {};
}
};
const getLayout = (obj) => {
if (isEmpty(obj)) return null;
const flipComponentDropdown = isFlipComponentDropdown(obj);
if (flipComponentDropdown) {
return flipComponentDropdown;
}
const handleEncryptedFieldsToggle = (event, field) => {
const isEditing = computedProps[field]['disabled'];
setComputedProps({
...computedProps,
[field]: {
...computedProps[field],
disabled: !isEditing,
},
});
if (isEditing) {
optionchanged(field, '');
} else {
//Send old field value if editing mode disabled for encrypted fields
const newOptions = { ...options };
const oldFieldValue = selectedDataSource?.['options']?.[field];
optionsChanged({ ...newOptions, [field]: oldFieldValue });
}
};
return (
<div className="row">
{Object.keys(obj).map((key) => {
const { label, type, encrypted, className } = obj[key];
const Element = getElement(type);
return (
<div className={cx('my-2', { 'col-md-12': !className, [className]: !!className })} key={key}>
<div className="d-flex align-items-center">
{label && (
<label
className="form-label"
data-cy={`label-${String(label).toLocaleLowerCase().replace(/\s+/g, '-')}`}
>
{label}
</label>
)}
{(type === 'password' || encrypted) && selectedDataSource?.id && (
<div className="mx-1 col">
<ButtonSolid
className="datasource-edit-btn mb-2"
type="a"
variant="tertiary"
target="_blank"
rel="noreferrer"
onClick={(event) => handleEncryptedFieldsToggle(event, key)}
>
{computedProps?.[key]?.['disabled'] ? 'Edit' : 'Cancel'}
</ButtonSolid>
</div>
)}
{(type === 'password' || encrypted) && (
<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>
<Element
{...getElementProps(obj[key])}
{...computedProps[key]}
data-cy={`${String(label).toLocaleLowerCase().replace(/\s+/g, '-')}-text-field`}
customWrap={true} //to be removed after whole ui is same
/>
</div>
);
})}
</div>
);
};
const isFlipComponentDropdown = (obj) => {
const flipComponentDropdown = find(obj, ['type', 'dropdown-component-flip']);
if (flipComponentDropdown) {
// options[key].value for datasource
// options[key] for dataquery
const selector = options?.[flipComponentDropdown?.key]?.value || options?.[flipComponentDropdown?.key];
return (
<>
<div className="row">
{flipComponentDropdown.commonFields && getLayout(flipComponentDropdown.commonFields)}
<div
className={cx('my-2', {
'col-md-12': !flipComponentDropdown.className,
[flipComponentDropdown.className]: !!flipComponentDropdown.className,
})}
>
{flipComponentDropdown.label && (
<label
className="form-label"
data-cy={`${String(flipComponentDropdown.label)
.toLocaleLowerCase()
.replace(/\s+/g, '-')}-dropdown-label`}
>
{flipComponentDropdown.label}
</label>
)}
<div data-cy={'query-select-dropdown'}>
<Select
{...getElementProps(flipComponentDropdown)}
styles={computeSelectStyles ? computeSelectStyles('100%') : {}}
useCustomStyles={computeSelectStyles ? true : false}
/>
</div>
{flipComponentDropdown.helpText && (
<span className="flip-dropdown-help-text">{flipComponentDropdown.helpText}</span>
)}
</div>
</div>
{getLayout(obj[selector])}
</>
);
}
};
const flipComponentDropdown = isFlipComponentDropdown(schema.properties);
if (flipComponentDropdown) {
return flipComponentDropdown;
}
return getLayout(schema.properties);
};
export default DynamicForm;