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>
This commit is contained in:
Henry Heng
2025-05-27 14:29:42 +08:00
committed by GitHub
parent e35a126b46
commit 5a37227d14
560 changed files with 62127 additions and 4100 deletions
+1 -1
View File
@@ -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>
+27
View File
@@ -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
}
+9
View File
@@ -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
}
+14
View File
@@ -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
}
+3
View File
@@ -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
}
+26 -13
View File
@@ -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
+30
View File
@@ -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
}
+22
View File
@@ -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
}
+17
View File
@@ -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
}
+10
View File
@@ -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
}
+7
View File
@@ -0,0 +1,7 @@
import client from './client'
const getLogs = (startDate, endDate) => client.get(`/logs?startDate=${startDate}&endDate=${endDate}`)
export default {
getLogs
}
+16
View File
@@ -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
}
+4 -1
View File
@@ -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
}
+7
View File
@@ -0,0 +1,7 @@
import client from './client'
const getSettings = () => client.get('/settings')
export default {
getSettings
}
+7
View File
@@ -0,0 +1,7 @@
import client from '@/api/client'
const getPricingPlans = (body) => client.get(`/pricing`, body)
export default {
getPricingPlans
}
+17
View File
@@ -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
}
+7
View File
@@ -0,0 +1,7 @@
import client from './client'
const ssoLogin = (providerName) => client.get(`/${providerName}/login`)
export default {
ssoLogin
}
+59
View File
@@ -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
}
+30
View File
@@ -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
}
+2
View File
@@ -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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 21 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

+1
View File
@@ -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

+1
View File
@@ -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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

+8 -3
View File
@@ -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
}
+54
View File
@@ -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 }
}
+11 -5
View File
@@ -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>
)
}
+1 -1
View File
@@ -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>
+12 -6
View File
@@ -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'
}
]
}
+249 -75
View File
@@ -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'
}
]
}
]
}
+1 -3
View File
@@ -2,8 +2,6 @@ import dashboard from './dashboard'
// ==============================|| MENU ITEMS ||============================== //
const menuItems = {
export const menuItems = {
items: [dashboard]
}
export default menuItems
+11 -5
View File
@@ -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'
}
]
}
+59
View File
@@ -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
+41 -8
View File
@@ -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>
)
}
]
}
+249 -21
View File
@@ -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 />
}
]
}
+82
View File
@@ -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
}
+3 -2
View File
@@ -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)
}
+11
View File
@@ -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
}
+3 -1
View File
@@ -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' }}>&nbsp;*</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' }}>&nbsp;*</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' }}>&nbsp;*</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'>
&nbsp;
</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
}
}))
+81
View File
@@ -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
+34 -3
View File
@@ -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)
}
+12
View File
@@ -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
+17
View File
@@ -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' }}>&nbsp;*</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' }}>&nbsp;*</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' }}>&nbsp;*</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
File diff suppressed because it is too large Load Diff
+17 -14
View File
@@ -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>
+14 -24
View File
@@ -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>
)
+42 -36
View File
@@ -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}>
+46
View File
@@ -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' }}>&nbsp;*</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
+45
View File
@@ -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
+472
View File
@@ -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' }}>&nbsp;*</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' }}>&nbsp;*</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' }}>&nbsp;*</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' }}>&nbsp;*</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' }}>&nbsp;*</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' }}>&nbsp;*</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' }}>&nbsp;*</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' }}>&nbsp;*</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' }}>&nbsp;*</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
+351
View File
@@ -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&apos;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' }}>&nbsp;*</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' }}>&nbsp;*</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

Some files were not shown because too many files have changed in this diff Show More