Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
D
dify
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
ai-tech
dify
Commits
2c77a74c
Unverified
Commit
2c77a74c
authored
Aug 15, 2023
by
Matri
Committed by
GitHub
Aug 15, 2023
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: frontend permission check (#784)
parent
440cf633
Changes
19
Show whitespace changes
Inline
Side-by-side
Showing
19 changed files
with
186 additions
and
74 deletions
+186
-74
workspace_service.py
api/services/workspace_service.py
+14
-3
layout.tsx
...p/(commonLayout)/app/(appDetailLayout)/[appId]/layout.tsx
+14
-7
AppCard.tsx
web/app/(commonLayout)/apps/AppCard.tsx
+4
-2
Apps.tsx
web/app/(commonLayout)/apps/Apps.tsx
+4
-2
Datasets.tsx
web/app/(commonLayout)/datasets/Datasets.tsx
+3
-2
appCard.tsx
web/app/components/app/overview/appCard.tsx
+22
-12
share-link.tsx
web/app/components/app/overview/share-link.tsx
+4
-2
secret-key-modal.tsx
web/app/components/develop/secret-key/secret-key-modal.tsx
+10
-8
index.tsx
...unt-setting/data-source-page/data-source-notion/index.tsx
+15
-5
Operate.tsx
...mponents/header/account-setting/key-validator/Operate.tsx
+10
-8
index.tsx
...components/header/account-setting/key-validator/index.tsx
+4
-1
index.tsx
.../components/header/account-setting/members-page/index.tsx
+7
-9
SerpapiPlugin.tsx
...ents/header/account-setting/plugin-page/SerpapiPlugin.tsx
+3
-0
index.tsx
web/app/components/header/app-selector/index.tsx
+4
-1
index.tsx
web/app/components/header/index.tsx
+5
-1
index.tsx
web/app/components/header/nav/nav-selector/index.tsx
+4
-2
app-context.tsx
web/context/app-context.tsx
+47
-9
common.ts
web/models/common.ts
+7
-0
common.ts
web/service/common.ts
+5
-0
No files found.
api/services/workspace_service.py
View file @
2c77a74c
from
flask_login
import
current_user
from
extensions.ext_database
import
db
from
models.account
import
Tenant
from
models.account
import
Tenant
,
TenantAccountJoin
from
models.provider
import
Provider
class
WorkspaceService
:
@
classmethod
def
get_tenant_info
(
cls
,
tenant
:
Tenant
):
if
not
tenant
:
return
None
tenant_info
=
{
'id'
:
tenant
.
id
,
'name'
:
tenant
.
name
,
...
...
@@ -13,10 +16,18 @@ class WorkspaceService:
'status'
:
tenant
.
status
,
'created_at'
:
tenant
.
created_at
,
'providers'
:
[],
'in_trial'
:
True
,
'trial_end_reason'
:
None
'in_trail'
:
True
,
'trial_end_reason'
:
None
,
'role'
:
'normal'
,
}
# Get role of user
tenant_account_join
=
db
.
session
.
query
(
TenantAccountJoin
)
.
filter
(
TenantAccountJoin
.
tenant_id
==
tenant
.
id
,
TenantAccountJoin
.
account_id
==
current_user
.
id
)
.
first
()
tenant_info
[
'role'
]
=
tenant_account_join
.
role
# Get providers
providers
=
db
.
session
.
query
(
Provider
)
.
filter
(
Provider
.
tenant_id
==
tenant
.
id
...
...
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout.tsx
View file @
2c77a74c
'use client'
import
type
{
FC
}
from
'react'
import
React
,
{
useEffect
}
from
'react'
import
React
,
{
useEffect
,
useMemo
}
from
'react'
import
cn
from
'classnames'
import
useSWR
from
'swr'
import
{
useTranslation
}
from
'react-i18next'
...
...
@@ -19,6 +19,7 @@ import {
import
s
from
'./style.module.css'
import
AppSideBar
from
'@/app/components/app-sidebar'
import
{
fetchAppDetail
}
from
'@/service/apps'
import
{
useAppContext
}
from
'@/context/app-context'
export
type
IAppDetailLayoutProps
=
{
children
:
React
.
ReactNode
...
...
@@ -31,15 +32,21 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
params
:
{
appId
},
// get appId in path
}
=
props
const
{
t
}
=
useTranslation
()
const
{
isCurrentWorkspaceManager
}
=
useAppContext
()
const
detailParams
=
{
url
:
'/apps'
,
id
:
appId
}
const
{
data
:
response
}
=
useSWR
(
detailParams
,
fetchAppDetail
)
const
navigation
=
[
const
navigation
=
useMemo
(()
=>
{
const
navs
=
[
{
name
:
t
(
'common.appMenus.overview'
),
href
:
`/app/
${
appId
}
/overview`
,
icon
:
ChartBarSquareIcon
,
selectedIcon
:
ChartBarSquareSolidIcon
},
{
name
:
t
(
'common.appMenus.promptEng'
),
href
:
`/app/
${
appId
}
/configuration`
,
icon
:
Cog8ToothIcon
,
selectedIcon
:
Cog8ToothSolidIcon
},
{
name
:
t
(
'common.appMenus.apiAccess'
),
href
:
`/app/
${
appId
}
/develop`
,
icon
:
CommandLineIcon
,
selectedIcon
:
CommandLineSolidIcon
},
{
name
:
t
(
'common.appMenus.logAndAnn'
),
href
:
`/app/
${
appId
}
/logs`
,
icon
:
DocumentTextIcon
,
selectedIcon
:
DocumentTextSolidIcon
},
]
if
(
isCurrentWorkspaceManager
)
navs
.
push
({
name
:
t
(
'common.appMenus.promptEng'
),
href
:
`/app/
${
appId
}
/configuration`
,
icon
:
Cog8ToothIcon
,
selectedIcon
:
Cog8ToothSolidIcon
})
return
navs
},
[
appId
,
isCurrentWorkspaceManager
,
t
])
const
appModeName
=
response
?.
mode
?.
toUpperCase
()
===
'COMPLETION'
?
t
(
'common.appModes.completionApp'
)
:
t
(
'common.appModes.chatApp'
)
useEffect
(()
=>
{
if
(
response
?.
name
)
...
...
web/app/(commonLayout)/apps/AppCard.tsx
View file @
2c77a74c
...
...
@@ -12,7 +12,7 @@ import Confirm from '@/app/components/base/confirm'
import
{
ToastContext
}
from
'@/app/components/base/toast'
import
{
deleteApp
}
from
'@/service/apps'
import
AppIcon
from
'@/app/components/base/app-icon'
import
AppsContext
from
'@/context/app-context'
import
AppsContext
,
{
useAppContext
}
from
'@/context/app-context'
export
type
AppCardProps
=
{
app
:
App
...
...
@@ -25,6 +25,7 @@ const AppCard = ({
}:
AppCardProps
)
=>
{
const
{
t
}
=
useTranslation
()
const
{
notify
}
=
useContext
(
ToastContext
)
const
{
isCurrentWorkspaceManager
}
=
useAppContext
()
const
mutateApps
=
useContextSelector
(
AppsContext
,
state
=>
state
.
mutateApps
)
...
...
@@ -55,7 +56,8 @@ const AppCard = ({
<
div
className=
{
style
.
listItemHeading
}
>
<
div
className=
{
style
.
listItemHeadingContent
}
>
{
app
.
name
}
</
div
>
</
div
>
<
span
className=
{
style
.
deleteAppIcon
}
onClick=
{
onDeleteClick
}
/>
{
isCurrentWorkspaceManager
&&
<
span
className=
{
style
.
deleteAppIcon
}
onClick=
{
onDeleteClick
}
/>
}
</
div
>
<
div
className=
{
style
.
listItemDescription
}
>
{
app
.
model_config
?.
pre_prompt
}
</
div
>
<
div
className=
{
style
.
listItemFooter
}
>
...
...
web/app/(commonLayout)/apps/Apps.tsx
View file @
2c77a74c
...
...
@@ -8,7 +8,7 @@ import AppCard from './AppCard'
import
NewAppCard
from
'./NewAppCard'
import
type
{
AppListResponse
}
from
'@/models/app'
import
{
fetchAppList
}
from
'@/service/apps'
import
{
useSelector
}
from
'@/context/app-context'
import
{
use
AppContext
,
use
Selector
}
from
'@/context/app-context'
import
{
NEED_REFRESH_APP_LIST_KEY
}
from
'@/config'
const
getKey
=
(
pageIndex
:
number
,
previousPageData
:
AppListResponse
)
=>
{
...
...
@@ -19,6 +19,7 @@ const getKey = (pageIndex: number, previousPageData: AppListResponse) => {
const
Apps
=
()
=>
{
const
{
t
}
=
useTranslation
()
const
{
isCurrentWorkspaceManager
}
=
useAppContext
()
const
{
data
,
isLoading
,
setSize
,
mutate
}
=
useSWRInfinite
(
getKey
,
fetchAppList
,
{
revalidateFirstPage
:
false
})
const
loadingStateRef
=
useRef
(
false
)
const
pageContainerRef
=
useSelector
(
state
=>
state
.
pageContainerRef
)
...
...
@@ -55,7 +56,8 @@ const Apps = () => {
{
data
?.
map
(({
data
:
apps
})
=>
apps
.
map
(
app
=>
(
<
AppCard
key=
{
app
.
id
}
app=
{
app
}
onDelete=
{
mutate
}
/>
)))
}
<
NewAppCard
ref=
{
anchorRef
}
onSuccess=
{
mutate
}
/>
{
isCurrentWorkspaceManager
&&
<
NewAppCard
ref=
{
anchorRef
}
onSuccess=
{
mutate
}
/>
}
</
nav
>
)
}
...
...
web/app/(commonLayout)/datasets/Datasets.tsx
View file @
2c77a74c
...
...
@@ -7,7 +7,7 @@ import NewDatasetCard from './NewDatasetCard'
import
DatasetCard
from
'./DatasetCard'
import
type
{
DataSetListResponse
}
from
'@/models/datasets'
import
{
fetchDatasets
}
from
'@/service/datasets'
import
{
useSelector
}
from
'@/context/app-context'
import
{
use
AppContext
,
use
Selector
}
from
'@/context/app-context'
const
getKey
=
(
pageIndex
:
number
,
previousPageData
:
DataSetListResponse
)
=>
{
if
(
!
pageIndex
||
previousPageData
.
has_more
)
...
...
@@ -16,6 +16,7 @@ const getKey = (pageIndex: number, previousPageData: DataSetListResponse) => {
}
const
Datasets
=
()
=>
{
const
{
isCurrentWorkspaceManager
}
=
useAppContext
()
const
{
data
,
isLoading
,
setSize
,
mutate
}
=
useSWRInfinite
(
getKey
,
fetchDatasets
,
{
revalidateFirstPage
:
false
})
const
loadingStateRef
=
useRef
(
false
)
const
pageContainerRef
=
useSelector
(
state
=>
state
.
pageContainerRef
)
...
...
@@ -44,7 +45,7 @@ const Datasets = () => {
{
data
?.
map
(({
data
:
datasets
})
=>
datasets
.
map
(
dataset
=>
(
<
DatasetCard
key=
{
dataset
.
id
}
dataset=
{
dataset
}
onDelete=
{
mutate
}
/>),
))
}
<
NewDatasetCard
ref=
{
anchorRef
}
/>
{
isCurrentWorkspaceManager
&&
<
NewDatasetCard
ref=
{
anchorRef
}
/>
}
</
nav
>
)
}
...
...
web/app/components/app/overview/appCard.tsx
View file @
2c77a74c
'use client'
import
type
{
FC
}
from
'react'
import
React
,
{
useState
}
from
'react'
import
React
,
{
use
Memo
,
use
State
}
from
'react'
import
{
Cog8ToothIcon
,
DocumentTextIcon
,
...
...
@@ -22,6 +22,7 @@ import Switch from '@/app/components/base/switch'
import
type
{
AppDetailResponse
}
from
'@/models/app'
import
'./style.css'
import
{
AppType
}
from
'@/types/app'
import
{
useAppContext
}
from
'@/context/app-context'
export
type
IAppCardProps
=
{
className
?:
string
...
...
@@ -48,22 +49,30 @@ function AppCard({
}:
IAppCardProps
)
{
const
router
=
useRouter
()
const
pathname
=
usePathname
()
const
{
currentWorkspace
,
isCurrentWorkspaceManager
}
=
useAppContext
()
const
[
showSettingsModal
,
setShowSettingsModal
]
=
useState
(
false
)
const
[
showShareModal
,
setShowShareModal
]
=
useState
(
false
)
const
[
showEmbedded
,
setShowEmbedded
]
=
useState
(
false
)
const
[
showCustomizeModal
,
setShowCustomizeModal
]
=
useState
(
false
)
const
{
t
}
=
useTranslation
()
const
OPERATIONS_MAP
=
{
const
OPERATIONS_MAP
=
useMemo
(()
=>
{
const
operationsMap
=
{
webapp
:
[
{
opName
:
t
(
'appOverview.overview.appInfo.preview'
),
opIcon
:
RocketLaunchIcon
},
{
opName
:
t
(
'appOverview.overview.appInfo.share.entry'
),
opIcon
:
ShareIcon
},
appInfo
.
mode
===
AppType
.
chat
?
{
opName
:
t
(
'appOverview.overview.appInfo.embedded.entry'
),
opIcon
:
EmbedIcon
}
:
false
,
{
opName
:
t
(
'appOverview.overview.appInfo.settings.entry'
),
opIcon
:
Cog8ToothIcon
},
].
filter
(
item
=>
!!
item
),
]
as
{
opName
:
string
;
opIcon
:
any
}[],
api
:
[{
opName
:
t
(
'appOverview.overview.apiInfo.doc'
),
opIcon
:
DocumentTextIcon
}],
app
:
[],
}
if
(
appInfo
.
mode
===
AppType
.
chat
)
operationsMap
.
webapp
.
push
({
opName
:
t
(
'appOverview.overview.appInfo.embedded.entry'
),
opIcon
:
EmbedIcon
})
if
(
isCurrentWorkspaceManager
)
operationsMap
.
webapp
.
push
({
opName
:
t
(
'appOverview.overview.appInfo.settings.entry'
),
opIcon
:
Cog8ToothIcon
})
return
operationsMap
},
[
isCurrentWorkspaceManager
,
appInfo
,
t
])
const
isApp
=
cardType
===
'app'
||
cardType
===
'webapp'
const
basicName
=
isApp
?
appInfo
?.
site
?.
title
:
t
(
'appOverview.overview.apiInfo.title'
)
...
...
@@ -129,7 +138,7 @@ function AppCard({
<
Tag
className=
"mr-2"
color=
{
runningStatus
?
'green'
:
'yellow'
}
>
{
runningStatus
?
t
(
'appOverview.overview.status.running'
)
:
t
(
'appOverview.overview.status.disable'
)
}
</
Tag
>
<
Switch
defaultValue=
{
runningStatus
}
onChange=
{
onChangeStatus
}
/>
<
Switch
defaultValue=
{
runningStatus
}
onChange=
{
onChangeStatus
}
disabled=
{
currentWorkspace
?.
role
===
'normal'
}
/>
</
div
>
</
div
>
<
div
className=
"flex flex-col justify-center py-2"
>
...
...
@@ -200,6 +209,7 @@ function AppCard({
onClose=
{
()
=>
setShowShareModal
(
false
)
}
linkUrl=
{
appUrl
}
onGenerateCode=
{
onGenerateCode
}
regeneratable=
{
isCurrentWorkspaceManager
}
/>
<
SettingsModal
appInfo=
{
appInfo
}
...
...
web/app/components/app/overview/share-link.tsx
View file @
2c77a74c
...
...
@@ -17,6 +17,7 @@ type IShareLinkProps = {
onClose
:
()
=>
void
onGenerateCode
:
()
=>
Promise
<
void
>
linkUrl
:
string
regeneratable
?:
boolean
}
const
prefixShare
=
'appOverview.overview.appInfo.share'
...
...
@@ -26,6 +27,7 @@ const ShareLinkModal: FC<IShareLinkProps> = ({
isShow
,
onClose
,
onGenerateCode
,
regeneratable
,
})
=>
{
const
[
genLoading
,
setGenLoading
]
=
useState
(
false
)
const
[
isCopied
,
setIsCopied
]
=
useState
(
false
)
...
...
@@ -51,7 +53,7 @@ const ShareLinkModal: FC<IShareLinkProps> = ({
<
LinkIcon
className=
'w-4 h-4 mr-2'
/>
{
t
(
`${prefixShare}.${isCopied ? 'linkCopied' : 'copyLink'}`
)
}
</
Button
>
<
Button
className=
'w-32 !px-0'
onClick=
{
async
()
=>
{
{
regeneratable
&&
<
Button
className=
'w-32 !px-0'
onClick=
{
async
()
=>
{
setGenLoading
(
true
)
await
onGenerateCode
()
setGenLoading
(
false
)
...
...
@@ -59,7 +61,7 @@ const ShareLinkModal: FC<IShareLinkProps> = ({
}
}
>
<
ArrowPathIcon
className=
{
`w-4 h-4 mr-2 ${genLoading ? 'generateLogo' : ''}`
}
/>
{
t
(
`${prefixShare}.regenerate`
)
}
</
Button
>
</
Button
>
}
</
div
>
</
Modal
>
}
...
...
web/app/components/develop/secret-key/secret-key-modal.tsx
View file @
2c77a74c
...
...
@@ -18,6 +18,7 @@ import Tooltip from '@/app/components/base/tooltip'
import
Loading
from
'@/app/components/base/loading'
import
Confirm
from
'@/app/components/base/confirm'
import
I18n
from
'@/context/i18n'
import
{
useAppContext
}
from
'@/context/app-context'
type
ISecretKeyModalProps
=
{
isShow
:
boolean
...
...
@@ -31,6 +32,7 @@ const SecretKeyModal = ({
onClose
,
}:
ISecretKeyModalProps
)
=>
{
const
{
t
}
=
useTranslation
()
const
{
currentWorkspace
,
isCurrentWorkspaceManager
}
=
useAppContext
()
const
[
showConfirmDelete
,
setShowConfirmDelete
]
=
useState
(
false
)
const
[
isVisible
,
setVisible
]
=
useState
(
false
)
const
[
newKey
,
setNewKey
]
=
useState
<
CreateApiKeyResponse
|
undefined
>
(
undefined
)
...
...
@@ -118,11 +120,13 @@ const SecretKeyModal = ({
setCopyValue
(
api
.
token
)
}
}
></
div
>
</
Tooltip
>
<
div
className=
{
`flex items-center justify-center flex-shrink-0 w-6 h-6 rounded-lg cursor-pointer ${s.trashIcon}`
}
onClick=
{
()
=>
{
{
isCurrentWorkspaceManager
&&
<
div
className=
{
`flex items-center justify-center flex-shrink-0 w-6 h-6 rounded-lg cursor-pointer ${s.trashIcon}`
}
onClick=
{
()
=>
{
setDelKeyId
(
api
.
id
)
setShowConfirmDelete
(
true
)
}
}
>
</
div
>
}
</
div
>
</
div
>
))
}
...
...
@@ -131,9 +135,7 @@ const SecretKeyModal = ({
)
}
<
div
className=
'flex'
>
<
Button
type=
'default'
className=
{
`flex flex-shrink-0 mt-4 ${s.autoWidth}`
}
onClick=
{
()
=>
onCreate
()
}
>
<
Button
type=
'default'
className=
{
`flex flex-shrink-0 mt-4 ${s.autoWidth}`
}
onClick=
{
onCreate
}
disabled=
{
!
currentWorkspace
||
currentWorkspace
.
role
===
'normal'
}
>
<
PlusIcon
className=
'flex flex-shrink-0 w-4 h-4'
/>
<
div
className=
'text-xs font-medium text-gray-800'
>
{
t
(
'appApi.apiKeyModal.createNewSecretKey'
)
}
</
div
>
</
Button
>
...
...
web/app/components/header/account-setting/data-source-page/data-source-notion/index.tsx
View file @
2c77a74c
...
...
@@ -8,6 +8,7 @@ import s from './style.module.css'
import
NotionIcon
from
'@/app/components/base/notion-icon'
import
{
apiPrefix
}
from
'@/config'
import
type
{
DataSourceNotion
as
TDataSourceNotion
}
from
'@/models/common'
import
{
useAppContext
}
from
'@/context/app-context'
type
DataSourceNotionProps
=
{
workspaces
:
TDataSourceNotion
[]
...
...
@@ -16,6 +17,8 @@ const DataSourceNotion = ({
workspaces
,
}:
DataSourceNotionProps
)
=>
{
const
{
t
}
=
useTranslation
()
const
{
isCurrentWorkspaceManager
}
=
useAppContext
()
const
connected
=
!!
workspaces
.
length
return
(
...
...
@@ -35,18 +38,25 @@ const DataSourceNotion = ({
}
</
div
>
{
!
connected
connected
?
(
<
Link
className=
'flex items-center ml-3 px-3 h-7 bg-white border border-gray-200 rounded-md text-xs font-medium text-gray-700 cursor-pointer'
href=
{
`${apiPrefix}/oauth/data-source/notion`
}
>
className=
{
`flex items-center ml-3 px-3 h-7 bg-white border border-gray-200
rounded-md text-xs font-medium text-gray-700
${isCurrentWorkspaceManager ? 'cursor-pointer' : 'grayscale opacity-50 cursor-default'}`
}
href=
{
isCurrentWorkspaceManager
?
`${apiPrefix}/oauth/data-source/notion`
:
'/'
}
>
{
t
(
'common.dataSource.connect'
)
}
</
Link
>
)
:
(
<
Link
href=
{
`${apiPrefix}/oauth/data-source/notion`
}
className=
'flex items-center px-3 h-7 bg-white border-[0.5px] border-gray-200 text-xs font-medium text-primary-600 rounded-md cursor-pointer'
>
href=
{
isCurrentWorkspaceManager
?
`${apiPrefix}/oauth/data-source/notion`
:
'/'
}
className=
{
`flex items-center px-3 h-7 bg-white border-[0.5px] border-gray-200 text-xs font-medium text-primary-600 rounded-md
${isCurrentWorkspaceManager ? 'cursor-pointer' : 'grayscale opacity-50 cursor-default'}`
}
>
<
PlusIcon
className=
'w-[14px] h-[14px] mr-[5px]'
/>
{
t
(
'common.dataSource.notion.addWorkspace'
)
}
</
Link
>
...
...
web/app/components/header/account-setting/key-validator/Operate.tsx
View file @
2c77a74c
...
...
@@ -5,6 +5,7 @@ import type { Status } from './declarations'
type
OperateProps
=
{
isOpen
:
boolean
status
:
Status
disabled
?:
boolean
onCancel
:
()
=>
void
onSave
:
()
=>
void
onAdd
:
()
=>
void
...
...
@@ -14,6 +15,7 @@ type OperateProps = {
const
Operate
=
({
isOpen
,
status
,
disabled
,
onCancel
,
onSave
,
onAdd
,
...
...
@@ -44,10 +46,10 @@ const Operate = ({
if
(
status
===
'add'
)
{
return
(
<
div
className=
'
px-3 h-[28px] bg-white border border-gray-200 rounded-md cursor-pointer
text-xs font-medium text-gray-700 flex items-center
'
onClick=
{
onAdd
}
>
<
div
className=
{
`
px-3 h-[28px] bg-white border border-gray-200 rounded-md cursor-pointer
text-xs font-medium text-gray-700 flex items-center
${disabled && 'opacity-50 cursor-default'}
}
`
}
onClick=
{
()
=>
!
disabled
&&
onAdd
()
}
>
{
t
(
'common.provider.addKey'
)
}
</
div
>
)
...
...
@@ -69,10 +71,10 @@ const Operate = ({
<
Indicator
color=
'green'
className=
'mr-4'
/>
)
}
<
div
className=
'
px-3 h-[28px] bg-white border border-gray-200 rounded-md cursor-pointer
text-xs font-medium text-gray-700 flex items-center
'
onClick=
{
onEdit
}
>
<
div
className=
{
`
px-3 h-[28px] bg-white border border-gray-200 rounded-md cursor-pointer
text-xs font-medium text-gray-700 flex items-center
${disabled && 'opacity-50 cursor-default'}
}
`
}
onClick=
{
()
=>
!
disabled
&&
onEdit
()
}
>
{
t
(
'common.provider.editKey'
)
}
</
div
>
</
div
>
...
...
web/app/components/header/account-setting/key-validator/index.tsx
View file @
2c77a74c
...
...
@@ -13,6 +13,7 @@ export type KeyValidatorProps = {
forms
:
Form
[]
keyFrom
:
KeyFrom
onSave
:
(
v
:
ValidateValue
)
=>
Promise
<
boolean
|
undefined
>
disabled
?:
boolean
}
const
KeyValidator
=
({
...
...
@@ -22,6 +23,7 @@ const KeyValidator = ({
forms
,
keyFrom
,
onSave
,
disabled
,
}:
KeyValidatorProps
)
=>
{
const
triggerKey
=
`plugins/
${
type
}
`
const
{
eventEmitter
}
=
useEventEmitterContextContext
()
...
...
@@ -85,10 +87,11 @@ const KeyValidator = ({
onSave=
{
handleSave
}
onAdd=
{
handleAdd
}
onEdit=
{
handleEdit
}
disabled=
{
disabled
}
/>
</
div
>
{
isOpen
&&
(
isOpen
&&
!
disabled
&&
(
<
div
className=
'px-4 py-3'
>
{
forms
.
map
(
form
=>
(
...
...
web/app/components/header/account-setting/members-page/index.tsx
View file @
2c77a74c
...
...
@@ -16,9 +16,9 @@ import { fetchMembers } from '@/service/common'
import
I18n
from
'@/context/i18n'
import
{
useAppContext
}
from
'@/context/app-context'
import
Avatar
from
'@/app/components/base/avatar'
import
{
useWorkspacesContext
}
from
'@/context/workspace-context'
dayjs
.
extend
(
relativeTime
)
const
MembersPage
=
()
=>
{
const
{
t
}
=
useTranslation
()
const
RoleMap
=
{
...
...
@@ -27,15 +27,13 @@ const MembersPage = () => {
normal
:
t
(
'common.members.normal'
),
}
const
{
locale
}
=
useContext
(
I18n
)
const
{
userProfile
}
=
useAppContext
()
const
{
userProfile
,
currentWorkspace
,
isCurrentWorkspaceManager
}
=
useAppContext
()
const
{
data
,
mutate
}
=
useSWR
({
url
:
'/workspaces/current/members'
},
fetchMembers
)
const
[
inviteModalVisible
,
setInviteModalVisible
]
=
useState
(
false
)
const
[
invitationLink
,
setInvitationLink
]
=
useState
(
''
)
const
[
invitedModalVisible
,
setInvitedModalVisible
]
=
useState
(
false
)
const
accounts
=
data
?.
accounts
||
[]
const
owner
=
accounts
.
filter
(
account
=>
account
.
role
===
'owner'
)?.[
0
]?.
email
===
userProfile
.
email
const
{
workspaces
}
=
useWorkspacesContext
()
const
currentWrokspace
=
workspaces
.
filter
(
item
=>
item
.
current
)?.[
0
]
return
(
<>
...
...
@@ -43,14 +41,14 @@ const MembersPage = () => {
<
div
className=
'flex items-center mb-4 p-3 bg-gray-50 rounded-2xl'
>
<
div
className=
{
cn
(
s
[
'logo-icon'
],
'shrink-0'
)
}
></
div
>
<
div
className=
'grow mx-2'
>
<
div
className=
'text-sm font-medium text-gray-900'
>
{
currentW
ro
kspace
?.
name
}
</
div
>
<
div
className=
'text-sm font-medium text-gray-900'
>
{
currentW
or
kspace
?.
name
}
</
div
>
<
div
className=
'text-xs text-gray-500'
>
{
t
(
'common.userProfile.workspace'
)
}
</
div
>
</
div
>
<
div
className=
'
shrink-0 flex items-center py-[7px] px-3 border-[0.5px] border-gray-200
<
div
className=
{
`
shrink-0 flex items-center py-[7px] px-3 border-[0.5px] border-gray-200
text-[13px] font-medium text-primary-600 bg-white
shadow-xs rounded-lg
cursor-pointer
'
onClick=
{
()
=>
setInviteModalVisible
(
true
)
}
>
shadow-xs rounded-lg
${isCurrentWorkspaceManager ? 'cursor-pointer' : 'grayscale opacity-50 cursor-default'}`
}
onClick=
{
()
=>
isCurrentWorkspaceManager
&&
setInviteModalVisible
(
true
)
}
>
<
UserPlusIcon
className=
'w-4 h-4 mr-2 '
/>
{
t
(
'common.members.invite'
)
}
</
div
>
...
...
web/app/components/header/account-setting/plugin-page/SerpapiPlugin.tsx
View file @
2c77a74c
...
...
@@ -6,6 +6,7 @@ import type { Form, ValidateValue } from '../key-validator/declarations'
import
{
updatePluginKey
,
validatePluginKey
}
from
'./utils'
import
{
useToastContext
}
from
'@/app/components/base/toast'
import
type
{
PluginProvider
}
from
'@/models/common'
import
{
useAppContext
}
from
'@/context/app-context'
type
SerpapiPluginProps
=
{
plugin
:
PluginProvider
...
...
@@ -16,6 +17,7 @@ const SerpapiPlugin = ({
onUpdate
,
}:
SerpapiPluginProps
)
=>
{
const
{
t
}
=
useTranslation
()
const
{
isCurrentWorkspaceManager
}
=
useAppContext
()
const
{
notify
}
=
useToastContext
()
const
forms
:
Form
[]
=
[{
...
...
@@ -70,6 +72,7 @@ const SerpapiPlugin = ({
link
:
'https://serpapi.com/manage-api-key'
,
}
}
onSave=
{
handleSave
}
disabled=
{
!
isCurrentWorkspaceManager
}
/>
)
}
...
...
web/app/components/header/app-selector/index.tsx
View file @
2c77a74c
...
...
@@ -8,6 +8,7 @@ import Indicator from '../indicator'
import
type
{
AppDetailResponse
}
from
'@/models/app'
import
NewAppDialog
from
'@/app/(commonLayout)/apps/NewAppDialog'
import
AppIcon
from
'@/app/components/base/app-icon'
import
{
useAppContext
}
from
'@/context/app-context'
type
IAppSelectorProps
=
{
appItems
:
AppDetailResponse
[]
...
...
@@ -16,6 +17,7 @@ type IAppSelectorProps = {
export
default
function
AppSelector
({
appItems
,
curApp
}:
IAppSelectorProps
)
{
const
router
=
useRouter
()
const
{
isCurrentWorkspaceManager
}
=
useAppContext
()
const
[
showNewAppDialog
,
setShowNewAppDialog
]
=
useState
(
false
)
const
{
t
}
=
useTranslation
()
...
...
@@ -77,7 +79,7 @@ export default function AppSelector({ appItems, curApp }: IAppSelectorProps) {
))
}
</
div
>)
}
<
Menu
.
Item
>
{
isCurrentWorkspaceManager
&&
<
Menu
.
Item
>
<
div
className=
'p-1'
onClick=
{
()
=>
setShowNewAppDialog
(
true
)
}
>
<
div
className=
'flex items-center h-12 rounded-lg cursor-pointer hover:bg-gray-100'
...
...
@@ -95,6 +97,7 @@ export default function AppSelector({ appItems, curApp }: IAppSelectorProps) {
</
div
>
</
div
>
</
Menu
.
Item
>
}
</
Menu
.
Items
>
</
Transition
>
</
Menu
>
...
...
web/app/components/header/index.tsx
View file @
2c77a74c
'use client'
import
Link
from
'next/link'
import
AccountDropdown
from
'./account-dropdown'
import
AppNav
from
'./app-nav'
...
...
@@ -8,6 +10,7 @@ import GithubStar from './github-star'
import
PluginNav
from
'./plugin-nav'
import
s
from
'./index.module.css'
import
{
WorkspaceProvider
}
from
'@/context/workspace-context'
import
{
useAppContext
}
from
'@/context/app-context'
const
navClassName
=
`
flex items-center relative mr-3 px-3 h-8 rounded-xl
...
...
@@ -16,6 +19,7 @@ const navClassName = `
`
const
Header
=
()
=>
{
const
{
isCurrentWorkspaceManager
}
=
useAppContext
()
return
(
<>
<
div
className=
'flex items-center'
>
...
...
@@ -29,7 +33,7 @@ const Header = () => {
<
ExploreNav
className=
{
navClassName
}
/>
<
AppNav
/>
<
PluginNav
className=
{
navClassName
}
/>
<
DatasetNav
/>
{
isCurrentWorkspaceManager
&&
<
DatasetNav
/>
}
</
div
>
<
div
className=
'flex items-center flex-shrink-0'
>
<
EnvNav
/>
...
...
web/app/components/header/nav/nav-selector/index.tsx
View file @
2c77a74c
...
...
@@ -6,6 +6,7 @@ import { useRouter } from 'next/navigation'
import
{
debounce
}
from
'lodash-es'
import
Indicator
from
'../../indicator'
import
AppIcon
from
'@/app/components/base/app-icon'
import
{
useAppContext
}
from
'@/context/app-context'
type
NavItem
=
{
id
:
string
...
...
@@ -29,6 +30,7 @@ const itemClassName = `
const
NavSelector
=
({
curNav
,
navs
,
createText
,
onCreate
,
onLoadmore
}:
INavSelectorProps
)
=>
{
const
router
=
useRouter
()
const
{
isCurrentWorkspaceManager
}
=
useAppContext
()
const
handleScroll
=
useCallback
(
debounce
((
e
)
=>
{
if
(
typeof
onLoadmore
===
'function'
)
{
...
...
@@ -81,7 +83,7 @@ const NavSelector = ({ curNav, navs, createText, onCreate, onLoadmore }: INavSel
))
}
</
div
>
<
Menu
.
Item
>
{
isCurrentWorkspaceManager
&&
<
Menu
.
Item
>
<
div
className=
'p-1'
onClick=
{
onCreate
}
>
<
div
className=
'flex items-center h-12 rounded-lg cursor-pointer hover:bg-gray-100'
...
...
@@ -98,7 +100,7 @@ const NavSelector = ({ curNav, navs, createText, onCreate, onLoadmore }: INavSel
<
div
className=
'font-normal text-[14px] text-gray-700'
>
{
createText
}
</
div
>
</
div
>
</
div
>
</
Menu
.
Item
>
</
Menu
.
Item
>
}
</
Menu
.
Items
>
</
Menu
>
</
div
>
...
...
web/context/app-context.tsx
View file @
2c77a74c
'use client'
import
{
createRef
,
use
Effect
,
useRef
,
useState
}
from
'react'
import
{
createRef
,
use
Callback
,
useEffect
,
useMemo
,
useRef
,
useState
}
from
'react'
import
useSWR
from
'swr'
import
{
createContext
,
useContext
,
useContextSelector
}
from
'use-context-selector'
import
type
{
FC
,
ReactNode
}
from
'react'
import
{
fetchAppList
}
from
'@/service/apps'
import
Loading
from
'@/app/components/base/loading'
import
{
fetchLanggeniusVersion
,
fetchUserProfile
}
from
'@/service/common'
import
{
fetch
CurrentWorkspace
,
fetch
LanggeniusVersion
,
fetchUserProfile
}
from
'@/service/common'
import
type
{
App
}
from
'@/types/app'
import
type
{
LangGeniusVersionResponse
,
UserProfileResponse
}
from
'@/models/common'
import
type
{
ICurrentWorkspace
,
LangGeniusVersionResponse
,
UserProfileResponse
}
from
'@/models/common'
export
type
AppContextValue
=
{
apps
:
App
[]
mutateApps
:
()
=>
void
mutateApps
:
VoidFunction
userProfile
:
UserProfileResponse
mutateUserProfile
:
()
=>
void
mutateUserProfile
:
VoidFunction
currentWorkspace
:
ICurrentWorkspace
isCurrentWorkspaceManager
:
boolean
mutateCurrentWorkspace
:
VoidFunction
pageContainerRef
:
React
.
RefObject
<
HTMLDivElement
>
langeniusVersionInfo
:
LangGeniusVersionResponse
useSelector
:
typeof
useSelector
...
...
@@ -30,6 +33,17 @@ const initialLangeniusVersionInfo = {
can_auto_update
:
false
,
}
const
initialWorkspaceInfo
:
ICurrentWorkspace
=
{
id
:
''
,
name
:
''
,
plan
:
''
,
status
:
''
,
created_at
:
0
,
role
:
'normal'
,
providers
:
[],
in_trail
:
true
,
}
const
AppContext
=
createContext
<
AppContextValue
>
({
apps
:
[],
mutateApps
:
()
=>
{
},
...
...
@@ -40,7 +54,10 @@ const AppContext = createContext<AppContextValue>({
avatar
:
''
,
is_password_set
:
false
,
},
currentWorkspace
:
initialWorkspaceInfo
,
isCurrentWorkspaceManager
:
false
,
mutateUserProfile
:
()
=>
{
},
mutateCurrentWorkspace
:
()
=>
{
},
pageContainerRef
:
createRef
(),
langeniusVersionInfo
:
initialLangeniusVersionInfo
,
useSelector
,
...
...
@@ -59,10 +76,14 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
const
{
data
:
appList
,
mutate
:
mutateApps
}
=
useSWR
({
url
:
'/apps'
,
params
:
{
page
:
1
}
},
fetchAppList
)
const
{
data
:
userProfileResponse
,
mutate
:
mutateUserProfile
}
=
useSWR
({
url
:
'/account/profile'
,
params
:
{}
},
fetchUserProfile
)
const
{
data
:
currentWorkspaceResponse
,
mutate
:
mutateCurrentWorkspace
}
=
useSWR
({
url
:
'/workspaces/current'
,
params
:
{}
},
fetchCurrentWorkspace
)
const
[
userProfile
,
setUserProfile
]
=
useState
<
UserProfileResponse
>
()
const
[
langeniusVersionInfo
,
setLangeniusVersionInfo
]
=
useState
<
LangGeniusVersionResponse
>
(
initialLangeniusVersionInfo
)
const
updateUserProfileAndVersion
=
async
()
=>
{
const
[
currentWorkspace
,
setCurrentWorkspace
]
=
useState
<
ICurrentWorkspace
>
(
initialWorkspaceInfo
)
const
isCurrentWorkspaceManager
=
useMemo
(()
=>
[
'owner'
,
'admin'
].
includes
(
currentWorkspace
.
role
),
[
currentWorkspace
.
role
])
const
updateUserProfileAndVersion
=
useCallback
(
async
()
=>
{
if
(
userProfileResponse
&&
!
userProfileResponse
.
bodyUsed
)
{
const
result
=
await
userProfileResponse
.
json
()
setUserProfile
(
result
)
...
...
@@ -71,16 +92,33 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
const
versionData
=
await
fetchLanggeniusVersion
({
url
:
'/version'
,
params
:
{
current_version
}
})
setLangeniusVersionInfo
({
...
versionData
,
current_version
,
latest_version
:
versionData
.
version
,
current_env
})
}
}
},
[
userProfileResponse
])
useEffect
(()
=>
{
updateUserProfileAndVersion
()
},
[
userProfileResponse
])
},
[
updateUserProfileAndVersion
,
userProfileResponse
])
useEffect
(()
=>
{
if
(
currentWorkspaceResponse
)
setCurrentWorkspace
(
currentWorkspaceResponse
)
},
[
currentWorkspaceResponse
])
if
(
!
appList
||
!
userProfile
)
return
<
Loading
type=
'app'
/>
return
(
<
AppContext
.
Provider
value=
{
{
apps
:
appList
.
data
,
mutateApps
,
userProfile
,
mutateUserProfile
,
pageContainerRef
,
langeniusVersionInfo
,
useSelector
}
}
>
<
AppContext
.
Provider
value=
{
{
apps
:
appList
.
data
,
mutateApps
,
userProfile
,
mutateUserProfile
,
pageContainerRef
,
langeniusVersionInfo
,
useSelector
,
currentWorkspace
,
isCurrentWorkspaceManager
,
mutateCurrentWorkspace
,
}
}
>
<
div
ref=
{
pageContainerRef
}
className=
'relative flex flex-col h-full overflow-auto bg-gray-100'
>
{
children
}
</
div
>
...
...
web/models/common.ts
View file @
2c77a74c
...
...
@@ -118,6 +118,13 @@ export type IWorkspace = {
current
:
boolean
}
export
type
ICurrentWorkspace
=
Omit
<
IWorkspace
,
'current'
>
&
{
role
:
'normal'
|
'admin'
|
'owner'
providers
:
Provider
[]
in_trail
:
boolean
trial_end_reason
?:
string
}
export
type
DataSourceNotionPage
=
{
page_icon
:
null
|
{
type
:
string
|
null
...
...
web/service/common.ts
View file @
2c77a74c
...
...
@@ -2,6 +2,7 @@ import type { Fetcher } from 'swr'
import
{
del
,
get
,
patch
,
post
,
put
}
from
'./base'
import
type
{
AccountIntegrate
,
CommonResponse
,
DataSourceNotion
,
ICurrentWorkspace
,
IWorkspace
,
LangGeniusVersionResponse
,
Member
,
OauthResponse
,
PluginProvider
,
Provider
,
ProviderAnthropicToken
,
ProviderAzureToken
,
SetupStatusResponse
,
TenantInfoResponse
,
UserProfileOriginResponse
,
...
...
@@ -87,6 +88,10 @@ export const fetchFilePreview: Fetcher<{ content: string }, { fileID: string }>
return
get
(
`/files/
${
fileID
}
/preview`
)
as
Promise
<
{
content
:
string
}
>
}
export
const
fetchCurrentWorkspace
:
Fetcher
<
ICurrentWorkspace
,
{
url
:
string
;
params
:
Record
<
string
,
any
>
}
>
=
({
url
,
params
})
=>
{
return
get
(
url
,
{
params
})
as
Promise
<
ICurrentWorkspace
>
}
export
const
fetchWorkspaces
:
Fetcher
<
{
workspaces
:
IWorkspace
[]
},
{
url
:
string
;
params
:
Record
<
string
,
any
>
}
>
=
({
url
,
params
})
=>
{
return
get
(
url
,
{
params
})
as
Promise
<
{
workspaces
:
IWorkspace
[]
}
>
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment