Release Platform v17 (v2.39.0) (#9502)

* bump version

* Sample data source (#9501)

* Added sample data populating script

* added expand-collapse  in add data soure menu

* Sample database

* Design changes

* Added CTA to buttons and added design changes

* Added code sanity fix for some services

* changed configration for create sampke db and code sanity fix

* Removed logs

* Added xlsx in dependency

* added migration for sample db

* Added loggin for testing

* Added await in migration

* Replace excel sheet with JSON files

* reverted package-lock file

* Fixed issues

* dependecy deletion

* Added schedular

* Added changes for bug fixes and typeorm query for creating sample db

* Removed color.scss file import

* Add logo in sample application

* add documentation link for sample db

* fixed migration issue for data queries creation

* removed sample db intergration

* bump version

* Remove .env file and code sanity

* deleted migration file

---------

Co-authored-by: Kritagya <kriks.iitk@.com>
Co-authored-by: Kritagya Kumar <kritagyakumar@192.168.1.6>
Co-authored-by: kriks7raptor <kritagya@raptorx.ai>
Co-authored-by: gsmithun4 <gsmithun4@gmail.com>

* Add data-cy for drag drop empty canvas card (#9513)

* Add sample db condition on all components (#9516)

* Add sample db condition on all components

* Changed empty state for container

* Condiiton on sample data source

---------

Co-authored-by: kriks7raptor <kritagya@raptorx.ai>

* Release fix: subpath (#9535)

* Add sample db condition on all components

* Changed empty state for container

* Condiiton on sample data source

* fixed subpath issue for workspace setting and folders

* Folder change handler in subpath

---------

Co-authored-by: kriks7raptor <kritagya@raptorx.ai>

* fixed version

* fixed version

* fixed version

* update server version

* Bump version to v2.39.0

---------

Co-authored-by: kriks7iitk <34170719+kriks7iitk@users.noreply.github.com>
Co-authored-by: Kritagya <kriks.iitk@.com>
Co-authored-by: Kritagya Kumar <kritagyakumar@192.168.1.6>
Co-authored-by: kriks7raptor <kritagya@raptorx.ai>
Co-authored-by: Ajith KV <ajith.jaban@gmail.com>
Co-authored-by: kriks7iitk <kriks.iitk@gmail.com>
Co-authored-by: Adish M <44204658+adishM98@users.noreply.github.com>
Co-authored-by: Muhsin Shah <muhsinshah21@gmail.com>
This commit is contained in:
Midhun G S 2024-04-30 21:49:37 +05:30 committed by GitHub
parent 4fd5666d09
commit eef2a49fa9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
61 changed files with 2671 additions and 181 deletions

View file

@ -1 +1 @@
2.38.0
2.39.0

View file

@ -1 +1 @@
2.38.0
2.39.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 789 KiB

View file

@ -4,7 +4,7 @@
"paths": {
"@/*": [
"./*"
]
]
}
}
}

View file

@ -6,7 +6,7 @@ import { ItemTypes } from './ItemTypes';
import { DraggableBox } from './DraggableBox';
import update from 'immutability-helper';
import { componentTypes } from './WidgetManager/components';
import { resolveReferences } from '@/_helpers/utils';
import { resolveReferences, getWorkspaceId } from '@/_helpers/utils';
import Comments from './Comments';
import { commentsService } from '@/_services';
import config from 'config';
@ -19,11 +19,18 @@ import { useAppVersionStore } from '@/_stores/appVersionStore';
import { useEditorStore } from '@/_stores/editorStore';
import { useAppInfo } from '@/_stores/appDataStore';
import { shallow } from 'zustand/shallow';
import { useDataQueriesActions } from '@/_stores/dataQueriesStore';
import { useQueryPanelActions } from '@/_stores/queryPanelStore';
import { useSampleDataSource } from '@/_stores/dataSourcesStore';
import _ from 'lodash';
// eslint-disable-next-line import/no-unresolved
import { diff } from 'deep-object-diff';
import './editor.theme.scss';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import BulkIcon from '@/_ui/Icon/BulkIcons';
import { isPDFSupported } from '@/_stores/utils';
import toast from 'react-hot-toast';
import { getSubpath } from '@/_helpers/routes';
const NO_OF_GRIDS = 43;
@ -51,6 +58,9 @@ export const Container = ({
}) => {
// Dont update first time to skip
// redundant save on app definition load
const { createDataQuery } = useDataQueriesActions();
const { setPreviewData } = useQueryPanelActions();
const sampleDataSource = useSampleDataSource();
const firstUpdate = useRef(true);
const { showComments, currentLayout } = useEditorStore(
@ -564,6 +574,22 @@ export const Container = ({
[setIsResizing]
);
const openAddUserWorkspaceSetting = () => {
const workspaceId = getWorkspaceId();
const subPath = getSubpath();
const path = subPath
? `${subPath}/${workspaceId}/workspace-settings?adduser=true`
: `/${workspaceId}/workspace-settings?adduser=true`;
window.open(path, '_blank');
};
const handleConnectSampleDB = () => {
const source = sampleDataSource;
const query = `SELECT tablename \nFROM pg_catalog.pg_tables \nWHERE schemaname='public';`;
createDataQuery(source, true, { query });
setPreviewData(null);
};
const draggingStatusChanged = useCallback(
(status) => {
setIsDragging(status);
@ -614,6 +640,10 @@ export const Container = ({
childComponents,
]);
const queryBoxText = sampleDataSource
? 'Connect to your data source or use our sample data source to start playing around!'
: 'Connect to a data source to be able to create a query';
return (
<div
{...(config.COMMENT_FEATURE_ENABLE && showComments && { onClick: handleAddThread })}
@ -695,20 +725,58 @@ export const Container = ({
})}
{Object.keys(boxes).length === 0 && !appLoading && !isDragging && (
<div style={{ paddingTop: '10%' }}>
<div className="mx-auto w-50 p-5 bg-light no-components-box">
<center className="text-muted" data-cy={`empty-editor-text`}>
You haven&apos;t added any components yet. Drag components from the right sidebar and drop here. Check out
our&nbsp;
<a
className="color-indigo9 "
href="https://docs.tooljet.com/docs/#quickstart-guide"
target="_blank"
rel="noreferrer"
>
guide
</a>{' '}
on adding components.
</center>
<div className="row empty-box-cont">
<div className="col-md-4 dotted-cont">
<div className="box-icon">
<BulkIcon name="addtemplate" width="25" viewBox="0 0 28 28" />
</div>
<div className={`title-text`} data-cy="empty-editor-text">
Drag and drop a component
</div>
<div className="title-desc">
Choose a component from the right side panel or use our pre-built templates to get started quickly!
</div>
</div>
<div className="col-md-4 dotted-cont">
<div className="box-icon">
<SolidIcon name="datasource" fill="#3E63DD" width="25" />
</div>
<div className={`title-text`}>Create a Query</div>
<div className="title-desc">{queryBoxText}</div>
{!!sampleDataSource && (
<div className="box-link">
<div className="child">
<a className="link-but" onClick={handleConnectSampleDB}>
Connect to sample data source{' '}
</a>
</div>
<div>
<BulkIcon name="arrowright" fill="#3E63DD" />
</div>
</div>
)}
</div>
<div className="col-md-4 dotted-cont">
<div className="box-icon">
<BulkIcon name="invitecollab" width="25" viewBox="0 0 28 28" />
</div>
<div className={`title-text `}>Share your application!</div>
<div className="title-desc">
Invite users to collaborate in real-time with multiplayer editing and comments for seamless development.
</div>
<div className="box-link">
<div className="child">
<a className="link-but" onClick={openAddUserWorkspaceSetting}>
Invite collaborators{' '}
</a>
</div>
<div>
<BulkIcon name="arrowright" fill="#3E63DD" />
</div>
</div>
</div>
</div>
</div>
)}

View file

@ -1,10 +1,11 @@
import React from 'react';
import { datasourceService, pluginsService, globalDatasourceService } from '@/_services';
import { datasourceService, pluginsService, globalDatasourceService, libraryAppService } from '@/_services';
import cx from 'classnames';
import { Modal, Button, Tab, Row, Col, ListGroup } from 'react-bootstrap';
import { toast } from 'react-hot-toast';
import { getSvgIcon } from '@/_helpers/appUtils';
import { TestConnection } from './TestConnection';
import { getWorkspaceId, deepEqual } from '@/_helpers/utils';
import {
DataBaseSources,
ApiSources,
@ -22,10 +23,11 @@ import { camelizeKeys, decamelizeKeys } from 'humps';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import { ConfirmDialog } from '@/_components';
import { deepEqual } from '../../_helpers/utils';
import { shallow } from 'zustand/shallow';
import { useDataSourcesStore } from '../../_stores/dataSourcesStore';
import { withRouter } from '@/_hoc/withRouter';
import { DATA_SOURCE_TYPE } from '@/_helpers/constants';
import './dataSourceManager.theme.scss';
import { useAppVersionStore } from '@/_stores/appVersionStore';
class DataSourceManagerComponent extends React.Component {
@ -71,6 +73,7 @@ class DataSourceManagerComponent extends React.Component {
createdDataSource: null,
unsavedChangesModal: false,
datasourceName,
creatingApp: false,
};
}
@ -531,6 +534,63 @@ class DataSourceManagerComponent extends React.Component {
);
};
createSampleApp = () => {
let _self = this;
_self.setState({ creatingApp: true });
libraryAppService
.createSampleApp()
.then((data) => {
const workspaceId = getWorkspaceId();
window.open(`/${workspaceId}/apps/${data.app[0].id}`, '_blank');
toast.success('App created successfully!');
_self.setState({ creatingApp: false });
})
.catch((errorResponse) => {
_self.setState({ creatingApp: false });
if (errorResponse.statusCode === 409) {
return false;
} else {
throw errorResponse;
}
});
};
renderSampleDBModal = () => {
const { dataSourceMeta, selectedDataSourceIcon, creatingApp } = this.state;
return (
<div className="sample-db-modal-body">
<div className="row sample-db-title">
<div className="col-md-1">
{getSvgIcon(dataSourceMeta?.kind?.toLowerCase(), 35, 35, selectedDataSourceIcon)}
</div>
<div className="col-md-1">PostgreSQL</div>
</div>
<div className={'sample-db-description'}>
<p className={`p ${this.props.darkMode ? 'dark' : ''}`}>
This PostgreSQL data source is a shared resource and may show varying data
<br /> due to real-time updates. It&apos;s reset daily for some consistency, but please note <br />
it&apos;s designed for user exploration, not production use.
</p>
</div>
<div className="create-btn-cont">
<ButtonSolid
className={`create-app-btn`}
isLoading={creatingApp}
// disabled={isSaving || this.props.isVersionReleased || isSaveDisabled}
variant="primary"
onClick={this.createSampleApp}
fill={this.props.darkMode && this.props.isVersionReleased ? '#4c5155' : '#FDFDFE'}
>
Create sample application
</ButtonSolid>
</div>
<div className="image-container">
<img src="assets/images/Sample data source.png" className="img-sample-db" alt="Sample data source" />
</div>
</div>
);
};
renderCardGroup = (source, type) => {
const openDataSourceConfirmModal = (dataSource) =>
this.setState({
@ -720,10 +780,18 @@ class DataSourceManagerComponent extends React.Component {
const createSelectedDataSource = (dataSource) => {
this.selectDataSource(dataSource);
};
const isSampleDb = selectedDataSource?.type === DATA_SOURCE_TYPE.SAMPLE;
const sampleDBmodalBodyStyle = isSampleDb ? { paddingBottom: '0px', borderBottom: '1px solid #E6E8EB' } : {};
const sampleDBmodalFooterStyle = isSampleDb ? { paddingTop: '8px' } : {};
const isSaveDisabled = selectedDataSource
? deepEqual(options, selectedDataSource?.options, ['encrypted']) && selectedDataSource?.name === datasourceName
: true;
this.props.setGlobalDataSourceStatus({ isEditing: !isSaveDisabled });
const docLink = isSampleDb
? 'https://docs.tooljet.com/docs/data-sources/sample-data-sources'
: selectedDataSource?.pluginId && selectedDataSource.pluginId.trim() !== ''
? `https://docs.tooljet.com/docs/marketplace/plugins/marketplace-plugin-${selectedDataSource?.kind}/`
: `https://docs.tooljet.com/docs/data-sources/${selectedDataSource?.kind}`;
return (
pluginsLoaded && (
<div>
@ -756,7 +824,7 @@ class DataSourceManagerComponent extends React.Component {
</div>
)}
<Modal.Title className="mt-3">
{selectedDataSource && (
{selectedDataSource && !isSampleDb ? (
<div className="row selected-ds">
{getSvgIcon(dataSourceMeta?.kind?.toLowerCase(), 35, 35, selectedDataSourceIcon)}
<div className="input-icon" style={{ width: '160px' }}>
@ -776,6 +844,13 @@ class DataSourceManagerComponent extends React.Component {
)}
</div>
</div>
) : (
<div className="row">
<div className="col-md-2">
<SolidIcon name="tooljet" />
</div>
<div className="col-md-10"> Sample data source</div>
</div>
)}
{!selectedDataSource && (
<span className="" data-cy="title-add-new-datasource">
@ -795,57 +870,65 @@ class DataSourceManagerComponent extends React.Component {
</div>
{this.renderEnvironmentsTab(selectedDataSource)}
</Modal.Header>
<Modal.Body>
{selectedDataSource && <div>{this.renderSourceComponent(selectedDataSource.kind, isPlugin)}</div>}
<Modal.Body style={sampleDBmodalBodyStyle}>
{selectedDataSource && !isSampleDb ? (
<div>{this.renderSourceComponent(selectedDataSource.kind, isPlugin)}</div>
) : (
selectedDataSource && isSampleDb && <div>{this.renderSampleDBModal()}</div>
)}
{!selectedDataSource && this.segregateDataSources(this.state.suggestingDatasources, this.props.darkMode)}
</Modal.Body>
{selectedDataSource && !dataSourceMeta.customTesting && (
<Modal.Footer>
<div className="row w-100">
<div className="card-body datasource-footer-info">
<div className="row">
<div className="col-1">
<SolidIcon name="information" fill="#3E63DD" />
</div>
<div className="col" style={{ maxWidth: '480px' }}>
<p data-cy="white-list-ip-text" className="tj-text">
{this.props.t(
'editor.queryManager.dataSourceManager.whiteListIP',
'Please white-list our IP address if the data source is not publicly accessible.'
)}
</p>
</div>
<div className="col-auto">
{isCopied ? (
<center className="my-2">
<span className="copied" data-cy="label-ip-copied">
{this.props.t('editor.queryManager.dataSourceManager.copied', 'Copied')}
</span>
</center>
) : (
<CopyToClipboard
text={config.SERVER_IP}
onCopy={() => {
this.setState({ isCopied: true });
}}
>
<ButtonSolid
type="button"
className={`datasource-copy-button`}
data-cy="button-copy-ip"
variant="tertiary"
leftIcon="copy"
iconWidth="12"
<Modal.Footer style={sampleDBmodalFooterStyle} className="modal-footer-class">
{selectedDataSource && !isSampleDb && (
<div className="row w-100">
<div className="card-body datasource-footer-info">
<div className="row">
<div className="col-1">
<SolidIcon name="information" fill="#3E63DD" />
</div>
<div className="col" style={{ maxWidth: '480px' }}>
<p data-cy="white-list-ip-text" className="tj-text">
{this.props.t(
'editor.queryManager.dataSourceManager.whiteListIP',
'Please white-list our IP address if the data source is not publicly accessible.'
)}
</p>
</div>
<div className="col-auto">
{isCopied ? (
<center className="my-2">
<span className="copied" data-cy="label-ip-copied">
{this.props.t('editor.queryManager.dataSourceManager.copied', 'Copied')}
</span>
</center>
) : (
<CopyToClipboard
text={config.SERVER_IP}
onCopy={() => {
this.setState({ isCopied: true });
}}
>
{this.props.t('editor.queryManager.dataSourceManager.copy', 'Copy')}
</ButtonSolid>
</CopyToClipboard>
)}
<ButtonSolid
type="button"
className={`datasource-copy-button`}
data-cy="button-copy-ip"
variant="tertiary"
leftIcon="copy"
iconWidth="12"
>
{this.props.t('editor.queryManager.dataSourceManager.copy', 'Copy')}
</ButtonSolid>
</CopyToClipboard>
)}
</div>
</div>
</div>
</div>
</div>
)}
{connectionTestError && (
<div className="row w-100">
@ -861,11 +944,7 @@ class DataSourceManagerComponent extends React.Component {
<SolidIcon name="logs" fill="#3E63DD" width="20" style={{ marginRight: '8px' }} />
<a
className="color-primary tj-docs-link tj-text-sm"
href={
selectedDataSource?.pluginId && selectedDataSource.pluginId.trim() !== ''
? `https://docs.tooljet.com/docs/marketplace/plugins/marketplace-plugin-${selectedDataSource.kind}/`
: `https://docs.tooljet.com/docs/data-sources/${selectedDataSource.kind}`
}
href={docLink}
target="_blank"
rel="noreferrer"
data-cy="link-read-documentation"
@ -873,7 +952,10 @@ class DataSourceManagerComponent extends React.Component {
{this.props.t('globals.readDocumentation', 'Read documentation')}
</a>
</div>
<div className="col-auto" data-cy="button-test-connection">
<div
className={!isSampleDb ? `col-auto` : 'col-auto test-connection-sample-db'}
data-cy="button-test-connection"
>
<TestConnection
kind={selectedDataSource.kind}
pluginId={selectedDataSource?.pluginId ?? this.state.selectedDataSourcePluginId}
@ -883,19 +965,21 @@ class DataSourceManagerComponent extends React.Component {
environmentId={this.props.currentEnvironment?.id}
/>
</div>
<div className="col-auto" data-cy="db-connection-save-button">
<ButtonSolid
className={`m-2 ${isSaving ? 'btn-loading' : ''}`}
isLoading={isSaving}
disabled={isSaving || this.props.isVersionReleased || isSaveDisabled}
variant="primary"
onClick={this.createDataSource}
leftIcon="floppydisk"
fill={this.props.darkMode && this.props.isVersionReleased ? '#4c5155' : '#FDFDFE'}
>
{this.props.t('globals.save', 'Save')}
</ButtonSolid>
</div>
{!isSampleDb && (
<div className="col-auto" data-cy="db-connection-save-button">
<ButtonSolid
className={`m-2 ${isSaving ? 'btn-loading' : ''}`}
isLoading={isSaving}
disabled={isSaving || this.props.isVersionReleased || isSaveDisabled}
variant="primary"
onClick={this.createDataSource}
leftIcon="floppydisk"
fill={this.props.darkMode && this.props.isVersionReleased ? '#4c5155' : '#FDFDFE'}
>
{this.props.t('globals.save', 'Save')}
</ButtonSolid>
</div>
)}
</Modal.Footer>
)}

View file

@ -0,0 +1,67 @@
@import '../../_styles/colors.scss';
.sample-db-modal-body{
min-height: calc(110vh - 380px);;
margin-bottom: 0;
padding-top: 20px;
.sample-db-title {
margin-top: 40px;
justify-content: center;
display: flex;
}
.sample-db-description {
margin-top: 20px;
justify-content: center;
display: flex;
.dark{
color: white !important;
}
.p{
// color: var(--);
font-size: 12px;
font-weight: 400;
text-align: center;
color: $color-light-slate-11;
}
}
.create-btn-cont{
justify-content: center;
display: flex;
.create-app-btn {
border-radius: 8px;
font-size: 12px;
height: 32px;
width: 168px;
padding: 0;
}
}
.img-sample-db{
height: 40%;
width: 55%;
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
}
}
.modal-footer-class{
.test-connection-sample-db{
margin-right: 90px;
}
}

View file

@ -13,8 +13,10 @@ import { useDataQueriesActions } from '@/_stores/dataQueriesStore';
import { useQueryPanelActions } from '@/_stores/queryPanelStore';
import { Tooltip } from 'react-tooltip';
import { authenticationService } from '@/_services';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import '../queryManager.theme.scss';
function DataSourcePicker({ dataSources, staticDataSources, darkMode, globalDataSources }) {
function DataSourcePicker({ dataSources, sampleDataSource, staticDataSources, darkMode, globalDataSources }) {
const allUserDefinedSources = [...dataSources, ...globalDataSources];
const [searchTerm, setSearchTerm] = useState();
const [filteredUserDefinedDataSources, setFilteredUserDefinedDataSources] = useState(allUserDefinedSources);
@ -23,6 +25,8 @@ function DataSourcePicker({ dataSources, staticDataSources, darkMode, globalData
const { setPreviewData } = useQueryPanelActions();
const { admin } = authenticationService.currentSessionValue;
const docLink = 'sampledb.com';
const handleChangeDataSource = (source) => {
createDataQuery(source);
setPreviewData(null);
@ -86,6 +90,50 @@ function DataSourcePicker({ dataSources, staticDataSources, darkMode, globalData
);
})}
</div>
{!!sampleDataSource && (
<div>
<label className="form-label sample-db-data-query-picker-form-label" data-cy={`landing-page-label-default`}>
Sample data sources
</label>
<div className="query-datasource-card-container d-flex justify-content-between mb-3 mt-2">
<ButtonSolid
key={`${sampleDataSource.id}-${sampleDataSource.kind}`}
variant="tertiary"
size="sm"
onClick={() => {
handleChangeDataSource(sampleDataSource);
}}
className="text-truncate"
data-cy={`${sampleDataSource.kind.toLowerCase().replace(/\s+/g, '-')}-sample-db-add-query-card`}
>
<DataSourceIcon source={sampleDataSource} height={14} />{' '}
{sampleDataSource.kind == 'postgresql' ? 'PostgreSQL' : 'ToolJetDB'}
</ButtonSolid>
</div>
{/* Info icon */}
<div className="open-doc-link-container">
<div className="col-md-1 info-btn">
<SolidIcon name="informationcircle" fill="#3E63DD" />
</div>
<div className="col-md-11">
<div className="message" data-cy="warning-text">
<p>
This is a shared resource and may show varying data due to real-time updates. It&apos;s reset daily
for some consistency, but please note it&apos;s designed for user exploration, not production
use.&nbsp;
<a onClick={handleAddClick} target="_blank" rel="noopener noreferrer" className="opn-git-btn">
Explore available data sources
</a>{' '}
<SolidIcon name="open" width={'8'} height={'8'} viewBox={'0 0 10 10'} />
</p>
</div>
</div>
</div>
</div>
)}
<div className="d-flex d-flex justify-content-between">
<label className="form-label py-1" style={{ width: 'auto' }} data-cy={`label-avilable-ds`}>
{`Available Data sources ${!isEmpty(allUserDefinedSources) ? '(' + allUserDefinedSources.length + ')' : 0}`}

View file

@ -6,18 +6,24 @@ import DataSourceIcon from './DataSourceIcon';
import { authenticationService } from '@/_services';
import { getWorkspaceId } from '@/_helpers/utils';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import { useDataSources, useGlobalDataSources } from '@/_stores/dataSourcesStore';
import { useDataSources, useGlobalDataSources, useSampleDataSource } from '@/_stores/dataSourcesStore';
import { useDataQueriesActions } from '@/_stores/dataQueriesStore';
import { staticDataSources } from '../constants';
import { useQueryPanelActions } from '@/_stores/queryPanelStore';
import Search from '@/_ui/Icon/solidIcons/Search';
import { Tooltip } from 'react-tooltip';
import { DataBaseSources, ApiSources, CloudStorageSources } from '@/Editor/DataSourceManager/SourceComponents';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import './../queryManager.theme.scss';
import { DATA_SOURCE_TYPE } from '@/_helpers/constants';
function DataSourceSelect({ isDisabled, selectRef, closePopup }) {
const dataSources = useDataSources();
const globalDataSources = useGlobalDataSources();
const [userDefinedSources, setUserDefinedSources] = useState([...dataSources, ...globalDataSources]);
const sampleDataSource = useSampleDataSource();
const [userDefinedSources, setUserDefinedSources] = useState(
[...dataSources, ...globalDataSources, !!sampleDataSource && sampleDataSource].filter(Boolean)
);
const [dataSourcesKinds, setDataSourcesKinds] = useState([]);
const [userDefinedSourcesOpts, setUserDefinedSourcesOpts] = useState([]);
const { createDataQuery } = useDataQueriesActions();
@ -28,10 +34,11 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup }) {
closePopup();
};
console.log(dataSourcesKinds);
useEffect(() => {
const allDataSources = [...dataSources, ...globalDataSources];
const shouldAddSampleDataSource = !!sampleDataSource;
const allDataSources = [...dataSources, ...globalDataSources, shouldAddSampleDataSource && sampleDataSource].filter(
Boolean
);
setUserDefinedSources(allDataSources);
const dataSourceKindsList = [...DataBaseSources, ...ApiSources, ...CloudStorageSources];
allDataSources.forEach(({ plugin }) => {
@ -42,41 +49,56 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup }) {
dataSourceKindsList.push({ name: plugin.name, kind: plugin.pluginId });
});
setDataSourcesKinds(dataSourceKindsList);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dataSources]);
useEffect(() => {
const sortedUserDefinedSources = userDefinedSources.sort((sourceA, sourceB) => {
// Custom sorting function
const typeA = sourceA?.type;
const typeB = sourceB?.type;
if (typeA === 'sample' && typeB !== 'sample') {
return -1; // typeA is 'sample', so it comes before typeB
} else if (typeB === 'sample' && typeA !== 'sample') {
return 1; // typeB is 'sample', so it comes after typeA
} else {
// Otherwise, maintain the original order
return 0;
}
});
setUserDefinedSourcesOpts(
Object.entries(groupBy(userDefinedSources, 'kind')).map(([kind, sources], index) => ({
label: (
<div>
{index === 0 && (
<div className="color-slate9 mb-2 pb-1" style={{ fontWeight: 500, marginTop: '-8px' }}>
Data sources
</div>
)}
<DataSourceIcon source={sources?.[0]} height={16} />
<span className="ms-1 small">{dataSourcesKinds.find((dsk) => dsk.kind === kind)?.name || kind}</span>
</div>
),
options: sources.map((source) => ({
Object.entries(groupBy(sortedUserDefinedSources, 'type')).flatMap(([type, sourcesWithType], index) =>
Object.entries(groupBy(sourcesWithType, 'kind')).map(([kind, sources], innerIndex) => ({
label: (
<div
className="py-2 px-2 rounded option-nested-datasource-selector small text-truncate"
data-tooltip-id="tooltip-for-add-query-dd-option"
data-tooltip-content={source.name}
data-cy={`ds-${source.name.toLowerCase()}`}
>
{source.name}
<Tooltip id="tooltip-for-add-query-dd-option" className="tooltip query-manager-ds-select-tooltip" />
<div key={`${kind}-${type}`}>
{((innerIndex === 0 && type !== DATA_SOURCE_TYPE.SAMPLE) || type === DATA_SOURCE_TYPE.SAMPLE) && (
<div className="color-slate9 mb-2 pb-1" style={{ fontWeight: 500, marginTop: '-8px' }}>
{type !== DATA_SOURCE_TYPE.SAMPLE ? 'Data Sources' : 'Sample data sources'}
</div>
)}
<DataSourceIcon source={sources?.[0]} height={16} />
<span className="ms-1 small">{dataSourcesKinds.find((dsk) => dsk.kind === kind)?.name || kind}</span>
</div>
),
value: source.id,
isNested: true,
source,
})),
}))
options: sources.map((source) => ({
label: (
<div
key={source.id}
className="py-2 px-2 rounded option-nested-datasource-selector small text-truncate"
data-tooltip-id="tooltip-for-add-query-dd-option"
data-tooltip-content={source.name}
data-cy={`ds-${source.name.toLowerCase()}`}
>
{source.name}
<Tooltip id="tooltip-for-add-query-dd-option" className="tooltip query-manager-ds-select-tooltip" />
</div>
),
value: source.id,
isNested: true,
source,
})),
}))
)
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userDefinedSources]);
@ -125,6 +147,7 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup }) {
menuPlacement="auto"
components={{
MenuList: MenuList,
GroupHeading: HideGroupHeading,
IndicatorSeparator: () => null,
DropdownIndicator,
}}
@ -226,6 +249,19 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup }) {
);
}
const HideGroupHeading = (props) => {
return (
<div
className="collapse-group-heading"
onClick={() => {
document.querySelector(`#${props.id}`).parentElement.parentElement.classList.toggle('collapsed-group');
}}
>
<components.GroupHeading {...props} />
</div>
);
};
const MenuList = ({ children, getStyles, innerRef, ...props }) => {
const navigate = useNavigate();
const menuListStyles = getStyles('menuList', props);

View file

@ -13,7 +13,7 @@ import { CustomToggleSwitch } from './CustomToggleSwitch';
import { EventManager } from '@/Editor/Inspector/EventManager';
import { staticDataSources, customToggles, mockDataQueryAsComponent } from '../constants';
import { DataSourceTypes } from '../../DataSourceManager/SourceComponents';
import { useDataSources, useGlobalDataSources } from '@/_stores/dataSourcesStore';
import { useDataSources, useGlobalDataSources, useSampleDataSource } from '@/_stores/dataSourcesStore';
import { useDataQueriesActions } from '@/_stores/dataQueriesStore';
import { useSelectedQuery, useSelectedDataSource } from '@/_stores/queryPanelStore';
import { useAppVersionStore } from '@/_stores/appVersionStore';
@ -32,6 +32,8 @@ export const QueryManagerBody = ({
const { t } = useTranslation();
const dataSources = useDataSources();
const globalDataSources = useGlobalDataSources();
const sampleDataSource = useSampleDataSource();
const selectedQuery = useSelectedQuery();
const selectedDataSource = useSelectedDataSource();
const { changeDataQuery, updateDataQuery } = useDataQueriesActions();
@ -113,6 +115,7 @@ export const QueryManagerBody = ({
dataSources={dataSources}
staticDataSources={staticDataSources}
globalDataSources={globalDataSources}
sampleDataSource={sampleDataSource}
darkMode={darkMode}
/>
</div>
@ -234,9 +237,9 @@ export const QueryManagerBody = ({
};
const renderChangeDataSource = () => {
const selectableDataSources = [...globalDataSources, ...dataSources].filter(
(ds) => ds.kind === selectedQuery?.kind
);
const selectableDataSources = [...dataSources, ...globalDataSources, !!sampleDataSource && sampleDataSource]
.filter(Boolean)
.filter((ds) => ds.kind === selectedQuery?.kind);
if (isEmpty(selectableDataSources)) {
return '';
}

View file

@ -4,7 +4,12 @@ import { QueryManagerHeader } from './Components/QueryManagerHeader';
import { QueryManagerBody } from './Components/QueryManagerBody';
import { runQuery } from '@/_helpers/appUtils';
import { defaultSources } from './constants';
import { useDataSources, useGlobalDataSources, useLoadingDataSources } from '@/_stores/dataSourcesStore';
import {
useDataSources,
useGlobalDataSources,
useLoadingDataSources,
useSampleDataSource,
} from '@/_stores/dataSourcesStore';
import { useQueryToBeRun, useSelectedQuery, useQueryPanelActions } from '@/_stores/queryPanelStore';
import { CodeHinterContext } from '@/Editor/CodeBuilder/CodeHinterContext';
import { resolveReferences } from '@/_helpers/utils';
@ -12,6 +17,7 @@ import { resolveReferences } from '@/_helpers/utils';
const QueryManager = ({ mode, appId, darkMode, apps, allComponents, appDefinition, editorRef }) => {
const loadingDataSources = useLoadingDataSources();
const dataSources = useDataSources();
const sampleDataSource = useSampleDataSource();
const globalDataSources = useGlobalDataSources();
const queryToBeRun = useQueryToBeRun();
const selectedQuery = useSelectedQuery();
@ -32,9 +38,9 @@ const QueryManager = ({ mode, appId, darkMode, apps, allComponents, appDefinitio
useEffect(() => {
if (selectedQuery) {
const selectedDS = [...dataSources, ...globalDataSources].find(
(datasource) => datasource.id === selectedQuery?.data_source_id
);
const selectedDS = [...dataSources, ...globalDataSources, !!sampleDataSource && sampleDataSource]
.filter(Boolean)
.find((datasource) => datasource.id === selectedQuery?.data_source_id);
//TODO: currently type is not taken into account. May create issues in importing REST apis. to be revamped when import app is revamped
if (
selectedQuery?.kind in defaultSources &&

View file

@ -0,0 +1,81 @@
.collapsed {
display: none;
}
.collapsed-group > div:not(.collapse-group-heading) {
display: none;
}
.collapse-group-heading::after {
flex-shrink: 0;
width: 36px;
height: 30px;
margin-left: auto;
margin-top: auto;
content: "";
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath fill='%23dadada' d='M10 14.142L4.516 8.657 3.102 10.071 10 16.97 16.899 10.071 15.485 8.657z'/%3E%3C/svg%3E");
background-repeat: no-repeat no-repeat;
background-size: 1.25rem;
background-position: center center;
color: #dadada;
transform: rotate(180deg);
background-color:none;
}
.collapsed-group .collapse-group-heading::after {
transform: rotate(0deg);
}
.collapse-group-heading {
display: flex;
}
.sample-db-data-query-picker-form-label {
width: auto !important;
}
.open-doc-link-container {
display: flex;
width: auto;
height: auto;
padding: 10px 12px 8px 12px;
border: 1px solid var(--indigo7);
background: var(--indigo2);
border-radius: 6px 6px 6px 6px;
margin-bottom: 10px;
.info-btn {
padding: 0px;
flex: 0 0 24px;
}
.message {
margin-left: 5px;
display: inline-block;
word-wrap: break-word;
width: auto;
height: auto;
font-size: 10px;
line-height: 13px;
color: var(--slate11);
p{
padding: 0;
margin: 0;
}
.open-git-btn {
margin-top: 5px;
color: var(--indigo9);
font-size: 10px;
font-weight: 500;
display: inline-block;
.open-icn {
margin-right: 2px;
}
}
}
}

View file

@ -24,13 +24,20 @@ const QueryPanel = ({
}) => {
const { updateQueryPanelHeight } = useQueryPanelActions();
const dataQueries = useDataQueries();
const queryManagerPreferences = useRef(JSON.parse(localStorage.getItem('queryManagerPreferences')) ?? {});
const queryManagerPreferences = useRef(
JSON.parse(localStorage.getItem('queryManagerPreferences')) ?? {
current: {
isExpanded: true,
queryPanelHeight: 100,
},
}
);
const queryPaneRef = useRef(null);
const [isExpanded, setExpanded] = useState(queryManagerPreferences.current?.isExpanded ?? true);
const [isDragging, setDragging] = useState(false);
const [height, setHeight] = useState(
queryManagerPreferences.current?.queryPanelHeight > 95
? 30
? 50
: queryManagerPreferences.current?.queryPanelHeight ?? 70
);
const [isTopOfQueryPanel, setTopOfQueryPanel] = useState(false);

View file

@ -0,0 +1,45 @@
.empty-box-cont{
display: flex;
justify-content: center;
.dotted-cont{
border: 1px dashed var(--indigo8);
border-radius: 6px;
margin: 0 10px 0 10px;
padding: 20px;
height: 192px;
width: 300px;
margin-bottom: 10px;
}
.title-text{
margin-top: 10px;
font-size: 16px;
font-weight: 600px;
}
.title-desc{
font-size: 14px;
margin-top: 5px;
font-weight: 400;
}
.box-link{
display: flex;
margin-top: 10px;
.chile {
margin-right: 10px;
width: auto;
}
.link-but{
font-weight: 500;
font-size: 14px;
color: #3E63DD;
;
}
}
}

View file

@ -3,10 +3,20 @@ import cx from 'classnames';
import { GlobalDataSourcesContext } from '..';
import { DataSourceTypes } from '../../Editor/DataSourceManager/SourceComponents';
import { getSvgIcon } from '@/_helpers/appUtils';
import DeleteIcon from '../Icons/DeleteIcon.svg';
import useGlobalDatasourceUnsavedChanges from '@/_hooks/useGlobalDatasourceUnsavedChanges';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import { ToolTip } from '@/_components';
import { DATA_SOURCE_TYPE } from '@/_helpers/constants';
export const ListItem = ({ dataSource, key, active, onDelete, updateSelectedDatasource }) => {
export const ListItem = ({
dataSource,
key,
active,
onDelete,
updateSelectedDatasource,
toolTipText,
disableDelButton = false,
}) => {
const {
setSelectedDataSource,
toggleDataSourceManagerModal,
@ -28,7 +38,12 @@ export const ListItem = ({ dataSource, key, active, onDelete, updateSelectedData
// sourceMeta would be missing on development setup when switching between branches
// if ds is already in branch while not available in another
const icon = getSvgIcon(sourceMeta?.kind?.toLowerCase(), 24, 24, dataSource?.plugin?.iconFile?.data);
const icon =
dataSource.type === DATA_SOURCE_TYPE.SAMPLE ? (
<SolidIcon name="tooljet" />
) : (
getSvgIcon(sourceMeta?.kind?.toLowerCase(), 24, 24, dataSource?.plugin?.iconFile?.data)
);
const focusModal = () => {
const element = document.getElementsByClassName('form-control-plaintext form-control-plaintext-sm')[0];
@ -44,36 +59,63 @@ export const ListItem = ({ dataSource, key, active, onDelete, updateSelectedData
updateSelectedDatasource(dataSource?.name);
};
const isSampleDb = dataSource.type == DATA_SOURCE_TYPE.SAMPLE;
const showDeleteButton = !isSampleDb;
return (
<div
key={key}
className={cx('mx-3 rounded-3 datasources-list', {
'datasources-list-item': active,
})}
<ToolTip
placement="right"
show={toolTipText ? true : false}
message={'Sample data source\ncannot be deleted'}
tooltipClassName="tooltip-sampl-db"
>
<div
role="button"
onClick={() => handleActions(selectDataSource)}
className="col d-flex align-items-center overflow-hidden"
data-cy={`${String(dataSource.name).toLowerCase().replace(/\s+/g, '-')}-button`}
key={key}
className={cx('mx-3 rounded-3 datasources-list', {
'datasources-list-item': active,
})}
>
<div>{icon}</div>
<div className="font-400 tj-text-xsm text-truncate" style={{ paddingLeft: '6px' }}>
{dataSource.name}
</div>
</div>
<div className="col-auto">
<button
className="ds-delete-btn"
onClick={() => onDelete(dataSource)}
data-cy={`${String(dataSource.name).toLowerCase().replace(/\s+/g, '-')}-delete-button`}
<div
role="button"
onClick={() => handleActions(selectDataSource)}
className="col d-flex align-items-center overflow-hidden"
data-cy={`${String(dataSource.name).toLowerCase().replace(/\s+/g, '-')}-button`}
>
<div>
<DeleteIcon width="14" height="14" />
<div>{icon}</div>
<div className="font-400 tj-text-xsm text-truncate" style={{ paddingLeft: '6px', display: 'flex' }}>
{dataSource.name}
{isSampleDb && (
<div
className="font-400 tj-text-xxsm text-truncate"
style={{ paddingTop: '3px', paddingLeft: '2px', color: '#687076' }}
>{`(postgres)`}</div>
)}
</div>
</button>
</div>
{showDeleteButton && (
<div className="col-auto">
{}
<button
title={'Delete'}
disabled={disableDelButton}
className="ds-delete-btn"
onClick={() => onDelete(dataSource)}
data-cy={`${String(dataSource.name).toLowerCase().replace(/\s+/g, '-')}-delete-button`}
>
<div>
<SolidIcon
width="14"
height="14"
name="delete"
fill={disableDelButton ? '#E6E8EB' : '#E54D2E'}
className={disableDelButton ? 'disabled-button' : ''}
/>
</div>
</button>
</div>
)}
</div>
</div>
</ToolTip>
);
};

View file

@ -8,6 +8,7 @@ import { globalDatasourceService } from '@/_services';
import EmptyFoldersIllustration from '@assets/images/icons/no-queries-added.svg';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import { SearchBox } from '@/_components/SearchBox';
import { DATA_SOURCE_TYPE } from '@/_helpers/constants';
export const List = ({ updateSelectedDatasource }) => {
const {
@ -145,10 +146,13 @@ export const List = ({ updateSelectedDatasource }) => {
{!isLoading && filteredData?.length ? (
<div className="list-group">
{filteredData?.map((source, idx) => {
const sanpleDBtoolTipText =
source.type == DATA_SOURCE_TYPE.SAMPLE ? 'Sample data source\ncannot be deleted' : '';
return (
<ListItem
dataSource={source}
key={idx}
toolTipText={sanpleDBtoolTipText}
active={selectedDataSource?.id === source?.id}
onDelete={deleteDataSource}
updateSelectedDatasource={updateSelectedDatasource}

View file

@ -5,6 +5,7 @@ import { globalDatasourceService, appEnvironmentService, authenticationService }
import { GlobalDataSourcesPage } from './GlobalDataSourcesPage';
import { toast } from 'react-hot-toast';
import { BreadCrumbContext } from '@/App/App';
import { DATA_SOURCE_TYPE } from '@/_helpers/constants';
export const GlobalDataSourcesContext = createContext({
showDataSourceManagerModal: false,
@ -73,7 +74,16 @@ export const GlobalDatasources = (props) => {
}
return ds;
})
.sort((a, b) => a.name.localeCompare(b.name));
.sort((a, b) => {
if (a.type === DATA_SOURCE_TYPE.SAMPLE && b.type !== DATA_SOURCE_TYPE.SAMPLE) {
return -1; // a comes before b
} else if (a.type !== DATA_SOURCE_TYPE.SAMPLE && b.type === DATA_SOURCE_TYPE.SAMPLE) {
return 1; // b comes before a
} else {
// If types are the same or both are not 'sample', sort by name
return a.name.localeCompare(b.name);
}
});
setDataSources([...(orderedDataSources ?? [])]);
const ds = dataSource && orderedDataSources.find((ds) => ds.id === dataSource.id);
if (!resetSelection && ds) {

View file

@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react';
import { isEmpty } from 'lodash';
import { useNavigate } from 'react-router-dom';
import Select from 'react-select';
import { getWorkspaceId } from '@/_helpers/utils';
export default function FolderFilter({ disabled, options, onChange, value }) {
const navigate = useNavigate();
@ -16,7 +17,7 @@ export default function FolderFilter({ disabled, options, onChange, value }) {
const updateFolderQuery = (name, id) => {
const path = `${id ? `?folder=${name}` : ''}`;
navigate({ pathname: location.pathname, search: path }, { replace: true });
navigate({ pathname: `/${getWorkspaceId()}`, search: path }, { replace: true });
};
useEffect(() => {

View file

@ -111,8 +111,8 @@ export const Folders = function Folders({
}
function updateFolderQuery(name) {
const path = `${name ? `?folder=${name}` : ''}`;
navigate({ pathname: location.pathname, search: path }, { replace: true });
const search = `${name ? `?folder=${name}` : ''}`;
navigate({ pathname: `/${getWorkspaceId()}`, search }, { replace: true });
}
function deleteFolder(folder) {

View file

@ -21,6 +21,7 @@ import { OrganizationList } from '@/_components/OrganizationManager/List';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import BulkIcon from '@/_ui/Icon/bulkIcons/index';
import { getWorkspaceId, pageTitles, setWindowTitle } from '@/_helpers/utils';
import { getQueryParams } from '@/_helpers/routes';
import { withRouter } from '@/_hoc/withRouter';
import FolderFilter from './FolderFilter';
import { APP_ERROR_TYPE } from '@/_helpers/error_constants';
@ -78,10 +79,18 @@ class HomePageComponent extends React.Component {
};
}
setQueryParameter = () => {
const showImportTemplateModal = getQueryParams('fromtemplate');
this.setState({
showTemplateLibraryModal: showImportTemplateModal ? showImportTemplateModal : false,
});
};
componentDidMount() {
setWindowTitle({ page: pageTitles.DASHBOARD });
this.fetchApps(1, this.state.currentFolder.id);
this.fetchFolders();
this.setQueryParameter();
}
fetchApps = (page = 1, folder, searchKey) => {
@ -529,10 +538,16 @@ class HomePageComponent extends React.Component {
});
};
removeQueryParameters = () => {
const urlWithoutParams = window.location.origin + window.location.pathname;
window.history.replaceState({}, document.title, urlWithoutParams);
};
showTemplateLibraryModal = () => {
this.setState({ showTemplateLibraryModal: true });
};
hideTemplateLibraryModal = () => {
this.removeQueryParameters();
this.setState({ showTemplateLibraryModal: false });
};

View file

@ -10,6 +10,7 @@ import UsersFilter from '../../ee/components/UsersPage/UsersFilter';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import ManageOrgUsersDrawer from './ManageOrgUsersDrawer';
import { USER_DRAWER_MODES } from '@/_helpers/utils';
import { getQueryParams } from '@/_helpers/routes';
class ManageOrgUsersComponent extends React.Component {
constructor(props) {
@ -38,6 +39,13 @@ class ManageOrgUsersComponent extends React.Component {
};
}
setQueryParameter = () => {
const showAdduserDrawer = getQueryParams('adduser');
this.setState({
isInviteUsersDrawerOpen: showAdduserDrawer ? showAdduserDrawer : false,
});
};
validateEmail(email) {
const re =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
@ -220,6 +228,10 @@ class ManageOrgUsersComponent extends React.Component {
}
};
componentDidMount() {
this.setQueryParameter();
}
generateInvitationURL = (user) => {
if (user.account_setup_token) {
return urlJoin(

View file

@ -8,6 +8,7 @@ import { BreadCrumbContext } from '../App/App';
import FolderList from '@/_ui/FolderList/FolderList';
import { OrganizationList } from '../_components/OrganizationManager/List';
import { getWorkspaceId } from '@/_helpers/utils';
import { getSubpath } from '@/_helpers/routes';
export function OrganizationSettings(props) {
const [admin, setAdmin] = useState(authenticationService.currentSessionValue?.admin);
@ -47,9 +48,9 @@ export function OrganizationSettings(props) {
const selectedTabFromRoute = location.pathname.split('/').pop();
if (selectedTabFromRoute === 'workspace-settings') {
setSelectedTab(admin ? 'Users' : 'Workspace variables');
window.location.href = admin
? `/${workspaceId}/workspace-settings/users`
: `/${workspaceId}/workspace-settings/workspace-variables`;
const subPath = getSubpath();
const path = subPath ? `${subPath}/${workspaceId}/workspace-settings` : `/${workspaceId}/workspace-settings`;
window.location.href = admin ? `${path}/users` : `${path}/workspace-variables`;
} else {
setSelectedTab(defaultOrgName(selectedTabFromRoute));
}

View file

@ -69,3 +69,14 @@ export const ERROR_MESSAGES = {
export const TOOLTIP_MESSAGES = {
SHARE_URL_UNAVAILABLE: 'Share URL is unavailable until current version is released',
};
export const DATA_SOURCE_TYPE = {
SAMPLE: 'sample',
LOCAL: 'local',
GLOBAL: 'global',
};
export const SAMPLE_DB_KIND = {
POSTGRESQL: 'postgresql',
TOOLJET_DB: 'tooljetdb',
};

View file

@ -4,6 +4,7 @@ import { authHeader, handleResponse } from '@/_helpers';
export const libraryAppService = {
deploy,
templateManifests,
createSampleApp,
};
function deploy(identifier, appName) {
@ -20,3 +21,8 @@ function templateManifests() {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/library_apps/`, requestOptions).then(handleResponse);
}
function createSampleApp() {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/library_apps/sample-app`, requestOptions).then(handleResponse);
}

View file

@ -136,10 +136,11 @@ export const useDataQueriesStore = create(
actions.setSelectedQuery(selectedQuery.id);
},
// createDataQuery: (appId, appVersionId, options, kind, name, selectedDataSource, shouldRunQuery) => {
createDataQuery: (selectedDataSource, shouldRunQuery) => {
createDataQuery: (selectedDataSource, shouldRunQuery, customOptions = {}) => {
const appVersionId = useAppVersionStore.getState().editingVersion?.id;
const appId = useAppDataStore.getState().appId;
const { options, name } = getDefaultOptions(selectedDataSource);
const { options: defaultOptions, name } = getDefaultOptions(selectedDataSource);
const options = { ...defaultOptions, ...customOptions };
const kind = selectedDataSource.kind;
const tempId = uuidv4();
set({ creatingQueryInProcessId: tempId });

View file

@ -1,10 +1,12 @@
import { create, zustandDevTools } from './utils';
import { datasourceService, globalDatasourceService } from '@/_services';
import { DATA_SOURCE_TYPE } from '@/_helpers/constants';
const initialState = {
dataSources: [],
loadingDataSources: true,
globalDataSources: [],
sampleDataSource: null,
globalDataSourceStatus: {
isSaving: false,
isEditing: false,
@ -31,7 +33,8 @@ export const useDataSourcesStore = create(
fetchGlobalDataSources: (organizationId) => {
globalDatasourceService.getAll(organizationId).then((data) => {
set({
globalDataSources: data.data_sources,
globalDataSources: data.data_sources?.filter((source) => source?.type != DATA_SOURCE_TYPE.SAMPLE),
sampleDataSource: data.data_sources?.filter((source) => source?.type == DATA_SOURCE_TYPE.SAMPLE)[0],
});
});
},
@ -50,6 +53,7 @@ export const useDataSourcesStore = create(
export const useDataSources = () => useDataSourcesStore((state) => state.dataSources);
export const useGlobalDataSources = () => useDataSourcesStore((state) => state.globalDataSources);
export const useSampleDataSource = () => useDataSourcesStore((state) => state.sampleDataSource);
export const useLoadingDataSources = () => useDataSourcesStore((state) => state.loadingDataSources);
export const useDataSourcesActions = () => useDataSourcesStore((state) => state.actions);
export const useGlobalDataSourcesStatus = () => useDataSourcesStore((state) => state.globalDataSourceStatus);

View file

@ -1,6 +1,7 @@
import { schemaUnavailableOptions } from '@/Editor/QueryManager/constants';
import { allOperations } from '@tooljet/plugins/client';
import { capitalize } from 'lodash';
import { DATA_SOURCE_TYPE } from '@/_helpers/constants';
import { useDataQueriesStore } from '@/_stores/dataQueriesStore';
export const getDefaultOptions = (source) => {
@ -36,17 +37,21 @@ export const getDefaultOptions = (source) => {
}
}
return { options, name: computeQueryName(source.kind) };
return { options, name: computeQueryName(source) };
};
const computeQueryName = (kind) => {
const computeQueryName = (source) => {
const { kind, type } = source;
const dataQueries = useDataQueriesStore.getState().dataQueries;
const currentQueriesForKind = dataQueries.filter((query) => query.kind === kind);
let currentQueriesForKind = dataQueries.filter((query) => query.kind === kind);
if (type == DATA_SOURCE_TYPE.SAMPLE) {
currentQueriesForKind = currentQueriesForKind.filter((query) => query.data_source_id === source.id);
}
let currentNumber = currentQueriesForKind.length + 1;
// eslint-disable-next-line no-constant-condition
while (true) {
const newName = `${kind}${currentNumber}`;
const newName = `${type != DATA_SOURCE_TYPE.SAMPLE ? kind : 'SMPL_query_'}${currentNumber}`;
if (dataQueries.find((query) => query.name === newName) === undefined) {
return newName;
}

View file

@ -40,6 +40,13 @@
.ds-delete-btn {
visibility: visible;
.disabled-button{
background-color: #FFFF;
width: 19px;
height: 19px;
padding: 3px;
}
}
}
@ -54,6 +61,10 @@
border: none;
background: none;
}
.tooltip-sampl-db{
width: 10px;
}
}
.datasources-list-item {

View file

@ -90,7 +90,7 @@ $border-radius: 4px;
.query-pane {
z-index: 101;
height: 350px;
height: 400px;
position: fixed;
left: 48px;
right: 300px;

View file

@ -0,0 +1,33 @@
import React from 'react';
const AddTemplate = ({
fill = '#AEBDEE',
width = '30',
className = '',
viewBox = '0 0 30 30',
secondaryFill = '#3E63DD',
}) => (
<svg
width={width}
className={className}
height={width}
viewBox={viewBox}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M8.65384 0.5H1.92307C0.860991 0.5 0 1.36099 0 2.42307V9.15384C0 10.2159 0.860991 11.0769 1.92307 11.0769H8.65384C9.71593 11.0769 10.5769 10.2159 10.5769 9.15384V2.42307C10.5769 1.36099 9.71593 0.5 8.65384 0.5ZM23.077 0.5H16.3461C15.284 0.5 14.423 1.36099 14.423 2.42307V9.15384C14.423 10.2159 15.284 11.0769 16.3461 11.0769H23.077C24.1389 11.0769 25 10.2159 25 9.15384V2.42307C25 1.36099 24.1389 0.5 23.077 0.5ZM1.92307 14.923H8.65384C9.71593 14.923 10.5769 15.784 10.5769 16.8461V23.577C10.5769 24.6389 9.71593 25.5 8.65384 25.5H1.92307C0.860991 25.5 0 24.6389 0 23.577V16.8461C0 15.784 0.860991 14.923 1.92307 14.923Z"
fill={fill}
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M23.5552 17.282C23.5552 16.5424 22.9556 15.9427 22.2159 15.9427C21.4763 15.9427 20.8767 16.5424 20.8767 17.282V20.3762H17.7826C17.043 20.3762 16.4434 20.9759 16.4434 21.7155C16.4434 22.4551 17.043 23.0548 17.7826 23.0548H20.8767V26.1489C20.8767 26.8885 21.4763 27.4882 22.2159 27.4882C22.9556 27.4882 23.5552 26.8885 23.5552 26.1489V23.0548H26.6495C27.3892 23.0548 27.9888 22.4551 27.9888 21.7155C27.9888 20.9759 27.3892 20.3762 26.6495 20.3762H23.5552V17.282Z"
fill={secondaryFill}
/>
</svg>
);
export default AddTemplate;

View file

@ -0,0 +1,38 @@
import React from 'react';
const InviteCollaborator = ({ fill = '#3E63DD', width = '30', className = '', viewBox = '0 0 30 30' }) => (
<svg
width={width}
className={className}
height={width}
viewBox={viewBox}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
opacity="0.4"
d="M26.0789 8.16301L11.1047 0.680449C8.44143 -0.650378 5.57576 2.08633 6.78509 4.80567L8.96691 9.71179C9.33163 10.5319 9.33163 11.4681 8.96691 12.2882L6.78509 17.1943C5.57576 19.9137 8.44142 22.6504 11.1047 21.3196L26.0789 13.837C28.4181 12.6681 28.4181 9.33192 26.0789 8.16301Z"
fill={fill}
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M8.83268 11C8.83268 10.4477 9.2804 10 9.83268 10H14.4993C15.0516 10 15.4993 10.4477 15.4993 11C15.4993 11.5523 15.0516 12 14.4993 12H9.83268C9.2804 12 8.83268 11.5523 8.83268 11Z"
fill={fill}
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M0.166016 8.33334C0.166016 7.78106 0.613731 7.33334 1.16602 7.33334H3.83268C4.38497 7.33334 4.83268 7.78106 4.83268 8.33334C4.83268 8.88563 4.38497 9.33334 3.83268 9.33334H1.16602C0.613731 9.33334 0.166016 8.88563 0.166016 8.33334Z"
fill={fill}
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M0.166016 13.6667C0.166016 13.1144 0.613731 12.6667 1.16602 12.6667H3.83268C4.38497 12.6667 4.83268 13.1144 4.83268 13.6667C4.83268 14.219 4.38497 14.6667 3.83268 14.6667H1.16602C0.613731 14.6667 0.166016 14.219 0.166016 13.6667Z"
fill={fill}
/>
</svg>
);
export default InviteCollaborator;

View file

@ -115,11 +115,15 @@ import Telescope from './Telescope.jsx';
import Unlock from './Unlock.jsx';
import DragHandle from './DragHandle.jsx';
import Lock from './Lock.jsx';
import AddTemplate from './AddTemplate.jsx';
import InviteCollaborator from './InviteCollabarator.jsx';
const Icon = (props) => {
switch (props.name) {
case 'addrectangle':
return <AddRectangle {...props} />;
case 'addtemplate':
return <AddTemplate {...props} />;
case 'apps':
return <Apps {...props} />;
case 'archive':
@ -245,6 +249,8 @@ const Icon = (props) => {
return <NotificationSilent {...props} />;
case 'notificationunread':
return <NotificationUnread {...props} />;
case 'invitecollab':
return <InviteCollaborator {...props} />;
case 'page':
return <Page {...props} />;
case 'pageAdd':

View file

@ -0,0 +1,25 @@
import React from 'react';
const InformationCircle = ({
fill = '#28303F',
innerFill = '#3E63DD',
width = '25',
className = '',
viewBox = '0 0 25 25',
}) => (
<svg width={width} height={width} viewBox={viewBox} fill="none" xmlns="http://www.w3.org/2000/svg">
<path
opacity="0.4"
d="M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12Z"
fill={fill}
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M12 7.25C12.4142 7.25 12.75 7.58579 12.75 8V9C12.75 9.41421 12.4142 9.75 12 9.75C11.5858 9.75 11.25 9.41421 11.25 9V8C11.25 7.58579 11.5858 7.25 12 7.25ZM12 10.75C12.4142 10.75 12.75 11.0858 12.75 11.5V16C12.75 16.4142 12.4142 16.75 12 16.75C11.5858 16.75 11.25 16.4142 11.25 16V11.5C11.25 11.0858 11.5858 10.75 12 10.75Z"
fill={innerFill}
/>
</svg>
);
export default InformationCircle;

View file

@ -0,0 +1,21 @@
import React from 'react';
const Open = ({ fill = '#3E63DD', width = '11', height = '10', className = '', viewBox = '0 0 11 10' }) => (
<svg
width={width}
height={height}
viewBox={viewBox}
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M9.88378 0.0644531L6.88379 0.0644536C6.67668 0.0644537 6.50879 0.232347 6.50879 0.439453C6.50879 0.64656 6.67668 0.814454 6.88379 0.814454L8.97845 0.814453L4.11862 5.67429C3.97218 5.82073 3.97218 6.05817 4.11862 6.20462C4.26507 6.35106 4.50251 6.35106 4.64895 6.20462L9.50879 1.34478L9.50879 3.43945C9.50879 3.64656 9.67668 3.81445 9.88379 3.81445C10.0909 3.81445 10.2588 3.64656 10.2588 3.43945L10.2588 0.439453C10.2588 0.232346 10.0909 0.0644531 9.88378 0.0644531ZM2.88379 1.06445C1.57211 1.06445 0.508789 2.12778 0.508789 3.43945V7.43945C0.508789 8.75113 1.57211 9.81445 2.88379 9.81445H6.88379C8.19547 9.81445 9.25879 8.75113 9.25879 7.43945V4.93945C9.25879 4.73235 9.0909 4.56445 8.88379 4.56445C8.67668 4.56445 8.50879 4.73235 8.50879 4.93945V7.43945C8.50879 8.33692 7.78125 9.06445 6.88379 9.06445H2.88379C1.98633 9.06445 1.25879 8.33692 1.25879 7.43945V3.43945C1.25879 2.54199 1.98633 1.81445 2.88379 1.81445H5.38379C5.5909 1.81445 5.75879 1.64656 5.75879 1.43945C5.75879 1.23235 5.5909 1.06445 5.38379 1.06445H2.88379Z"
fill={fill}
/>
</svg>
);
export default Open;

View file

@ -0,0 +1,24 @@
import React from 'react';
const TooljetIcon = ({ fill = '#889096', width = '25', className = '', viewBox = '0 0 20 20', style }) => (
<svg
width={width}
height={width}
viewBox={viewBox}
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
style={style}
>
<path
d="M6.89922 15.0499C6.89922 14.1499 6.89922 13.0999 6.89922 12.2C6.89922 12.05 6.89922 11.9 6.74922 11.75C5.69922 10.7 4.79922 9.79995 3.74922 8.74995C3.59922 8.59995 3.59922 8.59995 3.44922 8.59995C2.54922 8.59995 1.49922 8.59995 0.599219 8.59995H0.449219C0.749219 7.84995 1.04922 7.09995 1.64922 6.49995C2.09922 5.89995 2.54922 5.59995 3.29922 5.44995C3.59922 5.44995 3.74922 5.44995 4.04922 5.44995C4.49922 5.44995 4.94922 5.44995 5.24922 5.44995C5.39922 5.44995 5.39922 5.44995 5.54922 5.29995C6.74922 3.79995 8.24922 2.59995 10.1992 1.69995C11.2492 1.24995 12.2992 0.949951 13.4992 0.949951C13.9492 0.949951 14.3992 0.949951 14.8492 0.949951C14.8492 1.54995 14.8492 2.14995 14.8492 2.89995C14.8492 3.94995 14.3992 4.99995 13.9492 5.89995C13.1992 7.54995 11.9992 8.89995 10.6492 9.94995C10.4992 9.94995 10.4992 10.0999 10.4992 10.25C10.4992 10.7 10.4992 11.15 10.4992 11.75C10.4992 12.5 10.1992 13.25 9.59922 13.7C9.29922 14 8.99922 14.1499 8.54922 14.2999C7.79922 14.5999 7.34922 14.7499 6.89922 15.0499ZM11.2492 5.29995C11.2492 4.69995 10.7992 4.24995 10.1992 4.24995C9.59922 4.24995 8.99922 4.69995 8.99922 5.29995C8.99922 5.89995 9.59922 6.49995 10.1992 6.49995C10.7992 6.49995 11.2492 5.89995 11.2492 5.29995Z"
fill={fill}
/>
<path
d="M4.50078 12.8C3.60078 13.85 2.40078 14.3 1.05078 14.6C1.20078 13.1 1.50078 11.9 2.70078 11C3.30078 11.6 3.90078 12.2 4.50078 12.8Z"
fill={fill}
/>
</svg>
);
export default TooljetIcon;

View file

@ -160,6 +160,9 @@ import Uppercase from './Uppercase.jsx';
import Lowercase from './Lowercase.jsx';
import Capitalize from './Capitalize.jsx';
import Oblique from './Oblique.jsx';
import InformationCircle from './InformationCircle.jsx';
import Open from './Open.jsx';
import TooljetIcon from './TooljetIcon.jsx';
import TriangleUpCenter from './TriangleUpCenter.jsx';
import TriangleDownCenter from './TriangleDownCenter.jsx';
@ -293,6 +296,8 @@ const Icon = (props) => {
return <InnerJoinIcon {...props} />;
case 'inrectangle':
return <InRectangle {...props} />;
case 'informationcircle':
return <InformationCircle {...props} />;
case 'interactive':
return <Interactive {...props} />;
case 'italic':
@ -339,6 +344,8 @@ const Icon = (props) => {
return <NotificationUnread {...props} />;
case 'options':
return <Options {...props} />;
case 'open':
return <Open {...props} />;
case 'page':
return <Page {...props} />;
case 'pageAdd':
@ -411,6 +418,8 @@ const Icon = (props) => {
return <Table {...props} />;
case 'tick':
return <Tick {...props} />;
case 'tooljet':
return <TooljetIcon {...props} />;
case 'trash':
return <Trash {...props} />;
case 'uparrow':

View file

@ -53,4 +53,4 @@
"prepare": "husky install",
"update-version": "node update-version.js"
}
}
}

View file

@ -1 +1 @@
2.38.0
2.39.0

View file

@ -0,0 +1,20 @@
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
export class AlterTypeColumnOfDataSourceTable1710321318372 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.changeColumn(
'data_sources',
'type',
new TableColumn({
name: 'type',
type: 'enum',
enumName: 'type',
enum: ['static', 'default', 'sample'],
default: `'default'`,
isNullable: false,
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {}
}

View file

@ -15538,4 +15538,4 @@
}
}
}
}
}

View file

@ -4,7 +4,6 @@ import * as fs from 'fs';
import { ExecFileSyncOptions, execFileSync } from 'child_process';
import { buildAndValidateDatabaseConfig } from './database-config-utils';
import { isEmpty } from 'lodash';
async function createDatabaseFromFile(envPath: string): Promise<void> {
const result = dotenv.config({ path: envPath });
@ -100,6 +99,31 @@ async function createTooljetDb(envVars, dbName): Promise<void> {
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function createSampleDb(envVars, dbName): Promise<void> {
if (isEmpty(dbName)) {
throw new Error('Database name cannot be empty');
}
try {
executeCreateDb(
envVars.SAMPLE_PG_DB_HOST,
envVars.SAMPLE_PG_DB_PORT,
envVars.SAMPLE_PG_DB_USER,
envVars.SAMPLE_PG_DB_PASS,
dbName
);
} catch (error) {
if (error.message.includes(`database "${dbName}" already exists`)) {
console.log(
`Already present Sample database\n${dbName}\n HOST: ${envVars.SAMPLE_PG_DB_HOST}\n PORT: ${envVars.SAMPLE_PG_DB_PORT}`
);
} else {
throw error;
}
}
}
try {
checkCommandAvailable('createdb');
const nodeEnvPath = path.resolve(process.cwd(), process.env.NODE_ENV === 'test' ? '../.env.test' : '../.env');

View file

@ -49,6 +49,11 @@ function buildDbConfigFromDatabaseURL(data): any {
TOOLJET_DB_PORT: TJDBconfig?.port || data.TOOLJET_DB_PORT,
TOOLJET_DB_PASS: TJDBconfig?.password || data.TOOLJET_DB_PASS,
TOOLJET_DB_USER: TJDBconfig?.user || data.TOOLJET_DB_USER,
SAMPLE_DB: data.SAMPLE_DB || 'sample_db',
SAMPLE_PG_DB_HOST: config?.host || data.PG_HOST || data.SAMPLE_PG_DB_HOST,
SAMPLE_PG_DB_PORT: config?.port || data.PG_PORT || data.SAMPLE_PG_DB_PORT,
SAMPLE_PG_DB_USER: config?.user || data.PG_USER || data.SAMPLE_PG_DB_USER,
SAMPLE_PG_DB_PASS: config?.password || data.PG_PASS || data.SAMPLE_PG_DB_PASS,
});
if (error) {
@ -100,6 +105,13 @@ function validateDatabaseConfig(dbConfig: any): Joi.ValidationResult {
TOOLJET_DB: Joi.string().default('tooljet_db'),
TOOLJET_DB_OWNER: Joi.string().default('true'),
}),
...{
SAMPLE_PG_DB_HOST: Joi.string().default('localhost'),
SAMPLE_PG_DB_PORT: Joi.number().positive().default(5432),
SAMPLE_PG_DB_PASS: Joi.string().default(''),
SAMPLE_PG_DB_USER: Joi.string().required(),
SAMPLE_DB: Joi.string().default('sample_db'),
},
})
.unknown();
@ -122,6 +134,12 @@ export function buildAndValidateDatabaseConfig(): Joi.ValidationResult {
TOOLJET_DB_PASS: config.TOOLJET_DB_PASS,
TOOLJET_DB_USER: config.TOOLJET_DB_USER,
TOOLJET_DB_OWNER: config.TOOLJET_DB_OWNER,
ENABLE_SAMPLE_PG_DB: config.ENABLE_SAMPLE_PG_DB,
SAMPLE_DB: config.SAMPLE_DB || 'sample_db',
SAMPLE_PG_DB_HOST: config.SAMPLE_PG_DB_HOST || config.PG_HOST,
SAMPLE_PG_DB_PORT: config.SAMPLE_PG_DB_PORT || config.PG_PORT,
SAMPLE_PG_DB_USER: config.SAMPLE_PG_DB_USER || config.PG_USER,
SAMPLE_PG_DB_PASS: config.SAMPLE_PG_DB_PASS || config.PG_PASS,
};
return validateDatabaseConfig(dbConfig);

View file

@ -0,0 +1,162 @@
import * as fs from 'fs';
import * as path from 'path';
import { Client } from 'pg';
import { buildAndValidateDatabaseConfig } from './database-config-utils';
// PostgreSQL connection configuration
function createPGconnection(envVars): Client {
return new Client({
user: envVars.SAMPLE_PG_DB_USER,
host: envVars.SAMPLE_PG_DB_HOST,
database: envVars.SAMPLE_DB,
password: envVars.SAMPLE_PG_DB_PASS,
port: envVars.SAMPLE_PG_DB_PORT,
});
}
const folderPath = path.join(__dirname, '../src/assets/sample-data-json-files');
// Replace 'your_table_name' with the desired table name in your PostgreSQL database
// Read Excel file
async function connectToPostgreSQL(client) {
return new Promise<void>((resolve, reject) => {
client.connect((err) => {
if (err) {
console.error('Error connecting to PostgreSQL:', err);
reject(err); // Reject the promise if there's an error
} else {
console.log('Connected to PostgreSQL');
resolve(); // Resolve the promise if connected successfully
}
});
});
}
export async function populateSampleData(envVars) {
const client = createPGconnection(envVars);
try {
//Checking postgres connection
await connectToPostgreSQL(client);
// Read files from the folder asynchronously
const files = await fs.promises.readdir(folderPath);
for (const file of files) {
if (file.startsWith('.')) continue; // Skip hidden files
const filePath = path.join(folderPath, file);
// Read file contents asynchronously
const jsonString = await fs.promises.readFile(filePath, 'utf-8');
const parsedData = JSON.parse(jsonString);
const tableName = file
.replace(/\.json$/, '')
.replace(/[^\w]/g, '')
.replace(/\s+/g, '_')
.toLowerCase();
// Drop table if it exists
await dropTable(client, tableName);
// Create table query
await createTable(client, tableName, parsedData);
// Insert data into the table
await insertData(client, tableName, parsedData);
console.log(`Data populated for table: ${tableName}`);
}
} catch (error) {
console.error('Error populating sample data:', error);
} finally {
// Close the database connection
await client.end();
}
}
// Drop PostgreSQL table if it exists
async function dropTable(client: Client, tableName) {
const dropTableQuery = `DROP TABLE IF EXISTS ${tableName};`;
await client.query(dropTableQuery);
}
// Create PostgreSQL table
async function createTable(client, tableName, parsedData) {
let createTableQuery = `CREATE TABLE IF NOT EXISTS ${tableName} (`;
Object.keys(parsedData[0]).forEach((header) => {
const columnName = header;
const columnValues = parsedData.map((row) => row[header]);
let dataType;
// Analyze the data to determine the appropriate data type
if (columnValues.every((value) => typeof value === 'string' && (value === null || value.length <= 255))) {
dataType = 'VARCHAR';
} else if (columnValues.every((value) => typeof value === 'number' || value === null)) {
dataType = 'NUMERIC';
} else if (columnValues.every((value) => value instanceof Date || value === null || !isNaN(Date.parse(value)))) {
dataType = 'DATE';
} else {
dataType = 'VARCHAR(255)'; // Default to VARCHAR if data type is uncertain
}
createTableQuery += `${columnName
.trim()
.replace(/[^\w]/g, '_')
.replace(/\s+/g, '_')
.replace(/_+/g, '_')
.replace(/^_+|_+$/g, '')
.toLowerCase()} ${dataType}, `;
});
createTableQuery = createTableQuery.slice(0, -2); // Remove the last comma
createTableQuery += ' );';
console.log('creating table');
await client.query(createTableQuery);
}
export async function runPopulateScript() {
const { value: envVars, error } = buildAndValidateDatabaseConfig();
if (error) return;
populateSampleData(envVars);
}
// Insert data into PostgreSQL table
async function insertData(client: Client, tableName: string, data) {
const keys = Object.keys(data[0]);
const columns = keys
.map((key) =>
key
.trim()
.replace(/[^\w]/g, '_')
.replace(/\s+/g, '_')
.replace(/_+/g, '_')
.replace(/^_+|_+$/g, '')
.toLowerCase()
)
.join(', ');
const rows = data.map((row) => keys.map((key) => row[key]));
const insertQuery = `INSERT INTO ${tableName} (${columns}) VALUES ${rows
.map(
(row) =>
`(${row
.map((key, index) => {
if (key === null || key === undefined) {
return 'NULL';
} else if (typeof key === 'string') {
return `'${key.replace(/['",]/g, ' ')}'`; // Replace special characters only for strings
} else if (typeof key === 'number' && !Number.isInteger(key)) {
// For decimal numbers, convert them to string with proper decimal separator
return key.toString().replace(',', '.');
}
return key; // For non-string, non-decimal values, leave them unchanged
})
.join(', ')})`
)
.join(', ')}`;
await client.query(insertQuery);
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -26,6 +26,12 @@ export class LibraryAppsController {
return newApp;
}
@Get('sample-app')
@UseGuards(JwtAuthGuard)
async createSampleApp(@User() user) {
return await this.libraryAppCreationService.createSampleApp(user);
}
@Get()
@UseGuards(JwtAuthGuard)
async index() {

View file

@ -34,7 +34,7 @@ export class DataSource extends BaseEntity {
type: 'enum',
enumName: 'type',
name: 'type',
enum: [DataSourceTypes.STATIC, DataSourceTypes.DEFAULT],
enum: [DataSourceTypes.STATIC, DataSourceTypes.DEFAULT, DataSourceTypes.SAMPLE],
default: DataSourceTypes.DEFAULT,
})
type: string;

View file

@ -1,6 +1,7 @@
export enum DataSourceTypes {
STATIC = 'static',
DEFAULT = 'default',
SAMPLE = 'sample',
}
export enum DataSourceScopes {

View file

@ -260,16 +260,16 @@ export function isVersionGreaterThanOrEqual(version1: string, version2: string)
return true;
}
export const getMaxCopyNumber = (existNameList) => {
export const getMaxCopyNumber = (existNameList, splitChar = '_') => {
if (existNameList.length == 0) return '';
const filteredNames = existNameList.filter((name) => {
const parts = name.group.split('_');
const parts = name.split(splitChar);
return !isNaN(parseInt(parts[parts.length - 1]));
});
// Extracting numbers from the filtered names
const numbers = filteredNames.map((name) => {
const parts = name.group.split('_');
const parts = name.split(splitChar);
return parseInt(parts[parts.length - 1]);
});

View file

@ -17,11 +17,14 @@ import { Plugin } from 'src/entities/plugin.entity';
import { PluginsHelper } from 'src/helpers/plugins.helper';
import { AppEnvironmentService } from '@services/app_environments.service';
import { ImportExportResourcesModule } from '../import_export_resources/import_export_resources.module';
import { AppsService } from '@services/apps.service';
import { AppVersion } from 'src/entities/app_version.entity';
import { AppUser } from 'src/entities/app_user.entity';
import { TooljetDbModule } from '../tooljet_db/tooljet_db.module';
@Module({
imports: [
TypeOrmModule.forFeature([App, Credential, File, Plugin, DataSource]),
TypeOrmModule.forFeature([App, Credential, File, Plugin, DataSource, AppVersion, AppUser]),
CaslModule,
ImportExportResourcesModule,
TooljetDbModule,
@ -36,6 +39,7 @@ import { TooljetDbModule } from '../tooljet_db/tooljet_db.module';
PluginsService,
PluginsHelper,
AppEnvironmentService,
AppsService,
],
controllers: [LibraryAppsController],
})

View file

@ -53,6 +53,7 @@ import { TooljetDbModule } from '../tooljet_db/tooljet_db.module';
Credential,
Plugin,
Metadata,
DataSource,
]),
CaslModule,
MetaModule,

View file

@ -0,0 +1,12 @@
import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { runPopulateScript } from 'scripts/populate-sample-db';
@Injectable()
export class SampleDBScheduler {
@Cron(CronExpression.EVERY_DAY_AT_1AM)
async handleCron() {
console.log('starting job to populate sample data at ', new Date().toISOString());
runPopulateScript();
}
}

View file

@ -9,7 +9,7 @@ import { DataSource } from 'src/entities/data_source.entity';
import { DataSourceOptions } from 'src/entities/data_source_options.entity';
import { GroupPermission } from 'src/entities/group_permission.entity';
import { User } from 'src/entities/user.entity';
import { EntityManager } from 'typeorm';
import { EntityManager, In } from 'typeorm';
import { DataSourcesService } from './data_sources.service';
import {
dbTransactionWrap,
@ -1078,7 +1078,7 @@ export class AppImportExportService {
where: {
name: dataSource.name,
kind: dataSource.kind,
type: DataSourceTypes.DEFAULT,
type: In([DataSourceTypes.DEFAULT, DataSourceTypes.SAMPLE]),
scope: 'global',
organizationId: user.organizationId,
},

View file

@ -1,7 +1,7 @@
import { BadRequestException, Injectable, NotAcceptableException, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { App } from 'src/entities/app.entity';
import { EntityManager, MoreThan, Repository } from 'typeorm';
import { EntityManager, Like, MoreThan, Repository } from 'typeorm';
import { User } from 'src/entities/user.entity';
import { AppUser } from 'src/entities/app_user.entity';
import { AppVersion } from 'src/entities/app_version.entity';
@ -233,6 +233,12 @@ export class AppsService {
return await viewableAppsQb.getMany();
}
async findAll(organizationId: string, searchParam): Promise<App[]> {
return await this.appsRepository.find({
where: { organizationId, ...(searchParam.name && { name: Like(`${searchParam.name} %`) }) },
});
}
async update(appId: string, appUpdateDto: AppUpdateDto, manager?: EntityManager) {
const currentVersionId = appUpdateDto.current_version_id;
const isPublic = appUpdateDto.is_public;

View file

@ -293,7 +293,7 @@ export class GroupPermissionsService {
.getMany();
let newName = `${groupToDuplicate.group}_copy`;
const number = getMaxCopyNumber(existNameList);
const number = getMaxCopyNumber(existNameList.map((group) => group.group));
if (number) newName = `${groupToDuplicate.group}_copy_${number}`;
await dbTransactionWrap(async (manager: EntityManager) => {
newGroup = manager.create(GroupPermission, {

View file

@ -6,17 +6,34 @@ import { ImportExportResourcesService } from './import_export_resources.service'
import { ImportResourcesDto } from '@dto/import-resources.dto';
import { AppImportExportService } from './app_import_export.service';
import { isVersionGreaterThanOrEqual } from 'src/helpers/utils.helper';
import { AppsService } from './apps.service';
import { getMaxCopyNumber } from 'src/helpers/utils.helper';
@Injectable()
export class LibraryAppCreationService {
constructor(
private readonly importExportResourcesService: ImportExportResourcesService,
private readonly appImportExportService: AppImportExportService,
private readonly appsService: AppsService,
private readonly logger: Logger
) {}
async perform(currentUser: User, identifier: string, appName: string) {
const templateDefinition = this.findTemplateDefinition(identifier);
return this.importTemplate(currentUser, templateDefinition, appName);
}
async createSampleApp(currentUser: User) {
let name = 'Sample app ';
const allSampleApps = await this.appsService.findAll(currentUser?.organizationId, { name });
const existNameList = allSampleApps.map((app) => app.name);
const maxNumber = getMaxCopyNumber(existNameList, ' ');
name = `${name} ${maxNumber}`;
const sampleAppDef = JSON.parse(readFileSync(`templates/sample_app_def.json`, 'utf-8'));
return this.importTemplate(currentUser, sampleAppDef, name);
}
async importTemplate(currentUser: User, templateDefinition: any, appName: string) {
const importDto = new ImportResourcesDto();
importDto.organization_id = currentUser.organizationId;
importDto.app = templateDefinition.app || templateDefinition.appV2;

View file

@ -18,6 +18,7 @@ import { EmailService } from './email.service';
import { EncryptionService } from './encryption.service';
import { GroupPermissionsService } from './group_permissions.service';
import { OrganizationUsersService } from './organization_users.service';
import { DataSourcesService } from './data_sources.service';
import { UsersService } from './users.service';
import { InviteNewUserDto } from '@dto/invite-new-user.dto';
import { ConfigService } from '@nestjs/config';
@ -33,6 +34,10 @@ import { Response } from 'express';
import { AppEnvironmentService } from './app_environments.service';
import { DataBaseConstraints } from 'src/helpers/db_constraints.constants';
import { OrganizationUpdateDto } from '@dto/organization.dto';
import { DataSourceScopes, DataSourceTypes } from 'src/helpers/data_source.constants';
import { DataSource } from 'src/entities/data_source.entity';
import { AppEnvironment } from 'src/entities/app_environments.entity';
import { DataSourceOptions } from 'src/entities/data_source_options.entity';
const MAX_ROW_COUNT = 500;
@ -75,6 +80,7 @@ export class OrganizationsService {
@InjectRepository(SSOConfigs)
private ssoConfigRepository: Repository<SSOConfigs>,
private usersService: UsersService,
private dataSourceService: DataSourcesService,
private organizationUserService: OrganizationUsersService,
private groupPermissionService: GroupPermissionsService,
private appEnvironmentService: AppEnvironmentService,
@ -831,4 +837,63 @@ export class OrganizationsService {
if (result) throw new ConflictException(`${name ? 'Name' : 'Slug'} must be unique`);
return;
}
async createSampleDB(organizationId, manager: EntityManager) {
const config = {
name: 'Sample Data Source',
kind: 'postgresql',
type: DataSourceTypes.SAMPLE,
scope: DataSourceScopes.GLOBAL,
organizationId,
};
const options = [
{
key: 'host',
value: this.configService.get<string>('PG_HOST'),
encrypted: true,
},
{
key: 'port',
value: this.configService.get<string>('PG_PORT'),
encrypted: true,
},
{
key: 'database',
value: 'sample_db',
},
{
key: 'username',
value: this.configService.get<string>('PG_USER'),
encrypted: true,
},
{
key: 'password',
value: this.configService.get<string>('PG_PASS'),
encrypted: true,
},
{
key: 'ssl_enabled',
value: false,
encrypted: true,
},
{ key: 'ssl_certificate', value: 'none', encrypted: false },
];
const dataSource = manager.create(DataSource, config);
await manager.save(dataSource);
const allEnvs: AppEnvironment[] = await this.appEnvironmentService.getAll(organizationId, manager);
await Promise.all(
allEnvs?.map(async (env) => {
const parsedOptions = await this.dataSourceService.parseOptionsForCreate(options);
await manager.save(
manager.create(DataSourceOptions, {
environmentId: env.id,
dataSourceId: dataSource.id,
options: parsedOptions,
})
);
})
);
}
}

File diff suppressed because it is too large Load diff