diff --git a/web/app/(commonLayout)/datasets/create-from-pipeline/page.tsx b/web/app/(commonLayout)/datasets/create-from-pipeline/page.tsx new file mode 100644 index 0000000000..72f5ecdfd9 --- /dev/null +++ b/web/app/(commonLayout)/datasets/create-from-pipeline/page.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import CreateFromPipeline from '@/app/components/datasets/create-from-pipeline' + +const DatasetCreation = async () => { + return ( + + ) +} + +export default DatasetCreation diff --git a/web/app/components/base/app-icon/index.tsx b/web/app/components/base/app-icon/index.tsx index ac17af1988..7dad4f541d 100644 --- a/web/app/components/base/app-icon/index.tsx +++ b/web/app/components/base/app-icon/index.tsx @@ -1,11 +1,12 @@ 'use client' - -import type { FC } from 'react' +import React, { type FC, useRef } from 'react' import { init } from 'emoji-mart' import data from '@emoji-mart/data' import { cva } from 'class-variance-authority' import type { AppIconType } from '@/types/app' import classNames from '@/utils/classnames' +import { useHover } from 'ahooks' +import { RiEditLine } from '@remixicon/react' init({ data }) @@ -21,7 +22,7 @@ export type AppIconProps = { onClick?: () => void } const appIconVariants = cva( - 'flex items-center justify-center relative text-lg rounded-lg grow-0 shrink-0 overflow-hidden leading-none', + 'flex items-center justify-center relative text-lg rounded-2xl grow-0 shrink-0 overflow-hidden leading-none border-[0.5px] border-divider-regular', { variants: { size: { @@ -54,18 +55,31 @@ const AppIcon: FC = ({ onClick, }) => { const isValidImageIcon = iconType === 'image' && imageUrl + const Icon = (icon && icon !== '') ? : + const wrapperRef = useRef(null) + const isHovering = useHover(wrapperRef) - return - {isValidImageIcon - - ? - : (innerIcon || ((icon && icon !== '') ? : )) - } - + return ( + + { + isValidImageIcon + ? + : (innerIcon || Icon) + } + { + isHovering && ( + + + + ) + } + + ) } -export default AppIcon +export default React.memo(AppIcon) diff --git a/web/app/components/base/app-icon/style.module.css b/web/app/components/base/app-icon/style.module.css deleted file mode 100644 index 151bc6d3fc..0000000000 --- a/web/app/components/base/app-icon/style.module.css +++ /dev/null @@ -1,23 +0,0 @@ -.appIcon { - @apply flex items-center justify-center relative w-9 h-9 text-lg rounded-lg grow-0 shrink-0; -} - -.appIcon.large { - @apply w-10 h-10; -} - -.appIcon.small { - @apply w-8 h-8; -} - -.appIcon.tiny { - @apply w-6 h-6 text-base; -} - -.appIcon.xs { - @apply w-5 h-5 text-base; -} - -.appIcon.rounded { - @apply rounded-full; -} \ No newline at end of file diff --git a/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/dsl-confirm-modal.tsx b/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/dsl-confirm-modal.tsx new file mode 100644 index 0000000000..e6aadaa326 --- /dev/null +++ b/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/dsl-confirm-modal.tsx @@ -0,0 +1,46 @@ +import { useTranslation } from 'react-i18next' +import Modal from '@/app/components/base/modal' +import Button from '@/app/components/base/button' + +type DSLConfirmModalProps = { + versions?: { + importedVersion: string + systemVersion: string + } + onCancel: () => void + onConfirm: () => void + confirmDisabled?: boolean +} +const DSLConfirmModal = ({ + versions = { importedVersion: '', systemVersion: '' }, + onCancel, + onConfirm, + confirmDisabled = false, +}: DSLConfirmModalProps) => { + const { t } = useTranslation() + + return ( + onCancel()} + className='w-[480px]' + > + + {t('app.newApp.appCreateDSLErrorTitle')} + + {t('app.newApp.appCreateDSLErrorPart1')} + {t('app.newApp.appCreateDSLErrorPart2')} + + {t('app.newApp.appCreateDSLErrorPart3')}{versions.importedVersion} + {t('app.newApp.appCreateDSLErrorPart4')}{versions.systemVersion} + + + + onCancel()}>{t('app.newApp.Cancel')} + {t('app.newApp.Confirm')} + + + ) +} + +export default DSLConfirmModal diff --git a/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/index.tsx b/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/index.tsx new file mode 100644 index 0000000000..f3f417039d --- /dev/null +++ b/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/index.tsx @@ -0,0 +1,327 @@ +'use client' +import { useMemo, useRef, useState } from 'react' +import { useRouter } from 'next/navigation' +import { useContext } from 'use-context-selector' +import { useTranslation } from 'react-i18next' +import { RiCloseLine, RiCommandLine, RiCornerDownLeftLine } from '@remixicon/react' +import { useDebounceFn, useKeyPress } from 'ahooks' +import Button from '@/app/components/base/button' +import Input from '@/app/components/base/input' +import Modal from '@/app/components/base/modal' +import { ToastContext } from '@/app/components/base/toast' +import { + importDSL, + importDSLConfirm, +} from '@/service/apps' +import { + DSLImportMode, + DSLImportStatus, +} from '@/models/app' +import { useSelector as useAppContextWithSelector } from '@/context/app-context' +import { useProviderContextSelector } from '@/context/provider-context' +import AppsFull from '@/app/components/billing/apps-full-in-dialog' +import { NEED_REFRESH_APP_LIST_KEY } from '@/config' +import { getRedirection } from '@/utils/app-redirection' +import cn from '@/utils/classnames' +import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks' +import { noop } from 'lodash-es' +import Uploader from './uploader' + +type CreateFromDSLModalProps = { + show: boolean + onSuccess?: () => void + onClose: () => void + activeTab?: string + dslUrl?: string +} + +export enum CreateFromDSLModalTab { + FROM_FILE = 'from-file', + FROM_URL = 'from-url', +} + +const CreateFromDSLModal = ({ + show, + onSuccess, + onClose, + activeTab = CreateFromDSLModalTab.FROM_FILE, + dslUrl = '', +}: CreateFromDSLModalProps) => { + const { push } = useRouter() + const { t } = useTranslation() + const { notify } = useContext(ToastContext) + const [currentFile, setDSLFile] = useState() + const [fileContent, setFileContent] = useState() + const [currentTab, setCurrentTab] = useState(activeTab) + const [dslUrlValue, setDslUrlValue] = useState(dslUrl) + const [showErrorModal, setShowErrorModal] = useState(false) + const [versions, setVersions] = useState<{ importedVersion: string; systemVersion: string }>() + const [importId, setImportId] = useState() + const { handleCheckPluginDependencies } = usePluginDependencies() + + const readFile = (file: File) => { + const reader = new FileReader() + reader.onload = function (event) { + const content = event.target?.result + setFileContent(content as string) + } + reader.readAsText(file) + } + + const handleFile = (file?: File) => { + setDSLFile(file) + if (file) + readFile(file) + if (!file) + setFileContent('') + } + + const isCurrentWorkspaceEditor = useAppContextWithSelector(state => state.isCurrentWorkspaceEditor) + const plan = useProviderContextSelector(state => state.plan) + const enableBilling = useProviderContextSelector(state => state.enableBilling) + const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps) + + const isCreatingRef = useRef(false) + + const onCreate = async () => { + if (currentTab === CreateFromDSLModalTab.FROM_FILE && !currentFile) + return + if (currentTab === CreateFromDSLModalTab.FROM_URL && !dslUrlValue) + return + if (isCreatingRef.current) + return + isCreatingRef.current = true + try { + let response + + if (currentTab === CreateFromDSLModalTab.FROM_FILE) { + response = await importDSL({ + mode: DSLImportMode.YAML_CONTENT, + yaml_content: fileContent || '', + }) + } + if (currentTab === CreateFromDSLModalTab.FROM_URL) { + response = await importDSL({ + mode: DSLImportMode.YAML_URL, + yaml_url: dslUrlValue || '', + }) + } + + if (!response) + return + const { id, status, app_id, app_mode, imported_dsl_version, current_dsl_version } = response + if (status === DSLImportStatus.COMPLETED || status === DSLImportStatus.COMPLETED_WITH_WARNINGS) { + if (onSuccess) + onSuccess() + if (onClose) + onClose() + + notify({ + type: status === DSLImportStatus.COMPLETED ? 'success' : 'warning', + message: t(status === DSLImportStatus.COMPLETED ? 'app.newApp.appCreated' : 'app.newApp.caution'), + children: status === DSLImportStatus.COMPLETED_WITH_WARNINGS && t('app.newApp.appCreateDSLWarning'), + }) + localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') + if (app_id) + await handleCheckPluginDependencies(app_id) + getRedirection(isCurrentWorkspaceEditor, { id: app_id!, mode: app_mode }, push) + } + else if (status === DSLImportStatus.PENDING) { + setVersions({ + importedVersion: imported_dsl_version ?? '', + systemVersion: current_dsl_version ?? '', + }) + if (onClose) + onClose() + setTimeout(() => { + setShowErrorModal(true) + }, 300) + setImportId(id) + } + else { + notify({ type: 'error', message: t('app.newApp.appCreateFailed') }) + } + } + // eslint-disable-next-line unused-imports/no-unused-vars + catch (e) { + notify({ type: 'error', message: t('app.newApp.appCreateFailed') }) + } + finally { + isCreatingRef.current = false + } + } + + const { run: handleCreateApp } = useDebounceFn(onCreate, { wait: 300 }) + + useKeyPress(['meta.enter', 'ctrl.enter'], () => { + if (show && !isAppsFull && ((currentTab === CreateFromDSLModalTab.FROM_FILE && currentFile) || (currentTab === CreateFromDSLModalTab.FROM_URL && dslUrlValue))) + handleCreateApp() + }) + + useKeyPress('esc', () => { + if (show && !showErrorModal) + onClose() + }) + + const onDSLConfirm = async () => { + try { + if (!importId) + return + const response = await importDSLConfirm({ + import_id: importId, + }) + + const { status, app_id, app_mode } = response + + if (status === DSLImportStatus.COMPLETED) { + if (onSuccess) + onSuccess() + if (onClose) + onClose() + + notify({ + type: 'success', + message: t('app.newApp.appCreated'), + }) + if (app_id) + await handleCheckPluginDependencies(app_id) + localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') + getRedirection(isCurrentWorkspaceEditor, { id: app_id!, mode: app_mode }, push) + } + else if (status === DSLImportStatus.FAILED) { + notify({ type: 'error', message: t('app.newApp.appCreateFailed') }) + } + } + // eslint-disable-next-line unused-imports/no-unused-vars + catch (e) { + notify({ type: 'error', message: t('app.newApp.appCreateFailed') }) + } + } + + const tabs = [ + { + key: CreateFromDSLModalTab.FROM_FILE, + label: t('app.importFromDSLFile'), + }, + { + key: CreateFromDSLModalTab.FROM_URL, + label: t('app.importFromDSLUrl'), + }, + ] + + const buttonDisabled = useMemo(() => { + if (isAppsFull) + return true + if (currentTab === CreateFromDSLModalTab.FROM_FILE) + return !currentFile + if (currentTab === CreateFromDSLModalTab.FROM_URL) + return !dslUrlValue + return false + }, [isAppsFull, currentTab, currentFile, dslUrlValue]) + + return ( + <> + + + {t('app.importFromDSL')} + onClose()} + > + + + + + { + tabs.map(tab => ( + setCurrentTab(tab.key)} + > + {tab.label} + { + currentTab === tab.key && ( + + ) + } + + )) + } + + + { + currentTab === CreateFromDSLModalTab.FROM_FILE && ( + + ) + } + { + currentTab === CreateFromDSLModalTab.FROM_URL && ( + + DSL URL + setDslUrlValue(e.target.value)} + /> + + ) + } + + {isAppsFull && ( + + + + )} + + {t('app.newApp.Cancel')} + + {t('app.newApp.Create')} + + + + + + + + setShowErrorModal(false)} + className='w-[480px]' + > + + {t('app.newApp.appCreateDSLErrorTitle')} + + {t('app.newApp.appCreateDSLErrorPart1')} + {t('app.newApp.appCreateDSLErrorPart2')} + + {t('app.newApp.appCreateDSLErrorPart3')}{versions?.importedVersion} + {t('app.newApp.appCreateDSLErrorPart4')}{versions?.systemVersion} + + + + setShowErrorModal(false)}>{t('app.newApp.Cancel')} + {t('app.newApp.Confirm')} + + + > + ) +} + +export default CreateFromDSLModal diff --git a/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/uploader.tsx b/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/uploader.tsx new file mode 100644 index 0000000000..a21d622fac --- /dev/null +++ b/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/uploader.tsx @@ -0,0 +1,149 @@ +'use client' +import type { FC } from 'react' +import React, { useEffect, useRef, useState } from 'react' +import { + RiDeleteBinLine, +} from '@remixicon/react' +import { useTranslation } from 'react-i18next' +import { useContext } from 'use-context-selector' +import { formatFileSize } from '@/utils/format' +import cn from '@/utils/classnames' +import { Yaml as YamlIcon } from '@/app/components/base/icons/src/public/files' +import { ToastContext } from '@/app/components/base/toast' +import { UploadCloud01 } from '@/app/components/base/icons/src/vender/line/general' +import Button from '@/app/components/base/button' + +export type Props = { + file: File | undefined + updateFile: (file?: File) => void + className?: string +} + +const Uploader: FC = ({ + file, + updateFile, + className, +}) => { + const { t } = useTranslation() + const { notify } = useContext(ToastContext) + const [dragging, setDragging] = useState(false) + const dropRef = useRef(null) + const dragRef = useRef(null) + const fileUploader = useRef(null) + + const handleDragEnter = (e: DragEvent) => { + e.preventDefault() + e.stopPropagation() + e.target !== dragRef.current && setDragging(true) + } + const handleDragOver = (e: DragEvent) => { + e.preventDefault() + e.stopPropagation() + } + const handleDragLeave = (e: DragEvent) => { + e.preventDefault() + e.stopPropagation() + e.target === dragRef.current && setDragging(false) + } + const handleDrop = (e: DragEvent) => { + e.preventDefault() + e.stopPropagation() + setDragging(false) + if (!e.dataTransfer) + return + const files = [...e.dataTransfer.files] + if (files.length > 1) { + notify({ type: 'error', message: t('datasetCreation.stepOne.uploader.validation.count') }) + return + } + updateFile(files[0]) + } + const selectHandle = () => { + const originalFile = file + if (fileUploader.current) { + fileUploader.current.value = '' + fileUploader.current.click() + // If no file is selected, restore the original file + fileUploader.current.oncancel = () => updateFile(originalFile) + } + } + const removeFile = () => { + if (fileUploader.current) + fileUploader.current.value = '' + updateFile() + } + const fileChangeHandle = (e: React.ChangeEvent) => { + const currentFile = e.target.files?.[0] + updateFile(currentFile) + } + + useEffect(() => { + const dropArea = dropRef.current + dropArea?.addEventListener('dragenter', handleDragEnter) + dropArea?.addEventListener('dragover', handleDragOver) + dropArea?.addEventListener('dragleave', handleDragLeave) + dropArea?.addEventListener('drop', handleDrop) + return () => { + dropArea?.removeEventListener('dragenter', handleDragEnter) + dropArea?.removeEventListener('dragover', handleDragOver) + dropArea?.removeEventListener('dragleave', handleDragLeave) + dropArea?.removeEventListener('drop', handleDrop) + } + }, []) + + return ( + + + + {!file && ( + + + + + {t('datasetCreation.stepOne.uploader.button')} + + {t('datasetDocuments.list.batchModal.browse')} + + + + {dragging && } + + )} + {file && ( + + + + + + {file.name} + + YAML + ยท + {formatFileSize(file.size)} + + + + {t('datasetCreation.stepOne.uploader.change')} + + + + + + + )} + + + ) +} + +export default React.memo(Uploader) diff --git a/web/app/components/datasets/create-from-pipeline/create-options/create-from-scratch.tsx b/web/app/components/datasets/create-from-pipeline/create-options/create-from-scratch.tsx new file mode 100644 index 0000000000..19e173f433 --- /dev/null +++ b/web/app/components/datasets/create-from-pipeline/create-options/create-from-scratch.tsx @@ -0,0 +1,162 @@ +import AppIcon from '@/app/components/base/app-icon' +import type { AppIconSelection } from '@/app/components/base/app-icon-picker' +import AppIconPicker from '@/app/components/base/app-icon-picker' +import Input from '@/app/components/base/input' +import Textarea from '@/app/components/base/textarea' +import type { AppIconType } from '@/types/app' +import { RiCloseLine } from '@remixicon/react' +import React, { useCallback, useRef, useState } from 'react' +import PermissionSelector from '../../settings/permission-selector' +import { DatasetPermission } from '@/models/datasets' +import { useMembers } from '@/service/use-common' +import Button from '@/app/components/base/button' +import { useTranslation } from 'react-i18next' +import Toast from '@/app/components/base/toast' + +type CreateFromScratchProps = { + onClose: () => void + onCreate: () => void +} + +const DEFAULT_APP_ICON: AppIconSelection = { + type: 'emoji', + icon: '๐', + background: '#FFF4ED', +} + +const CreateFromScratch = ({ + onClose, + onCreate, +}: CreateFromScratchProps) => { + const { t } = useTranslation() + const [name, setName] = useState('') + const [appIcon, setAppIcon] = useState(DEFAULT_APP_ICON) + const [description, setDescription] = useState('') + const [permission, setPermission] = useState(DatasetPermission.onlyMe) + const [showAppIconPicker, setShowAppIconPicker] = useState(false) + const [selectedMemberIDs, setSelectedMemberIDs] = useState([]) + const previousAppIcon = useRef(DEFAULT_APP_ICON) + + const { data: memberList } = useMembers() + + const handleAppNameChange = useCallback((event: React.ChangeEvent) => { + const value = event.target.value + setName(value) + }, []) + + const handleOpenAppIconPicker = useCallback(() => { + setShowAppIconPicker(true) + previousAppIcon.current = appIcon + }, [appIcon]) + + const handleSelectAppIcon = useCallback((icon: AppIconSelection) => { + setAppIcon(icon) + setShowAppIconPicker(false) + }, []) + + const handleCloseAppIconPicker = useCallback(() => { + setAppIcon(previousAppIcon.current) + setShowAppIconPicker(false) + }, []) + + const handleDescriptionChange = useCallback((event: React.ChangeEvent) => { + const value = event.target.value + setDescription(value) + }, []) + + const handlePermissionChange = useCallback((value?: DatasetPermission) => { + setPermission(value!) + }, []) + + const handleCreate = useCallback(() => { + if (!name) { + Toast.notify({ + type: 'error', + message: 'Please enter a name for the Knowledge Base.', + }) + return + } + onCreate() + onClose() + }, [name, onCreate, onClose]) + + return ( + + {/* Header */} + + + Create Knowledge + + + + + + {/* Form */} + + + + Knowledge name & icon + + + + + + Knowledge description + + + + Permissions + + + + {/* Actions */} + + + {t('common.operation.cancel')} + + + {t('common.operation.create')} + + + {showAppIconPicker && ( + + )} + + ) +} + +export default React.memo(CreateFromScratch) diff --git a/web/app/components/datasets/create-from-pipeline/create-options/index.tsx b/web/app/components/datasets/create-from-pipeline/create-options/index.tsx new file mode 100644 index 0000000000..8e56f28738 --- /dev/null +++ b/web/app/components/datasets/create-from-pipeline/create-options/index.tsx @@ -0,0 +1,87 @@ +import React, { useCallback, useMemo, useState } from 'react' +import Item from './item' +import { RiAddCircleFill, RiFileUploadLine } from '@remixicon/react' +import Modal from '@/app/components/base/modal' +import CreateFromScratch from './create-from-scratch' +import { useRouter, useSearchParams } from 'next/navigation' +import CreateFromDSLModal, { CreateFromDSLModalTab } from './create-from-dsl-modal' +import { useProviderContextSelector } from '@/context/provider-context' + +const CreateOptions = () => { + const [showCreateModal, setShowCreateModal] = useState(false) + const [showImportModal, setShowImportModal] = useState(false) + + const onPlanInfoChanged = useProviderContextSelector(state => state.onPlanInfoChanged) + const searchParams = useSearchParams() + const { replace } = useRouter() + const dslUrl = searchParams.get('remoteInstallUrl') || undefined + + const activeTab = useMemo(() => { + if (dslUrl) + return CreateFromDSLModalTab.FROM_URL + + return undefined + }, [dslUrl]) + + const openCreateFromScratch = useCallback(() => { + setShowCreateModal(true) + }, []) + + const closeCreateFromScratch = useCallback(() => { + setShowCreateModal(false) + }, []) + + const handleCreateFromScratch = useCallback(() => { + setShowCreateModal(false) + }, []) + + const openImportFromDSL = useCallback(() => { + setShowImportModal(true) + }, []) + + const onCloseImportModal = useCallback(() => { + setShowImportModal(false) + if (dslUrl) + replace('/datasets/create-from-pipeline') + }, [dslUrl, replace]) + + const onImportFromDSLSuccess = useCallback(() => { + onPlanInfoChanged() + }, [onPlanInfoChanged]) + + return ( + + + + + + + + + ) +} + +export default React.memo(CreateOptions) diff --git a/web/app/components/datasets/create-from-pipeline/create-options/item.tsx b/web/app/components/datasets/create-from-pipeline/create-options/item.tsx new file mode 100644 index 0000000000..b3fb81aab4 --- /dev/null +++ b/web/app/components/datasets/create-from-pipeline/create-options/item.tsx @@ -0,0 +1,37 @@ +import type { RemixiconComponentType } from '@remixicon/react' +import React from 'react' + +type ItemProps = { + Icon: RemixiconComponentType + title: string + description: string + onClick: () => void +} + +const Item = ({ + Icon, + title, + description, + onClick, +}: ItemProps) => { + return ( + + + + + + + {title} + + + {description} + + + + ) +} + +export default React.memo(Item) diff --git a/web/app/components/datasets/create-from-pipeline/header-effect.tsx b/web/app/components/datasets/create-from-pipeline/header-effect.tsx new file mode 100644 index 0000000000..2462d28b96 --- /dev/null +++ b/web/app/components/datasets/create-from-pipeline/header-effect.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +const HeaderEffect = () => { + return ( + + ) +} + +export default React.memo(HeaderEffect) diff --git a/web/app/components/datasets/create-from-pipeline/header.tsx b/web/app/components/datasets/create-from-pipeline/header.tsx new file mode 100644 index 0000000000..4838b924ed --- /dev/null +++ b/web/app/components/datasets/create-from-pipeline/header.tsx @@ -0,0 +1,21 @@ +import React from 'react' +import { RiArrowLeftLine } from '@remixicon/react' +import Button from '../../base/button' + +const Header = () => { + return ( + + Create knowledge pipeline + + + + + + + ) +} + +export default React.memo(Header) diff --git a/web/app/components/datasets/create-from-pipeline/index.tsx b/web/app/components/datasets/create-from-pipeline/index.tsx new file mode 100644 index 0000000000..77f4104a2d --- /dev/null +++ b/web/app/components/datasets/create-from-pipeline/index.tsx @@ -0,0 +1,19 @@ +'use client' +import HeaderEffect from './header-effect' +import Header from './header' +import CreateOptions from './create-options' + +const CreateFromPipeline = () => { + return ( + + + + + + ) +} + +export default CreateFromPipeline diff --git a/web/app/components/datasets/list/dataset-card/index.tsx b/web/app/components/datasets/list/dataset-card/index.tsx index 2a83a43dd6..91177c470d 100644 --- a/web/app/components/datasets/list/dataset-card/index.tsx +++ b/web/app/components/datasets/list/dataset-card/index.tsx @@ -155,7 +155,10 @@ const DatasetCard = ({ > 0 && 'visible', + )} > { queryFn: () => get('/data-source/integrates'), }) } + +type MemberResponse = { + accounts: Member[] | null +} + +export const useMembers = () => { + return useQuery({ + queryKey: [NAME_SPACE, 'members'], + queryFn: (params: Record) => get('/workspaces/current/members', { + params, + }), + }) +}