mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 12:59:18 +08:00
switch back to query
This commit is contained in:
parent
f530efeda3
commit
3c77a8fab9
@ -14,6 +14,7 @@ import AppIcon from '@/app/components/base/app-icon'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { useAppList } from '@/service/use-apps'
|
||||
import { useCreateDeploymentInstance } from '../hooks/use-deployment-mutations'
|
||||
import { useDeploymentsStore } from '../store'
|
||||
|
||||
const MAX_STUDIO_SOURCE_APPS = 100
|
||||
@ -202,7 +203,7 @@ export const AppPicker: FC<AppPickerProps> = ({ apps, isLoading, value, onChange
|
||||
const CreateInstanceForm: FC<{ onClose: () => void }> = ({ onClose }) => {
|
||||
const { t } = useTranslation('deployments')
|
||||
const router = useRouter()
|
||||
const createInstance = useDeploymentsStore(state => state.createInstance)
|
||||
const createInstance = useCreateDeploymentInstance()
|
||||
const openDeployDrawer = useDeploymentsStore(state => state.openDeployDrawer)
|
||||
const { data: appList, isLoading } = useAppList({ page: 1, limit: MAX_STUDIO_SOURCE_APPS, name: '' })
|
||||
const apps = useMemo<AppInfo[]>(() => {
|
||||
@ -223,11 +224,12 @@ const CreateInstanceForm: FC<{ onClose: () => void }> = ({ onClose }) => {
|
||||
|
||||
setIsSubmitting(true)
|
||||
try {
|
||||
const result = await createInstance({
|
||||
const result = await createInstance.mutateAsync({
|
||||
sourceAppId: appId,
|
||||
name: name.trim(),
|
||||
description: description.trim() || undefined,
|
||||
})
|
||||
onClose()
|
||||
if (thenDeploy) {
|
||||
openDeployDrawer({
|
||||
appId: result.appInstanceId,
|
||||
|
||||
@ -2,30 +2,25 @@
|
||||
|
||||
import type { FC } from 'react'
|
||||
import { Dialog, DialogCloseButton, DialogContent } from '@langgenius/dify-ui/dialog'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { deploymentAppDataQueryOptions } from '../data'
|
||||
import { useDeploymentAppData } from '../hooks/use-deployment-data'
|
||||
import { useStartDeployment } from '../hooks/use-deployment-mutations'
|
||||
import { useSourceApps } from '../hooks/use-source-apps'
|
||||
import { useDeploymentAppData, useDeploymentsStore } from '../store'
|
||||
import { useDeploymentsStore } from '../store'
|
||||
import { DeployForm } from './deploy-drawer/form'
|
||||
|
||||
const DeployDrawer: FC = () => {
|
||||
const { t } = useTranslation('deployments')
|
||||
const drawer = useDeploymentsStore(state => state.deployDrawer)
|
||||
const drawerAppId = drawer.appId
|
||||
const storedAppData = useDeploymentAppData(drawerAppId)
|
||||
const closeDeployDrawer = useDeploymentsStore(state => state.closeDeployDrawer)
|
||||
const startDeploy = useDeploymentsStore(state => state.startDeploy)
|
||||
const startDeploy = useStartDeployment()
|
||||
const open = drawer.open
|
||||
const { environmentOptions } = useSourceApps({ enabled: open })
|
||||
|
||||
useQuery({
|
||||
...deploymentAppDataQueryOptions(drawerAppId ?? ''),
|
||||
queryFn: () => useDeploymentsStore.getState().fetchAppData(drawerAppId!),
|
||||
enabled: open && Boolean(drawerAppId) && !storedAppData,
|
||||
const { data: appData } = useDeploymentAppData(drawerAppId, {
|
||||
enabled: open && Boolean(drawerAppId),
|
||||
})
|
||||
|
||||
const appData = storedAppData
|
||||
const environments = environmentOptions
|
||||
const releases = appData?.releaseHistory.data?.map(row => row.release ?? row).filter(release => release.id) ?? []
|
||||
const defaultReleaseId = releases[0]?.id
|
||||
@ -57,13 +52,15 @@ const DeployDrawer: FC = () => {
|
||||
lockedEnvId={drawer.environmentId}
|
||||
presetReleaseId={drawer.releaseId}
|
||||
onCancel={closeDeployDrawer}
|
||||
onSubmit={({ environmentId, releaseId, releaseNote }) =>
|
||||
startDeploy({
|
||||
onSubmit={({ environmentId, releaseId, releaseNote }) => {
|
||||
closeDeployDrawer()
|
||||
startDeploy.mutate({
|
||||
appId: drawerAppId,
|
||||
environmentId,
|
||||
releaseId,
|
||||
releaseNote,
|
||||
})}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
|
||||
@ -10,8 +10,11 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from '@langgenius/dify-ui/alert-dialog'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toAppInfoFromOverview } from '../data'
|
||||
import { useCachedDeploymentAppData } from '../hooks/use-deployment-data'
|
||||
import { useStartDeployment } from '../hooks/use-deployment-mutations'
|
||||
import { useSourceApps } from '../hooks/use-source-apps'
|
||||
import { useDeploymentAppData, useDeploymentInstance, useDeploymentsStore } from '../store'
|
||||
import { useDeploymentsStore } from '../store'
|
||||
import {
|
||||
activeRelease,
|
||||
deployedRows,
|
||||
@ -33,10 +36,9 @@ const InfoRow: FC<{ label: string, value: string }> = ({ label, value }) => {
|
||||
const RollbackModal: FC = () => {
|
||||
const { t } = useTranslation('deployments')
|
||||
const modal = useDeploymentsStore(state => state.rollbackModal)
|
||||
const appData = useDeploymentAppData(modal.appId)
|
||||
const storedApp = useDeploymentInstance(modal.appId)
|
||||
const { data: appData } = useCachedDeploymentAppData(modal.appId)
|
||||
const closeRollbackModal = useDeploymentsStore(state => state.closeRollbackModal)
|
||||
const rollbackDeployment = useDeploymentsStore(state => state.rollbackDeployment)
|
||||
const rollbackDeployment = useStartDeployment()
|
||||
const { appMap, environmentOptions } = useSourceApps()
|
||||
|
||||
const currentRow = deployedRows(appData?.environmentDeployments.data)
|
||||
@ -47,12 +49,18 @@ const RollbackModal: FC = () => {
|
||||
const currentRelease = activeRelease(currentRow)
|
||||
const environment = currentRow?.environment
|
||||
?? environmentOptions.find(env => env.id === modal.environmentId)
|
||||
const app = storedApp ?? (modal.appId ? appMap.get(modal.appId) : undefined)
|
||||
const app = toAppInfoFromOverview(appData?.overview.instance)
|
||||
?? (modal.appId ? appMap.get(modal.appId) : undefined)
|
||||
|
||||
const confirm = () => {
|
||||
if (!modal.appId || !modal.environmentId || !modal.targetReleaseId)
|
||||
return
|
||||
rollbackDeployment(modal.appId, modal.environmentId, modal.targetReleaseId)
|
||||
closeRollbackModal()
|
||||
rollbackDeployment.mutate({
|
||||
appId: modal.appId,
|
||||
environmentId: modal.environmentId,
|
||||
releaseId: modal.targetReleaseId,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@ -3,11 +3,17 @@
|
||||
import type { FC } from 'react'
|
||||
import type {
|
||||
AccessPermission,
|
||||
AccessSubject,
|
||||
ConsoleEnvironmentSummary,
|
||||
} from '@/contract/console/deployments'
|
||||
import { useMemo } from 'react'
|
||||
import { useDeploymentAppData, useDeploymentsStore } from '../store'
|
||||
import { deploymentsSelectors } from '../store/selectors'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useCachedDeploymentAppData } from '../hooks/use-deployment-data'
|
||||
import {
|
||||
useGenerateDeploymentApiKey,
|
||||
useRevokeDeploymentApiKey,
|
||||
useSetEnvironmentAccessPolicy,
|
||||
useToggleDeploymentAccessChannel,
|
||||
} from '../hooks/use-deployment-mutations'
|
||||
import {
|
||||
deployedRows,
|
||||
} from '../utils'
|
||||
@ -31,13 +37,15 @@ type AccessTabProps = {
|
||||
}
|
||||
|
||||
const AccessTab: FC<AccessTabProps> = ({ instanceId: appId }) => {
|
||||
const appData = useDeploymentAppData(appId)
|
||||
const createdApiToken = useDeploymentsStore(deploymentsSelectors.createdApiToken)
|
||||
const clearCreatedApiToken = useDeploymentsStore(state => state.clearCreatedApiToken)
|
||||
const generateApiKey = useDeploymentsStore(state => state.generateApiKey)
|
||||
const revokeApiKey = useDeploymentsStore(state => state.revokeApiKey)
|
||||
const toggleAccessChannel = useDeploymentsStore(state => state.toggleAccessChannel)
|
||||
const setEnvironmentAccessPolicy = useDeploymentsStore(state => state.setEnvironmentAccessPolicy)
|
||||
const { data: appData } = useCachedDeploymentAppData(appId)
|
||||
const [createdApiToken, setCreatedApiToken] = useState<{
|
||||
appId: string
|
||||
token: string
|
||||
}>()
|
||||
const generateApiKey = useGenerateDeploymentApiKey()
|
||||
const revokeApiKey = useRevokeDeploymentApiKey()
|
||||
const toggleAccessChannel = useToggleDeploymentAccessChannel()
|
||||
const setEnvironmentAccessPolicy = useSetEnvironmentAccessPolicy()
|
||||
|
||||
const accessConfig = appData?.accessConfig
|
||||
const deploymentRows = useMemo(
|
||||
@ -56,18 +64,37 @@ const AccessTab: FC<AccessTabProps> = ({ instanceId: appId }) => {
|
||||
const apiEnabled = accessConfig?.developerApi?.enabled ?? false
|
||||
const apiKeys = accessConfig?.developerApi?.apiKeys ?? []
|
||||
const handleGenerateApiKey = (environmentId: string) => {
|
||||
void (async () => {
|
||||
await generateApiKey(appId, environmentId)
|
||||
})()
|
||||
generateApiKey.mutate(
|
||||
{ appId, environmentId },
|
||||
{
|
||||
onSuccess: (response) => {
|
||||
if (response.apiToken?.token)
|
||||
setCreatedApiToken({ appId, token: response.apiToken.token })
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
const handleRevokeApiKey = (environmentId: string, apiKeyId: string) => {
|
||||
void (async () => {
|
||||
await revokeApiKey(appId, environmentId, apiKeyId)
|
||||
})()
|
||||
revokeApiKey.mutate({ appId, environmentId, apiKeyId })
|
||||
}
|
||||
const handleSetEnvironmentAccessPolicy = async (
|
||||
appId: string,
|
||||
environmentId: string,
|
||||
accessMode: string,
|
||||
subjects: AccessSubject[],
|
||||
) => {
|
||||
await setEnvironmentAccessPolicy.mutateAsync({
|
||||
appId,
|
||||
environmentId,
|
||||
accessMode,
|
||||
subjects,
|
||||
})
|
||||
}
|
||||
const webappRows = accessConfig?.accessChannels?.webappRows?.filter(row => row.url) ?? []
|
||||
const runEnabled = accessConfig?.accessChannels?.enabled ?? false
|
||||
const visibleCreatedApiToken = createdApiToken?.appId === appId ? createdApiToken : undefined
|
||||
const visibleCreatedApiToken = createdApiToken?.appId === appId
|
||||
? createdApiToken.token
|
||||
: undefined
|
||||
const cliDomain = getUrlOrigin(accessConfig?.accessChannels?.cli?.url)
|
||||
const cliDocsUrl = cliDomain ? `${cliDomain}/cli` : undefined
|
||||
|
||||
@ -77,24 +104,24 @@ const AccessTab: FC<AccessTabProps> = ({ instanceId: appId }) => {
|
||||
appId={appId}
|
||||
environments={deployedEnvs}
|
||||
policies={policies}
|
||||
onSetPolicy={setEnvironmentAccessPolicy}
|
||||
onSetPolicy={handleSetEnvironmentAccessPolicy}
|
||||
/>
|
||||
<AccessChannelsSection
|
||||
runEnabled={runEnabled}
|
||||
webappRows={webappRows}
|
||||
cliDomain={cliDomain}
|
||||
cliDocsUrl={cliDocsUrl}
|
||||
onToggle={enabled => toggleAccessChannel(appId, 'webapp', enabled)}
|
||||
onToggle={enabled => toggleAccessChannel.mutate({ appId, channel: 'webapp', enabled })}
|
||||
/>
|
||||
<DeveloperApiSection
|
||||
apiEnabled={apiEnabled}
|
||||
environments={deployedEnvs}
|
||||
apiKeys={apiKeys}
|
||||
createdToken={visibleCreatedApiToken?.token}
|
||||
onToggle={enabled => toggleAccessChannel(appId, 'api', enabled)}
|
||||
createdToken={visibleCreatedApiToken}
|
||||
onToggle={enabled => toggleAccessChannel.mutate({ appId, channel: 'api', enabled })}
|
||||
onGenerate={handleGenerateApiKey}
|
||||
onRevoke={handleRevokeApiKey}
|
||||
onClearCreatedToken={clearCreatedApiToken}
|
||||
onClearCreatedToken={() => setCreatedApiToken(undefined)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -363,7 +363,6 @@ export const EnvironmentPermissionRow: FC<EnvironmentPermissionRowProps> = ({
|
||||
permissionKeyToAccessMode(nextKind),
|
||||
nextKind === 'specific' ? policySubjects(nextSubjects) : [],
|
||||
)
|
||||
await policyQuery.refetch()
|
||||
setDraft({})
|
||||
}
|
||||
catch {
|
||||
|
||||
@ -10,8 +10,10 @@ import {
|
||||
} from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCachedDeploymentAppData } from '../hooks/use-deployment-data'
|
||||
import { useUndeployDeployment } from '../hooks/use-deployment-mutations'
|
||||
import { useSourceApps } from '../hooks/use-source-apps'
|
||||
import { useDeploymentAppData, useDeploymentsStore } from '../store'
|
||||
import { useDeploymentsStore } from '../store'
|
||||
import {
|
||||
activeRelease,
|
||||
deployedRows,
|
||||
@ -36,9 +38,9 @@ type DeployTabProps = {
|
||||
|
||||
const DeployTab: FC<DeployTabProps> = ({ instanceId: appId }) => {
|
||||
const { t } = useTranslation('deployments')
|
||||
const appData = useDeploymentAppData(appId)
|
||||
const { data: appData } = useCachedDeploymentAppData(appId)
|
||||
const openDeployDrawer = useDeploymentsStore(state => state.openDeployDrawer)
|
||||
const undeployDeployment = useDeploymentsStore(state => state.undeployDeployment)
|
||||
const undeployDeployment = useUndeployDeployment()
|
||||
const { environmentOptions } = useSourceApps()
|
||||
|
||||
const rows = useMemo(
|
||||
@ -179,7 +181,11 @@ const DeployTab: FC<DeployTabProps> = ({ instanceId: appId }) => {
|
||||
<DropdownMenuContent placement="bottom-end" sideOffset={4} popupClassName="w-[200px]">
|
||||
<DropdownMenuItem
|
||||
className="gap-2 px-3"
|
||||
onClick={() => undeployDeployment(appId, envId, deploymentId(row), status === 'deploying')}
|
||||
onClick={() => undeployDeployment.mutate({
|
||||
appId,
|
||||
runtimeInstanceId: deploymentId(row),
|
||||
isDeploying: status === 'deploying',
|
||||
})}
|
||||
>
|
||||
<span className="system-sm-regular text-text-destructive">
|
||||
{status === 'deploying' ? t('deployTab.cancelDeployment') : t('deployTab.undeploy')}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import type { AppInfo } from '../types'
|
||||
import type { InstanceDetailTabKey } from './tabs'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { useMemo } from 'react'
|
||||
@ -11,9 +10,9 @@ import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { useRouter, useSelectedLayoutSegment } from '@/next/navigation'
|
||||
import DeployDrawer from '../components/deploy-drawer'
|
||||
import RollbackModal from '../components/rollback-modal'
|
||||
import { useDeploymentData } from '../hooks/use-deployment-data'
|
||||
import { toAppInfoFromOverview } from '../data'
|
||||
import { useDeploymentAppData } from '../hooks/use-deployment-data'
|
||||
import { useSourceApps } from '../hooks/use-source-apps'
|
||||
import { useDeploymentAppData, useDeploymentInstance } from '../store'
|
||||
import { deployedRows, deploymentStatus } from '../utils'
|
||||
import { DeploymentSidebar } from './deployment-sidebar'
|
||||
import { isInstanceDetailTabKey } from './tabs'
|
||||
@ -30,23 +29,19 @@ const InstanceDetail: FC<InstanceDetailProps> = ({ instanceId, children }) => {
|
||||
const selectedSegment = useSelectedLayoutSegment()
|
||||
const selectedTab = selectedSegment ?? undefined
|
||||
const activeTab: InstanceDetailTabKey = isInstanceDetailTabKey(selectedTab) ? selectedTab : 'overview'
|
||||
const storedApp = useDeploymentInstance(instanceId)
|
||||
const appData = useDeploymentAppData(instanceId)
|
||||
const detailQuery = useDeploymentAppData(instanceId, { enabled: Boolean(instanceId) })
|
||||
const appData = detailQuery.data
|
||||
const { appMap, isLoading: isLoadingApps } = useSourceApps()
|
||||
useDocumentTitle(t('documentTitle.detail'))
|
||||
|
||||
const app = useMemo(
|
||||
() => storedApp ?? appMap.get(instanceId),
|
||||
[storedApp, instanceId, appMap],
|
||||
const detailApp = useMemo(
|
||||
() => toAppInfoFromOverview(appData?.overview.instance),
|
||||
[appData?.overview.instance],
|
||||
)
|
||||
const app = useMemo(
|
||||
() => detailApp ?? appMap.get(instanceId),
|
||||
[detailApp, instanceId, appMap],
|
||||
)
|
||||
const detailApps = useMemo<AppInfo[]>(() => [
|
||||
app ?? {
|
||||
id: instanceId,
|
||||
name: instanceId,
|
||||
mode: 'workflow',
|
||||
},
|
||||
], [app, instanceId])
|
||||
const detailQuery = useDeploymentData(detailApps, { enabled: Boolean(instanceId) })
|
||||
const appDeployments = useMemo(
|
||||
() => deployedRows(appData?.environmentDeployments.data),
|
||||
[appData?.environmentDeployments.data],
|
||||
|
||||
@ -7,8 +7,10 @@ import { useTranslation } from 'react-i18next'
|
||||
import { getAppModeLabel } from '@/app/components/app-sidebar/app-info/app-mode-labels'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { StatusBadge } from '../components/status-badge'
|
||||
import { toAppInfoFromOverview } from '../data'
|
||||
import { useCachedDeploymentAppData } from '../hooks/use-deployment-data'
|
||||
import { useSourceApps } from '../hooks/use-source-apps'
|
||||
import { useDeploymentAppData, useDeploymentInstance, useDeploymentsStore } from '../store'
|
||||
import { useDeploymentsStore } from '../store'
|
||||
import { releaseLabel, webappUrl } from '../utils'
|
||||
|
||||
type OverviewTabProps = {
|
||||
@ -90,12 +92,11 @@ const OverviewTab: FC<OverviewTabProps> = ({ instanceId }) => {
|
||||
const { t } = useTranslation('deployments')
|
||||
const { t: tCommon } = useTranslation()
|
||||
const router = useRouter()
|
||||
const appData = useDeploymentAppData(instanceId)
|
||||
const { data: appData } = useCachedDeploymentAppData(instanceId)
|
||||
const openDeployDrawer = useDeploymentsStore(state => state.openDeployDrawer)
|
||||
const storedApp = useDeploymentInstance(instanceId)
|
||||
const { appMap } = useSourceApps()
|
||||
const app = storedApp ?? appMap.get(instanceId)
|
||||
const overview = appData?.overview
|
||||
const app = toAppInfoFromOverview(overview?.instance) ?? appMap.get(instanceId)
|
||||
const overviewApp = overview?.instance
|
||||
const deployments = useMemo(
|
||||
() => overview?.deployments?.filter(row => row.environment?.id && row.status?.toLowerCase() !== 'undeployed') ?? [],
|
||||
|
||||
@ -18,8 +18,13 @@ import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { toAppInfoFromOverview } from '../data'
|
||||
import { useCachedDeploymentAppData } from '../hooks/use-deployment-data'
|
||||
import {
|
||||
useDeleteDeploymentInstance,
|
||||
useUpdateDeploymentInstance,
|
||||
} from '../hooks/use-deployment-mutations'
|
||||
import { useSourceApps } from '../hooks/use-source-apps'
|
||||
import { useDeploymentAppData, useDeploymentInstance, useDeploymentsStore } from '../store'
|
||||
import { deployedRows } from '../utils'
|
||||
|
||||
type SettingsTabProps = {
|
||||
@ -176,12 +181,11 @@ const SettingsForm: FC<SettingsFormProps> = ({ app, settings, hasDeployments, on
|
||||
|
||||
const SettingsTab: FC<SettingsTabProps> = ({ instanceId }) => {
|
||||
const router = useRouter()
|
||||
const storedApp = useDeploymentInstance(instanceId)
|
||||
const appData = useDeploymentAppData(instanceId)
|
||||
const updateInstance = useDeploymentsStore(state => state.updateInstance)
|
||||
const deleteInstance = useDeploymentsStore(state => state.deleteInstance)
|
||||
const { data: appData } = useCachedDeploymentAppData(instanceId)
|
||||
const updateInstance = useUpdateDeploymentInstance()
|
||||
const deleteInstance = useDeleteDeploymentInstance()
|
||||
const { appMap } = useSourceApps()
|
||||
const app = storedApp ?? appMap.get(instanceId)
|
||||
const app = toAppInfoFromOverview(appData?.overview.instance) ?? appMap.get(instanceId)
|
||||
const settingsQuery = useQuery(consoleQuery.deployments.settings.queryOptions({
|
||||
input: {
|
||||
params: {
|
||||
@ -204,11 +208,13 @@ const SettingsTab: FC<SettingsTabProps> = ({ instanceId }) => {
|
||||
settings={settingsQuery.data}
|
||||
hasDeployments={hasDeployments}
|
||||
onSave={async (patch) => {
|
||||
await updateInstance(instanceId, patch)
|
||||
await settingsQuery.refetch()
|
||||
await updateInstance.mutateAsync({
|
||||
appId: instanceId,
|
||||
...patch,
|
||||
})
|
||||
}}
|
||||
onDelete={async () => {
|
||||
await deleteInstance(instanceId)
|
||||
await deleteInstance.mutateAsync(instanceId)
|
||||
router.push('/deployments')
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -4,7 +4,7 @@ import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDeploymentAppData } from '../store'
|
||||
import { useCachedDeploymentAppData } from '../hooks/use-deployment-data'
|
||||
import {
|
||||
deployedRows,
|
||||
formatDate,
|
||||
@ -23,7 +23,7 @@ type VersionsTabProps = {
|
||||
|
||||
const VersionsTab: FC<VersionsTabProps> = ({ instanceId: appId }) => {
|
||||
const { t } = useTranslation('deployments')
|
||||
const appData = useDeploymentAppData(appId)
|
||||
const { data: appData } = useCachedDeploymentAppData(appId)
|
||||
const releaseRows = useMemo(
|
||||
() => appData?.releaseHistory.data?.filter(row => (row.release ?? row).id) ?? [],
|
||||
[appData?.releaseHistory.data],
|
||||
|
||||
@ -10,8 +10,9 @@ import {
|
||||
} from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCachedDeploymentAppData } from '../../hooks/use-deployment-data'
|
||||
import { useSourceApps } from '../../hooks/use-source-apps'
|
||||
import { useDeploymentAppData, useDeploymentsStore } from '../../store'
|
||||
import { useDeploymentsStore } from '../../store'
|
||||
import {
|
||||
activeRelease,
|
||||
deployedRows,
|
||||
@ -28,7 +29,7 @@ type DeployReleaseMenuProps = {
|
||||
|
||||
export const DeployReleaseMenu: FC<DeployReleaseMenuProps> = ({ appId, releaseId }) => {
|
||||
const { t } = useTranslation('deployments')
|
||||
const appData = useDeploymentAppData(appId)
|
||||
const { data: appData } = useCachedDeploymentAppData(appId)
|
||||
const openDeployDrawer = useDeploymentsStore(state => state.openDeployDrawer)
|
||||
const openRollbackModal = useDeploymentsStore(state => state.openRollbackModal)
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
@ -1,28 +1,41 @@
|
||||
'use client'
|
||||
|
||||
import type { AppInfo } from '../types'
|
||||
import { useQueries } from '@tanstack/react-query'
|
||||
import { deploymentAppDataQueryOptions } from '../data'
|
||||
import { useDeploymentsStore } from '../store'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useMemo } from 'react'
|
||||
import {
|
||||
deploymentAppDataQueryOptions,
|
||||
toAppInfoFromOverview,
|
||||
} from '../data'
|
||||
|
||||
type UseDeploymentDataOptions = {
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export function useDeploymentData(apps: AppInfo[], options: UseDeploymentDataOptions = {}) {
|
||||
export function useDeploymentAppData(appId?: string, options: UseDeploymentDataOptions = {}) {
|
||||
const { enabled = true } = options
|
||||
|
||||
const queries = useQueries({
|
||||
queries: apps.map(app => ({
|
||||
...deploymentAppDataQueryOptions(app.id),
|
||||
queryFn: () => useDeploymentsStore.getState().fetchAppData(app.id),
|
||||
enabled: enabled && Boolean(app.id),
|
||||
})),
|
||||
return useQuery({
|
||||
...deploymentAppDataQueryOptions(appId ?? ''),
|
||||
enabled: enabled && Boolean(appId),
|
||||
})
|
||||
}
|
||||
|
||||
export function useCachedDeploymentAppData(appId?: string) {
|
||||
return useQuery({
|
||||
...deploymentAppDataQueryOptions(appId ?? ''),
|
||||
enabled: false,
|
||||
})
|
||||
}
|
||||
|
||||
export function useDeploymentAppInfo(appId?: string, options: UseDeploymentDataOptions = {}) {
|
||||
const query = useDeploymentAppData(appId, options)
|
||||
const app = useMemo(
|
||||
() => toAppInfoFromOverview(query.data?.overview.instance),
|
||||
[query.data?.overview.instance],
|
||||
)
|
||||
|
||||
return {
|
||||
isLoading: queries.some(query => query.isLoading),
|
||||
isFetching: queries.some(query => query.isFetching),
|
||||
isError: queries.some(query => query.isError),
|
||||
...query,
|
||||
data: app,
|
||||
}
|
||||
}
|
||||
|
||||
324
web/features/deployments/hooks/use-deployment-mutations.ts
Normal file
324
web/features/deployments/hooks/use-deployment-mutations.ts
Normal file
@ -0,0 +1,324 @@
|
||||
'use client'
|
||||
|
||||
import type {
|
||||
CreateDeploymentParams,
|
||||
CreateInstanceParams,
|
||||
DeploymentAppData,
|
||||
UpdateInstanceParams,
|
||||
} from '../data'
|
||||
import type { AccessSubject, ConsoleReleaseSummary } from '@/contract/console/deployments'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import {
|
||||
cancelDeployment,
|
||||
createApiKey,
|
||||
createAppInstance,
|
||||
createDeployment,
|
||||
deleteApiKey,
|
||||
deleteAppInstance,
|
||||
deploymentAppDataQueryKey,
|
||||
patchAccessChannel,
|
||||
patchDeveloperAPI,
|
||||
refreshDeploymentAppDataWhenReady,
|
||||
undeployEnvironment,
|
||||
updateAppInstance,
|
||||
updateEnvironmentAccessPolicy,
|
||||
waitForAppInstanceInDeploymentList,
|
||||
} from '../data'
|
||||
|
||||
export type CreateDeploymentInstanceResult = {
|
||||
appInstanceId: string
|
||||
initialRelease?: ConsoleReleaseSummary
|
||||
appData?: DeploymentAppData
|
||||
}
|
||||
|
||||
type UpdateDeploymentInstanceParams = {
|
||||
appId: string
|
||||
} & UpdateInstanceParams
|
||||
|
||||
type UndeployDeploymentParams = {
|
||||
appId: string
|
||||
runtimeInstanceId?: string
|
||||
isDeploying?: boolean
|
||||
}
|
||||
|
||||
type GenerateApiKeyParams = {
|
||||
appId: string
|
||||
environmentId: string
|
||||
}
|
||||
|
||||
type RevokeApiKeyParams = GenerateApiKeyParams & {
|
||||
apiKeyId: string
|
||||
}
|
||||
|
||||
type ToggleAccessChannelParams = {
|
||||
appId: string
|
||||
channel: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
type SetEnvironmentAccessPolicyParams = {
|
||||
appId: string
|
||||
environmentId: string
|
||||
accessMode: string
|
||||
subjects: AccessSubject[]
|
||||
}
|
||||
|
||||
const createApiKeyLabel = (
|
||||
appData: DeploymentAppData | undefined,
|
||||
environmentId: string,
|
||||
) => {
|
||||
const existingCount = appData?.accessConfig.developerApi?.apiKeys?.filter(key =>
|
||||
(key.environmentId ?? key.environment?.id) === environmentId,
|
||||
).length ?? 0
|
||||
const environmentName = appData
|
||||
?.environmentDeployments
|
||||
.data
|
||||
?.find(row => row.environment?.id === environmentId)
|
||||
?.environment
|
||||
?.name ?? 'env'
|
||||
|
||||
return `${environmentName}-key-${String(existingCount + 1).padStart(3, '0')}`
|
||||
}
|
||||
|
||||
export const useCreateDeploymentInstance = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationKey: consoleQuery.deployments.createInstance.mutationKey(),
|
||||
mutationFn: async (params: CreateInstanceParams): Promise<CreateDeploymentInstanceResult> => {
|
||||
const response = await createAppInstance(params)
|
||||
if (!response.appInstanceId)
|
||||
throw new Error('Create app instance did not return an appInstanceId.')
|
||||
|
||||
const [appData] = await Promise.all([
|
||||
refreshDeploymentAppDataWhenReady(response.appInstanceId).catch(() => undefined),
|
||||
waitForAppInstanceInDeploymentList(response.appInstanceId).catch(() => undefined),
|
||||
])
|
||||
|
||||
return {
|
||||
appInstanceId: response.appInstanceId,
|
||||
initialRelease: response.initialRelease,
|
||||
appData,
|
||||
}
|
||||
},
|
||||
onSuccess: async (result) => {
|
||||
if (result.appData) {
|
||||
queryClient.setQueryData(
|
||||
deploymentAppDataQueryKey(result.appInstanceId),
|
||||
result.appData,
|
||||
)
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.deployments.list.key(),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: deploymentAppDataQueryKey(result.appInstanceId),
|
||||
}),
|
||||
])
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateDeploymentInstance = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationKey: consoleQuery.deployments.updateInstance.mutationKey(),
|
||||
mutationFn: ({ appId, ...patch }: UpdateDeploymentInstanceParams) =>
|
||||
updateAppInstance(appId, patch),
|
||||
onSuccess: async (_data, variables) => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.deployments.list.key(),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: deploymentAppDataQueryKey(variables.appId),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.deployments.settings.queryKey({
|
||||
input: {
|
||||
params: {
|
||||
appInstanceId: variables.appId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
])
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteDeploymentInstance = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationKey: consoleQuery.deployments.deleteInstance.mutationKey(),
|
||||
mutationFn: (appId: string) => deleteAppInstance(appId),
|
||||
onSuccess: async (_data, appId) => {
|
||||
queryClient.removeQueries({
|
||||
queryKey: deploymentAppDataQueryKey(appId),
|
||||
})
|
||||
queryClient.removeQueries({
|
||||
queryKey: consoleQuery.deployments.settings.queryKey({
|
||||
input: {
|
||||
params: {
|
||||
appInstanceId: appId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.deployments.list.key(),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useStartDeployment = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationKey: consoleQuery.deployments.createDeployment.mutationKey(),
|
||||
mutationFn: (params: CreateDeploymentParams) => createDeployment(params),
|
||||
onSuccess: async (_data, variables) => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.deployments.list.key(),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: deploymentAppDataQueryKey(variables.appId),
|
||||
}),
|
||||
])
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useUndeployDeployment = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationKey: consoleQuery.deployments.undeployEnvironment.mutationKey(),
|
||||
mutationFn: ({ appId, runtimeInstanceId, isDeploying }: UndeployDeploymentParams) => {
|
||||
if (!runtimeInstanceId)
|
||||
return Promise.resolve(undefined)
|
||||
if (isDeploying)
|
||||
return cancelDeployment(appId, runtimeInstanceId)
|
||||
return undeployEnvironment(appId, runtimeInstanceId)
|
||||
},
|
||||
onSuccess: async (_data, variables) => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.deployments.list.key(),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: deploymentAppDataQueryKey(variables.appId),
|
||||
}),
|
||||
])
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useGenerateDeploymentApiKey = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationKey: consoleQuery.deployments.createEnvironmentAPIToken.mutationKey(),
|
||||
mutationFn: ({ appId, environmentId }: GenerateApiKeyParams) => {
|
||||
const appData = queryClient.getQueryData<DeploymentAppData>(
|
||||
deploymentAppDataQueryKey(appId),
|
||||
)
|
||||
|
||||
return createApiKey(appId, environmentId, createApiKeyLabel(appData, environmentId))
|
||||
},
|
||||
onSuccess: async (_data, variables) => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.deployments.list.key(),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: deploymentAppDataQueryKey(variables.appId),
|
||||
}),
|
||||
])
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useRevokeDeploymentApiKey = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationKey: consoleQuery.deployments.deleteEnvironmentAPIToken.mutationKey(),
|
||||
mutationFn: ({ appId, apiKeyId }: RevokeApiKeyParams) => deleteApiKey(appId, apiKeyId),
|
||||
onSuccess: async (_data, variables) => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.deployments.list.key(),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: deploymentAppDataQueryKey(variables.appId),
|
||||
}),
|
||||
])
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useToggleDeploymentAccessChannel = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationKey: consoleQuery.deployments.patchAccessChannel.mutationKey(),
|
||||
mutationFn: ({ appId, channel, enabled }: ToggleAccessChannelParams) => {
|
||||
if (channel === 'api')
|
||||
return patchDeveloperAPI(appId, enabled)
|
||||
return patchAccessChannel(appId, enabled)
|
||||
},
|
||||
onSuccess: async (_data, variables) => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.deployments.list.key(),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: deploymentAppDataQueryKey(variables.appId),
|
||||
}),
|
||||
])
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useSetEnvironmentAccessPolicy = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationKey: consoleQuery.deployments.updateEnvironmentAccessPolicy.mutationKey(),
|
||||
mutationFn: ({
|
||||
appId,
|
||||
environmentId,
|
||||
accessMode,
|
||||
subjects,
|
||||
}: SetEnvironmentAccessPolicyParams) =>
|
||||
updateEnvironmentAccessPolicy(appId, environmentId, accessMode, subjects),
|
||||
onSuccess: async (_data, variables) => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.deployments.list.key(),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: deploymentAppDataQueryKey(variables.appId),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.deployments.environmentAccessPolicy.queryKey({
|
||||
input: {
|
||||
params: {
|
||||
appInstanceId: variables.appId,
|
||||
environmentId: variables.environmentId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
])
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -1,10 +1,10 @@
|
||||
'use client'
|
||||
import type { AppInfo } from '../types'
|
||||
import type { AppDeploymentSummary, EnvironmentOption } from '@/contract/console/deployments'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useMemo } from 'react'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { useDeploymentsStore } from '../store'
|
||||
import { deploymentsSelectors } from '../store/selectors'
|
||||
import { toAppInfoFromSummary } from '../data'
|
||||
|
||||
const MAX_SOURCE_APPS = 100
|
||||
|
||||
@ -15,10 +15,16 @@ type UseSourceAppsOptions = {
|
||||
notDeployed?: boolean
|
||||
}
|
||||
|
||||
type DeploymentEnvironmentFilter = {
|
||||
id?: string
|
||||
name?: string
|
||||
kind?: string
|
||||
disabled?: boolean
|
||||
disabledReason?: string
|
||||
}
|
||||
|
||||
export function useSourceApps(options: UseSourceAppsOptions = {}) {
|
||||
const { enabled = true, environmentId, keyword, notDeployed } = options
|
||||
const instancesById = useDeploymentsStore(deploymentsSelectors.instancesById)
|
||||
const listRefreshToken = useDeploymentsStore(deploymentsSelectors.listRefreshToken)
|
||||
const query = useMemo(() => ({
|
||||
pageNumber: 1,
|
||||
resultsPerPage: MAX_SOURCE_APPS,
|
||||
@ -26,38 +32,47 @@ export function useSourceApps(options: UseSourceAppsOptions = {}) {
|
||||
...(notDeployed ? { notDeployed: true } : {}),
|
||||
...(keyword?.trim() ? { query: keyword.trim() } : {}),
|
||||
}), [environmentId, keyword, notDeployed])
|
||||
const sourceAppsList = useDeploymentsStore(deploymentsSelectors.sourceAppsList(query))
|
||||
|
||||
const listQueryOptions = consoleQuery.deployments.list.queryOptions({
|
||||
const listQuery = useQuery(consoleQuery.deployments.list.queryOptions({
|
||||
input: { query },
|
||||
enabled,
|
||||
staleTime: 30 * 1000,
|
||||
})
|
||||
|
||||
const listQuery = useQuery({
|
||||
...listQueryOptions,
|
||||
queryKey: [
|
||||
...consoleQuery.deployments.list.queryKey({ input: { query } }),
|
||||
listRefreshToken,
|
||||
],
|
||||
queryFn: () => useDeploymentsStore.getState().fetchSourceApps(query),
|
||||
})
|
||||
}))
|
||||
|
||||
const apps = useMemo<AppInfo[]>(() => {
|
||||
return sourceAppsList.appIds
|
||||
.map(id => instancesById[id])
|
||||
return (listQuery.data?.data ?? [])
|
||||
.map(toAppInfoFromSummary)
|
||||
.filter((app): app is AppInfo => Boolean(app))
|
||||
}, [sourceAppsList.appIds, instancesById])
|
||||
}, [listQuery.data?.data])
|
||||
|
||||
const appMap = useMemo<Map<string, AppInfo>>(() => {
|
||||
return new Map(apps.map(a => [a.id, a]))
|
||||
}, [apps])
|
||||
|
||||
const summaries = useMemo<Record<string, AppDeploymentSummary>>(() => {
|
||||
return Object.fromEntries(
|
||||
(listQuery.data?.data ?? [])
|
||||
.filter(summary => summary.id)
|
||||
.map(summary => [summary.id!, summary]),
|
||||
)
|
||||
}, [listQuery.data?.data])
|
||||
|
||||
const environmentOptions = useMemo<EnvironmentOption[]>(() => {
|
||||
return ((listQuery.data?.filters ?? []) as DeploymentEnvironmentFilter[])
|
||||
.filter(filter => filter.kind === 'environment' && filter.id)
|
||||
.map(filter => ({
|
||||
id: filter.id,
|
||||
name: filter.name,
|
||||
disabled: filter.disabled,
|
||||
disabledReason: filter.disabledReason,
|
||||
}))
|
||||
}, [listQuery.data?.filters])
|
||||
|
||||
return {
|
||||
apps,
|
||||
appMap,
|
||||
summaries: sourceAppsList.summaries,
|
||||
environmentOptions: sourceAppsList.environmentOptions,
|
||||
summaries,
|
||||
environmentOptions,
|
||||
isLoading: listQuery.isLoading,
|
||||
isFetching: listQuery.isFetching,
|
||||
isError: listQuery.isError,
|
||||
|
||||
@ -19,7 +19,8 @@ import { AppTypeIcon } from '@/app/components/app/type-selector'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { useDeploymentAppData, useDeploymentsStore } from '../store'
|
||||
import { useCachedDeploymentAppData } from '../hooks/use-deployment-data'
|
||||
import { useDeploymentsStore } from '../store'
|
||||
import { deployedRows, deploymentStatus, environmentId, environmentName, releaseLabel } from '../utils'
|
||||
|
||||
type InstanceCardProps = {
|
||||
@ -32,7 +33,7 @@ export const InstanceCard: FC<InstanceCardProps> = ({ app, summary }) => {
|
||||
const router = useRouter()
|
||||
const { formatTimeFromNow } = useFormatTimeFromNow()
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
const appData = useDeploymentAppData(app.id)
|
||||
const { data: appData } = useCachedDeploymentAppData(app.id)
|
||||
const openDeployDrawer = useDeploymentsStore(state => state.openDeployDrawer)
|
||||
|
||||
const navigateToDetail = () => router.push(`/deployments/${app.id}/overview`)
|
||||
|
||||
@ -6,8 +6,9 @@ import { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Nav from '@/app/components/header/nav'
|
||||
import { useParams, useRouter, useSelectedLayoutSegment } from '@/next/navigation'
|
||||
import { useDeploymentAppInfo } from '../hooks/use-deployment-data'
|
||||
import { useSourceApps } from '../hooks/use-source-apps'
|
||||
import { useDeploymentInstance, useDeploymentsStore } from '../store'
|
||||
import { useDeploymentsStore } from '../store'
|
||||
|
||||
const DeploymentsNav = () => {
|
||||
const { t } = useTranslation()
|
||||
@ -18,7 +19,9 @@ const DeploymentsNav = () => {
|
||||
const instanceId = params?.instanceId
|
||||
|
||||
const openCreateInstanceModal = useDeploymentsStore(state => state.openCreateInstanceModal)
|
||||
const currentInstance = useDeploymentInstance(instanceId)
|
||||
const { data: currentInstance } = useDeploymentAppInfo(instanceId, {
|
||||
enabled: isActive && Boolean(instanceId),
|
||||
})
|
||||
|
||||
const { apps } = useSourceApps({ enabled: isActive })
|
||||
|
||||
|
||||
@ -1,19 +1,60 @@
|
||||
import type { DeploymentsStore } from './store/actions'
|
||||
|
||||
import { create } from 'zustand'
|
||||
import { createDeploymentsActions } from './store/actions'
|
||||
import { initialDeploymentsState } from './store/initial-state'
|
||||
import { deploymentsSelectors } from './store/selectors'
|
||||
|
||||
export const useDeploymentsStore = create<DeploymentsStore>()((...parameters) => ({
|
||||
...initialDeploymentsState,
|
||||
...createDeploymentsActions(...parameters),
|
||||
type OpenDeployDrawerParams = {
|
||||
appId: string
|
||||
environmentId?: string
|
||||
releaseId?: string
|
||||
}
|
||||
|
||||
type OpenRollbackParams = {
|
||||
appId: string
|
||||
environmentId: string
|
||||
targetReleaseId: string
|
||||
deploymentId?: string
|
||||
}
|
||||
|
||||
type DeploymentsStore = {
|
||||
deployDrawer: {
|
||||
open: boolean
|
||||
appId?: string
|
||||
environmentId?: string
|
||||
releaseId?: string
|
||||
}
|
||||
rollbackModal: {
|
||||
open: boolean
|
||||
appId?: string
|
||||
environmentId?: string
|
||||
deploymentId?: string
|
||||
targetReleaseId?: string
|
||||
}
|
||||
createInstanceModal: { open: boolean }
|
||||
|
||||
openDeployDrawer: (params: OpenDeployDrawerParams) => void
|
||||
closeDeployDrawer: () => void
|
||||
openRollbackModal: (params: OpenRollbackParams) => void
|
||||
closeRollbackModal: () => void
|
||||
openCreateInstanceModal: () => void
|
||||
closeCreateInstanceModal: () => void
|
||||
}
|
||||
|
||||
export const useDeploymentsStore = create<DeploymentsStore>()(set => ({
|
||||
deployDrawer: { open: false },
|
||||
rollbackModal: { open: false },
|
||||
createInstanceModal: { open: false },
|
||||
|
||||
openDeployDrawer: params => set({
|
||||
deployDrawer: {
|
||||
open: true,
|
||||
appId: params.appId,
|
||||
environmentId: params.environmentId,
|
||||
releaseId: params.releaseId,
|
||||
},
|
||||
}),
|
||||
closeDeployDrawer: () => set({ deployDrawer: { open: false } }),
|
||||
openRollbackModal: ({ appId, environmentId, deploymentId, targetReleaseId }) => set({
|
||||
rollbackModal: { open: true, appId, environmentId, deploymentId, targetReleaseId },
|
||||
}),
|
||||
closeRollbackModal: () => set({ rollbackModal: { open: false } }),
|
||||
openCreateInstanceModal: () => set({ createInstanceModal: { open: true } }),
|
||||
closeCreateInstanceModal: () => set({ createInstanceModal: { open: false } }),
|
||||
}))
|
||||
|
||||
export const useDeploymentInstance = (appId?: string) => {
|
||||
return useDeploymentsStore(deploymentsSelectors.instance(appId))
|
||||
}
|
||||
|
||||
export const useDeploymentAppData = (appId?: string) => {
|
||||
return useDeploymentsStore(deploymentsSelectors.appData(appId))
|
||||
}
|
||||
|
||||
@ -1,365 +0,0 @@
|
||||
import type { StateCreator } from 'zustand'
|
||||
import type {
|
||||
CreateInstanceParams,
|
||||
DeploymentAppData,
|
||||
ListAppDeploymentsQuery,
|
||||
} from '../data'
|
||||
import type { AppInfo } from '../types'
|
||||
import type { DeploymentsState } from './initial-state'
|
||||
import type { AccessSubject, ConsoleReleaseSummary, ListAppDeploymentsReply } from '@/contract/console/deployments'
|
||||
import {
|
||||
cancelDeployment,
|
||||
createApiKey,
|
||||
createAppInstance,
|
||||
createDeployment,
|
||||
deleteApiKey,
|
||||
deleteAppInstance,
|
||||
fetchDeploymentAppData,
|
||||
listAppDeployments,
|
||||
patchAccessChannel,
|
||||
patchDeveloperAPI,
|
||||
refreshDeploymentAppDataWhenReady,
|
||||
toAppInfoFromOverview,
|
||||
toAppInfoFromSummary,
|
||||
undeployEnvironment,
|
||||
updateAppInstance,
|
||||
updateEnvironmentAccessPolicy,
|
||||
waitForAppInstanceInDeploymentList,
|
||||
} from '../data'
|
||||
import {
|
||||
sourceAppsListKey,
|
||||
toSourceAppsList,
|
||||
} from './source-apps'
|
||||
|
||||
export type StartDeployParams = {
|
||||
appId: string
|
||||
environmentId: string
|
||||
releaseId?: string
|
||||
releaseNote?: string
|
||||
}
|
||||
|
||||
type OpenDeployDrawerParams = {
|
||||
appId: string
|
||||
environmentId?: string
|
||||
releaseId?: string
|
||||
}
|
||||
|
||||
type OpenRollbackParams = {
|
||||
appId: string
|
||||
environmentId: string
|
||||
targetReleaseId: string
|
||||
deploymentId?: string
|
||||
}
|
||||
|
||||
export type CreateInstanceResult = {
|
||||
appInstanceId: string
|
||||
initialRelease?: ConsoleReleaseSummary
|
||||
}
|
||||
|
||||
export type DeploymentsAction = {
|
||||
openDeployDrawer: (params: OpenDeployDrawerParams) => void
|
||||
closeDeployDrawer: () => void
|
||||
|
||||
openRollbackModal: (params: OpenRollbackParams) => void
|
||||
closeRollbackModal: () => void
|
||||
|
||||
openCreateInstanceModal: () => void
|
||||
closeCreateInstanceModal: () => void
|
||||
|
||||
upsertInstances: (apps: AppInfo[]) => void
|
||||
applyAppData: (data: DeploymentAppData) => void
|
||||
bumpDeploymentListRefresh: () => void
|
||||
fetchSourceApps: (query: ListAppDeploymentsQuery) => Promise<ListAppDeploymentsReply>
|
||||
fetchAppData: (appId: string) => Promise<DeploymentAppData>
|
||||
refreshAppData: (appId: string) => Promise<void>
|
||||
|
||||
createInstance: (params: CreateInstanceParams) => Promise<CreateInstanceResult>
|
||||
updateInstance: (appId: string, patch: Pick<AppInfo, 'name' | 'description'>) => Promise<void>
|
||||
deleteInstance: (appId: string) => Promise<void>
|
||||
|
||||
startDeploy: (params: StartDeployParams) => Promise<void>
|
||||
retryDeploy: (appId: string, environmentId: string, targetReleaseId: string) => Promise<void>
|
||||
rollbackDeployment: (appId: string, environmentId: string, targetReleaseId: string) => Promise<void>
|
||||
undeployDeployment: (appId: string, environmentId: string, deploymentId?: string, isDeploying?: boolean) => Promise<void>
|
||||
|
||||
generateApiKey: (appId: string, environmentId: string) => Promise<void>
|
||||
revokeApiKey: (appId: string, environmentId: string, apiKeyId: string) => Promise<void>
|
||||
clearCreatedApiToken: () => void
|
||||
toggleAccessChannel: (appId: string, channel: string, enabled: boolean) => Promise<void>
|
||||
setEnvironmentAccessPolicy: (
|
||||
appId: string,
|
||||
environmentId: string,
|
||||
accessMode: string,
|
||||
subjects: AccessSubject[],
|
||||
) => Promise<void>
|
||||
}
|
||||
|
||||
export type DeploymentsStore = DeploymentsState & DeploymentsAction
|
||||
|
||||
type Setter = Parameters<StateCreator<DeploymentsStore>>[0]
|
||||
type Getter = Parameters<StateCreator<DeploymentsStore>>[1]
|
||||
|
||||
class DeploymentsActionImpl implements DeploymentsAction {
|
||||
readonly #get: Getter
|
||||
readonly #set: Setter
|
||||
|
||||
constructor(set: Setter, get: Getter, _api?: unknown) {
|
||||
void _api
|
||||
this.#set = set
|
||||
this.#get = get
|
||||
}
|
||||
|
||||
openDeployDrawer = (params: OpenDeployDrawerParams) => {
|
||||
this.#set({
|
||||
deployDrawer: {
|
||||
open: true,
|
||||
appId: params.appId,
|
||||
environmentId: params.environmentId,
|
||||
releaseId: params.releaseId,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
closeDeployDrawer = () => {
|
||||
this.#set({ deployDrawer: { open: false } })
|
||||
}
|
||||
|
||||
openRollbackModal = ({ appId, environmentId, deploymentId, targetReleaseId }: OpenRollbackParams) => {
|
||||
this.#set({
|
||||
rollbackModal: { open: true, appId, environmentId, deploymentId, targetReleaseId },
|
||||
})
|
||||
}
|
||||
|
||||
closeRollbackModal = () => {
|
||||
this.#set({ rollbackModal: { open: false } })
|
||||
}
|
||||
|
||||
openCreateInstanceModal = () => {
|
||||
this.#set({ createInstanceModal: { open: true } })
|
||||
}
|
||||
|
||||
closeCreateInstanceModal = () => {
|
||||
this.#set({ createInstanceModal: { open: false } })
|
||||
}
|
||||
|
||||
upsertInstances = (apps: AppInfo[]) => {
|
||||
this.#set(state => ({
|
||||
instancesById: apps.reduce((next, app) => {
|
||||
next[app.id] = {
|
||||
...next[app.id],
|
||||
...app,
|
||||
}
|
||||
return next
|
||||
}, { ...state.instancesById }),
|
||||
}))
|
||||
}
|
||||
|
||||
applyAppData = (data: DeploymentAppData) => {
|
||||
this.#set(state => ({
|
||||
appData: {
|
||||
...state.appData,
|
||||
[data.appId]: data,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
bumpDeploymentListRefresh = () => {
|
||||
this.#set(state => ({
|
||||
listRefreshToken: state.listRefreshToken + 1,
|
||||
}))
|
||||
}
|
||||
|
||||
#applySourceAppsList = (query: ListAppDeploymentsQuery, response: ListAppDeploymentsReply) => {
|
||||
this.#set(state => ({
|
||||
sourceAppLists: {
|
||||
...state.sourceAppLists,
|
||||
[sourceAppsListKey(query)]: toSourceAppsList(response),
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
fetchSourceApps = async (query: ListAppDeploymentsQuery) => {
|
||||
const response = await listAppDeployments(query)
|
||||
const apps = response.data
|
||||
?.map(toAppInfoFromSummary)
|
||||
.filter((app): app is AppInfo => Boolean(app)) ?? []
|
||||
this.upsertInstances(apps)
|
||||
this.#applySourceAppsList(query, response)
|
||||
return response
|
||||
}
|
||||
|
||||
fetchAppData = async (appId: string) => {
|
||||
const data = await fetchDeploymentAppData(appId)
|
||||
this.applyAppData(data)
|
||||
const app = toAppInfoFromOverview(data.overview.instance)
|
||||
if (app)
|
||||
this.upsertInstances([app])
|
||||
return data
|
||||
}
|
||||
|
||||
refreshAppData = async (appId: string) => {
|
||||
const data = await fetchDeploymentAppData(appId)
|
||||
this.applyAppData(data)
|
||||
const app = toAppInfoFromOverview(data.overview.instance)
|
||||
if (app)
|
||||
this.upsertInstances([app])
|
||||
}
|
||||
|
||||
createInstance = async ({ sourceAppId, name, description }: CreateInstanceParams) => {
|
||||
const response = await createAppInstance({ sourceAppId, name, description })
|
||||
if (!response.appInstanceId)
|
||||
throw new Error('Create app instance did not return an appInstanceId.')
|
||||
|
||||
this.#set({ createInstanceModal: { open: false } })
|
||||
await Promise.allSettled([
|
||||
refreshDeploymentAppDataWhenReady(response.appInstanceId)
|
||||
.then((data) => {
|
||||
this.applyAppData(data)
|
||||
const app = toAppInfoFromOverview(data.overview.instance)
|
||||
if (app)
|
||||
this.upsertInstances([app])
|
||||
}),
|
||||
waitForAppInstanceInDeploymentList(response.appInstanceId).then((list) => {
|
||||
const query = {
|
||||
pageNumber: 1,
|
||||
resultsPerPage: 100,
|
||||
}
|
||||
const apps = list?.data
|
||||
?.map(toAppInfoFromSummary)
|
||||
.filter((app): app is AppInfo => Boolean(app)) ?? []
|
||||
this.upsertInstances(apps)
|
||||
if (list)
|
||||
this.#applySourceAppsList(query, list)
|
||||
}),
|
||||
])
|
||||
this.bumpDeploymentListRefresh()
|
||||
return {
|
||||
appInstanceId: response.appInstanceId,
|
||||
initialRelease: response.initialRelease,
|
||||
}
|
||||
}
|
||||
|
||||
updateInstance = async (appId: string, patch: Pick<AppInfo, 'name' | 'description'>) => {
|
||||
await updateAppInstance(appId, {
|
||||
name: patch.name,
|
||||
description: patch.description,
|
||||
})
|
||||
await this.refreshAppData(appId)
|
||||
this.bumpDeploymentListRefresh()
|
||||
this.#set(state => ({
|
||||
instancesById: {
|
||||
...state.instancesById,
|
||||
[appId]: {
|
||||
...state.instancesById[appId],
|
||||
id: appId,
|
||||
name: patch.name,
|
||||
mode: state.instancesById[appId]?.mode ?? 'workflow',
|
||||
description: patch.description,
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
deleteInstance = async (appId: string) => {
|
||||
await deleteAppInstance(appId)
|
||||
this.#set((state) => {
|
||||
const { [appId]: _removed, ...appData } = state.appData
|
||||
const { [appId]: _removedInstance, ...instancesById } = state.instancesById
|
||||
return {
|
||||
instancesById,
|
||||
appData,
|
||||
}
|
||||
})
|
||||
this.bumpDeploymentListRefresh()
|
||||
}
|
||||
|
||||
startDeploy = async ({ appId, environmentId, releaseId, releaseNote }: StartDeployParams) => {
|
||||
this.#set({ deployDrawer: { open: false } })
|
||||
await createDeployment({ appId, environmentId, releaseId, releaseNote })
|
||||
await this.refreshAppData(appId)
|
||||
this.bumpDeploymentListRefresh()
|
||||
}
|
||||
|
||||
retryDeploy = async (appId: string, environmentId: string, targetReleaseId: string) => {
|
||||
await createDeployment({ appId, environmentId, releaseId: targetReleaseId })
|
||||
await this.refreshAppData(appId)
|
||||
this.bumpDeploymentListRefresh()
|
||||
}
|
||||
|
||||
rollbackDeployment = async (appId: string, environmentId: string, targetReleaseId: string) => {
|
||||
this.#set({ rollbackModal: { open: false } })
|
||||
await createDeployment({ appId, environmentId, releaseId: targetReleaseId })
|
||||
await this.refreshAppData(appId)
|
||||
this.bumpDeploymentListRefresh()
|
||||
}
|
||||
|
||||
undeployDeployment = async (appId: string, _environmentId: string, runtimeInstanceId?: string, isDeploying?: boolean) => {
|
||||
if (!runtimeInstanceId)
|
||||
return
|
||||
if (isDeploying)
|
||||
await cancelDeployment(appId, runtimeInstanceId)
|
||||
else
|
||||
await undeployEnvironment(appId, runtimeInstanceId)
|
||||
await this.refreshAppData(appId)
|
||||
this.bumpDeploymentListRefresh()
|
||||
}
|
||||
|
||||
generateApiKey = async (appId: string, environmentId: string) => {
|
||||
const appData = this.#get().appData[appId]
|
||||
const existingCount = appData?.accessConfig.developerApi?.apiKeys?.filter(key =>
|
||||
(key.environmentId ?? key.environment?.id) === environmentId,
|
||||
).length ?? 0
|
||||
const environmentName = appData
|
||||
?.environmentDeployments
|
||||
.data
|
||||
?.find(row => row.environment?.id === environmentId)
|
||||
?.environment
|
||||
?.name ?? 'env'
|
||||
const label = `${environmentName}-key-${String(existingCount + 1).padStart(3, '0')}`
|
||||
const response = await createApiKey(appId, environmentId, label)
|
||||
await this.refreshAppData(appId)
|
||||
if (response.apiToken?.token) {
|
||||
this.#set({
|
||||
createdApiToken: {
|
||||
id: response.apiToken.id,
|
||||
appId,
|
||||
environmentId: response.apiToken.environmentId ?? response.apiToken.environment?.id,
|
||||
maskedPrefix: response.apiToken.maskedPrefix ?? response.apiToken.maskedKey,
|
||||
name: response.apiToken.name || label,
|
||||
token: response.apiToken.token,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
revokeApiKey = async (appId: string, _environmentId: string, apiKeyId: string) => {
|
||||
await deleteApiKey(appId, apiKeyId)
|
||||
await this.refreshAppData(appId)
|
||||
}
|
||||
|
||||
clearCreatedApiToken = () => {
|
||||
this.#set({ createdApiToken: undefined })
|
||||
}
|
||||
|
||||
toggleAccessChannel = async (appId: string, channel: string, enabled: boolean) => {
|
||||
if (channel === 'api')
|
||||
await patchDeveloperAPI(appId, enabled)
|
||||
else
|
||||
await patchAccessChannel(appId, enabled)
|
||||
await this.refreshAppData(appId)
|
||||
}
|
||||
|
||||
setEnvironmentAccessPolicy = async (
|
||||
appId: string,
|
||||
environmentId: string,
|
||||
accessMode: string,
|
||||
subjects: AccessSubject[],
|
||||
) => {
|
||||
await updateEnvironmentAccessPolicy(appId, environmentId, accessMode, subjects)
|
||||
await this.refreshAppData(appId)
|
||||
}
|
||||
}
|
||||
|
||||
export const createDeploymentsActions = (
|
||||
...parameters: Parameters<StateCreator<DeploymentsStore>>
|
||||
) => new DeploymentsActionImpl(...parameters)
|
||||
@ -1,44 +0,0 @@
|
||||
import type { DeploymentAppData } from '../data'
|
||||
import type { AppInfo } from '../types'
|
||||
import type { SourceAppsList } from './source-apps'
|
||||
import type { APIToken } from '@/contract/console/deployments'
|
||||
|
||||
export type CreatedApiToken = Pick<APIToken, 'id' | 'environmentId' | 'maskedPrefix' | 'name'> & {
|
||||
appId: string
|
||||
token: string
|
||||
}
|
||||
|
||||
export type DeploymentsState = {
|
||||
instancesById: Record<string, AppInfo>
|
||||
appData: Record<string, DeploymentAppData>
|
||||
sourceAppLists: Record<string, SourceAppsList>
|
||||
listRefreshToken: number
|
||||
createdApiToken?: CreatedApiToken
|
||||
|
||||
deployDrawer: {
|
||||
open: boolean
|
||||
appId?: string
|
||||
environmentId?: string
|
||||
releaseId?: string
|
||||
}
|
||||
rollbackModal: {
|
||||
open: boolean
|
||||
appId?: string
|
||||
environmentId?: string
|
||||
deploymentId?: string
|
||||
targetReleaseId?: string
|
||||
}
|
||||
createInstanceModal: { open: boolean }
|
||||
}
|
||||
|
||||
export const initialDeploymentsState: DeploymentsState = {
|
||||
instancesById: {},
|
||||
appData: {},
|
||||
sourceAppLists: {},
|
||||
listRefreshToken: 0,
|
||||
createdApiToken: undefined,
|
||||
|
||||
deployDrawer: { open: false },
|
||||
rollbackModal: { open: false },
|
||||
createInstanceModal: { open: false },
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
import type { ListAppDeploymentsQuery } from '../data'
|
||||
import type { DeploymentsStore } from './actions'
|
||||
import { emptySourceAppsList, sourceAppsListKey } from './source-apps'
|
||||
|
||||
export const deploymentsSelectors = {
|
||||
appData: (appId?: string) => (state: DeploymentsStore) =>
|
||||
appId ? state.appData[appId] : undefined,
|
||||
createdApiToken: (state: DeploymentsStore) => state.createdApiToken,
|
||||
instance: (appId?: string) => (state: DeploymentsStore) =>
|
||||
appId ? state.instancesById[appId] : undefined,
|
||||
instancesById: (state: DeploymentsStore) => state.instancesById,
|
||||
listRefreshToken: (state: DeploymentsStore) => state.listRefreshToken,
|
||||
sourceAppsList: (query: ListAppDeploymentsQuery) => (state: DeploymentsStore) =>
|
||||
state.sourceAppLists[sourceAppsListKey(query)] ?? emptySourceAppsList,
|
||||
}
|
||||
@ -1,53 +0,0 @@
|
||||
import type { ListAppDeploymentsQuery } from '../data'
|
||||
import type {
|
||||
AppDeploymentSummary,
|
||||
EnvironmentOption,
|
||||
ListAppDeploymentsReply,
|
||||
} from '@/contract/console/deployments'
|
||||
|
||||
export type SourceAppsList = {
|
||||
appIds: string[]
|
||||
summaries: Record<string, AppDeploymentSummary>
|
||||
environmentOptions: EnvironmentOption[]
|
||||
}
|
||||
|
||||
export const emptySourceAppsList: SourceAppsList = {
|
||||
appIds: [],
|
||||
summaries: {},
|
||||
environmentOptions: [],
|
||||
}
|
||||
|
||||
export const sourceAppsListKey = ({
|
||||
environmentId = '',
|
||||
notDeployed = false,
|
||||
pageNumber = 1,
|
||||
query = '',
|
||||
resultsPerPage = 100,
|
||||
}: ListAppDeploymentsQuery) => {
|
||||
return [
|
||||
environmentId,
|
||||
notDeployed ? 'not-deployed' : 'all',
|
||||
pageNumber,
|
||||
resultsPerPage,
|
||||
query.trim(),
|
||||
].join('|')
|
||||
}
|
||||
|
||||
export const toSourceAppsList = (response: ListAppDeploymentsReply): SourceAppsList => {
|
||||
const summaries = Object.fromEntries(
|
||||
(response.data ?? [])
|
||||
.filter(summary => summary.id)
|
||||
.map(summary => [summary.id!, summary]),
|
||||
)
|
||||
|
||||
return {
|
||||
appIds: Object.keys(summaries),
|
||||
summaries,
|
||||
environmentOptions: response.filters
|
||||
?.filter(filter => filter.kind === 'environment' && filter.id)
|
||||
.map(filter => ({
|
||||
id: filter.id,
|
||||
name: filter.name,
|
||||
})) ?? [],
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user