diff --git a/.gitignore b/.gitignore index 836bddbb49..f3ac957e14 100644 --- a/.gitignore +++ b/.gitignore @@ -249,3 +249,5 @@ scripts/stress-test/reports/ .qoder/* .eslintcache + +temp diff --git a/web/app/(commonLayout)/deployments/[instanceId]/access/page.tsx b/web/app/(commonLayout)/deployments/[instanceId]/access/page.tsx new file mode 100644 index 0000000000..8963295f4f --- /dev/null +++ b/web/app/(commonLayout)/deployments/[instanceId]/access/page.tsx @@ -0,0 +1,12 @@ +import AccessTab from '@/app/components/deployments/instance-detail/access-tab' + +type PageProps = { + params: Promise<{ instanceId: string }> +} + +const InstanceDetailAccessPage = async ({ params }: PageProps) => { + const { instanceId } = await params + return +} + +export default InstanceDetailAccessPage diff --git a/web/app/(commonLayout)/deployments/[instanceId]/deploy/page.tsx b/web/app/(commonLayout)/deployments/[instanceId]/deploy/page.tsx new file mode 100644 index 0000000000..8f31bdd95d --- /dev/null +++ b/web/app/(commonLayout)/deployments/[instanceId]/deploy/page.tsx @@ -0,0 +1,12 @@ +import DeployTab from '@/app/components/deployments/instance-detail/deploy-tab' + +type PageProps = { + params: Promise<{ instanceId: string }> +} + +const InstanceDetailDeployPage = async ({ params }: PageProps) => { + const { instanceId } = await params + return +} + +export default InstanceDetailDeployPage diff --git a/web/app/(commonLayout)/deployments/[instanceId]/layout.tsx b/web/app/(commonLayout)/deployments/[instanceId]/layout.tsx new file mode 100644 index 0000000000..366825ad32 --- /dev/null +++ b/web/app/(commonLayout)/deployments/[instanceId]/layout.tsx @@ -0,0 +1,19 @@ +import type { ReactNode } from 'react' +import InstanceDetail from '@/app/components/deployments/instance-detail' + +type LayoutProps = { + children: ReactNode + params: Promise<{ instanceId: string }> +} + +const InstanceDetailLayout = async ({ children, params }: LayoutProps) => { + const { instanceId } = await params + + return ( + + {children} + + ) +} + +export default InstanceDetailLayout diff --git a/web/app/(commonLayout)/deployments/[instanceId]/overview/page.tsx b/web/app/(commonLayout)/deployments/[instanceId]/overview/page.tsx new file mode 100644 index 0000000000..994d2980d5 --- /dev/null +++ b/web/app/(commonLayout)/deployments/[instanceId]/overview/page.tsx @@ -0,0 +1,23 @@ +'use client' +import type { FC } from 'react' +import type { InstanceDetailTabKey } from '@/app/components/deployments/instance-detail/tabs' +import * as React from 'react' +import { use } from 'react' +import OverviewTab from '@/app/components/deployments/instance-detail/overview-tab' +import { useRouter } from '@/next/navigation' + +type PageProps = { + params: Promise<{ instanceId: string }> +} + +const InstanceDetailOverviewPage: FC = ({ params }) => { + const { instanceId } = use(params) + const router = useRouter() + const handleSwitchTab = (tab: InstanceDetailTabKey) => { + router.push(`/deployments/${instanceId}/${tab}`) + } + + return +} + +export default InstanceDetailOverviewPage diff --git a/web/app/(commonLayout)/deployments/[instanceId]/page.tsx b/web/app/(commonLayout)/deployments/[instanceId]/page.tsx new file mode 100644 index 0000000000..be3aab6b10 --- /dev/null +++ b/web/app/(commonLayout)/deployments/[instanceId]/page.tsx @@ -0,0 +1,12 @@ +import { redirect } from '@/next/navigation' + +type PageProps = { + params: Promise<{ instanceId: string }> +} + +const InstanceDetailPage = async ({ params }: PageProps) => { + const { instanceId } = await params + redirect(`/deployments/${instanceId}/overview`) +} + +export default InstanceDetailPage diff --git a/web/app/(commonLayout)/deployments/[instanceId]/settings/page.tsx b/web/app/(commonLayout)/deployments/[instanceId]/settings/page.tsx new file mode 100644 index 0000000000..94b43acb70 --- /dev/null +++ b/web/app/(commonLayout)/deployments/[instanceId]/settings/page.tsx @@ -0,0 +1,12 @@ +import SettingsTab from '@/app/components/deployments/instance-detail/settings-tab' + +type PageProps = { + params: Promise<{ instanceId: string }> +} + +const InstanceDetailSettingsPage = async ({ params }: PageProps) => { + const { instanceId } = await params + return +} + +export default InstanceDetailSettingsPage diff --git a/web/app/(commonLayout)/deployments/[instanceId]/versions/page.tsx b/web/app/(commonLayout)/deployments/[instanceId]/versions/page.tsx new file mode 100644 index 0000000000..15258c9975 --- /dev/null +++ b/web/app/(commonLayout)/deployments/[instanceId]/versions/page.tsx @@ -0,0 +1,12 @@ +import VersionsTab from '@/app/components/deployments/instance-detail/versions-tab' + +type PageProps = { + params: Promise<{ instanceId: string }> +} + +const InstanceDetailVersionsPage = async ({ params }: PageProps) => { + const { instanceId } = await params + return +} + +export default InstanceDetailVersionsPage diff --git a/web/app/(commonLayout)/deployments/page.tsx b/web/app/(commonLayout)/deployments/page.tsx new file mode 100644 index 0000000000..ef73c79dfc --- /dev/null +++ b/web/app/(commonLayout)/deployments/page.tsx @@ -0,0 +1,13 @@ +'use client' +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import DeploymentsMain from '@/app/components/deployments' +import useDocumentTitle from '@/hooks/use-document-title' + +const DeploymentsPage = () => { + const { t } = useTranslation('deployments') + useDocumentTitle(t('documentTitle.list')) + return +} + +export default React.memo(DeploymentsPage) diff --git a/web/app/components/deployments/create-instance-modal.tsx b/web/app/components/deployments/create-instance-modal.tsx new file mode 100644 index 0000000000..7ea7934d11 --- /dev/null +++ b/web/app/components/deployments/create-instance-modal.tsx @@ -0,0 +1,296 @@ +'use client' +import type { FC } from 'react' +import type { AppInfo } from './types' +import type { AppModeEnum } from '@/types/app' +import { Button } from '@langgenius/dify-ui/button' +import { cn } from '@langgenius/dify-ui/cn' +import { Dialog, DialogCloseButton, DialogContent, DialogDescription, DialogTitle } from '@langgenius/dify-ui/dialog' +import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' +import * as React from 'react' +import { useMemo, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { AppTypeIcon } from '@/app/components/app/type-selector' +import AppIcon from '@/app/components/base/app-icon' +import Input from '@/app/components/base/input' +import { useRouter } from '@/next/navigation' +import { useDeploymentsStore } from './store' +import { useSourceApps } from './use-source-apps' + +type AppPickerProps = { + apps: AppInfo[] + isLoading: boolean + value: string + onChange: (appId: string) => void +} + +export const AppPicker: FC = ({ apps, isLoading, value, onChange }) => { + const { t } = useTranslation('deployments') + const [open, setOpen] = useState(false) + const [keywords, setKeywords] = useState('') + const triggerRef = useRef(null) + const [triggerWidth, setTriggerWidth] = useState(undefined) + + const selected = useMemo(() => apps.find(a => a.id === value), [apps, value]) + + const filtered = useMemo(() => { + const q = keywords.trim().toLowerCase() + if (!q) + return apps + return apps.filter(a => a.name.toLowerCase().includes(q) || a.mode.toLowerCase().includes(q)) + }, [apps, keywords]) + + const handleOpenChange = (next: boolean) => { + if (next && triggerRef.current) + setTriggerWidth(triggerRef.current.offsetWidth) + if (!next) + setKeywords('') + setOpen(next) + } + + if (isLoading) { + return ( +
+ {t('createModal.loadingApps')} +
+ ) + } + + if (apps.length === 0) { + return ( +
+ {t('createModal.noApps')} +
+ ) + } + + return ( + + + )} + > + {selected + ? ( +
+
+ + +
+ {selected.name} + {selected.mode} +
+ ) + : ( + + {t('createModal.appPickerPlaceholder')} + + )} + +
+ +
+
+ setKeywords(e.target.value)} + onClear={() => setKeywords('')} + autoFocus + /> +
+
+ {filtered.length === 0 + ? ( +
+ {t('createModal.appSearchEmpty')} +
+ ) + : filtered.map((app) => { + const isSelected = app.id === value + return ( + + ) + })} +
+
+
+
+ ) +} + +const CreateInstanceForm: FC<{ onClose: () => void }> = ({ onClose }) => { + const { t } = useTranslation('deployments') + const router = useRouter() + const createInstance = useDeploymentsStore(state => state.createInstance) + const openDeployDrawer = useDeploymentsStore(state => state.openDeployDrawer) + const { apps, isLoading } = useSourceApps() + + const [appId, setAppId] = useState('') + const [name, setName] = useState('') + const [description, setDescription] = useState('') + + const selectedApp = apps.find(a => a.id === appId) + const canCreate = Boolean(appId && name.trim()) + + const handleCreate = (thenDeploy: boolean) => { + if (!canCreate) + return + const instanceId = createInstance({ + appId, + name: name.trim(), + description: description.trim() || undefined, + }) + if (thenDeploy) { + openDeployDrawer({ instanceId }) + return + } + router.push(`/deployments/${instanceId}/overview`) + } + + return ( +
+
+ + {t('createModal.title')} + + + {t('createModal.description')} + +
+ +
+ + +
+ +
+ + setName(e.target.value)} + className="flex h-8 items-center rounded-lg border-[0.5px] border-components-input-border-active bg-components-input-bg-normal px-3 text-[13px] font-medium text-text-secondary outline-hidden placeholder:text-text-quaternary" + /> +
+ +
+ +