switch back to query

This commit is contained in:
Stephen Zhou 2026-04-30 09:56:50 +08:00
parent f530efeda3
commit 3c77a8fab9
No known key found for this signature in database
21 changed files with 576 additions and 614 deletions

View File

@ -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,

View File

@ -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>

View File

@ -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 (

View File

@ -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>
)

View File

@ -363,7 +363,6 @@ export const EnvironmentPermissionRow: FC<EnvironmentPermissionRowProps> = ({
permissionKeyToAccessMode(nextKind),
nextKind === 'specific' ? policySubjects(nextSubjects) : [],
)
await policyQuery.refetch()
setDraft({})
}
catch {

View File

@ -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')}

View File

@ -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],

View File

@ -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') ?? [],

View File

@ -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')
}}
/>

View File

@ -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],

View File

@ -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)

View File

@ -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,
}
}

View 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,
},
},
}),
}),
])
},
})
}

View File

@ -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,

View File

@ -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`)

View File

@ -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 })

View File

@ -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))
}

View File

@ -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)

View File

@ -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 },
}

View File

@ -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,
}

View File

@ -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,
})) ?? [],
}
}