diff --git a/web/features/deployments/components/create-instance-modal.tsx b/web/features/deployments/components/create-instance-modal.tsx index 69dc54d95c..a649633f6a 100644 --- a/web/features/deployments/components/create-instance-modal.tsx +++ b/web/features/deployments/components/create-instance-modal.tsx @@ -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 = ({ 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(() => { @@ -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, diff --git a/web/features/deployments/components/deploy-drawer.tsx b/web/features/deployments/components/deploy-drawer.tsx index 8b7a0362fa..2ea1ab97c4 100644 --- a/web/features/deployments/components/deploy-drawer.tsx +++ b/web/features/deployments/components/deploy-drawer.tsx @@ -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, - })} + }) + }} /> )} diff --git a/web/features/deployments/components/rollback-modal.tsx b/web/features/deployments/components/rollback-modal.tsx index aafb77fc81..b4dd227232 100644 --- a/web/features/deployments/components/rollback-modal.tsx +++ b/web/features/deployments/components/rollback-modal.tsx @@ -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 ( diff --git a/web/features/deployments/detail/access-tab.tsx b/web/features/deployments/detail/access-tab.tsx index 8fb7cc6663..b5fbf695f8 100644 --- a/web/features/deployments/detail/access-tab.tsx +++ b/web/features/deployments/detail/access-tab.tsx @@ -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 = ({ 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 = ({ 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 = ({ instanceId: appId }) => { appId={appId} environments={deployedEnvs} policies={policies} - onSetPolicy={setEnvironmentAccessPolicy} + onSetPolicy={handleSetEnvironmentAccessPolicy} /> toggleAccessChannel(appId, 'webapp', enabled)} + onToggle={enabled => toggleAccessChannel.mutate({ appId, channel: 'webapp', enabled })} /> toggleAccessChannel(appId, 'api', enabled)} + createdToken={visibleCreatedApiToken} + onToggle={enabled => toggleAccessChannel.mutate({ appId, channel: 'api', enabled })} onGenerate={handleGenerateApiKey} onRevoke={handleRevokeApiKey} - onClearCreatedToken={clearCreatedApiToken} + onClearCreatedToken={() => setCreatedApiToken(undefined)} /> ) diff --git a/web/features/deployments/detail/access-tab/permissions.tsx b/web/features/deployments/detail/access-tab/permissions.tsx index bc39dde773..26d6afac2e 100644 --- a/web/features/deployments/detail/access-tab/permissions.tsx +++ b/web/features/deployments/detail/access-tab/permissions.tsx @@ -363,7 +363,6 @@ export const EnvironmentPermissionRow: FC = ({ permissionKeyToAccessMode(nextKind), nextKind === 'specific' ? policySubjects(nextSubjects) : [], ) - await policyQuery.refetch() setDraft({}) } catch { diff --git a/web/features/deployments/detail/deploy-tab.tsx b/web/features/deployments/detail/deploy-tab.tsx index c7bd59ae49..a5858ba091 100644 --- a/web/features/deployments/detail/deploy-tab.tsx +++ b/web/features/deployments/detail/deploy-tab.tsx @@ -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 = ({ 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 = ({ instanceId: appId }) => { undeployDeployment(appId, envId, deploymentId(row), status === 'deploying')} + onClick={() => undeployDeployment.mutate({ + appId, + runtimeInstanceId: deploymentId(row), + isDeploying: status === 'deploying', + })} > {status === 'deploying' ? t('deployTab.cancelDeployment') : t('deployTab.undeploy')} diff --git a/web/features/deployments/detail/index.tsx b/web/features/deployments/detail/index.tsx index 57d65cf08f..96bb7a54bd 100644 --- a/web/features/deployments/detail/index.tsx +++ b/web/features/deployments/detail/index.tsx @@ -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 = ({ 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(() => [ - 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], diff --git a/web/features/deployments/detail/overview-tab.tsx b/web/features/deployments/detail/overview-tab.tsx index 99b73d7888..4401d50ea0 100644 --- a/web/features/deployments/detail/overview-tab.tsx +++ b/web/features/deployments/detail/overview-tab.tsx @@ -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 = ({ 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') ?? [], diff --git a/web/features/deployments/detail/settings-tab.tsx b/web/features/deployments/detail/settings-tab.tsx index 7191a3a39e..8764856220 100644 --- a/web/features/deployments/detail/settings-tab.tsx +++ b/web/features/deployments/detail/settings-tab.tsx @@ -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 = ({ app, settings, hasDeployments, on const SettingsTab: FC = ({ 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 = ({ 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') }} /> diff --git a/web/features/deployments/detail/versions-tab.tsx b/web/features/deployments/detail/versions-tab.tsx index 242a16d4dc..4ded426165 100644 --- a/web/features/deployments/detail/versions-tab.tsx +++ b/web/features/deployments/detail/versions-tab.tsx @@ -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 = ({ 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], diff --git a/web/features/deployments/detail/versions-tab/deploy-release-menu.tsx b/web/features/deployments/detail/versions-tab/deploy-release-menu.tsx index 849598b228..2d2a94e469 100644 --- a/web/features/deployments/detail/versions-tab/deploy-release-menu.tsx +++ b/web/features/deployments/detail/versions-tab/deploy-release-menu.tsx @@ -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 = ({ 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) diff --git a/web/features/deployments/hooks/use-deployment-data.ts b/web/features/deployments/hooks/use-deployment-data.ts index f0edfa7e54..6edc4ff9d7 100644 --- a/web/features/deployments/hooks/use-deployment-data.ts +++ b/web/features/deployments/hooks/use-deployment-data.ts @@ -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, } } diff --git a/web/features/deployments/hooks/use-deployment-mutations.ts b/web/features/deployments/hooks/use-deployment-mutations.ts new file mode 100644 index 0000000000..90d5625939 --- /dev/null +++ b/web/features/deployments/hooks/use-deployment-mutations.ts @@ -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 => { + 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( + 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, + }, + }, + }), + }), + ]) + }, + }) +} diff --git a/web/features/deployments/hooks/use-source-apps.ts b/web/features/deployments/hooks/use-source-apps.ts index 3b707c2150..34e553a633 100644 --- a/web/features/deployments/hooks/use-source-apps.ts +++ b/web/features/deployments/hooks/use-source-apps.ts @@ -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(() => { - 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>(() => { return new Map(apps.map(a => [a.id, a])) }, [apps]) + const summaries = useMemo>(() => { + return Object.fromEntries( + (listQuery.data?.data ?? []) + .filter(summary => summary.id) + .map(summary => [summary.id!, summary]), + ) + }, [listQuery.data?.data]) + + const environmentOptions = useMemo(() => { + 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, diff --git a/web/features/deployments/list/instance-card.tsx b/web/features/deployments/list/instance-card.tsx index 0c445eabdc..04086df99c 100644 --- a/web/features/deployments/list/instance-card.tsx +++ b/web/features/deployments/list/instance-card.tsx @@ -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 = ({ 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`) diff --git a/web/features/deployments/nav/index.tsx b/web/features/deployments/nav/index.tsx index 3b65af7b04..9b8c5d862e 100644 --- a/web/features/deployments/nav/index.tsx +++ b/web/features/deployments/nav/index.tsx @@ -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 }) diff --git a/web/features/deployments/store.ts b/web/features/deployments/store.ts index 03e9f07401..5e98605d7c 100644 --- a/web/features/deployments/store.ts +++ b/web/features/deployments/store.ts @@ -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()((...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()(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)) -} diff --git a/web/features/deployments/store/actions.ts b/web/features/deployments/store/actions.ts deleted file mode 100644 index 6a74eb1e21..0000000000 --- a/web/features/deployments/store/actions.ts +++ /dev/null @@ -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 - fetchAppData: (appId: string) => Promise - refreshAppData: (appId: string) => Promise - - createInstance: (params: CreateInstanceParams) => Promise - updateInstance: (appId: string, patch: Pick) => Promise - deleteInstance: (appId: string) => Promise - - startDeploy: (params: StartDeployParams) => Promise - retryDeploy: (appId: string, environmentId: string, targetReleaseId: string) => Promise - rollbackDeployment: (appId: string, environmentId: string, targetReleaseId: string) => Promise - undeployDeployment: (appId: string, environmentId: string, deploymentId?: string, isDeploying?: boolean) => Promise - - generateApiKey: (appId: string, environmentId: string) => Promise - revokeApiKey: (appId: string, environmentId: string, apiKeyId: string) => Promise - clearCreatedApiToken: () => void - toggleAccessChannel: (appId: string, channel: string, enabled: boolean) => Promise - setEnvironmentAccessPolicy: ( - appId: string, - environmentId: string, - accessMode: string, - subjects: AccessSubject[], - ) => Promise -} - -export type DeploymentsStore = DeploymentsState & DeploymentsAction - -type Setter = Parameters>[0] -type Getter = Parameters>[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) => { - 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> -) => new DeploymentsActionImpl(...parameters) diff --git a/web/features/deployments/store/initial-state.ts b/web/features/deployments/store/initial-state.ts deleted file mode 100644 index 9cd2d2d791..0000000000 --- a/web/features/deployments/store/initial-state.ts +++ /dev/null @@ -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 & { - appId: string - token: string -} - -export type DeploymentsState = { - instancesById: Record - appData: Record - sourceAppLists: Record - 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 }, -} diff --git a/web/features/deployments/store/selectors.ts b/web/features/deployments/store/selectors.ts deleted file mode 100644 index 8fee0b0a7c..0000000000 --- a/web/features/deployments/store/selectors.ts +++ /dev/null @@ -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, -} diff --git a/web/features/deployments/store/source-apps.ts b/web/features/deployments/store/source-apps.ts deleted file mode 100644 index bfff4beaae..0000000000 --- a/web/features/deployments/store/source-apps.ts +++ /dev/null @@ -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 - 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, - })) ?? [], - } -}