import {FormEvent, useEffect, useMemo, useState} from 'react' import {Plus, RefreshCw, Rocket, SlidersHorizontal, Trash2} from 'lucide-react' import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' import { createAppConfigConfigurationProfile, createAppConfigDeploymentStrategy, createAppConfigEnvironment, createAppConfigHostedConfigurationVersion, deleteAppConfigConfigurationProfile, deleteAppConfigDeploymentStrategy, deleteAppConfigEnvironment, deleteAppConfigHostedConfigurationVersion, getAppConfigDeployment, getAppConfigHostedConfigurationVersion, listAppConfigConfigurationProfiles, listAppConfigDeploymentStrategies, listAppConfigEnvironments, listAppConfigHostedConfigurationVersions, startAppConfigDeployment, } from '@/api/cloudProxyClient' import type {CloudProvider} from '@/types/cloud' import type {AppConfigDeployment, CloudResource} from '@/types/resource' import {useAccountId} from '@/lib/accountStore' const TERMINAL_DEPLOYMENT_STATES = new Set(['COMPLETE', 'ROLLED_BACK', 'STOPPED']) const PROFILE_TYPES = ['AWS.Freeform', 'AWS.AppConfig.FeatureFlags'] interface AppConfigPanelProps { cloud: CloudProvider resource?: CloudResource runtimeReachable: boolean } export function AppConfigPanel({cloud, resource, runtimeReachable}: AppConfigPanelProps) { const accountId = useAccountId() return ( ) } interface AccountScopedAppConfigPanelProps extends AppConfigPanelProps { accountId: string } function AccountScopedAppConfigPanel({accountId, cloud, resource, runtimeReachable}: AccountScopedAppConfigPanelProps) { const qc = useQueryClient() const applicationId = resource?.id const [selectedProfileId, setSelectedProfileId] = useState() const [selectedVersionNumber, setSelectedVersionNumber] = useState() const [environmentName, setEnvironmentName] = useState('') const [profileName, setProfileName] = useState('') const [profileType, setProfileType] = useState(PROFILE_TYPES[0]) const [versionContent, setVersionContent] = useState('') const [versionContentType, setVersionContentType] = useState('application/json') const [versionDescription, setVersionDescription] = useState('') const [strategyName, setStrategyName] = useState('') const [strategyBake, setStrategyBake] = useState('') const [deployEnvironmentId, setDeployEnvironmentId] = useState('') const [deployProfileId, setDeployProfileId] = useState('') const [deployVersion, setDeployVersion] = useState('') const [deployStrategyId, setDeployStrategyId] = useState('') const [confirmProfile, setConfirmProfile] = useState(null) const [confirmVersion, setConfirmVersion] = useState(null) const [confirmStrategy, setConfirmStrategy] = useState(null) const [confirmEnvironment, setConfirmEnvironment] = useState(null) const [activeDeployment, setActiveDeployment] = useState() const environmentsKey = useMemo( () => ['appconfig-environments', accountId, cloud, applicationId], [accountId, cloud, applicationId], ) const profilesKey = useMemo( () => ['appconfig-profiles', accountId, cloud, applicationId], [accountId, cloud, applicationId], ) const versionsKey = useMemo( () => ['appconfig-versions', accountId, cloud, applicationId, selectedProfileId], [accountId, cloud, applicationId, selectedProfileId], ) const environmentsQuery = useQuery({ queryKey: environmentsKey, queryFn: ({signal}) => listAppConfigEnvironments(cloud, applicationId ?? '', signal), enabled: Boolean(applicationId) && runtimeReachable, }) const profilesQuery = useQuery({ queryKey: profilesKey, queryFn: ({signal}) => listAppConfigConfigurationProfiles(cloud, applicationId ?? '', signal), enabled: Boolean(applicationId) && runtimeReachable, }) const versionsQuery = useQuery({ queryKey: versionsKey, queryFn: ({signal}) => listAppConfigHostedConfigurationVersions(cloud, applicationId ?? '', selectedProfileId ?? '', signal), enabled: Boolean(applicationId && selectedProfileId) && runtimeReachable, }) const deployVersionsQuery = useQuery({ queryKey: ['appconfig-versions', accountId, cloud, applicationId, deployProfileId], queryFn: ({signal}) => listAppConfigHostedConfigurationVersions(cloud, applicationId ?? '', deployProfileId ?? '', signal), enabled: Boolean(applicationId && deployProfileId) && runtimeReachable, }) const strategiesQuery = useQuery({ queryKey: ['appconfig-strategies', accountId, cloud], queryFn: ({signal}) => listAppConfigDeploymentStrategies(cloud, signal), enabled: runtimeReachable, }) const versionContentQuery = useQuery({ queryKey: ['appconfig-version-content', accountId, cloud, applicationId, selectedProfileId, selectedVersionNumber], queryFn: ({signal}) => getAppConfigHostedConfigurationVersion(cloud, applicationId ?? '', selectedProfileId ?? '', selectedVersionNumber ?? 0, signal), enabled: Boolean(applicationId && selectedProfileId && selectedVersionNumber) && runtimeReachable, }) const deploymentQuery = useQuery({ queryKey: ['appconfig-deployment', accountId, cloud, applicationId, activeDeployment?.environmentId, activeDeployment?.deploymentNumber], queryFn: ({signal}) => getAppConfigDeployment(cloud, applicationId ?? '', activeDeployment?.environmentId ?? '', activeDeployment?.deploymentNumber ?? 0, signal), enabled: Boolean(applicationId && activeDeployment) && runtimeReachable, refetchInterval: (query) => (TERMINAL_DEPLOYMENT_STATES.has(query.state.data?.state ?? '') ? false : 2000), }) const createEnvironmentMut = useMutation({ mutationFn: () => createAppConfigEnvironment(cloud, applicationId ?? '', {name: environmentName}), onSuccess: () => { setEnvironmentName('') void qc.invalidateQueries({queryKey: environmentsKey}) }, }) const deleteEnvironmentMut = useMutation({ mutationFn: (environmentId: string) => deleteAppConfigEnvironment(cloud, applicationId ?? '', environmentId), onSuccess: (_, environmentId) => { if (deployEnvironmentId === environmentId) setDeployEnvironmentId('') if (activeDeployment?.environmentId === environmentId) setActiveDeployment(undefined) setConfirmEnvironment(null) void qc.invalidateQueries({queryKey: environmentsKey}) }, }) const createProfileMut = useMutation({ mutationFn: () => createAppConfigConfigurationProfile(cloud, applicationId ?? '', { name: profileName, locationUri: 'hosted', type: profileType, }), onSuccess: (profile) => { setProfileName('') setSelectedProfileId(profile.id) void qc.invalidateQueries({queryKey: profilesKey}) }, }) const deleteProfileMut = useMutation({ mutationFn: (profileId: string) => deleteAppConfigConfigurationProfile(cloud, applicationId ?? '', profileId), onSuccess: (_, profileId) => { if (selectedProfileId === profileId) setSelectedProfileId(undefined) if (deployProfileId === profileId) { setDeployProfileId('') setDeployVersion('') } setConfirmProfile(null) void qc.invalidateQueries({queryKey: profilesKey}) }, }) const createVersionMut = useMutation({ mutationFn: () => createAppConfigHostedConfigurationVersion(cloud, applicationId ?? '', selectedProfileId ?? '', { content: versionContent, contentType: versionContentType, ...(versionDescription.trim() ? {description: versionDescription} : {}), }), onSuccess: (version) => { setSelectedVersionNumber(version.versionNumber) setVersionContent('') setVersionDescription('') void qc.invalidateQueries({queryKey: versionsKey}) void qc.invalidateQueries({queryKey: ['appconfig-versions', accountId, cloud, applicationId, deployProfileId]}) }, }) const deleteVersionMut = useMutation({ mutationFn: (versionNumber: number) => deleteAppConfigHostedConfigurationVersion(cloud, applicationId ?? '', selectedProfileId ?? '', versionNumber), onSuccess: (_, versionNumber) => { if (selectedVersionNumber === versionNumber) setSelectedVersionNumber(undefined) if (deployVersion === String(versionNumber)) setDeployVersion('') setConfirmVersion(null) void qc.invalidateQueries({queryKey: versionsKey}) }, }) const createStrategyMut = useMutation({ mutationFn: () => createAppConfigDeploymentStrategy(cloud, { name: strategyName, deploymentDurationInMinutes: 0, growthFactor: 100, ...(strategyBake.trim() ? {finalBakeTimeInMinutes: Number(strategyBake)} : {}), }), onSuccess: () => { setStrategyName('') setStrategyBake('') void qc.invalidateQueries({queryKey: ['appconfig-strategies', accountId, cloud]}) }, }) const deleteStrategyMut = useMutation({ mutationFn: (strategyId: string) => deleteAppConfigDeploymentStrategy(cloud, strategyId), onSuccess: (_, strategyId) => { if (deployStrategyId === strategyId) setDeployStrategyId('') setConfirmStrategy(null) void qc.invalidateQueries({queryKey: ['appconfig-strategies', accountId, cloud]}) }, }) const startDeploymentMut = useMutation({ mutationFn: () => startAppConfigDeployment(cloud, applicationId ?? '', deployEnvironmentId, { configurationProfileId: deployProfileId, configurationVersion: deployVersion, deploymentStrategyId: deployStrategyId, }), onSuccess: (deployment) => setActiveDeployment(deployment), }) useEffect(() => { setSelectedProfileId(undefined) setSelectedVersionNumber(undefined) setActiveDeployment(undefined) setDeployEnvironmentId('') setDeployProfileId('') setDeployVersion('') setDeployStrategyId('') }, [cloud, applicationId]) useEffect(() => { setSelectedVersionNumber(undefined) }, [selectedProfileId]) if (cloud !== 'aws') return null if (!applicationId) { return ( Select an application Environments, configuration profiles, hosted versions, and deployments are loaded after an application is selected. ) } const environments = environmentsQuery.data ?? [] const profiles = profilesQuery.data ?? [] const versions = versionsQuery.data ?? [] const strategies = strategiesQuery.data ?? [] const deployment = deploymentQuery.data ?? activeDeployment const canDeploy = Boolean(deployEnvironmentId && deployProfileId && deployVersion && deployStrategyId) && runtimeReachable function submitEnvironment(event: FormEvent) { event.preventDefault() if (environmentName.trim()) createEnvironmentMut.mutate() } function submitProfile(event: FormEvent) { event.preventDefault() if (profileName.trim()) createProfileMut.mutate() } function submitVersion(event: FormEvent) { event.preventDefault() if (versionContent.trim()) createVersionMut.mutate() } function submitStrategy(event: FormEvent) { event.preventDefault() if (strategyName.trim()) createStrategyMut.mutate() } return ( setEnvironmentName(event.target.value)} placeholder="Environment name" disabled={!runtimeReachable}/> {createEnvironmentMut.isPending ? 'Creating' : 'Create'} {createEnvironmentMut.error instanceof Error && {createEnvironmentMut.error.message}} {environmentsQuery.error instanceof Error && {environmentsQuery.error.message}} {deleteEnvironmentMut.error instanceof Error && {deleteEnvironmentMut.error.message}} {environmentsQuery.isLoading && Loading environments} {!environmentsQuery.isLoading && environments.length === 0 && No environments} {environments.map((environment) => ( setDeployEnvironmentId(environment.id)} > {environment.name} {[environment.state, environment.description].filter(Boolean).join(' · ') || 'No details'} {confirmEnvironment === environment.id ? ( { event.stopPropagation() deleteEnvironmentMut.mutate(environment.id) }} > Confirm ) : ( { event.stopPropagation() setConfirmEnvironment(environment.id) }} /> )} ))} setProfileName(event.target.value)} placeholder="Profile name" disabled={!runtimeReachable}/> setProfileType(event.target.value)} disabled={!runtimeReachable}> {PROFILE_TYPES.map((type) => {type})} {createProfileMut.isPending ? 'Creating' : 'Create'} {createProfileMut.error instanceof Error && {createProfileMut.error.message}} {profilesQuery.error instanceof Error && {profilesQuery.error.message}} {deleteProfileMut.error instanceof Error && {deleteProfileMut.error.message}} {profilesQuery.isLoading && Loading profiles} {!profilesQuery.isLoading && profiles.length === 0 && No configuration profiles} {profiles.map((profile) => ( setSelectedProfileId(profile.id)} > {profile.name} {profile.type ?? 'Unknown type'} {confirmProfile === profile.id ? ( { event.stopPropagation() deleteProfileMut.mutate(profile.id) }} > Confirm ) : ( { event.stopPropagation() setConfirmProfile(profile.id) }} /> )} ))} profile.id === selectedProfileId)?.name ?? 'Select profile'} detail={`${versions.length} versions`} /> setVersionContentType(event.target.value)} placeholder="Content type" disabled={!runtimeReachable}/> setVersionDescription(event.target.value)} placeholder="Description (optional)" disabled={!runtimeReachable}/> {createVersionMut.isPending ? 'Saving' : 'Create version'} setVersionContent(event.target.value)} placeholder={selectedProfileId ? 'Configuration content' : 'Select a profile to add a version'} spellCheck={false} disabled={!selectedProfileId || !runtimeReachable} /> {createVersionMut.error instanceof Error && {createVersionMut.error.message}} {versionsQuery.error instanceof Error && {versionsQuery.error.message}} {deleteVersionMut.error instanceof Error && {deleteVersionMut.error.message}} Version Content type Description {versions.map((version) => ( setSelectedVersionNumber(version.versionNumber)}>{version.versionNumber} setSelectedVersionNumber(version.versionNumber)}>{version.contentType ?? '-'} setSelectedVersionNumber(version.versionNumber)}>{version.description ?? '-'} {confirmVersion === version.versionNumber ? ( deleteVersionMut.mutate(version.versionNumber)}>Confirm ) : ( setConfirmVersion(version.versionNumber)}> )} ))} {selectedProfileId && !versionsQuery.isLoading && versions.length === 0 && ( No hosted versionsCreate a version to store configuration content. )} {selectedVersionNumber !== undefined && ( <> Version {selectedVersionNumber} content {versionContentQuery.isLoading && Loading content} {versionContentQuery.error instanceof Error && {versionContentQuery.error.message}} {versionContentQuery.data?.content != null && ( {versionContentQuery.data.content} )} > )} { event.preventDefault() if (canDeploy) startDeploymentMut.mutate() }}> setDeployEnvironmentId(event.target.value)} disabled={!runtimeReachable}> Select environment {environments.map((environment) => {environment.name})} { setDeployProfileId(event.target.value) setDeployVersion('') }} disabled={!runtimeReachable}> Select profile {profiles.map((profile) => {profile.name})} setDeployVersion(event.target.value)} disabled={!runtimeReachable || !deployProfileId}> Select version {(deployVersionsQuery.data ?? []).map((version) => ( v{version.versionNumber} ))} setDeployStrategyId(event.target.value)} disabled={!runtimeReachable}> Select strategy {strategies.map((strategy) => {strategy.name})} {startDeploymentMut.isPending ? 'Deploying' : 'Start deployment'} {startDeploymentMut.error instanceof Error && {startDeploymentMut.error.message}} {deployment && ( Deployment #{deployment.deploymentNumber} {deployment.state ?? 'Unknown state'} {deployment.percentageComplete !== null ? ` · ${deployment.percentageComplete}%` : ''} {!TERMINAL_DEPLOYMENT_STATES.has(deployment.state ?? '') && ( deploymentQuery.refetch()}> )} )} setStrategyName(event.target.value)} placeholder="Immediate strategy name" disabled={!runtimeReachable}/> setStrategyBake(event.target.value)} placeholder="Bake min" disabled={!runtimeReachable}/> {createStrategyMut.isPending ? 'Creating' : 'Create'} {createStrategyMut.error instanceof Error && {createStrategyMut.error.message}} {strategiesQuery.error instanceof Error && {strategiesQuery.error.message}} {deleteStrategyMut.error instanceof Error && {deleteStrategyMut.error.message}} {strategiesQuery.isLoading && Loading strategies} {!strategiesQuery.isLoading && strategies.length === 0 && No deployment strategies} {strategies.map((strategy) => ( {strategy.name} {strategy.deploymentDurationInMinutes ?? 0} min · growth {strategy.growthFactor ?? 0}% · bake {strategy.finalBakeTimeInMinutes ?? 0} min {confirmStrategy === strategy.id ? ( deleteStrategyMut.mutate(strategy.id)}>Confirm ) : ( setConfirmStrategy(strategy.id)}/> )} ))} ) } function ColumnHeader({eyebrow, title, detail}: {eyebrow: string; title: string; detail: string}) { return ( {eyebrow} {title} {detail} ) }
Environments, configuration profiles, hosted versions, and deployments are loaded after an application is selected.
Create a version to store configuration content.
{versionContentQuery.data.content}