Chore/refractor (#4454)
* markdown files and env examples cleanup * components update * update jsonlines description * server refractor * update telemetry * add execute custom node * add ui refractor * add username and password authenticate * correctly retrieve past images in agentflowv2 * disable e2e temporarily * add existing username and password authenticate * update migration to default workspace * update todo * blob storage migrating * throw error on agent tool call error * add missing execution import * add referral * chore: add error message when importData is undefined * migrate api keys to db * fix: data too long for column executionData * migrate api keys from json to db at init * add info on account setup * update docstore missing fields --------- Co-authored-by: chungyau97 <chungyau97@gmail.com>
@@ -30,7 +30,7 @@ const ErrorBoundary = ({ error }) => {
|
||||
<pre style={{ margin: 0, overflowWrap: 'break-word', whiteSpace: 'pre-wrap', textAlign: 'center' }}>
|
||||
<code>{`Status: ${error.response.status}`}</code>
|
||||
<br />
|
||||
<code>{error.response.data.message}</code>
|
||||
<code>{error.response?.data?.message}</code>
|
||||
</pre>
|
||||
</Box>
|
||||
</Card>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import client from '@/api/client'
|
||||
|
||||
const inviteAccount = (body) => client.post(`/account/invite`, body)
|
||||
const registerAccount = (body) => client.post(`/account/register`, body)
|
||||
const verifyAccountEmail = (body) => client.post('/account/verify', body)
|
||||
const resendVerificationEmail = (body) => client.post('/account/resend-verification', body)
|
||||
const forgotPassword = (body) => client.post('/account/forgot-password', body)
|
||||
const resetPassword = (body) => client.post('/account/reset-password', body)
|
||||
const getBillingData = () => client.get('/account/billing')
|
||||
const cancelSubscription = (body) => client.post('/account/cancel-subscription', body)
|
||||
const logout = () => client.post('/account/logout')
|
||||
const getBasicAuth = () => client.get('/account/basic-auth')
|
||||
const checkBasicAuth = (body) => client.post('/account/basic-auth', body)
|
||||
|
||||
export default {
|
||||
getBillingData,
|
||||
inviteAccount,
|
||||
registerAccount,
|
||||
verifyAccountEmail,
|
||||
resendVerificationEmail,
|
||||
forgotPassword,
|
||||
resetPassword,
|
||||
cancelSubscription,
|
||||
logout,
|
||||
getBasicAuth,
|
||||
checkBasicAuth
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import client from './client'
|
||||
|
||||
const fetchLoginActivity = (body) => client.post(`/audit/login-activity`, body)
|
||||
const deleteLoginActivity = (body) => client.post(`/audit/login-activity/delete`, body)
|
||||
|
||||
export default {
|
||||
fetchLoginActivity,
|
||||
deleteLoginActivity
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import client from './client'
|
||||
|
||||
// auth
|
||||
const resolveLogin = (body) => client.post(`/auth/resolve`, body)
|
||||
const login = (body) => client.post(`/auth/login`, body)
|
||||
|
||||
// permissions
|
||||
const getAllPermissions = () => client.get(`/auth/permissions`)
|
||||
|
||||
export default {
|
||||
resolveLogin,
|
||||
login,
|
||||
getAllPermissions
|
||||
}
|
||||
@@ -20,6 +20,8 @@ const getIsChatflowStreaming = (id) => client.get(`/chatflows-streaming/${id}`)
|
||||
|
||||
const getAllowChatflowUploads = (id) => client.get(`/chatflows-uploads/${id}`)
|
||||
|
||||
const getHasChatflowChanged = (id, lastUpdatedDateTime) => client.get(`/chatflows/has-changed/${id}/${lastUpdatedDateTime}`)
|
||||
|
||||
const generateAgentflow = (body) => client.post(`/agentflowv2-generator/generate`, body)
|
||||
|
||||
export default {
|
||||
@@ -33,5 +35,6 @@ export default {
|
||||
deleteChatflow,
|
||||
getIsChatflowStreaming,
|
||||
getAllowChatflowUploads,
|
||||
getHasChatflowChanged,
|
||||
generateAgentflow
|
||||
}
|
||||
|
||||
@@ -1,26 +1,39 @@
|
||||
import axios from 'axios'
|
||||
import { baseURL } from '@/store/constant'
|
||||
import { baseURL, ErrorMessage } from '@/store/constant'
|
||||
import AuthUtils from '@/utils/authUtils'
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: `${baseURL}/api/v1`,
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
'x-request-from': 'internal'
|
||||
}
|
||||
},
|
||||
withCredentials: true
|
||||
})
|
||||
|
||||
apiClient.interceptors.request.use(function (config) {
|
||||
const username = localStorage.getItem('username')
|
||||
const password = localStorage.getItem('password')
|
||||
|
||||
if (username && password) {
|
||||
config.auth = {
|
||||
username,
|
||||
password
|
||||
apiClient.interceptors.response.use(
|
||||
function (response) {
|
||||
return response
|
||||
},
|
||||
async (error) => {
|
||||
if (error.response.status === 401) {
|
||||
// check if refresh is needed
|
||||
if (error.response.data.message === ErrorMessage.TOKEN_EXPIRED && error.response.data.retry === true) {
|
||||
const originalRequest = error.config
|
||||
// call api to get new token
|
||||
const response = await axios.post(`${baseURL}/api/v1/auth/refreshToken`, {}, { withCredentials: true })
|
||||
if (response.data.id) {
|
||||
// retry the original request
|
||||
return apiClient.request(originalRequest)
|
||||
}
|
||||
}
|
||||
localStorage.removeItem('username')
|
||||
localStorage.removeItem('password')
|
||||
AuthUtils.removeCurrentUser()
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
})
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default apiClient
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import client from './client'
|
||||
|
||||
const getAllDatasets = () => client.get('/datasets')
|
||||
|
||||
//dataset
|
||||
const getDataset = (id) => client.get(`/datasets/set/${id}`)
|
||||
const createDataset = (body) => client.post(`/datasets/set`, body)
|
||||
const updateDataset = (id, body) => client.put(`/datasets/set/${id}`, body)
|
||||
const deleteDataset = (id) => client.delete(`/datasets/set/${id}`)
|
||||
|
||||
//rows
|
||||
const createDatasetRow = (body) => client.post(`/datasets/rows`, body)
|
||||
const updateDatasetRow = (id, body) => client.put(`/datasets/rows/${id}`, body)
|
||||
const deleteDatasetRow = (id) => client.delete(`/datasets/rows/${id}`)
|
||||
const deleteDatasetItems = (ids) => client.patch(`/datasets/rows`, { ids })
|
||||
|
||||
const reorderDatasetRow = (body) => client.post(`/datasets/reorder`, body)
|
||||
|
||||
export default {
|
||||
getAllDatasets,
|
||||
getDataset,
|
||||
createDataset,
|
||||
updateDataset,
|
||||
deleteDataset,
|
||||
createDatasetRow,
|
||||
updateDatasetRow,
|
||||
deleteDatasetRow,
|
||||
deleteDatasetItems,
|
||||
reorderDatasetRow
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import client from './client'
|
||||
|
||||
//evaluation
|
||||
const getAllEvaluations = () => client.get('/evaluations')
|
||||
const getIsOutdated = (id) => client.get(`/evaluations/is-outdated/${id}`)
|
||||
const getEvaluation = (id) => client.get(`/evaluations/${id}`)
|
||||
const createEvaluation = (body) => client.post(`/evaluations`, body)
|
||||
const deleteEvaluation = (id) => client.delete(`/evaluations/${id}`)
|
||||
const runAgain = (id) => client.get(`/evaluations/run-again/${id}`)
|
||||
const getVersions = (id) => client.get(`/evaluations/versions/${id}`)
|
||||
const deleteEvaluations = (ids, isDeleteAllVersion) => client.patch(`/evaluations`, { ids, isDeleteAllVersion })
|
||||
|
||||
export default {
|
||||
createEvaluation,
|
||||
deleteEvaluation,
|
||||
getAllEvaluations,
|
||||
getEvaluation,
|
||||
getIsOutdated,
|
||||
runAgain,
|
||||
getVersions,
|
||||
deleteEvaluations
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import client from './client'
|
||||
|
||||
const getAllEvaluators = () => client.get('/evaluators')
|
||||
|
||||
//evaluators
|
||||
const createEvaluator = (body) => client.post(`/evaluators`, body)
|
||||
const getEvaluator = (id) => client.get(`/evaluators/${id}`)
|
||||
const updateEvaluator = (id, body) => client.put(`/evaluators/${id}`, body)
|
||||
const deleteEvaluator = (id) => client.delete(`/evaluators/${id}`)
|
||||
|
||||
export default {
|
||||
getAllEvaluators,
|
||||
createEvaluator,
|
||||
getEvaluator,
|
||||
updateEvaluator,
|
||||
deleteEvaluator
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import client from './client'
|
||||
|
||||
const getAllFiles = () => client.get('/files')
|
||||
|
||||
const deleteFile = (path) => client.delete(`/files`, { params: { path } })
|
||||
|
||||
export default {
|
||||
getAllFiles,
|
||||
deleteFile
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import client from './client'
|
||||
|
||||
const getLogs = (startDate, endDate) => client.get(`/logs?startDate=${startDate}&endDate=${endDate}`)
|
||||
|
||||
export default {
|
||||
getLogs
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import client from '@/api/client'
|
||||
|
||||
// TODO: use this endpoint but without the org id because org id will be null
|
||||
const getLoginMethods = (organizationId) => client.get(`/loginmethod?organizationId=${organizationId}`)
|
||||
// TODO: don't use this endpoint.
|
||||
const getDefaultLoginMethods = () => client.get(`/loginmethod/default`)
|
||||
const updateLoginMethods = (body) => client.put(`/loginmethod`, body)
|
||||
|
||||
const testLoginMethod = (body) => client.post(`/loginmethod/test`, body)
|
||||
|
||||
export default {
|
||||
getLoginMethods,
|
||||
updateLoginMethods,
|
||||
testLoginMethod,
|
||||
getDefaultLoginMethods
|
||||
}
|
||||
@@ -7,9 +7,12 @@ const getNodesByCategory = (name) => client.get(`/nodes/category/${name}`)
|
||||
|
||||
const executeCustomFunctionNode = (body) => client.post(`/node-custom-function`, body)
|
||||
|
||||
const executeNodeLoadMethod = (name, body) => client.post(`/node-load-method/${name}`, body)
|
||||
|
||||
export default {
|
||||
getAllNodes,
|
||||
getSpecificNode,
|
||||
executeCustomFunctionNode,
|
||||
getNodesByCategory
|
||||
getNodesByCategory,
|
||||
executeNodeLoadMethod
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import client from './client'
|
||||
|
||||
const getSettings = () => client.get('/settings')
|
||||
|
||||
export default {
|
||||
getSettings
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import client from '@/api/client'
|
||||
|
||||
const getPricingPlans = (body) => client.get(`/pricing`, body)
|
||||
|
||||
export default {
|
||||
getPricingPlans
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import client from './client'
|
||||
|
||||
const getAllRolesByOrganizationId = (organizationId) => client.get(`/role?organizationId=${organizationId}`)
|
||||
const getRoleById = (id) => client.get(`/auth/roles/${id}`)
|
||||
const createRole = (body) => client.post(`/role`, body)
|
||||
const updateRole = (body) => client.put(`/role`, body)
|
||||
const getRoleByName = (name) => client.get(`/auth/roles/name/${name}`)
|
||||
const deleteRole = (id, organizationId) => client.delete(`/role?id=${id}&organizationId=${organizationId}`)
|
||||
|
||||
export default {
|
||||
getAllRolesByOrganizationId,
|
||||
getRoleById,
|
||||
createRole,
|
||||
updateRole,
|
||||
getRoleByName,
|
||||
deleteRole
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import client from './client'
|
||||
|
||||
const ssoLogin = (providerName) => client.get(`/${providerName}/login`)
|
||||
|
||||
export default {
|
||||
ssoLogin
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import client from './client'
|
||||
|
||||
// users
|
||||
const getUserById = (id) => client.get(`/user?id=${id}`)
|
||||
const updateUser = (body) => client.put(`/user`, body)
|
||||
|
||||
// organization users
|
||||
const getAllUsersByOrganizationId = (organizationId) => client.get(`/organizationuser?organizationId=${organizationId}`)
|
||||
const getUserByUserIdOrganizationId = (organizationId, userId) =>
|
||||
client.get(`/organizationuser?organizationId=${organizationId}&userId=${userId}`)
|
||||
const getOrganizationsByUserId = (userId) => client.get(`/organizationuser?userId=${userId}`)
|
||||
const updateOrganizationUser = (body) => client.put(`/organizationuser`, body)
|
||||
const deleteOrganizationUser = (organizationId, userId) =>
|
||||
client.delete(`/organizationuser?organizationId=${organizationId}&userId=${userId}`)
|
||||
|
||||
const getAdditionalSeatsQuantity = (subscriptionId) =>
|
||||
client.get(`/organization/additional-seats-quantity?subscriptionId=${subscriptionId}`)
|
||||
const getCustomerDefaultSource = (customerId) => client.get(`/organization/customer-default-source?customerId=${customerId}`)
|
||||
const getAdditionalSeatsProration = (subscriptionId, quantity) =>
|
||||
client.get(`/organization/additional-seats-proration?subscriptionId=${subscriptionId}&quantity=${quantity}`)
|
||||
const updateAdditionalSeats = (subscriptionId, quantity, prorationDate) =>
|
||||
client.post(`/organization/update-additional-seats`, { subscriptionId, quantity, prorationDate })
|
||||
const getPlanProration = (subscriptionId, newPlanId) =>
|
||||
client.get(`/organization/plan-proration?subscriptionId=${subscriptionId}&newPlanId=${newPlanId}`)
|
||||
const updateSubscriptionPlan = (subscriptionId, newPlanId, prorationDate) =>
|
||||
client.post(`/organization/update-subscription-plan`, { subscriptionId, newPlanId, prorationDate })
|
||||
const getCurrentUsage = () => client.get(`/organization/get-current-usage`)
|
||||
|
||||
// workspace users
|
||||
const getAllUsersByWorkspaceId = (workspaceId) => client.get(`/workspaceuser?workspaceId=${workspaceId}`)
|
||||
const getUserByRoleId = (roleId) => client.get(`/workspaceuser?roleId=${roleId}`)
|
||||
const getUserByUserIdWorkspaceId = (userId, workspaceId) => client.get(`/workspaceuser?userId=${userId}&workspaceId=${workspaceId}`)
|
||||
const getWorkspacesByUserId = (userId) => client.get(`/workspaceuser?userId=${userId}`)
|
||||
const getWorkspacesByOrganizationIdUserId = (organizationId, userId) =>
|
||||
client.get(`/workspaceuser?organizationId=${organizationId}&userId=${userId}`)
|
||||
const deleteWorkspaceUser = (workspaceId, userId) => client.delete(`/workspaceuser?workspaceId=${workspaceId}&userId=${userId}`)
|
||||
|
||||
export default {
|
||||
getUserById,
|
||||
updateUser,
|
||||
getAllUsersByOrganizationId,
|
||||
getUserByUserIdOrganizationId,
|
||||
getOrganizationsByUserId,
|
||||
getAllUsersByWorkspaceId,
|
||||
getUserByRoleId,
|
||||
getUserByUserIdWorkspaceId,
|
||||
getWorkspacesByUserId,
|
||||
getWorkspacesByOrganizationIdUserId,
|
||||
updateOrganizationUser,
|
||||
deleteWorkspaceUser,
|
||||
getAdditionalSeatsQuantity,
|
||||
getCustomerDefaultSource,
|
||||
getAdditionalSeatsProration,
|
||||
updateAdditionalSeats,
|
||||
getPlanProration,
|
||||
updateSubscriptionPlan,
|
||||
getCurrentUsage,
|
||||
deleteOrganizationUser
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import client from './client'
|
||||
|
||||
const getAllWorkspacesByOrganizationId = (organizationId) => client.get(`/workspace?organizationId=${organizationId}`)
|
||||
|
||||
const getWorkspaceById = (id) => client.get(`/workspace?id=${id}`)
|
||||
|
||||
const unlinkUsers = (id, body) => client.post(`/workspace/unlink-users/${id}`, body)
|
||||
const linkUsers = (id, body) => client.post(`/workspace/link-users/${id}`, body)
|
||||
|
||||
const switchWorkspace = (id) => client.post(`/workspace/switch?id=${id}`)
|
||||
|
||||
const createWorkspace = (body) => client.post(`/workspace`, body)
|
||||
const updateWorkspace = (body) => client.put(`/workspace`, body)
|
||||
const deleteWorkspace = (id) => client.delete(`/workspace/${id}`)
|
||||
|
||||
const getSharedWorkspacesForItem = (id) => client.get(`/workspace/shared/${id}`)
|
||||
const setSharedWorkspacesForItem = (id, body) => client.post(`/workspace/shared/${id}`, body)
|
||||
|
||||
export default {
|
||||
getAllWorkspacesByOrganizationId,
|
||||
getWorkspaceById,
|
||||
createWorkspace,
|
||||
updateWorkspace,
|
||||
deleteWorkspace,
|
||||
unlinkUsers,
|
||||
linkUsers,
|
||||
switchWorkspace,
|
||||
getSharedWorkspacesForItem,
|
||||
setSharedWorkspacesForItem
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none"><path fill="#000000" d="M12.549 1h-4.55l1.407 4.38h4.548l-3.68 2.61 1.406 4.405c2.37-1.725 3.143-4.336 2.274-7.016L12.55 1zM2.045 5.38h4.55L8 1H3.45L2.045 5.38c-.868 2.68-.094 5.29 2.275 7.015L5.725 7.99l-3.68-2.612zm2.275 7.015L8 15l3.68-2.605L8 9.745l-3.68 2.65z"/></svg>
|
||||
|
After Width: | Height: | Size: 493 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" width="561" height="493" viewBox="0 0 561 493" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M876.03027,689.45c-.98047,1.37-1.97021,2.73-2.95019,4.08A16.82838,16.82838,0,0,1,863.5,696.5h-527a16.90383,16.90383,0,0,1-9.21-2.72c-.91016-1.2-1.81006-2.41-2.72022-3.62006l.91016-.5L592.27,541.78a16.01919,16.01919,0,0,1,15.47021-.02L875.12988,688.95Z" transform="translate(-319.5 -203.5)" fill="#6c63ff"/><path d="M863.5,378.5,632.28169,244.96964a64.023,64.023,0,0,0-63.98147-.03153L336.5,378.5a17.0241,17.0241,0,0,0-17,17v284a17.01984,17.01984,0,0,0,17,17h527a17.02879,17.02879,0,0,0,17-17v-284A17.02408,17.02408,0,0,0,863.5,378.5Zm15,301a15.03649,15.03649,0,0,1-15,15h-527a15.02706,15.02706,0,0,1-15-15v-284a15.01828,15.01828,0,0,1,15-15L568.30022,246.93811a64.023,64.023,0,0,1,63.98147.03153L863.5,380.5a15.01828,15.01828,0,0,1,15,15Z" transform="translate(-319.5 -203.5)" fill="#3f3d56"/><path d="M600.2998,539.18018a15.36345,15.36345,0,0,1-5.116-.8584l-.30249-.10694-.06128-.67236c-.18848.09277-.37866.18164-.56909.26563l-.20118.08837-.20141-.08886c-.42139-.18506-.83985-.39453-1.24365-.62207L408.5,433.73242V222.5A18.5208,18.5208,0,0,1,427,204H773a18.5208,18.5208,0,0,1,18.5,18.5V434.00244l-.25488.14356-183.25,103.04A15.75694,15.75694,0,0,1,600.2998,539.18018Z" transform="translate(-319.5 -203.5)" fill="#fff"/><path d="M600.2998,539.68018a15.85649,15.85649,0,0,1-5.282-.88672l-.60547-.21338-.02588-.28565-.33691.14795-.40234-.17676c-.43653-.19189-.86963-.40869-1.28784-.64453L408,434.02539V222.5a19.02154,19.02154,0,0,1,19-19H773a19.02162,19.02162,0,0,1,19,19V434.29492L608.24,537.62158A16.2527,16.2527,0,0,1,600.2998,539.68018Zm-4.01342-2.57666a14.49247,14.49247,0,0,0,10.97436-1.22559L790,433.125V222.5a17.01917,17.01917,0,0,0-17-17H427a17.01909,17.01909,0,0,0-17,17V432.85449l11.98962,6.7334,171.35047,96.29053q.34973.197.71.3706.36035-.17358.70923-.37011l1.34668-.75879Z" transform="translate(-319.5 -203.5)" fill="#3f3d56"/><path d="M876.06982,385.88,803.5,426.68,791,433.71,607.75,536.75a15.24213,15.24213,0,0,1-7.4502,1.93,14.91079,14.91079,0,0,1-4.9497-.83,12.05366,12.05366,0,0,1-1.3003-.5q-.61449-.27-1.1997-.6L421.5,440.46,409,433.44l-84.91992-47.72a1.011,1.011,0,0,1-.37988-1.37.99933.99933,0,0,1,1.35986-.38L409,431.14l12.5,7.02L593.83008,535a13.07441,13.07441,0,0,0,1.77978.83c.26026.1.53028.19.8003.27A13.26424,13.26424,0,0,0,606.77,535L791,431.42l12.5-7.03,71.58984-40.25a.99849.99849,0,1,1,.98,1.74Z" transform="translate(-319.5 -203.5)" fill="#3f3d56"/><path d="M483.5748,269.5h-28a8,8,0,0,1,0-16h28a8,8,0,0,1,0,16Z" transform="translate(-319.5 -203.5)" fill="#6c63ff"/><path d="M516.5748,296.5h-61a8,8,0,0,1,0-16h61a8,8,0,0,1,0,16Z" transform="translate(-319.5 -203.5)" fill="#e6e6e6"/><path d="M687,368.5H514a8,8,0,0,1,0-16H687a8,8,0,0,1,0,16Z" transform="translate(-319.5 -203.5)" fill="#6c63ff"/><path d="M703,399.5H497a8,8,0,0,1,0-16H703a8,8,0,0,1,0,16Z" transform="translate(-319.5 -203.5)" fill="#e6e6e6"/><path d="M703,429.5H497a8,8,0,0,1,0-16H703a8,8,0,0,1,0,16Z" transform="translate(-319.5 -203.5)" fill="#e6e6e6"/></svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" width="50px" height="50px"> <path d="M17.791,46.836C18.502,46.53,19,45.823,19,45v-5.4c0-0.197,0.016-0.402,0.041-0.61C19.027,38.994,19.014,38.997,19,39 c0,0-3,0-3.6,0c-1.5,0-2.8-0.6-3.4-1.8c-0.7-1.3-1-3.5-2.8-4.7C8.9,32.3,9.1,32,9.7,32c0.6,0.1,1.9,0.9,2.7,2c0.9,1.1,1.8,2,3.4,2 c2.487,0,3.82-0.125,4.622-0.555C21.356,34.056,22.649,33,24,33v-0.025c-5.668-0.182-9.289-2.066-10.975-4.975 c-3.665,0.042-6.856,0.405-8.677,0.707c-0.058-0.327-0.108-0.656-0.151-0.987c1.797-0.296,4.843-0.647,8.345-0.714 c-0.112-0.276-0.209-0.559-0.291-0.849c-3.511-0.178-6.541-0.039-8.187,0.097c-0.02-0.332-0.047-0.663-0.051-0.999 c1.649-0.135,4.597-0.27,8.018-0.111c-0.079-0.5-0.13-1.011-0.13-1.543c0-1.7,0.6-3.5,1.7-5c-0.5-1.7-1.2-5.3,0.2-6.6 c2.7,0,4.6,1.3,5.5,2.1C21,13.4,22.9,13,25,13s4,0.4,5.6,1.1c0.9-0.8,2.8-2.1,5.5-2.1c1.5,1.4,0.7,5,0.2,6.6c1.1,1.5,1.7,3.2,1.6,5 c0,0.484-0.045,0.951-0.11,1.409c3.499-0.172,6.527-0.034,8.204,0.102c-0.002,0.337-0.033,0.666-0.051,0.999 c-1.671-0.138-4.775-0.28-8.359-0.089c-0.089,0.336-0.197,0.663-0.325,0.98c3.546,0.046,6.665,0.389,8.548,0.689 c-0.043,0.332-0.093,0.661-0.151,0.987c-1.912-0.306-5.171-0.664-8.879-0.682C35.112,30.873,31.557,32.75,26,32.969V33 c2.6,0,5,3.9,5,6.6V45c0,0.823,0.498,1.53,1.209,1.836C41.37,43.804,48,35.164,48,25C48,12.318,37.683,2,25,2S2,12.318,2,25 C2,35.164,8.63,43.804,17.791,46.836z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/><path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/><path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/><path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/><path d="M1 1h22v22H1z" fill="none"/></svg>
|
||||
|
After Width: | Height: | Size: 742 B |
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="21" height="21" viewBox="0 0 21 21"><title>MS-SymbolLockup</title><rect x="1" y="1" width="9" height="9" fill="#f25022"/><rect x="1" y="11" width="9" height="9" fill="#00a4ef"/><rect x="11" y="1" width="9" height="9" fill="#7fba00"/><rect x="11" y="11" width="9" height="9" fill="#ffb900"/></svg>
|
||||
|
After Width: | Height: | Size: 343 B |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 11 KiB |
@@ -1,25 +1,30 @@
|
||||
import { useState } from 'react'
|
||||
import { useError } from '@/store/context/ErrorContext'
|
||||
|
||||
export default (apiFunc) => {
|
||||
const [data, setData] = useState(null)
|
||||
const [error, setError] = useState(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setApiError] = useState(null)
|
||||
const { setError, handleError } = useError()
|
||||
|
||||
const request = async (...args) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await apiFunc(...args)
|
||||
setData(result.data)
|
||||
setError(null)
|
||||
setApiError(null)
|
||||
} catch (err) {
|
||||
setError(err || 'Unexpected Error!')
|
||||
handleError(err || 'Unexpected Error!')
|
||||
setApiError(err || 'Unexpected Error!')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
error,
|
||||
data,
|
||||
loading,
|
||||
request
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useSelector } from 'react-redux'
|
||||
import { useConfig } from '@/store/context/ConfigContext'
|
||||
|
||||
export const useAuth = () => {
|
||||
const { isOpenSource } = useConfig()
|
||||
const permissions = useSelector((state) => state.auth.permissions)
|
||||
const features = useSelector((state) => state.auth.features)
|
||||
const isGlobal = useSelector((state) => state.auth.isGlobal)
|
||||
const currentUser = useSelector((state) => state.auth.user)
|
||||
|
||||
const hasPermission = (permissionId) => {
|
||||
if (isOpenSource || isGlobal) {
|
||||
return true
|
||||
}
|
||||
if (!permissionId) return false
|
||||
const permissionIds = permissionId.split(',')
|
||||
if (permissions && permissions.length) {
|
||||
return permissionIds.some((permissionId) => permissions.includes(permissionId))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const hasAssignedWorkspace = (workspaceId) => {
|
||||
if (isOpenSource || isGlobal) {
|
||||
return true
|
||||
}
|
||||
const activeWorkspaceId = currentUser?.activeWorkspaceId || ''
|
||||
if (workspaceId === activeWorkspaceId) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const hasDisplay = (display) => {
|
||||
if (!display) {
|
||||
return true
|
||||
}
|
||||
|
||||
// if it has display flag, but user has no features, then it should not be displayed
|
||||
if (!features || Array.isArray(features) || Object.keys(features).length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
// check if the display flag is in the features
|
||||
if (Object.hasOwnProperty.call(features, display)) {
|
||||
const flag = features[display] === 'true' || features[display] === true
|
||||
return flag
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return { hasPermission, hasAssignedWorkspace, hasDisplay }
|
||||
}
|
||||
@@ -12,6 +12,8 @@ import { Provider } from 'react-redux'
|
||||
import { SnackbarProvider } from 'notistack'
|
||||
import ConfirmContextProvider from '@/store/context/ConfirmContextProvider'
|
||||
import { ReactFlowContext } from '@/store/context/ReactFlowContext'
|
||||
import { ConfigProvider } from '@/store/context/ConfigContext'
|
||||
import { ErrorProvider } from '@/store/context/ErrorContext'
|
||||
|
||||
const container = document.getElementById('root')
|
||||
const root = createRoot(container)
|
||||
@@ -21,11 +23,15 @@ root.render(
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<SnackbarProvider>
|
||||
<ConfirmContextProvider>
|
||||
<ReactFlowContext>
|
||||
<App />
|
||||
</ReactFlowContext>
|
||||
</ConfirmContextProvider>
|
||||
<ConfigProvider>
|
||||
<ErrorProvider>
|
||||
<ConfirmContextProvider>
|
||||
<ReactFlowContext>
|
||||
<App />
|
||||
</ReactFlowContext>
|
||||
</ConfirmContextProvider>
|
||||
</ErrorProvider>
|
||||
</ConfigProvider>
|
||||
</SnackbarProvider>
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Outlet } from 'react-router-dom'
|
||||
import { Box, useTheme } from '@mui/material'
|
||||
|
||||
// ==============================|| MINIMAL LAYOUT ||============================== //
|
||||
|
||||
const AuthLayout = () => {
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
[theme.breakpoints.down(1367)]: {
|
||||
alignItems: 'start',
|
||||
overflowY: 'auto',
|
||||
py: '64px'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Outlet />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default AuthLayout
|
||||
@@ -0,0 +1,435 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
// material-ui
|
||||
import {
|
||||
Breadcrumbs,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
CircularProgress,
|
||||
Typography,
|
||||
Stack,
|
||||
Chip,
|
||||
ListItemText,
|
||||
ListItemIcon,
|
||||
Select
|
||||
} from '@mui/material'
|
||||
import { Check } from '@mui/icons-material'
|
||||
import { alpha, styled, emphasize } from '@mui/material/styles'
|
||||
|
||||
import { IconChevronDown } from '@tabler/icons-react'
|
||||
|
||||
// api
|
||||
import userApi from '@/api/user'
|
||||
import workspaceApi from '@/api/workspace'
|
||||
|
||||
// hooks
|
||||
import useApi from '@/hooks/useApi'
|
||||
|
||||
// store
|
||||
import { store } from '@/store'
|
||||
import { workspaceSwitchSuccess } from '@/store/reducers/authSlice'
|
||||
|
||||
// ==============================|| OrgWorkspaceBreadcrumbs ||============================== //
|
||||
|
||||
const StyledMenu = styled((props) => (
|
||||
<Menu
|
||||
elevation={0}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right'
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right'
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
))(({ theme }) => ({
|
||||
'& .MuiPaper-root': {
|
||||
borderRadius: 6,
|
||||
marginTop: theme.spacing(1),
|
||||
minWidth: 180,
|
||||
boxShadow:
|
||||
'rgb(255, 255, 255) 0px 0px 0px 0px, rgba(0, 0, 0, 0.05) 0px 0px 0px 1px, rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.05) 0px 4px 6px -2px',
|
||||
'& .MuiMenu-list': {
|
||||
padding: '4px 0'
|
||||
},
|
||||
'& .MuiMenuItem-root': {
|
||||
'& .MuiSvgIcon-root': {
|
||||
fontSize: 18,
|
||||
color: theme.palette.text.secondary,
|
||||
marginRight: theme.spacing(1.5)
|
||||
},
|
||||
'&:active': {
|
||||
backgroundColor: alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
const StyledBreadcrumb = styled(Chip)(({ theme, isDarkMode }) => {
|
||||
const backgroundColor = isDarkMode ? theme.palette.grey[800] : theme.palette.grey[100]
|
||||
return {
|
||||
backgroundColor,
|
||||
height: theme.spacing(3),
|
||||
color: theme.palette.text.primary,
|
||||
fontWeight: theme.typography.fontWeightRegular,
|
||||
'&:hover, &:focus': {
|
||||
backgroundColor: emphasize(backgroundColor, 0.06)
|
||||
},
|
||||
'&:active': {
|
||||
boxShadow: theme.shadows[1],
|
||||
backgroundColor: emphasize(backgroundColor, 0.12)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const OrgWorkspaceBreadcrumbs = () => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const user = useSelector((state) => state.auth.user)
|
||||
const isAuthenticated = useSelector((state) => state.auth.isAuthenticated)
|
||||
const customization = useSelector((state) => state.customization)
|
||||
|
||||
const [orgAnchorEl, setOrgAnchorEl] = useState(null)
|
||||
const [workspaceAnchorEl, setWorkspaceAnchorEl] = useState(null)
|
||||
const orgMenuOpen = Boolean(orgAnchorEl)
|
||||
const workspaceMenuOpen = Boolean(workspaceAnchorEl)
|
||||
|
||||
const [assignedOrganizations, setAssignedOrganizations] = useState([])
|
||||
const [activeOrganizationId, setActiveOrganizationId] = useState(undefined)
|
||||
const [assignedWorkspaces, setAssignedWorkspaces] = useState([])
|
||||
const [activeWorkspaceId, setActiveWorkspaceId] = useState(undefined)
|
||||
const [isWorkspaceSwitching, setIsWorkspaceSwitching] = useState(false)
|
||||
const [isOrganizationSwitching, setIsOrganizationSwitching] = useState(false)
|
||||
const [showWorkspaceUnavailableDialog, setShowWorkspaceUnavailableDialog] = useState(false)
|
||||
|
||||
const getOrganizationsByUserIdApi = useApi(userApi.getOrganizationsByUserId)
|
||||
const getWorkspacesByUserIdApi = useApi(userApi.getWorkspacesByUserId)
|
||||
const switchWorkspaceApi = useApi(workspaceApi.switchWorkspace)
|
||||
|
||||
const handleOrgClick = (event) => {
|
||||
setOrgAnchorEl(event.currentTarget)
|
||||
}
|
||||
|
||||
const handleWorkspaceClick = (event) => {
|
||||
setWorkspaceAnchorEl(event.currentTarget)
|
||||
}
|
||||
|
||||
const handleOrgClose = () => {
|
||||
setOrgAnchorEl(null)
|
||||
}
|
||||
|
||||
const handleWorkspaceClose = () => {
|
||||
setWorkspaceAnchorEl(null)
|
||||
}
|
||||
|
||||
const handleOrgSwitch = async (orgId) => {
|
||||
setOrgAnchorEl(null)
|
||||
if (activeOrganizationId !== orgId) {
|
||||
setIsOrganizationSwitching(true)
|
||||
setActiveOrganizationId(orgId)
|
||||
// Fetch workspaces for the new organization
|
||||
getWorkspacesByUserIdApi.request(user.id)
|
||||
}
|
||||
}
|
||||
|
||||
const handleUnavailableOrgSwitch = async (orgId) => {
|
||||
setOrgAnchorEl(null)
|
||||
setActiveOrganizationId(orgId)
|
||||
// Fetch workspaces for the new organization
|
||||
try {
|
||||
const response = await userApi.getWorkspacesByUserId(user.id)
|
||||
const workspaces = response.data
|
||||
const filteredAssignedWorkspaces = workspaces.filter((item) => item.workspace.organizationId === orgId)
|
||||
const formattedAssignedWorkspaces = filteredAssignedWorkspaces.map((item) => ({
|
||||
id: item.workspaceId,
|
||||
name: item.workspace.name
|
||||
}))
|
||||
|
||||
const sortedWorkspaces = [...formattedAssignedWorkspaces].sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
setAssignedWorkspaces(sortedWorkspaces)
|
||||
} catch (error) {
|
||||
console.error('Error fetching workspaces:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const switchWorkspace = async (id) => {
|
||||
setWorkspaceAnchorEl(null)
|
||||
if (activeWorkspaceId !== id) {
|
||||
setIsWorkspaceSwitching(true)
|
||||
switchWorkspaceApi.request(id)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch workspaces when component mounts
|
||||
if (isAuthenticated && user) {
|
||||
getOrganizationsByUserIdApi.request(user.id)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isAuthenticated, user])
|
||||
|
||||
useEffect(() => {
|
||||
if (getWorkspacesByUserIdApi.data) {
|
||||
const filteredAssignedWorkspaces = getWorkspacesByUserIdApi.data.filter(
|
||||
(item) => item.workspace.organizationId === activeOrganizationId
|
||||
)
|
||||
const formattedAssignedWorkspaces = filteredAssignedWorkspaces.map((item) => ({
|
||||
id: item.workspaceId,
|
||||
name: item.workspace.name
|
||||
}))
|
||||
|
||||
const sortedWorkspaces = [...formattedAssignedWorkspaces].sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
// Only check workspace availability if we're not in the process of switching organizations
|
||||
if (!isOrganizationSwitching) {
|
||||
setTimeout(() => {
|
||||
if (user && user.activeWorkspaceId && !sortedWorkspaces.find((item) => item.id === user.activeWorkspaceId)) {
|
||||
setShowWorkspaceUnavailableDialog(true)
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
|
||||
setAssignedWorkspaces(sortedWorkspaces)
|
||||
|
||||
if (isOrganizationSwitching && sortedWorkspaces.length > 0) {
|
||||
// After organization switch, switch to the first workspace in the list
|
||||
switchWorkspaceApi.request(sortedWorkspaces[0].id)
|
||||
} else {
|
||||
setIsOrganizationSwitching(false)
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getWorkspacesByUserIdApi.data])
|
||||
|
||||
useEffect(() => {
|
||||
if (getWorkspacesByUserIdApi.error) {
|
||||
setIsWorkspaceSwitching(false)
|
||||
}
|
||||
}, [getWorkspacesByUserIdApi.error])
|
||||
|
||||
useEffect(() => {
|
||||
if (getOrganizationsByUserIdApi.data) {
|
||||
const formattedAssignedOrgs = getOrganizationsByUserIdApi.data.map((organization) => ({
|
||||
id: organization.organizationId,
|
||||
name: `${organization.user.name || organization.user.email}'s Organization`
|
||||
}))
|
||||
|
||||
const sortedOrgs = [...formattedAssignedOrgs].sort((a, b) => a.name.localeCompare(b.name))
|
||||
// Only check workspace availability after a short delay to allow store updates to complete
|
||||
setTimeout(() => {
|
||||
if (user && user.activeOrganizationId && !sortedOrgs.find((item) => item.id === user.activeOrganizationId)) {
|
||||
setActiveOrganizationId(undefined)
|
||||
setShowWorkspaceUnavailableDialog(true)
|
||||
}
|
||||
}, 500)
|
||||
|
||||
setAssignedOrganizations(sortedOrgs)
|
||||
|
||||
getWorkspacesByUserIdApi.request(user.id)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getOrganizationsByUserIdApi.data])
|
||||
|
||||
useEffect(() => {
|
||||
if (getOrganizationsByUserIdApi.error) {
|
||||
setIsOrganizationSwitching(false)
|
||||
}
|
||||
}, [getOrganizationsByUserIdApi.error])
|
||||
|
||||
useEffect(() => {
|
||||
if (switchWorkspaceApi.data) {
|
||||
setIsWorkspaceSwitching(false)
|
||||
setIsOrganizationSwitching(false)
|
||||
store.dispatch(workspaceSwitchSuccess(switchWorkspaceApi.data))
|
||||
|
||||
// get the current path and navigate to the same after refresh
|
||||
navigate('/', { replace: true })
|
||||
navigate(0)
|
||||
}
|
||||
}, [switchWorkspaceApi.data, navigate])
|
||||
|
||||
useEffect(() => {
|
||||
if (switchWorkspaceApi.error) {
|
||||
setIsWorkspaceSwitching(false)
|
||||
setIsOrganizationSwitching(false)
|
||||
}
|
||||
}, [switchWorkspaceApi.error])
|
||||
|
||||
useEffect(() => {
|
||||
setActiveOrganizationId(user.activeOrganizationId)
|
||||
setActiveWorkspaceId(user.activeWorkspaceId)
|
||||
}, [user])
|
||||
|
||||
return (
|
||||
<>
|
||||
{isAuthenticated && user ? (
|
||||
<>
|
||||
<StyledMenu anchorEl={orgAnchorEl} open={orgMenuOpen} onClose={handleOrgClose}>
|
||||
{assignedOrganizations.map((org) => (
|
||||
<MenuItem key={org.id} onClick={() => handleOrgSwitch(org.id)} selected={org.id === activeOrganizationId}>
|
||||
<ListItemText>{org.name}</ListItemText>
|
||||
{org.id === activeOrganizationId && (
|
||||
<ListItemIcon sx={{ minWidth: 'auto' }}>
|
||||
<Check />
|
||||
</ListItemIcon>
|
||||
)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</StyledMenu>
|
||||
<StyledMenu anchorEl={workspaceAnchorEl} open={workspaceMenuOpen} onClose={handleWorkspaceClose}>
|
||||
{assignedWorkspaces.map((workspace) => (
|
||||
<MenuItem
|
||||
key={workspace.id}
|
||||
onClick={() => switchWorkspace(workspace.id)}
|
||||
selected={workspace.id === activeWorkspaceId}
|
||||
>
|
||||
<ListItemText>{workspace.name}</ListItemText>
|
||||
{workspace.id === activeWorkspaceId && (
|
||||
<ListItemIcon sx={{ minWidth: 'auto' }}>
|
||||
<Check />
|
||||
</ListItemIcon>
|
||||
)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</StyledMenu>
|
||||
<Breadcrumbs aria-label='breadcrumb'>
|
||||
<StyledBreadcrumb
|
||||
isDarkMode={customization.isDarkMode}
|
||||
label={assignedOrganizations.find((org) => org.id === activeOrganizationId)?.name || 'Organization'}
|
||||
deleteIcon={<IconChevronDown size={16} />}
|
||||
onDelete={handleOrgClick}
|
||||
onClick={handleOrgClick}
|
||||
/>
|
||||
<StyledBreadcrumb
|
||||
isDarkMode={customization.isDarkMode}
|
||||
label={assignedWorkspaces.find((ws) => ws.id === activeWorkspaceId)?.name || 'Workspace'}
|
||||
deleteIcon={<IconChevronDown size={16} />}
|
||||
onDelete={handleWorkspaceClick}
|
||||
onClick={handleWorkspaceClick}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
</>
|
||||
) : null}
|
||||
<Dialog open={isOrganizationSwitching} PaperProps={{ style: { backgroundColor: 'transparent', boxShadow: 'none' } }}>
|
||||
<DialogContent>
|
||||
<Stack spacing={2} alignItems='center'>
|
||||
<CircularProgress />
|
||||
<Typography variant='body1' style={{ color: 'white' }}>
|
||||
Switching organization...
|
||||
</Typography>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Dialog open={isWorkspaceSwitching} PaperProps={{ style: { backgroundColor: 'transparent', boxShadow: 'none' } }}>
|
||||
<DialogContent>
|
||||
<Stack spacing={2} alignItems='center'>
|
||||
<CircularProgress />
|
||||
<Typography variant='body1' style={{ color: 'white' }}>
|
||||
Switching workspace...
|
||||
</Typography>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Dialog
|
||||
open={showWorkspaceUnavailableDialog}
|
||||
disableEscapeKeyDown
|
||||
disableBackdropClick
|
||||
PaperProps={{
|
||||
style: {
|
||||
padding: '20px',
|
||||
minWidth: '400px'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<Stack spacing={3}>
|
||||
<Typography variant='h5'>Workspace Unavailable</Typography>
|
||||
{assignedWorkspaces.length > 0 && !activeOrganizationId ? (
|
||||
<>
|
||||
<Typography variant='body1'>
|
||||
Your current workspace is no longer available. Please select another workspace to continue.
|
||||
</Typography>
|
||||
<Select
|
||||
fullWidth
|
||||
value=''
|
||||
onChange={(event) => {
|
||||
setShowWorkspaceUnavailableDialog(false)
|
||||
switchWorkspace(event.target.value)
|
||||
}}
|
||||
displayEmpty
|
||||
>
|
||||
<MenuItem disabled value=''>
|
||||
<em>Select Workspace</em>
|
||||
</MenuItem>
|
||||
{assignedWorkspaces.map((workspace, index) => (
|
||||
<MenuItem key={index} value={workspace.id}>
|
||||
{workspace.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Typography variant='body1'>
|
||||
Workspace is no longer available. Please select a different organization/workspace to continue.
|
||||
</Typography>
|
||||
<Select
|
||||
fullWidth
|
||||
value={activeOrganizationId || ''}
|
||||
onChange={(event) => {
|
||||
handleUnavailableOrgSwitch(event.target.value)
|
||||
}}
|
||||
displayEmpty
|
||||
>
|
||||
<MenuItem disabled value=''>
|
||||
<em>Select Organization</em>
|
||||
</MenuItem>
|
||||
{assignedOrganizations.map((org, index) => (
|
||||
<MenuItem key={index} value={org.id}>
|
||||
{org.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{activeOrganizationId && assignedWorkspaces.length > 0 && (
|
||||
<Select
|
||||
fullWidth
|
||||
value={activeWorkspaceId || ''}
|
||||
onChange={(event) => {
|
||||
setShowWorkspaceUnavailableDialog(false)
|
||||
switchWorkspace(event.target.value)
|
||||
}}
|
||||
displayEmpty
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
<MenuItem disabled value=''>
|
||||
<em>Select Workspace</em>
|
||||
</MenuItem>
|
||||
{assignedWorkspaces.map((workspace, index) => (
|
||||
<MenuItem key={index} value={workspace.id}>
|
||||
{workspace.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
OrgWorkspaceBreadcrumbs.propTypes = {}
|
||||
|
||||
export default OrgWorkspaceBreadcrumbs
|
||||
@@ -1,10 +1,12 @@
|
||||
import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction, REMOVE_DIRTY } from '@/store/actions'
|
||||
import { exportData, stringify } from '@/utils/exportImport'
|
||||
import useNotifier from '@/utils/useNotifier'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction, REMOVE_DIRTY } from '@/store/actions'
|
||||
import { exportData, stringify } from '@/utils/exportImport'
|
||||
import useNotifier from '@/utils/useNotifier'
|
||||
|
||||
// material-ui
|
||||
import {
|
||||
@@ -35,22 +37,23 @@ import { useTheme } from '@mui/material/styles'
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||
|
||||
// project imports
|
||||
import { PermissionListItemButton } from '@/ui-component/button/RBACButtons'
|
||||
import MainCard from '@/ui-component/cards/MainCard'
|
||||
import AboutDialog from '@/ui-component/dialog/AboutDialog'
|
||||
import Transitions from '@/ui-component/extended/Transitions'
|
||||
|
||||
// assets
|
||||
import ExportingGIF from '@/assets/images/Exporting.gif'
|
||||
import { IconFileExport, IconFileUpload, IconInfoCircle, IconLogout, IconSettings, IconX } from '@tabler/icons-react'
|
||||
import { IconFileExport, IconFileUpload, IconInfoCircle, IconLogout, IconSettings, IconUserEdit, IconX } from '@tabler/icons-react'
|
||||
import './index.css'
|
||||
|
||||
//API
|
||||
// API
|
||||
import exportImportApi from '@/api/exportimport'
|
||||
|
||||
// Hooks
|
||||
import useApi from '@/hooks/useApi'
|
||||
import { useConfig } from '@/store/context/ConfigContext'
|
||||
import { getErrorMessage } from '@/utils/errorHandler'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
const dataToExport = [
|
||||
'Agentflows',
|
||||
@@ -165,21 +168,60 @@ ExportDialog.propTypes = {
|
||||
onExport: PropTypes.func
|
||||
}
|
||||
|
||||
const ImportDialog = ({ show }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
|
||||
const component = show ? (
|
||||
<Dialog open={show} fullWidth maxWidth='sm' aria-labelledby='import-dialog-title' aria-describedby='import-dialog-description'>
|
||||
<DialogTitle sx={{ fontSize: '1rem' }} id='import-dialog-title'>
|
||||
Importing...
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ height: 'auto', display: 'flex', justifyContent: 'center', mb: 3 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<img
|
||||
style={{
|
||||
objectFit: 'cover',
|
||||
height: 'auto',
|
||||
width: 'auto'
|
||||
}}
|
||||
src={ExportingGIF}
|
||||
alt='ImportingGIF'
|
||||
/>
|
||||
<span>Importing data might takes a while</span>
|
||||
</div>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
) : null
|
||||
|
||||
return createPortal(component, portalElement)
|
||||
}
|
||||
|
||||
ImportDialog.propTypes = {
|
||||
show: PropTypes.bool
|
||||
}
|
||||
|
||||
// ==============================|| PROFILE MENU ||============================== //
|
||||
|
||||
const ProfileSection = ({ username, handleLogout }) => {
|
||||
const ProfileSection = ({ handleLogout }) => {
|
||||
const theme = useTheme()
|
||||
|
||||
const customization = useSelector((state) => state.customization)
|
||||
const { isCloud } = useConfig()
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [aboutDialogOpen, setAboutDialogOpen] = useState(false)
|
||||
|
||||
const [exportDialogOpen, setExportDialogOpen] = useState(false)
|
||||
const [importDialogOpen, setImportDialogOpen] = useState(false)
|
||||
|
||||
const anchorRef = useRef(null)
|
||||
const inputRef = useRef()
|
||||
|
||||
const navigate = useNavigate()
|
||||
const currentUser = useSelector((state) => state.auth.user)
|
||||
const isAuthenticated = useSelector((state) => state.auth.isAuthenticated)
|
||||
|
||||
const importAllApi = useApi(exportImportApi.importData)
|
||||
const exportAllApi = useApi(exportImportApi.exportData)
|
||||
@@ -223,6 +265,7 @@ const ProfileSection = ({ username, handleLogout }) => {
|
||||
if (!e.target.files) return
|
||||
|
||||
const file = e.target.files[0]
|
||||
setImportDialogOpen(true)
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = (evt) => {
|
||||
@@ -236,6 +279,7 @@ const ProfileSection = ({ username, handleLogout }) => {
|
||||
}
|
||||
|
||||
const importAllSuccess = () => {
|
||||
setImportDialogOpen(false)
|
||||
dispatch({ type: REMOVE_DIRTY })
|
||||
enqueueSnackbar({
|
||||
message: `Import All successful`,
|
||||
@@ -284,6 +328,7 @@ const ProfileSection = ({ username, handleLogout }) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (importAllApi.error) {
|
||||
setImportDialogOpen(false)
|
||||
let errMsg = 'Invalid Imported File'
|
||||
let error = importAllApi.error
|
||||
if (error?.response?.data) {
|
||||
@@ -331,7 +376,6 @@ const ProfileSection = ({ username, handleLogout }) => {
|
||||
if (prevOpen.current === true && open === false) {
|
||||
anchorRef.current.focus()
|
||||
}
|
||||
|
||||
prevOpen.current = open
|
||||
}, [open])
|
||||
|
||||
@@ -380,10 +424,16 @@ const ProfileSection = ({ username, handleLogout }) => {
|
||||
<Paper>
|
||||
<ClickAwayListener onClickAway={handleClose}>
|
||||
<MainCard border={false} elevation={16} content={false} boxShadow shadow={theme.shadows[16]}>
|
||||
{username && (
|
||||
{isAuthenticated && currentUser ? (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography component='span' variant='h4'>
|
||||
{username}
|
||||
{currentUser.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography component='span' variant='h4'>
|
||||
User
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
@@ -406,7 +456,8 @@ const ProfileSection = ({ username, handleLogout }) => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ListItemButton
|
||||
<PermissionListItemButton
|
||||
permissionId='workspace:export'
|
||||
sx={{ borderRadius: `${customization.borderRadius}px` }}
|
||||
onClick={() => {
|
||||
setExportDialogOpen(true)
|
||||
@@ -416,8 +467,9 @@ const ProfileSection = ({ username, handleLogout }) => {
|
||||
<IconFileExport stroke={1.5} size='1.3rem' />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={<Typography variant='body2'>Export</Typography>} />
|
||||
</ListItemButton>
|
||||
<ListItemButton
|
||||
</PermissionListItemButton>
|
||||
<PermissionListItemButton
|
||||
permissionId='workspace:import'
|
||||
sx={{ borderRadius: `${customization.borderRadius}px` }}
|
||||
onClick={() => {
|
||||
importAll()
|
||||
@@ -427,7 +479,7 @@ const ProfileSection = ({ username, handleLogout }) => {
|
||||
<IconFileUpload stroke={1.5} size='1.3rem' />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={<Typography variant='body2'>Import</Typography>} />
|
||||
</ListItemButton>
|
||||
</PermissionListItemButton>
|
||||
<input ref={inputRef} type='file' hidden onChange={fileChange} accept='.json' />
|
||||
<ListItemButton
|
||||
sx={{ borderRadius: `${customization.borderRadius}px` }}
|
||||
@@ -439,19 +491,31 @@ const ProfileSection = ({ username, handleLogout }) => {
|
||||
<ListItemIcon>
|
||||
<IconInfoCircle stroke={1.5} size='1.3rem' />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={<Typography variant='body2'>About Flowise</Typography>} />
|
||||
<ListItemText primary={<Typography variant='body2'>Version</Typography>} />
|
||||
</ListItemButton>
|
||||
{localStorage.getItem('username') && localStorage.getItem('password') && (
|
||||
{isAuthenticated && !currentUser.isSSO && !isCloud && (
|
||||
<ListItemButton
|
||||
sx={{ borderRadius: `${customization.borderRadius}px` }}
|
||||
onClick={handleLogout}
|
||||
onClick={() => {
|
||||
setOpen(false)
|
||||
navigate('/user-profile')
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<IconLogout stroke={1.5} size='1.3rem' />
|
||||
<IconUserEdit stroke={1.5} size='1.3rem' />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={<Typography variant='body2'>Logout</Typography>} />
|
||||
<ListItemText primary={<Typography variant='body2'>Update Profile</Typography>} />
|
||||
</ListItemButton>
|
||||
)}
|
||||
<ListItemButton
|
||||
sx={{ borderRadius: `${customization.borderRadius}px` }}
|
||||
onClick={handleLogout}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<IconLogout stroke={1.5} size='1.3rem' />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={<Typography variant='body2'>Logout</Typography>} />
|
||||
</ListItemButton>
|
||||
</List>
|
||||
</Box>
|
||||
</PerfectScrollbar>
|
||||
@@ -463,12 +527,12 @@ const ProfileSection = ({ username, handleLogout }) => {
|
||||
</Popper>
|
||||
<AboutDialog show={aboutDialogOpen} onCancel={() => setAboutDialogOpen(false)} />
|
||||
<ExportDialog show={exportDialogOpen} onCancel={() => setExportDialogOpen(false)} onExport={(data) => onExport(data)} />
|
||||
<ImportDialog show={importDialogOpen} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
ProfileSection.propTypes = {
|
||||
username: PropTypes.string,
|
||||
handleLogout: PropTypes.func
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
// material-ui
|
||||
import { Check } from '@mui/icons-material'
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
CircularProgress,
|
||||
Button,
|
||||
Select,
|
||||
Typography,
|
||||
Stack,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Menu,
|
||||
MenuItem,
|
||||
DialogActions
|
||||
} from '@mui/material'
|
||||
import { alpha, styled } from '@mui/material/styles'
|
||||
|
||||
// api
|
||||
import userApi from '@/api/user'
|
||||
import workspaceApi from '@/api/workspace'
|
||||
import accountApi from '@/api/account.api'
|
||||
|
||||
// hooks
|
||||
import useApi from '@/hooks/useApi'
|
||||
import { useConfig } from '@/store/context/ConfigContext'
|
||||
|
||||
// store
|
||||
import { store } from '@/store'
|
||||
import { logoutSuccess, workspaceSwitchSuccess } from '@/store/reducers/authSlice'
|
||||
|
||||
// ==============================|| WORKSPACE SWITCHER ||============================== //
|
||||
|
||||
const StyledMenu = styled((props) => (
|
||||
<Menu
|
||||
elevation={0}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right'
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right'
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
))(({ theme }) => ({
|
||||
'& .MuiPaper-root': {
|
||||
borderRadius: 6,
|
||||
marginTop: theme.spacing(1),
|
||||
minWidth: 180,
|
||||
boxShadow:
|
||||
'rgb(255, 255, 255) 0px 0px 0px 0px, rgba(0, 0, 0, 0.05) 0px 0px 0px 1px, rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.05) 0px 4px 6px -2px',
|
||||
'& .MuiMenu-list': {
|
||||
padding: '4px 0'
|
||||
},
|
||||
'& .MuiMenuItem-root': {
|
||||
'& .MuiSvgIcon-root': {
|
||||
fontSize: 18,
|
||||
color: theme.palette.text.secondary,
|
||||
marginRight: theme.spacing(1.5)
|
||||
},
|
||||
'&:active': {
|
||||
backgroundColor: alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
const WorkspaceSwitcher = () => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const user = useSelector((state) => state.auth.user)
|
||||
const isAuthenticated = useSelector((state) => state.auth.isAuthenticated)
|
||||
const features = useSelector((state) => state.auth.features)
|
||||
|
||||
const { isEnterpriseLicensed } = useConfig()
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
const open = Boolean(anchorEl)
|
||||
const prevOpen = useRef(open)
|
||||
|
||||
const [assignedWorkspaces, setAssignedWorkspaces] = useState([])
|
||||
const [activeWorkspace, setActiveWorkspace] = useState(undefined)
|
||||
const [isSwitching, setIsSwitching] = useState(false)
|
||||
const [showWorkspaceUnavailableDialog, setShowWorkspaceUnavailableDialog] = useState(false)
|
||||
const [showErrorDialog, setShowErrorDialog] = useState(false)
|
||||
const [errorMessage, setErrorMessage] = useState('')
|
||||
|
||||
const getWorkspacesByOrganizationIdUserIdApi = useApi(userApi.getWorkspacesByOrganizationIdUserId)
|
||||
const getWorkspacesByUserIdApi = useApi(userApi.getWorkspacesByUserId)
|
||||
const switchWorkspaceApi = useApi(workspaceApi.switchWorkspace)
|
||||
const logoutApi = useApi(accountApi.logout)
|
||||
|
||||
const handleClick = (event) => {
|
||||
setAnchorEl(event.currentTarget)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
const switchWorkspace = async (id) => {
|
||||
setAnchorEl(null)
|
||||
if (activeWorkspace !== id) {
|
||||
setIsSwitching(true)
|
||||
switchWorkspaceApi.request(id)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
logoutApi.request()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch workspaces when component mounts
|
||||
if (isAuthenticated && user) {
|
||||
const WORKSPACE_FLAG = 'feat:workspaces'
|
||||
if (Object.hasOwnProperty.call(features, WORKSPACE_FLAG)) {
|
||||
const flag = features[WORKSPACE_FLAG] === 'true' || features[WORKSPACE_FLAG] === true
|
||||
if (flag) {
|
||||
if (isEnterpriseLicensed) {
|
||||
getWorkspacesByOrganizationIdUserIdApi.request(user.activeOrganizationId, user.id)
|
||||
} else {
|
||||
getWorkspacesByUserIdApi.request(user.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isAuthenticated, user, features, isEnterpriseLicensed])
|
||||
|
||||
useEffect(() => {
|
||||
if (getWorkspacesByOrganizationIdUserIdApi.data) {
|
||||
const formattedAssignedWorkspaces = getWorkspacesByOrganizationIdUserIdApi.data.map((item) => ({
|
||||
id: item.workspaceId,
|
||||
name: item.workspace.name
|
||||
}))
|
||||
|
||||
const sortedWorkspaces = [...formattedAssignedWorkspaces].sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
// Only check workspace availability after a short delay to allow store updates to complete
|
||||
setTimeout(() => {
|
||||
if (user && user.activeWorkspaceId && !sortedWorkspaces.find((item) => item.id === user.activeWorkspaceId)) {
|
||||
setShowWorkspaceUnavailableDialog(true)
|
||||
}
|
||||
}, 500)
|
||||
|
||||
setAssignedWorkspaces(sortWorkspaces(sortedWorkspaces))
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getWorkspacesByOrganizationIdUserIdApi.data, user.activeWorkspaceId])
|
||||
|
||||
useEffect(() => {
|
||||
if (getWorkspacesByUserIdApi.data) {
|
||||
const formattedAssignedWorkspaces = getWorkspacesByUserIdApi.data.map((item) => ({
|
||||
id: item.workspaceId,
|
||||
name: item.workspace.name
|
||||
}))
|
||||
|
||||
const sortedWorkspaces = [...formattedAssignedWorkspaces].sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
// Only check workspace availability after a short delay to allow store updates to complete
|
||||
setTimeout(() => {
|
||||
if (user && user.activeWorkspaceId && !sortedWorkspaces.find((item) => item.id === user.activeWorkspaceId)) {
|
||||
setShowWorkspaceUnavailableDialog(true)
|
||||
}
|
||||
}, 500)
|
||||
|
||||
setAssignedWorkspaces(sortWorkspaces(sortedWorkspaces))
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getWorkspacesByUserIdApi.data, user.activeWorkspaceId])
|
||||
|
||||
useEffect(() => {
|
||||
if (switchWorkspaceApi.data) {
|
||||
setIsSwitching(false)
|
||||
store.dispatch(workspaceSwitchSuccess(switchWorkspaceApi.data))
|
||||
|
||||
// get the current path and navigate to the same after refresh
|
||||
navigate('/', { replace: true })
|
||||
navigate(0)
|
||||
}
|
||||
}, [switchWorkspaceApi.data, navigate])
|
||||
|
||||
useEffect(() => {
|
||||
if (switchWorkspaceApi.error) {
|
||||
setIsSwitching(false)
|
||||
setShowWorkspaceUnavailableDialog(false)
|
||||
|
||||
// Set error message and show error dialog
|
||||
setErrorMessage(switchWorkspaceApi.error.message || 'Failed to switch workspace')
|
||||
setShowErrorDialog(true)
|
||||
}
|
||||
}, [switchWorkspaceApi.error])
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
if (logoutApi.data && logoutApi.data.message === 'logged_out') {
|
||||
store.dispatch(logoutSuccess())
|
||||
window.location.href = logoutApi.data.redirectTo
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}, [logoutApi.data])
|
||||
|
||||
useEffect(() => {
|
||||
setActiveWorkspace(user.activeWorkspace)
|
||||
|
||||
prevOpen.current = open
|
||||
}, [open, user])
|
||||
|
||||
const sortWorkspaces = (assignedWorkspaces) => {
|
||||
// Sort workspaces alphabetically by name, with special characters last
|
||||
const sortedWorkspaces = assignedWorkspaces
|
||||
? [...assignedWorkspaces].sort((a, b) => {
|
||||
const isSpecialA = /^[^a-zA-Z0-9]/.test(a.name)
|
||||
const isSpecialB = /^[^a-zA-Z0-9]/.test(b.name)
|
||||
|
||||
// If one has special char and other doesn't, special char goes last
|
||||
if (isSpecialA && !isSpecialB) return 1
|
||||
if (!isSpecialA && isSpecialB) return -1
|
||||
|
||||
// If both are special or both are not special, sort alphabetically
|
||||
return a.name.localeCompare(b.name, undefined, {
|
||||
numeric: true,
|
||||
sensitivity: 'base'
|
||||
})
|
||||
})
|
||||
: []
|
||||
return sortedWorkspaces
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isAuthenticated &&
|
||||
user &&
|
||||
assignedWorkspaces?.length > 1 &&
|
||||
!(assignedWorkspaces.length === 1 && user.activeWorkspace === 'Default Workspace') ? (
|
||||
<>
|
||||
<Button
|
||||
sx={{ mr: 4 }}
|
||||
id='workspace-switcher'
|
||||
aria-controls={open ? 'workspace-switcher-menu' : undefined}
|
||||
aria-haspopup='true'
|
||||
aria-expanded={open ? 'true' : undefined}
|
||||
disableElevation
|
||||
onClick={handleClick}
|
||||
endIcon={<KeyboardArrowDownIcon />}
|
||||
>
|
||||
{user.activeWorkspace}
|
||||
</Button>
|
||||
<StyledMenu
|
||||
id='workspace-switcher-menu'
|
||||
MenuListProps={{
|
||||
'aria-labelledby': 'workspace-switcher'
|
||||
}}
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
>
|
||||
{assignedWorkspaces.map((item, index) => (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
switchWorkspace(item.id)
|
||||
}}
|
||||
key={index}
|
||||
disableRipple
|
||||
>
|
||||
{item.id === user.activeWorkspaceId ? (
|
||||
<>
|
||||
<ListItemIcon>
|
||||
<Check />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{item.name}</ListItemText>
|
||||
</>
|
||||
) : (
|
||||
<ListItemText inset>{item.name}</ListItemText>
|
||||
)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</StyledMenu>
|
||||
</>
|
||||
) : null}
|
||||
<Dialog open={isSwitching} PaperProps={{ style: { backgroundColor: 'transparent', boxShadow: 'none' } }}>
|
||||
<DialogContent>
|
||||
<Stack spacing={2} alignItems='center'>
|
||||
<CircularProgress />
|
||||
<Typography variant='body1' style={{ color: 'white' }}>
|
||||
Switching workspace...
|
||||
</Typography>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
open={showWorkspaceUnavailableDialog}
|
||||
disableEscapeKeyDown
|
||||
disableBackdropClick
|
||||
PaperProps={{
|
||||
style: {
|
||||
padding: '20px',
|
||||
minWidth: '400px'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<Stack spacing={3}>
|
||||
<Typography variant='h5'>Workspace Unavailable</Typography>
|
||||
<Typography variant='body1'>
|
||||
Your current workspace is no longer available. Please select another workspace to continue.
|
||||
</Typography>
|
||||
<Select
|
||||
fullWidth
|
||||
value=''
|
||||
onChange={(event) => {
|
||||
setShowWorkspaceUnavailableDialog(false)
|
||||
switchWorkspace(event.target.value)
|
||||
}}
|
||||
displayEmpty
|
||||
>
|
||||
<MenuItem disabled value=''>
|
||||
<em>Select Workspace</em>
|
||||
</MenuItem>
|
||||
{assignedWorkspaces.map((workspace, index) => (
|
||||
<MenuItem key={index} value={workspace.id}>
|
||||
{workspace.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
{assignedWorkspaces.length === 0 && (
|
||||
<DialogActions>
|
||||
<Button onClick={handleLogout} variant='contained' color='primary'>
|
||||
Logout
|
||||
</Button>
|
||||
</DialogActions>
|
||||
)}
|
||||
</Dialog>
|
||||
|
||||
{/* Error Dialog */}
|
||||
<Dialog
|
||||
open={showErrorDialog}
|
||||
disableEscapeKeyDown
|
||||
disableBackdropClick
|
||||
PaperProps={{
|
||||
style: {
|
||||
padding: '20px',
|
||||
minWidth: '400px'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<Stack spacing={3}>
|
||||
<Typography variant='h5'>Workspace Switch Error</Typography>
|
||||
<Typography variant='body1'>{errorMessage}</Typography>
|
||||
{isEnterpriseLicensed && (
|
||||
<Typography variant='body2' color='text.secondary'>
|
||||
Please contact your administrator for assistance.
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleLogout} variant='contained' color='primary'>
|
||||
Logout
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
WorkspaceSwitcher.propTypes = {}
|
||||
|
||||
export default WorkspaceSwitcher
|
||||
@@ -1,22 +1,35 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
// material-ui
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { Avatar, Box, ButtonBase, Switch } from '@mui/material'
|
||||
import { styled } from '@mui/material/styles'
|
||||
import { Button, Avatar, Box, ButtonBase, Switch, Typography, Link } from '@mui/material'
|
||||
import { useTheme, styled, darken } from '@mui/material/styles'
|
||||
|
||||
// project imports
|
||||
import LogoSection from '../LogoSection'
|
||||
import ProfileSection from './ProfileSection'
|
||||
import WorkspaceSwitcher from '@/layout/MainLayout/Header/WorkspaceSwitcher'
|
||||
import OrgWorkspaceBreadcrumbs from '@/layout/MainLayout/Header/OrgWorkspaceBreadcrumbs'
|
||||
import PricingDialog from '@/ui-component/subscription/PricingDialog'
|
||||
|
||||
// assets
|
||||
import { IconMenu2 } from '@tabler/icons-react'
|
||||
import { IconMenu2, IconX, IconSparkles } from '@tabler/icons-react'
|
||||
|
||||
// store
|
||||
import { store } from '@/store'
|
||||
import { SET_DARKMODE } from '@/store/actions'
|
||||
import { useConfig } from '@/store/context/ConfigContext'
|
||||
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions'
|
||||
import { logoutSuccess } from '@/store/reducers/authSlice'
|
||||
|
||||
// API
|
||||
import accountApi from '@/api/account.api'
|
||||
|
||||
// Hooks
|
||||
import useApi from '@/hooks/useApi'
|
||||
import useNotifier from '@/utils/useNotifier'
|
||||
|
||||
// ==============================|| MAIN NAVBAR / HEADER ||============================== //
|
||||
|
||||
@@ -67,14 +80,87 @@ const MaterialUISwitch = styled(Switch)(({ theme }) => ({
|
||||
}
|
||||
}))
|
||||
|
||||
const GitHubStarButton = ({ starCount, isDark }) => {
|
||||
const theme = useTheme()
|
||||
|
||||
const formattedStarCount = starCount.toLocaleString()
|
||||
|
||||
return (
|
||||
<Link href='https://github.com/FlowiseAI/Flowise' target='_blank' underline='none' sx={{ display: 'inline-flex' }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
borderRadius: '3px',
|
||||
overflow: 'hidden',
|
||||
border: `1px solid ${isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'}`,
|
||||
fontSize: '12px',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif',
|
||||
fontWeight: 600,
|
||||
lineHeight: 1
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '3px 10px',
|
||||
backgroundColor: isDark ? darken(theme.palette.background.paper, 0.2) : '#f6f8fa',
|
||||
color: isDark ? '#c9d1d9' : '#24292e',
|
||||
borderRight: `1px solid ${isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'}`
|
||||
}}
|
||||
>
|
||||
<svg height='16' width='16' viewBox='0 0 16 16' style={{ marginRight: '4px', fill: isDark ? '#c9d1d9' : '#24292e' }}>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
d='M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z'
|
||||
></path>
|
||||
</svg>
|
||||
<Typography variant='caption' sx={{ fontWeight: 600, color: isDark ? 'white' : theme.palette.text.primary }}>
|
||||
Star
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '3px 10px',
|
||||
backgroundColor: isDark ? theme.palette.background.paper : 'white'
|
||||
}}
|
||||
>
|
||||
<Typography variant='caption' sx={{ fontWeight: 600, color: isDark ? 'white' : theme.palette.text.primary }}>
|
||||
{formattedStarCount}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
GitHubStarButton.propTypes = {
|
||||
starCount: PropTypes.number.isRequired,
|
||||
isDark: PropTypes.bool.isRequired
|
||||
}
|
||||
|
||||
const Header = ({ handleLeftDrawerToggle }) => {
|
||||
const theme = useTheme()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const customization = useSelector((state) => state.customization)
|
||||
const logoutApi = useApi(accountApi.logout)
|
||||
|
||||
const [isDark, setIsDark] = useState(customization.isDarkMode)
|
||||
const dispatch = useDispatch()
|
||||
const { isEnterpriseLicensed, isCloud, isOpenSource } = useConfig()
|
||||
const currentUser = useSelector((state) => state.auth.user)
|
||||
const isAuthenticated = useSelector((state) => state.auth.isAuthenticated)
|
||||
const [isPricingOpen, setIsPricingOpen] = useState(false)
|
||||
const [starCount, setStarCount] = useState(0)
|
||||
|
||||
useNotifier()
|
||||
|
||||
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
||||
|
||||
const changeDarkMode = () => {
|
||||
dispatch({ type: SET_DARKMODE, isDarkMode: !isDark })
|
||||
@@ -83,15 +169,52 @@ const Header = ({ handleLeftDrawerToggle }) => {
|
||||
}
|
||||
|
||||
const signOutClicked = () => {
|
||||
localStorage.removeItem('username')
|
||||
localStorage.removeItem('password')
|
||||
navigate('/', { replace: true })
|
||||
navigate(0)
|
||||
logoutApi.request()
|
||||
enqueueSnackbar({
|
||||
message: 'Logging out...',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'success',
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
if (logoutApi.data && logoutApi.data.message === 'logged_out') {
|
||||
store.dispatch(logoutSuccess())
|
||||
window.location.href = logoutApi.data.redirectTo
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}, [logoutApi.data])
|
||||
|
||||
useEffect(() => {
|
||||
if (isCloud || isOpenSource) {
|
||||
const fetchStarCount = async () => {
|
||||
try {
|
||||
const response = await fetch('https://api.github.com/repos/FlowiseAI/Flowise')
|
||||
const data = await response.json()
|
||||
if (data.stargazers_count) {
|
||||
setStarCount(data.stargazers_count)
|
||||
}
|
||||
} catch (error) {
|
||||
setStarCount(0)
|
||||
}
|
||||
}
|
||||
|
||||
fetchStarCount()
|
||||
}
|
||||
}, [isCloud, isOpenSource])
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* logo & toggler button */}
|
||||
<Box
|
||||
sx={{
|
||||
width: 228,
|
||||
@@ -104,31 +227,91 @@ const Header = ({ handleLeftDrawerToggle }) => {
|
||||
<Box component='span' sx={{ display: { xs: 'none', md: 'block' }, flexGrow: 1 }}>
|
||||
<LogoSection />
|
||||
</Box>
|
||||
<ButtonBase sx={{ borderRadius: '12px', overflow: 'hidden' }}>
|
||||
<Avatar
|
||||
variant='rounded'
|
||||
sx={{
|
||||
...theme.typography.commonAvatar,
|
||||
...theme.typography.mediumAvatar,
|
||||
transition: 'all .2s ease-in-out',
|
||||
background: theme.palette.secondary.light,
|
||||
color: theme.palette.secondary.dark,
|
||||
'&:hover': {
|
||||
background: theme.palette.secondary.dark,
|
||||
color: theme.palette.secondary.light
|
||||
}
|
||||
}}
|
||||
onClick={handleLeftDrawerToggle}
|
||||
color='inherit'
|
||||
>
|
||||
<IconMenu2 stroke={1.5} size='1.3rem' />
|
||||
</Avatar>
|
||||
</ButtonBase>
|
||||
{isAuthenticated && (
|
||||
<ButtonBase sx={{ borderRadius: '12px', overflow: 'hidden' }}>
|
||||
<Avatar
|
||||
variant='rounded'
|
||||
sx={{
|
||||
...theme.typography.commonAvatar,
|
||||
...theme.typography.mediumAvatar,
|
||||
transition: 'all .2s ease-in-out',
|
||||
background: theme.palette.secondary.light,
|
||||
color: theme.palette.secondary.dark,
|
||||
'&:hover': {
|
||||
background: theme.palette.secondary.dark,
|
||||
color: theme.palette.secondary.light
|
||||
}
|
||||
}}
|
||||
onClick={handleLeftDrawerToggle}
|
||||
color='inherit'
|
||||
>
|
||||
<IconMenu2 stroke={1.5} size='1.3rem' />
|
||||
</Avatar>
|
||||
</ButtonBase>
|
||||
)}
|
||||
</Box>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
{isCloud || isOpenSource ? (
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
px: 4,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
'& span': {
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<GitHubStarButton starCount={starCount} isDark={isDark} />
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
)}
|
||||
{isEnterpriseLicensed && isAuthenticated && <WorkspaceSwitcher />}
|
||||
{isCloud && isAuthenticated && <OrgWorkspaceBreadcrumbs />}
|
||||
{isCloud && currentUser?.isOrganizationAdmin && (
|
||||
<Button
|
||||
variant='contained'
|
||||
sx={{
|
||||
mr: 1,
|
||||
ml: 2,
|
||||
borderRadius: 15,
|
||||
background: (theme) =>
|
||||
`linear-gradient(90deg, ${theme.palette.primary.main} 10%, ${theme.palette.secondary.main} 100%)`,
|
||||
color: (theme) => theme.palette.secondary.contrastText,
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.2)',
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': {
|
||||
background: (theme) =>
|
||||
`linear-gradient(90deg, ${darken(theme.palette.primary.main, 0.1)} 10%, ${darken(
|
||||
theme.palette.secondary.main,
|
||||
0.1
|
||||
)} 100%)`,
|
||||
boxShadow: '0 4px 8px rgba(0,0,0,0.3)'
|
||||
}
|
||||
}}
|
||||
onClick={() => setIsPricingOpen(true)}
|
||||
startIcon={<IconSparkles size={20} />}
|
||||
>
|
||||
Upgrade
|
||||
</Button>
|
||||
)}
|
||||
{isPricingOpen && isCloud && (
|
||||
<PricingDialog
|
||||
open={isPricingOpen}
|
||||
onClose={(planUpdated) => {
|
||||
setIsPricingOpen(false)
|
||||
if (planUpdated) {
|
||||
navigate('/')
|
||||
navigate(0)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<MaterialUISwitch checked={isDark} onChange={changeDarkMode} />
|
||||
<Box sx={{ ml: 2 }}></Box>
|
||||
<ProfileSection handleLogout={signOutClicked} username={localStorage.getItem('username') ?? ''} />
|
||||
<ProfileSection handleLogout={signOutClicked} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions'
|
||||
import { store } from '@/store'
|
||||
|
||||
// material-ui
|
||||
import { Divider, Box, Button, List, ListItemButton, ListItemIcon, Typography } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
|
||||
// project imports
|
||||
import useNotifier from '@/utils/useNotifier'
|
||||
import { useConfig } from '@/store/context/ConfigContext'
|
||||
|
||||
// API
|
||||
import { logoutSuccess } from '@/store/reducers/authSlice'
|
||||
|
||||
// Hooks
|
||||
import useApi from '@/hooks/useApi'
|
||||
|
||||
// icons
|
||||
import { IconFileText, IconLogout, IconX } from '@tabler/icons-react'
|
||||
import accountApi from '@/api/account.api'
|
||||
|
||||
const CloudMenuList = () => {
|
||||
const customization = useSelector((state) => state.customization)
|
||||
const dispatch = useDispatch()
|
||||
useNotifier()
|
||||
const theme = useTheme()
|
||||
|
||||
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
||||
|
||||
const logoutApi = useApi(accountApi.logout)
|
||||
const { isCloud } = useConfig()
|
||||
|
||||
const signOutClicked = () => {
|
||||
logoutApi.request()
|
||||
enqueueSnackbar({
|
||||
message: 'Logging out...',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'success',
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
if (logoutApi.data && logoutApi.data.message === 'logged_out') {
|
||||
store.dispatch(logoutSuccess())
|
||||
window.location.href = logoutApi.data.redirectTo
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}, [logoutApi.data])
|
||||
|
||||
return (
|
||||
<>
|
||||
{isCloud && (
|
||||
<Box>
|
||||
<Divider sx={{ height: '1px', borderColor: theme.palette.grey[900] + 25, my: 0 }} />
|
||||
<List sx={{ p: '16px', py: 2, display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<a href='https://docs.flowiseai.com' target='_blank' rel='noreferrer' style={{ textDecoration: 'none' }}>
|
||||
<ListItemButton
|
||||
sx={{
|
||||
borderRadius: `${customization.borderRadius}px`,
|
||||
alignItems: 'flex-start',
|
||||
backgroundColor: 'inherit',
|
||||
py: 1.25,
|
||||
pl: '24px'
|
||||
}}
|
||||
>
|
||||
<ListItemIcon sx={{ my: 'auto', minWidth: 36 }}>
|
||||
<IconFileText size='1.3rem' strokeWidth='1.5' />
|
||||
</ListItemIcon>
|
||||
<Typography variant='body1' color='inherit' sx={{ my: 0.5 }}>
|
||||
Documentation
|
||||
</Typography>
|
||||
</ListItemButton>
|
||||
</a>
|
||||
<ListItemButton
|
||||
onClick={signOutClicked}
|
||||
sx={{
|
||||
borderRadius: `${customization.borderRadius}px`,
|
||||
alignItems: 'flex-start',
|
||||
backgroundColor: 'inherit',
|
||||
py: 1.25,
|
||||
pl: '24px'
|
||||
}}
|
||||
>
|
||||
<ListItemIcon sx={{ my: 'auto', minWidth: 36 }}>
|
||||
<IconLogout size='1.3rem' strokeWidth='1.5' />
|
||||
</ListItemIcon>
|
||||
<Typography variant='body1' color='inherit' sx={{ my: 0.5 }}>
|
||||
Logout
|
||||
</Typography>
|
||||
</ListItemButton>
|
||||
</List>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CloudMenuList
|
||||
@@ -7,19 +7,25 @@ import { Divider, List, Typography } from '@mui/material'
|
||||
// project imports
|
||||
import NavItem from '../NavItem'
|
||||
import NavCollapse from '../NavCollapse'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import { Available } from '@/ui-component/rbac/available'
|
||||
|
||||
// ==============================|| SIDEBAR MENU LIST GROUP ||============================== //
|
||||
|
||||
const NavGroup = ({ item }) => {
|
||||
const theme = useTheme()
|
||||
const { hasPermission, hasDisplay } = useAuth()
|
||||
|
||||
// menu list collapse & items
|
||||
const items = item.children?.map((menu) => {
|
||||
const listItems = (menu, level = 1) => {
|
||||
// Filter based on display and permission
|
||||
if (!shouldDisplayMenu(menu)) return null
|
||||
|
||||
// Handle item and group types
|
||||
switch (menu.type) {
|
||||
case 'collapse':
|
||||
return <NavCollapse key={menu.id} menu={menu} level={1} />
|
||||
return <NavCollapse key={menu.id} menu={menu} level={level} />
|
||||
case 'item':
|
||||
return <NavItem key={menu.id} item={menu} level={1} navType='MENU' />
|
||||
return <NavItem key={menu.id} item={menu} level={level} navType='MENU' />
|
||||
default:
|
||||
return (
|
||||
<Typography key={menu.id} variant='h6' color='error' align='center'>
|
||||
@@ -27,7 +33,40 @@ const NavGroup = ({ item }) => {
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const shouldDisplayMenu = (menu) => {
|
||||
// Handle permission check
|
||||
if (menu.permission && !hasPermission(menu.permission)) {
|
||||
return false // Do not render if permission is lacking
|
||||
}
|
||||
|
||||
// If `display` is defined, check against cloud/enterprise conditions
|
||||
if (menu.display) {
|
||||
const shouldsiplay = hasDisplay(menu.display)
|
||||
return shouldsiplay
|
||||
}
|
||||
|
||||
// If `display` is not defined, display by default
|
||||
return true
|
||||
}
|
||||
|
||||
const renderPrimaryItems = () => {
|
||||
const primaryGroup = item.children.find((child) => child.id === 'primary')
|
||||
return primaryGroup.children
|
||||
}
|
||||
|
||||
const renderNonPrimaryGroups = () => {
|
||||
let nonprimaryGroups = item.children.filter((child) => child.id !== 'primary')
|
||||
// Display chilren based on permission and display
|
||||
nonprimaryGroups = nonprimaryGroups.map((group) => {
|
||||
const children = group.children.filter((menu) => shouldDisplayMenu(menu))
|
||||
return { ...group, children }
|
||||
})
|
||||
// Get rid of group with empty children
|
||||
nonprimaryGroups = nonprimaryGroups.filter((group) => group.children.length > 0)
|
||||
return nonprimaryGroups
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -44,13 +83,31 @@ const NavGroup = ({ item }) => {
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
sx={{ py: '20px' }}
|
||||
sx={{ p: '16px', py: 2, display: 'flex', flexDirection: 'column', gap: 1 }}
|
||||
>
|
||||
{items}
|
||||
{renderPrimaryItems().map((menu) => listItems(menu))}
|
||||
</List>
|
||||
|
||||
{/* group divider */}
|
||||
<Divider sx={{ mt: 0.25, mb: 1.25 }} />
|
||||
{renderNonPrimaryGroups().map((group) => {
|
||||
const groupPermissions = group.children.map((menu) => menu.permission).join(',')
|
||||
return (
|
||||
<Available key={group.id} permission={groupPermissions}>
|
||||
<>
|
||||
<Divider sx={{ height: '1px', borderColor: theme.palette.grey[900] + 25, my: 0 }} />
|
||||
<List
|
||||
subheader={
|
||||
<Typography variant='caption' sx={{ ...theme.typography.subMenuCaption }} display='block' gutterBottom>
|
||||
{group.title}
|
||||
</Typography>
|
||||
}
|
||||
sx={{ p: '16px', py: 2, display: 'flex', flexDirection: 'column', gap: 1 }}
|
||||
>
|
||||
{group.children.map((menu) => listItems(menu))}
|
||||
</List>
|
||||
</>
|
||||
</Available>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -101,7 +101,6 @@ const NavItem = ({ item, level, navType, onClick, onUploadFile }) => {
|
||||
disabled={item.disabled}
|
||||
sx={{
|
||||
borderRadius: `${customization.borderRadius}px`,
|
||||
mb: 0.5,
|
||||
alignItems: 'flex-start',
|
||||
backgroundColor: level > 1 ? 'transparent !important' : 'inherit',
|
||||
py: level > 1 ? 1 : 1.25,
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// material-ui
|
||||
import { Typography } from '@mui/material'
|
||||
import { Box, Typography } from '@mui/material'
|
||||
|
||||
// project imports
|
||||
import NavGroup from './NavGroup'
|
||||
import menuItem from '@/menu-items'
|
||||
import { menuItems } from '@/menu-items'
|
||||
|
||||
// ==============================|| SIDEBAR MENU LIST ||============================== //
|
||||
|
||||
const MenuList = () => {
|
||||
const navItems = menuItem.items.map((item) => {
|
||||
const navItems = menuItems.items.map((item) => {
|
||||
switch (item.type) {
|
||||
case 'group':
|
||||
return <NavGroup key={item.id} item={item} />
|
||||
@@ -21,7 +21,7 @@ const MenuList = () => {
|
||||
}
|
||||
})
|
||||
|
||||
return <>{navItems}</>
|
||||
return <Box>{navItems}</Box>
|
||||
}
|
||||
|
||||
export default MenuList
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Box, Skeleton, Typography } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import PropTypes from 'prop-types'
|
||||
import { StyledButton } from '@/ui-component/button/StyledButton'
|
||||
|
||||
const TrialInfo = ({ billingPortalUrl, isLoading, paymentMethodExists, trialDaysLeft }) => {
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
p: '24px',
|
||||
py: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'start',
|
||||
gap: 2,
|
||||
borderTop: 1,
|
||||
borderBottom: '1px solid',
|
||||
borderColor: theme.palette.grey[900] + 25,
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Box display='flex' flexDirection='column' gap={1} sx={{ width: '100%' }}>
|
||||
<Skeleton width='100%' height={32} />
|
||||
<Skeleton width='100%' height={32} />
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
<Typography variant='body1' color='inherit' sx={{ lineHeight: '1.5' }}>
|
||||
There are{' '}
|
||||
<Typography variant='' color='error'>
|
||||
{trialDaysLeft} days left
|
||||
</Typography>{' '}
|
||||
in your trial. {!paymentMethodExists ? 'Update your payment method to avoid service interruption.' : ''}
|
||||
</Typography>
|
||||
{!paymentMethodExists && (
|
||||
<a href={billingPortalUrl} target='_blank' rel='noreferrer' style={{ width: '100%' }}>
|
||||
<StyledButton variant='contained' sx={{ borderRadius: 2, height: 32, width: '100%' }}>
|
||||
Update Payment Method
|
||||
</StyledButton>
|
||||
</a>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
TrialInfo.propTypes = {
|
||||
billingPortalUrl: PropTypes.string,
|
||||
isLoading: PropTypes.bool,
|
||||
paymentMethodExists: PropTypes.bool,
|
||||
trialDaysLeft: PropTypes.number
|
||||
}
|
||||
|
||||
export default TrialInfo
|
||||
@@ -1,4 +1,5 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
// material-ui
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
@@ -11,6 +12,9 @@ import { BrowserView, MobileView } from 'react-device-detect'
|
||||
// project imports
|
||||
import MenuList from './MenuList'
|
||||
import LogoSection from '../LogoSection'
|
||||
import CloudMenuList from '@/layout/MainLayout/Sidebar/CloudMenuList'
|
||||
|
||||
// store
|
||||
import { drawerWidth, headerHeight } from '@/store/constant'
|
||||
|
||||
// ==============================|| SIDEBAR DRAWER ||============================== //
|
||||
@@ -18,6 +22,7 @@ import { drawerWidth, headerHeight } from '@/store/constant'
|
||||
const Sidebar = ({ drawerOpen, drawerToggle, window }) => {
|
||||
const theme = useTheme()
|
||||
const matchUpMd = useMediaQuery(theme.breakpoints.up('md'))
|
||||
const isAuthenticated = useSelector((state) => state.auth.isAuthenticated)
|
||||
|
||||
const drawer = (
|
||||
<>
|
||||
@@ -36,16 +41,18 @@ const Sidebar = ({ drawerOpen, drawerToggle, window }) => {
|
||||
component='div'
|
||||
style={{
|
||||
height: !matchUpMd ? 'calc(100vh - 56px)' : `calc(100vh - ${headerHeight}px)`,
|
||||
paddingLeft: '16px',
|
||||
paddingRight: '16px'
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
>
|
||||
<MenuList />
|
||||
<CloudMenuList />
|
||||
</PerfectScrollbar>
|
||||
</BrowserView>
|
||||
<MobileView>
|
||||
<Box sx={{ px: 2 }}>
|
||||
<MenuList />
|
||||
<CloudMenuList />
|
||||
</Box>
|
||||
</MobileView>
|
||||
</>
|
||||
@@ -62,30 +69,31 @@ const Sidebar = ({ drawerOpen, drawerToggle, window }) => {
|
||||
}}
|
||||
aria-label='mailbox folders'
|
||||
>
|
||||
<Drawer
|
||||
container={container}
|
||||
variant={matchUpMd ? 'persistent' : 'temporary'}
|
||||
anchor='left'
|
||||
open={drawerOpen}
|
||||
onClose={drawerToggle}
|
||||
sx={{
|
||||
'& .MuiDrawer-paper': {
|
||||
width: drawerWidth,
|
||||
background: theme.palette.background.default,
|
||||
color: theme.palette.text.primary,
|
||||
[theme.breakpoints.up('md')]: {
|
||||
top: `${headerHeight}px`
|
||||
},
|
||||
borderRight: drawerOpen ? '1px solid' : 'none',
|
||||
borderColor: drawerOpen ? theme.palette.primary[200] + 75 : 'transparent',
|
||||
zIndex: 1000
|
||||
}
|
||||
}}
|
||||
ModalProps={{ keepMounted: true }}
|
||||
color='inherit'
|
||||
>
|
||||
{drawer}
|
||||
</Drawer>
|
||||
{isAuthenticated && (
|
||||
<Drawer
|
||||
container={container}
|
||||
variant={matchUpMd ? 'persistent' : 'temporary'}
|
||||
anchor='left'
|
||||
open={drawerOpen}
|
||||
onClose={drawerToggle}
|
||||
sx={{
|
||||
'& .MuiDrawer-paper': {
|
||||
width: drawerWidth,
|
||||
background: theme.palette.background.default,
|
||||
color: theme.palette.text.primary,
|
||||
[theme.breakpoints.up('md')]: {
|
||||
top: `${headerHeight}px`
|
||||
},
|
||||
borderRight: drawerOpen ? '1px solid' : 'none',
|
||||
borderColor: drawerOpen ? theme.palette.grey[900] + 25 : 'transparent'
|
||||
}
|
||||
}}
|
||||
ModalProps={{ keepMounted: true }}
|
||||
color='inherit'
|
||||
>
|
||||
{drawer}
|
||||
</Drawer>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ const MainLayout = () => {
|
||||
transition: leftDrawerOpened ? theme.transitions.create('width') : 'none'
|
||||
}}
|
||||
>
|
||||
<Toolbar sx={{ height: `${headerHeight}px`, borderBottom: '1px solid', borderColor: theme.palette.primary[200] + 75 }}>
|
||||
<Toolbar sx={{ height: `${headerHeight}px`, borderBottom: '1px solid', borderColor: theme.palette.grey[900] + 25 }}>
|
||||
<Header handleLeftDrawerToggle={handleLeftDrawerToggle} />
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
@@ -50,42 +50,48 @@ const agent_settings = {
|
||||
title: 'Configuration',
|
||||
type: 'item',
|
||||
url: '',
|
||||
icon: icons.IconAdjustmentsHorizontal
|
||||
icon: icons.IconAdjustmentsHorizontal,
|
||||
permission: 'agentflows:config'
|
||||
},
|
||||
{
|
||||
id: 'saveAsTemplate',
|
||||
title: 'Save As Template',
|
||||
type: 'item',
|
||||
url: '',
|
||||
icon: icons.IconTemplate
|
||||
icon: icons.IconTemplate,
|
||||
permission: 'templates:flowexport'
|
||||
},
|
||||
{
|
||||
id: 'duplicateChatflow',
|
||||
title: 'Duplicate Agents',
|
||||
type: 'item',
|
||||
url: '',
|
||||
icon: icons.IconCopy
|
||||
icon: icons.IconCopy,
|
||||
permission: 'agentflows:duplicate'
|
||||
},
|
||||
{
|
||||
id: 'loadChatflow',
|
||||
title: 'Load Agents',
|
||||
type: 'item',
|
||||
url: '',
|
||||
icon: icons.IconFileUpload
|
||||
icon: icons.IconFileUpload,
|
||||
permission: 'agentflows:import'
|
||||
},
|
||||
{
|
||||
id: 'exportChatflow',
|
||||
title: 'Export Agents',
|
||||
type: 'item',
|
||||
url: '',
|
||||
icon: icons.IconFileExport
|
||||
icon: icons.IconFileExport,
|
||||
permission: 'agentflows:export'
|
||||
},
|
||||
{
|
||||
id: 'deleteChatflow',
|
||||
title: 'Delete Agents',
|
||||
type: 'item',
|
||||
url: '',
|
||||
icon: icons.IconTrash
|
||||
icon: icons.IconTrash,
|
||||
permission: 'agentflows:delete'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -35,14 +35,16 @@ const customAssistantSettings = {
|
||||
title: 'Configuration',
|
||||
type: 'item',
|
||||
url: '',
|
||||
icon: icons.IconAdjustmentsHorizontal
|
||||
icon: icons.IconAdjustmentsHorizontal,
|
||||
permission: 'assistants:update'
|
||||
},
|
||||
{
|
||||
id: 'deleteAssistant',
|
||||
title: 'Delete Assistant',
|
||||
type: 'item',
|
||||
url: '',
|
||||
icon: icons.IconTrash
|
||||
icon: icons.IconTrash,
|
||||
permission: 'assistants:delete'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// assets
|
||||
import {
|
||||
IconList,
|
||||
IconUsersGroup,
|
||||
IconHierarchy,
|
||||
IconBuildingStore,
|
||||
@@ -7,23 +8,50 @@ import {
|
||||
IconTool,
|
||||
IconLock,
|
||||
IconRobot,
|
||||
IconSettings,
|
||||
IconVariable,
|
||||
IconFiles,
|
||||
IconTestPipe,
|
||||
IconMicroscope,
|
||||
IconDatabase,
|
||||
IconChartHistogram,
|
||||
IconUserEdit,
|
||||
IconFileUpload,
|
||||
IconClipboardList,
|
||||
IconStack2,
|
||||
IconUsers,
|
||||
IconLockCheck,
|
||||
IconFileDatabase,
|
||||
IconShieldLock,
|
||||
IconListCheck
|
||||
} from '@tabler/icons-react'
|
||||
|
||||
// constant
|
||||
const icons = {
|
||||
IconListCheck,
|
||||
IconUsersGroup,
|
||||
IconHierarchy,
|
||||
IconUsersGroup,
|
||||
IconBuildingStore,
|
||||
IconList,
|
||||
IconKey,
|
||||
IconTool,
|
||||
IconLock,
|
||||
IconRobot,
|
||||
IconSettings,
|
||||
IconVariable,
|
||||
IconFiles
|
||||
IconFiles,
|
||||
IconTestPipe,
|
||||
IconMicroscope,
|
||||
IconDatabase,
|
||||
IconUserEdit,
|
||||
IconChartHistogram,
|
||||
IconFileUpload,
|
||||
IconClipboardList,
|
||||
IconStack2,
|
||||
IconUsers,
|
||||
IconLockCheck,
|
||||
IconFileDatabase,
|
||||
IconShieldLock,
|
||||
IconListCheck
|
||||
}
|
||||
|
||||
// ==============================|| DASHBOARD MENU ITEMS ||============================== //
|
||||
@@ -34,84 +62,230 @@ const dashboard = {
|
||||
type: 'group',
|
||||
children: [
|
||||
{
|
||||
id: 'chatflows',
|
||||
title: 'Chatflows',
|
||||
type: 'item',
|
||||
url: '/chatflows',
|
||||
icon: icons.IconHierarchy,
|
||||
breadcrumbs: true
|
||||
id: 'primary',
|
||||
title: '',
|
||||
type: 'group',
|
||||
children: [
|
||||
{
|
||||
id: 'chatflows',
|
||||
title: 'Chatflows',
|
||||
type: 'item',
|
||||
url: '/chatflows',
|
||||
icon: icons.IconHierarchy,
|
||||
breadcrumbs: true,
|
||||
permission: 'chatflows:view'
|
||||
},
|
||||
{
|
||||
id: 'agentflows',
|
||||
title: 'Agentflows',
|
||||
type: 'item',
|
||||
url: '/agentflows',
|
||||
icon: icons.IconUsersGroup,
|
||||
breadcrumbs: true,
|
||||
permission: 'agentflows:view'
|
||||
},
|
||||
{
|
||||
id: 'executions',
|
||||
title: 'Executions',
|
||||
type: 'item',
|
||||
url: '/executions',
|
||||
icon: icons.IconListCheck,
|
||||
breadcrumbs: true,
|
||||
permission: 'executions:view'
|
||||
},
|
||||
{
|
||||
id: 'assistants',
|
||||
title: 'Assistants',
|
||||
type: 'item',
|
||||
url: '/assistants',
|
||||
icon: icons.IconRobot,
|
||||
breadcrumbs: true,
|
||||
permission: 'assistants:view'
|
||||
},
|
||||
{
|
||||
id: 'marketplaces',
|
||||
title: 'Marketplaces',
|
||||
type: 'item',
|
||||
url: '/marketplaces',
|
||||
icon: icons.IconBuildingStore,
|
||||
breadcrumbs: true,
|
||||
permission: 'templates:marketplace,templates:custom'
|
||||
},
|
||||
{
|
||||
id: 'tools',
|
||||
title: 'Tools',
|
||||
type: 'item',
|
||||
url: '/tools',
|
||||
icon: icons.IconTool,
|
||||
breadcrumbs: true,
|
||||
permission: 'tools:view'
|
||||
},
|
||||
{
|
||||
id: 'credentials',
|
||||
title: 'Credentials',
|
||||
type: 'item',
|
||||
url: '/credentials',
|
||||
icon: icons.IconLock,
|
||||
breadcrumbs: true,
|
||||
permission: 'credentials:view'
|
||||
},
|
||||
{
|
||||
id: 'variables',
|
||||
title: 'Variables',
|
||||
type: 'item',
|
||||
url: '/variables',
|
||||
icon: icons.IconVariable,
|
||||
breadcrumbs: true,
|
||||
permission: 'variables:view'
|
||||
},
|
||||
{
|
||||
id: 'apikey',
|
||||
title: 'API Keys',
|
||||
type: 'item',
|
||||
url: '/apikey',
|
||||
icon: icons.IconKey,
|
||||
breadcrumbs: true,
|
||||
permission: 'apikeys:view'
|
||||
},
|
||||
{
|
||||
id: 'document-stores',
|
||||
title: 'Document Stores',
|
||||
type: 'item',
|
||||
url: '/document-stores',
|
||||
icon: icons.IconFiles,
|
||||
breadcrumbs: true,
|
||||
permission: 'documentStores:view'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'agentflows',
|
||||
title: 'Agentflows',
|
||||
type: 'item',
|
||||
url: '/agentflows',
|
||||
icon: icons.IconUsersGroup,
|
||||
breadcrumbs: true
|
||||
id: 'evaluations',
|
||||
title: 'Evaluations',
|
||||
type: 'group',
|
||||
children: [
|
||||
{
|
||||
id: 'datasets',
|
||||
title: 'Datasets',
|
||||
type: 'item',
|
||||
url: '/datasets',
|
||||
icon: icons.IconDatabase,
|
||||
breadcrumbs: true,
|
||||
display: 'feat:datasets',
|
||||
permission: 'datasets:view'
|
||||
},
|
||||
{
|
||||
id: 'evaluators',
|
||||
title: 'Evaluators',
|
||||
type: 'item',
|
||||
url: '/evaluators',
|
||||
icon: icons.IconTestPipe,
|
||||
breadcrumbs: true,
|
||||
display: 'feat:evaluators',
|
||||
permission: 'evaluators:view'
|
||||
},
|
||||
{
|
||||
id: 'evaluations',
|
||||
title: 'Evaluations',
|
||||
type: 'item',
|
||||
url: '/evaluations',
|
||||
icon: icons.IconChartHistogram,
|
||||
breadcrumbs: true,
|
||||
display: 'feat:evaluations',
|
||||
permission: 'evaluations:view'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'executions',
|
||||
title: 'Executions',
|
||||
type: 'item',
|
||||
url: '/executions',
|
||||
icon: icons.IconListCheck,
|
||||
breadcrumbs: true
|
||||
id: 'management',
|
||||
title: 'User & Workspace Management',
|
||||
type: 'group',
|
||||
children: [
|
||||
{
|
||||
id: 'sso',
|
||||
title: 'SSO Config',
|
||||
type: 'item',
|
||||
url: '/sso-config',
|
||||
icon: icons.IconShieldLock,
|
||||
breadcrumbs: true,
|
||||
display: 'feat:sso-config',
|
||||
permission: 'sso:manage'
|
||||
},
|
||||
{
|
||||
id: 'roles',
|
||||
title: 'Roles',
|
||||
type: 'item',
|
||||
url: '/roles',
|
||||
icon: icons.IconLockCheck,
|
||||
breadcrumbs: true,
|
||||
display: 'feat:roles',
|
||||
permission: 'roles:manage'
|
||||
},
|
||||
{
|
||||
id: 'users',
|
||||
title: 'Users',
|
||||
type: 'item',
|
||||
url: '/users',
|
||||
icon: icons.IconUsers,
|
||||
breadcrumbs: true,
|
||||
display: 'feat:users',
|
||||
permission: 'users:manage'
|
||||
},
|
||||
{
|
||||
id: 'workspaces',
|
||||
title: 'Workspaces',
|
||||
type: 'item',
|
||||
url: '/workspaces',
|
||||
icon: icons.IconStack2,
|
||||
breadcrumbs: true,
|
||||
display: 'feat:workspaces',
|
||||
permission: 'workspace:view'
|
||||
},
|
||||
{
|
||||
id: 'login-activity',
|
||||
title: 'Login Activity',
|
||||
type: 'item',
|
||||
url: '/login-activity',
|
||||
icon: icons.IconClipboardList,
|
||||
breadcrumbs: true,
|
||||
display: 'feat:login-activity',
|
||||
permission: 'loginActivity:view'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'assistants',
|
||||
title: 'Assistants',
|
||||
type: 'item',
|
||||
url: '/assistants',
|
||||
icon: icons.IconRobot,
|
||||
breadcrumbs: true
|
||||
},
|
||||
{
|
||||
id: 'marketplaces',
|
||||
title: 'Marketplaces',
|
||||
type: 'item',
|
||||
url: '/marketplaces',
|
||||
icon: icons.IconBuildingStore,
|
||||
breadcrumbs: true
|
||||
},
|
||||
{
|
||||
id: 'tools',
|
||||
title: 'Tools',
|
||||
type: 'item',
|
||||
url: '/tools',
|
||||
icon: icons.IconTool,
|
||||
breadcrumbs: true
|
||||
},
|
||||
{
|
||||
id: 'credentials',
|
||||
title: 'Credentials',
|
||||
type: 'item',
|
||||
url: '/credentials',
|
||||
icon: icons.IconLock,
|
||||
breadcrumbs: true
|
||||
},
|
||||
{
|
||||
id: 'variables',
|
||||
title: 'Variables',
|
||||
type: 'item',
|
||||
url: '/variables',
|
||||
icon: icons.IconVariable,
|
||||
breadcrumbs: true
|
||||
},
|
||||
{
|
||||
id: 'apikey',
|
||||
title: 'API Keys',
|
||||
type: 'item',
|
||||
url: '/apikey',
|
||||
icon: icons.IconKey,
|
||||
breadcrumbs: true
|
||||
},
|
||||
{
|
||||
id: 'document-stores',
|
||||
title: 'Document Stores',
|
||||
type: 'item',
|
||||
url: '/document-stores',
|
||||
icon: icons.IconFiles,
|
||||
breadcrumbs: true
|
||||
id: 'others',
|
||||
title: 'Others',
|
||||
type: 'group',
|
||||
children: [
|
||||
{
|
||||
id: 'logs',
|
||||
title: 'Logs',
|
||||
type: 'item',
|
||||
url: '/logs',
|
||||
icon: icons.IconList,
|
||||
breadcrumbs: true,
|
||||
display: 'feat:logs',
|
||||
permission: 'logs:view'
|
||||
},
|
||||
// {
|
||||
// id: 'files',
|
||||
// title: 'Files',
|
||||
// type: 'item',
|
||||
// url: '/files',
|
||||
// icon: icons.IconFileDatabase,
|
||||
// breadcrumbs: true,
|
||||
// display: 'feat:files',
|
||||
// },
|
||||
{
|
||||
id: 'account',
|
||||
title: 'Account Settings',
|
||||
type: 'item',
|
||||
url: '/account',
|
||||
icon: icons.IconSettings,
|
||||
breadcrumbs: true,
|
||||
display: 'feat:account'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ import dashboard from './dashboard'
|
||||
|
||||
// ==============================|| MENU ITEMS ||============================== //
|
||||
|
||||
const menuItems = {
|
||||
export const menuItems = {
|
||||
items: [dashboard]
|
||||
}
|
||||
|
||||
export default menuItems
|
||||
|
||||
@@ -57,6 +57,7 @@ const settings = {
|
||||
title: 'Configuration',
|
||||
type: 'item',
|
||||
url: '',
|
||||
permission: 'chatflows:config',
|
||||
icon: icons.IconAdjustmentsHorizontal
|
||||
},
|
||||
{
|
||||
@@ -64,35 +65,40 @@ const settings = {
|
||||
title: 'Save As Template',
|
||||
type: 'item',
|
||||
url: '',
|
||||
icon: icons.IconTemplate
|
||||
icon: icons.IconTemplate,
|
||||
permission: 'templates:flowexport'
|
||||
},
|
||||
{
|
||||
id: 'duplicateChatflow',
|
||||
title: 'Duplicate Chatflow',
|
||||
type: 'item',
|
||||
url: '',
|
||||
icon: icons.IconCopy
|
||||
icon: icons.IconCopy,
|
||||
permission: 'chatflows:duplicate'
|
||||
},
|
||||
{
|
||||
id: 'loadChatflow',
|
||||
title: 'Load Chatflow',
|
||||
type: 'item',
|
||||
url: '',
|
||||
icon: icons.IconFileUpload
|
||||
icon: icons.IconFileUpload,
|
||||
permission: 'chatflows:import'
|
||||
},
|
||||
{
|
||||
id: 'exportChatflow',
|
||||
title: 'Export Chatflow',
|
||||
type: 'item',
|
||||
url: '',
|
||||
icon: icons.IconFileExport
|
||||
icon: icons.IconFileExport,
|
||||
permission: 'chatflows:export'
|
||||
},
|
||||
{
|
||||
id: 'deleteChatflow',
|
||||
title: 'Delete Chatflow',
|
||||
type: 'item',
|
||||
url: '',
|
||||
icon: icons.IconTrash
|
||||
icon: icons.IconTrash,
|
||||
permission: 'chatflows:delete'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { lazy } from 'react'
|
||||
|
||||
import Loadable from '@/ui-component/loading/Loadable'
|
||||
import AuthLayout from '@/layout/AuthLayout'
|
||||
|
||||
const ResolveLoginPage = Loadable(lazy(() => import('@/views/auth/login')))
|
||||
const SignInPage = Loadable(lazy(() => import('@/views/auth/signIn')))
|
||||
const RegisterPage = Loadable(lazy(() => import('@/views/auth/register')))
|
||||
const VerifyEmailPage = Loadable(lazy(() => import('@/views/auth/verify-email')))
|
||||
const ForgotPasswordPage = Loadable(lazy(() => import('@/views/auth/forgotPassword')))
|
||||
const ResetPasswordPage = Loadable(lazy(() => import('@/views/auth/resetPassword')))
|
||||
const UnauthorizedPage = Loadable(lazy(() => import('@/views/auth/unauthorized')))
|
||||
const OrganizationSetupPage = Loadable(lazy(() => import('@/views/organization/index')))
|
||||
const LicenseExpiredPage = Loadable(lazy(() => import('@/views/auth/expired')))
|
||||
|
||||
const AuthRoutes = {
|
||||
path: '/',
|
||||
element: <AuthLayout />,
|
||||
children: [
|
||||
{
|
||||
path: '/login',
|
||||
element: <ResolveLoginPage />
|
||||
},
|
||||
{
|
||||
path: '/signin',
|
||||
element: <SignInPage />
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
element: <RegisterPage />
|
||||
},
|
||||
{
|
||||
path: '/verify',
|
||||
element: <VerifyEmailPage />
|
||||
},
|
||||
{
|
||||
path: '/forgot-password',
|
||||
element: <ForgotPasswordPage />
|
||||
},
|
||||
{
|
||||
path: '/reset-password',
|
||||
element: <ResetPasswordPage />
|
||||
},
|
||||
{
|
||||
path: '/unauthorized',
|
||||
element: <UnauthorizedPage />
|
||||
},
|
||||
{
|
||||
path: '/organization-setup',
|
||||
element: <OrganizationSetupPage />
|
||||
},
|
||||
{
|
||||
path: '/license-expired',
|
||||
element: <LicenseExpiredPage />
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export default AuthRoutes
|
||||
@@ -3,6 +3,7 @@ import { lazy } from 'react'
|
||||
// project imports
|
||||
import Loadable from '@/ui-component/loading/Loadable'
|
||||
import MinimalLayout from '@/layout/MinimalLayout'
|
||||
import { RequireAuth } from '@/routes/RequireAuth'
|
||||
|
||||
// canvas routing
|
||||
const Canvas = Loadable(lazy(() => import('@/views/canvas')))
|
||||
@@ -18,35 +19,67 @@ const CanvasRoutes = {
|
||||
children: [
|
||||
{
|
||||
path: '/canvas',
|
||||
element: <Canvas />
|
||||
element: (
|
||||
<RequireAuth permission={'chatflows:view'}>
|
||||
<Canvas />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/canvas/:id',
|
||||
element: <Canvas />
|
||||
element: (
|
||||
<RequireAuth permission={'chatflows:view'}>
|
||||
<Canvas />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/agentcanvas',
|
||||
element: <Canvas />
|
||||
element: (
|
||||
<RequireAuth permission={'agentflows:view'}>
|
||||
<Canvas />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/agentcanvas/:id',
|
||||
element: <Canvas />
|
||||
element: (
|
||||
<RequireAuth permission={'agentflows:view'}>
|
||||
<Canvas />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/v2/agentcanvas',
|
||||
element: <CanvasV2 />
|
||||
element: (
|
||||
<RequireAuth permission={'agentflows:view'}>
|
||||
<CanvasV2 />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/v2/agentcanvas/:id',
|
||||
element: <CanvasV2 />
|
||||
element: (
|
||||
<RequireAuth permission={'agentflows:view'}>
|
||||
<CanvasV2 />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/marketplace/:id',
|
||||
element: <MarketplaceCanvas />
|
||||
element: (
|
||||
<RequireAuth permission={'templates:marketplace,templates:custom'}>
|
||||
<MarketplaceCanvas />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/v2/marketplace/:id',
|
||||
element: <MarketplaceCanvasV2 />
|
||||
element: (
|
||||
<RequireAuth permission={'templates:marketplace,templates:custom'}>
|
||||
<MarketplaceCanvasV2 />
|
||||
</RequireAuth>
|
||||
)
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { lazy } from 'react'
|
||||
import MainLayout from '@/layout/MainLayout'
|
||||
import Loadable from '@/ui-component/loading/Loadable'
|
||||
|
||||
import { RequireAuth } from '@/routes/RequireAuth'
|
||||
|
||||
// chatflows routing
|
||||
const Chatflows = Loadable(lazy(() => import('@/views/chatflows')))
|
||||
|
||||
@@ -39,9 +41,35 @@ const LoaderConfigPreviewChunks = Loadable(lazy(() => import('@/views/docstore/L
|
||||
const VectorStoreConfigure = Loadable(lazy(() => import('@/views/docstore/VectorStoreConfigure')))
|
||||
const VectorStoreQuery = Loadable(lazy(() => import('@/views/docstore/VectorStoreQuery')))
|
||||
|
||||
// execution routing
|
||||
// Evaluations routing
|
||||
const EvalEvaluation = Loadable(lazy(() => import('@/views/evaluations/index')))
|
||||
const EvaluationResult = Loadable(lazy(() => import('@/views/evaluations/EvaluationResult')))
|
||||
const EvalDatasetRows = Loadable(lazy(() => import('@/views/datasets/DatasetItems')))
|
||||
const EvalDatasets = Loadable(lazy(() => import('@/views/datasets')))
|
||||
const Evaluators = Loadable(lazy(() => import('@/views/evaluators')))
|
||||
|
||||
// account routing
|
||||
const Account = Loadable(lazy(() => import('@/views/account')))
|
||||
const UserProfile = Loadable(lazy(() => import('@/views/account/UserProfile')))
|
||||
|
||||
// files routing
|
||||
const Files = Loadable(lazy(() => import('@/views/files')))
|
||||
|
||||
// logs routing
|
||||
const Logs = Loadable(lazy(() => import('@/views/serverlogs')))
|
||||
|
||||
// executions routing
|
||||
const Executions = Loadable(lazy(() => import('@/views/agentexecutions')))
|
||||
|
||||
// enterprise features
|
||||
const UsersPage = Loadable(lazy(() => import('@/views/users')))
|
||||
const RolesPage = Loadable(lazy(() => import('@/views/roles')))
|
||||
const LoginActivityPage = Loadable(lazy(() => import('@/views/auth/loginActivity')))
|
||||
const Workspaces = Loadable(lazy(() => import('@/views/workspace')))
|
||||
const WorkspaceDetails = Loadable(lazy(() => import('@/views/workspace/WorkspaceUsers')))
|
||||
const SSOConfig = Loadable(lazy(() => import('@/views/auth/ssoConfig')))
|
||||
const SSOSuccess = Loadable(lazy(() => import('@/views/auth/ssoSuccess')))
|
||||
|
||||
// ==============================|| MAIN ROUTING ||============================== //
|
||||
|
||||
const MainRoutes = {
|
||||
@@ -50,83 +78,283 @@ const MainRoutes = {
|
||||
children: [
|
||||
{
|
||||
path: '/',
|
||||
element: <Chatflows />
|
||||
element: (
|
||||
<RequireAuth permission={'chatflows:view'}>
|
||||
<Chatflows />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/chatflows',
|
||||
element: <Chatflows />
|
||||
element: (
|
||||
<RequireAuth permission={'chatflows:view'}>
|
||||
<Chatflows />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/agentflows',
|
||||
element: <Agentflows />
|
||||
element: (
|
||||
<RequireAuth permission={'agentflows:view'}>
|
||||
<Agentflows />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/executions',
|
||||
element: <Executions />
|
||||
element: (
|
||||
<RequireAuth permission={'executions:view'}>
|
||||
<Executions />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/marketplaces',
|
||||
element: <Marketplaces />
|
||||
element: (
|
||||
<RequireAuth permission={'templates:marketplace,templates:custom'}>
|
||||
<Marketplaces />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/apikey',
|
||||
element: <APIKey />
|
||||
element: (
|
||||
<RequireAuth permission={'apikeys:view'}>
|
||||
<APIKey />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/tools',
|
||||
element: <Tools />
|
||||
element: (
|
||||
<RequireAuth permission={'tools:view'}>
|
||||
<Tools />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/assistants',
|
||||
element: <Assistants />
|
||||
element: (
|
||||
<RequireAuth permission={'assistants:view'}>
|
||||
<Assistants />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/assistants/custom',
|
||||
element: <CustomAssistantLayout />
|
||||
element: (
|
||||
<RequireAuth permission={'assistants:view'}>
|
||||
<CustomAssistantLayout />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/assistants/custom/:id',
|
||||
element: <CustomAssistantConfigurePreview />
|
||||
element: (
|
||||
<RequireAuth permission={'assistants:view'}>
|
||||
<CustomAssistantConfigurePreview />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/assistants/openai',
|
||||
element: <OpenAIAssistantLayout />
|
||||
element: (
|
||||
<RequireAuth permission={'assistants:view'}>
|
||||
<OpenAIAssistantLayout />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/credentials',
|
||||
element: <Credentials />
|
||||
element: (
|
||||
<RequireAuth permission={'credentials:view'}>
|
||||
<Credentials />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/variables',
|
||||
element: <Variables />
|
||||
element: (
|
||||
<RequireAuth permission={'variables:view'}>
|
||||
<Variables />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/document-stores',
|
||||
element: <Documents />
|
||||
element: (
|
||||
<RequireAuth permission={'documentStores:view'}>
|
||||
<Documents />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/document-stores/:storeId',
|
||||
element: <DocumentStoreDetail />
|
||||
element: (
|
||||
<RequireAuth permission={'documentStores:view'}>
|
||||
<DocumentStoreDetail />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/document-stores/chunks/:storeId/:fileId',
|
||||
element: <ShowStoredChunks />
|
||||
element: (
|
||||
<RequireAuth permission={'documentStores:view'}>
|
||||
<ShowStoredChunks />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/document-stores/:storeId/:name',
|
||||
element: <LoaderConfigPreviewChunks />
|
||||
element: (
|
||||
<RequireAuth permission={'documentStores:view'}>
|
||||
<LoaderConfigPreviewChunks />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/document-stores/vector/:storeId',
|
||||
element: <VectorStoreConfigure />
|
||||
element: (
|
||||
<RequireAuth permission={'documentStores:view'}>
|
||||
<VectorStoreConfigure />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/document-stores/vector/:storeId/:docId',
|
||||
element: <VectorStoreConfigure />
|
||||
element: (
|
||||
<RequireAuth permission={'documentStores:view'}>
|
||||
<VectorStoreConfigure />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/document-stores/query/:storeId',
|
||||
element: <VectorStoreQuery />
|
||||
element: (
|
||||
<RequireAuth permission={'documentStores:view'}>
|
||||
<VectorStoreQuery />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/datasets',
|
||||
element: (
|
||||
<RequireAuth permission={'datasets:view'} display={'feat:datasets'}>
|
||||
<EvalDatasets />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/dataset_rows/:id',
|
||||
element: (
|
||||
<RequireAuth permission={'datasets:view'} display={'feat:datasets'}>
|
||||
<EvalDatasetRows />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/evaluations',
|
||||
element: (
|
||||
<RequireAuth permission={'evaluations:view'} display={'feat:evaluations'}>
|
||||
<EvalEvaluation />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/evaluation_results/:id',
|
||||
element: (
|
||||
<RequireAuth permission={'evaluations:view'} display={'feat:evaluations'}>
|
||||
<EvaluationResult />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/evaluators',
|
||||
element: (
|
||||
<RequireAuth permission={'evaluators:view'} display={'feat:evaluators'}>
|
||||
<Evaluators />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/logs',
|
||||
element: (
|
||||
<RequireAuth permission={'logs:view'} display={'feat:logs'}>
|
||||
<Logs />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/files',
|
||||
element: (
|
||||
<RequireAuth display={'feat:files'}>
|
||||
<Files />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/account',
|
||||
element: (
|
||||
<RequireAuth display={'feat:account'}>
|
||||
<Account />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/users',
|
||||
element: (
|
||||
<RequireAuth permission={'users:manage'} display={'feat:users'}>
|
||||
<UsersPage />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/user-profile',
|
||||
element: <UserProfile />
|
||||
},
|
||||
{
|
||||
path: '/roles',
|
||||
element: (
|
||||
<RequireAuth permission={'roles:manage'} display={'feat:roles'}>
|
||||
<RolesPage />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/login-activity',
|
||||
element: (
|
||||
<RequireAuth permission={'loginActivity:view'} display={'feat:login-activity'}>
|
||||
<LoginActivityPage />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/workspaces',
|
||||
element: (
|
||||
<RequireAuth permission={'workspace:view'} display={'feat:workspaces'}>
|
||||
<Workspaces />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/workspace-users/:id',
|
||||
element: (
|
||||
<RequireAuth permission={'workspace:view'} display={'feat:workspaces'}>
|
||||
<WorkspaceDetails />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/sso-config',
|
||||
element: (
|
||||
<RequireAuth permission={'sso:manage'} display={'feat:sso-config'}>
|
||||
<SSOConfig />
|
||||
</RequireAuth>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/sso-success',
|
||||
element: <SSOSuccess />
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Navigate } from 'react-router'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { useConfig } from '@/store/context/ConfigContext'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
/**
|
||||
* Checks if a feature flag is enabled
|
||||
* @param {Object} features - Feature flags object
|
||||
* @param {string} display - Feature flag key to check
|
||||
* @param {React.ReactElement} children - Components to render if feature is enabled
|
||||
* @returns {React.ReactElement} Children or unauthorized redirect
|
||||
*/
|
||||
const checkFeatureFlag = (features, display, children) => {
|
||||
// Validate features object exists and is properly formatted
|
||||
if (!features || Array.isArray(features) || Object.keys(features).length === 0) {
|
||||
return <Navigate to='/unauthorized' replace />
|
||||
}
|
||||
|
||||
// Check if feature flag exists and is enabled
|
||||
if (Object.hasOwnProperty.call(features, display)) {
|
||||
const isFeatureEnabled = features[display] === 'true' || features[display] === true
|
||||
return isFeatureEnabled ? children : <Navigate to='/unauthorized' replace />
|
||||
}
|
||||
|
||||
return <Navigate to='/unauthorized' replace />
|
||||
}
|
||||
|
||||
export const RequireAuth = ({ permission, display, children }) => {
|
||||
const location = useLocation()
|
||||
const { isCloud, isOpenSource, isEnterpriseLicensed } = useConfig()
|
||||
const { hasPermission } = useAuth()
|
||||
const isGlobal = useSelector((state) => state.auth.isGlobal)
|
||||
const currentUser = useSelector((state) => state.auth.user)
|
||||
const features = useSelector((state) => state.auth.features)
|
||||
const permissions = useSelector((state) => state.auth.permissions)
|
||||
|
||||
// Step 1: Authentication Check
|
||||
// Redirect to login if user is not authenticated
|
||||
if (!currentUser) {
|
||||
return <Navigate to='/login' replace state={{ path: location.pathname }} />
|
||||
}
|
||||
|
||||
// Step 2: Deployment Type Specific Logic
|
||||
// Open Source: Only show features without display property
|
||||
if (isOpenSource) {
|
||||
return !display ? children : <Navigate to='/unauthorized' replace />
|
||||
}
|
||||
|
||||
// Cloud & Enterprise: Check both permissions and feature flags
|
||||
if (isCloud || isEnterpriseLicensed) {
|
||||
// Allow access to basic features (no display property)
|
||||
if (!display) return children
|
||||
|
||||
// Check if user has any permissions
|
||||
if (permissions.length === 0) {
|
||||
return <Navigate to='/unauthorized' replace state={{ path: location.pathname }} />
|
||||
}
|
||||
|
||||
// Organization admins bypass permission checks
|
||||
if (isGlobal) {
|
||||
return checkFeatureFlag(features, display, children)
|
||||
}
|
||||
|
||||
// Check user permissions and feature flags
|
||||
if (!permission || hasPermission(permission)) {
|
||||
return checkFeatureFlag(features, display, children)
|
||||
}
|
||||
|
||||
return <Navigate to='/unauthorized' replace />
|
||||
}
|
||||
|
||||
// Fallback: Allow access if none of the above conditions match
|
||||
return children
|
||||
}
|
||||
|
||||
RequireAuth.propTypes = {
|
||||
permission: PropTypes.string,
|
||||
display: PropTypes.string,
|
||||
children: PropTypes.element
|
||||
}
|
||||
@@ -4,11 +4,12 @@ import { useRoutes } from 'react-router-dom'
|
||||
import MainRoutes from './MainRoutes'
|
||||
import CanvasRoutes from './CanvasRoutes'
|
||||
import ChatbotRoutes from './ChatbotRoutes'
|
||||
import ExecutionRoutes from './ExecutionRoutes'
|
||||
import config from '@/config'
|
||||
import AuthRoutes from '@/routes/AuthRoutes'
|
||||
import ExecutionRoutes from './ExecutionRoutes'
|
||||
|
||||
// ==============================|| ROUTING RENDER ||============================== //
|
||||
|
||||
export default function ThemeRoutes() {
|
||||
return useRoutes([MainRoutes, CanvasRoutes, ChatbotRoutes, ExecutionRoutes], config.basename)
|
||||
return useRoutes([MainRoutes, AuthRoutes, CanvasRoutes, ChatbotRoutes, ExecutionRoutes], config.basename)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,17 @@ export const baseURL = import.meta.env.VITE_API_BASE_URL || window.location.orig
|
||||
export const uiBaseURL = import.meta.env.VITE_UI_BASE_URL || window.location.origin
|
||||
export const FLOWISE_CREDENTIAL_ID = 'FLOWISE_CREDENTIAL_ID'
|
||||
export const REDACTED_CREDENTIAL_VALUE = '_FLOWISE_BLANK_07167752-1a71-43b1-bf8f-4f32252165db'
|
||||
export const ErrorMessage = {
|
||||
INVALID_MISSING_TOKEN: 'Invalid or Missing token',
|
||||
TOKEN_EXPIRED: 'Token Expired',
|
||||
REFRESH_TOKEN_EXPIRED: 'Refresh Token Expired',
|
||||
FORBIDDEN: 'Forbidden',
|
||||
UNKNOWN_USER: 'Unknown Username or Password',
|
||||
INCORRECT_PASSWORD: 'Incorrect Password',
|
||||
INACTIVE_USER: 'Inactive User',
|
||||
INVALID_WORKSPACE: 'No Workspace Assigned',
|
||||
UNKNOWN_ERROR: 'Unknown Error'
|
||||
}
|
||||
export const AGENTFLOW_ICONS = [
|
||||
{
|
||||
name: 'conditionAgentflow',
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import platformsettingsApi from '@/api/platformsettings'
|
||||
import PropTypes from 'prop-types'
|
||||
import { createContext, useContext, useEffect, useState } from 'react'
|
||||
|
||||
const ConfigContext = createContext()
|
||||
|
||||
export const ConfigProvider = ({ children }) => {
|
||||
const [config, setConfig] = useState({})
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [isEnterpriseLicensed, setEnterpriseLicensed] = useState(false)
|
||||
const [isCloud, setCloudLicensed] = useState(false)
|
||||
const [isOpenSource, setOpenSource] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const userSettings = platformsettingsApi.getSettings()
|
||||
Promise.all([userSettings])
|
||||
.then(([currentSettingsData]) => {
|
||||
const finalData = {
|
||||
...currentSettingsData.data
|
||||
}
|
||||
setConfig(finalData)
|
||||
if (finalData.PLATFORM_TYPE) {
|
||||
if (finalData.PLATFORM_TYPE === 'enterprise') {
|
||||
setEnterpriseLicensed(true)
|
||||
setCloudLicensed(false)
|
||||
setOpenSource(false)
|
||||
} else if (finalData.PLATFORM_TYPE === 'cloud') {
|
||||
setCloudLicensed(true)
|
||||
setEnterpriseLicensed(false)
|
||||
setOpenSource(false)
|
||||
} else {
|
||||
setOpenSource(true)
|
||||
setEnterpriseLicensed(false)
|
||||
setCloudLicensed(false)
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(false)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error fetching data:', error)
|
||||
setLoading(false)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ConfigContext.Provider value={{ config, loading, isEnterpriseLicensed, isCloud, isOpenSource }}>{children}</ConfigContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useConfig = () => useContext(ConfigContext)
|
||||
|
||||
ConfigProvider.propTypes = {
|
||||
children: PropTypes.any
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { createContext, useContext, useState } from 'react'
|
||||
import { redirectWhenUnauthorized } from '@/utils/genericHelper'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { store } from '@/store'
|
||||
import { logoutSuccess } from '@/store/reducers/authSlice'
|
||||
import { ErrorMessage } from '../constant'
|
||||
|
||||
const ErrorContext = createContext()
|
||||
|
||||
export const ErrorProvider = ({ children }) => {
|
||||
const [error, setError] = useState(null)
|
||||
const navigate = useNavigate()
|
||||
|
||||
const handleError = async (err) => {
|
||||
console.error(err)
|
||||
if (err?.response?.status === 403) {
|
||||
navigate('/unauthorized')
|
||||
} else if (err?.response?.status === 401) {
|
||||
if (ErrorMessage.INVALID_MISSING_TOKEN === err?.response?.data?.message) {
|
||||
store.dispatch(logoutSuccess())
|
||||
navigate('/login')
|
||||
} else {
|
||||
const isRedirect = err?.response?.data?.redirectTo && err?.response?.data?.error
|
||||
|
||||
if (isRedirect) {
|
||||
redirectWhenUnauthorized({
|
||||
error: err.response.data.error,
|
||||
redirectTo: err.response.data.redirectTo
|
||||
})
|
||||
} else {
|
||||
const currentPath = window.location.pathname
|
||||
if (currentPath !== '/signin' && currentPath !== '/login') {
|
||||
store.dispatch(logoutSuccess())
|
||||
navigate('/login')
|
||||
}
|
||||
}
|
||||
}
|
||||
} else setError(err)
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorContext.Provider
|
||||
value={{
|
||||
error,
|
||||
setError,
|
||||
handleError
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ErrorContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useError = () => useContext(ErrorContext)
|
||||
|
||||
ErrorProvider.propTypes = {
|
||||
children: PropTypes.any
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import customizationReducer from './reducers/customizationReducer'
|
||||
import canvasReducer from './reducers/canvasReducer'
|
||||
import notifierReducer from './reducers/notifierReducer'
|
||||
import dialogReducer from './reducers/dialogReducer'
|
||||
import authReducer from './reducers/authSlice'
|
||||
|
||||
// ==============================|| COMBINE REDUCER ||============================== //
|
||||
|
||||
@@ -12,7 +13,8 @@ const reducer = combineReducers({
|
||||
customization: customizationReducer,
|
||||
canvas: canvasReducer,
|
||||
notifier: notifierReducer,
|
||||
dialog: dialogReducer
|
||||
dialog: dialogReducer,
|
||||
auth: authReducer
|
||||
})
|
||||
|
||||
export default reducer
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// authSlice.js
|
||||
import { createSlice } from '@reduxjs/toolkit'
|
||||
import AuthUtils from '@/utils/authUtils'
|
||||
|
||||
const initialState = {
|
||||
user: localStorage.getItem('user') ? JSON.parse(localStorage.getItem('user')) : null,
|
||||
isAuthenticated: 'true' === localStorage.getItem('isAuthenticated'),
|
||||
isGlobal: 'true' === localStorage.getItem('isGlobal'),
|
||||
token: null,
|
||||
permissions:
|
||||
localStorage.getItem('permissions') && localStorage.getItem('permissions') !== 'undefined'
|
||||
? JSON.parse(localStorage.getItem('permissions'))
|
||||
: null,
|
||||
features: localStorage.getItem('features') ? JSON.parse(localStorage.getItem('features')) : null
|
||||
}
|
||||
|
||||
const authSlice = createSlice({
|
||||
name: 'auth',
|
||||
initialState,
|
||||
reducers: {
|
||||
loginSuccess: (state, action) => {
|
||||
AuthUtils.updateStateAndLocalStorage(state, action.payload)
|
||||
},
|
||||
logoutSuccess: (state) => {
|
||||
state.user = null
|
||||
state.token = null
|
||||
state.permissions = null
|
||||
state.features = null
|
||||
state.isAuthenticated = false
|
||||
state.isGlobal = false
|
||||
AuthUtils.removeCurrentUser()
|
||||
},
|
||||
workspaceSwitchSuccess: (state, action) => {
|
||||
AuthUtils.updateStateAndLocalStorage(state, action.payload)
|
||||
},
|
||||
upgradePlanSuccess: (state, action) => {
|
||||
AuthUtils.updateStateAndLocalStorage(state, action.payload)
|
||||
},
|
||||
userProfileUpdated: (state, action) => {
|
||||
const user = AuthUtils.extractUser(action.payload)
|
||||
state.user.name = user.name
|
||||
state.user.email = user.email
|
||||
AuthUtils.updateCurrentUser(state.user)
|
||||
},
|
||||
workspaceNameUpdated: (state, action) => {
|
||||
const updatedWorkspace = action.payload
|
||||
// find the matching assignedWorkspace and update it
|
||||
const assignedWorkspaces = state.user.assignedWorkspaces.map((workspace) => {
|
||||
if (workspace.id === updatedWorkspace.id) {
|
||||
return {
|
||||
...workspace,
|
||||
name: updatedWorkspace.name
|
||||
}
|
||||
}
|
||||
return workspace
|
||||
})
|
||||
state.user.assignedWorkspaces = assignedWorkspaces
|
||||
AuthUtils.updateCurrentUser(state.user)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const { loginSuccess, logoutSuccess, workspaceSwitchSuccess, upgradePlanSuccess, userProfileUpdated, workspaceNameUpdated } =
|
||||
authSlice.actions
|
||||
export default authSlice.reducer
|
||||
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types'
|
||||
|
||||
import { styled, alpha } from '@mui/material/styles'
|
||||
import Menu from '@mui/material/Menu'
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
import { PermissionMenuItem } from '@/ui-component/button/RBACButtons'
|
||||
import EditIcon from '@mui/icons-material/Edit'
|
||||
import Divider from '@mui/material/Divider'
|
||||
import FileCopyIcon from '@mui/icons-material/FileCopy'
|
||||
@@ -317,48 +317,84 @@ export default function FlowListMenu({ chatflow, isAgentCanvas, setError, update
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<MenuItem onClick={handleFlowRename} disableRipple>
|
||||
<PermissionMenuItem
|
||||
permissionId={isAgentCanvas ? 'agentflows:update' : 'chatflows:update'}
|
||||
onClick={handleFlowRename}
|
||||
disableRipple
|
||||
>
|
||||
<EditIcon />
|
||||
Rename
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleDuplicate} disableRipple>
|
||||
</PermissionMenuItem>
|
||||
<PermissionMenuItem
|
||||
permissionId={isAgentCanvas ? 'agentflows:duplicate' : 'chatflows:duplicate'}
|
||||
onClick={handleDuplicate}
|
||||
disableRipple
|
||||
>
|
||||
<FileCopyIcon />
|
||||
Duplicate
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleExport} disableRipple>
|
||||
</PermissionMenuItem>
|
||||
<PermissionMenuItem
|
||||
permissionId={isAgentCanvas ? 'agentflows:export' : 'chatflows:export'}
|
||||
onClick={handleExport}
|
||||
disableRipple
|
||||
>
|
||||
<FileDownloadIcon />
|
||||
Export
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleExportTemplate} disableRipple>
|
||||
</PermissionMenuItem>
|
||||
<PermissionMenuItem permissionId={'templates:flowexport'} onClick={handleExportTemplate} disableRipple>
|
||||
<ExportTemplateOutlinedIcon />
|
||||
Save As Template
|
||||
</MenuItem>
|
||||
</PermissionMenuItem>
|
||||
<Divider sx={{ my: 0.5 }} />
|
||||
<MenuItem onClick={handleFlowStarterPrompts} disableRipple>
|
||||
<PermissionMenuItem
|
||||
permissionId={isAgentCanvas ? 'agentflows:config' : 'chatflows:config'}
|
||||
onClick={handleFlowStarterPrompts}
|
||||
disableRipple
|
||||
>
|
||||
<PictureInPictureAltIcon />
|
||||
Starter Prompts
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleFlowChatFeedback} disableRipple>
|
||||
</PermissionMenuItem>
|
||||
<PermissionMenuItem
|
||||
permissionId={isAgentCanvas ? 'agentflows:config' : 'chatflows:config'}
|
||||
onClick={handleFlowChatFeedback}
|
||||
disableRipple
|
||||
>
|
||||
<ThumbsUpDownOutlinedIcon />
|
||||
Chat Feedback
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleAllowedDomains} disableRipple>
|
||||
</PermissionMenuItem>
|
||||
<PermissionMenuItem
|
||||
permissionId={isAgentCanvas ? 'agentflows:domains' : 'chatflows:domains'}
|
||||
onClick={handleAllowedDomains}
|
||||
disableRipple
|
||||
>
|
||||
<VpnLockOutlinedIcon />
|
||||
Allowed Domains
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleSpeechToText} disableRipple>
|
||||
</PermissionMenuItem>
|
||||
<PermissionMenuItem
|
||||
permissionId={isAgentCanvas ? 'agentflows:config' : 'chatflows:config'}
|
||||
onClick={handleSpeechToText}
|
||||
disableRipple
|
||||
>
|
||||
<MicNoneOutlinedIcon />
|
||||
Speech To Text
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleFlowCategory} disableRipple>
|
||||
</PermissionMenuItem>
|
||||
<PermissionMenuItem
|
||||
permissionId={isAgentCanvas ? 'agentflows:update' : 'chatflows:update'}
|
||||
onClick={handleFlowCategory}
|
||||
disableRipple
|
||||
>
|
||||
<FileCategoryIcon />
|
||||
Update Category
|
||||
</MenuItem>
|
||||
</PermissionMenuItem>
|
||||
<Divider sx={{ my: 0.5 }} />
|
||||
<MenuItem onClick={handleDelete} disableRipple>
|
||||
<PermissionMenuItem
|
||||
permissionId={isAgentCanvas ? 'agentflows:delete' : 'chatflows:delete'}
|
||||
onClick={handleDelete}
|
||||
disableRipple
|
||||
>
|
||||
<FileDeleteIcon />
|
||||
Delete
|
||||
</MenuItem>
|
||||
</PermissionMenuItem>
|
||||
</StyledMenu>
|
||||
<SaveChatflowDialog
|
||||
show={flowDialogOpen}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import * as PropTypes from 'prop-types'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import { StyledButton, StyledToggleButton } from '@/ui-component/button/StyledButton'
|
||||
import { Button, IconButton, ListItemButton, MenuItem, Tab } from '@mui/material'
|
||||
|
||||
export const StyledPermissionButton = ({ permissionId, display, ...props }) => {
|
||||
const { hasPermission, hasDisplay } = useAuth()
|
||||
|
||||
if (!hasPermission(permissionId) || !hasDisplay(display)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <StyledButton {...props} />
|
||||
}
|
||||
|
||||
export const StyledPermissionToggleButton = ({ permissionId, display, ...props }) => {
|
||||
const { hasPermission, hasDisplay } = useAuth()
|
||||
|
||||
if (!hasPermission(permissionId) || !hasDisplay(display)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <StyledToggleButton {...props} />
|
||||
}
|
||||
|
||||
export const PermissionIconButton = ({ permissionId, display, ...props }) => {
|
||||
const { hasPermission, hasDisplay } = useAuth()
|
||||
|
||||
if (!hasPermission(permissionId) || !hasDisplay(display)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <IconButton {...props} />
|
||||
}
|
||||
|
||||
export const PermissionButton = ({ permissionId, display, ...props }) => {
|
||||
const { hasPermission, hasDisplay } = useAuth()
|
||||
|
||||
if (!hasPermission(permissionId) || !hasDisplay(display)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <Button {...props} />
|
||||
}
|
||||
|
||||
export const PermissionTab = ({ permissionId, display, ...props }) => {
|
||||
const { hasPermission, hasDisplay } = useAuth()
|
||||
|
||||
if (!hasPermission(permissionId) || !hasDisplay(display)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <Tab {...props} />
|
||||
}
|
||||
|
||||
export const PermissionMenuItem = ({ permissionId, display, ...props }) => {
|
||||
const { hasPermission, hasDisplay } = useAuth()
|
||||
|
||||
if (!hasPermission(permissionId) || !hasDisplay(display)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <MenuItem {...props} />
|
||||
}
|
||||
|
||||
export const PermissionListItemButton = ({ permissionId, display, ...props }) => {
|
||||
const { hasPermission, hasDisplay } = useAuth()
|
||||
|
||||
if (!hasPermission(permissionId) || !hasDisplay(display)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <ListItemButton {...props} />
|
||||
}
|
||||
|
||||
StyledPermissionButton.propTypes = { permissionId: PropTypes.string, display: PropTypes.array }
|
||||
StyledPermissionToggleButton.propTypes = { permissionId: PropTypes.string, display: PropTypes.array }
|
||||
PermissionIconButton.propTypes = { permissionId: PropTypes.string, display: PropTypes.array }
|
||||
PermissionButton.propTypes = { permissionId: PropTypes.string, display: PropTypes.array }
|
||||
PermissionTab.propTypes = { permissionId: PropTypes.string, display: PropTypes.array }
|
||||
PermissionMenuItem.propTypes = { permissionId: PropTypes.string, display: PropTypes.array }
|
||||
PermissionListItemButton.propTypes = { permissionId: PropTypes.string, display: PropTypes.array }
|
||||
@@ -22,6 +22,7 @@ const MainCard = forwardRef(function MainCard(
|
||||
py: 0
|
||||
},
|
||||
darkTitle,
|
||||
maxWidth = 'full',
|
||||
secondary,
|
||||
shadow,
|
||||
sx = {},
|
||||
@@ -40,7 +41,7 @@ const MainCard = forwardRef(function MainCard(
|
||||
':hover': {
|
||||
boxShadow: boxShadow ? shadow || '0 2px 14px 0 rgb(32 40 45 / 8%)' : 'inherit'
|
||||
},
|
||||
maxWidth: '1280px',
|
||||
maxWidth: maxWidth === 'sm' ? '800px' : maxWidth === 'md' ? '960px' : '1280px',
|
||||
mx: 'auto',
|
||||
...sx
|
||||
}}
|
||||
@@ -66,6 +67,7 @@ const MainCard = forwardRef(function MainCard(
|
||||
MainCard.propTypes = {
|
||||
border: PropTypes.bool,
|
||||
boxShadow: PropTypes.bool,
|
||||
maxWidth: PropTypes.oneOf(['full', 'sm', 'md']),
|
||||
children: PropTypes.node,
|
||||
content: PropTypes.bool,
|
||||
contentClass: PropTypes.string,
|
||||
|
||||
@@ -13,22 +13,11 @@ const AboutDialog = ({ show, onCancel }) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
const username = localStorage.getItem('username')
|
||||
const password = localStorage.getItem('password')
|
||||
|
||||
const config = {}
|
||||
if (username && password) {
|
||||
config.auth = {
|
||||
username,
|
||||
password
|
||||
}
|
||||
config.headers = {
|
||||
'Content-type': 'application/json',
|
||||
'x-request-from': 'internal'
|
||||
}
|
||||
}
|
||||
const latestReleaseReq = axios.get('https://api.github.com/repos/FlowiseAI/Flowise/releases/latest')
|
||||
const currentVersionReq = axios.get(`${baseURL}/api/v1/version`, { ...config })
|
||||
const currentVersionReq = axios.get(`${baseURL}/api/v1/version`, {
|
||||
withCredentials: true,
|
||||
headers: { 'Content-type': 'application/json', 'x-request-from': 'internal' }
|
||||
})
|
||||
|
||||
Promise.all([latestReleaseReq, currentVersionReq])
|
||||
.then(([latestReleaseData, currentVersionData]) => {
|
||||
|
||||
@@ -83,7 +83,7 @@ const ExpandTextDialog = ({ show, dialogProps, onCancel, onInputHintDialogClicke
|
||||
useEffect(() => {
|
||||
if (executeCustomFunctionNodeApi.error) {
|
||||
if (typeof executeCustomFunctionNodeApi.error === 'object' && executeCustomFunctionNodeApi.error?.response?.data) {
|
||||
setCodeExecutedResult(JSON.stringify(executeCustomFunctionNodeApi.error?.response?.data, null, 2))
|
||||
setCodeExecutedResult(executeCustomFunctionNodeApi.error?.response?.data)
|
||||
} else if (typeof executeCustomFunctionNodeApi.error === 'string') {
|
||||
setCodeExecutedResult(executeCustomFunctionNodeApi.error)
|
||||
}
|
||||
@@ -118,7 +118,7 @@ const ExpandTextDialog = ({ show, dialogProps, onCancel, onInputHintDialogClicke
|
||||
borderColor: theme.palette.grey['500'],
|
||||
borderRadius: '12px',
|
||||
height: '100%',
|
||||
maxHeight: languageType === 'js' ? 'calc(100vh - 330px)' : 'calc(100vh - 220px)',
|
||||
maxHeight: languageType === 'js' ? 'calc(100vh - 250px)' : 'calc(100vh - 220px)',
|
||||
overflowX: 'hidden',
|
||||
backgroundColor: 'white'
|
||||
}}
|
||||
@@ -126,7 +126,7 @@ const ExpandTextDialog = ({ show, dialogProps, onCancel, onInputHintDialogClicke
|
||||
<CodeEditor
|
||||
disabled={dialogProps.disabled}
|
||||
value={inputValue}
|
||||
height={languageType === 'js' ? 'calc(100vh - 330px)' : 'calc(100vh - 220px)'}
|
||||
height={languageType === 'js' ? 'calc(100vh - 250px)' : 'calc(100vh - 220px)'}
|
||||
theme={customization.isDarkMode ? 'dark' : 'light'}
|
||||
lang={languageType}
|
||||
placeholder={inputParam.placeholder}
|
||||
@@ -175,7 +175,9 @@ const ExpandTextDialog = ({ show, dialogProps, onCancel, onInputHintDialogClicke
|
||||
<div style={{ marginTop: '15px' }}>
|
||||
<CodeEditor
|
||||
disabled={true}
|
||||
value={codeExecutedResult.toString()}
|
||||
value={
|
||||
typeof codeExecutedResult === 'object' ? JSON.stringify(codeExecutedResult, null, 2) : codeExecutedResult
|
||||
}
|
||||
height='max-content'
|
||||
theme={customization.isDarkMode ? 'dark' : 'light'}
|
||||
lang={'js'}
|
||||
|
||||
@@ -0,0 +1,730 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
// Material
|
||||
import {
|
||||
Autocomplete,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Box,
|
||||
Chip,
|
||||
Typography,
|
||||
TextField,
|
||||
Stack,
|
||||
Tooltip,
|
||||
styled,
|
||||
Popper,
|
||||
CircularProgress
|
||||
} from '@mui/material'
|
||||
import { autocompleteClasses } from '@mui/material/Autocomplete'
|
||||
|
||||
// Project imports
|
||||
import { StyledButton } from '@/ui-component/button/StyledButton'
|
||||
import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog'
|
||||
|
||||
// Icons
|
||||
import { IconX, IconCircleCheck, IconUser } from '@tabler/icons-react'
|
||||
|
||||
// API
|
||||
import accountApi from '@/api/account.api'
|
||||
import roleApi from '@/api/role'
|
||||
import userApi from '@/api/user'
|
||||
import workspaceApi from '@/api/workspace'
|
||||
|
||||
// Hooks
|
||||
import useApi from '@/hooks/useApi'
|
||||
|
||||
// utils
|
||||
import useNotifier from '@/utils/useNotifier'
|
||||
|
||||
// store
|
||||
import {
|
||||
enqueueSnackbar as enqueueSnackbarAction,
|
||||
closeSnackbar as closeSnackbarAction,
|
||||
HIDE_CANVAS_DIALOG,
|
||||
SHOW_CANVAS_DIALOG
|
||||
} from '@/store/actions'
|
||||
|
||||
const StyledChip = styled(Chip)(({ theme, chiptype }) => {
|
||||
let backgroundColor, color
|
||||
switch (chiptype) {
|
||||
case 'new':
|
||||
backgroundColor = theme.palette.success.light
|
||||
color = theme.palette.success.contrastText
|
||||
break
|
||||
case 'existing':
|
||||
backgroundColor = theme.palette.primary.main
|
||||
color = theme.palette.primary.contrastText
|
||||
break
|
||||
case 'already-in-workspace':
|
||||
backgroundColor = theme.palette.grey[300]
|
||||
color = theme.palette.text.primary
|
||||
break
|
||||
default:
|
||||
backgroundColor = theme.palette.primary.main
|
||||
color = theme.palette.primary.contrastText
|
||||
}
|
||||
return {
|
||||
backgroundColor,
|
||||
color,
|
||||
'& .MuiChip-deleteIcon': {
|
||||
color
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const StyledPopper = styled(Popper)({
|
||||
boxShadow: '0px 8px 10px -5px rgb(0 0 0 / 20%), 0px 16px 24px 2px rgb(0 0 0 / 14%), 0px 6px 30px 5px rgb(0 0 0 / 12%)',
|
||||
borderRadius: '10px',
|
||||
[`& .${autocompleteClasses.listbox}`]: {
|
||||
boxSizing: 'border-box',
|
||||
'& ul': {
|
||||
padding: 10,
|
||||
margin: 10
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const InviteUsersDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
|
||||
const dispatch = useDispatch()
|
||||
|
||||
// ==============================|| Snackbar ||============================== //
|
||||
|
||||
useNotifier()
|
||||
|
||||
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
||||
|
||||
const currentUser = useSelector((state) => state.auth.user)
|
||||
|
||||
const [searchString, setSearchString] = useState('')
|
||||
const [workspaces, setWorkspaces] = useState([])
|
||||
const [selectedWorkspace, setSelectedWorkspace] = useState()
|
||||
const [userSearchResults, setUserSearchResults] = useState([])
|
||||
const [orgUsers, setOrgUsers] = useState([])
|
||||
const [allUsers, setAllUsers] = useState([])
|
||||
const [selectedUsers, setSelectedUsers] = useState([])
|
||||
const [availableRoles, setAvailableRoles] = useState([])
|
||||
const [selectedRole, setSelectedRole] = useState('')
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
|
||||
const getAllRolesApi = useApi(roleApi.getAllRolesByOrganizationId)
|
||||
const getAllWorkspacesByOrganizationIdApi = useApi(workspaceApi.getAllWorkspacesByOrganizationId)
|
||||
const getWorkspacesByUserIdApi = useApi(userApi.getWorkspacesByUserId)
|
||||
|
||||
useEffect(() => {
|
||||
if (getAllWorkspacesByOrganizationIdApi.data) {
|
||||
const workspaces = getAllWorkspacesByOrganizationIdApi.data.map((workspace) => ({
|
||||
id: workspace.id,
|
||||
label: workspace.name,
|
||||
name: workspace.name,
|
||||
description: workspace.description
|
||||
}))
|
||||
setWorkspaces(workspaces)
|
||||
if (dialogProps.type === 'EDIT' && dialogProps.data && dialogProps.data.isWorkspaceUser) {
|
||||
// when clicking on edit user in users page
|
||||
const userActiveWorkspace = workspaces.find(
|
||||
(workspace) => workspace.id === dialogProps.data.activeWorkspaceId || workspace.id === dialogProps.data.workspaceId
|
||||
)
|
||||
setSelectedWorkspace(userActiveWorkspace)
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getAllWorkspacesByOrganizationIdApi.data])
|
||||
|
||||
useEffect(() => {
|
||||
if (getAllRolesApi.data) {
|
||||
const roles = getAllRolesApi.data.map((role) => ({
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
label: role.name,
|
||||
description: role.description
|
||||
}))
|
||||
setAvailableRoles(roles)
|
||||
if (
|
||||
dialogProps.type === 'EDIT' &&
|
||||
dialogProps.data &&
|
||||
Array.isArray(dialogProps.data.assignedRoles) &&
|
||||
dialogProps.data.assignedRoles.length > 0
|
||||
) {
|
||||
const userActiveRole = roles.find((role) => role.name === dialogProps.data.assignedRoles[0].role)
|
||||
if (userActiveRole) setSelectedRole(userActiveRole)
|
||||
}
|
||||
if (dialogProps.type === 'EDIT' && dialogProps.data && dialogProps.data.role && dialogProps.data.role.name) {
|
||||
const userActiveRole = roles.find((role) => role.name === dialogProps.data.role.name)
|
||||
if (userActiveRole) setSelectedRole(userActiveRole)
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getAllRolesApi.data])
|
||||
|
||||
useEffect(() => {
|
||||
if (getWorkspacesByUserIdApi.data) {
|
||||
const data = getWorkspacesByUserIdApi.data[0]
|
||||
const selectedRole = {
|
||||
id: data.role.id,
|
||||
label: data.role.name,
|
||||
name: data.role.name,
|
||||
description: data.role.description
|
||||
}
|
||||
const selectedWorkspace = {
|
||||
id: data.workspace.id,
|
||||
label: data.workspace.name,
|
||||
name: data.workspace.name,
|
||||
description: data.workspace.description
|
||||
}
|
||||
setSelectedRole(selectedRole)
|
||||
setSelectedWorkspace(selectedWorkspace)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getWorkspacesByUserIdApi.data])
|
||||
|
||||
useEffect(() => {
|
||||
getAllRolesApi.request(currentUser.activeOrganizationId)
|
||||
getAllWorkspacesByOrganizationIdApi.request(currentUser.activeOrganizationId)
|
||||
setSearchString('')
|
||||
setUserSearchResults([])
|
||||
setSelectedUsers([])
|
||||
fetchInitialData()
|
||||
if (dialogProps.type === 'ADD' && dialogProps.data) {
|
||||
// when clicking on add user in workspace page
|
||||
const workspace = dialogProps.data
|
||||
setSelectedWorkspace({
|
||||
id: workspace.id,
|
||||
label: workspace.name,
|
||||
name: workspace.name,
|
||||
description: workspace.description
|
||||
})
|
||||
} else if (dialogProps.type === 'ADD' && !dialogProps.data) {
|
||||
// when clicking on add user in users page
|
||||
setSelectedWorkspace(null)
|
||||
} else if (dialogProps.type === 'EDIT' && dialogProps.data && !dialogProps.data.isWorkspaceUser) {
|
||||
getWorkspacesByUserIdApi.request(dialogProps.data.userId)
|
||||
}
|
||||
return () => {
|
||||
setSearchString('')
|
||||
setAllUsers([])
|
||||
setOrgUsers([])
|
||||
setUserSearchResults([])
|
||||
setSelectedUsers([])
|
||||
setWorkspaces([])
|
||||
setSelectedRole(null)
|
||||
setSelectedWorkspace(null)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dialogProps])
|
||||
|
||||
useEffect(() => {
|
||||
if (allUsers.length > 0) {
|
||||
if (dialogProps.type === 'EDIT' && dialogProps.data) {
|
||||
const selectedUser = allUsers.find((item) => item.userId === dialogProps.data.userId)
|
||||
const selectedUserObj = {
|
||||
...selectedUser,
|
||||
isNewUser: false,
|
||||
alreadyInWorkspace: true
|
||||
}
|
||||
// when clicking on edit user in users page
|
||||
handleChange(null, [selectedUserObj])
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [allUsers])
|
||||
|
||||
useEffect(() => {
|
||||
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
|
||||
else dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||
return () => dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||
}, [show, dispatch])
|
||||
|
||||
const fetchInitialData = async () => {
|
||||
try {
|
||||
const response = await userApi.getAllUsersByOrganizationId(currentUser.activeOrganizationId)
|
||||
if (response.data) {
|
||||
let existingUserIds = []
|
||||
|
||||
if (dialogProps.data && dialogProps.type === 'ADD') {
|
||||
// If we're in workspace context (WorkspaceUsers.jsx)
|
||||
// Get existing workspace users
|
||||
const workspaceUsers = await userApi.getAllUsersByWorkspaceId(dialogProps.data.id)
|
||||
existingUserIds = workspaceUsers.data.map((user) => user.userId)
|
||||
setOrgUsers(workspaceUsers.data)
|
||||
} else if (!dialogProps.data && dialogProps.type === 'ADD') {
|
||||
// If we're in organization context (index.jsx)
|
||||
// The existing users are already in the response.data
|
||||
existingUserIds = response.data.filter((user) => user.status.toLowerCase() !== 'invited').map((user) => user.userId)
|
||||
setOrgUsers(response.data)
|
||||
}
|
||||
|
||||
// Filter out:
|
||||
// 1. Current user
|
||||
// 2. Organization owners
|
||||
// 3. Users already in the workspace (if in workspace context)
|
||||
// 4. Active users in the organization (if in organization context)
|
||||
const filteredUsers = response.data.filter(
|
||||
(user) => user.userId !== currentUser.id && !user.isOrgOwner && !existingUserIds.includes(user.userId)
|
||||
)
|
||||
|
||||
setUserSearchResults(() => filteredUsers)
|
||||
setAllUsers(() => filteredUsers) // Set original list only once
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching initial user data:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const saveInvite = async () => {
|
||||
if (selectedUsers.length) {
|
||||
const existingEmails = []
|
||||
for (const orgUser of orgUsers) {
|
||||
if (selectedUsers.some((user) => user.email === orgUser.user.email)) {
|
||||
existingEmails.push(orgUser.user.email)
|
||||
}
|
||||
}
|
||||
if (existingEmails.length > 0) {
|
||||
enqueueSnackbar({
|
||||
message: `The following users are already in the workspace or organization: ${existingEmails.join(', ')}`,
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
setIsSaving(true)
|
||||
try {
|
||||
const responses = await Promise.all(
|
||||
selectedUsers.map(async (item) => {
|
||||
const saveObj = item.isNewUser
|
||||
? {
|
||||
user: {
|
||||
email: item.email,
|
||||
createdBy: currentUser.id
|
||||
},
|
||||
workspace: {
|
||||
id: selectedWorkspace.id
|
||||
},
|
||||
role: {
|
||||
id: selectedRole.id
|
||||
}
|
||||
}
|
||||
: {
|
||||
user: {
|
||||
email: item.user.email,
|
||||
createdBy: currentUser.id
|
||||
},
|
||||
workspace: {
|
||||
id: selectedWorkspace.id
|
||||
},
|
||||
role: {
|
||||
id: selectedRole.id
|
||||
}
|
||||
}
|
||||
|
||||
const response = await accountApi.inviteAccount(saveObj)
|
||||
return response.data
|
||||
})
|
||||
)
|
||||
if (responses.length > 0) {
|
||||
enqueueSnackbar({
|
||||
message: 'Users invited to workspace',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'success',
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
onConfirm() // Pass the first ID or modify as needed
|
||||
} else {
|
||||
throw new Error('No data received from the server')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in saveInvite:', error)
|
||||
enqueueSnackbar({
|
||||
message: `Failed to invite users to workspace: ${error.response?.data?.message || error.message || 'Unknown error'}`,
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
persist: true,
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const validateEmail = (email) => {
|
||||
return email.match(
|
||||
/^(([^<>()[\]\\.,;:\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,}))$/
|
||||
)
|
||||
}
|
||||
|
||||
const handleChange = (event, newValue) => {
|
||||
const updatedUsers = newValue
|
||||
.filter((item) => {
|
||||
if (item.isNewUser) {
|
||||
// For new invites, validate the email
|
||||
return validateEmail(item.email)
|
||||
}
|
||||
return true // Keep all existing users
|
||||
})
|
||||
.map((item) => {
|
||||
if (item.isNewUser) {
|
||||
// This is a new invite
|
||||
return {
|
||||
email: item.email,
|
||||
isNewUser: true,
|
||||
alreadyInWorkspace: false
|
||||
}
|
||||
} else {
|
||||
const existingUser =
|
||||
userSearchResults.length > 0
|
||||
? userSearchResults.find((result) => result.user.email === item.user.email)
|
||||
: selectedUsers.find((result) => result.user.email === item.user.email)
|
||||
return {
|
||||
...existingUser,
|
||||
isNewUser: false,
|
||||
alreadyInWorkspace: selectedWorkspace
|
||||
? existingUser &&
|
||||
existingUser.workspaceNames &&
|
||||
existingUser.workspaceNames.some((ws) => ws.id === selectedWorkspace.id)
|
||||
: false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
setSelectedUsers(updatedUsers)
|
||||
|
||||
// If any invalid emails were filtered out, show a notification
|
||||
if (updatedUsers.length < newValue.length) {
|
||||
enqueueSnackbar({
|
||||
message: 'One or more invalid emails were removed.',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'warning',
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleInputChange = (event, newInputValue) => {
|
||||
setSearchString(newInputValue)
|
||||
const searchTerm = newInputValue.toLowerCase()
|
||||
const filteredUsers = allUsers.filter(
|
||||
(item) => item.user.name.toLowerCase().includes(searchTerm) || item.user.email.toLowerCase().includes(searchTerm)
|
||||
)
|
||||
setUserSearchResults(filteredUsers)
|
||||
setAllUsers((prevResults) => {
|
||||
const newResults = [...prevResults]
|
||||
filteredUsers.forEach((item) => {
|
||||
if (!newResults.some((result) => result.user.id === item.user.id)) {
|
||||
newResults.push(item)
|
||||
}
|
||||
})
|
||||
return newResults
|
||||
})
|
||||
}
|
||||
|
||||
const userSearchFilterOptions = (options, { inputValue }) => {
|
||||
const filteredOptions = options.filter((option) => option !== null && option !== undefined) ?? []
|
||||
|
||||
// First filter out already selected users
|
||||
const selectedUserEmails = selectedUsers.filter((user) => !user.isNewUser && user.user).map((user) => user.user.email)
|
||||
|
||||
const unselectedOptions = filteredOptions.filter((option) => !option.user || !selectedUserEmails.includes(option.user.email))
|
||||
|
||||
const filterByNameOrEmail = unselectedOptions.filter(
|
||||
(option) =>
|
||||
(option.user && option.user.name && option.user.name.toLowerCase().includes(inputValue.toLowerCase())) ||
|
||||
(option.user && option.user.email && option.user.email.toLowerCase().includes(inputValue.toLowerCase()))
|
||||
)
|
||||
|
||||
// Early email detection regex
|
||||
const partialEmailRegex = /^[^\s@]+@?[^\s@]*$/
|
||||
|
||||
if (filterByNameOrEmail.length === 0 && partialEmailRegex.test(inputValue)) {
|
||||
// If it looks like an email (even partially), show the invite option
|
||||
const inviteEmail = inputValue.includes('@') ? inputValue : `${inputValue}@`
|
||||
// Check if this email is already in the selected users list
|
||||
const isAlreadySelected = selectedUsers.some(
|
||||
(user) =>
|
||||
(user.isNewUser && user.email === inviteEmail) || (!user.isNewUser && user.user && user.user.email === inviteEmail)
|
||||
)
|
||||
|
||||
if (!isAlreadySelected) {
|
||||
return [{ name: `Invite ${inviteEmail}`, email: inviteEmail, isNewUser: true }]
|
||||
}
|
||||
}
|
||||
|
||||
if (filterByNameOrEmail.length === 0) {
|
||||
return [{ name: 'No results found', email: '', isNoResult: true, disabled: true }]
|
||||
}
|
||||
|
||||
return filterByNameOrEmail
|
||||
}
|
||||
|
||||
const renderUserSearchInput = (params) => (
|
||||
<TextField {...params} variant='outlined' placeholder={selectedUsers.length > 0 ? '' : 'Invite users by name or email'} />
|
||||
)
|
||||
|
||||
const renderUserSearchOptions = (props, option) => {
|
||||
// Custom logic to determine if an option is selected, since state.selected seems unreliable
|
||||
const isOptionSelected = option.isNewUser
|
||||
? selectedUsers.some((user) => user.isNewUser && user.email === option.email)
|
||||
: selectedUsers.some((user) => !user.isNewUser && user.user && user.user.email === option.user?.email)
|
||||
|
||||
return (
|
||||
<li {...props} {...(option.disabled ? { style: { pointerEvents: 'none', opacity: 0.5 } } : {})}>
|
||||
{option.isNoResult ? (
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
px: 1,
|
||||
py: 0.5
|
||||
}}
|
||||
>
|
||||
<Typography color='text.secondary'>No results found</Typography>
|
||||
</Box>
|
||||
) : option.isNewUser ? (
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
px: 1,
|
||||
py: 0.5
|
||||
}}
|
||||
>
|
||||
<Typography variant='h5' color='primary'>
|
||||
{option.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
px: 1,
|
||||
py: 0.5
|
||||
}}
|
||||
>
|
||||
<Stack flexDirection='column'>
|
||||
<Typography variant='h5'>{option.user.name}</Typography>
|
||||
<Typography>{option.user.email}</Typography>
|
||||
</Stack>
|
||||
{isOptionSelected ? <IconCircleCheck /> : null}
|
||||
</Box>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
const renderSelectedUsersTags = (tagValue, getTagProps) => {
|
||||
return selectedUsers.map((option, index) => {
|
||||
const chipProps = getTagProps({ index })
|
||||
let chipType = option.isNewUser ? 'new' : 'existing'
|
||||
if (option.alreadyInWorkspace) {
|
||||
chipType = 'already-in-workspace'
|
||||
}
|
||||
const ChipComponent = option.isNewUser ? (
|
||||
<StyledChip label={option.name || option.email} {...chipProps} chiptype={chipType} />
|
||||
) : (
|
||||
<StyledChip label={option.user.name || option.user.email} {...chipProps} chiptype={chipType} />
|
||||
)
|
||||
|
||||
const tooltipTitle = option.alreadyInWorkspace
|
||||
? `${option.user.name || option.user.email} is already a member of this workspace and won't be invited again.`
|
||||
: option.isNewUser
|
||||
? 'An invitation will be sent to this email address'
|
||||
: ''
|
||||
|
||||
return tooltipTitle ? (
|
||||
<Tooltip key={chipProps.key} title={tooltipTitle} arrow>
|
||||
{ChipComponent}
|
||||
</Tooltip>
|
||||
) : (
|
||||
ChipComponent
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const handleWorkspaceChange = (event, newWorkspace) => {
|
||||
setSelectedWorkspace(newWorkspace)
|
||||
setSelectedUsers((prevUsers) =>
|
||||
prevUsers.map((user) => ({
|
||||
...user,
|
||||
alreadyInWorkspace: newWorkspace
|
||||
? user.workspaceNames && newWorkspace && user.workspaceNames.some((ws) => ws.id === newWorkspace.id)
|
||||
: false
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
const handleRoleChange = (event, newRole) => {
|
||||
setSelectedRole(newRole)
|
||||
}
|
||||
|
||||
const getWorkspaceValue = () => {
|
||||
if (dialogProps.data) {
|
||||
return selectedWorkspace || {}
|
||||
}
|
||||
return selectedWorkspace || null
|
||||
}
|
||||
|
||||
const getRoleValue = () => {
|
||||
if (dialogProps.data && dialogProps.type === 'ADD') {
|
||||
return selectedRole || {}
|
||||
}
|
||||
return selectedRole || null
|
||||
}
|
||||
|
||||
const checkDisabled = () => {
|
||||
if (isSaving || selectedUsers.length === 0 || !selectedWorkspace || !selectedRole) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const checkWorkspaceDisabled = () => {
|
||||
if (dialogProps.data && dialogProps.type === 'ADD') {
|
||||
return Boolean(selectedWorkspace)
|
||||
} else if (dialogProps.data && dialogProps.type === 'EDIT') {
|
||||
return dialogProps.disableWorkspaceSelection
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const component = show ? (
|
||||
<Dialog
|
||||
fullWidth
|
||||
maxWidth='md'
|
||||
open={show}
|
||||
onClose={onCancel}
|
||||
aria-labelledby='alert-dialog-title'
|
||||
aria-describedby='alert-dialog-description'
|
||||
>
|
||||
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
<IconUser style={{ marginRight: '10px' }} />
|
||||
Invite Users
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Box>
|
||||
<Typography>
|
||||
Select Users<span style={{ color: 'red' }}> *</span>
|
||||
</Typography>
|
||||
<Autocomplete
|
||||
multiple
|
||||
options={allUsers}
|
||||
getOptionKey={(option) => option.userId}
|
||||
getOptionLabel={(option) => option.email || ''}
|
||||
filterOptions={userSearchFilterOptions}
|
||||
onChange={handleChange}
|
||||
inputValue={searchString}
|
||||
onInputChange={handleInputChange}
|
||||
isOptionEqualToValue={(option, value) => {
|
||||
// Compare based on user.email for existing users or email for new users
|
||||
if (option.isNewUser && value.isNewUser) {
|
||||
return option.email === value.email
|
||||
} else if (!option.isNewUser && !value.isNewUser) {
|
||||
return option.user?.email === value.user?.email
|
||||
}
|
||||
return false
|
||||
}}
|
||||
renderInput={renderUserSearchInput}
|
||||
renderOption={renderUserSearchOptions}
|
||||
renderTags={renderSelectedUsersTags}
|
||||
sx={{ mt: 1 }}
|
||||
value={selectedUsers}
|
||||
PopperComponent={StyledPopper}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 2 }}>
|
||||
<Box sx={{ gridColumn: 'span 1' }}>
|
||||
<Typography>
|
||||
Workspace<span style={{ color: 'red' }}> *</span>
|
||||
</Typography>
|
||||
<Autocomplete
|
||||
disabled={checkWorkspaceDisabled()}
|
||||
getOptionLabel={(option) => option.label || ''}
|
||||
onChange={handleWorkspaceChange}
|
||||
options={workspaces}
|
||||
renderInput={(params) => <TextField {...params} variant='outlined' placeholder='Select Workspace' />}
|
||||
sx={{ mt: 0.5 }}
|
||||
value={getWorkspaceValue()}
|
||||
PopperComponent={StyledPopper}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ gridColumn: 'span 1' }}>
|
||||
<Typography>
|
||||
Role to Assign<span style={{ color: 'red' }}> *</span>
|
||||
</Typography>
|
||||
<Autocomplete
|
||||
getOptionLabel={(option) => option.label || ''}
|
||||
onChange={handleRoleChange}
|
||||
options={availableRoles}
|
||||
renderInput={(params) => <TextField {...params} variant='outlined' placeholder='Select Role' />}
|
||||
sx={{ mt: 0.5 }}
|
||||
value={getRoleValue()}
|
||||
PopperComponent={StyledPopper}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||
<Button onClick={() => onCancel()} disabled={isSaving}>
|
||||
{dialogProps.cancelButtonName}
|
||||
</Button>
|
||||
<StyledButton
|
||||
disabled={checkDisabled()}
|
||||
variant='contained'
|
||||
onClick={saveInvite}
|
||||
startIcon={isSaving ? <CircularProgress size={20} color='inherit' /> : null}
|
||||
>
|
||||
{dialogProps.confirmButtonName}
|
||||
</StyledButton>
|
||||
</DialogActions>
|
||||
<ConfirmDialog />
|
||||
</Dialog>
|
||||
) : null
|
||||
|
||||
return createPortal(component, portalElement)
|
||||
}
|
||||
|
||||
InviteUsersDialog.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
dialogProps: PropTypes.object,
|
||||
onCancel: PropTypes.func,
|
||||
onConfirm: PropTypes.func
|
||||
}
|
||||
|
||||
export default InviteUsersDialog
|
||||
@@ -1,70 +0,0 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { Dialog, DialogActions, DialogContent, Typography, DialogTitle } from '@mui/material'
|
||||
import { StyledButton } from '@/ui-component/button/StyledButton'
|
||||
import { Input } from '@/ui-component/input/Input'
|
||||
|
||||
const LoginDialog = ({ show, dialogProps, onConfirm }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
const usernameInput = {
|
||||
label: 'Username',
|
||||
name: 'username',
|
||||
type: 'string',
|
||||
placeholder: 'john doe'
|
||||
}
|
||||
const passwordInput = {
|
||||
label: 'Password',
|
||||
name: 'password',
|
||||
type: 'password'
|
||||
}
|
||||
const [usernameVal, setUsernameVal] = useState('')
|
||||
const [passwordVal, setPasswordVal] = useState('')
|
||||
|
||||
const component = show ? (
|
||||
<Dialog
|
||||
onKeyUp={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
onConfirm(usernameVal, passwordVal)
|
||||
}
|
||||
}}
|
||||
open={show}
|
||||
fullWidth
|
||||
maxWidth='xs'
|
||||
aria-labelledby='alert-dialog-title'
|
||||
aria-describedby='alert-dialog-description'
|
||||
>
|
||||
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
|
||||
{dialogProps.title}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography>Username</Typography>
|
||||
<Input
|
||||
inputParam={usernameInput}
|
||||
onChange={(newValue) => setUsernameVal(newValue)}
|
||||
value={usernameVal}
|
||||
showDialog={false}
|
||||
/>
|
||||
<div style={{ marginTop: 20 }}></div>
|
||||
<Typography>Password</Typography>
|
||||
<Input inputParam={passwordInput} onChange={(newValue) => setPasswordVal(newValue)} value={passwordVal} />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<StyledButton variant='contained' onClick={() => onConfirm(usernameVal, passwordVal)}>
|
||||
{dialogProps.confirmButtonName}
|
||||
</StyledButton>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
) : null
|
||||
|
||||
return createPortal(component, portalElement)
|
||||
}
|
||||
|
||||
LoginDialog.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
dialogProps: PropTypes.object,
|
||||
onConfirm: PropTypes.func
|
||||
}
|
||||
|
||||
export default LoginDialog
|
||||
@@ -0,0 +1,229 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions'
|
||||
import { cloneDeep } from 'lodash'
|
||||
|
||||
// Material
|
||||
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Box, Stack, OutlinedInput, Typography } from '@mui/material'
|
||||
|
||||
// Project imports
|
||||
import { StyledButton } from '@/ui-component/button/StyledButton'
|
||||
import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog'
|
||||
import { Grid } from '@/ui-component/grid/Grid'
|
||||
|
||||
// Icons
|
||||
import { IconX, IconShare } from '@tabler/icons-react'
|
||||
|
||||
// API
|
||||
import workspaceApi from '@/api/workspace'
|
||||
import userApi from '@/api/user'
|
||||
|
||||
// Hooks
|
||||
import useApi from '@/hooks/useApi'
|
||||
|
||||
// utils
|
||||
import useNotifier from '@/utils/useNotifier'
|
||||
|
||||
// const
|
||||
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions'
|
||||
|
||||
const ShareWithWorkspaceDialog = ({ show, dialogProps, onCancel, setError }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
|
||||
const dispatch = useDispatch()
|
||||
|
||||
// ==============================|| Snackbar ||============================== //
|
||||
|
||||
useNotifier()
|
||||
const getSharedWorkspacesForItemApi = useApi(workspaceApi.getSharedWorkspacesForItem)
|
||||
const getWorkspacesByOrganizationIdUserIdApi = useApi(userApi.getWorkspacesByOrganizationIdUserId)
|
||||
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
||||
|
||||
const user = useSelector((state) => state.auth.user)
|
||||
|
||||
const [outputSchema, setOutputSchema] = useState([])
|
||||
|
||||
const [name, setName] = useState('')
|
||||
|
||||
const onRowUpdate = (newRow) => {
|
||||
setTimeout(() => {
|
||||
setOutputSchema((prevRows) => {
|
||||
let allRows = [...cloneDeep(prevRows)]
|
||||
const indexToUpdate = allRows.findIndex((row) => row.id === newRow.id)
|
||||
if (indexToUpdate >= 0) {
|
||||
allRows[indexToUpdate] = { ...newRow }
|
||||
}
|
||||
return allRows
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{ field: 'workspaceName', headerName: 'Workspace', editable: false, flex: 1 },
|
||||
{ field: 'shared', headerName: 'Share', type: 'boolean', editable: true, width: 180 }
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (getSharedWorkspacesForItemApi.data) {
|
||||
const data = getSharedWorkspacesForItemApi.data
|
||||
if (data && data.length > 0) {
|
||||
outputSchema.map((row) => {
|
||||
data.map((ws) => {
|
||||
if (row.id === ws.workspaceId) {
|
||||
row.shared = true
|
||||
}
|
||||
})
|
||||
})
|
||||
setOutputSchema([...outputSchema])
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getSharedWorkspacesForItemApi.data])
|
||||
|
||||
useEffect(() => {
|
||||
if (getSharedWorkspacesForItemApi.error && setError) {
|
||||
setError(getSharedWorkspacesForItemApi.error)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getSharedWorkspacesForItemApi.error])
|
||||
|
||||
useEffect(() => {
|
||||
if (getWorkspacesByOrganizationIdUserIdApi.data) {
|
||||
const workspaces = []
|
||||
getWorkspacesByOrganizationIdUserIdApi.data
|
||||
.filter((ws) => ws.workspace.id !== user.activeWorkspaceId)
|
||||
.map((ws) => {
|
||||
workspaces.push({
|
||||
id: ws.workspace.id,
|
||||
workspaceName: ws.workspace.name,
|
||||
shared: false
|
||||
})
|
||||
})
|
||||
setOutputSchema([...workspaces])
|
||||
}
|
||||
}, [getWorkspacesByOrganizationIdUserIdApi.data, user.activeWorkspaceId])
|
||||
|
||||
useEffect(() => {
|
||||
if (getWorkspacesByOrganizationIdUserIdApi.error && setError) {
|
||||
setError(getWorkspacesByOrganizationIdUserIdApi.error)
|
||||
}
|
||||
}, [getWorkspacesByOrganizationIdUserIdApi.error, setError])
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
getWorkspacesByOrganizationIdUserIdApi.request(user.activeOrganizationId, user.id)
|
||||
}
|
||||
setName(dialogProps.data.name)
|
||||
getSharedWorkspacesForItemApi.request(dialogProps.data.id)
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dialogProps, user])
|
||||
|
||||
useEffect(() => {
|
||||
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
|
||||
else dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||
return () => dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||
}, [show, dispatch])
|
||||
|
||||
const shareItemRequest = async () => {
|
||||
try {
|
||||
const obj = {
|
||||
itemType: dialogProps.data.itemType,
|
||||
workspaceIds: []
|
||||
}
|
||||
outputSchema.map((row) => {
|
||||
if (row.shared) {
|
||||
obj.workspaceIds.push(row.id)
|
||||
}
|
||||
})
|
||||
const sharedResp = await workspaceApi.setSharedWorkspacesForItem(dialogProps.data.id, obj)
|
||||
if (sharedResp.data) {
|
||||
enqueueSnackbar({
|
||||
message: 'Items Shared Successfully',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'success',
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
onCancel()
|
||||
}
|
||||
} catch (error) {
|
||||
if (setError) setError(error)
|
||||
enqueueSnackbar({
|
||||
message: `Failed to share Item: ${
|
||||
typeof error.response.data === 'object' ? error.response.data.message : error.response.data
|
||||
}`,
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
persist: true,
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
|
||||
const component = show ? (
|
||||
<Dialog
|
||||
fullWidth
|
||||
maxWidth='md'
|
||||
open={show}
|
||||
onClose={onCancel}
|
||||
aria-labelledby='alert-dialog-title'
|
||||
aria-describedby='alert-dialog-description'
|
||||
>
|
||||
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
<IconShare style={{ marginRight: '10px' }} />
|
||||
{dialogProps.data.title}
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Stack sx={{ position: 'relative' }} direction='row'>
|
||||
<Typography variant='overline'>Name</Typography>
|
||||
</Stack>
|
||||
<OutlinedInput id='name' type='string' disabled={true} fullWidth placeholder={name} value={name} name='name' />
|
||||
</Box>
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Grid columns={columns} rows={outputSchema} onRowUpdate={onRowUpdate} />
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => onCancel()}>{dialogProps.cancelButtonName}</Button>
|
||||
<StyledButton onClick={shareItemRequest} variant='contained'>
|
||||
{dialogProps.confirmButtonName}
|
||||
</StyledButton>
|
||||
</DialogActions>
|
||||
<ConfirmDialog />
|
||||
</Dialog>
|
||||
) : null
|
||||
|
||||
return createPortal(component, portalElement)
|
||||
}
|
||||
|
||||
ShareWithWorkspaceDialog.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
dialogProps: PropTypes.object,
|
||||
onCancel: PropTypes.func,
|
||||
onConfirm: PropTypes.func,
|
||||
setError: PropTypes.func
|
||||
}
|
||||
|
||||
export default ShareWithWorkspaceDialog
|
||||
@@ -31,18 +31,17 @@ const StyledPopper = styled(Popper)({
|
||||
const fetchList = async ({ name, nodeData, previousNodes, currentNode }) => {
|
||||
const selectedParam = nodeData.inputParams.find((param) => param.name === name)
|
||||
const loadMethod = selectedParam?.loadMethod
|
||||
const username = localStorage.getItem('username')
|
||||
const password = localStorage.getItem('password')
|
||||
|
||||
let config = {
|
||||
headers: {
|
||||
'x-request-from': 'internal',
|
||||
'Content-type': 'application/json'
|
||||
},
|
||||
withCredentials: true
|
||||
}
|
||||
|
||||
let lists = await axios
|
||||
.post(
|
||||
`${baseURL}/api/v1/node-load-method/${nodeData.name}`,
|
||||
{ ...nodeData, loadMethod, previousNodes, currentNode },
|
||||
{
|
||||
auth: username && password ? { username, password } : undefined,
|
||||
headers: { 'Content-type': 'application/json', 'x-request-from': 'internal' }
|
||||
}
|
||||
)
|
||||
.post(`${baseURL}/api/v1/node-load-method/${nodeData.name}`, { ...nodeData, loadMethod, previousNodes, currentNode }, config)
|
||||
.then(async function (response) {
|
||||
return response.data
|
||||
})
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { Box, Typography } from '@mui/material'
|
||||
import { gridSpacing } from '@/store/constant'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const SettingsSection = ({ action, children, title }) => {
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(2, 1fr)',
|
||||
border: 1,
|
||||
borderColor: theme.palette.grey[900] + 25,
|
||||
borderRadius: 2
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
gridColumn: 'span 2 / span 2',
|
||||
px: 2.5,
|
||||
py: 2,
|
||||
borderBottom: 1,
|
||||
borderColor: theme.palette.grey[900] + 25
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ m: 0 }} variant='h3'>
|
||||
{title}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
gridColumn: 'span 2 / span 2',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: gridSpacing
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
{action && (
|
||||
<Box
|
||||
sx={{
|
||||
gridColumn: 'span 2 / span 2',
|
||||
px: 2.5,
|
||||
py: 2,
|
||||
borderTop: 1,
|
||||
borderColor: theme.palette.grey[900] + 25
|
||||
}}
|
||||
>
|
||||
{action}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
SettingsSection.propTypes = {
|
||||
action: PropTypes.node,
|
||||
children: PropTypes.node,
|
||||
title: PropTypes.string
|
||||
}
|
||||
|
||||
export default SettingsSection
|
||||
@@ -32,6 +32,8 @@ export const Input = ({ inputParam, value, nodes, edges, nodeId, onChange, disab
|
||||
return 'password'
|
||||
case 'number':
|
||||
return 'number'
|
||||
case 'email':
|
||||
return 'email'
|
||||
default:
|
||||
return 'text'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
|
||||
export const Available = ({ permission, children }) => {
|
||||
const { hasPermission } = useAuth()
|
||||
if (hasPermission(permission)) {
|
||||
return children
|
||||
}
|
||||
}
|
||||
|
||||
Available.propTypes = {
|
||||
permission: PropTypes.string,
|
||||
children: PropTypes.element
|
||||
}
|
||||
@@ -0,0 +1,670 @@
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Grid,
|
||||
Typography,
|
||||
Button,
|
||||
IconButton,
|
||||
Box,
|
||||
CircularProgress,
|
||||
DialogActions
|
||||
} from '@mui/material'
|
||||
import { IconX, IconCheck, IconCreditCard, IconExternalLink, IconAlertCircle } from '@tabler/icons-react'
|
||||
import { useTheme, alpha } from '@mui/material/styles'
|
||||
import accountApi from '@/api/account.api'
|
||||
import pricingApi from '@/api/pricing'
|
||||
import workspaceApi from '@/api/workspace'
|
||||
import userApi from '@/api/user'
|
||||
import useApi from '@/hooks/useApi'
|
||||
import { useSnackbar } from 'notistack'
|
||||
import { store } from '@/store'
|
||||
import { upgradePlanSuccess } from '@/store/reducers/authSlice'
|
||||
|
||||
const PricingDialog = ({ open, onClose }) => {
|
||||
const customization = useSelector((state) => state.customization)
|
||||
const currentUser = useSelector((state) => state.auth.user)
|
||||
const theme = useTheme()
|
||||
const { enqueueSnackbar } = useSnackbar()
|
||||
|
||||
const [openPlanDialog, setOpenPlanDialog] = useState(false)
|
||||
const [selectedPlan, setSelectedPlan] = useState(null)
|
||||
const [prorationInfo, setProrationInfo] = useState(null)
|
||||
const [isUpdatingPlan, setIsUpdatingPlan] = useState(false)
|
||||
const [purchasedSeats, setPurchasedSeats] = useState(0)
|
||||
const [occupiedSeats, setOccupiedSeats] = useState(0)
|
||||
const [workspaceCount, setWorkspaceCount] = useState(0)
|
||||
const [isOpeningBillingPortal, setIsOpeningBillingPortal] = useState(false)
|
||||
|
||||
const getPricingPlansApi = useApi(pricingApi.getPricingPlans)
|
||||
const getCustomerDefaultSourceApi = useApi(userApi.getCustomerDefaultSource)
|
||||
const getPlanProrationApi = useApi(userApi.getPlanProration)
|
||||
const getAdditionalSeatsQuantityApi = useApi(userApi.getAdditionalSeatsQuantity)
|
||||
const getAllWorkspacesApi = useApi(workspaceApi.getAllWorkspacesByOrganizationId)
|
||||
|
||||
useEffect(() => {
|
||||
getPricingPlansApi.request()
|
||||
getAdditionalSeatsQuantityApi.request(currentUser?.activeOrganizationSubscriptionId)
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const handlePlanClick = async (plan) => {
|
||||
if (plan.title === 'Enterprise') {
|
||||
window.location.href = 'mailto:hello@flowiseai.com'
|
||||
return
|
||||
}
|
||||
|
||||
setSelectedPlan(plan)
|
||||
setOpenPlanDialog(true)
|
||||
getCustomerDefaultSourceApi.request(currentUser?.activeOrganizationCustomerId)
|
||||
}
|
||||
|
||||
const handleBillingPortalClick = async () => {
|
||||
setIsOpeningBillingPortal(true)
|
||||
try {
|
||||
const response = await accountApi.getBillingData()
|
||||
if (response.data?.url) {
|
||||
setOpenPlanDialog(false)
|
||||
window.open(response.data.url, '_blank')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error accessing billing portal:', error)
|
||||
}
|
||||
setIsOpeningBillingPortal(false)
|
||||
}
|
||||
|
||||
const handleUpdatePlan = async () => {
|
||||
if (!selectedPlan || !prorationInfo) return
|
||||
|
||||
setIsUpdatingPlan(true)
|
||||
try {
|
||||
const response = await userApi.updateSubscriptionPlan(
|
||||
currentUser.activeOrganizationSubscriptionId,
|
||||
selectedPlan.prodId,
|
||||
prorationInfo.prorationDate
|
||||
)
|
||||
if (response.data.status === 'success') {
|
||||
// Subscription updated successfully
|
||||
store.dispatch(upgradePlanSuccess(response.data.user))
|
||||
enqueueSnackbar('Subscription updated successfully!', { variant: 'success' })
|
||||
onClose(true)
|
||||
} else {
|
||||
const errorMessage = response.data.message || 'Subscription failed to update'
|
||||
enqueueSnackbar(errorMessage, { variant: 'error' })
|
||||
onClose()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating plan:', error)
|
||||
const errorMessage = err.response?.data?.message || 'Failed to verify subscription'
|
||||
enqueueSnackbar(errorMessage, { variant: 'error' })
|
||||
onClose()
|
||||
} finally {
|
||||
setIsUpdatingPlan(false)
|
||||
setOpenPlanDialog(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (getAllWorkspacesApi.data) {
|
||||
setWorkspaceCount(getAllWorkspacesApi.data?.length || 0)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getAllWorkspacesApi.data])
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
getCustomerDefaultSourceApi.data &&
|
||||
getCustomerDefaultSourceApi.data?.invoice_settings?.default_payment_method &&
|
||||
currentUser?.activeOrganizationSubscriptionId
|
||||
) {
|
||||
getPlanProrationApi.request(currentUser.activeOrganizationSubscriptionId, selectedPlan.prodId)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getCustomerDefaultSourceApi.data])
|
||||
|
||||
useEffect(() => {
|
||||
if (getPlanProrationApi.data) {
|
||||
setProrationInfo(getPlanProrationApi.data)
|
||||
}
|
||||
}, [getPlanProrationApi.data])
|
||||
|
||||
useEffect(() => {
|
||||
if (getAdditionalSeatsQuantityApi.data) {
|
||||
const purchased = getAdditionalSeatsQuantityApi.data?.quantity || 0
|
||||
const occupied = getAdditionalSeatsQuantityApi.data?.totalOrgUsers || 1
|
||||
|
||||
setPurchasedSeats(purchased)
|
||||
setOccupiedSeats(occupied)
|
||||
}
|
||||
}, [getAdditionalSeatsQuantityApi.data])
|
||||
|
||||
const pricingPlans = useMemo(() => {
|
||||
if (!getPricingPlansApi.data) return []
|
||||
|
||||
return getPricingPlansApi.data.map((plan) => {
|
||||
// Enterprise plan has special handling
|
||||
if (plan.title === 'Enterprise') {
|
||||
return {
|
||||
...plan,
|
||||
buttonText: 'Contact Us',
|
||||
buttonVariant: 'outlined',
|
||||
buttonAction: () => handlePlanClick(plan)
|
||||
}
|
||||
}
|
||||
|
||||
const isCurrentPlanValue = currentUser?.activeOrganizationProductId === plan.prodId
|
||||
const isStarterPlan = plan.title === 'Starter'
|
||||
|
||||
if (isCurrentPlanValue && (plan.title === 'Pro' || plan.title === 'Enterprise')) {
|
||||
getAllWorkspacesApi.request(currentUser?.activeOrganizationId)
|
||||
}
|
||||
|
||||
return {
|
||||
...plan,
|
||||
currentPlan: isCurrentPlanValue,
|
||||
isStarterPlan,
|
||||
buttonText: isCurrentPlanValue ? 'Current Plan' : 'Get Started',
|
||||
buttonVariant: plan.mostPopular ? 'contained' : 'outlined',
|
||||
disabled: isCurrentPlanValue || !currentUser.isOrganizationAdmin,
|
||||
buttonAction: () => handlePlanClick(plan)
|
||||
}
|
||||
})
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getPricingPlansApi.data, currentUser.isOrganizationAdmin])
|
||||
|
||||
const handleClose = () => {
|
||||
if (!isUpdatingPlan) {
|
||||
setProrationInfo(null)
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
const handlePlanDialogClose = () => {
|
||||
if (!isUpdatingPlan) {
|
||||
setProrationInfo(null)
|
||||
setOpenPlanDialog(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
maxWidth='lg'
|
||||
PaperProps={{
|
||||
sx: {
|
||||
borderRadius: 2,
|
||||
backgroundColor: (theme) => theme.palette.background.default,
|
||||
boxShadow: customization.isDarkMode ? '0 0 50px 0 rgba(255, 255, 255, 0.5)' : '0 0 10px 0 rgba(0, 0, 0, 0.1)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTitle
|
||||
sx={{
|
||||
mt: 2,
|
||||
p: 2,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
<Typography variant='h3'>Pricing Plans</Typography>
|
||||
<IconButton
|
||||
onClick={handleClose}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
right: 8,
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)'
|
||||
}}
|
||||
disabled={isUpdatingPlan}
|
||||
>
|
||||
<IconX />
|
||||
</IconButton>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Grid container spacing={3} sx={{ p: 2 }}>
|
||||
{pricingPlans.map((plan) => (
|
||||
<Grid item xs={12} sm={6} md={3} key={plan.title}>
|
||||
<Box
|
||||
sx={{
|
||||
p: 3,
|
||||
height: '100%',
|
||||
border: '2px solid',
|
||||
borderColor: (theme) =>
|
||||
plan.mostPopular
|
||||
? theme.palette.primary.main
|
||||
: plan.currentPlan
|
||||
? theme.palette.success.main
|
||||
: theme.palette.background.paper,
|
||||
borderRadius: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minHeight: '450px',
|
||||
position: 'relative',
|
||||
boxShadow: customization.isDarkMode
|
||||
? '0 0 10px 0 rgba(255, 255, 255, 0.5)'
|
||||
: '0 0 10px 0 rgba(0, 0, 0, 0.1)',
|
||||
backgroundColor: (theme) => (plan.currentPlan ? alpha(theme.palette.success.main, 0.05) : 'inherit')
|
||||
}}
|
||||
>
|
||||
{plan.currentPlan && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
right: 12,
|
||||
backgroundColor: 'success.dark',
|
||||
borderRadius: 1,
|
||||
px: 1,
|
||||
py: 0.5
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ color: 'white' }} variant='caption' fontWeight='bold'>
|
||||
Current Plan
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{plan.mostPopular && !plan.currentPlan && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
right: 12,
|
||||
backgroundColor: 'primary.main',
|
||||
borderRadius: 1,
|
||||
px: 1,
|
||||
py: 0.5
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ color: 'white' }} variant='caption' fontWeight='bold'>
|
||||
Most Popular
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<Typography variant='h4' gutterBottom>
|
||||
{plan.title}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant='body2'
|
||||
color='text.secondary'
|
||||
sx={{
|
||||
opacity: customization.isDarkMode ? 0.7 : 1
|
||||
}}
|
||||
gutterBottom
|
||||
>
|
||||
{plan.subtitle}
|
||||
</Typography>
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant='h3' component='span'>
|
||||
{plan.price}
|
||||
</Typography>
|
||||
{plan.period && (
|
||||
<Typography
|
||||
sx={{
|
||||
opacity: customization.isDarkMode ? 0.7 : 1
|
||||
}}
|
||||
variant='body1'
|
||||
component='span'
|
||||
color='text.secondary'
|
||||
>
|
||||
{plan.period}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
{plan.features.map((feature, index) => (
|
||||
<Box key={index} sx={{ display: 'flex', alignItems: 'start', mb: 1 }}>
|
||||
<IconCheck color={theme.palette.success.dark} size={15} style={{ marginRight: 8 }} />
|
||||
<Box>
|
||||
<Typography variant='body1'>{feature.text}</Typography>
|
||||
{feature.subtext && (
|
||||
<Typography
|
||||
sx={{
|
||||
opacity: customization.isDarkMode ? 0.7 : 1
|
||||
}}
|
||||
variant='caption'
|
||||
color='text.secondary'
|
||||
>
|
||||
{feature.subtext}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
{plan.isStarterPlan && !plan.currentPlan && (
|
||||
<Box
|
||||
sx={{
|
||||
mt: 1,
|
||||
mb: -1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
bgcolor: 'warning.light',
|
||||
color: '#FF9800',
|
||||
px: 2,
|
||||
py: 0.5,
|
||||
borderRadius: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '0.9rem',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
First Month Free
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
<Button
|
||||
fullWidth
|
||||
variant={plan.buttonVariant}
|
||||
sx={{ mt: 3 }}
|
||||
onClick={plan.buttonAction}
|
||||
disabled={plan.disabled}
|
||||
>
|
||||
{plan.currentPlan ? 'Current Plan' : plan.buttonText}
|
||||
</Button>
|
||||
</Box>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog fullWidth maxWidth='sm' open={openPlanDialog} onClose={handlePlanDialogClose}>
|
||||
<DialogTitle variant='h4'>Confirm Plan Change</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ mt: 2, display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
{purchasedSeats > 0 || occupiedSeats > 1 ? (
|
||||
<Typography
|
||||
color='error'
|
||||
sx={{
|
||||
p: 2,
|
||||
borderRadius: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1
|
||||
}}
|
||||
>
|
||||
<IconAlertCircle size={20} />
|
||||
You must remove additional seats and users before changing your plan.
|
||||
</Typography>
|
||||
) : workspaceCount > 1 ? (
|
||||
<>
|
||||
<Typography
|
||||
color='error'
|
||||
sx={{
|
||||
p: 2,
|
||||
borderRadius: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1
|
||||
}}
|
||||
>
|
||||
<IconAlertCircle size={20} />
|
||||
You must remove all workspaces except the default workspace before changing your plan.
|
||||
</Typography>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{getCustomerDefaultSourceApi.loading ? (
|
||||
<CircularProgress size={20} />
|
||||
) : getCustomerDefaultSourceApi.data?.invoice_settings?.default_payment_method ? (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, p: 2 }}>
|
||||
<Typography variant='subtitle2'>Payment Method</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{getCustomerDefaultSourceApi.data.invoice_settings.default_payment_method.card && (
|
||||
<>
|
||||
<IconCreditCard size={20} stroke={1.5} color={theme.palette.primary.main} />
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Typography sx={{ textTransform: 'capitalize' }}>
|
||||
{
|
||||
getCustomerDefaultSourceApi.data.invoice_settings.default_payment_method
|
||||
.card.brand
|
||||
}
|
||||
</Typography>
|
||||
<Typography>
|
||||
••••{' '}
|
||||
{
|
||||
getCustomerDefaultSourceApi.data.invoice_settings.default_payment_method
|
||||
.card.last4
|
||||
}
|
||||
</Typography>
|
||||
<Typography color='text.secondary'>
|
||||
(expires{' '}
|
||||
{
|
||||
getCustomerDefaultSourceApi.data.invoice_settings.default_payment_method
|
||||
.card.exp_month
|
||||
}
|
||||
/
|
||||
{
|
||||
getCustomerDefaultSourceApi.data.invoice_settings.default_payment_method
|
||||
.card.exp_year
|
||||
}
|
||||
)
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, p: 2 }}>
|
||||
<Typography color='error' sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<IconAlertCircle size={20} />
|
||||
No payment method found
|
||||
</Typography>
|
||||
<Button
|
||||
disabled={isOpeningBillingPortal}
|
||||
variant='contained'
|
||||
endIcon={!isOpeningBillingPortal && <IconExternalLink />}
|
||||
onClick={handleBillingPortalClick}
|
||||
>
|
||||
{isOpeningBillingPortal ? (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<CircularProgress size={16} color='inherit' />
|
||||
<span>Opening Billing Portal...</span>
|
||||
</Box>
|
||||
) : (
|
||||
'Add Payment Method in Billing Portal'
|
||||
)}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{getPlanProrationApi.loading && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<CircularProgress size={16} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{prorationInfo && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderRadius: 1,
|
||||
p: 2
|
||||
}}
|
||||
>
|
||||
{/* Date Range */}
|
||||
<Typography variant='body2' color='text.secondary'>
|
||||
{new Date(prorationInfo.currentPeriodStart * 1000).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})}{' '}
|
||||
-{' '}
|
||||
{new Date(prorationInfo.currentPeriodEnd * 1000).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
})}
|
||||
</Typography>
|
||||
|
||||
{/* First Month Free Notice */}
|
||||
{selectedPlan?.title === 'Starter' && prorationInfo.eligibleForFirstMonthFree && (
|
||||
<Box
|
||||
sx={{
|
||||
p: 1.5,
|
||||
bgcolor: 'warning.light',
|
||||
color: 'warning.dark',
|
||||
borderRadius: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
fontWeight: 'medium'
|
||||
}}
|
||||
>
|
||||
<Typography variant='body2' fontWeight='bold'>
|
||||
{`You're eligible for your first month free!`}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Base Plan */}
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant='body2'>{selectedPlan.title} Plan</Typography>
|
||||
<Typography variant='body2'>
|
||||
{prorationInfo.currency} {Math.max(0, prorationInfo.newPlanAmount).toFixed(2)}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{selectedPlan?.title === 'Starter' && prorationInfo.eligibleForFirstMonthFree && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant='body2'>First Month Discount</Typography>
|
||||
<Typography variant='body2' color='success.main'>
|
||||
-{prorationInfo.currency} {Math.max(0, prorationInfo.newPlanAmount).toFixed(2)}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Credit Balance */}
|
||||
{prorationInfo.prorationAmount > 0 && prorationInfo.creditBalance !== 0 && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<Typography variant='body2'>Applied account balance</Typography>
|
||||
<Typography
|
||||
variant='body2'
|
||||
color={prorationInfo.creditBalance < 0 ? 'success.main' : 'error.main'}
|
||||
>
|
||||
{prorationInfo.currency} {prorationInfo.creditBalance.toFixed(2)}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{prorationInfo.prorationAmount < 0 && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<Typography variant='body2'>Credit balance</Typography>
|
||||
<Typography
|
||||
variant='body2'
|
||||
color={prorationInfo.prorationAmount < 0 ? 'success.main' : 'error.main'}
|
||||
>
|
||||
{prorationInfo.currency} {prorationInfo.prorationAmount < 0 ? '+' : ''}
|
||||
{Math.abs(prorationInfo.prorationAmount).toFixed(2)}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Next Payment */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
pt: 1.5,
|
||||
borderTop: `1px solid ${theme.palette.divider}`
|
||||
}}
|
||||
>
|
||||
<Typography variant='h5'>Due today</Typography>
|
||||
<Typography variant='h5'>
|
||||
{prorationInfo.currency}{' '}
|
||||
{Math.max(0, prorationInfo.prorationAmount + prorationInfo.creditBalance).toFixed(2)}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{prorationInfo.prorationAmount < 0 && (
|
||||
<Typography
|
||||
variant='body2'
|
||||
sx={{
|
||||
color: 'info.main',
|
||||
fontStyle: 'italic'
|
||||
}}
|
||||
>
|
||||
Your available credit will automatically apply to your next invoice.
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</DialogContent>
|
||||
{getCustomerDefaultSourceApi.data?.invoice_settings?.default_payment_method && (
|
||||
<DialogActions>
|
||||
<Button onClick={handlePlanDialogClose} disabled={isUpdatingPlan}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant='contained'
|
||||
onClick={handleUpdatePlan}
|
||||
disabled={
|
||||
getCustomerDefaultSourceApi.loading ||
|
||||
!getCustomerDefaultSourceApi.data ||
|
||||
getPlanProrationApi.loading ||
|
||||
isUpdatingPlan ||
|
||||
!prorationInfo ||
|
||||
purchasedSeats > 0 ||
|
||||
occupiedSeats > 1 ||
|
||||
workspaceCount > 1
|
||||
}
|
||||
>
|
||||
{isUpdatingPlan ? (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<CircularProgress size={16} color='inherit' />
|
||||
<span>Updating Plan...</span>
|
||||
</Box>
|
||||
) : (
|
||||
'Confirm Change'
|
||||
)}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
)}
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
PricingDialog.propTypes = {
|
||||
open: PropTypes.bool,
|
||||
onClose: PropTypes.func
|
||||
}
|
||||
|
||||
export default PricingDialog
|
||||
@@ -0,0 +1,173 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { styled } from '@mui/material/styles'
|
||||
import {
|
||||
IconButton,
|
||||
Paper,
|
||||
Skeleton,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Tooltip,
|
||||
Typography,
|
||||
useTheme
|
||||
} from '@mui/material'
|
||||
import { tableCellClasses } from '@mui/material/TableCell'
|
||||
import { IconTrash } from '@tabler/icons-react'
|
||||
|
||||
const StyledTableCell = styled(TableCell)(({ theme }) => ({
|
||||
borderColor: theme.palette.grey[900] + 25,
|
||||
|
||||
[`&.${tableCellClasses.head}`]: {
|
||||
color: theme.palette.grey[900]
|
||||
},
|
||||
[`&.${tableCellClasses.body}`]: {
|
||||
fontSize: 14,
|
||||
height: 64
|
||||
}
|
||||
}))
|
||||
|
||||
const StyledTableRow = styled(TableRow)(() => ({
|
||||
// hide last border
|
||||
'&:last-child td, &:last-child th': {
|
||||
border: 0
|
||||
}
|
||||
}))
|
||||
|
||||
export const FilesTable = ({ data, isLoading, filterFunction, handleDelete }) => {
|
||||
const theme = useTheme()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableContainer sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }} component={Paper}>
|
||||
<Table sx={{ minWidth: 650 }} size='small' aria-label='a dense table'>
|
||||
<TableHead
|
||||
sx={{
|
||||
backgroundColor: customization.isDarkMode ? theme.palette.common.black : theme.palette.grey[100],
|
||||
height: 56
|
||||
}}
|
||||
>
|
||||
<TableRow>
|
||||
<StyledTableCell component='th' scope='row' style={{ width: '25%' }} key='0'>
|
||||
Name
|
||||
</StyledTableCell>
|
||||
<StyledTableCell style={{ width: '40%' }} key='1'>
|
||||
Path
|
||||
</StyledTableCell>
|
||||
<StyledTableCell style={{ width: '25%' }} key='2'>
|
||||
Size
|
||||
</StyledTableCell>
|
||||
<StyledTableCell style={{ width: '10%' }} key='3'>
|
||||
Actions
|
||||
</StyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<StyledTableRow>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
</StyledTableRow>
|
||||
<StyledTableRow>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
</StyledTableRow>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{data?.filter(filterFunction).map((row, index) => (
|
||||
<StyledTableRow key={index}>
|
||||
<StyledTableCell key='0'>
|
||||
<Tooltip title={row.name}>
|
||||
<Typography
|
||||
sx={{
|
||||
display: '-webkit-box',
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
WebkitLineClamp: 1,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
{row.name.split('/').pop()}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</StyledTableCell>
|
||||
<StyledTableCell key='1'>
|
||||
<Tooltip title={row.path}>
|
||||
<Typography
|
||||
sx={{
|
||||
display: '-webkit-box',
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
WebkitLineClamp: 1,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
{row.path}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</StyledTableCell>
|
||||
<StyledTableCell key='2'>
|
||||
<Typography
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
fontSize: '.9rem',
|
||||
fontWeight: 200
|
||||
}}
|
||||
>
|
||||
{`${row.size.toFixed(2)} MB`}
|
||||
</Typography>
|
||||
</StyledTableCell>
|
||||
<StyledTableCell key='3'>
|
||||
<IconButton color='error' onClick={() => handleDelete(row)} size='small'>
|
||||
<IconTrash />
|
||||
</IconButton>
|
||||
</StyledTableCell>
|
||||
</StyledTableRow>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
FilesTable.propTypes = {
|
||||
data: PropTypes.array,
|
||||
images: PropTypes.object,
|
||||
isLoading: PropTypes.bool,
|
||||
filterFunction: PropTypes.func,
|
||||
handleDelete: PropTypes.func
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
import { tableCellClasses } from '@mui/material/TableCell'
|
||||
import FlowListMenu from '../button/FlowListMenu'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
|
||||
const StyledTableCell = styled(TableCell)(({ theme }) => ({
|
||||
borderColor: theme.palette.grey[900] + 25,
|
||||
@@ -48,6 +49,10 @@ const getLocalStorageKeyName = (name, isAgentCanvas) => {
|
||||
}
|
||||
|
||||
export const FlowListTable = ({ data, images = {}, icons = {}, isLoading, filterFunction, updateFlowsApi, setError, isAgentCanvas }) => {
|
||||
const { hasPermission } = useAuth()
|
||||
const isActionsAvailable = isAgentCanvas
|
||||
? hasPermission('agentflows:update,agentflows:delete,agentflows:config,agentflows:domains,templates:flowexport,agentflows:export')
|
||||
: hasPermission('chatflows:update,chatflows:delete,chatflows:config,chatflows:domains,templates:flowexport,chatflows:export')
|
||||
const theme = useTheme()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
|
||||
@@ -118,9 +123,11 @@ export const FlowListTable = ({ data, images = {}, icons = {}, isLoading, filter
|
||||
Last Modified Date
|
||||
</TableSortLabel>
|
||||
</StyledTableCell>
|
||||
<StyledTableCell style={{ width: '10%' }} key='4'>
|
||||
Actions
|
||||
</StyledTableCell>
|
||||
{isActionsAvailable && (
|
||||
<StyledTableCell style={{ width: '10%' }} key='4'>
|
||||
Actions
|
||||
</StyledTableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -139,9 +146,11 @@ export const FlowListTable = ({ data, images = {}, icons = {}, isLoading, filter
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
{isActionsAvailable && (
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
)}
|
||||
</StyledTableRow>
|
||||
<StyledTableRow>
|
||||
<StyledTableCell>
|
||||
@@ -156,9 +165,11 @@ export const FlowListTable = ({ data, images = {}, icons = {}, isLoading, filter
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
{isActionsAvailable && (
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
)}
|
||||
</StyledTableRow>
|
||||
</>
|
||||
) : (
|
||||
@@ -278,21 +289,23 @@ export const FlowListTable = ({ data, images = {}, icons = {}, isLoading, filter
|
||||
<StyledTableCell key='3'>
|
||||
{moment(row.updatedDate).format('MMMM Do, YYYY HH:mm:ss')}
|
||||
</StyledTableCell>
|
||||
<StyledTableCell key='4'>
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
spacing={1}
|
||||
justifyContent='center'
|
||||
alignItems='center'
|
||||
>
|
||||
<FlowListMenu
|
||||
isAgentCanvas={isAgentCanvas}
|
||||
chatflow={row}
|
||||
setError={setError}
|
||||
updateFlowsApi={updateFlowsApi}
|
||||
/>
|
||||
</Stack>
|
||||
</StyledTableCell>
|
||||
{isActionsAvailable && (
|
||||
<StyledTableCell key='4'>
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
spacing={1}
|
||||
justifyContent='center'
|
||||
alignItems='center'
|
||||
>
|
||||
<FlowListMenu
|
||||
isAgentCanvas={isAgentCanvas}
|
||||
chatflow={row}
|
||||
setError={setError}
|
||||
updateFlowsApi={updateFlowsApi}
|
||||
/>
|
||||
</Stack>
|
||||
</StyledTableCell>
|
||||
)}
|
||||
</StyledTableRow>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -15,10 +15,10 @@ import {
|
||||
TableRow,
|
||||
Typography,
|
||||
Stack,
|
||||
useTheme,
|
||||
IconButton
|
||||
useTheme
|
||||
} from '@mui/material'
|
||||
import { IconTrash } from '@tabler/icons-react'
|
||||
import { IconShare, IconTrash } from '@tabler/icons-react'
|
||||
import { PermissionIconButton } from '@/ui-component/button/RBACButtons'
|
||||
|
||||
const StyledTableCell = styled(TableCell)(({ theme }) => ({
|
||||
borderColor: theme.palette.grey[900] + 25,
|
||||
@@ -49,7 +49,8 @@ export const MarketplaceTable = ({
|
||||
goToCanvas,
|
||||
goToTool,
|
||||
isLoading,
|
||||
onDelete
|
||||
onDelete,
|
||||
onShare
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
@@ -86,15 +87,8 @@ export const MarketplaceTable = ({
|
||||
<StyledTableCell sx={{ minWidth: '100px' }} key='4'>
|
||||
Use cases
|
||||
</StyledTableCell>
|
||||
<StyledTableCell key='5'>Nodes</StyledTableCell>
|
||||
<StyledTableCell component='th' scope='row' key='6'>
|
||||
|
||||
</StyledTableCell>
|
||||
{onDelete && (
|
||||
<StyledTableCell component='th' scope='row' key='7'>
|
||||
Delete
|
||||
</StyledTableCell>
|
||||
)}
|
||||
<StyledTableCell key='5'>Badges</StyledTableCell>
|
||||
<StyledTableCell component='th' scope='row' key='6'></StyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -122,11 +116,6 @@ export const MarketplaceTable = ({
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
{onDelete && (
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
)}
|
||||
</StyledTableRow>
|
||||
<StyledTableRow>
|
||||
<StyledTableCell>
|
||||
@@ -150,11 +139,6 @@ export const MarketplaceTable = ({
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
{onDelete && (
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
)}
|
||||
</StyledTableRow>
|
||||
</>
|
||||
) : (
|
||||
@@ -223,20 +207,6 @@ export const MarketplaceTable = ({
|
||||
</Stack>
|
||||
</StyledTableCell>
|
||||
<StyledTableCell key='5'>
|
||||
<Stack flexDirection='row' sx={{ gap: 1, flexWrap: 'wrap' }}>
|
||||
{row.categories &&
|
||||
row.categories.map((tag, index) => (
|
||||
<Chip
|
||||
variant='outlined'
|
||||
key={index}
|
||||
size='small'
|
||||
label={tag}
|
||||
style={{ marginRight: 3, marginBottom: 3 }}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</StyledTableCell>
|
||||
<StyledTableCell key='6'>
|
||||
<Typography>
|
||||
{row.badge &&
|
||||
row.badge
|
||||
@@ -252,13 +222,35 @@ export const MarketplaceTable = ({
|
||||
))}
|
||||
</Typography>
|
||||
</StyledTableCell>
|
||||
{onDelete && (
|
||||
<StyledTableCell key='7'>
|
||||
<IconButton title='Delete' color='error' onClick={() => onDelete(row)}>
|
||||
<IconTrash />
|
||||
</IconButton>
|
||||
</StyledTableCell>
|
||||
)}
|
||||
<StyledTableCell key='6' colSpan={row.shared ? 2 : undefined}>
|
||||
{row.shared ? (
|
||||
<Typography>Shared Template</Typography>
|
||||
) : (
|
||||
<>
|
||||
{onShare && (
|
||||
<PermissionIconButton
|
||||
display={'feat:workspaces'}
|
||||
permissionId={'templates:custom-share'}
|
||||
title='Share'
|
||||
color='primary'
|
||||
onClick={() => onShare(row)}
|
||||
>
|
||||
<IconShare />
|
||||
</PermissionIconButton>
|
||||
)}
|
||||
{onDelete && (
|
||||
<PermissionIconButton
|
||||
permissionId={'templates:custom-delete'}
|
||||
title='Delete'
|
||||
color='error'
|
||||
onClick={() => onDelete(row)}
|
||||
>
|
||||
<IconTrash />
|
||||
</PermissionIconButton>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</StyledTableCell>
|
||||
</StyledTableRow>
|
||||
))}
|
||||
</>
|
||||
@@ -280,5 +272,6 @@ MarketplaceTable.propTypes = {
|
||||
goToTool: PropTypes.func,
|
||||
goToCanvas: PropTypes.func,
|
||||
isLoading: PropTypes.bool,
|
||||
onDelete: PropTypes.func
|
||||
onDelete: PropTypes.func,
|
||||
onShare: PropTypes.func
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { styled } from '@mui/material/styles'
|
||||
import { TableCell, TableRow } from '@mui/material'
|
||||
import { tableCellClasses } from '@mui/material/TableCell'
|
||||
|
||||
export const StyledTableCell = styled(TableCell)(({ theme }) => ({
|
||||
borderColor: theme.palette.grey[900] + 25,
|
||||
|
||||
[`&.${tableCellClasses.head}`]: {
|
||||
color: theme.palette.grey[900]
|
||||
},
|
||||
[`&.${tableCellClasses.body}`]: {
|
||||
fontSize: 14,
|
||||
height: 64
|
||||
}
|
||||
}))
|
||||
|
||||
export const StyledTableRow = styled(TableRow)(() => ({
|
||||
// hide last border
|
||||
'&:last-child td, &:last-child th': {
|
||||
border: 0
|
||||
}
|
||||
}))
|
||||
@@ -0,0 +1,81 @@
|
||||
const getCurrentUser = () => {
|
||||
if (!localStorage.getItem('user') || localStorage.getItem('user') === 'undefined') return undefined
|
||||
return JSON.parse(localStorage.getItem('user'))
|
||||
}
|
||||
|
||||
const updateCurrentUser = (user) => {
|
||||
let stringifiedUser = user
|
||||
if (typeof user === 'object') {
|
||||
stringifiedUser = JSON.stringify(user)
|
||||
}
|
||||
localStorage.setItem('user', stringifiedUser)
|
||||
}
|
||||
|
||||
const removeCurrentUser = () => {
|
||||
_removeFromStorage()
|
||||
clearAllCookies()
|
||||
}
|
||||
|
||||
const _removeFromStorage = () => {
|
||||
localStorage.removeItem('isAuthenticated')
|
||||
localStorage.removeItem('isGlobal')
|
||||
localStorage.removeItem('user')
|
||||
localStorage.removeItem('permissions')
|
||||
localStorage.removeItem('features')
|
||||
localStorage.removeItem('isSSO')
|
||||
}
|
||||
|
||||
const clearAllCookies = () => {
|
||||
document.cookie.split(';').forEach((cookie) => {
|
||||
const name = cookie.split('=')[0].trim()
|
||||
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`
|
||||
})
|
||||
}
|
||||
|
||||
const extractUser = (payload) => {
|
||||
const user = {
|
||||
id: payload.id,
|
||||
email: payload.email,
|
||||
name: payload.name,
|
||||
status: payload.status,
|
||||
role: payload.role,
|
||||
isSSO: payload.isSSO,
|
||||
activeOrganizationId: payload.activeOrganizationId,
|
||||
activeOrganizationSubscriptionId: payload.activeOrganizationSubscriptionId,
|
||||
activeOrganizationCustomerId: payload.activeOrganizationCustomerId,
|
||||
activeOrganizationProductId: payload.activeOrganizationProductId,
|
||||
activeWorkspaceId: payload.activeWorkspaceId,
|
||||
activeWorkspace: payload.activeWorkspace,
|
||||
lastLogin: payload.lastLogin,
|
||||
isOrganizationAdmin: payload.isOrganizationAdmin,
|
||||
assignedWorkspaces: payload.assignedWorkspaces,
|
||||
permissions: payload.permissions
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
const updateStateAndLocalStorage = (state, payload) => {
|
||||
const user = extractUser(payload)
|
||||
state.user = user
|
||||
state.token = payload.token
|
||||
state.permissions = payload.permissions
|
||||
state.features = payload.features
|
||||
state.isAuthenticated = true
|
||||
state.isGlobal = user.isOrganizationAdmin
|
||||
localStorage.setItem('isAuthenticated', 'true')
|
||||
localStorage.setItem('isGlobal', state.isGlobal)
|
||||
localStorage.setItem('isSSO', state.user.isSSO)
|
||||
localStorage.setItem('user', JSON.stringify(user))
|
||||
localStorage.setItem('permissions', JSON.stringify(payload.permissions))
|
||||
localStorage.setItem('features', JSON.stringify(payload.features))
|
||||
}
|
||||
|
||||
const AuthUtils = {
|
||||
getCurrentUser,
|
||||
updateCurrentUser,
|
||||
removeCurrentUser,
|
||||
updateStateAndLocalStorage,
|
||||
extractUser
|
||||
}
|
||||
|
||||
export default AuthUtils
|
||||
@@ -66,6 +66,37 @@ const sanitizeAssistant = (Assistant) => {
|
||||
}
|
||||
}
|
||||
|
||||
const sanitizeCustomTemplate = (CustomTemplate) => {
|
||||
try {
|
||||
return CustomTemplate.map((customTemplate) => {
|
||||
return { ...customTemplate, usecases: JSON.stringify(customTemplate.usecases), workspaceId: undefined }
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`exportImport.sanitizeCustomTemplate ${getErrorMessage(error)}`)
|
||||
}
|
||||
}
|
||||
|
||||
const sanitizeDocumentStore = (DocumentStore) => {
|
||||
try {
|
||||
return DocumentStore.map((documentStore) => {
|
||||
return { ...documentStore, workspaceId: undefined }
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`exportImport.sanitizeDocumentStore ${getErrorMessage(error)}`)
|
||||
}
|
||||
}
|
||||
|
||||
const sanitizeExecution = (Execution) => {
|
||||
try {
|
||||
return Execution.map((execution) => {
|
||||
execution.agentflow.workspaceId = undefined
|
||||
return { ...execution, workspaceId: undefined }
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`exportImport.sanitizeExecution ${getErrorMessage(error)}`)
|
||||
}
|
||||
}
|
||||
|
||||
export const stringify = (object) => {
|
||||
try {
|
||||
return JSON.stringify(object, null, 2)
|
||||
@@ -86,10 +117,10 @@ export const exportData = (exportAllData) => {
|
||||
ChatFlow: sanitizeChatflow(exportAllData.ChatFlow),
|
||||
ChatMessage: exportAllData.ChatMessage,
|
||||
ChatMessageFeedback: exportAllData.ChatMessageFeedback,
|
||||
CustomTemplate: exportAllData.CustomTemplate,
|
||||
DocumentStore: exportAllData.DocumentStore,
|
||||
CustomTemplate: sanitizeCustomTemplate(exportAllData.CustomTemplate),
|
||||
DocumentStore: sanitizeDocumentStore(exportAllData.DocumentStore),
|
||||
DocumentStoreFileChunk: exportAllData.DocumentStoreFileChunk,
|
||||
Execution: exportAllData.Execution,
|
||||
Execution: sanitizeExecution(exportAllData.Execution),
|
||||
Tool: sanitizeTool(exportAllData.Tool),
|
||||
Variable: sanitizeVariable(exportAllData.Variable)
|
||||
}
|
||||
|
||||
@@ -982,6 +982,18 @@ export const kFormatter = (num) => {
|
||||
return item ? (num / item.value).toFixed(1).replace(regexp, '').concat(item.symbol) : '0'
|
||||
}
|
||||
|
||||
export const redirectWhenUnauthorized = ({ error, redirectTo }) => {
|
||||
if (error === 'unauthorized') {
|
||||
window.location.href = redirectTo
|
||||
} else if (error === 'subscription_canceled') {
|
||||
window.location.href = `${redirectTo}?error=${error}`
|
||||
}
|
||||
}
|
||||
|
||||
export const truncateString = (str, maxLength) => {
|
||||
return str.length > maxLength ? `${str.slice(0, maxLength - 3)}...` : str
|
||||
}
|
||||
|
||||
const toCamelCase = (str) => {
|
||||
return str
|
||||
.split(' ') // Split by space to process each word
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const passwordSchema = z
|
||||
.string()
|
||||
.min(8, 'Password must be at least 8 characters')
|
||||
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
|
||||
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
|
||||
.regex(/\d/, 'Password must contain at least one digit')
|
||||
.regex(/[@$!%*?&-]/, 'Password must contain at least one special character (@$!%*?&-)')
|
||||
|
||||
export const validatePassword = (password) => {
|
||||
const result = passwordSchema.safeParse(password)
|
||||
if (!result.success) {
|
||||
return result.error.errors.map((err) => err.message)
|
||||
}
|
||||
return []
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
// material-ui
|
||||
import { Box, Button, OutlinedInput, Stack, Typography } from '@mui/material'
|
||||
|
||||
// project imports
|
||||
import ErrorBoundary from '@/ErrorBoundary'
|
||||
import ViewHeader from '@/layout/MainLayout/ViewHeader'
|
||||
import { StyledButton } from '@/ui-component/button/StyledButton'
|
||||
import MainCard from '@/ui-component/cards/MainCard'
|
||||
import SettingsSection from '@/ui-component/form/settings'
|
||||
import { BackdropLoader } from '@/ui-component/loading/BackdropLoader'
|
||||
|
||||
// API
|
||||
import userApi from '@/api/user'
|
||||
import useApi from '@/hooks/useApi'
|
||||
|
||||
// Store
|
||||
import { store } from '@/store'
|
||||
import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '@/store/actions'
|
||||
import { gridSpacing } from '@/store/constant'
|
||||
import { useError } from '@/store/context/ErrorContext'
|
||||
import { userProfileUpdated } from '@/store/reducers/authSlice'
|
||||
|
||||
// utils
|
||||
import useNotifier from '@/utils/useNotifier'
|
||||
import { validatePassword } from '@/utils/validation'
|
||||
|
||||
// Icons
|
||||
import { IconAlertTriangle, IconX } from '@tabler/icons-react'
|
||||
|
||||
const UserProfile = () => {
|
||||
useNotifier()
|
||||
const { error, setError } = useError()
|
||||
|
||||
const dispatch = useDispatch()
|
||||
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
||||
|
||||
const currentUser = useSelector((state) => state.auth.user)
|
||||
const isAuthenticated = useSelector((state) => state.auth.isAuthenticated)
|
||||
|
||||
const [newPasswordVal, setNewPasswordVal] = useState('')
|
||||
const [confirmPasswordVal, setConfirmPasswordVal] = useState('')
|
||||
const [usernameVal, setUsernameVal] = useState('')
|
||||
const [emailVal, setEmailVal] = useState('')
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [authErrors, setAuthErrors] = useState([])
|
||||
|
||||
const getUserApi = useApi(userApi.getUserById)
|
||||
|
||||
const validateAndSubmit = async () => {
|
||||
const validationErrors = []
|
||||
setAuthErrors([])
|
||||
if (!isAuthenticated) {
|
||||
validationErrors.push('User is not authenticated')
|
||||
}
|
||||
if (currentUser.isSSO) {
|
||||
validationErrors.push('User is a SSO user, unable to update details')
|
||||
}
|
||||
if (!usernameVal) {
|
||||
validationErrors.push('Name cannot be left blank!')
|
||||
}
|
||||
if (!emailVal) {
|
||||
validationErrors.push('Email cannot be left blank!')
|
||||
}
|
||||
if (newPasswordVal || confirmPasswordVal) {
|
||||
if (newPasswordVal !== confirmPasswordVal) {
|
||||
validationErrors.push('New Password and Confirm Password do not match')
|
||||
}
|
||||
const passwordErrors = validatePassword(newPasswordVal)
|
||||
if (passwordErrors.length > 0) {
|
||||
validationErrors.push(...passwordErrors)
|
||||
}
|
||||
}
|
||||
if (validationErrors.length > 0) {
|
||||
setAuthErrors(validationErrors)
|
||||
return
|
||||
}
|
||||
const body = {
|
||||
id: currentUser.id,
|
||||
email: emailVal,
|
||||
name: usernameVal
|
||||
}
|
||||
if (newPasswordVal) body.password = newPasswordVal
|
||||
setLoading(true)
|
||||
try {
|
||||
const updateResponse = await userApi.updateUser(body)
|
||||
setAuthErrors([])
|
||||
setLoading(false)
|
||||
if (updateResponse.data) {
|
||||
store.dispatch(userProfileUpdated(updateResponse.data))
|
||||
enqueueSnackbar({
|
||||
message: 'User Details Updated!',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'success',
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
setLoading(false)
|
||||
setAuthErrors([typeof error.response.data === 'object' ? error.response.data.message : error.response.data])
|
||||
enqueueSnackbar({
|
||||
message: `Failed to update user details`,
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
persist: true,
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (getUserApi.data) {
|
||||
const user = getUserApi.data
|
||||
setEmailVal(user.email)
|
||||
setUsernameVal(user.name)
|
||||
setLoading(false)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getUserApi.data])
|
||||
|
||||
useEffect(() => {
|
||||
if (getUserApi.error) {
|
||||
setLoading(false)
|
||||
setError(getUserApi.error)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getUserApi.error])
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
getUserApi.request(currentUser.id)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<MainCard>
|
||||
{error ? (
|
||||
<ErrorBoundary error={error} />
|
||||
) : (
|
||||
<Stack flexDirection='column' sx={{ gap: 3 }}>
|
||||
<ViewHeader search={false} title='Settings' />
|
||||
{authErrors && authErrors.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderRadius: 10,
|
||||
background: 'rgb(254,252,191)',
|
||||
padding: 10,
|
||||
paddingTop: 15,
|
||||
marginTop: 10,
|
||||
marginBottom: 10
|
||||
}}
|
||||
>
|
||||
<Box sx={{ p: 2 }}>
|
||||
<IconAlertTriangle size={25} color='orange' />
|
||||
</Box>
|
||||
<Stack flexDirection='column'>
|
||||
<span style={{ color: 'rgb(116,66,16)' }}>
|
||||
<ul>
|
||||
{authErrors.map((msg, key) => (
|
||||
<strong key={key}>
|
||||
<li>{msg}</li>
|
||||
</strong>
|
||||
))}
|
||||
</ul>
|
||||
</span>
|
||||
</Stack>
|
||||
</div>
|
||||
)}
|
||||
<SettingsSection
|
||||
action={
|
||||
<StyledButton variant='contained' style={{ borderRadius: 2, height: 40 }} onClick={validateAndSubmit}>
|
||||
Save
|
||||
</StyledButton>
|
||||
}
|
||||
title='Profile'
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: gridSpacing,
|
||||
px: 2.5,
|
||||
py: 2
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<Typography>Email</Typography>
|
||||
<div style={{ flexGrow: 1 }}></div>
|
||||
</div>
|
||||
<OutlinedInput
|
||||
id='email'
|
||||
type='string'
|
||||
fullWidth
|
||||
size='small'
|
||||
placeholder='Your login Id'
|
||||
name='name'
|
||||
onChange={(e) => setEmailVal(e.target.value)}
|
||||
value={emailVal}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<Typography>
|
||||
Full Name<span style={{ color: 'red' }}> *</span>
|
||||
</Typography>
|
||||
<div style={{ flexGrow: 1 }}></div>
|
||||
</div>
|
||||
<OutlinedInput
|
||||
id='name'
|
||||
type='string'
|
||||
fullWidth
|
||||
size='small'
|
||||
placeholder='Your Name'
|
||||
name='name'
|
||||
onChange={(e) => setUsernameVal(e.target.value)}
|
||||
value={usernameVal}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<Typography>
|
||||
New Password<span style={{ color: 'red' }}> *</span>
|
||||
</Typography>
|
||||
<div style={{ flexGrow: 1 }}></div>
|
||||
</div>
|
||||
<OutlinedInput
|
||||
id='np'
|
||||
type='password'
|
||||
fullWidth
|
||||
size='small'
|
||||
name='new_password'
|
||||
onChange={(e) => setNewPasswordVal(e.target.value)}
|
||||
value={newPasswordVal}
|
||||
/>
|
||||
<Typography variant='caption'>
|
||||
<i>
|
||||
Password must be at least 8 characters long and contain at least one lowercase letter, one
|
||||
uppercase letter, one digit, and one special character (@$!%*?&-).
|
||||
</i>
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<Typography>
|
||||
Confirm Password<span style={{ color: 'red' }}> *</span>
|
||||
</Typography>
|
||||
<div style={{ flexGrow: 1 }}></div>
|
||||
</div>
|
||||
<OutlinedInput
|
||||
id='npc'
|
||||
type='password'
|
||||
fullWidth
|
||||
size='small'
|
||||
name='new_cnf_password'
|
||||
onChange={(e) => setConfirmPasswordVal(e.target.value)}
|
||||
value={confirmPasswordVal}
|
||||
/>
|
||||
<Typography variant='caption'>
|
||||
<i>Retype your new password. Must match the password typed above.</i>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</SettingsSection>
|
||||
</Stack>
|
||||
)}
|
||||
</MainCard>
|
||||
{loading && <BackdropLoader open={loading} />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserProfile
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
import MainCard from '@/ui-component/cards/MainCard'
|
||||
import ErrorBoundary from '@/ErrorBoundary'
|
||||
import ViewHeader from '@/layout/MainLayout/ViewHeader'
|
||||
import { Available } from '@/ui-component/rbac/available'
|
||||
|
||||
// API
|
||||
import useApi from '@/hooks/useApi'
|
||||
@@ -336,20 +337,22 @@ const AgentExecutions = () => {
|
||||
<Button variant='outlined' onClick={resetFilters} size='small'>
|
||||
Reset
|
||||
</Button>
|
||||
<Tooltip title='Delete selected executions'>
|
||||
<span>
|
||||
<IconButton
|
||||
sx={{ height: 30, width: 30 }}
|
||||
size='small'
|
||||
color='error'
|
||||
onClick={handleDeleteDialogOpen}
|
||||
edge='end'
|
||||
disabled={selectedExecutionIds.length === 0}
|
||||
>
|
||||
<IconTrash />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Available permissions={['executions:delete']}>
|
||||
<Tooltip title='Delete selected executions'>
|
||||
<span>
|
||||
<IconButton
|
||||
sx={{ height: 30, width: 30 }}
|
||||
size='small'
|
||||
color='error'
|
||||
onClick={handleDeleteDialogOpen}
|
||||
edge='end'
|
||||
disabled={selectedExecutionIds.length === 0}
|
||||
>
|
||||
<IconTrash />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Available>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -10,12 +10,11 @@ import MainCard from '@/ui-component/cards/MainCard'
|
||||
import ItemCard from '@/ui-component/cards/ItemCard'
|
||||
import { gridSpacing } from '@/store/constant'
|
||||
import AgentsEmptySVG from '@/assets/images/agents_empty.svg'
|
||||
import LoginDialog from '@/ui-component/dialog/LoginDialog'
|
||||
import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog'
|
||||
import { FlowListTable } from '@/ui-component/table/FlowListTable'
|
||||
import { StyledButton } from '@/ui-component/button/StyledButton'
|
||||
import ViewHeader from '@/layout/MainLayout/ViewHeader'
|
||||
import ErrorBoundary from '@/ErrorBoundary'
|
||||
import { StyledPermissionButton } from '@/ui-component/button/RBACButtons'
|
||||
|
||||
// API
|
||||
import chatflowsApi from '@/api/chatflows'
|
||||
@@ -25,6 +24,7 @@ import useApi from '@/hooks/useApi'
|
||||
|
||||
// const
|
||||
import { baseURL, AGENTFLOW_ICONS } from '@/store/constant'
|
||||
import { useError } from '@/store/context/ErrorContext'
|
||||
|
||||
// icons
|
||||
import { IconPlus, IconLayoutGrid, IconList } from '@tabler/icons-react'
|
||||
@@ -36,12 +36,10 @@ const Agentflows = () => {
|
||||
const theme = useTheme()
|
||||
|
||||
const [isLoading, setLoading] = useState(true)
|
||||
const [error, setError] = useState(null)
|
||||
const [images, setImages] = useState({})
|
||||
const [icons, setIcons] = useState({})
|
||||
const [search, setSearch] = useState('')
|
||||
const [loginDialogOpen, setLoginDialogOpen] = useState(false)
|
||||
const [loginDialogProps, setLoginDialogProps] = useState({})
|
||||
const { error, setError } = useError()
|
||||
|
||||
const getAllAgentflows = useApi(chatflowsApi.getAllAgentflows)
|
||||
const [view, setView] = useState(localStorage.getItem('flowDisplayStyle') || 'card')
|
||||
@@ -72,12 +70,6 @@ const Agentflows = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const onLoginClick = (username, password) => {
|
||||
localStorage.setItem('username', username)
|
||||
localStorage.setItem('password', password)
|
||||
navigate(0)
|
||||
}
|
||||
|
||||
const addNew = () => {
|
||||
if (agentflowVersion === 'v2') {
|
||||
navigate('/v2/agentcanvas')
|
||||
@@ -102,16 +94,10 @@ const Agentflows = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (getAllAgentflows.error) {
|
||||
if (getAllAgentflows.error?.response?.status === 401) {
|
||||
setLoginDialogProps({
|
||||
title: 'Login',
|
||||
confirmButtonName: 'Login'
|
||||
})
|
||||
setLoginDialogOpen(true)
|
||||
} else {
|
||||
setError(getAllAgentflows.error)
|
||||
}
|
||||
setError(getAllAgentflows.error)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getAllAgentflows.error])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -228,9 +214,15 @@ const Agentflows = () => {
|
||||
<IconList />
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
<StyledButton variant='contained' onClick={addNew} startIcon={<IconPlus />} sx={{ borderRadius: 2, height: 40 }}>
|
||||
<StyledPermissionButton
|
||||
permissionId={'agentflows:create'}
|
||||
variant='contained'
|
||||
onClick={addNew}
|
||||
startIcon={<IconPlus />}
|
||||
sx={{ borderRadius: 2, height: 40 }}
|
||||
>
|
||||
Add New
|
||||
</StyledButton>
|
||||
</StyledPermissionButton>
|
||||
</ViewHeader>
|
||||
{!view || view === 'card' ? (
|
||||
<>
|
||||
@@ -280,8 +272,6 @@ const Agentflows = () => {
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<LoginDialog show={loginDialogOpen} dialogProps={loginDialogProps} onConfirm={onLoginClick} />
|
||||
<ConfirmDialog />
|
||||
</MainCard>
|
||||
)
|
||||
|
||||
@@ -27,14 +27,16 @@ import { useTheme, styled } from '@mui/material/styles'
|
||||
|
||||
// project imports
|
||||
import MainCard from '@/ui-component/cards/MainCard'
|
||||
import { StyledButton } from '@/ui-component/button/StyledButton'
|
||||
import APIKeyDialog from './APIKeyDialog'
|
||||
import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog'
|
||||
import ViewHeader from '@/layout/MainLayout/ViewHeader'
|
||||
import ErrorBoundary from '@/ErrorBoundary'
|
||||
import { PermissionButton, StyledPermissionButton } from '@/ui-component/button/RBACButtons'
|
||||
import { Available } from '@/ui-component/rbac/available'
|
||||
|
||||
// API
|
||||
import apiKeyApi from '@/api/apikey'
|
||||
import { useError } from '@/store/context/ErrorContext'
|
||||
|
||||
// Hooks
|
||||
import useApi from '@/hooks/useApi'
|
||||
@@ -130,16 +132,20 @@ function APIKeyRow(props) {
|
||||
)}
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>{moment(props.apiKey.createdAt).format('MMMM Do, YYYY')}</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<IconButton title='Edit' color='primary' onClick={props.onEditClick}>
|
||||
<IconEdit />
|
||||
</IconButton>
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<IconButton title='Delete' color='error' onClick={props.onDeleteClick}>
|
||||
<IconTrash />
|
||||
</IconButton>
|
||||
</StyledTableCell>
|
||||
<Available permission={'apikeys:update,apikeys:create'}>
|
||||
<StyledTableCell>
|
||||
<IconButton title='Edit' color='primary' onClick={props.onEditClick}>
|
||||
<IconEdit />
|
||||
</IconButton>
|
||||
</StyledTableCell>
|
||||
</Available>
|
||||
<Available permission={'apikeys:delete'}>
|
||||
<StyledTableCell>
|
||||
<IconButton title='Delete' color='error' onClick={props.onDeleteClick}>
|
||||
<IconTrash />
|
||||
</IconButton>
|
||||
</StyledTableCell>
|
||||
</Available>
|
||||
</TableRow>
|
||||
{open && (
|
||||
<TableRow sx={{ '& td': { border: 0 } }}>
|
||||
@@ -199,12 +205,12 @@ const APIKey = () => {
|
||||
|
||||
const dispatch = useDispatch()
|
||||
useNotifier()
|
||||
const { error, setError } = useError()
|
||||
|
||||
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
||||
|
||||
const [isLoading, setLoading] = useState(true)
|
||||
const [error, setError] = useState(null)
|
||||
const [showDialog, setShowDialog] = useState(false)
|
||||
const [dialogProps, setDialogProps] = useState({})
|
||||
const [apiKeys, setAPIKeys] = useState([])
|
||||
@@ -354,12 +360,6 @@ const APIKey = () => {
|
||||
}
|
||||
}, [getAllAPIKeysApi.data])
|
||||
|
||||
useEffect(() => {
|
||||
if (getAllAPIKeysApi.error) {
|
||||
setError(getAllAPIKeysApi.error)
|
||||
}
|
||||
}, [getAllAPIKeysApi.error])
|
||||
|
||||
return (
|
||||
<>
|
||||
<MainCard>
|
||||
@@ -374,7 +374,8 @@ const APIKey = () => {
|
||||
title='API Keys'
|
||||
description='Flowise API & SDK authentication keys'
|
||||
>
|
||||
<Button
|
||||
<PermissionButton
|
||||
permissionId={'apikeys:import'}
|
||||
variant='outlined'
|
||||
sx={{ borderRadius: 2, height: '100%' }}
|
||||
onClick={uploadDialog}
|
||||
@@ -382,8 +383,9 @@ const APIKey = () => {
|
||||
id='btn_importApiKeys'
|
||||
>
|
||||
Import
|
||||
</Button>
|
||||
<StyledButton
|
||||
</PermissionButton>
|
||||
<StyledPermissionButton
|
||||
permissionId={'apikeys:create'}
|
||||
variant='contained'
|
||||
sx={{ borderRadius: 2, height: '100%' }}
|
||||
onClick={addNew}
|
||||
@@ -391,7 +393,7 @@ const APIKey = () => {
|
||||
id='btn_createApiKey'
|
||||
>
|
||||
Create Key
|
||||
</StyledButton>
|
||||
</StyledPermissionButton>
|
||||
</ViewHeader>
|
||||
{!isLoading && apiKeys.length <= 0 ? (
|
||||
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
|
||||
@@ -423,8 +425,12 @@ const APIKey = () => {
|
||||
<StyledTableCell>API Key</StyledTableCell>
|
||||
<StyledTableCell>Usage</StyledTableCell>
|
||||
<StyledTableCell>Created</StyledTableCell>
|
||||
<StyledTableCell> </StyledTableCell>
|
||||
<StyledTableCell> </StyledTableCell>
|
||||
<Available permission={'apikeys:update,apikeys:create'}>
|
||||
<StyledTableCell> </StyledTableCell>
|
||||
</Available>
|
||||
<Available permission={'apikeys:delete'}>
|
||||
<StyledTableCell> </StyledTableCell>
|
||||
</Available>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -443,12 +449,12 @@ const APIKey = () => {
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<Available permission={'apikeys:update,apikeys:create'}>
|
||||
<StyledTableCell> </StyledTableCell>
|
||||
</Available>
|
||||
<Available permission={'apikeys:delete'}>
|
||||
<StyledTableCell> </StyledTableCell>
|
||||
</Available>
|
||||
</StyledTableRow>
|
||||
<StyledTableRow>
|
||||
<StyledTableCell>
|
||||
@@ -463,12 +469,12 @@ const APIKey = () => {
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<Available permission={'apikeys:update,apikeys:create'}>
|
||||
<StyledTableCell> </StyledTableCell>
|
||||
</Available>
|
||||
<Available permission={'apikeys:delete'}>
|
||||
<StyledTableCell> </StyledTableCell>
|
||||
</Available>
|
||||
</StyledTableRow>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -39,6 +39,7 @@ import ViewLeadsDialog from '@/ui-component/dialog/ViewLeadsDialog'
|
||||
import Settings from '@/views/settings'
|
||||
import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog'
|
||||
import PromptGeneratorDialog from '@/ui-component/dialog/PromptGeneratorDialog'
|
||||
import { Available } from '@/ui-component/rbac/available'
|
||||
import ExpandTextDialog from '@/ui-component/dialog/ExpandTextDialog'
|
||||
|
||||
// API
|
||||
@@ -866,26 +867,28 @@ const CustomAssistantConfigurePreview = () => {
|
||||
</Avatar>
|
||||
</ButtonBase>
|
||||
)}
|
||||
<ButtonBase title={`Save`} sx={{ borderRadius: '50%', mr: 2 }}>
|
||||
<Avatar
|
||||
variant='rounded'
|
||||
sx={{
|
||||
...theme.typography.commonAvatar,
|
||||
...theme.typography.mediumAvatar,
|
||||
transition: 'all .2s ease-in-out',
|
||||
background: theme.palette.canvasHeader.saveLight,
|
||||
color: theme.palette.canvasHeader.saveDark,
|
||||
'&:hover': {
|
||||
background: theme.palette.canvasHeader.saveDark,
|
||||
color: theme.palette.canvasHeader.saveLight
|
||||
}
|
||||
}}
|
||||
color='inherit'
|
||||
onClick={onSaveAndProcess}
|
||||
>
|
||||
<IconDeviceFloppy stroke={1.5} size='1.3rem' />
|
||||
</Avatar>
|
||||
</ButtonBase>
|
||||
<Available permission={'assistants:create'}>
|
||||
<ButtonBase title={`Save`} sx={{ borderRadius: '50%', mr: 2 }}>
|
||||
<Avatar
|
||||
variant='rounded'
|
||||
sx={{
|
||||
...theme.typography.commonAvatar,
|
||||
...theme.typography.mediumAvatar,
|
||||
transition: 'all .2s ease-in-out',
|
||||
background: theme.palette.canvasHeader.saveLight,
|
||||
color: theme.palette.canvasHeader.saveDark,
|
||||
'&:hover': {
|
||||
background: theme.palette.canvasHeader.saveDark,
|
||||
color: theme.palette.canvasHeader.saveLight
|
||||
}
|
||||
}}
|
||||
color='inherit'
|
||||
onClick={onSaveAndProcess}
|
||||
>
|
||||
<IconDeviceFloppy stroke={1.5} size='1.3rem' />
|
||||
</Avatar>
|
||||
</ButtonBase>
|
||||
</Available>
|
||||
{customAssistantFlowId && !loadingAssistant && (
|
||||
<ButtonBase ref={settingsRef} title='Settings' sx={{ borderRadius: '50%' }}>
|
||||
<Avatar
|
||||
@@ -908,25 +911,27 @@ const CustomAssistantConfigurePreview = () => {
|
||||
</ButtonBase>
|
||||
)}
|
||||
{!customAssistantFlowId && !loadingAssistant && (
|
||||
<ButtonBase ref={settingsRef} title='Delete Assistant' sx={{ borderRadius: '50%' }}>
|
||||
<Avatar
|
||||
variant='rounded'
|
||||
sx={{
|
||||
...theme.typography.commonAvatar,
|
||||
...theme.typography.mediumAvatar,
|
||||
transition: 'all .2s ease-in-out',
|
||||
background: theme.palette.error.light,
|
||||
color: theme.palette.error.dark,
|
||||
'&:hover': {
|
||||
background: theme.palette.error.dark,
|
||||
color: theme.palette.error.light
|
||||
}
|
||||
}}
|
||||
onClick={handleDeleteFlow}
|
||||
>
|
||||
<IconTrash stroke={1.5} size='1.3rem' />
|
||||
</Avatar>
|
||||
</ButtonBase>
|
||||
<Available permission={'assistants:delete'}>
|
||||
<ButtonBase ref={settingsRef} title='Delete Assistant' sx={{ borderRadius: '50%' }}>
|
||||
<Avatar
|
||||
variant='rounded'
|
||||
sx={{
|
||||
...theme.typography.commonAvatar,
|
||||
...theme.typography.mediumAvatar,
|
||||
transition: 'all .2s ease-in-out',
|
||||
background: theme.palette.error.light,
|
||||
color: theme.palette.error.dark,
|
||||
'&:hover': {
|
||||
background: theme.palette.error.dark,
|
||||
color: theme.palette.error.light
|
||||
}
|
||||
}}
|
||||
onClick={handleDeleteFlow}
|
||||
>
|
||||
<IconTrash stroke={1.5} size='1.3rem' />
|
||||
</Avatar>
|
||||
</ButtonBase>
|
||||
</Available>
|
||||
)}
|
||||
</Toolbar>
|
||||
</Box>
|
||||
@@ -1237,20 +1242,22 @@ const CustomAssistantConfigurePreview = () => {
|
||||
</Button>
|
||||
</Box>
|
||||
{selectedChatModel && Object.keys(selectedChatModel).length > 0 && (
|
||||
<Button
|
||||
fullWidth
|
||||
title='Save Assistant'
|
||||
sx={{
|
||||
mt: 1,
|
||||
mb: 1,
|
||||
borderRadius: 20,
|
||||
background: 'linear-gradient(45deg, #673ab7 30%, #1e88e5 90%)'
|
||||
}}
|
||||
variant='contained'
|
||||
onClick={onSaveAndProcess}
|
||||
>
|
||||
Save Assistant
|
||||
</Button>
|
||||
<Available permission={'assistants:create'}>
|
||||
<Button
|
||||
fullWidth
|
||||
title='Save Assistant'
|
||||
sx={{
|
||||
mt: 1,
|
||||
mb: 1,
|
||||
borderRadius: 20,
|
||||
background: 'linear-gradient(45deg, #673ab7 30%, #1e88e5 90%)'
|
||||
}}
|
||||
variant='contained'
|
||||
onClick={onSaveAndProcess}
|
||||
>
|
||||
Save Assistant
|
||||
</Button>
|
||||
</Available>
|
||||
)}
|
||||
</div>
|
||||
</Grid>
|
||||
|
||||
@@ -10,9 +10,9 @@ import MainCard from '@/ui-component/cards/MainCard'
|
||||
import ItemCard from '@/ui-component/cards/ItemCard'
|
||||
import { baseURL, gridSpacing } from '@/store/constant'
|
||||
import AssistantEmptySVG from '@/assets/images/assistant_empty.svg'
|
||||
import { StyledButton } from '@/ui-component/button/StyledButton'
|
||||
import AddCustomAssistantDialog from './AddCustomAssistantDialog'
|
||||
import ErrorBoundary from '@/ErrorBoundary'
|
||||
import { StyledPermissionButton } from '@/ui-component/button/RBACButtons'
|
||||
|
||||
// API
|
||||
import assistantsApi from '@/api/assistants'
|
||||
@@ -101,14 +101,15 @@ const CustomAssistantLayout = () => {
|
||||
description='Create custom assistants with your choice of LLMs'
|
||||
onBack={() => navigate(-1)}
|
||||
>
|
||||
<StyledButton
|
||||
<StyledPermissionButton
|
||||
permissionId={'assistants:create'}
|
||||
variant='contained'
|
||||
sx={{ borderRadius: 2, height: 40 }}
|
||||
onClick={addNew}
|
||||
startIcon={<IconPlus />}
|
||||
>
|
||||
Add
|
||||
</StyledButton>
|
||||
</StyledPermissionButton>
|
||||
</ViewHeader>
|
||||
{isLoading ? (
|
||||
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
OutlinedInput
|
||||
} from '@mui/material'
|
||||
|
||||
import { StyledButton } from '@/ui-component/button/StyledButton'
|
||||
import { TooltipWithParser } from '@/ui-component/tooltip/TooltipWithParser'
|
||||
import { Dropdown } from '@/ui-component/dropdown/Dropdown'
|
||||
import { MultiDropdown } from '@/ui-component/dropdown/MultiDropdown'
|
||||
@@ -30,6 +29,7 @@ import { File } from '@/ui-component/file/File'
|
||||
import { BackdropLoader } from '@/ui-component/loading/BackdropLoader'
|
||||
import DeleteConfirmDialog from './DeleteConfirmDialog'
|
||||
import AssistantVectorStoreDialog from './AssistantVectorStoreDialog'
|
||||
import { StyledPermissionButton } from '@/ui-component/button/RBACButtons'
|
||||
|
||||
// Icons
|
||||
import { IconX, IconPlus } from '@tabler/icons-react'
|
||||
@@ -205,6 +205,7 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
|
||||
|
||||
useEffect(() => {
|
||||
if (getSpecificAssistantApi.error) {
|
||||
const error = getSpecificAssistantApi.error
|
||||
let errMsg = ''
|
||||
if (error?.response?.data) {
|
||||
errMsg = typeof error.response.data === 'object' ? error.response.data.message : error.response.data
|
||||
@@ -1035,22 +1036,33 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) =
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ p: 3, pt: 0 }}>
|
||||
{dialogProps.type === 'EDIT' && (
|
||||
<StyledButton color='secondary' variant='contained' onClick={() => onSyncClick()}>
|
||||
<StyledPermissionButton
|
||||
permissionId={'assistants:create,assistants:update'}
|
||||
color='secondary'
|
||||
variant='contained'
|
||||
onClick={() => onSyncClick()}
|
||||
>
|
||||
Sync
|
||||
</StyledButton>
|
||||
</StyledPermissionButton>
|
||||
)}
|
||||
{dialogProps.type === 'EDIT' && (
|
||||
<StyledButton color='error' variant='contained' onClick={() => onDeleteClick()}>
|
||||
<StyledPermissionButton
|
||||
permissionId={'assistants:delete'}
|
||||
color='error'
|
||||
variant='contained'
|
||||
onClick={() => onDeleteClick()}
|
||||
>
|
||||
Delete
|
||||
</StyledButton>
|
||||
</StyledPermissionButton>
|
||||
)}
|
||||
<StyledButton
|
||||
<StyledPermissionButton
|
||||
permissionId={'assistants:create,assistants:update'}
|
||||
disabled={!(assistantModel && assistantCredential)}
|
||||
variant='contained'
|
||||
onClick={() => (dialogProps.type === 'ADD' ? addNewAssistant() : saveAssistant())}
|
||||
>
|
||||
{dialogProps.confirmButtonName}
|
||||
</StyledButton>
|
||||
</StyledPermissionButton>
|
||||
</DialogActions>
|
||||
<DeleteConfirmDialog
|
||||
show={deleteDialogOpen}
|
||||
|
||||
@@ -2,16 +2,16 @@ import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
// material-ui
|
||||
import { Box, Stack, Button, Skeleton } from '@mui/material'
|
||||
import { Box, Stack, Skeleton } from '@mui/material'
|
||||
|
||||
// project imports
|
||||
import MainCard from '@/ui-component/cards/MainCard'
|
||||
import ItemCard from '@/ui-component/cards/ItemCard'
|
||||
import { StyledButton } from '@/ui-component/button/StyledButton'
|
||||
import AssistantDialog from './AssistantDialog'
|
||||
import LoadAssistantDialog from './LoadAssistantDialog'
|
||||
import ViewHeader from '@/layout/MainLayout/ViewHeader'
|
||||
import ErrorBoundary from '@/ErrorBoundary'
|
||||
import { PermissionButton, StyledPermissionButton } from '@/ui-component/button/RBACButtons'
|
||||
|
||||
// API
|
||||
import assistantsApi from '@/api/assistants'
|
||||
@@ -123,22 +123,24 @@ const OpenAIAssistantLayout = () => {
|
||||
description='Create assistants using OpenAI Assistant API'
|
||||
onBack={() => navigate(-1)}
|
||||
>
|
||||
<Button
|
||||
<PermissionButton
|
||||
permissionId={'assistants:create'}
|
||||
variant='outlined'
|
||||
onClick={loadExisting}
|
||||
startIcon={<IconFileUpload />}
|
||||
sx={{ borderRadius: 2, height: 40 }}
|
||||
>
|
||||
Load
|
||||
</Button>
|
||||
<StyledButton
|
||||
</PermissionButton>
|
||||
<StyledPermissionButton
|
||||
permissionId={'assistants:create'}
|
||||
variant='contained'
|
||||
sx={{ borderRadius: 2, height: 40 }}
|
||||
onClick={addNew}
|
||||
startIcon={<IconPlus />}
|
||||
>
|
||||
Add
|
||||
</StyledButton>
|
||||
</StyledPermissionButton>
|
||||
</ViewHeader>
|
||||
{isLoading ? (
|
||||
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import MainCard from '@/ui-component/cards/MainCard'
|
||||
import { Box, Stack, Typography } from '@mui/material'
|
||||
import contactSupport from '@/assets/images/contact_support.svg'
|
||||
import { StyledButton } from '@/ui-component/button/StyledButton'
|
||||
|
||||
// ==============================|| License Expired Page ||============================== //
|
||||
|
||||
const LicenseExpired = () => {
|
||||
return (
|
||||
<>
|
||||
<MainCard>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: 'calc(100vh - 210px)'
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
flexDirection='column'
|
||||
>
|
||||
<Box sx={{ p: 2, height: 'auto', mb: 4 }}>
|
||||
<img style={{ objectFit: 'cover', height: '16vh', width: 'auto' }} src={contactSupport} alt='contact support' />
|
||||
</Box>
|
||||
<Typography sx={{ mb: 2 }} variant='h4' component='div' fontWeight='bold'>
|
||||
Your enterprise license has expired
|
||||
</Typography>
|
||||
<Typography variant='body1' component='div' sx={{ mb: 2 }}>
|
||||
Please contact our support team to renew your license.
|
||||
</Typography>
|
||||
<a href='mailto:support@flowiseai.com'>
|
||||
<StyledButton sx={{ px: 2, py: 1 }}>Contact Support</StyledButton>
|
||||
</a>
|
||||
</Stack>
|
||||
</Box>
|
||||
</MainCard>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default LicenseExpired
|
||||
@@ -0,0 +1,146 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
// material-ui
|
||||
import { Alert, Box, Stack, Typography, useTheme } from '@mui/material'
|
||||
|
||||
// project imports
|
||||
import { StyledButton } from '@/ui-component/button/StyledButton'
|
||||
import MainCard from '@/ui-component/cards/MainCard'
|
||||
import { Input } from '@/ui-component/input/Input'
|
||||
import { BackdropLoader } from '@/ui-component/loading/BackdropLoader'
|
||||
|
||||
// API
|
||||
import accountApi from '@/api/account.api'
|
||||
|
||||
// Hooks
|
||||
import useApi from '@/hooks/useApi'
|
||||
import { useConfig } from '@/store/context/ConfigContext'
|
||||
|
||||
// utils
|
||||
import useNotifier from '@/utils/useNotifier'
|
||||
|
||||
// Icons
|
||||
import { IconCircleCheck, IconExclamationCircle } from '@tabler/icons-react'
|
||||
|
||||
// ==============================|| ForgotPasswordPage ||============================== //
|
||||
|
||||
const ForgotPasswordPage = () => {
|
||||
const theme = useTheme()
|
||||
useNotifier()
|
||||
|
||||
const usernameInput = {
|
||||
label: 'Username',
|
||||
name: 'username',
|
||||
type: 'email',
|
||||
placeholder: 'user@company.com'
|
||||
}
|
||||
const [usernameVal, setUsernameVal] = useState('')
|
||||
const { isEnterpriseLicensed } = useConfig()
|
||||
|
||||
const [isLoading, setLoading] = useState(false)
|
||||
const [responseMsg, setResponseMsg] = useState(undefined)
|
||||
|
||||
const forgotPasswordApi = useApi(accountApi.forgotPassword)
|
||||
|
||||
const sendResetRequest = async (event) => {
|
||||
event.preventDefault()
|
||||
const body = {
|
||||
user: {
|
||||
email: usernameVal
|
||||
}
|
||||
}
|
||||
setLoading(true)
|
||||
await forgotPasswordApi.request(body)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (forgotPasswordApi.error) {
|
||||
const errMessage =
|
||||
typeof forgotPasswordApi.error.response.data === 'object'
|
||||
? forgotPasswordApi.error.response.data.message
|
||||
: forgotPasswordApi.error.response.data
|
||||
setResponseMsg({
|
||||
type: 'error',
|
||||
msg: errMessage ?? 'Failed to send instructions, please contact your administrator.'
|
||||
})
|
||||
setLoading(false)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [forgotPasswordApi.error])
|
||||
|
||||
useEffect(() => {
|
||||
if (forgotPasswordApi.data) {
|
||||
setResponseMsg({
|
||||
type: 'success',
|
||||
msg: 'Password reset instructions sent to the email.'
|
||||
})
|
||||
setLoading(false)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [forgotPasswordApi.data])
|
||||
|
||||
return (
|
||||
<>
|
||||
<MainCard>
|
||||
<Stack flexDirection='column' sx={{ width: '480px', gap: 3 }}>
|
||||
{responseMsg && responseMsg?.type === 'error' && (
|
||||
<Alert icon={<IconExclamationCircle />} variant='filled' severity='error'>
|
||||
{responseMsg.msg}
|
||||
</Alert>
|
||||
)}
|
||||
{responseMsg && responseMsg?.type !== 'error' && (
|
||||
<Alert icon={<IconCircleCheck />} variant='filled' severity='success'>
|
||||
{responseMsg.msg}
|
||||
</Alert>
|
||||
)}
|
||||
<Stack sx={{ gap: 1 }}>
|
||||
<Typography variant='h1'>Forgot Password?</Typography>
|
||||
<Typography variant='body2' sx={{ color: theme.palette.grey[600] }}>
|
||||
Have a reset password code?{' '}
|
||||
<Link style={{ color: theme.palette.primary.main }} to='/reset-password'>
|
||||
Change your password here
|
||||
</Link>
|
||||
.
|
||||
</Typography>
|
||||
</Stack>
|
||||
<form onSubmit={sendResetRequest}>
|
||||
<Stack sx={{ width: '100%', flexDirection: 'column', alignItems: 'left', justifyContent: 'center', gap: 2 }}>
|
||||
<Box>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<Typography>
|
||||
Email<span style={{ color: 'red' }}> *</span>
|
||||
</Typography>
|
||||
<Typography align='left'></Typography>
|
||||
<div style={{ flexGrow: 1 }}></div>
|
||||
</div>
|
||||
<Input
|
||||
inputParam={usernameInput}
|
||||
onChange={(newValue) => setUsernameVal(newValue)}
|
||||
value={usernameVal}
|
||||
showDialog={false}
|
||||
/>
|
||||
{isEnterpriseLicensed && (
|
||||
<Typography variant='caption'>
|
||||
<i>If you forgot the email you used for signing up, please contact your administrator.</i>
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<StyledButton
|
||||
variant='contained'
|
||||
style={{ borderRadius: 12, height: 40, marginRight: 5 }}
|
||||
disabled={!usernameVal}
|
||||
type='submit'
|
||||
>
|
||||
Send Reset Password Instructions
|
||||
</StyledButton>
|
||||
</Stack>
|
||||
</form>
|
||||
<BackdropLoader open={isLoading} />
|
||||
</Stack>
|
||||
</MainCard>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ForgotPasswordPage
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
// material-ui
|
||||
import { BackdropLoader } from '@/ui-component/loading/BackdropLoader'
|
||||
|
||||
// project imports
|
||||
import MainCard from '@/ui-component/cards/MainCard'
|
||||
|
||||
// API
|
||||
import authApi from '@/api/auth'
|
||||
|
||||
// Hooks
|
||||
import useApi from '@/hooks/useApi'
|
||||
|
||||
// ==============================|| ResolveLoginPage ||============================== //
|
||||
|
||||
const ResolveLoginPage = () => {
|
||||
const resolveLogin = useApi(authApi.resolveLogin)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(false)
|
||||
}, [resolveLogin.error])
|
||||
|
||||
useEffect(() => {
|
||||
resolveLogin.request({})
|
||||
setLoading(true)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(false)
|
||||
if (resolveLogin.data) {
|
||||
window.location.href = resolveLogin.data.redirectUrl
|
||||
}
|
||||
}, [resolveLogin.data])
|
||||
|
||||
return (
|
||||
<>
|
||||
<MainCard maxWidth='md'>{loading && <BackdropLoader open={loading} />}</MainCard>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ResolveLoginPage
|
||||
@@ -0,0 +1,638 @@
|
||||
import { forwardRef, useEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import moment from 'moment/moment'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
// material-ui
|
||||
import {
|
||||
Box,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Table,
|
||||
TableBody,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
IconButton,
|
||||
useTheme,
|
||||
Checkbox,
|
||||
Button,
|
||||
OutlinedInput,
|
||||
MenuItem,
|
||||
Select,
|
||||
InputLabel,
|
||||
FormControl,
|
||||
ListItemText,
|
||||
ListItemButton
|
||||
} from '@mui/material'
|
||||
|
||||
// project imports
|
||||
import MainCard from '@/ui-component/cards/MainCard'
|
||||
import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog'
|
||||
import ViewHeader from '@/layout/MainLayout/ViewHeader'
|
||||
import ErrorBoundary from '@/ErrorBoundary'
|
||||
import { StyledTableCell, StyledTableRow } from '@/ui-component/table/TableStyles'
|
||||
import DatePicker from 'react-datepicker'
|
||||
import 'react-datepicker/dist/react-datepicker.css'
|
||||
|
||||
// API
|
||||
import auditApi from '@/api/audit'
|
||||
|
||||
// Hooks
|
||||
import useApi from '@/hooks/useApi'
|
||||
import useConfirm from '@/hooks/useConfirm'
|
||||
|
||||
// utils
|
||||
import useNotifier from '@/utils/useNotifier'
|
||||
|
||||
// Icons
|
||||
import { IconCircleX, IconChevronLeft, IconChevronRight, IconTrash, IconX, IconLogin, IconLogout } from '@tabler/icons-react'
|
||||
|
||||
// store
|
||||
import { useError } from '@/store/context/ErrorContext'
|
||||
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions'
|
||||
import { PermissionButton } from '@/ui-component/button/RBACButtons'
|
||||
|
||||
const activityTypes = [
|
||||
'Login Success',
|
||||
'Logout Success',
|
||||
'Unknown User',
|
||||
'Incorrect Credential',
|
||||
'User Disabled',
|
||||
'No Assigned Workspace',
|
||||
'Unknown Activity'
|
||||
]
|
||||
const MenuProps = {
|
||||
PaperProps: {
|
||||
style: {
|
||||
width: 160
|
||||
}
|
||||
}
|
||||
}
|
||||
const SelectStyles = {
|
||||
'& .MuiOutlinedInput-notchedOutline': {
|
||||
borderRadius: 2
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================|| Login Activity ||============================== //
|
||||
|
||||
const DatePickerCustomInput = forwardRef(function DatePickerCustomInput({ value, onClick }, ref) {
|
||||
return (
|
||||
<ListItemButton style={{ borderRadius: 15, border: '1px solid #e0e0e0' }} onClick={onClick} ref={ref}>
|
||||
{value}
|
||||
</ListItemButton>
|
||||
)
|
||||
})
|
||||
|
||||
DatePickerCustomInput.propTypes = {
|
||||
value: PropTypes.string,
|
||||
onClick: PropTypes.func
|
||||
}
|
||||
const LoginActivity = () => {
|
||||
const theme = useTheme()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
const dispatch = useDispatch()
|
||||
useNotifier()
|
||||
const { error, setError } = useError()
|
||||
|
||||
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
||||
|
||||
const [isLoading, setLoading] = useState(true)
|
||||
|
||||
const { confirm } = useConfirm()
|
||||
|
||||
const getLoginActivityApi = useApi(auditApi.fetchLoginActivity)
|
||||
const [activity, setActivity] = useState([])
|
||||
const [typeFilter, setTypeFilter] = useState([])
|
||||
const [totalRecords, setTotalRecords] = useState(0)
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [start, setStart] = useState(1)
|
||||
const [end, setEnd] = useState(50)
|
||||
const [startDate, setStartDate] = useState(new Date(new Date().setMonth(new Date().getMonth() - 1)))
|
||||
const [endDate, setEndDate] = useState(new Date())
|
||||
|
||||
const [selected, setSelected] = useState([])
|
||||
|
||||
const onStartDateSelected = (date) => {
|
||||
setStartDate(date)
|
||||
refreshData(currentPage, date, endDate, typeFilter)
|
||||
}
|
||||
|
||||
const onEndDateSelected = (date) => {
|
||||
setEndDate(date)
|
||||
refreshData(currentPage, startDate, date, typeFilter)
|
||||
}
|
||||
|
||||
const onSelectAllClick = (event) => {
|
||||
if (event.target.checked) {
|
||||
const newSelected = activity.map((n) => n.id)
|
||||
setSelected(newSelected)
|
||||
return
|
||||
}
|
||||
setSelected([])
|
||||
}
|
||||
|
||||
const handleSelect = (event, id) => {
|
||||
const selectedIndex = selected.indexOf(id)
|
||||
let newSelected = []
|
||||
|
||||
if (selectedIndex === -1) {
|
||||
newSelected = newSelected.concat(selected, id)
|
||||
} else if (selectedIndex === 0) {
|
||||
newSelected = newSelected.concat(selected.slice(1))
|
||||
} else if (selectedIndex === selected.length - 1) {
|
||||
newSelected = newSelected.concat(selected.slice(0, -1))
|
||||
} else if (selectedIndex > 0) {
|
||||
newSelected = newSelected.concat(selected.slice(0, selectedIndex), selected.slice(selectedIndex + 1))
|
||||
}
|
||||
setSelected(newSelected)
|
||||
}
|
||||
|
||||
const refreshData = (_page, _start, _end, _filter) => {
|
||||
const activityCodes = []
|
||||
if (_filter.length > 0) {
|
||||
_filter.forEach((type) => {
|
||||
activityCodes.push(getActivityCode(type))
|
||||
})
|
||||
}
|
||||
getLoginActivityApi.request({
|
||||
pageNo: _page,
|
||||
startDate: _start,
|
||||
endDate: _end,
|
||||
activityCodes: activityCodes
|
||||
})
|
||||
}
|
||||
|
||||
const changePage = (newPage) => {
|
||||
setLoading(true)
|
||||
setCurrentPage(newPage)
|
||||
refreshData(newPage, startDate, endDate, typeFilter)
|
||||
}
|
||||
|
||||
const handleTypeFilterChange = (event) => {
|
||||
const {
|
||||
target: { value }
|
||||
} = event
|
||||
let newVar = typeof value === 'string' ? value.split(',') : value
|
||||
setTypeFilter(newVar)
|
||||
refreshData(currentPage, startDate, endDate, newVar)
|
||||
}
|
||||
|
||||
function getActivityDescription(activityCode) {
|
||||
switch (activityCode) {
|
||||
case 0:
|
||||
return 'Login Success'
|
||||
case 1:
|
||||
return 'Logout Success'
|
||||
case -1:
|
||||
return 'Unknown User'
|
||||
case -2:
|
||||
return 'Incorrect Credential'
|
||||
case -3:
|
||||
return 'User Disabled'
|
||||
case -4:
|
||||
return 'No Assigned Workspace'
|
||||
default:
|
||||
return 'Unknown Activity'
|
||||
}
|
||||
}
|
||||
|
||||
function getActivityCode(activityDescription) {
|
||||
switch (activityDescription) {
|
||||
case 'Login Success':
|
||||
return 0
|
||||
case 'Logout Success':
|
||||
return 1
|
||||
case 'Unknown User':
|
||||
return -1
|
||||
case 'Incorrect Credential':
|
||||
return -2
|
||||
case 'User Disabled':
|
||||
return -3
|
||||
case 'No Assigned Workspace':
|
||||
return -4
|
||||
default:
|
||||
return -99
|
||||
}
|
||||
}
|
||||
|
||||
const deleteLoginActivity = async () => {
|
||||
const confirmPayload = {
|
||||
title: `Delete`,
|
||||
description: `Delete ${selected.length} ${selected.length > 1 ? 'records' : 'record'}? `,
|
||||
confirmButtonName: 'Delete',
|
||||
cancelButtonName: 'Cancel'
|
||||
}
|
||||
const isConfirmed = await confirm(confirmPayload)
|
||||
//
|
||||
if (isConfirmed) {
|
||||
try {
|
||||
const deleteResp = await auditApi.deleteLoginActivity({
|
||||
selected: selected
|
||||
})
|
||||
if (deleteResp.data) {
|
||||
enqueueSnackbar({
|
||||
message: selected.length + ' Login Activity Records Deleted Successfully',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'success',
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
onConfirm()
|
||||
}
|
||||
} catch (error) {
|
||||
enqueueSnackbar({
|
||||
message: `Failed to delete records: ${
|
||||
typeof error.response.data === 'object' ? error.response.data.message : error.response.data
|
||||
}`,
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
persist: true,
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onConfirm = () => {
|
||||
getLoginActivityApi.request()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getLoginActivityApi.request({
|
||||
pageNo: 1
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(getLoginActivityApi.loading)
|
||||
}, [getLoginActivityApi.loading])
|
||||
|
||||
useEffect(() => {
|
||||
if (getLoginActivityApi.error) {
|
||||
setError(getLoginActivityApi.error)
|
||||
}
|
||||
}, [getLoginActivityApi.error, setError])
|
||||
|
||||
useEffect(() => {
|
||||
if (getLoginActivityApi.data) {
|
||||
const data = getLoginActivityApi.data
|
||||
setTotalRecords(data.count)
|
||||
setLoading(false)
|
||||
setCurrentPage(data.currentPage)
|
||||
setStart(data.currentPage * data.pageSize - (data.pageSize - 1))
|
||||
setEnd(data.currentPage * data.pageSize > data.count ? data.count : data.currentPage * data.pageSize)
|
||||
setActivity(data.data)
|
||||
setSelected([])
|
||||
}
|
||||
}, [getLoginActivityApi.data])
|
||||
|
||||
return (
|
||||
<>
|
||||
<MainCard>
|
||||
{error ? (
|
||||
<ErrorBoundary error={error} />
|
||||
) : (
|
||||
<Stack flexDirection='column' sx={{ gap: 3 }}>
|
||||
<ViewHeader search={false} title='Login Activity'></ViewHeader>
|
||||
<Stack flexDirection='row'>
|
||||
<Box sx={{ p: 2, height: 'auto', width: '100%' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
overflow: 'hidden',
|
||||
marginBottom: 10
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row'
|
||||
}}
|
||||
>
|
||||
<div style={{ marginRight: 10 }}>
|
||||
<b style={{ marginRight: 10 }}>From: </b>
|
||||
<DatePicker
|
||||
selected={startDate}
|
||||
onChange={(date) => onStartDateSelected(date)}
|
||||
selectsStart
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
customInput={<DatePickerCustomInput />}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginRight: 10 }}>
|
||||
<b style={{ marginRight: 10 }}>To: </b>
|
||||
<DatePicker
|
||||
selected={endDate}
|
||||
onChange={(date) => onEndDateSelected(date)}
|
||||
selectsEnd
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
minDate={startDate}
|
||||
maxDate={new Date()}
|
||||
customInput={<DatePickerCustomInput />}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FormControl
|
||||
sx={{
|
||||
borderRadius: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'end',
|
||||
minWidth: 300,
|
||||
maxWidth: 300
|
||||
}}
|
||||
>
|
||||
<InputLabel size='small' id='type-label'>
|
||||
Filter By
|
||||
</InputLabel>
|
||||
<Select
|
||||
size='small'
|
||||
labelId='type-label'
|
||||
multiple
|
||||
value={typeFilter}
|
||||
onChange={handleTypeFilterChange}
|
||||
id='type-checkbox'
|
||||
input={<OutlinedInput label='Badge' />}
|
||||
renderValue={(selected) => selected.join(', ')}
|
||||
MenuProps={MenuProps}
|
||||
sx={SelectStyles}
|
||||
>
|
||||
{activityTypes.map((name) => (
|
||||
<MenuItem
|
||||
key={name}
|
||||
value={name}
|
||||
sx={{ display: 'flex', alignItems: 'center', gap: 1, p: 1 }}
|
||||
>
|
||||
<Checkbox checked={typeFilter.indexOf(name) > -1} sx={{ p: 0 }} />
|
||||
<ListItemText primary={name} />
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
marginRight: 10,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
size='small'
|
||||
onClick={() => changePage(currentPage - 1)}
|
||||
style={{ marginRight: 10 }}
|
||||
variant='outlined'
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
<IconChevronLeft
|
||||
color={
|
||||
customization.isDarkMode
|
||||
? currentPage === 1
|
||||
? '#616161'
|
||||
: 'white'
|
||||
: currentPage === 1
|
||||
? '#e0e0e0'
|
||||
: 'black'
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
Showing {Math.min(start, totalRecords)}-{end} of {totalRecords} Records
|
||||
<IconButton
|
||||
size='small'
|
||||
onClick={() => changePage(currentPage + 1)}
|
||||
style={{ marginLeft: 10 }}
|
||||
variant='outlined'
|
||||
disabled={end >= totalRecords}
|
||||
>
|
||||
<IconChevronRight
|
||||
color={
|
||||
customization.isDarkMode
|
||||
? end >= totalRecords
|
||||
? '#616161'
|
||||
: 'white'
|
||||
: end >= totalRecords
|
||||
? '#e0e0e0'
|
||||
: 'black'
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
<PermissionButton
|
||||
permissionId={'loginActivity:delete'}
|
||||
sx={{ mt: 1, mb: 2 }}
|
||||
variant='outlined'
|
||||
disabled={selected.length === 0}
|
||||
onClick={deleteLoginActivity}
|
||||
color='error'
|
||||
startIcon={<IconTrash />}
|
||||
>
|
||||
{'Delete Selected'}
|
||||
</PermissionButton>
|
||||
</div>
|
||||
</div>
|
||||
<TableContainer
|
||||
style={{ display: 'flex', flexDirection: 'row' }}
|
||||
sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }}
|
||||
component={Paper}
|
||||
>
|
||||
<Table sx={{ minWidth: 650 }} aria-label='users table'>
|
||||
<TableHead
|
||||
sx={{
|
||||
backgroundColor: customization.isDarkMode
|
||||
? theme.palette.common.black
|
||||
: theme.palette.grey[100],
|
||||
height: 40
|
||||
}}
|
||||
>
|
||||
<TableRow>
|
||||
<StyledTableCell style={{ width: '5%' }}>
|
||||
<Checkbox
|
||||
color='primary'
|
||||
checked={selected.length === (activity || []).length}
|
||||
onChange={onSelectAllClick}
|
||||
/>
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>Activity</StyledTableCell>
|
||||
<StyledTableCell>User</StyledTableCell>
|
||||
<StyledTableCell>Date</StyledTableCell>
|
||||
<StyledTableCell>Method</StyledTableCell>
|
||||
<StyledTableCell>Message</StyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<StyledTableRow>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
</StyledTableRow>
|
||||
<StyledTableRow>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
</StyledTableRow>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{activity.map((item, index) => (
|
||||
<StyledTableRow
|
||||
hover
|
||||
key={index}
|
||||
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
|
||||
>
|
||||
<StyledTableCell component='th' scope='row' style={{ width: '5%' }}>
|
||||
<Checkbox
|
||||
color='primary'
|
||||
checked={selected.indexOf(item.id) !== -1}
|
||||
onChange={(event) => handleSelect(event, item.id)}
|
||||
/>
|
||||
</StyledTableCell>
|
||||
<StyledTableCell component='th' scope='row'>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'left'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 25,
|
||||
height: 25,
|
||||
borderRadius: '50%',
|
||||
marginRight: 10
|
||||
}}
|
||||
>
|
||||
{item.activityCode === 0 && (
|
||||
<IconLogin
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: '50%',
|
||||
objectFit: 'contain',
|
||||
color: theme.palette.success.dark
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{item.activityCode === 1 && (
|
||||
<IconLogout
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: '50%',
|
||||
objectFit: 'contain',
|
||||
color: theme.palette.secondary.dark
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{item.activityCode < 0 && (
|
||||
<IconCircleX
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: '50%',
|
||||
objectFit: 'contain',
|
||||
color: theme.palette.error.dark
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div>{getActivityDescription(item.activityCode)}</div>
|
||||
</div>
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>{item.username}</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
{moment(item.attemptedDateTime).format('MMMM Do, YYYY, HH:mm')}
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
{item.loginMode ? item.loginMode : 'Email/Password'}
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>{item.message}</StyledTableCell>
|
||||
</StyledTableRow>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)}
|
||||
</MainCard>
|
||||
<ConfirmDialog />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginActivity
|
||||
@@ -0,0 +1,472 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { z } from 'zod'
|
||||
|
||||
// material-ui
|
||||
import { Alert, Box, Button, Divider, Icon, List, ListItemText, OutlinedInput, Stack, Typography, useTheme } from '@mui/material'
|
||||
|
||||
// project imports
|
||||
import { StyledButton } from '@/ui-component/button/StyledButton'
|
||||
import { Input } from '@/ui-component/input/Input'
|
||||
import { BackdropLoader } from '@/ui-component/loading/BackdropLoader'
|
||||
|
||||
// API
|
||||
import accountApi from '@/api/account.api'
|
||||
import loginMethodApi from '@/api/loginmethod'
|
||||
import ssoApi from '@/api/sso'
|
||||
|
||||
// Hooks
|
||||
import useApi from '@/hooks/useApi'
|
||||
import { useConfig } from '@/store/context/ConfigContext'
|
||||
|
||||
// utils
|
||||
import useNotifier from '@/utils/useNotifier'
|
||||
import { passwordSchema } from '@/utils/validation'
|
||||
|
||||
// Icons
|
||||
import Auth0SSOLoginIcon from '@/assets/images/auth0.svg'
|
||||
import GithubSSOLoginIcon from '@/assets/images/github.svg'
|
||||
import GoogleSSOLoginIcon from '@/assets/images/google.svg'
|
||||
import AzureSSOLoginIcon from '@/assets/images/microsoft-azure.svg'
|
||||
import { store } from '@/store'
|
||||
import { loginSuccess } from '@/store/reducers/authSlice'
|
||||
import { IconCircleCheck, IconExclamationCircle } from '@tabler/icons-react'
|
||||
|
||||
// ==============================|| Register ||============================== //
|
||||
|
||||
// IMPORTANT: when updating this schema, update the schema on the server as well
|
||||
// packages/server/src/enterprise/Interface.Enterprise.ts
|
||||
const RegisterEnterpriseUserSchema = z
|
||||
.object({
|
||||
username: z.string().min(1, 'Name is required'),
|
||||
email: z.string().min(1, 'Email is required').email('Invalid email address'),
|
||||
password: passwordSchema,
|
||||
confirmPassword: z.string().min(1, 'Confirm Password is required'),
|
||||
token: z.string().min(1, 'Invite Code is required')
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
message: "Passwords don't match",
|
||||
path: ['confirmPassword']
|
||||
})
|
||||
|
||||
const RegisterCloudUserSchema = z
|
||||
.object({
|
||||
username: z.string().min(1, 'Name is required'),
|
||||
email: z.string().min(1, 'Email is required').email('Invalid email address'),
|
||||
password: passwordSchema,
|
||||
confirmPassword: z.string().min(1, 'Confirm Password is required')
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
message: "Passwords don't match",
|
||||
path: ['confirmPassword']
|
||||
})
|
||||
|
||||
const RegisterPage = () => {
|
||||
const theme = useTheme()
|
||||
useNotifier()
|
||||
const { isEnterpriseLicensed, isCloud, isOpenSource } = useConfig()
|
||||
|
||||
const usernameInput = {
|
||||
label: 'Username',
|
||||
name: 'username',
|
||||
type: 'text',
|
||||
placeholder: 'John Doe'
|
||||
}
|
||||
|
||||
const passwordInput = {
|
||||
label: 'Password',
|
||||
name: 'password',
|
||||
type: 'password',
|
||||
placeholder: '********'
|
||||
}
|
||||
|
||||
const confirmPasswordInput = {
|
||||
label: 'Confirm Password',
|
||||
name: 'confirmPassword',
|
||||
type: 'password',
|
||||
placeholder: '********'
|
||||
}
|
||||
|
||||
const emailInput = {
|
||||
label: 'EMail',
|
||||
name: 'email',
|
||||
type: 'email',
|
||||
placeholder: 'user@company.com'
|
||||
}
|
||||
|
||||
const inviteCodeInput = {
|
||||
label: 'Invite Code',
|
||||
name: 'inviteCode',
|
||||
type: 'text'
|
||||
}
|
||||
|
||||
const [params] = useSearchParams()
|
||||
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [confirmPassword, setConfirmPassword] = useState('')
|
||||
const [token, setToken] = useState(params.get('token') ?? '')
|
||||
const [username, setUsername] = useState('')
|
||||
const [configuredSsoProviders, setConfiguredSsoProviders] = useState([])
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [authError, setAuthError] = useState('')
|
||||
const [successMsg, setSuccessMsg] = useState(undefined)
|
||||
|
||||
const registerApi = useApi(accountApi.registerAccount)
|
||||
const ssoLoginApi = useApi(ssoApi.ssoLogin)
|
||||
const getDefaultProvidersApi = useApi(loginMethodApi.getDefaultLoginMethods)
|
||||
const navigate = useNavigate()
|
||||
|
||||
const register = async (event) => {
|
||||
event.preventDefault()
|
||||
if (isEnterpriseLicensed) {
|
||||
const result = RegisterEnterpriseUserSchema.safeParse({
|
||||
username,
|
||||
email,
|
||||
token,
|
||||
password,
|
||||
confirmPassword
|
||||
})
|
||||
if (result.success) {
|
||||
setLoading(true)
|
||||
const body = {
|
||||
user: {
|
||||
name: username,
|
||||
email,
|
||||
credential: password,
|
||||
tempToken: token
|
||||
}
|
||||
}
|
||||
await registerApi.request(body)
|
||||
} else {
|
||||
const errorMessages = result.error.errors.map((err) => err.message)
|
||||
setAuthError(errorMessages.join(', '))
|
||||
}
|
||||
} else if (isCloud) {
|
||||
const formData = new FormData(event.target)
|
||||
const referral = formData.get('referral')
|
||||
const result = RegisterCloudUserSchema.safeParse({
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
confirmPassword
|
||||
})
|
||||
if (result.success) {
|
||||
setLoading(true)
|
||||
const body = {
|
||||
user: {
|
||||
name: username,
|
||||
email,
|
||||
credential: password
|
||||
}
|
||||
}
|
||||
if (referral) {
|
||||
body.user.referral = referral
|
||||
}
|
||||
await registerApi.request(body)
|
||||
} else {
|
||||
const errorMessages = result.error.errors.map((err) => err.message)
|
||||
setAuthError(errorMessages.join(', '))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const signInWithSSO = (ssoProvider) => {
|
||||
//ssoLoginApi.request(ssoProvider)
|
||||
window.location.href = `/api/v1/${ssoProvider}/login`
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (registerApi.error) {
|
||||
if (isEnterpriseLicensed) {
|
||||
setAuthError(
|
||||
`Error in registering user. Please contact your administrator. (${registerApi.error?.response?.data?.message})`
|
||||
)
|
||||
} else if (isCloud) {
|
||||
setAuthError(`Error in registering user. Please try again.`)
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [registerApi.error])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpenSource) {
|
||||
getDefaultProvidersApi.request()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (ssoLoginApi.data) {
|
||||
store.dispatch(loginSuccess(ssoLoginApi.data))
|
||||
navigate(location.state?.path || '/chatflows')
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ssoLoginApi.data])
|
||||
|
||||
useEffect(() => {
|
||||
if (ssoLoginApi.error) {
|
||||
if (ssoLoginApi.error?.response?.status === 401 && ssoLoginApi.error?.response?.data.redirectUrl) {
|
||||
window.location.href = ssoLoginApi.error.response.data.redirectUrl
|
||||
} else {
|
||||
setAuthError(ssoLoginApi.error.message)
|
||||
}
|
||||
}
|
||||
}, [ssoLoginApi.error])
|
||||
|
||||
useEffect(() => {
|
||||
if (getDefaultProvidersApi.data && getDefaultProvidersApi.data.providers) {
|
||||
//data is an array of objects, store only the provider attribute
|
||||
setConfiguredSsoProviders(getDefaultProvidersApi.data.providers.map((provider) => provider))
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getDefaultProvidersApi.data])
|
||||
|
||||
useEffect(() => {
|
||||
if (registerApi.data) {
|
||||
setLoading(false)
|
||||
setAuthError(undefined)
|
||||
setConfirmPassword('')
|
||||
setPassword('')
|
||||
setToken('')
|
||||
setUsername('')
|
||||
setEmail('')
|
||||
if (isEnterpriseLicensed) {
|
||||
setSuccessMsg('Registration Successful. You will be redirected to the sign in page shortly.')
|
||||
} else if (isCloud) {
|
||||
setSuccessMsg('To complete your registration, please click on the verification link we sent to your email address')
|
||||
}
|
||||
setTimeout(() => {
|
||||
navigate('/signin')
|
||||
}, 3000)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [registerApi.data])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
maxHeight: '100vh',
|
||||
overflowY: 'auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
padding: '24px'
|
||||
}}
|
||||
>
|
||||
<Stack flexDirection='column' sx={{ width: '480px', gap: 3 }}>
|
||||
{authError && (
|
||||
<Alert icon={<IconExclamationCircle />} variant='filled' severity='error'>
|
||||
{authError.split(', ').length > 0 ? (
|
||||
<List dense sx={{ py: 0 }}>
|
||||
{authError.split(', ').map((error, index) => (
|
||||
<ListItemText key={index} primary={error} primaryTypographyProps={{ color: '#fff !important' }} />
|
||||
))}
|
||||
</List>
|
||||
) : (
|
||||
authError
|
||||
)}
|
||||
</Alert>
|
||||
)}
|
||||
{successMsg && (
|
||||
<Alert icon={<IconCircleCheck />} variant='filled' severity='success'>
|
||||
{successMsg}
|
||||
</Alert>
|
||||
)}
|
||||
<Stack sx={{ gap: 1 }}>
|
||||
<Typography variant='h1'>Sign Up</Typography>
|
||||
<Typography variant='body2' sx={{ color: theme.palette.grey[600] }}>
|
||||
Already have an account?{' '}
|
||||
<Link style={{ color: theme.palette.primary.main }} to='/signin'>
|
||||
Sign In
|
||||
</Link>
|
||||
.
|
||||
</Typography>
|
||||
</Stack>
|
||||
<form onSubmit={register} data-rewardful>
|
||||
<Stack sx={{ width: '100%', flexDirection: 'column', alignItems: 'left', justifyContent: 'center', gap: 2 }}>
|
||||
<Box>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<Typography>
|
||||
Full Name<span style={{ color: 'red' }}> *</span>
|
||||
</Typography>
|
||||
<div style={{ flexGrow: 1 }}></div>
|
||||
</div>
|
||||
<Input
|
||||
inputParam={usernameInput}
|
||||
placeholder='Display Name'
|
||||
onChange={(newValue) => setUsername(newValue)}
|
||||
value={username}
|
||||
showDialog={false}
|
||||
/>
|
||||
<Typography variant='caption'>
|
||||
<i>Is used for display purposes only.</i>
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<Typography>
|
||||
Email<span style={{ color: 'red' }}> *</span>
|
||||
</Typography>
|
||||
<div style={{ flexGrow: 1 }}></div>
|
||||
</div>
|
||||
<Input
|
||||
inputParam={emailInput}
|
||||
onChange={(newValue) => setEmail(newValue)}
|
||||
value={email}
|
||||
showDialog={false}
|
||||
/>
|
||||
<Typography variant='caption'>
|
||||
<i>Kindly use a valid email address. Will be used as login id.</i>
|
||||
</Typography>
|
||||
</Box>
|
||||
{isEnterpriseLicensed && (
|
||||
<Box>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<Typography>
|
||||
Invite Code<span style={{ color: 'red' }}> *</span>
|
||||
</Typography>
|
||||
<div style={{ flexGrow: 1 }}></div>
|
||||
</div>
|
||||
<OutlinedInput
|
||||
fullWidth
|
||||
type='string'
|
||||
placeholder='Paste in the invite code.'
|
||||
multiline={false}
|
||||
inputParam={inviteCodeInput}
|
||||
onChange={(e) => setToken(e.target.value)}
|
||||
value={token}
|
||||
/>
|
||||
<Typography variant='caption'>
|
||||
<i>Please copy the token you would have received in your email.</i>
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<Box>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<Typography>
|
||||
Password<span style={{ color: 'red' }}> *</span>
|
||||
</Typography>
|
||||
<div style={{ flexGrow: 1 }}></div>
|
||||
</div>
|
||||
<Input inputParam={passwordInput} onChange={(newValue) => setPassword(newValue)} value={password} />
|
||||
<Typography variant='caption'>
|
||||
<i>
|
||||
Password must be at least 8 characters long and contain at least one lowercase letter, one uppercase
|
||||
letter, one digit, and one special character (@$!%*?&-).
|
||||
</i>
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<Typography>
|
||||
Confirm Password<span style={{ color: 'red' }}> *</span>
|
||||
</Typography>
|
||||
<div style={{ flexGrow: 1 }}></div>
|
||||
</div>
|
||||
<Input
|
||||
inputParam={confirmPasswordInput}
|
||||
onChange={(newValue) => setConfirmPassword(newValue)}
|
||||
value={confirmPassword}
|
||||
/>
|
||||
<Typography variant='caption'>
|
||||
<i>Confirm your password. Must match the password typed above.</i>
|
||||
</Typography>
|
||||
</Box>
|
||||
<StyledButton variant='contained' style={{ borderRadius: 12, height: 40, marginRight: 5 }} type='submit'>
|
||||
Create Account
|
||||
</StyledButton>
|
||||
{configuredSsoProviders.length > 0 && <Divider sx={{ width: '100%' }}>OR</Divider>}
|
||||
{configuredSsoProviders &&
|
||||
configuredSsoProviders.map(
|
||||
(ssoProvider) =>
|
||||
//https://learn.microsoft.com/en-us/entra/identity-platform/howto-add-branding-in-apps
|
||||
ssoProvider === 'azure' && (
|
||||
<Button
|
||||
key={ssoProvider}
|
||||
variant='outlined'
|
||||
style={{ borderRadius: 12, height: 45, marginRight: 5, lineHeight: 0 }}
|
||||
onClick={() => signInWithSSO(ssoProvider)}
|
||||
startIcon={
|
||||
<Icon>
|
||||
<img src={AzureSSOLoginIcon} alt={'MicrosoftSSO'} width={20} height={20} />
|
||||
</Icon>
|
||||
}
|
||||
>
|
||||
Sign In With Microsoft
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
{configuredSsoProviders &&
|
||||
configuredSsoProviders.map(
|
||||
(ssoProvider) =>
|
||||
ssoProvider === 'google' && (
|
||||
<Button
|
||||
key={ssoProvider}
|
||||
variant='outlined'
|
||||
style={{ borderRadius: 12, height: 45, marginRight: 5, lineHeight: 0 }}
|
||||
onClick={() => signInWithSSO(ssoProvider)}
|
||||
startIcon={
|
||||
<Icon>
|
||||
<img src={GoogleSSOLoginIcon} alt={'GoogleSSO'} width={20} height={20} />
|
||||
</Icon>
|
||||
}
|
||||
>
|
||||
Sign In With Google
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
{configuredSsoProviders &&
|
||||
configuredSsoProviders.map(
|
||||
(ssoProvider) =>
|
||||
ssoProvider === 'auth0' && (
|
||||
<Button
|
||||
key={ssoProvider}
|
||||
variant='outlined'
|
||||
style={{ borderRadius: 12, height: 45, marginRight: 5, lineHeight: 0 }}
|
||||
onClick={() => signInWithSSO(ssoProvider)}
|
||||
startIcon={
|
||||
<Icon>
|
||||
<img src={Auth0SSOLoginIcon} alt={'Auth0SSO'} width={20} height={20} />
|
||||
</Icon>
|
||||
}
|
||||
>
|
||||
Sign In With Auth0 by Okta
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
{configuredSsoProviders &&
|
||||
configuredSsoProviders.map(
|
||||
(ssoProvider) =>
|
||||
ssoProvider === 'github' && (
|
||||
<Button
|
||||
key={ssoProvider}
|
||||
variant='outlined'
|
||||
style={{ borderRadius: 12, height: 45, marginRight: 5, lineHeight: 0 }}
|
||||
onClick={() => signInWithSSO(ssoProvider)}
|
||||
startIcon={
|
||||
<Icon>
|
||||
<img src={GithubSSOLoginIcon} alt={'GithubSSO'} width={20} height={20} />
|
||||
</Icon>
|
||||
}
|
||||
>
|
||||
Sign In With Github
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
</Stack>
|
||||
</form>
|
||||
</Stack>
|
||||
</Box>
|
||||
{loading && <BackdropLoader open={loading} />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default RegisterPage
|
||||
@@ -0,0 +1,257 @@
|
||||
import { useState } from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom'
|
||||
|
||||
// material-ui
|
||||
import { Alert, Box, Button, OutlinedInput, Stack, Typography, useTheme } from '@mui/material'
|
||||
|
||||
// project imports
|
||||
import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '@/store/actions'
|
||||
import { StyledButton } from '@/ui-component/button/StyledButton'
|
||||
import MainCard from '@/ui-component/cards/MainCard'
|
||||
import { Input } from '@/ui-component/input/Input'
|
||||
import { BackdropLoader } from '@/ui-component/loading/BackdropLoader'
|
||||
|
||||
// API
|
||||
import accountApi from '@/api/account.api'
|
||||
|
||||
// utils
|
||||
import useNotifier from '@/utils/useNotifier'
|
||||
import { validatePassword } from '@/utils/validation'
|
||||
|
||||
// Icons
|
||||
import { IconExclamationCircle, IconX } from '@tabler/icons-react'
|
||||
|
||||
// ==============================|| ResetPasswordPage ||============================== //
|
||||
|
||||
const ResetPasswordPage = () => {
|
||||
const theme = useTheme()
|
||||
useNotifier()
|
||||
const navigate = useNavigate()
|
||||
const dispatch = useDispatch()
|
||||
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
||||
|
||||
const emailInput = {
|
||||
label: 'Email',
|
||||
name: 'email',
|
||||
type: 'email',
|
||||
placeholder: 'user@company.com'
|
||||
}
|
||||
|
||||
const passwordInput = {
|
||||
label: 'Password',
|
||||
name: 'password',
|
||||
type: 'password',
|
||||
placeholder: '********'
|
||||
}
|
||||
|
||||
const confirmPasswordInput = {
|
||||
label: 'Confirm Password',
|
||||
name: 'confirmPassword',
|
||||
type: 'password',
|
||||
placeholder: '********'
|
||||
}
|
||||
|
||||
const resetPasswordInput = {
|
||||
label: 'Reset Token',
|
||||
name: 'resetToken',
|
||||
type: 'text'
|
||||
}
|
||||
|
||||
const [params] = useSearchParams()
|
||||
const token = params.get('token')
|
||||
|
||||
const [emailVal, setEmailVal] = useState('')
|
||||
const [newPasswordVal, setNewPasswordVal] = useState('')
|
||||
const [confirmPasswordVal, setConfirmPasswordVal] = useState('')
|
||||
const [tokenVal, setTokenVal] = useState(token ?? '')
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [authErrors, setAuthErrors] = useState([])
|
||||
|
||||
const goLogin = () => {
|
||||
navigate('/signin', { replace: true })
|
||||
}
|
||||
|
||||
const validateAndSubmit = async (event) => {
|
||||
event.preventDefault()
|
||||
const validationErrors = []
|
||||
setAuthErrors([])
|
||||
if (!tokenVal) {
|
||||
validationErrors.push('Token cannot be left blank!')
|
||||
}
|
||||
if (newPasswordVal !== confirmPasswordVal) {
|
||||
validationErrors.push('New Password and Confirm Password do not match.')
|
||||
}
|
||||
const passwordErrors = validatePassword(newPasswordVal)
|
||||
if (passwordErrors.length > 0) {
|
||||
validationErrors.push(...passwordErrors)
|
||||
}
|
||||
if (validationErrors.length > 0) {
|
||||
setAuthErrors(validationErrors)
|
||||
return
|
||||
}
|
||||
const body = {
|
||||
user: {
|
||||
email: emailVal,
|
||||
tempToken: tokenVal,
|
||||
password: newPasswordVal
|
||||
}
|
||||
}
|
||||
setLoading(true)
|
||||
try {
|
||||
const updateResponse = await accountApi.resetPassword(body)
|
||||
setAuthErrors([])
|
||||
setLoading(false)
|
||||
if (updateResponse.data) {
|
||||
enqueueSnackbar({
|
||||
message: 'Password reset successful',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'success',
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
setEmailVal('')
|
||||
setTokenVal('')
|
||||
setNewPasswordVal('')
|
||||
setConfirmPasswordVal('')
|
||||
goLogin()
|
||||
}
|
||||
} catch (error) {
|
||||
setLoading(false)
|
||||
setAuthErrors([typeof error.response.data === 'object' ? error.response.data.message : error.response.data])
|
||||
enqueueSnackbar({
|
||||
message: `Failed to reset password!`,
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
persist: true,
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<MainCard>
|
||||
<Stack flexDirection='column' sx={{ maxWidth: '480px', gap: 3 }}>
|
||||
{authErrors && authErrors.length > 0 && (
|
||||
<Alert icon={<IconExclamationCircle />} variant='filled' severity='error'>
|
||||
<ul style={{ margin: 0 }}>
|
||||
{authErrors.map((msg, key) => (
|
||||
<li key={key}>{msg}</li>
|
||||
))}
|
||||
</ul>
|
||||
</Alert>
|
||||
)}
|
||||
<Stack sx={{ gap: 1 }}>
|
||||
<Typography variant='h1'>Reset Password</Typography>
|
||||
<Typography variant='body2' sx={{ color: theme.palette.grey[600] }}>
|
||||
<Link style={{ color: theme.palette.primary.main }} to='/signin'>
|
||||
Back to Login
|
||||
</Link>
|
||||
.
|
||||
</Typography>
|
||||
</Stack>
|
||||
<form onSubmit={validateAndSubmit}>
|
||||
<Stack sx={{ width: '100%', flexDirection: 'column', alignItems: 'left', justifyContent: 'center', gap: 2 }}>
|
||||
<Box>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<Typography>
|
||||
Email<span style={{ color: 'red' }}> *</span>
|
||||
</Typography>
|
||||
<Typography align='left'></Typography>
|
||||
<div style={{ flexGrow: 1 }}></div>
|
||||
</div>
|
||||
<Input
|
||||
inputParam={emailInput}
|
||||
onChange={(newValue) => setEmailVal(newValue)}
|
||||
value={emailVal}
|
||||
showDialog={false}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<Typography>
|
||||
Reset Token<span style={{ color: 'red' }}> *</span>
|
||||
</Typography>
|
||||
<div style={{ flexGrow: 1 }}></div>
|
||||
</div>
|
||||
<OutlinedInput
|
||||
fullWidth
|
||||
type='string'
|
||||
placeholder='Paste in the reset token.'
|
||||
multiline={true}
|
||||
rows={3}
|
||||
inputParam={resetPasswordInput}
|
||||
onChange={(e) => setTokenVal(e.target.value)}
|
||||
value={tokenVal}
|
||||
sx={{ mt: '8px' }}
|
||||
/>
|
||||
<Typography variant='caption'>
|
||||
<i>Please copy the token you received in your email.</i>
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<Typography>
|
||||
New Password<span style={{ color: 'red' }}> *</span>
|
||||
</Typography>
|
||||
<Typography align='left'></Typography>
|
||||
<div style={{ flexGrow: 1 }}></div>
|
||||
</div>
|
||||
<Input
|
||||
inputParam={passwordInput}
|
||||
onChange={(newValue) => setNewPasswordVal(newValue)}
|
||||
value={newPasswordVal}
|
||||
showDialog={false}
|
||||
/>
|
||||
<Typography variant='caption'>
|
||||
<i>
|
||||
Password must be at least 8 characters long and contain at least one lowercase letter, one uppercase
|
||||
letter, one digit, and one special character (@$!%*?&-).
|
||||
</i>
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<Typography>
|
||||
Confirm Password<span style={{ color: 'red' }}> *</span>
|
||||
</Typography>
|
||||
<div style={{ flexGrow: 1 }}></div>
|
||||
</div>
|
||||
<Input
|
||||
inputParam={confirmPasswordInput}
|
||||
onChange={(newValue) => setConfirmPasswordVal(newValue)}
|
||||
value={confirmPasswordVal}
|
||||
showDialog={false}
|
||||
/>
|
||||
<Typography variant='caption'>
|
||||
<i>Confirm your new password. Must match the password typed above.</i>
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<StyledButton variant='contained' style={{ borderRadius: 12, height: 40, marginRight: 5 }} type='submit'>
|
||||
Update Password
|
||||
</StyledButton>
|
||||
</Stack>
|
||||
</form>
|
||||
</Stack>
|
||||
</MainCard>
|
||||
{loading && <BackdropLoader open={loading} />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ResetPasswordPage
|
||||
@@ -0,0 +1,351 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom'
|
||||
|
||||
// material-ui
|
||||
import { Stack, useTheme, Typography, Box, Alert, Button, Divider, Icon } from '@mui/material'
|
||||
import { IconExclamationCircle } from '@tabler/icons-react'
|
||||
import { LoadingButton } from '@mui/lab'
|
||||
|
||||
// project imports
|
||||
import MainCard from '@/ui-component/cards/MainCard'
|
||||
import { Input } from '@/ui-component/input/Input'
|
||||
|
||||
// Hooks
|
||||
import useApi from '@/hooks/useApi'
|
||||
import { useConfig } from '@/store/context/ConfigContext'
|
||||
|
||||
// API
|
||||
import authApi from '@/api/auth'
|
||||
import accountApi from '@/api/account.api'
|
||||
import loginMethodApi from '@/api/loginmethod'
|
||||
import ssoApi from '@/api/sso'
|
||||
|
||||
// utils
|
||||
import useNotifier from '@/utils/useNotifier'
|
||||
|
||||
// store
|
||||
import { loginSuccess, logoutSuccess } from '@/store/reducers/authSlice'
|
||||
import { store } from '@/store'
|
||||
|
||||
// icons
|
||||
import AzureSSOLoginIcon from '@/assets/images/microsoft-azure.svg'
|
||||
import GoogleSSOLoginIcon from '@/assets/images/google.svg'
|
||||
import Auth0SSOLoginIcon from '@/assets/images/auth0.svg'
|
||||
import GithubSSOLoginIcon from '@/assets/images/github.svg'
|
||||
|
||||
// ==============================|| SignInPage ||============================== //
|
||||
|
||||
const SignInPage = () => {
|
||||
const theme = useTheme()
|
||||
useSelector((state) => state.customization)
|
||||
useNotifier()
|
||||
const { isEnterpriseLicensed, isCloud, isOpenSource } = useConfig()
|
||||
|
||||
const usernameInput = {
|
||||
label: 'Username',
|
||||
name: 'username',
|
||||
type: 'email',
|
||||
placeholder: 'user@company.com'
|
||||
}
|
||||
const passwordInput = {
|
||||
label: 'Password',
|
||||
name: 'password',
|
||||
type: 'password',
|
||||
placeholder: '********'
|
||||
}
|
||||
const [usernameVal, setUsernameVal] = useState('')
|
||||
const [passwordVal, setPasswordVal] = useState('')
|
||||
const [configuredSsoProviders, setConfiguredSsoProviders] = useState([])
|
||||
const [authError, setAuthError] = useState(undefined)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [showResendButton, setShowResendButton] = useState(false)
|
||||
const [successMessage, setSuccessMessage] = useState('')
|
||||
|
||||
const loginApi = useApi(authApi.login)
|
||||
const ssoLoginApi = useApi(ssoApi.ssoLogin)
|
||||
const getDefaultProvidersApi = useApi(loginMethodApi.getDefaultLoginMethods)
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const resendVerificationApi = useApi(accountApi.resendVerificationEmail)
|
||||
|
||||
const doLogin = (event) => {
|
||||
event.preventDefault()
|
||||
setLoading(true)
|
||||
const body = {
|
||||
email: usernameVal,
|
||||
password: passwordVal
|
||||
}
|
||||
loginApi.request(body)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (loginApi.error) {
|
||||
setLoading(false)
|
||||
if (loginApi.error.response.status === 401 && loginApi.error.response.data.redirectUrl) {
|
||||
window.location.href = loginApi.error.response.data.data.redirectUrl
|
||||
} else {
|
||||
setAuthError(loginApi.error.response.data.message)
|
||||
}
|
||||
}
|
||||
}, [loginApi.error])
|
||||
|
||||
useEffect(() => {
|
||||
store.dispatch(logoutSuccess())
|
||||
if (!isOpenSource) {
|
||||
getDefaultProvidersApi.request()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
// Parse the "user" query parameter from the URL
|
||||
const queryParams = new URLSearchParams(location.search)
|
||||
const errorData = queryParams.get('error')
|
||||
if (!errorData) return
|
||||
const parsedErrorData = JSON.parse(decodeURIComponent(errorData))
|
||||
setAuthError(parsedErrorData.message)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [location.search])
|
||||
|
||||
useEffect(() => {
|
||||
if (loginApi.data) {
|
||||
setLoading(false)
|
||||
store.dispatch(loginSuccess(loginApi.data))
|
||||
navigate(location.state?.path || '/chatflows')
|
||||
//navigate(0)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [loginApi.data])
|
||||
|
||||
useEffect(() => {
|
||||
if (ssoLoginApi.data) {
|
||||
store.dispatch(loginSuccess(ssoLoginApi.data))
|
||||
navigate(location.state?.path || '/chatflows')
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ssoLoginApi.data])
|
||||
|
||||
useEffect(() => {
|
||||
if (ssoLoginApi.error) {
|
||||
if (ssoLoginApi.error?.response?.status === 401 && ssoLoginApi.error?.response?.data.redirectUrl) {
|
||||
window.location.href = ssoLoginApi.error.response.data.redirectUrl
|
||||
} else {
|
||||
setAuthError(ssoLoginApi.error.message)
|
||||
}
|
||||
}
|
||||
}, [ssoLoginApi.error])
|
||||
|
||||
useEffect(() => {
|
||||
if (getDefaultProvidersApi.data && getDefaultProvidersApi.data.providers) {
|
||||
//data is an array of objects, store only the provider attribute
|
||||
setConfiguredSsoProviders(getDefaultProvidersApi.data.providers.map((provider) => provider))
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getDefaultProvidersApi.data])
|
||||
|
||||
useEffect(() => {
|
||||
if (authError === 'User Email Unverified') {
|
||||
setShowResendButton(true)
|
||||
} else {
|
||||
setShowResendButton(false)
|
||||
}
|
||||
}, [authError])
|
||||
|
||||
const signInWithSSO = (ssoProvider) => {
|
||||
window.location.href = `/api/v1/${ssoProvider}/login`
|
||||
}
|
||||
|
||||
const handleResendVerification = async () => {
|
||||
try {
|
||||
await resendVerificationApi.request({ email: usernameVal })
|
||||
setAuthError(undefined)
|
||||
setSuccessMessage('Verification email has been sent successfully.')
|
||||
setShowResendButton(false)
|
||||
} catch (error) {
|
||||
setAuthError(error.response?.data?.message || 'Failed to send verification email.')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<MainCard maxWidth='sm'>
|
||||
<Stack flexDirection='column' sx={{ width: '480px', gap: 3 }}>
|
||||
{successMessage && (
|
||||
<Alert variant='filled' severity='success' onClose={() => setSuccessMessage('')}>
|
||||
{successMessage}
|
||||
</Alert>
|
||||
)}
|
||||
{authError && (
|
||||
<Alert icon={<IconExclamationCircle />} variant='filled' severity='error'>
|
||||
{authError}
|
||||
</Alert>
|
||||
)}
|
||||
{showResendButton && (
|
||||
<Stack sx={{ gap: 1 }}>
|
||||
<Button variant='text' onClick={handleResendVerification}>
|
||||
Resend Verification Email
|
||||
</Button>
|
||||
</Stack>
|
||||
)}
|
||||
<Stack sx={{ gap: 1 }}>
|
||||
<Typography variant='h1'>Sign In</Typography>
|
||||
{isCloud && (
|
||||
<Typography variant='body2' sx={{ color: theme.palette.grey[600] }}>
|
||||
Don't have an account?{' '}
|
||||
<Link style={{ color: `${theme.palette.primary.main}` }} to='/register'>
|
||||
Sign up for free
|
||||
</Link>
|
||||
.
|
||||
</Typography>
|
||||
)}
|
||||
{isEnterpriseLicensed && (
|
||||
<Typography variant='body2' sx={{ color: theme.palette.grey[600] }}>
|
||||
Have an invite code?{' '}
|
||||
<Link style={{ color: `${theme.palette.primary.main}` }} to='/register'>
|
||||
Sign up for an account
|
||||
</Link>
|
||||
.
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
<form onSubmit={doLogin}>
|
||||
<Stack sx={{ width: '100%', flexDirection: 'column', alignItems: 'left', justifyContent: 'center', gap: 2 }}>
|
||||
<Box sx={{ p: 0 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<Typography>
|
||||
Email<span style={{ color: 'red' }}> *</span>
|
||||
</Typography>
|
||||
<div style={{ flexGrow: 1 }}></div>
|
||||
</div>
|
||||
<Input
|
||||
inputParam={usernameInput}
|
||||
onChange={(newValue) => setUsernameVal(newValue)}
|
||||
value={usernameVal}
|
||||
showDialog={false}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ p: 0 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<Typography>
|
||||
Password<span style={{ color: 'red' }}> *</span>
|
||||
</Typography>
|
||||
<div style={{ flexGrow: 1 }}></div>
|
||||
</div>
|
||||
<Input inputParam={passwordInput} onChange={(newValue) => setPasswordVal(newValue)} value={passwordVal} />
|
||||
<Typography variant='body2' sx={{ color: theme.palette.grey[600], mt: 1, textAlign: 'right' }}>
|
||||
<Link style={{ color: theme.palette.primary.main }} to='/forgot-password'>
|
||||
Forgot password?
|
||||
</Link>
|
||||
</Typography>
|
||||
{isCloud && (
|
||||
<Typography variant='body2' sx={{ color: theme.palette.grey[600], mt: 1, textAlign: 'right' }}>
|
||||
<a
|
||||
href='https://docs.flowiseai.com/migration-guide/cloud-migration'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
style={{ color: theme.palette.primary.main }}
|
||||
>
|
||||
Migrate from existing account?
|
||||
</a>
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<LoadingButton
|
||||
loading={loading}
|
||||
variant='contained'
|
||||
style={{ borderRadius: 12, height: 40, marginRight: 5 }}
|
||||
type='submit'
|
||||
>
|
||||
Login
|
||||
</LoadingButton>
|
||||
{configuredSsoProviders && configuredSsoProviders.length > 0 && <Divider sx={{ width: '100%' }}>OR</Divider>}
|
||||
{configuredSsoProviders &&
|
||||
configuredSsoProviders.map(
|
||||
(ssoProvider) =>
|
||||
//https://learn.microsoft.com/en-us/entra/identity-platform/howto-add-branding-in-apps
|
||||
ssoProvider === 'azure' && (
|
||||
<Button
|
||||
key={ssoProvider}
|
||||
variant='outlined'
|
||||
style={{ borderRadius: 12, height: 45, marginRight: 5, lineHeight: 0 }}
|
||||
onClick={() => signInWithSSO(ssoProvider)}
|
||||
startIcon={
|
||||
<Icon>
|
||||
<img src={AzureSSOLoginIcon} alt={'MicrosoftSSO'} width={20} height={20} />
|
||||
</Icon>
|
||||
}
|
||||
>
|
||||
Sign In With Microsoft
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
{configuredSsoProviders &&
|
||||
configuredSsoProviders.map(
|
||||
(ssoProvider) =>
|
||||
ssoProvider === 'google' && (
|
||||
<Button
|
||||
key={ssoProvider}
|
||||
variant='outlined'
|
||||
style={{ borderRadius: 12, height: 45, marginRight: 5, lineHeight: 0 }}
|
||||
onClick={() => signInWithSSO(ssoProvider)}
|
||||
startIcon={
|
||||
<Icon>
|
||||
<img src={GoogleSSOLoginIcon} alt={'GoogleSSO'} width={20} height={20} />
|
||||
</Icon>
|
||||
}
|
||||
>
|
||||
Sign In With Google
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
{configuredSsoProviders &&
|
||||
configuredSsoProviders.map(
|
||||
(ssoProvider) =>
|
||||
ssoProvider === 'auth0' && (
|
||||
<Button
|
||||
key={ssoProvider}
|
||||
variant='outlined'
|
||||
style={{ borderRadius: 12, height: 45, marginRight: 5, lineHeight: 0 }}
|
||||
onClick={() => signInWithSSO(ssoProvider)}
|
||||
startIcon={
|
||||
<Icon>
|
||||
<img src={Auth0SSOLoginIcon} alt={'Auth0SSO'} width={20} height={20} />
|
||||
</Icon>
|
||||
}
|
||||
>
|
||||
Sign In With Auth0 by Okta
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
{configuredSsoProviders &&
|
||||
configuredSsoProviders.map(
|
||||
(ssoProvider) =>
|
||||
ssoProvider === 'github' && (
|
||||
<Button
|
||||
key={ssoProvider}
|
||||
variant='outlined'
|
||||
style={{ borderRadius: 12, height: 45, marginRight: 5, lineHeight: 0 }}
|
||||
onClick={() => signInWithSSO(ssoProvider)}
|
||||
startIcon={
|
||||
<Icon>
|
||||
<img src={GithubSSOLoginIcon} alt={'GithubSSO'} width={20} height={20} />
|
||||
</Icon>
|
||||
}
|
||||
>
|
||||
Sign In With Github
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
</Stack>
|
||||
</form>
|
||||
</Stack>
|
||||
</MainCard>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SignInPage
|
||||