diff --git a/frontend/ee b/frontend/ee
index 387bb6e55b..c5513e3034 160000
--- a/frontend/ee
+++ b/frontend/ee
@@ -1 +1 @@
-Subproject commit 387bb6e55bc6a7600b7125bb9e22ca2b17dfe65d
+Subproject commit c5513e303482c45289a974bb1c918ae22be6ec9c
diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenu.jsx b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenu.jsx
index d4b8573c7d..68a54c2ca4 100644
--- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenu.jsx
+++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenu.jsx
@@ -12,11 +12,12 @@ import './style.scss';
import { SortableTree } from './Tree/SortableTree';
import { PageGroupMenu } from './AddPageButton';
import { PageHandlerMenu } from './PageHandlerMenu.jsx';
+import AppPermissionsModal from '@/modules/Appbuilder/components/AppPermissionsModal';
import { EditModal } from './EditModal';
import { SettingsModal } from './SettingsModal';
import { DeletePageConfirmationModal } from './DeletePageConfirmationModal';
import SolidIcon from '@/_ui/Icon/SolidIcons';
-import PagePermission from './PagePermission';
+import { appPermissionService } from '@/_services';
export const PageMenu = ({ darkMode, switchPage, pinned, setPinned }) => {
const showAddNewPageInput = useStore((state) => state.showAddNewPageInput);
@@ -27,6 +28,12 @@ export const PageMenu = ({ darkMode, switchPage, pinned, setPinned }) => {
const shouldFreeze = useStore((state) => state.getShouldFreeze());
const enableReleasedVersionPopupState = useStore((state) => state.enableReleasedVersionPopupState);
const closePageEditPopover = useStore((state) => state.closePageEditPopover);
+ const editingPageId = useStore((state) => state.editingPage?.id);
+ const editingPageName = useStore((state) => state.editingPage?.name);
+ const showPagePermissionModal = useStore((state) => state.showPagePermissionModal);
+ const togglePagePermissionModal = useStore((state) => state.togglePagePermissionModal);
+ const updatePageWithPermissions = useStore((state) => state.updatePageWithPermissions);
+
useEffect(() => {
return () => {
closePageEditPopover();
@@ -95,7 +102,21 @@ export const PageMenu = ({ darkMode, switchPage, pinned, setPinned }) => {
>
- {isLicensed ?
: <>>}
+ {isLicensed && (
+
appPermissionService.getPagePermission(appId, id)}
+ createPermission={(id, appId, body) => appPermissionService.createPagePermission(appId, id, body)}
+ updatePermission={(id, appId, body) => appPermissionService.updatePagePermission(appId, id, body)}
+ deletePermission={(id, appId) => appPermissionService.deletePagePermission(appId, id)}
+ onSuccess={(data) => updatePageWithPermissions(editingPageId, data)}
+ />
+ )}
diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx
deleted file mode 100644
index 160a941ebe..0000000000
--- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx
+++ /dev/null
@@ -1,509 +0,0 @@
-import React, { useEffect, useMemo, useState } from 'react';
-import { components } from 'react-select';
-import ModalBase from '@/_ui/Modal';
-import Select from '@/_ui/Select';
-import SolidIcon from '@/_ui/Icon/SolidIcons';
-import useStore from '@/AppBuilder/_stores/store';
-import { appPermissionService } from '@/_services';
-import { ConfirmDialog } from '@/_components';
-import toast from 'react-hot-toast';
-import Spinner from '@/_ui/Spinner';
-import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
-
-const PERMISSION_TYPES = {
- single: 'SINGLE',
- group: 'GROUP',
- all: 'ALL',
-};
-
-export default function PagePermission({ darkMode }) {
- const { moduleId } = useModuleContext();
- const showPagePermissionModal = useStore((state) => state.showPagePermissionModal);
- const togglePagePermissionModal = useStore((state) => state.togglePagePermissionModal);
- const editingPage = useStore((state) => state.editingPage);
- const appId = useStore((state) => state.appStore.modules[moduleId].app.appId);
- const selectedUserGroups = useStore((state) => state.selectedUserGroups);
- const setSelectedUserGroups = useStore((state) => state.setSelectedUserGroups);
- const selectedUsers = useStore((state) => state.selectedUsers);
- const setSelectedUsers = useStore((state) => state.setSelectedUsers);
- const pagePermission = useStore((state) => state.pagePermission);
- const setPagePermission = useStore((state) => state.setPagePermission);
- const updatePageWithPermissions = useStore((state) => state.updatePageWithPermissions);
-
- const [pagePermissionType, setPagePermissionType] = useState('all');
- const [showUserGroupSelect, toggleUserGroupSelect] = useState(false);
- const [showUsersSelect, toggleUsersSelect] = useState(false);
- const [showConfirmDelete, setShowConfirmDelete] = useState(false);
- const [isLoading, setIsLoading] = useState(false);
- const [isPermissionsLoading, setPermissionsLoading] = useState(true);
- const [initialSelectedGroups, setInitialSelectedGroups] = useState([]);
- const [initialSelectedUsers, setInitialSelectedUsers] = useState([]);
- const [initalPagePermissionType, setInitialPagePermissionType] = useState('all');
-
- useEffect(() => {
- if (!showPagePermissionModal) return;
- const fetchPagePermission = () => {
- appPermissionService.getPagePermission(appId, editingPage?.id).then((data) => {
- if (data) {
- if (data[0] && data[0]?.type === PERMISSION_TYPES.group) {
- const groups =
- data[0]?.groups?.map((user) => ({
- label: user?.permissionGroup?.name,
- value: user?.permissionGroup?.id,
- count: user?.permissionGroup?.count,
- })) ?? [];
- setPagePermissionType(data[0]?.type?.toLowerCase());
- setInitialPagePermissionType(data[0]?.type?.toLowerCase());
- setPagePermission(data);
- toggleUserGroupSelect(true);
- setInitialSelectedGroups(groups);
- data?.length && setSelectedUserGroups(groups);
- } else if (data[0] && data[0]?.type === PERMISSION_TYPES.single) {
- const users =
- data[0]?.users?.map(({ user }) => {
- const firstName = user.firstName || '';
- const lastName = user.lastName || '';
- return {
- value: user.id,
- label: `${firstName} ${lastName}`.trim(),
- email: user.email,
- initials: `${firstName[0] || ''}${lastName[0] || ''}`.toUpperCase(),
- };
- }) ?? [];
- setPagePermissionType(data[0]?.type?.toLowerCase());
- setInitialPagePermissionType(data[0]?.type?.toLowerCase());
- setPagePermission(data);
- toggleUsersSelect(true);
- setInitialSelectedUsers(users);
- data?.length && setSelectedUsers(users);
- }
- }
- setPermissionsLoading(false);
- });
- };
- fetchPagePermission();
- }, [showPagePermissionModal]);
-
- const isSelectionUnchanged = useMemo(() => {
- if (pagePermissionType === 'group') {
- if (!selectedUserGroups.length) return true;
- const current = selectedUserGroups
- .map((g) => g.value)
- .sort()
- .join(',');
- const initial = initialSelectedGroups
- .map((g) => g.value)
- .sort()
- .join(',');
- return current === initial;
- } else if (pagePermissionType === 'single') {
- if (!selectedUsers.length) return true;
- const current = selectedUsers
- .map((u) => u.value)
- .sort()
- .join(',');
- const initial = initialSelectedUsers
- .map((u) => u.value)
- .sort()
- .join(',');
- return current === initial;
- } else {
- if (!pagePermission?.length) {
- return true;
- } else {
- return initalPagePermissionType == pagePermissionType;
- }
- }
- }, [
- pagePermissionType,
- selectedUserGroups,
- initialSelectedGroups,
- selectedUsers,
- initialSelectedUsers,
- initalPagePermissionType,
- ]);
-
- const permissionTypeOptions = useMemo(
- () => [
- {
- label: 'All users with access to the app',
- value: 'all',
- icon: 'globe',
- },
- {
- label: 'Users',
- value: 'single',
- icon: 'user',
- },
- {
- label: 'User groups',
- value: 'group',
- icon: 'usergroup',
- },
- ],
- []
- );
- const handlePermissionTypeChange = (value) => {
- switch (value) {
- case 'group': {
- toggleUserGroupSelect(true);
- toggleUsersSelect(false);
- setPagePermissionType('group');
- break;
- }
- case 'single': {
- toggleUsersSelect(true);
- toggleUserGroupSelect(false);
- setPagePermissionType('single');
- break;
- }
- case 'all': {
- toggleUsersSelect(false);
- toggleUserGroupSelect(false);
- setPagePermissionType('all');
- }
- }
- };
-
- const handlePagePermissionModalClose = () => {
- togglePagePermissionModal(false);
- toggleUserGroupSelect(false);
- toggleUsersSelect(false);
- setPagePermissionType('all');
- setPagePermission(null);
- setSelectedUsers([]);
- setSelectedUserGroups([]);
- setInitialSelectedGroups([]);
- setInitialSelectedUsers([]);
- };
-
- const createPagePermission = () => {
- const body = {
- pageId: editingPage?.id,
- type: PERMISSION_TYPES[pagePermissionType],
- ...(pagePermissionType === 'group'
- ? { groups: selectedUserGroups.map((group) => group?.value) }
- : { users: selectedUsers.map((user) => user?.value) }),
- };
- setIsLoading(true);
- appPermissionService
- .createPagePermission(appId, editingPage?.id, body)
- .then((data) => {
- toast.success('Permission successfully created!', {
- className: 'text-nowrap w-auto mw-100',
- });
- updatePageWithPermissions(editingPage?.id, data);
- })
- .catch(() => {
- toast.error('Permission could not be created. Please try again!', {
- className: 'text-nowrap w-auto mw-100',
- });
- })
- .finally(() => {
- setIsLoading(false);
- handlePagePermissionModalClose();
- });
- };
-
- const updatePagePermission = () => {
- const body = {
- pageId: editingPage?.id,
- type: PERMISSION_TYPES[pagePermissionType],
- ...(pagePermissionType === 'group'
- ? { groups: selectedUserGroups.map((group) => group?.value) }
- : { users: selectedUsers.map((user) => user?.value) }),
- };
- setIsLoading(true);
- appPermissionService
- .updatePagePermission(appId, editingPage?.id, body)
- .then((data) => {
- toast.success('Permission successfully updated!', {
- className: 'text-nowrap w-auto mw-100',
- });
- updatePageWithPermissions(editingPage?.id, data);
- })
- .catch(() => {
- toast.error('Permission could not be updated. Please try again!', {
- className: 'text-nowrap w-auto mw-100',
- });
- })
- .finally(() => {
- setIsLoading(false);
- handlePagePermissionModalClose();
- });
- };
-
- const deletePagePermission = () => {
- setIsLoading(true);
- appPermissionService
- .deletePagePermission(appId, editingPage?.id)
- .then((data) => {
- toast.success('Permission successfully deleted!', {
- className: 'text-nowrap w-auto mw-100',
- });
- updatePageWithPermissions(editingPage?.id, []);
- })
- .catch(() => {
- toast.error('Permission could not be deleted. Please try again!', {
- className: 'text-nowrap w-auto mw-100',
- });
- setShowConfirmDelete(false);
- togglePagePermissionModal(true);
- })
- .finally(() => {
- setIsLoading(false);
- setShowConfirmDelete(false);
- });
- };
-
- const renderPermissionTypeOptions = ({ label, icon }) => {
- return (
-
- );
- };
-
- return (
- <>
-
- Page permission
-
- }
- handleConfirm={!pagePermission ? createPagePermission : updatePagePermission}
- show={showPagePermissionModal}
- isLoading={isLoading}
- handleClose={handlePagePermissionModalClose}
- confirmBtnProps={{
- title: pagePermission
- ? 'Save changes'
- : pagePermissionType === 'all'
- ? 'Default permission'
- : 'Create permission',
- disabled: isPermissionsLoading || isSelectionUnchanged,
- tooltipMessage: '',
- leftIcon: pagePermission && 'save',
- className: 'action-btn-page-permission',
- }}
- darkMode={darkMode}
- className="page-permissions-modal"
- >
-
- {isPermissionsLoading ? (
-
-
-
- ) : (
- <>
-
-
-
-
-
-
-
- Only selected users will be allowed to access this page. Read docs to know more.
-
-
-
-
-
-
- {showUserGroupSelect &&
}
- {showUsersSelect &&
}
- >
- )}
-
-
- {showConfirmDelete && (
- deletePagePermission()}
- onCancel={() => setShowConfirmDelete(false)}
- confirmButtonText={'Delete'}
- darkMode={darkMode}
- confirmButtonIcon={'trash'}
- confirmButtonIconWidth="20"
- confirmButtonIconFill={'var(--slate3)'}
- />
- )}
- >
- );
-}
-
-const UserGroupSelect = () => {
- const { moduleId } = useModuleContext();
- const appId = useStore((state) => state.appStore.modules[moduleId].app.appId);
- const selectedUserGroups = useStore((state) => state.selectedUserGroups);
- const setSelectedUserGroups = useStore((state) => state.setSelectedUserGroups);
- const [userGroups, setUserGroups] = useState([]);
- useEffect(() => {
- const fetchUserGroups = () => {
- appPermissionService.getUsers(appId, 'user-groups').then((data) => {
- if (data?.length) {
- const groups = [];
- data.map((group) => {
- groups.push({ value: group.id, label: group.name, count: group.count });
- });
- setUserGroups(groups);
- }
- });
- };
- fetchUserGroups();
- }, []);
-
- const CustomOption = (props) => {
- const { data, isFocused, isSelected } = props;
-
- return (
-
-
-
-
-
{data.label}
-
{data.count} users
-
-
-
- );
- };
-
- return (
-
-
-
- );
-};
-
-const UserSelect = () => {
- const { moduleId } = useModuleContext();
- const appId = useStore((state) => state.appStore.modules[moduleId].app.appId);
- const editingPage = useStore((state) => state.editingPage);
- const selectedUsers = useStore((state) => state.selectedUsers);
- const setSelectedUsers = useStore((state) => state.setSelectedUsers);
- const [users, setUsers] = useState([]);
- useEffect(() => {
- const fetchUsers = () => {
- appPermissionService.getUsers(appId, 'users').then((data) => {
- if (data?.length) {
- const users = [];
- data.map((user) => {
- const firstName = user.firstName || '';
- const lastName = user.lastName || '';
- users.push({
- value: user.id,
- label: `${firstName} ${lastName}`.trim(),
- email: user.email,
- initials: `${firstName[0] || ''}${lastName[0] || ''}`.toUpperCase(),
- });
- });
- setUsers(users);
- }
- });
- };
- fetchUsers();
- }, []);
-
- const CustomOption = (props) => {
- const { data, isFocused, isSelected } = props;
- return (
-
-
-
-
{data.initials}
-
-
{data.label}
-
{data.email}
-
-
-
- );
- };
-
- const selectStyles = {
- option: (base) => ({
- ...base,
- padding: '8px 0px',
- }),
- };
- return (
-
-
-
- );
-};
-
-const CustomMenuList = (props) => {
- const { info } = props.selectProps;
- return (
-
-
- {props.children}
-
- );
-};
diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/style.scss b/frontend/src/AppBuilder/LeftSidebar/PageMenu/style.scss
index bad5bae2af..4510123efc 100644
--- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/style.scss
+++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/style.scss
@@ -289,115 +289,3 @@
}
}
}
-
-.page-permission {
- .info-container {
- display: flex;
- width: auto;
- height: auto;
- padding: 10px 12px 8px 12px;
- border: 1px solid var(--slate5);
- background: var(--slate2);
- border-radius: 6px 6px 6px 6px;
- margin-bottom: 13px;
- margin-top: 0px;
- }
-
- .permission-type-select {
- align-items: center;
-
- .col-auto {
- padding-right: 0px;
- }
- }
-}
-
-.page-permissions-modal {
- #header-actions {
- display: flex;
- align-items: center;
- gap: 12px;
- }
-
- .react-select__option {
- padding: 8px 0px;
-
- input {
- margin-right: 10px;
- }
- }
-
- .react-select__menu-list {
- overflow-y: unset !important;
- }
-
- .user-select-option {
- display: flex;
- align-items: center;
- padding: 8px 12px;
- cursor: pointer;
-
- &.focused {
- background-color: #f3f4f6; // Tailwind's gray-100 vibe
- }
-
- .avatar {
- background-color: var(--slate5); // light gray
- color: var(--slate12); // dark text
- font-weight: 500;
- font-size: 16px;
- width: 36px;
- height: 36px;
- border-radius: 6px;
- display: flex;
- align-items: center;
- justify-content: center;
- margin-right: 12px;
- flex-shrink: 0;
- }
-
- .user-info {
- display: flex;
- flex-direction: column;
-
- .name {
- font-weight: 500;
- font-size: 14px;
- color: var(--slate12);
- }
-
- .email {
- font-size: 12px;
- color: var(--slate10);
- }
- }
-
- .group-info {
- display: flex;
- flex-direction: row;
- gap: 8px;
- align-items: center;
-
- .name {
- font-weight: 400;
- font-size: 14px;
- color: var(--slate12);
- }
-
- .count {
- font-size: 12px;
- color: var(--slate9);
- }
- }
- }
-}
-
-.page-permission {
- .spinner-center {
- min-height: 250px;
- }
-}
-
-.modal-base .modal-footer .action-btn-page-permission svg path {
- fill: var(--indigo1) !important;
-}
\ No newline at end of file
diff --git a/frontend/src/AppBuilder/QueryPanel/QueryCard.jsx b/frontend/src/AppBuilder/QueryPanel/QueryCard.jsx
index dc6f075d33..2d192ec666 100644
--- a/frontend/src/AppBuilder/QueryPanel/QueryCard.jsx
+++ b/frontend/src/AppBuilder/QueryPanel/QueryCard.jsx
@@ -1,10 +1,10 @@
-import React, { useState, useCallback } from 'react';
+import React, { useState } from 'react';
import { Tooltip } from 'react-tooltip';
+import { ToolTip } from '@/_components/ToolTip';
import { updateQuerySuggestions } from '@/_helpers/appUtils';
// import { Confirm } from '../Viewer/Confirm';
import { toast } from 'react-hot-toast';
import { shallow } from 'zustand/shallow';
-import Copy from '@/_ui/Icon/solidIcons/Copy';
import DataSourceIcon from '../QueryManager/Components/DataSourceIcon';
import { isQueryRunnable, decodeEntities } from '@/_helpers/utils';
import { canDeleteDataSource, canReadDataSource, canUpdateDataSource } from '@/_helpers';
@@ -12,13 +12,10 @@ import useStore from '@/AppBuilder/_stores/store';
//TODO: Remove this
import { Confirm } from '@/Editor/Viewer/Confirm';
// TODO: enable delete query confirmation popup
-import { debounce } from 'lodash';
-import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
+import { Button as ButtonComponent } from '@/components/ui/Button/Button.jsx';
+import SolidIcon from '@/_ui/Icon/SolidIcons';
export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
- const { moduleId } = useModuleContext();
- const appId = useStore((state) => state.appStore.modules[moduleId].app.appId);
-
const isQuerySelected = useStore((state) => state.queryPanel.isQuerySelected(dataQuery.id), shallow);
const setSelectedQuery = useStore((state) => state.queryPanel.setSelectedQuery);
const checkExistingQueryName = useStore((state) => state.dataQuery.checkExistingQueryName);
@@ -26,9 +23,16 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
const isDeletingQueryInProcess = useStore((state) => state.dataQuery.isDeletingQueryInProcess);
const renameQuery = useStore((state) => state.dataQuery.renameQuery);
const deleteDataQueries = useStore((state) => state.dataQuery.deleteDataQueries);
- const duplicateQuery = useStore((state) => state.dataQuery.duplicateQuery);
const setPreviewData = useStore((state) => state.queryPanel.setPreviewData);
- const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false);
+ const shouldFreeze = useStore((state) => state.getShouldFreeze());
+
+ const renamingQueryId = useStore((state) => state.queryPanel.renamingQueryId);
+ const deletingQueryId = useStore((state) => state.queryPanel.deletingQueryId);
+ const setRenamingQuery = useStore((state) => state.queryPanel.setRenamingQuery);
+ const deleteDataQuery = useStore((state) => state.queryPanel.deleteDataQuery);
+ const isRenaming = renamingQueryId === dataQuery.id;
+ const isDeleting = deletingQueryId === dataQuery.id;
+
const hasPermissions =
selectedDataSourceScope === 'global'
? canUpdateDataSource(dataQuery?.data_source_id) ||
@@ -36,57 +40,77 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
canDeleteDataSource()
: true;
- const shouldFreeze = useStore((state) => state.getShouldFreeze());
-
- const [renamingQuery, setRenamingQuery] = useState(false);
-
- const deleteDataQuery = (e) => {
- e.stopPropagation();
- setShowDeleteConfirmation(true);
- };
+ const toggleQueryHandlerMenu = useStore((state) => state.queryPanel.toggleQueryHandlerMenu);
+ const featureAccess = useStore((state) => state?.license?.featureAccess, shallow);
+ const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid;
+ const isRestricted = dataQuery.permissions && dataQuery.permissions.length !== 0;
const updateQueryName = (dataQuery, newName) => {
const { name } = dataQuery;
if (name === newName) {
- return setRenamingQuery(false);
+ return setRenamingQuery(null);
}
const isNewQueryNameAlreadyExists = checkExistingQueryName(newName);
if (newName && !isNewQueryNameAlreadyExists) {
renameQuery(dataQuery?.id, newName);
- setRenamingQuery(false);
+ setRenamingQuery(null);
updateQuerySuggestions(name, newName);
} else {
if (isNewQueryNameAlreadyExists) {
toast.error('Query name already exists');
}
- setRenamingQuery(false);
+ setRenamingQuery(null);
}
};
const executeDataQueryDeletion = () => {
- setShowDeleteConfirmation(false);
+ deleteDataQuery(null);
deleteDataQueries(dataQuery?.id);
setPreviewData(null);
};
- // To prevent user clicking from continuous clicks
- const debouncedDuplicateQuery = useCallback(
- debounce((queryId, appId) => {
- duplicateQuery(queryId, appId);
- setPreviewData(null);
- }, 500),
- [duplicateQuery]
- );
+ const getTooltip = () => {
+ const permission = dataQuery.permissions?.[0];
+ if (!permission) return null;
+
+ const users = permission.groups || permission.users || [];
+ if (users.length === 0) return null;
+
+ const isSingle = permission.type === 'SINGLE';
+ const isGroup = permission.type === 'GROUP';
+
+ if (isSingle) {
+ return users.length === 1
+ ? `Access restricted to ${users[0].user.email}`
+ : `Access restricted to ${users.length} users`;
+ }
+
+ if (isGroup) {
+ return users.length === 1
+ ? `Access restricted to ${users[0].permission_group?.name || users[0].permissionGroup?.name} group`
+ : `Access restricted to ${users.length} user groups`;
+ }
+
+ return null;
+ };
return (
<>
{
+ onClick={(e) => {
if (isQuerySelected) return;
- setSelectedQuery(dataQuery?.id);
- setPreviewData(null);
+ const menuBtn = document.getElementById(`query-handler-menu-${dataQuery?.id}`);
+ if (menuBtn.contains(e.target)) {
+ e.stopPropagation();
+ } else {
+ toggleQueryHandlerMenu(false);
+ }
+ setTimeout(() => {
+ setSelectedQuery(dataQuery?.id);
+ setPreviewData(null);
+ }, 0);
}}
role="button"
>
@@ -94,7 +118,7 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
- {renamingQuery ? (
+ {isRenaming ? (
{
data-tooltip-dynamic="true"
>
{decodeEntities(dataQuery.name)}
- {' '}
+
+
+
+ {licenseValid && isRestricted && }
+
+ {' '}
{!isQueryRunnable(dataQuery) &&
Draft}
{localDs && (
<>
@@ -143,80 +172,24 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
)}
- {!shouldFreeze && isQuerySelected && (
-
-
setRenamingQuery(true)}
- >
-
-
-
-
-
debouncedDuplicateQuery(dataQuery?.id, appId)}
- >
-
-
-
-
-
- {isDeletingQueryInProcess ? (
-
- ) : (
-
-
-
-
-
- )}
-
-
-
- )}
+
+ toggleQueryHandlerMenu(true, `query-handler-menu-${dataQuery?.id}`)}
+ size="small"
+ variant="outline"
+ className=""
+ id={`query-handler-menu-${dataQuery?.id}`}
+ />
+
setShowDeleteConfirmation(false)}
+ onCancel={() => deleteDataQuery(null)}
darkMode={darkMode}
/>
>
diff --git a/frontend/src/AppBuilder/QueryPanel/QueryCardMenu.jsx b/frontend/src/AppBuilder/QueryPanel/QueryCardMenu.jsx
new file mode 100644
index 0000000000..a9e3030b51
--- /dev/null
+++ b/frontend/src/AppBuilder/QueryPanel/QueryCardMenu.jsx
@@ -0,0 +1,174 @@
+import React, { useCallback } from 'react';
+import { Overlay, Popover } from 'react-bootstrap';
+import useStore from '@/AppBuilder/_stores/store';
+import classNames from 'classnames';
+import Edit from '@/_ui/Icon/bulkIcons/Edit';
+import Trash from '@/_ui/Icon/solidIcons/Trash';
+import Copy from '@/_ui/Icon/solidIcons/Copy';
+import SolidIcon from '@/_ui/Icon/SolidIcons';
+import { shallow } from 'zustand/shallow';
+import { ToolTip } from '@/_components/ToolTip';
+import { debounce } from 'lodash';
+import usePopoverObserver from '@/AppBuilder/_hooks/usePopoverObserver';
+import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
+
+const QueryCardMenu = ({ darkMode }) => {
+ const { moduleId } = useModuleContext();
+ const appId = useStore((state) => state.appStore.modules[moduleId].app.appId);
+ const selectedQuery = useStore((state) => state.queryPanel.selectedQuery);
+ const toggleQueryPermissionModal = useStore((state) => state.queryPanel.toggleQueryPermissionModal);
+ const featureAccess = useStore((state) => state?.license?.featureAccess, shallow);
+ const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid;
+ const targetBtnForMenu = useStore((state) => state.queryPanel.targetBtnForMenu);
+ const targetElement = document.getElementById(targetBtnForMenu);
+ const showQueryHandlerMenu = useStore((state) => state.queryPanel.showQueryHandlerMenu);
+ const toggleQueryHandlerMenu = useStore((state) => state.queryPanel.toggleQueryHandlerMenu);
+ const duplicateQuery = useStore((state) => state.dataQuery.duplicateQuery);
+ const setPreviewData = useStore((state) => state.queryPanel.setPreviewData);
+ const setRenamingQuery = useStore((state) => state.queryPanel.setRenamingQuery);
+ const deleteDataQuery = useStore((state) => state.queryPanel.deleteDataQuery);
+
+ const QUERY_MENU_OPTIONS = [
+ {
+ label: 'Rename',
+ value: 'rename',
+ icon: ,
+ showTooltip: false,
+ },
+ {
+ label: 'Duplicate',
+ value: 'duplicate',
+ icon: ,
+ showTooltip: false,
+ },
+ {
+ label: 'Query permission',
+ value: 'permission',
+ icon: (
+
+ ),
+ trailingIcon: ,
+ },
+ {
+ label: 'Delete',
+ value: 'delete',
+ icon: ,
+ showTooltip: false,
+ },
+ ];
+
+ // To prevent user clicking from continuous clicks
+ const debouncedDuplicateQuery = useCallback(
+ debounce((queryId, appId) => {
+ duplicateQuery(queryId, appId);
+ setPreviewData(null);
+ }, 500),
+ [duplicateQuery]
+ );
+
+ const handleQueryMenuActions = (value) => {
+ if (value === 'rename') {
+ setRenamingQuery(selectedQuery?.id);
+ }
+ if (value === 'duplicate') {
+ debouncedDuplicateQuery(selectedQuery?.id, appId);
+ }
+ if (value === 'permission') {
+ if (!licenseValid) return;
+ toggleQueryPermissionModal(true);
+ }
+ if (value === 'delete') {
+ deleteDataQuery(selectedQuery?.id);
+ }
+ toggleQueryHandlerMenu(false);
+ };
+
+ usePopoverObserver(
+ document.getElementsByClassName('query-list')[0],
+ targetElement,
+ document.getElementById('query-list-menu'),
+ showQueryHandlerMenu,
+ () => (document.getElementById('query-list-menu').style.display = 'block'),
+ () => (document.getElementById('query-list-menu').style.display = 'none')
+ );
+
+ return (
+ toggleQueryHandlerMenu(false)}
+ popperConfig={{
+ modifiers: [
+ {
+ name: 'flip',
+ options: {
+ fallbackPlacements: ['top-start'],
+ flipVariations: true,
+ allowedAutoPlacements: ['top', 'bottom'],
+ boundary: 'viewport',
+ },
+ },
+ {
+ name: 'offset',
+ options: {
+ offset: [0, 3],
+ },
+ },
+ ],
+ }}
+ >
+ {(props) => (
+
+ )}
+
+ );
+};
+
+export default QueryCardMenu;
diff --git a/frontend/src/AppBuilder/QueryPanel/QueryDataPane.jsx b/frontend/src/AppBuilder/QueryPanel/QueryDataPane.jsx
index 9ac052ae51..97b4daa68f 100644
--- a/frontend/src/AppBuilder/QueryPanel/QueryDataPane.jsx
+++ b/frontend/src/AppBuilder/QueryPanel/QueryDataPane.jsx
@@ -16,6 +16,10 @@ import DataSourceSelect from '../QueryManager/Components/DataSourceSelect';
import { OverlayTrigger, Popover } from 'react-bootstrap';
import FolderEmpty from '@/_ui/Icon/solidIcons/FolderEmpty';
import useStore from '@/AppBuilder/_stores/store';
+import AppPermissionsModal from '@/modules/Appbuilder/components/AppPermissionsModal';
+import { shallow } from 'zustand/shallow';
+import { appPermissionService } from '@/_services';
+import QueryCardMenu from './QueryCardMenu';
export const QueryDataPane = ({ darkMode }) => {
const { t } = useTranslation();
@@ -34,6 +38,12 @@ export const QueryDataPane = ({ darkMode }) => {
function isDataSourceLocal(dataQuery) {
return dataSources.some((dataSource) => dataSource.id === dataQuery.data_source_id);
}
+ const featureAccess = useStore((state) => state?.license?.featureAccess, shallow);
+ const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid;
+ const selectedQuery = useStore((state) => state.queryPanel.selectedQuery);
+ const showQueryPermissionModal = useStore((state) => state.queryPanel.showQueryPermissionModal);
+ const toggleQueryPermissionModal = useStore((state) => state.queryPanel.toggleQueryPermissionModal);
+ const setQueries = useStore((state) => state.dataQuery.setQueries);
useEffect(() => {
setQueryPanelSearchTerm(searchTermForFilters);
@@ -171,6 +181,33 @@ export const QueryDataPane = ({ darkMode }) => {
{filteredQueries.map((query) => (
))}
+
+ {licenseValid && (
+ appPermissionService.getQueryPermission(appId, id)}
+ createPermission={(id, appId, body) => appPermissionService.createQueryPermission(appId, id, body)}
+ updatePermission={(id, appId, body) => appPermissionService.updateQueryPermission(appId, id, body)}
+ deletePermission={(id, appId) => appPermissionService.deleteQueryPermission(appId, id)}
+ onSuccess={(data) => {
+ const updatedDataQueries = dataQueries.map((query) => {
+ if (query.id === selectedQuery.id) {
+ return {
+ ...query,
+ permissions: data.length === 0 || data.length === undefined ? [] : [data[0]],
+ };
+ }
+ return query;
+ });
+ setQueries(updatedDataQueries);
+ }}
+ />
+ )}
normalizeQueryTransformationOptions(query));
setQueries(dataQueries, moduleId);
diff --git a/frontend/src/AppBuilder/_stores/slices/appSlice.js b/frontend/src/AppBuilder/_stores/slices/appSlice.js
index 6962c6752d..fdb81e97a9 100644
--- a/frontend/src/AppBuilder/_stores/slices/appSlice.js
+++ b/frontend/src/AppBuilder/_stores/slices/appSlice.js
@@ -16,6 +16,11 @@ const initialState = {
pageSwitchInProgress: false,
isTJDarkMode: localStorage.getItem('darkMode') === 'true',
isViewer: false,
+ isComponentLayoutReady: false,
+ appPermission: {
+ selectedUsers: [],
+ selectedUserGroups: [],
+ },
appStore: {
modules: {
canvas: {
@@ -276,4 +281,12 @@ export const createAppSlice = (set, get) => ({
return get().appStore.modules[moduleId].app.homePageId;
},
updateIsTJDarkMode: (newMode) => set({ isTJDarkMode: newMode }, false, 'updateIsTJDarkMode'),
+ setSelectedUserGroups: (groups) =>
+ set((state) => {
+ state.appPermission.selectedUserGroups = groups;
+ }),
+ setSelectedUsers: (users) =>
+ set((state) => {
+ state.appPermission.selectedUsers = users;
+ }),
});
diff --git a/frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js b/frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js
index 99d1ee660b..34bea1bf84 100644
--- a/frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js
+++ b/frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js
@@ -1,4 +1,4 @@
-import { dataqueryService } from '@/_services';
+import { dataqueryService, appPermissionService } from '@/_services';
import { getDefaultOptions } from '@/_stores/storeHelper';
import { v4 as uuidv4 } from 'uuid';
import _, { isEmpty, throttle } from 'lodash';
@@ -270,7 +270,6 @@ export const createDataQuerySlice = (set, get) => ({
)
.then((data) => {
set((state) => {
- state.dataQuery.creatingQueryInProcessId = null;
state.dataQuery.queries.modules[moduleId] = [
{
...data,
@@ -308,6 +307,42 @@ export const createDataQuerySlice = (set, get) => ({
};
createAppVersionEventHandlers(newEvent, moduleId);
});
+
+ if (queryToClone.permissions && queryToClone.permissions.length !== 0) {
+ const body = {
+ type: queryToClone.permissions[0]?.type,
+ ...(queryToClone.permissions[0]?.type === 'GROUP'
+ ? {
+ groups: (queryToClone.permissions[0]?.groups || queryToClone.permissions[0]?.users || []).map(
+ (group) => group.permissionGroupsId || group.permission_groups_id
+ ),
+ }
+ : { users: queryToClone.permissions[0]?.users.map((user) => user.userId || user.user_id) }),
+ };
+ appPermissionService
+ .createQueryPermission(appId, data.id, body)
+ .then((newQuery) => {
+ const dataQueries = get().dataQuery.queries.modules[moduleId];
+ const updatedDataQueries = dataQueries.map((query) => {
+ if (query.id === data.id) {
+ return {
+ ...query,
+ permissions: newQuery.length === 0 || newQuery.length === undefined ? [] : [newQuery[0]],
+ };
+ }
+ return query;
+ });
+ get().dataQuery.setQueries(updatedDataQueries);
+ })
+ .catch(() => {
+ toast.error('Permission could not be created. Please try again!', {
+ className: 'text-nowrap w-auto mw-100',
+ });
+ });
+ }
+ set((state) => {
+ state.dataQuery.creatingQueryInProcessId = null;
+ });
})
.catch((error) => {
console.error('error', error);
@@ -438,7 +473,10 @@ export const createDataQuerySlice = (set, get) => ({
const queries = get().dataQuery.queries.modules[moduleId];
try {
for (const query of queries) {
- if ((query.options.runOnPageLoad || query.options.run_on_page_load) && isQueryRunnable(query)) {
+ if (
+ (query.options?.runOnPageLoad || query.options?.run_on_page_load) &&
+ (query.restricted || isQueryRunnable(query))
+ ) {
await get().queryPanel.runQuery(
query.id,
query.name,
diff --git a/frontend/src/AppBuilder/_stores/slices/pageMenuSlice.js b/frontend/src/AppBuilder/_stores/slices/pageMenuSlice.js
index 5ece338ed8..8c67ab9825 100644
--- a/frontend/src/AppBuilder/_stores/slices/pageMenuSlice.js
+++ b/frontend/src/AppBuilder/_stores/slices/pageMenuSlice.js
@@ -100,10 +100,6 @@ export const createPageMenuSlice = (set, get) => {
pageSettingSelected: false,
pageSettings: {},
showPagePermissionModal: false,
- permissionPage: null,
- selectedUserGroups: [],
- selectedUsers: [],
- pagePermission: null,
toggleSearch: (show) =>
set((state) => {
@@ -432,26 +428,12 @@ export const createPageMenuSlice = (set, get) => {
}
},
- setPagePermission: (pagePermission) =>
- set((state) => {
- state.pagePermission = pagePermission;
- }),
-
togglePagePermissionModal: (show) => {
set((state) => {
state.showPagePermissionModal = show;
});
},
- setSelectedUserGroups: (groups) =>
- set((state) => {
- state.selectedUserGroups = groups;
- }),
-
- setSelectedUsers: (users) =>
- set((state) => {
- state.selectedUsers = users;
- }),
setEditingPage: (page) =>
set((state) => {
state.editingPage = page;
diff --git a/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js b/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js
index f33f67c66e..903b548ca2 100644
--- a/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js
+++ b/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js
@@ -26,6 +26,12 @@ const initialState = {
loadingDataQueries: false,
isPreviewQueryLoading: false,
queryPanelSearchTem: '',
+ showQueryPermissionModal: false,
+ targetBtnForMenu: null,
+ showQueryHandlerMenu: false,
+ showDeleteConfirmation: false,
+ renamingQueryId: null,
+ deletingQueryId: null,
};
export const createQueryPanelSlice = (set, get) => ({
@@ -223,6 +229,7 @@ export const createQueryPanelSlice = (set, get) => ({
selectedEnvironment,
isPublicAccess,
currentVersionId,
+ currentMode,
} = get();
const {
queryPreviewData,
@@ -352,14 +359,15 @@ export const createQueryPanelSlice = (set, get) => ({
let queryExecutionPromise = null;
if (query.kind === 'runjs') {
- queryExecutionPromise = executeMultilineJS(query.options.code, query?.id, false, mode, parameters, moduleId);
+ queryExecutionPromise = executeMultilineJS(query.options?.code, query?.id, false, mode, parameters, moduleId);
} else if (query.kind === 'runpy') {
- queryExecutionPromise = executeRunPycode(query.options.code, query, false, mode, queryState, moduleId);
+ queryExecutionPromise = executeRunPycode(query.options?.code, query, false, mode, queryState, moduleId);
} else if (query.kind === 'workflows') {
queryExecutionPromise = executeWorkflow(
moduleId,
- query.options.workflowId,
- query.options.blocking,
+ query,
+ query.options?.workflowId,
+ query.options?.blocking,
query.options?.params,
(currentAppEnvironmentId ?? environmentId) || selectedEnvironment?.id //TODO: currentAppEnvironmentId may no longer required. Need to check
);
@@ -374,7 +382,8 @@ export const createQueryPanelSlice = (set, get) => ({
options,
query?.options,
versionId,
- !isPublicAccess ? (currentAppEnvironmentId ?? environmentId) || selectedEnvironment?.id : undefined //TODO: currentAppEnvironmentId may no longer required. Need to check
+ !isPublicAccess ? (currentAppEnvironmentId ?? environmentId) || selectedEnvironment?.id : undefined, //TODO: currentAppEnvironmentId may no longer required. Need to check
+ currentMode
);
}
@@ -447,7 +456,7 @@ export const createQueryPanelSlice = (set, get) => ({
queryId,
{
isLoading: false,
- ...(query.kind === 'restapi'
+ ...(query.kind === 'restapi' || data.data.type === 'tj-401'
? {
metadata: data.metadata,
request: data.data.requestObject,
@@ -725,6 +734,28 @@ export const createQueryPanelSlice = (set, get) => ({
const {
queryPanel: { evaluatePythonCode },
} = get();
+
+ if (query.restricted) {
+ return {
+ status: 'failed',
+ message: 'Query could not be completed',
+ description: 'Response code 401 (Unauthorized)',
+ data: {
+ type: 'tj-401',
+ responseObject: {
+ statusCode: 401,
+ responseBody: 'Unauthorized Access',
+ },
+ },
+ metadata: {
+ response: {
+ statusCode: 401,
+ responseBody: 'Unauthorized Access',
+ },
+ },
+ };
+ }
+
return { data: await evaluatePythonCode({ code, query, isPreview, mode, currentState }) };
},
@@ -948,12 +979,33 @@ export const createQueryPanelSlice = (set, get) => ({
// queries: updatedQueries,
// });
},
- executeWorkflow: async (moduleId = 'canvas', workflowId, _blocking = false, params = {}, appEnvId) => {
+ executeWorkflow: async (moduleId = 'canvas', query, workflowId, _blocking = false, params = {}, appEnvId) => {
const { getAppId, getAllExposedValues } = get();
const appId = getAppId('canvas');
const currentState = getAllExposedValues(moduleId);
const resolvedParams = get().resolveReferences(moduleId, params, currentState, {}, {});
+ if (query.restricted) {
+ return {
+ status: 'failed',
+ message: 'Query could not be completed',
+ description: 'Response code 401 (Unauthorized)',
+ data: {
+ type: 'tj-401',
+ responseObject: {
+ statusCode: 401,
+ responseBody: 'Unauthorized Access',
+ },
+ },
+ metadata: {
+ response: {
+ statusCode: 401,
+ responseBody: 'Unauthorized Access',
+ },
+ },
+ };
+ }
+
try {
const response = await workflowExecutionsService.execute(workflowId, resolvedParams, appId, appEnvId);
return { data: response.result, status: 'ok' };
@@ -1004,6 +1056,27 @@ export const createQueryPanelSlice = (set, get) => ({
const queryDetails = dataQuery.queries.modules?.[moduleId].find((q) => q.id === queryId);
+ if (queryDetails.restricted) {
+ return {
+ status: 'failed',
+ message: 'Query could not be completed',
+ description: 'Response code 401 (Unauthorized)',
+ data: {
+ type: 'tj-401',
+ responseObject: {
+ statusCode: 401,
+ responseBody: 'Unauthorized Access',
+ },
+ },
+ metadata: {
+ response: {
+ statusCode: 401,
+ responseBody: 'Unauthorized Access',
+ },
+ },
+ };
+ }
+
const defaultParams =
queryDetails?.options?.parameters?.reduce(
(paramObj, param) => ({
@@ -1154,5 +1227,24 @@ export const createQueryPanelSlice = (set, get) => ({
};
previewQuery(query, false, undefined, moduleId);
},
+ toggleQueryPermissionModal: (show) => {
+ set((state) => {
+ state.queryPanel.showQueryPermissionModal = show;
+ });
+ },
+ toggleQueryHandlerMenu: (show, id) => {
+ set((state) => {
+ if (show) state.queryPanel.targetBtnForMenu = id;
+ state.queryPanel.showQueryHandlerMenu = show;
+ });
+ },
+ setRenamingQuery: (queryId) =>
+ set((state) => {
+ state.queryPanel.renamingQueryId = queryId;
+ }),
+ deleteDataQuery: (queryId) =>
+ set((state) => {
+ state.queryPanel.deletingQueryId = queryId;
+ }),
},
});
diff --git a/frontend/src/HomePage/HomePage.jsx b/frontend/src/HomePage/HomePage.jsx
index b0d698398e..b7b2a5980b 100644
--- a/frontend/src/HomePage/HomePage.jsx
+++ b/frontend/src/HomePage/HomePage.jsx
@@ -381,7 +381,7 @@ class HomePageComponent extends React.Component {
}
};
- importFile = async (importJSON, appName, skipPagePermissionsGroupCheck = false) => {
+ importFile = async (importJSON, appName, skipPermissionsGroupCheck = false) => {
this.setState({ isImportingApp: true });
// For backward compatibility with legacy app import
const organization_id = this.state.currentUser?.organization_id;
@@ -397,7 +397,7 @@ class HomePageComponent extends React.Component {
const requestBody = {
organization_id,
...importJSON,
- skip_page_permissions_group_check: skipPagePermissionsGroupCheck,
+ skip_permissions_group_check: skipPermissionsGroupCheck,
};
let installedPluginsInfo = [];
try {
diff --git a/frontend/src/ToolJetUI/List/list.scss b/frontend/src/ToolJetUI/List/list.scss
index c5aa03aada..04258a0788 100644
--- a/frontend/src/ToolJetUI/List/list.scss
+++ b/frontend/src/ToolJetUI/List/list.scss
@@ -64,6 +64,7 @@ button:focus:not(:focus-visible) {
padding: 10px 14px;
cursor: pointer;
display: flex;
+ align-items: center;
&:hover {
background-color: var(--slate3);
@@ -78,6 +79,10 @@ button:focus:not(:focus-visible) {
.color-tomato9 {
color: var(--tomato9)
}
+
+ .color-disabled {
+ color: var(--text-disabled);
+ }
}
}
diff --git a/frontend/src/_services/appPermission.service.js b/frontend/src/_services/appPermission.service.js
index 85cb6ee004..fd2438f058 100644
--- a/frontend/src/_services/appPermission.service.js
+++ b/frontend/src/_services/appPermission.service.js
@@ -7,6 +7,10 @@ export const appPermissionService = {
createPagePermission,
updatePagePermission,
deletePagePermission,
+ getQueryPermission,
+ createQueryPermission,
+ updateQueryPermission,
+ deleteQueryPermission,
};
function getPagePermission(appId, pageId) {
@@ -47,3 +51,41 @@ function deletePagePermission(appId, pageId) {
};
return fetch(`${config.apiUrl}/app-permissions/${appId}/pages/${pageId}`, requestOptions).then(handleResponse);
}
+
+function getQueryPermission(appId, queryId) {
+ const requestOptions = {
+ method: 'GET',
+ headers: authHeader(),
+ credentials: 'include',
+ };
+ return fetch(`${config.apiUrl}/app-permissions/${appId}/queries/${queryId}`, requestOptions).then(handleResponse);
+}
+
+function createQueryPermission(appId, queryId, body) {
+ const requestOptions = {
+ method: 'POST',
+ headers: authHeader(),
+ credentials: 'include',
+ body: JSON.stringify(body),
+ };
+ return fetch(`${config.apiUrl}/app-permissions/${appId}/queries/${queryId}`, requestOptions).then(handleResponse);
+}
+
+function updateQueryPermission(appId, queryId, body) {
+ const requestOptions = {
+ method: 'PUT',
+ headers: authHeader(),
+ credentials: 'include',
+ body: JSON.stringify(body),
+ };
+ return fetch(`${config.apiUrl}/app-permissions/${appId}/queries/${queryId}`, requestOptions).then(handleResponse);
+}
+
+function deleteQueryPermission(appId, queryId) {
+ const requestOptions = {
+ method: 'DELETE',
+ headers: authHeader(),
+ credentials: 'include',
+ };
+ return fetch(`${config.apiUrl}/app-permissions/${appId}/queries/${queryId}`, requestOptions).then(handleResponse);
+}
diff --git a/frontend/src/_services/dataquery.service.js b/frontend/src/_services/dataquery.service.js
index 3919a6b0f3..3dc5e26593 100644
--- a/frontend/src/_services/dataquery.service.js
+++ b/frontend/src/_services/dataquery.service.js
@@ -13,9 +13,9 @@ export const dataqueryService = {
bulkUpdateQueryOptions,
};
-function getAll(appVersionId) {
+function getAll(appVersionId, mode) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
- return fetch(`${config.apiUrl}/data-queries/${appVersionId}`, requestOptions).then(handleResponse);
+ return fetch(`${config.apiUrl}/data-queries/${appVersionId}?mode=${mode}`, requestOptions).then(handleResponse);
}
function create(app_id, app_version_id, name, kind, options, data_source_id, plugin_id) {
@@ -72,7 +72,7 @@ function del(id, versionId) {
return fetch(`${config.apiUrl}/data-queries/${id}/versions/${versionId}`, requestOptions).then(handleResponse);
}
-function run(queryId, resolvedOptions, options, versionId, environmentId) {
+function run(queryId, resolvedOptions, options, versionId, environmentId, mode) {
const body = {
resolvedOptions: resolvedOptions,
options: options,
@@ -80,7 +80,7 @@ function run(queryId, resolvedOptions, options, versionId, environmentId) {
let url = `${config.apiUrl}/data-queries/${queryId}/versions/${versionId}/run${
environmentId && environmentId !== 'undefined' ? `/${environmentId}` : ''
- }`;
+ }?mode=${mode}`;
//For public/released apps
if (!environmentId || !versionId) {
diff --git a/frontend/src/_styles/queryManager.scss b/frontend/src/_styles/queryManager.scss
index 1fed6a3a13..861dbd78fe 100644
--- a/frontend/src/_styles/queryManager.scss
+++ b/frontend/src/_styles/queryManager.scss
@@ -166,7 +166,7 @@ $border-radius: 4px;
}
.query-row:hover .query-rename-delete-btn {
- display: flex;
+ display: flex !important;
}
.query-row {
@@ -189,12 +189,10 @@ $border-radius: 4px;
}
.query-rename-delete-btn {
- display: none;
align-items: center;
justify-content: flex-end;
- gap: 8px;
margin-left: 2px;
- width: 66px;
+ width: 22px;
height: 20px;
}
diff --git a/frontend/src/_ui/Icon/solidIcons/MoreVertical01.jsx b/frontend/src/_ui/Icon/solidIcons/MoreVertical01.jsx
new file mode 100644
index 0000000000..dbda2756d5
--- /dev/null
+++ b/frontend/src/_ui/Icon/solidIcons/MoreVertical01.jsx
@@ -0,0 +1,21 @@
+import React from 'react';
+
+const MoreVertical01 = ({ fill = '#11181C', width = '12', height = '13', className = '', viewBox = '0 0 12 13' }) => (
+
+);
+
+export default MoreVertical01;
diff --git a/frontend/src/_ui/Icon/solidIcons/index.js b/frontend/src/_ui/Icon/solidIcons/index.js
index 0e7192edaf..c6fb056b78 100644
--- a/frontend/src/_ui/Icon/solidIcons/index.js
+++ b/frontend/src/_ui/Icon/solidIcons/index.js
@@ -239,6 +239,7 @@ import EnterpriseCrown from './EnterrpiseCrown.jsx';
import FileCode from './FileCode.jsx';
import Corners from './Corners.jsx';
import Moon from './Moon.jsx';
+import MoreVertical01 from './MoreVertical01.jsx';
import RemoveFolder from './RemoveFolder.jsx';
const Icon = (props) => {
@@ -725,6 +726,8 @@ const Icon = (props) => {
return ;
case 'moon':
return ;
+ case 'morevertical01':
+ return ;
default:
return ;
}
diff --git a/frontend/src/modules/Appbuilder/components/AppPermissionsModal/AppPermissionsModal.jsx b/frontend/src/modules/Appbuilder/components/AppPermissionsModal/AppPermissionsModal.jsx
new file mode 100644
index 0000000000..75be7509b2
--- /dev/null
+++ b/frontend/src/modules/Appbuilder/components/AppPermissionsModal/AppPermissionsModal.jsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent';
+
+const AppPermissionsModal = () => {
+ return <>>;
+};
+
+export default withEditionSpecificComponent(AppPermissionsModal, 'Appbuilder');
diff --git a/frontend/src/modules/Appbuilder/components/AppPermissionsModal/index.js b/frontend/src/modules/Appbuilder/components/AppPermissionsModal/index.js
new file mode 100644
index 0000000000..cae7417742
--- /dev/null
+++ b/frontend/src/modules/Appbuilder/components/AppPermissionsModal/index.js
@@ -0,0 +1 @@
+export { default } from './AppPermissionsModal';
diff --git a/frontend/src/modules/Appbuilder/components/index.js b/frontend/src/modules/Appbuilder/components/index.js
index 196fb3b07b..8913fdae56 100644
--- a/frontend/src/modules/Appbuilder/components/index.js
+++ b/frontend/src/modules/Appbuilder/components/index.js
@@ -4,6 +4,7 @@ import LogoNavDropdown from './LogoNavDropdown';
import AppEnvironments from './AppEnvironments';
import ThemeSelect from './ThemeSelect';
import ColorSwatches from './ColorSwatches';
+import AppPermissionsModal from './AppPermissionsModal';
import ComponentModuleTab from './ComponentModuleTab';
export {
@@ -13,5 +14,6 @@ export {
AppEnvironments,
ThemeSelect,
ColorSwatches,
+ AppPermissionsModal,
ComponentModuleTab,
};
diff --git a/server/ee b/server/ee
index 9e988341ae..8df193d06e 160000
--- a/server/ee
+++ b/server/ee
@@ -1 +1 @@
-Subproject commit 9e988341aeb7be4348bd7d893a4389507785281c
+Subproject commit 8df193d06e146f72447a3fa95de4544cd47080d5
diff --git a/server/migrations/1747759439358-CreateQueryPermissions.ts b/server/migrations/1747759439358-CreateQueryPermissions.ts
new file mode 100644
index 0000000000..fe6bce52ce
--- /dev/null
+++ b/server/migrations/1747759439358-CreateQueryPermissions.ts
@@ -0,0 +1,51 @@
+import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
+
+export class CreateQueryPermissions1747759439358 implements MigrationInterface {
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.createTable(
+ new Table({
+ name: 'query_permissions',
+ columns: [
+ {
+ name: 'id',
+ type: 'uuid',
+ isGenerated: true,
+ default: 'gen_random_uuid()',
+ isPrimary: true,
+ },
+ {
+ name: 'query_id',
+ type: 'uuid',
+ isNullable: false,
+ },
+ {
+ name: 'type',
+ type: 'enum',
+ enum: ['SINGLE', 'GROUP'],
+ },
+ {
+ name: 'created_at',
+ type: 'timestamp',
+ isNullable: false,
+ default: 'now()',
+ },
+ ],
+ }),
+ true
+ );
+
+ await queryRunner.createForeignKey(
+ 'query_permissions',
+ new TableForeignKey({
+ columnNames: ['query_id'],
+ referencedColumnNames: ['id'],
+ referencedTableName: 'data_queries',
+ onDelete: 'CASCADE',
+ })
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.dropTable('query_permissions');
+ }
+}
diff --git a/server/migrations/1747763331564-CreateQueryUsers.ts b/server/migrations/1747763331564-CreateQueryUsers.ts
new file mode 100644
index 0000000000..819b736fba
--- /dev/null
+++ b/server/migrations/1747763331564-CreateQueryUsers.ts
@@ -0,0 +1,76 @@
+import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
+
+export class CreateQueryUsers1747763331564 implements MigrationInterface {
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.createTable(
+ new Table({
+ name: 'query_users',
+ columns: [
+ {
+ name: 'id',
+ type: 'uuid',
+ isGenerated: true,
+ default: 'gen_random_uuid()',
+ isPrimary: true,
+ },
+ {
+ name: 'query_permissions_id',
+ type: 'uuid',
+ isNullable: false,
+ },
+ {
+ name: 'user_id',
+ type: 'uuid',
+ isNullable: true,
+ },
+ {
+ name: 'permission_groups_id',
+ type: 'uuid',
+ isNullable: true,
+ },
+ {
+ name: 'created_at',
+ type: 'timestamp',
+ isNullable: false,
+ default: 'now()',
+ },
+ ],
+ }),
+ true
+ );
+
+ await queryRunner.createForeignKey(
+ 'query_users',
+ new TableForeignKey({
+ columnNames: ['query_permissions_id'],
+ referencedColumnNames: ['id'],
+ referencedTableName: 'query_permissions',
+ onDelete: 'CASCADE',
+ })
+ );
+
+ await queryRunner.createForeignKey(
+ 'query_users',
+ new TableForeignKey({
+ columnNames: ['user_id'],
+ referencedColumnNames: ['id'],
+ referencedTableName: 'users',
+ onDelete: 'CASCADE',
+ })
+ );
+
+ await queryRunner.createForeignKey(
+ 'query_users',
+ new TableForeignKey({
+ columnNames: ['permission_groups_id'],
+ referencedColumnNames: ['id'],
+ referencedTableName: 'permission_groups',
+ onDelete: 'CASCADE',
+ })
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.dropTable('query_users');
+ }
+}
diff --git a/server/src/dto/import-resources.dto.ts b/server/src/dto/import-resources.dto.ts
index 89b3ee182c..3ad6d2cee0 100644
--- a/server/src/dto/import-resources.dto.ts
+++ b/server/src/dto/import-resources.dto.ts
@@ -31,7 +31,7 @@ export class ImportResourcesDto {
@IsOptional()
@IsBoolean()
- skip_page_permissions_group_check?: boolean;
+ skip_permissions_group_check?: boolean;
}
export class ImportAppDto {
diff --git a/server/src/entities/data_query.entity.ts b/server/src/entities/data_query.entity.ts
index 49e5b1f1a5..7e3a8d25a5 100644
--- a/server/src/entities/data_query.entity.ts
+++ b/server/src/entities/data_query.entity.ts
@@ -10,11 +10,13 @@ import {
JoinTable,
ManyToMany,
AfterLoad,
+ OneToMany,
} from 'typeorm';
import { App } from './app.entity';
import { AppVersion } from './app_version.entity';
import { DataSource } from './data_source.entity';
import { Plugin } from './plugin.entity';
+import { QueryPermission } from './query_permissions.entity';
@Entity({ name: 'data_queries' })
export class DataQuery extends BaseEntity {
@@ -81,6 +83,9 @@ export class DataQuery extends BaseEntity {
app: App;
+ @OneToMany(() => QueryPermission, (permission) => permission.query)
+ permissions: QueryPermission[];
+
@AfterLoad()
updatePlugin() {
if (this.plugins?.length) this.plugin = this.plugins[0];
diff --git a/server/src/entities/group_permissions.entity.ts b/server/src/entities/group_permissions.entity.ts
index d321d84b5f..26667796ae 100644
--- a/server/src/entities/group_permissions.entity.ts
+++ b/server/src/entities/group_permissions.entity.ts
@@ -14,6 +14,7 @@ import { GroupUsers } from './group_users.entity';
import { GranularPermissions } from './granular_permissions.entity';
import { GROUP_PERMISSIONS_TYPE } from '@modules/group-permissions/constants';
import { PageUser } from './page_users.entity';
+import { QueryUser } from './query_users.entity';
@Entity({ name: 'permission_groups' })
export class GroupPermissions extends BaseEntity {
@@ -72,5 +73,8 @@ export class GroupPermissions extends BaseEntity {
@OneToMany(() => PageUser, (pageUser) => pageUser.permissionGroup)
pageUsers: PageUser[];
+ @OneToMany(() => QueryUser, (queryUser) => queryUser.permissionGroup)
+ queryUsers: QueryUser[];
+
disabled?: boolean;
}
diff --git a/server/src/entities/query_permissions.entity.ts b/server/src/entities/query_permissions.entity.ts
new file mode 100644
index 0000000000..c693c6b994
--- /dev/null
+++ b/server/src/entities/query_permissions.entity.ts
@@ -0,0 +1,29 @@
+import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, OneToMany } from 'typeorm';
+import { DataQuery } from './data_query.entity';
+import { QueryUser } from './query_users.entity';
+import { PAGE_PERMISSION_TYPE } from '@modules/app-permissions/constants';
+
+@Entity('query_permissions')
+export class QueryPermission {
+ @PrimaryGeneratedColumn('uuid')
+ id: string;
+
+ @Column({ name: 'query_id', type: 'uuid', nullable: false })
+ queryId: string;
+
+ @Column({
+ type: 'enum',
+ enum: PAGE_PERMISSION_TYPE,
+ })
+ type: PAGE_PERMISSION_TYPE;
+
+ @CreateDateColumn({ name: 'created_at' })
+ createdAt: Date;
+
+ @ManyToOne(() => DataQuery, (query) => query.permissions, { onDelete: 'CASCADE' })
+ @JoinColumn({ name: 'query_id' })
+ query: DataQuery;
+
+ @OneToMany(() => QueryUser, (queryUser) => queryUser.queryPermission)
+ users: QueryUser[];
+}
diff --git a/server/src/entities/query_users.entity.ts b/server/src/entities/query_users.entity.ts
new file mode 100644
index 0000000000..46b16be0af
--- /dev/null
+++ b/server/src/entities/query_users.entity.ts
@@ -0,0 +1,34 @@
+import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn } from 'typeorm';
+import { User } from './user.entity';
+import { QueryPermission } from './query_permissions.entity';
+import { GroupPermissions } from './group_permissions.entity';
+
+@Entity('query_users')
+export class QueryUser {
+ @PrimaryGeneratedColumn('uuid')
+ id: string;
+
+ @Column({ name: 'query_permissions_id', type: 'uuid' })
+ queryPermissionsId: string;
+
+ @Column({ name: 'user_id', type: 'uuid', nullable: true })
+ userId: string | null;
+
+ @Column({ name: 'permission_groups_id', type: 'uuid', nullable: true })
+ permissionGroupsId: string | null;
+
+ @CreateDateColumn({ name: 'created_at' })
+ createdAt: Date;
+
+ @ManyToOne(() => QueryPermission, { onDelete: 'CASCADE' })
+ @JoinColumn({ name: 'query_permissions_id' })
+ queryPermission: QueryPermission;
+
+ @ManyToOne(() => User, { onDelete: 'CASCADE', nullable: true })
+ @JoinColumn({ name: 'user_id' })
+ user: User;
+
+ @ManyToOne(() => GroupPermissions, { onDelete: 'CASCADE', nullable: true })
+ @JoinColumn({ name: 'permission_groups_id' })
+ permissionGroup: GroupPermissions;
+}
diff --git a/server/src/entities/user.entity.ts b/server/src/entities/user.entity.ts
index e052e11245..515b1f469e 100644
--- a/server/src/entities/user.entity.ts
+++ b/server/src/entities/user.entity.ts
@@ -30,6 +30,7 @@ import { AiConversation } from './ai_conversation.entity';
import { AiResponseVote } from './ai_response_vote.entity';
import { USER_ROLE } from '@modules/group-permissions/constants';
import { PageUser } from './page_users.entity';
+import { QueryUser } from './query_users.entity';
@Entity({ name: 'users' })
export class User extends BaseEntity {
@@ -188,6 +189,9 @@ export class User extends BaseEntity {
@OneToMany(() => PageUser, (pageUser) => pageUser.user)
pageUsers: PageUser[];
+ @OneToMany(() => QueryUser, (queryUser) => queryUser.user)
+ queryUsers: QueryUser[];
+
organizationId: string;
invitedOrganizationId: string;
organizationIds?: Array;
diff --git a/server/src/helpers/error_type.constant.ts b/server/src/helpers/error_type.constant.ts
index cb483e8896..e34ce5b69d 100644
--- a/server/src/helpers/error_type.constant.ts
+++ b/server/src/helpers/error_type.constant.ts
@@ -1,7 +1,7 @@
export const APP_ERROR_TYPE = {
IMPORT_EXPORT_SERVICE: {
UNSUPPORTED_VERSION_ERROR: 'Apps built on later versions of ToolJet cannot be imported',
- PAGE_PERMISSION_GROUP_ERROR: 'Following groups are missing from the workspace',
+ PERMISSION_GROUP_ERROR: 'Following groups are missing from the workspace',
PERMISSION_CHECK: 'permission-check',
},
};
diff --git a/server/src/modules/app-permissions/ability/index.ts b/server/src/modules/app-permissions/ability/index.ts
index d2e8c263b2..5a03f417d5 100644
--- a/server/src/modules/app-permissions/ability/index.ts
+++ b/server/src/modules/app-permissions/ability/index.ts
@@ -38,6 +38,10 @@ export class FeatureAbilityFactory extends AbilityFactory
FEATURE_KEY.CREATE_PAGE_PERMISSIONS,
FEATURE_KEY.UPDATE_PAGE_PERMISSIONS,
FEATURE_KEY.DELETE_PAGE_PERMISSIONS,
+ FEATURE_KEY.FETCH_QUERY_PERMISSIONS,
+ FEATURE_KEY.CREATE_QUERY_PERMISSIONS,
+ FEATURE_KEY.UPDATE_QUERY_PERMISSIONS,
+ FEATURE_KEY.DELETE_QUERY_PERMISSIONS,
],
App
);
@@ -56,6 +60,10 @@ export class FeatureAbilityFactory extends AbilityFactory
FEATURE_KEY.CREATE_PAGE_PERMISSIONS,
FEATURE_KEY.UPDATE_PAGE_PERMISSIONS,
FEATURE_KEY.DELETE_PAGE_PERMISSIONS,
+ FEATURE_KEY.FETCH_QUERY_PERMISSIONS,
+ FEATURE_KEY.CREATE_QUERY_PERMISSIONS,
+ FEATURE_KEY.UPDATE_QUERY_PERMISSIONS,
+ FEATURE_KEY.DELETE_QUERY_PERMISSIONS,
],
App
);
@@ -66,7 +74,15 @@ export class FeatureAbilityFactory extends AbilityFactory
isAllAppsViewable ||
(userAppPermissions?.viewableAppsId?.length && appId && userAppPermissions.viewableAppsId.includes(appId))
) {
- can([FEATURE_KEY.FETCH_USERS, FEATURE_KEY.FETCH_USER_GROUPS, FEATURE_KEY.FETCH_PAGE_PERMISSIONS], App);
+ can(
+ [
+ FEATURE_KEY.FETCH_USERS,
+ FEATURE_KEY.FETCH_USER_GROUPS,
+ FEATURE_KEY.FETCH_PAGE_PERMISSIONS,
+ FEATURE_KEY.FETCH_QUERY_PERMISSIONS,
+ ],
+ App
+ );
}
}
}
diff --git a/server/src/modules/app-permissions/constants/features.ts b/server/src/modules/app-permissions/constants/features.ts
index 6d77625ec5..360b1cf4c9 100644
--- a/server/src/modules/app-permissions/constants/features.ts
+++ b/server/src/modules/app-permissions/constants/features.ts
@@ -10,5 +10,9 @@ export const FEATURES: FeaturesConfig = {
[FEATURE_KEY.CREATE_PAGE_PERMISSIONS]: {},
[FEATURE_KEY.UPDATE_PAGE_PERMISSIONS]: {},
[FEATURE_KEY.DELETE_PAGE_PERMISSIONS]: {},
+ [FEATURE_KEY.FETCH_QUERY_PERMISSIONS]: {},
+ [FEATURE_KEY.CREATE_QUERY_PERMISSIONS]: {},
+ [FEATURE_KEY.UPDATE_QUERY_PERMISSIONS]: {},
+ [FEATURE_KEY.DELETE_QUERY_PERMISSIONS]: {},
},
};
diff --git a/server/src/modules/app-permissions/constants/index.ts b/server/src/modules/app-permissions/constants/index.ts
index c1d2afe78b..ff5063e948 100644
--- a/server/src/modules/app-permissions/constants/index.ts
+++ b/server/src/modules/app-permissions/constants/index.ts
@@ -4,6 +4,12 @@ export enum PAGE_PERMISSION_TYPE {
ALL = 'ALL',
}
+export enum PERMISSION_ENTITY_TYPE {
+ PAGE = 'PAGE',
+ QUERY = 'QUERY',
+ COMPONENT = 'COMPONENT',
+}
+
export enum FEATURE_KEY {
FETCH_USERS = 'fetch_users',
FETCH_USER_GROUPS = 'fetch_user_groups',
@@ -11,4 +17,8 @@ export enum FEATURE_KEY {
CREATE_PAGE_PERMISSIONS = 'create_page_permissions',
UPDATE_PAGE_PERMISSIONS = 'update_page_permissions',
DELETE_PAGE_PERMISSIONS = 'delete_page_permissions',
+ FETCH_QUERY_PERMISSIONS = 'fetch_query_permissions',
+ CREATE_QUERY_PERMISSIONS = 'create_query_permissions',
+ UPDATE_QUERY_PERMISSIONS = 'update_query_permissions',
+ DELETE_QUERY_PERMISSIONS = 'delete_query_permissions',
}
diff --git a/server/src/modules/app-permissions/controller.ts b/server/src/modules/app-permissions/controller.ts
index 2d0ccea9ce..d317e3115b 100644
--- a/server/src/modules/app-permissions/controller.ts
+++ b/server/src/modules/app-permissions/controller.ts
@@ -8,7 +8,7 @@ import { MODULES } from '@modules/app/constants/modules';
import { InitFeature } from '@modules/app/decorators/init-feature.decorator';
import { FEATURE_KEY } from './constants';
import { JwtAuthGuard } from '@modules/session/guards/jwt-auth.guard';
-import { CreatePagePermissionDto } from './dto';
+import { CreatePermissionDto } from './dto';
@InitModule(MODULES.APP_PERMISSIONS)
@UseGuards(JwtAuthGuard, FeatureAbilityGuard)
@@ -53,7 +53,7 @@ export class AppPermissionsController implements IAppPermissionsController {
@User() user,
@Param('appId') appId: string,
@Param('pageId') pageId: string,
- @Body() body: CreatePagePermissionDto,
+ @Body() body: CreatePermissionDto,
@Res({ passthrough: true }) response: Response
): Promise {
throw new NotFoundException();
@@ -65,7 +65,7 @@ export class AppPermissionsController implements IAppPermissionsController {
@User() user,
@Param('appId') appId: string,
@Param('pageId') pageId: string,
- @Body() body: CreatePagePermissionDto,
+ @Body() body: CreatePermissionDto,
@Res({ passthrough: true }) response: Response
): Promise {
throw new NotFoundException();
@@ -81,4 +81,50 @@ export class AppPermissionsController implements IAppPermissionsController {
): Promise {
throw new NotFoundException();
}
+
+ @InitFeature(FEATURE_KEY.FETCH_QUERY_PERMISSIONS)
+ @Get(':appId/queries/:queryId')
+ async fetchQueryPermissions(
+ @User() user,
+ @Param('appId') appId: string,
+ @Param('queryId') queryId: string,
+ @Res({ passthrough: true }) response: Response
+ ): Promise {
+ throw new NotFoundException();
+ }
+
+ @InitFeature(FEATURE_KEY.CREATE_QUERY_PERMISSIONS)
+ @Post(':appId/queries/:queryId')
+ async createQueryPermissions(
+ @User() user,
+ @Param('appId') appId: string,
+ @Param('queryId') queryId: string,
+ @Body() body: CreatePermissionDto,
+ @Res({ passthrough: true }) response: Response
+ ): Promise {
+ throw new NotFoundException();
+ }
+
+ @InitFeature(FEATURE_KEY.UPDATE_QUERY_PERMISSIONS)
+ @Put(':appId/queries/:queryId')
+ async updateQueryPermissions(
+ @User() user,
+ @Param('appId') appId: string,
+ @Param('queryId') queryId: string,
+ @Body() body: CreatePermissionDto,
+ @Res({ passthrough: true }) response: Response
+ ): Promise {
+ throw new NotFoundException();
+ }
+
+ @InitFeature(FEATURE_KEY.DELETE_QUERY_PERMISSIONS)
+ @Delete(':appId/queries/:queryId')
+ async deleteQueryPermissions(
+ @User() user,
+ @Param('appId') appId: string,
+ @Param('queryId') queryId: string,
+ @Res({ passthrough: true }) response: Response
+ ): Promise {
+ throw new NotFoundException();
+ }
}
diff --git a/server/src/modules/app-permissions/dto/index.ts b/server/src/modules/app-permissions/dto/index.ts
index 20a1bd98b8..b32fafe0a1 100644
--- a/server/src/modules/app-permissions/dto/index.ts
+++ b/server/src/modules/app-permissions/dto/index.ts
@@ -2,10 +2,10 @@ import { IsUUID, IsEnum, IsArray, IsString, IsOptional, ValidateIf } from 'class
import { Type } from 'class-transformer';
import { PAGE_PERMISSION_TYPE } from '../constants';
-export class CreatePagePermissionDto {
+export class CreatePermissionDto {
@IsUUID(4)
@IsOptional()
- pageId: string;
+ id: string;
@IsEnum(PAGE_PERMISSION_TYPE)
type: PAGE_PERMISSION_TYPE;
diff --git a/server/src/modules/app-permissions/interfaces/IController.ts b/server/src/modules/app-permissions/interfaces/IController.ts
index bfa35aa730..a7808d280d 100644
--- a/server/src/modules/app-permissions/interfaces/IController.ts
+++ b/server/src/modules/app-permissions/interfaces/IController.ts
@@ -1,6 +1,6 @@
import { User } from '@entities/user.entity';
import { Response } from 'express';
-import { CreatePagePermissionDto } from '../dto';
+import { CreatePermissionDto } from '../dto';
export interface IAppPermissionsController {
fetchUsers(user: User, appId: string, response: Response): Promise;
@@ -13,7 +13,7 @@ export interface IAppPermissionsController {
user: User,
appId: string,
pageId: string,
- body: CreatePagePermissionDto,
+ body: CreatePermissionDto,
response: Response
): Promise;
@@ -21,9 +21,29 @@ export interface IAppPermissionsController {
user: User,
appId: string,
pageId: string,
- body: CreatePagePermissionDto,
+ body: CreatePermissionDto,
response: Response
): Promise;
deletePagePermissions(user: User, appId: string, pageId: string, response: Response): Promise;
+
+ fetchQueryPermissions(user: User, appId: string, queryId: string, response: Response): Promise;
+
+ createQueryPermissions(
+ user: User,
+ appId: string,
+ queryId: string,
+ body: CreatePermissionDto,
+ response: Response
+ ): Promise;
+
+ updateQueryPermissions(
+ user: User,
+ appId: string,
+ queryId: string,
+ body: CreatePermissionDto,
+ response: Response
+ ): Promise;
+
+ deleteQueryPermissions(user: User, appId: string, queryId: string, response: Response): Promise;
}
diff --git a/server/src/modules/app-permissions/interfaces/IService.ts b/server/src/modules/app-permissions/interfaces/IService.ts
index cad5fef726..aa1892da43 100644
--- a/server/src/modules/app-permissions/interfaces/IService.ts
+++ b/server/src/modules/app-permissions/interfaces/IService.ts
@@ -1,16 +1,23 @@
import { User } from '@entities/user.entity';
-import { CreatePagePermissionDto } from '../dto';
+import { CreatePermissionDto } from '../dto';
+import { PERMISSION_ENTITY_TYPE } from '../constants';
export interface IAppPermissionsService {
fetchUsers(appId: string, user: User): Promise;
fetchUserGroups(appId: string, user: User): Promise;
- fetchPagePermissions(pageId: string): Promise;
+ fetchAppPermissions(type: PERMISSION_ENTITY_TYPE, id: string): Promise;
- createPagePermissions(pageId: string, body: CreatePagePermissionDto): Promise;
+ createAppPermissions(type: PERMISSION_ENTITY_TYPE, id: string, body: CreatePermissionDto): Promise;
- updatePagePermissions(appId: string, pageId: string, body: CreatePagePermissionDto, user: User): Promise;
+ updateAppPermissions(
+ type: PERMISSION_ENTITY_TYPE,
+ appId: string,
+ id: string,
+ body: CreatePermissionDto,
+ user: User
+ ): Promise;
- deletePagePermissions(pageId: string): Promise;
+ deleteAppPermissions(type: PERMISSION_ENTITY_TYPE, id: string): Promise;
}
diff --git a/server/src/modules/app-permissions/interfaces/IUtilService.ts b/server/src/modules/app-permissions/interfaces/IUtilService.ts
index 06654ed9e9..dbd390982a 100644
--- a/server/src/modules/app-permissions/interfaces/IUtilService.ts
+++ b/server/src/modules/app-permissions/interfaces/IUtilService.ts
@@ -1,13 +1,17 @@
import { User } from '@entities/user.entity';
import { GroupPermissions } from '@entities/group_permissions.entity';
-import { CreatePagePermissionDto } from '../dto';
+import { CreatePermissionDto } from '../dto';
export interface IUtilService {
getUsersWithViewAccess(appId: string, organizationId: string): Promise;
getUserGroupsWithViewAccess(appId: string, organizationId: string): Promise;
- createPagePermission(pageId: string, body: CreatePagePermissionDto): Promise;
+ createPagePermission(pageId: string, body: CreatePermissionDto): Promise;
- updatePagePermission(pageId: string, body: CreatePagePermissionDto): Promise;
+ updatePagePermission(pageId: string, body: CreatePermissionDto): Promise;
+
+ createQueryPermission(queryId: string, body: CreatePermissionDto): Promise;
+
+ updateQueryPermission(queryId: string, body: CreatePermissionDto): Promise;
}
diff --git a/server/src/modules/app-permissions/module.ts b/server/src/modules/app-permissions/module.ts
index 704c6c1374..5e3e3db107 100644
--- a/server/src/modules/app-permissions/module.ts
+++ b/server/src/modules/app-permissions/module.ts
@@ -7,8 +7,12 @@ import { User } from '@entities/user.entity';
import { RolesRepository } from '@modules/roles/repository';
import { PageUsersRepository } from './repositories/page-users.repository';
import { PagePermissionsRepository } from './repositories/page-permissions.repository';
+import { QueryUsersRepository } from './repositories/query-users.repository';
+import { QueryPermissionsRepository } from './repositories/query-permissions.repository';
import { PageUser } from '@entities/page_users.entity';
import { PagePermission } from '@entities/page_permissions.entity';
+import { QueryUser } from '@entities/query_users.entity';
+import { QueryPermission } from '@entities/query_permissions.entity';
export class AppPermissionsModule {
static async register(configs: { IS_GET_CONTEXT: boolean }): Promise {
@@ -19,7 +23,9 @@ export class AppPermissionsModule {
return {
module: AppPermissionsModule,
- imports: [TypeOrmModule.forFeature([GroupPermissions, User, PageUser, PagePermission])],
+ imports: [
+ TypeOrmModule.forFeature([GroupPermissions, User, PageUser, PagePermission, QueryUser, QueryPermission]),
+ ],
controllers: [AppPermissionsController],
providers: [
AppPermissionsService,
@@ -27,6 +33,8 @@ export class AppPermissionsModule {
RolesRepository,
PageUsersRepository,
PagePermissionsRepository,
+ QueryUsersRepository,
+ QueryPermissionsRepository,
FeatureAbilityFactory,
],
exports: [AppPermissionsUtilService, AppPermissionsService],
diff --git a/server/src/modules/app-permissions/repositories/query-permissions.repository.ts b/server/src/modules/app-permissions/repositories/query-permissions.repository.ts
new file mode 100644
index 0000000000..d83976ca68
--- /dev/null
+++ b/server/src/modules/app-permissions/repositories/query-permissions.repository.ts
@@ -0,0 +1,58 @@
+import { QueryPermission } from '@entities/query_permissions.entity';
+import { Injectable } from '@nestjs/common';
+import { DataSource, EntityManager, Repository } from 'typeorm';
+import { QueryUsersRepository } from './query-users.repository';
+import { dbTransactionWrap } from '@helpers/database.helper';
+import { PAGE_PERMISSION_TYPE } from '../constants';
+
+@Injectable()
+export class QueryPermissionsRepository extends Repository {
+ constructor(private dataSource: DataSource, private readonly queryUsersRepository: QueryUsersRepository) {
+ super(QueryPermission, dataSource.createEntityManager());
+ }
+
+ async getQueryPermissions(queryId: string, manager?: EntityManager): Promise {
+ return dbTransactionWrap(async (manager: EntityManager) => {
+ const queryPermissions = await manager.find(QueryPermission, {
+ where: { queryId },
+ relations: ['users', 'users.user', 'users.permissionGroup'],
+ });
+
+ return queryPermissions.map((permission) => {
+ if (permission.type === PAGE_PERMISSION_TYPE.GROUP) {
+ return {
+ ...permission,
+ groups: permission.users,
+ users: undefined,
+ };
+ }
+ return permission;
+ });
+ }, manager || this.manager);
+ }
+
+ async createQueryPermissions(
+ queryId: string,
+ type: PAGE_PERMISSION_TYPE,
+ manager?: EntityManager
+ ): Promise {
+ return dbTransactionWrap(async (manager: EntityManager) => {
+ const existingPermission = await manager.findOne(QueryPermission, { where: { queryId } });
+ if (existingPermission) {
+ throw new Error(`Query permission already exists for Query id: ${queryId}`);
+ }
+
+ const queryPermission = manager.create(QueryPermission, {
+ queryId,
+ type,
+ });
+ return manager.save(queryPermission);
+ }, manager || this.manager);
+ }
+
+ async deleteQueryPermissions(queryId: string, manager?: EntityManager): Promise {
+ return dbTransactionWrap(async (manager: EntityManager) => {
+ await manager.delete(QueryPermission, { queryId });
+ }, manager || this.manager);
+ }
+}
diff --git a/server/src/modules/app-permissions/repositories/query-users.repository.ts b/server/src/modules/app-permissions/repositories/query-users.repository.ts
new file mode 100644
index 0000000000..b5a2cc08fa
--- /dev/null
+++ b/server/src/modules/app-permissions/repositories/query-users.repository.ts
@@ -0,0 +1,83 @@
+import { QueryUser } from '@entities/query_users.entity';
+import { Injectable } from '@nestjs/common';
+import { DataSource, EntityManager, Repository } from 'typeorm';
+import { dbTransactionWrap } from '@helpers/database.helper';
+import { QueryPermission } from '@entities/query_permissions.entity';
+
+@Injectable()
+export class QueryUsersRepository extends Repository {
+ constructor(private dataSource: DataSource) {
+ super(QueryUser, dataSource.createEntityManager());
+ }
+
+ async createQueryUsersWithSingle(
+ queryPermissionsId: string,
+ users: string[],
+ manager?: EntityManager
+ ): Promise {
+ return dbTransactionWrap(async (manager: EntityManager) => {
+ const queryUsers = users.map((userId) => {
+ return manager.create(QueryUser, {
+ queryPermissionsId,
+ userId,
+ permissionGroupsId: null,
+ });
+ });
+ return manager.save(queryUsers);
+ }, manager || this.manager);
+ }
+
+ async createQueryUsersWithGroup(
+ queryPermissionsId: string,
+ groups: string[],
+ manager?: EntityManager
+ ): Promise {
+ return dbTransactionWrap(async (manager: EntityManager) => {
+ const queryUsers = groups.map((permissionGroupsId) => {
+ return manager.create(QueryUser, {
+ queryPermissionsId,
+ permissionGroupsId,
+ userId: null,
+ });
+ });
+ return manager.save(queryUsers);
+ }, manager || this.manager);
+ }
+
+ async checkQueryUserWithGroup(
+ queryPermission: QueryPermission,
+ userId: string,
+ manager?: EntityManager
+ ): Promise {
+ return dbTransactionWrap(async (manager: EntityManager) => {
+ const result = await manager
+ .createQueryBuilder(QueryUser, 'query_users')
+ .innerJoin('query_users.permissionGroup', 'group')
+ .innerJoin('group.groupUsers', 'groupUser')
+ .where('query_users.queryPermission = :permissionId', {
+ permissionId: queryPermission.id,
+ })
+ .andWhere('groupUser.userId = :userId', { userId })
+ .getOne();
+
+ return !!result;
+ }, manager || this.manager);
+ }
+
+ async checkQueryUserWithSingle(
+ queryPermission: QueryPermission,
+ userId: string,
+ manager?: EntityManager
+ ): Promise {
+ return dbTransactionWrap(async (manager: EntityManager) => {
+ const queryUser = await manager.findOne(QueryUser, {
+ where: {
+ queryPermission: { id: queryPermission.id },
+ userId,
+ },
+ });
+
+ return !!queryUser;
+ }, manager || this.manager);
+ }
+}
diff --git a/server/src/modules/app-permissions/service.ts b/server/src/modules/app-permissions/service.ts
index c6b5bc640d..0aed033079 100644
--- a/server/src/modules/app-permissions/service.ts
+++ b/server/src/modules/app-permissions/service.ts
@@ -13,19 +13,19 @@ export class AppPermissionsService implements IAppPermissionsService {
throw new Error('Method not implemented.');
}
- async fetchPagePermissions(pageId) {
+ async fetchAppPermissions(type, id) {
throw new Error('Method not implemented.');
}
- async createPagePermissions(pageId, body) {
+ async createAppPermissions(type, id, body) {
throw new Error('Method not implemented.');
}
- async updatePagePermissions(appId, pageId, body, user) {
+ async updateAppPermissions(type, appId, id, body, user) {
throw new Error('Method not implemented.');
}
- async deletePagePermissions(pageId) {
+ async deleteAppPermissions(type, id) {
throw new Error('Method not implemented.');
}
}
diff --git a/server/src/modules/app-permissions/types/index.ts b/server/src/modules/app-permissions/types/index.ts
index 86a41afba1..d377f5a08f 100644
--- a/server/src/modules/app-permissions/types/index.ts
+++ b/server/src/modules/app-permissions/types/index.ts
@@ -9,6 +9,10 @@ interface Features {
[FEATURE_KEY.CREATE_PAGE_PERMISSIONS]: FeatureConfig;
[FEATURE_KEY.UPDATE_PAGE_PERMISSIONS]: FeatureConfig;
[FEATURE_KEY.DELETE_PAGE_PERMISSIONS]: FeatureConfig;
+ [FEATURE_KEY.FETCH_QUERY_PERMISSIONS]: FeatureConfig;
+ [FEATURE_KEY.CREATE_QUERY_PERMISSIONS]: FeatureConfig;
+ [FEATURE_KEY.UPDATE_QUERY_PERMISSIONS]: FeatureConfig;
+ [FEATURE_KEY.DELETE_QUERY_PERMISSIONS]: FeatureConfig;
}
export interface FeaturesConfig {
diff --git a/server/src/modules/app-permissions/util.service.ts b/server/src/modules/app-permissions/util.service.ts
index 71432a0e4b..7ff894a058 100644
--- a/server/src/modules/app-permissions/util.service.ts
+++ b/server/src/modules/app-permissions/util.service.ts
@@ -2,7 +2,7 @@ import { User } from '@entities/user.entity';
import { IUtilService } from './interfaces/IUtilService';
import { Injectable } from '@nestjs/common';
import { GroupPermissions } from '@entities/group_permissions.entity';
-import { CreatePagePermissionDto } from './dto';
+import { CreatePermissionDto } from './dto';
@Injectable()
export class AppPermissionsUtilService implements IUtilService {
@@ -16,11 +16,19 @@ export class AppPermissionsUtilService implements IUtilService {
throw new Error('Method not implemented.');
}
- async createPagePermission(pageId: string, body: CreatePagePermissionDto): Promise {
+ async createPagePermission(pageId: string, body: CreatePermissionDto): Promise {
throw new Error('Method not implemented.');
}
- async updatePagePermission(pageId: string, body: CreatePagePermissionDto): Promise {
+ async updatePagePermission(pageId: string, body: CreatePermissionDto): Promise {
+ throw new Error('Method not implemented.');
+ }
+
+ async createQueryPermission(queryId: string, body: CreatePermissionDto): Promise {
+ throw new Error('Method not implemented.');
+ }
+
+ async updateQueryPermission(queryId: string, body: CreatePermissionDto): Promise {
throw new Error('Method not implemented.');
}
}
diff --git a/server/src/modules/apps/services/app-import-export.service.ts b/server/src/modules/apps/services/app-import-export.service.ts
index ccee419f3a..7a6a089548 100644
--- a/server/src/modules/apps/services/app-import-export.service.ts
+++ b/server/src/modules/apps/services/app-import-export.service.ts
@@ -39,6 +39,8 @@ import { PAGE_PERMISSION_TYPE } from '@modules/app-permissions/constants';
import { PagePermission } from '@entities/page_permissions.entity';
import { PageUser } from '@entities/page_users.entity';
import { UsersUtilService } from '@modules/users/util.service';
+import { QueryPermission } from '@entities/query_permissions.entity';
+import { QueryUser } from '@entities/query_users.entity';
interface AppResourceMappings {
defaultDataSourceIdMapping: Record;
dataQueryMapping: Record;
@@ -161,6 +163,9 @@ export class AppImportExportService {
if (dataSources?.length) {
dataQueries = await manager
.createQueryBuilder(DataQuery, 'data_queries')
+ .leftJoinAndSelect('data_queries.permissions', 'permission')
+ .leftJoinAndSelect('permission.users', 'queryUser')
+ .leftJoinAndSelect('queryUser.permissionGroup', 'permissionGroup')
.where('data_queries.dataSourceId IN(:...dataSourceId)', {
dataSourceId: dataSources?.map((v) => v.id),
})
@@ -213,6 +218,21 @@ export class AppImportExportService {
};
});
+ const queriesWithPermissionGroups = dataQueries.map((query) => {
+ const groupPermission = query.permissions.find((perm) => perm.type === 'GROUP');
+
+ return {
+ ...query,
+ permissions: groupPermission
+ ? {
+ permissionGroup: groupPermission.users
+ .map((user) => user.permissionGroup?.name)
+ .filter((name): name is string => Boolean(name)),
+ }
+ : undefined,
+ };
+ });
+
const components =
pages.length > 0
? await manager
@@ -236,7 +256,7 @@ export class AppImportExportService {
appToExport['components'] = components;
appToExport['pages'] = pagesWithPermissionGroups;
appToExport['events'] = events;
- appToExport['dataQueries'] = dataQueries;
+ appToExport['dataQueries'] = queriesWithPermissionGroups;
appToExport['dataSources'] = dataSources;
appToExport['appVersions'] = appVersions;
appToExport['appEnvironments'] = appEnvironments;
@@ -1119,7 +1139,15 @@ export class AppImportExportService {
});
await manager.save(newQuery);
+
+ if (importingQuery.permissions) {
+ newQuery.permissions = importingQuery.permissions;
+ }
+
appResourceMappings.dataQueryMapping[importingQuery.id] = newQuery.id;
+
+ //create query permissions of query if flag enabled in dto
+ await this.createQueryPermissionsForGroups(newQuery, organizationId, manager);
}
return appResourceMappings;
@@ -1355,7 +1383,7 @@ export class AppImportExportService {
return pageSettings;
}
- async checkIfGroupPermissionsExist(pages, organizationId) {
+ async checkIfGroupPermissionsExist(pages, queries, organizationId) {
const allGroupNames = new Set();
for (const page of pages) {
@@ -1365,6 +1393,15 @@ export class AppImportExportService {
}
}
+ for (const query of queries) {
+ const groupNames = query.permissions?.permissionGroup || [];
+ for (const name of groupNames) {
+ if (!allGroupNames.has(name)) {
+ allGroupNames.add(name);
+ }
+ }
+ }
+
if (!allGroupNames.size) return;
return await dbTransactionWrap(async (manager: EntityManager) => {
@@ -1425,6 +1462,41 @@ export class AppImportExportService {
await manager.save(pageUsers);
}
+ async createQueryPermissionsForGroups(query, organizationId: string, manager: EntityManager) {
+ const groupNames = query.permissions?.permissionGroup || [];
+ if (!groupNames.length) return;
+
+ const existingGroups = await manager
+ .createQueryBuilder(GroupPermissions, 'gp')
+ .where('gp.name IN (:...names)', { names: groupNames })
+ .andWhere('gp.organizationId = :organizationId', { organizationId })
+ .getMany();
+
+ const groupMap = new Map(existingGroups.map((g) => [g.name, g]));
+
+ // Filter to only existing group names
+ const validGroupNames = groupNames.filter((name) => groupMap.has(name));
+
+ // If no valid group names exist, do not create permissions
+ if (!validGroupNames.length) return;
+
+ const permission = manager.create(QueryPermission, {
+ queryId: query.id,
+ type: PAGE_PERMISSION_TYPE.GROUP,
+ });
+
+ const savedPermission = await manager.save(permission);
+
+ const queryUsers = validGroupNames.map((name) =>
+ manager.create(QueryUser, {
+ queryPermissionsId: savedPermission.id,
+ permissionGroupsId: groupMap.get(name).id,
+ })
+ );
+
+ await manager.save(queryUsers);
+ }
+
async createAppVersionsForImportedApp(
manager: EntityManager,
user: User,
diff --git a/server/src/modules/data-queries/controller.ts b/server/src/modules/data-queries/controller.ts
index 831834ef8a..e734810703 100644
--- a/server/src/modules/data-queries/controller.ts
+++ b/server/src/modules/data-queries/controller.ts
@@ -1,4 +1,4 @@
-import { Controller, Get, Param, Body, Post, Patch, Delete, UseGuards, Put, Res } from '@nestjs/common';
+import { Controller, Get, Param, Body, Post, Patch, Delete, UseGuards, Put, Res, Query } from '@nestjs/common';
import { JwtAuthGuard } from '@modules/session/guards/jwt-auth.guard';
import { DataQueriesService } from './service';
import { User, UserEntity } from '@modules/app/decorators/user.decorator';
@@ -30,8 +30,8 @@ export class DataQueriesController implements IDataQueriesController {
@InitFeature(FEATURE_KEY.GET)
@UseGuards(JwtAuthGuard, ValidateAppVersionGuard, ValidateQueryAppGuard, AppFeatureAbilityGuard)
@Get(':versionId')
- index(@Param('versionId') versionId: string) {
- return this.dataQueriesService.getAll(versionId);
+ index(@User() user: UserEntity, @Param('versionId') versionId: string, @Query('mode') mode?: string) {
+ return this.dataQueriesService.getAll(user, versionId, mode);
}
@InitFeature(FEATURE_KEY.CREATE)
@@ -113,7 +113,8 @@ export class DataQueriesController implements IDataQueriesController {
@Body() updateDataQueryDto: UpdateDataQueryDto,
@Ability() ability: AppAbility,
@DataSource() dataSource: DataSourceEntity,
- @Res({ passthrough: true }) response: Response
+ @Res({ passthrough: true }) response: Response,
+ @Query('mode') mode?: string
) {
return this.dataQueriesService.runQueryOnBuilder(
user,
@@ -122,7 +123,8 @@ export class DataQueriesController implements IDataQueriesController {
updateDataQueryDto,
ability,
dataSource,
- response
+ response,
+ mode
);
}
diff --git a/server/src/modules/data-queries/interfaces/IController.ts b/server/src/modules/data-queries/interfaces/IController.ts
index d68f28b382..59292772e2 100644
--- a/server/src/modules/data-queries/interfaces/IController.ts
+++ b/server/src/modules/data-queries/interfaces/IController.ts
@@ -7,7 +7,7 @@ import { App } from '@entities/app.entity';
import { UpdateSourceDto } from '../dto';
import { Response } from 'express';
export interface IDataQueriesController {
- index(versionId: string): Promise