mirror of https://github.com/langgenius/dify.git
app online user list
This commit is contained in:
parent
86a9a51952
commit
b937fc8978
|
|
@ -1044,7 +1044,7 @@ class WorkflowOnlineUsersApi(Resource):
|
|||
|
||||
workflow_ids = [id.strip() for id in args["workflow_ids"].split(",")]
|
||||
|
||||
results = {}
|
||||
results = []
|
||||
for workflow_id in workflow_ids:
|
||||
users_json = redis_client.hgetall(f"workflow_online_users:{workflow_id}")
|
||||
|
||||
|
|
@ -1054,7 +1054,7 @@ class WorkflowOnlineUsersApi(Resource):
|
|||
users.append(json.loads(user_info_json))
|
||||
except Exception:
|
||||
continue
|
||||
results[workflow_id] = users
|
||||
results.append({"workflow_id": workflow_id, "users": users})
|
||||
|
||||
return {"data": results}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from flask_restx import fields
|
||||
|
||||
online_user_partial_fields = {
|
||||
"id": fields.String,
|
||||
"user_id": fields.String,
|
||||
"username": fields.String,
|
||||
"avatar": fields.String,
|
||||
"sid": fields.String,
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ import { useGlobalPublicStore } from '@/context/global-public-context'
|
|||
import { formatTime } from '@/utils/time'
|
||||
import { useGetUserCanAccessApp } from '@/service/access-control'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { UserAvatarList } from '@/app/components/base/user-avatar-list'
|
||||
import type { WorkflowOnlineUser } from '@/models/app'
|
||||
|
||||
const EditAppModal = dynamic(() => import('@/app/components/explore/create-app-modal'), {
|
||||
ssr: false,
|
||||
|
|
@ -55,9 +57,10 @@ const AccessControl = dynamic(() => import('@/app/components/app/app-access-cont
|
|||
export type AppCardProps = {
|
||||
app: App
|
||||
onRefresh?: () => void
|
||||
onlineUsers?: WorkflowOnlineUser[]
|
||||
}
|
||||
|
||||
const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
const AppCard = ({ app, onRefresh, onlineUsers = [] }: AppCardProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
||||
|
|
@ -331,6 +334,19 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
|||
return `${t('datasetDocuments.segment.editedAt')} ${timeText}`
|
||||
}, [app.updated_at, app.created_at])
|
||||
|
||||
const onlineUserAvatars = useMemo(() => {
|
||||
if (!onlineUsers.length)
|
||||
return []
|
||||
|
||||
return onlineUsers
|
||||
.map(user => ({
|
||||
id: user.user_id || user.sid || '',
|
||||
name: user.username || 'User',
|
||||
avatar_url: user.avatar || undefined,
|
||||
}))
|
||||
.filter(user => !!user.id)
|
||||
}, [onlineUsers])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
|
|
@ -375,6 +391,11 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
|||
<RiVerifiedBadgeLine className='h-4 w-4 text-text-quaternary' />
|
||||
</Tooltip>}
|
||||
</div>
|
||||
<div>
|
||||
{onlineUserAvatars.length > 0 && (
|
||||
<UserAvatarList users={onlineUserAvatars} maxVisible={3} size={20} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='title-wrapper h-[90px] px-[14px] text-xs leading-normal text-text-tertiary'>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import {
|
||||
useRouter,
|
||||
} from 'next/navigation'
|
||||
import useSWRInfinite from 'swr/infinite'
|
||||
import useSWR from 'swr'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDebounceFn } from 'ahooks'
|
||||
import {
|
||||
|
|
@ -19,8 +20,8 @@ import AppCard from './app-card'
|
|||
import NewAppCard from './new-app-card'
|
||||
import useAppsQueryState from './hooks/use-apps-query-state'
|
||||
import { useDSLDragDrop } from './hooks/use-dsl-drag-drop'
|
||||
import type { AppListResponse } from '@/models/app'
|
||||
import { fetchAppList } from '@/service/apps'
|
||||
import type { AppListResponse, WorkflowOnlineUser } from '@/models/app'
|
||||
import { fetchAppList, fetchWorkflowOnlineUsers } from '@/service/apps'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { CheckModal } from '@/hooks/use-pay'
|
||||
|
|
@ -112,6 +113,37 @@ const List = () => {
|
|||
},
|
||||
)
|
||||
|
||||
const apps = useMemo(() => data?.flatMap(page => page.data) ?? [], [data])
|
||||
|
||||
const workflowIds = useMemo(() => {
|
||||
const ids = new Set<string>()
|
||||
apps.forEach((appItem) => {
|
||||
const workflowId = appItem.id
|
||||
if (!workflowId)
|
||||
return
|
||||
|
||||
if (appItem.mode === 'workflow' || appItem.mode === 'advanced-chat')
|
||||
ids.add(workflowId)
|
||||
})
|
||||
return Array.from(ids)
|
||||
}, [apps])
|
||||
|
||||
const { data: onlineUsersByWorkflow, mutate: refreshOnlineUsers } = useSWR<Record<string, WorkflowOnlineUser[]>>(
|
||||
workflowIds.length ? { workflowIds } : null,
|
||||
fetchWorkflowOnlineUsers,
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!workflowIds.length)
|
||||
return
|
||||
|
||||
const timer = window.setInterval(() => {
|
||||
refreshOnlineUsers()
|
||||
}, 10000)
|
||||
|
||||
return () => window.clearInterval(timer)
|
||||
}, [workflowIds.join(','), refreshOnlineUsers])
|
||||
|
||||
const anchorRef = useRef<HTMLDivElement>(null)
|
||||
const options = [
|
||||
{ value: 'all', text: t('app.types.all'), icon: <RiApps2Line className='mr-1 h-[14px] w-[14px]' /> },
|
||||
|
|
@ -213,7 +245,12 @@ const List = () => {
|
|||
{isCurrentWorkspaceEditor
|
||||
&& <NewAppCard ref={newAppCardRef} onSuccess={mutate} selectedAppType={activeTab} />}
|
||||
{data.map(({ data: apps }) => apps.map(app => (
|
||||
<AppCard key={app.id} app={app} onRefresh={mutate} />
|
||||
<AppCard
|
||||
key={app.id}
|
||||
app={app}
|
||||
onRefresh={mutate}
|
||||
onlineUsers={onlineUsersByWorkflow?.[app.id] ?? []}
|
||||
/>
|
||||
)))}
|
||||
</div>
|
||||
: <div className='relative grid grow grid-cols-1 content-start gap-4 overflow-hidden px-12 pt-2 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6'>
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ export const UserAvatarList: FC<UserAvatarListProps> = memo(({
|
|||
)}
|
||||
{shouldShowCount && remainingCount > 0 && (
|
||||
<div
|
||||
className={'flex items-center justify-center rounded-full bg-components-panel-on-panel-item-bg text-[10px] leading-none text-text-secondary ring-2 ring-components-panel-bg'}
|
||||
className={'flex items-center justify-center rounded-full bg-gray-500 text-[10px] leading-none text-white ring-2 ring-components-panel-bg'}
|
||||
style={{
|
||||
zIndex: 0,
|
||||
width: size,
|
||||
|
|
|
|||
|
|
@ -159,6 +159,20 @@ export type AppVoicesListResponse = [{
|
|||
value: string
|
||||
}]
|
||||
|
||||
export type WorkflowOnlineUser = {
|
||||
user_id?: string
|
||||
username?: string
|
||||
avatar?: string | null
|
||||
sid?: string
|
||||
}
|
||||
|
||||
export type WorkflowOnlineUsersResponse = {
|
||||
data: Record<string, WorkflowOnlineUser[]> | Array<{
|
||||
workflow_id: string
|
||||
users: WorkflowOnlineUser[]
|
||||
}>
|
||||
}
|
||||
|
||||
export type TracingStatus = {
|
||||
enabled: boolean
|
||||
tracing_provider: TracingProvider | null
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Fetcher } from 'swr'
|
||||
import { del, get, patch, post, put } from './base'
|
||||
import type { ApiKeysListResponse, AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDailyMessagesResponse, AppDetailResponse, AppListResponse, AppStatisticsResponse, AppTemplatesResponse, AppTokenCostsResponse, AppVoicesListResponse, CreateApiKeyResponse, DSLImportMode, DSLImportResponse, GenerationIntroductionResponse, TracingConfig, TracingStatus, UpdateAppModelConfigResponse, UpdateAppSiteCodeResponse, UpdateOpenAIKeyResponse, ValidateOpenAIKeyResponse, WorkflowDailyConversationsResponse } from '@/models/app'
|
||||
import type { ApiKeysListResponse, AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDailyMessagesResponse, AppDetailResponse, AppListResponse, AppStatisticsResponse, AppTemplatesResponse, AppTokenCostsResponse, AppVoicesListResponse, CreateApiKeyResponse, DSLImportMode, DSLImportResponse, GenerationIntroductionResponse, TracingConfig, TracingStatus, UpdateAppModelConfigResponse, UpdateAppSiteCodeResponse, UpdateOpenAIKeyResponse, ValidateOpenAIKeyResponse, WorkflowDailyConversationsResponse, WorkflowOnlineUser, WorkflowOnlineUsersResponse } from '@/models/app'
|
||||
import type { CommonResponse } from '@/models/common'
|
||||
import type { AppIconType, AppMode, ModelConfig } from '@/types/app'
|
||||
import type { TracingProvider } from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type'
|
||||
|
|
@ -9,6 +9,31 @@ export const fetchAppList: Fetcher<AppListResponse, { url: string; params?: Reco
|
|||
return get<AppListResponse>(url, { params })
|
||||
}
|
||||
|
||||
export const fetchWorkflowOnlineUsers: Fetcher<Record<string, WorkflowOnlineUser[]>, { workflowIds: string[] }> = async ({ workflowIds }) => {
|
||||
if (!workflowIds.length)
|
||||
return {}
|
||||
|
||||
const params = { workflow_ids: workflowIds.join(',') }
|
||||
const response = await get<WorkflowOnlineUsersResponse>('apps/workflows/online-users', { params })
|
||||
|
||||
if (!response || !response.data)
|
||||
return {}
|
||||
|
||||
if (Array.isArray(response.data)) {
|
||||
return response.data.reduce<Record<string, WorkflowOnlineUser[]>>((acc, item) => {
|
||||
if (item?.workflow_id)
|
||||
acc[item.workflow_id] = item.users || []
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
return Object.entries(response.data).reduce<Record<string, WorkflowOnlineUser[]>>((acc, [workflowId, users]) => {
|
||||
if (workflowId)
|
||||
acc[workflowId] = users || []
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
export const fetchAppDetail: Fetcher<AppDetailResponse, { url: string; id: string }> = ({ url, id }) => {
|
||||
return get<AppDetailResponse>(`${url}/${id}`)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue