diff --git a/.gitignore b/.gitignore index 2898e7c588b..9422ebbd6af 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,7 @@ node_modules .settings/ *.sublime-workspace .zed* +.codex/environments # IDE - VSCode .vscode/* diff --git a/apps/console/src/routeTree.gen.ts b/apps/console/src/routeTree.gen.ts index 9bf6c7e1077..866abbfcc35 100644 --- a/apps/console/src/routeTree.gen.ts +++ b/apps/console/src/routeTree.gen.ts @@ -186,6 +186,9 @@ import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdEnviron import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyRouteRouteImport } from './routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/route' import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyIndexRouteImport } from './routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/index' import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilySummaryRouteImport } from './routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/summary' +import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyServiceInformationRouteImport } from './routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/service-information' +import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyOverridesRouteImport } from './routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/overrides' +import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyBlueprintSetupRouteImport } from './routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/blueprint-setup' import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceServiceIdMonitoringAlertsAlertIdEditRouteImport } from './routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/monitoring/alerts.$alertId.edit' import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceServiceIdMonitoringAlertsCreateMetricMetricRouteImport } from './routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/monitoring/alerts.create.metric.$metric' @@ -1646,6 +1649,33 @@ const AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironm AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyRouteRoute, } as any, ) +const AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyServiceInformationRoute = + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyServiceInformationRouteImport.update( + { + id: '/service-information', + path: '/service-information', + getParentRoute: () => + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyRouteRoute, + } as any, + ) +const AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyOverridesRoute = + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyOverridesRouteImport.update( + { + id: '/overrides', + path: '/overrides', + getParentRoute: () => + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyRouteRoute, + } as any, + ) +const AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyBlueprintSetupRoute = + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyBlueprintSetupRouteImport.update( + { + id: '/blueprint-setup', + path: '/blueprint-setup', + getParentRoute: () => + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyRouteRoute, + } as any, + ) const AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceServiceIdMonitoringAlertsAlertIdEditRoute = AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceServiceIdMonitoringAlertsAlertIdEditRouteImport.update( { @@ -1841,6 +1871,9 @@ export interface FileRoutesByFullPath { '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyRouteRouteWithChildren '/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/deployments/logs/$executionId': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceServiceIdDeploymentsLogsExecutionIdRoute '/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/monitoring/alerts/$alertId/edit': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceServiceIdMonitoringAlertsAlertIdEditRoute + '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/blueprint-setup': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyBlueprintSetupRoute + '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/overrides': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyOverridesRoute + '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/service-information': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyServiceInformationRoute '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/summary': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilySummaryRoute '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyIndexRoute '/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/monitoring/alerts/create/metric/$metric': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceServiceIdMonitoringAlertsCreateMetricMetricRoute @@ -2001,6 +2034,9 @@ export interface FileRoutesByTo { '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/terraform': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateTerraformIndexRoute '/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/deployments/logs/$executionId': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceServiceIdDeploymentsLogsExecutionIdRoute '/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/monitoring/alerts/$alertId/edit': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceServiceIdMonitoringAlertsAlertIdEditRoute + '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/blueprint-setup': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyBlueprintSetupRoute + '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/overrides': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyOverridesRoute + '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/service-information': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyServiceInformationRoute '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/summary': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilySummaryRoute '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyIndexRoute '/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/monitoring/alerts/create/metric/$metric': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceServiceIdMonitoringAlertsCreateMetricMetricRoute @@ -2183,6 +2219,9 @@ export interface FileRoutesById { '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyRouteRouteWithChildren '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/deployments/logs/$executionId': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceServiceIdDeploymentsLogsExecutionIdRoute '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/monitoring/alerts/$alertId/edit': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceServiceIdMonitoringAlertsAlertIdEditRoute + '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/blueprint-setup': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyBlueprintSetupRoute + '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/overrides': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyOverridesRoute + '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/service-information': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyServiceInformationRoute '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/summary': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilySummaryRoute '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyIndexRoute '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/monitoring/alerts/create/metric/$metric': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceServiceIdMonitoringAlertsCreateMetricMetricRoute @@ -2365,6 +2404,9 @@ export interface FileRouteTypes { | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily' | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/deployments/logs/$executionId' | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/monitoring/alerts/$alertId/edit' + | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/blueprint-setup' + | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/overrides' + | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/service-information' | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/summary' | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/' | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/monitoring/alerts/create/metric/$metric' @@ -2525,6 +2567,9 @@ export interface FileRouteTypes { | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/terraform' | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/deployments/logs/$executionId' | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/monitoring/alerts/$alertId/edit' + | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/blueprint-setup' + | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/overrides' + | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/service-information' | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/summary' | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily' | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/monitoring/alerts/create/metric/$metric' @@ -2706,6 +2751,9 @@ export interface FileRouteTypes { | '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily' | '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/deployments/logs/$executionId' | '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/monitoring/alerts/$alertId/edit' + | '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/blueprint-setup' + | '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/overrides' + | '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/service-information' | '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/summary' | '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/' | '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/monitoring/alerts/create/metric/$metric' @@ -3959,6 +4007,27 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilySummaryRouteImport parentRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyRouteRoute } + '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/service-information': { + id: '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/service-information' + path: '/service-information' + fullPath: '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/service-information' + preLoaderRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyServiceInformationRouteImport + parentRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyRouteRoute + } + '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/overrides': { + id: '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/overrides' + path: '/overrides' + fullPath: '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/overrides' + preLoaderRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyOverridesRouteImport + parentRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyRouteRoute + } + '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/blueprint-setup': { + id: '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/blueprint-setup' + path: '/blueprint-setup' + fullPath: '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/blueprint-setup' + preLoaderRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyBlueprintSetupRouteImport + parentRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyRouteRoute + } '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/monitoring/alerts/$alertId/edit': { id: '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/monitoring/alerts/$alertId/edit' path: '/$alertId/edit' @@ -4541,12 +4610,21 @@ const AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironm ) interface AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyRouteRouteChildren { + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyBlueprintSetupRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyBlueprintSetupRoute + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyOverridesRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyOverridesRoute + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyServiceInformationRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyServiceInformationRoute AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilySummaryRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilySummaryRoute AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyIndexRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyIndexRoute } const AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyRouteRouteChildren: AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyRouteRouteChildren = { + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyBlueprintSetupRoute: + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyBlueprintSetupRoute, + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyOverridesRoute: + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyOverridesRoute, + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyServiceInformationRoute: + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyServiceInformationRoute, AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilySummaryRoute: AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilySummaryRoute, AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateBlueprintProviderServiceFamilyIndexRoute: diff --git a/apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/blueprint-setup.tsx b/apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/blueprint-setup.tsx new file mode 100644 index 00000000000..42e3ab6ee3e --- /dev/null +++ b/apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/blueprint-setup.tsx @@ -0,0 +1,21 @@ +import { createFileRoute } from '@tanstack/react-router' +import { BlueprintManifestFieldsProvider, BlueprintSetupSection } from '@qovery/domains/services/feature' +import { useDocumentTitle } from '@qovery/shared/util-hooks' + +export const Route = createFileRoute( + '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/blueprint-setup' +)({ + component: RouteComponent, +}) + +function RouteComponent() { + const { serviceFamily } = Route.useParams() + + useDocumentTitle(`${serviceFamily} setup`) + + return ( + + + + ) +} diff --git a/apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/index.tsx b/apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/index.tsx index 19f146e0386..c4dcbd74e02 100644 --- a/apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/index.tsx +++ b/apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/index.tsx @@ -1,6 +1,4 @@ -import { createFileRoute } from '@tanstack/react-router' -import { BlueprintConfigurationView } from '@qovery/domains/services/feature' -import { useDocumentTitle } from '@qovery/shared/util-hooks' +import { Navigate, createFileRoute } from '@tanstack/react-router' export const Route = createFileRoute( '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/' @@ -9,9 +7,13 @@ export const Route = createFileRoute( }) function RouteComponent() { - const { serviceFamily } = Route.useParams() + const { organizationId, projectId, environmentId, provider, serviceFamily } = Route.useParams() - useDocumentTitle(`${serviceFamily} configuration`) - - return + return ( + + ) } diff --git a/apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/overrides.tsx b/apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/overrides.tsx new file mode 100644 index 00000000000..2007ee62850 --- /dev/null +++ b/apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/overrides.tsx @@ -0,0 +1,24 @@ +import { createFileRoute } from '@tanstack/react-router' +import { + BlueprintManifestFieldsProvider, + BlueprintOverridesConfigurationSection, +} from '@qovery/domains/services/feature' +import { useDocumentTitle } from '@qovery/shared/util-hooks' + +export const Route = createFileRoute( + '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/overrides' +)({ + component: RouteComponent, +}) + +function RouteComponent() { + const { serviceFamily } = Route.useParams() + + useDocumentTitle(`${serviceFamily} overrides`) + + return ( + + + + ) +} diff --git a/apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/service-information.tsx b/apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/service-information.tsx new file mode 100644 index 00000000000..e3cdb5e4c56 --- /dev/null +++ b/apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/service-information.tsx @@ -0,0 +1,17 @@ +import { createFileRoute } from '@tanstack/react-router' +import { BlueprintServiceInformationSection } from '@qovery/domains/services/feature' +import { useDocumentTitle } from '@qovery/shared/util-hooks' + +export const Route = createFileRoute( + '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/service-information' +)({ + component: RouteComponent, +}) + +function RouteComponent() { + const { serviceFamily } = Route.useParams() + + useDocumentTitle(`${serviceFamily} configuration`) + + return +} diff --git a/apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/summary.tsx b/apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/summary.tsx index f4a3d97fa97..7803e7fb131 100644 --- a/apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/summary.tsx +++ b/apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily/summary.tsx @@ -1,5 +1,5 @@ import { createFileRoute } from '@tanstack/react-router' -import { BlueprintStepSummary } from '@qovery/domains/services/feature' +import { BlueprintManifestFieldsProvider, BlueprintStepSummary } from '@qovery/domains/services/feature' import { useDocumentTitle } from '@qovery/shared/util-hooks' export const Route = createFileRoute( @@ -11,5 +11,9 @@ export const Route = createFileRoute( function RouteComponent() { useDocumentTitle('Summary - Create service from blueprint') - return + return ( + + + + ) } diff --git a/libs/domains/services/data-access/src/lib/domains-services-data-access.ts b/libs/domains/services/data-access/src/lib/domains-services-data-access.ts index 95586e965f9..2e48a837e0d 100644 --- a/libs/domains/services/data-access/src/lib/domains-services-data-access.ts +++ b/libs/domains/services/data-access/src/lib/domains-services-data-access.ts @@ -302,6 +302,13 @@ export const services = createQueryKeys('services', { return response.data.results }, }), + blueprintUpdate: ({ blueprintId }: { blueprintId: string }) => ({ + queryKey: [blueprintId], + async queryFn() { + const response = await blueprintApi.checkBlueprintUpdate(blueprintId) + return response.data + }, + }), deploymentStatus: (environmentId: string, serviceId: string) => ({ queryKey: [environmentId, serviceId], // NOTE: Value is set by WebSocket diff --git a/libs/domains/services/feature/src/index.ts b/libs/domains/services/feature/src/index.ts index 438cb9322fe..5bdab058317 100644 --- a/libs/domains/services/feature/src/index.ts +++ b/libs/domains/services/feature/src/index.ts @@ -33,6 +33,7 @@ export * from './lib/hooks/use-argocd-manifest/use-argocd-manifest' export * from './lib/hooks/use-blueprint-catalog/use-blueprint-catalog' export * from './lib/hooks/use-blueprint-catalog-service-readme/use-blueprint-catalog-service-readme' export * from './lib/hooks/use-blueprint-catalog-service-manifest/use-blueprint-catalog-service-manifest' +export * from './lib/hooks/use-blueprint-update/use-blueprint-update' export * from './lib/hooks/use-blueprint-service-created-socket/use-blueprint-service-created-socket' export * from './lib/blueprint-query-boundary/blueprint-query-boundary' export * from './lib/hooks/use-service-deployment-and-running-statuses/use-service-deployment-and-running-statuses' diff --git a/libs/domains/services/feature/src/lib/hooks/use-blueprint-catalog-service-manifest/use-blueprint-catalog-service-manifest.ts b/libs/domains/services/feature/src/lib/hooks/use-blueprint-catalog-service-manifest/use-blueprint-catalog-service-manifest.ts index 1e80f409152..dc7d096c69f 100644 --- a/libs/domains/services/feature/src/lib/hooks/use-blueprint-catalog-service-manifest/use-blueprint-catalog-service-manifest.ts +++ b/libs/domains/services/feature/src/lib/hooks/use-blueprint-catalog-service-manifest/use-blueprint-catalog-service-manifest.ts @@ -1,4 +1,5 @@ -import { useQuery } from '@tanstack/react-query' +import { useQuery, useQueryClient } from '@tanstack/react-query' +import { useCallback } from 'react' import { queries } from '@qovery/state/util-queries' export interface UseBlueprintCatalogServiceManifestProps { @@ -39,4 +40,30 @@ export function useBlueprintCatalogServiceManifest({ }) } +export function usePrefetchBlueprintCatalogServiceManifest({ + organizationId, + provider, + serviceFamily, + serviceVersion, + environmentId = '', +}: UseBlueprintCatalogServiceManifestProps) { + const queryClient = useQueryClient() + + return useCallback(async () => { + if (!organizationId || !provider || !serviceFamily || !serviceVersion || !environmentId) { + return + } + + await queryClient.prefetchQuery( + queries.services.blueprintCatalogServiceManifest({ + organizationId, + provider, + serviceFamily, + serviceVersion, + environmentId, + }) + ) + }, [environmentId, organizationId, provider, queryClient, serviceFamily, serviceVersion]) +} + export default useBlueprintCatalogServiceManifest diff --git a/libs/domains/services/feature/src/lib/hooks/use-blueprint-update/use-blueprint-update.ts b/libs/domains/services/feature/src/lib/hooks/use-blueprint-update/use-blueprint-update.ts new file mode 100644 index 00000000000..990d196314a --- /dev/null +++ b/libs/domains/services/feature/src/lib/hooks/use-blueprint-update/use-blueprint-update.ts @@ -0,0 +1,18 @@ +import { useQuery } from '@tanstack/react-query' +import { queries } from '@qovery/state/util-queries' + +export interface UseBlueprintUpdateProps { + blueprintId: string + enabled?: boolean + suspense?: boolean +} + +export function useBlueprintUpdate({ blueprintId, enabled = true, suspense = false }: UseBlueprintUpdateProps) { + return useQuery({ + ...queries.services.blueprintUpdate({ blueprintId }), + enabled, + suspense, + }) +} + +export default useBlueprintUpdate diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/blueprint/blueprint-configuration-view/blueprint-configuration-view.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/blueprint/blueprint-configuration-view/blueprint-configuration-view.tsx index 451ba0b4e41..d3cc7b1348c 100644 --- a/libs/domains/services/feature/src/lib/service-creation-flow/blueprint/blueprint-configuration-view/blueprint-configuration-view.tsx +++ b/libs/domains/services/feature/src/lib/service-creation-flow/blueprint/blueprint-configuration-view/blueprint-configuration-view.tsx @@ -1,7 +1,8 @@ -import { useNavigate, useSearch } from '@tanstack/react-router' -import { useEffect, useState } from 'react' -import { type ServiceCreateSection } from '@qovery/shared/router' -import { Button, FunnelFlowBody, Icon, InputText } from '@qovery/shared/ui' +import { useNavigate, useParams } from '@tanstack/react-router' +import { useEffect, useMemo, useState } from 'react' +import { type Value } from '@qovery/shared/interfaces' +import { Button, FunnelFlowBody, Icon, InputSelect, InputText } from '@qovery/shared/ui' +import { usePrefetchBlueprintCatalogServiceManifest } from '../../../hooks/use-blueprint-catalog-service-manifest/use-blueprint-catalog-service-manifest' import { useBlueprintCreateContext } from '../blueprint-create-context/blueprint-create-context' import { type BlueprintFieldValue, @@ -10,52 +11,43 @@ import { getFieldValidationError, getStringFieldValue, isFieldValid, + sortBlueprintMajorVersions, } from '../blueprint-creation-utils/blueprint-creation-utils' +import { useBlueprintManifestFields } from '../blueprint-manifest-context/blueprint-manifest-context' import { BlueprintManifestVariableInput } from './blueprint-creation-components/blueprint-manifest-variable-input/blueprint-manifest-variable-input' import { BlueprintSection } from './blueprint-creation-components/blueprint-section/blueprint-section' -import { OverridesSection } from './blueprint-creation-components/overrides-section/overrides-section' +import { OverridesSectionCard } from './blueprint-creation-components/overrides-section-card/overrides-section-card' -function isBlueprintConfigurationSection(value: unknown): value is ServiceCreateSection { - return value === 'service-information' || value === 'blueprint-setup' || value === 'overrides' +type BlueprintConfigurationSection = 'service-information' | 'blueprint-setup' | 'overrides' + +interface BlueprintConfigurationViewProps { + currentSection: BlueprintConfigurationSection +} + +export function BlueprintServiceInformationSection() { + return +} + +export function BlueprintSetupSection() { + return } -export function BlueprintConfigurationView() { +export function BlueprintOverridesConfigurationSection() { + return +} + +export function BlueprintConfigurationView({ currentSection }: BlueprintConfigurationViewProps) { const navigate = useNavigate() - const search = useSearch({ strict: false }) as { section?: unknown } - const { - blueprint, - creationFlowUrl, - form, - optionalBlueprintFields, - overridableContextBlueprintFields, - onViewDetails, - requiredBlueprintFields, - serviceVersion, - setCurrentStep, - } = useBlueprintCreateContext() - const initialSection = isBlueprintConfigurationSection(search.section) ? search.section : 'service-information' - const [currentSection, setCurrentSection] = useState(initialSection) - const serviceName = form.watch('serviceName') - const blueprintFieldValues = form.watch('fields') - const hasOverrideFields = optionalBlueprintFields.length > 0 || overridableContextBlueprintFields.length > 0 - const isServiceInformationValid = Boolean(serviceName.trim()) - const isBlueprintSetupValid = requiredBlueprintFields.every((field) => - isFieldValid(field, blueprintFieldValues[field.name]) - ) - const updateFieldValue = (name: string, value: BlueprintFieldValue) => { - form.setValue(getBlueprintFieldPath(name), value, { shouldDirty: true, shouldValidate: true }) + const { blueprint, creationFlowUrl, onViewDetails, setCurrentStep } = useBlueprintCreateContext() + const isServiceInformationValid = useIsServiceInformationValid() + const navigateToSection = (section: BlueprintConfigurationSection) => { + navigate({ to: `${creationFlowUrl}/${section}` }) } useEffect(() => { setCurrentStep(1) }, [setCurrentStep]) - useEffect(() => { - if (isBlueprintConfigurationSection(search.section)) { - setCurrentSection(search.section) - } - }, [search.section]) - return (
@@ -75,30 +67,10 @@ export function BlueprintConfigurationView() { completed={currentSection !== 'service-information'} iconName="circle-info" title="Service information" - onClick={() => setCurrentSection('service-information')} + onClick={() => navigateToSection('service-information')} > {currentSection === 'service-information' && ( - <> - form.setValue('serviceName', event.currentTarget.value)} - autoFocus - /> - - - + navigateToSection('blueprint-setup')} /> )} @@ -108,79 +80,217 @@ export function BlueprintConfigurationView() { disabled={!isServiceInformationValid} iconName="chart-bullet" title="Blueprint setup" - onClick={() => setCurrentSection('blueprint-setup')} + onClick={() => navigateToSection('blueprint-setup')} > - {currentSection === 'blueprint-setup' && ( - <> - {requiredBlueprintFields.map((field, index) => ( - updateFieldValue(field.name, value)} - /> - ))} - - - )} + {currentSection === 'blueprint-setup' && } - setCurrentSection('overrides')} - > - {hasOverrideFields && ( - <> - {optionalBlueprintFields.map((field, index) => ( - updateFieldValue(field.name, value)} - /> - ))} - {overridableContextBlueprintFields.map((field, index) => ( - updateFieldValue(field.name, event.currentTarget.value)} - /> - ))} - - )} - + + {currentSection === 'service-information' ? ( + navigateToSection('overrides')} /> + ) : ( + navigateToSection('overrides')} /> + )} -
- -
+ {currentSection === 'service-information' ? ( + + ) : ( + + )} ) } + +interface ServiceInformationSectionContentProps { + onContinue: () => void +} + +function ServiceInformationSectionContent({ onContinue }: ServiceInformationSectionContentProps) { + const { blueprint, form, organizationId, serviceVersion } = useBlueprintCreateContext() + const { environmentId = '', serviceFamily = blueprint.serviceFamily ?? '' } = useParams({ strict: false }) + const prefetchBlueprintManifestFields = usePrefetchBlueprintCatalogServiceManifest({ + organizationId, + provider: blueprint.provider, + serviceFamily, + serviceVersion, + environmentId, + }) + const [isLoadingBlueprintSetup, setIsLoadingBlueprintSetup] = useState(false) + const serviceName = form.watch('serviceName') + const versionTag = form.watch('versionTag') + const isServiceInformationValid = useIsServiceInformationValid() + const blueprintVersionOptions = useMemo( + () => + sortBlueprintMajorVersions(blueprint.majorVersions).map((majorVersion) => ({ + label: majorVersion.serviceVersion, + value: majorVersion.latestTag, + })), + [blueprint.majorVersions] + ) + const handleContinue = async () => { + setIsLoadingBlueprintSetup(true) + await prefetchBlueprintManifestFields().catch(() => undefined) + onContinue() + } + + return ( + <> + form.setValue('serviceName', event.currentTarget.value)} + autoFocus + /> + {blueprintVersionOptions.length > 1 ? ( + + form.setValue('versionTag', Array.isArray(value) ? value[0] : value, { shouldDirty: true }) + } + isSearchable={blueprintVersionOptions.length > 6} + /> + ) : ( + + )} + + + ) +} + +function BlueprintSetupSectionContent() { + const { form } = useBlueprintCreateContext() + const { requiredBlueprintFields } = useBlueprintManifestFields() + const blueprintFieldValues = form.watch('fields') + const updateFieldValue = (name: string, value: BlueprintFieldValue) => { + form.setValue(getBlueprintFieldPath(name), value, { shouldDirty: true, shouldValidate: true }) + } + + return ( + <> + {requiredBlueprintFields.map((field, index) => ( + updateFieldValue(field.name, value)} + /> + ))} + + ) +} + +interface BlueprintOverridesSectionProps { + currentSection: Exclude + onClick: () => void +} + +function BlueprintOverridesSection({ currentSection, onClick }: BlueprintOverridesSectionProps) { + const { optionalBlueprintFields, overridableContextBlueprintFields } = useBlueprintManifestFields() + const isBlueprintSetupValid = useIsBlueprintSetupValid() + const hasOverrideFields = optionalBlueprintFields.length > 0 || overridableContextBlueprintFields.length > 0 + + return ( + + {currentSection === 'overrides' && hasOverrideFields && } + + ) +} + +function OverridesSectionContent() { + const { form } = useBlueprintCreateContext() + const { optionalBlueprintFields, overridableContextBlueprintFields } = useBlueprintManifestFields() + const blueprintFieldValues = form.watch('fields') + const updateFieldValue = (name: string, value: BlueprintFieldValue) => { + form.setValue(getBlueprintFieldPath(name), value, { shouldDirty: true, shouldValidate: true }) + } + + return ( + <> + {optionalBlueprintFields.map((field, index) => ( + updateFieldValue(field.name, value)} + /> + ))} + {overridableContextBlueprintFields.map((field, index) => ( + updateFieldValue(field.name, event.currentTarget.value)} + /> + ))} + + ) +} + +function ConfirmBlueprintCreationFooter() { + const navigate = useNavigate() + const { creationFlowUrl } = useBlueprintCreateContext() + const isServiceInformationValid = useIsServiceInformationValid() + const isBlueprintSetupValid = useIsBlueprintSetupValid() + + return ( +
+ +
+ ) +} + +function DisabledConfirmBlueprintCreationFooter() { + return ( +
+ +
+ ) +} + +function useIsBlueprintSetupValid() { + const { form } = useBlueprintCreateContext() + const { requiredBlueprintFields } = useBlueprintManifestFields() + const blueprintFieldValues = form.watch('fields') + + return requiredBlueprintFields.every((field) => isFieldValid(field, blueprintFieldValues[field.name])) +} + +function useIsServiceInformationValid() { + const { form } = useBlueprintCreateContext() + const serviceName = form.watch('serviceName') + + return Boolean(serviceName.trim()) +} diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/blueprint/blueprint-configuration-view/blueprint-creation-components/overrides-section/overrides-section.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/blueprint/blueprint-configuration-view/blueprint-creation-components/overrides-section-card/overrides-section-card.tsx similarity index 89% rename from libs/domains/services/feature/src/lib/service-creation-flow/blueprint/blueprint-configuration-view/blueprint-creation-components/overrides-section/overrides-section.tsx rename to libs/domains/services/feature/src/lib/service-creation-flow/blueprint/blueprint-configuration-view/blueprint-creation-components/overrides-section-card/overrides-section-card.tsx index 3a28626c809..bb026ebc65b 100644 --- a/libs/domains/services/feature/src/lib/service-creation-flow/blueprint/blueprint-configuration-view/blueprint-creation-components/overrides-section/overrides-section.tsx +++ b/libs/domains/services/feature/src/lib/service-creation-flow/blueprint/blueprint-configuration-view/blueprint-creation-components/overrides-section-card/overrides-section-card.tsx @@ -2,14 +2,14 @@ import { type ReactNode } from 'react' import { Heading, Icon, Section } from '@qovery/shared/ui' import { BlueprintSection } from '../blueprint-section/blueprint-section' -export interface OverridesSectionProps { +export interface OverridesSectionCardProps { active: boolean children?: ReactNode disabled?: boolean onClick: () => void } -export function OverridesSection({ active, children, disabled = false, onClick }: OverridesSectionProps) { +export function OverridesSectionCard({ active, children, disabled = false, onClick }: OverridesSectionCardProps) { if (!active) { return ( onViewDetails: () => void serviceVersion: string - requiredBlueprintFields: BlueprintManifestVariableField[] - optionalBlueprintFields: BlueprintManifestVariableField[] - overridableContextBlueprintFields: OverridableBlueprintManifestContextVariableField[] } export interface BlueprintCreationFlowProps extends PropsWithChildren { diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/blueprint/blueprint-creation-flow.spec.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/blueprint/blueprint-creation-flow.spec.tsx index f790615b34b..199389a7114 100644 --- a/libs/domains/services/feature/src/lib/service-creation-flow/blueprint/blueprint-creation-flow.spec.tsx +++ b/libs/domains/services/feature/src/lib/service-creation-flow/blueprint/blueprint-creation-flow.spec.tsx @@ -1,18 +1,22 @@ import { useParams } from '@tanstack/react-router' import { act, within } from '@testing-library/react' import type { BlueprintItem, BlueprintManifestResponseResultsInner } from 'qovery-typescript-axios' -import { type ReactNode, useEffect, useState } from 'react' +import { type ReactNode, useEffect, useLayoutEffect, useState } from 'react' +import selectEvent from 'react-select-event' import { renderWithProviders, screen, waitFor } from '@qovery/shared/util-tests' import { - BlueprintConfigurationView, BlueprintCreationFlow, + BlueprintManifestFieldsProvider, + BlueprintOverridesConfigurationSection, + BlueprintServiceInformationSection, + BlueprintSetupSection, BlueprintStepSummary, useBlueprintCreateContext, } from './blueprint-creation-flow' const mockNavigate = jest.fn() -const mockUseSearch = jest.fn() const mockUseBlueprintCatalogServiceManifest = jest.fn() +const mockPrefetchBlueprintManifestFields = jest.fn() const mockUseBlueprintCatalogServiceReadme = jest.fn() const mockUseBlueprintServiceCreatedSocket = jest.fn() const mockCreateBlueprint = jest.fn() @@ -22,7 +26,6 @@ jest.mock('@tanstack/react-router', () => ({ ...jest.requireActual('@tanstack/react-router'), useNavigate: () => mockNavigate, useParams: jest.fn(), - useSearch: () => mockUseSearch(), })) jest.mock('@qovery/shared/ui', () => { @@ -43,6 +46,7 @@ jest.mock('@qovery/shared/ui', () => { jest.mock('../../hooks/use-blueprint-catalog-service-manifest/use-blueprint-catalog-service-manifest', () => ({ useBlueprintCatalogServiceManifest: (props: unknown) => mockUseBlueprintCatalogServiceManifest(props), + usePrefetchBlueprintCatalogServiceManifest: () => mockPrefetchBlueprintManifestFields, })) jest.mock('../../hooks/use-blueprint-catalog-service-readme/use-blueprint-catalog-service-readme', () => ({ @@ -120,25 +124,50 @@ function renderBlueprintFlow(children: JSX.Element) { function FillFormValues({ children }: { children: JSX.Element }) { const { form } = useBlueprintCreateContext() - - useEffect(() => { - form.setValue('serviceName', 'custom-postgres') - form.setValue('fields', { - db_name: 'production', - db_username: 'postgres', - db_password: 'super-secret', - skip_final_snapshot: true, + const [ready, setReady] = useState(false) + + useLayoutEffect(() => { + form.reset({ + ...form.getValues(), + serviceName: 'custom-postgres', + loadedVersionTag: 'aws/postgres/17/1.0.0', + fields: { + db_name: 'production', + db_username: 'postgres', + db_password: 'super-secret', + skip_final_snapshot: true, + }, }) + setReady(true) }, [form]) + if (!ready) { + return null + } + return children } -function BlueprintFlowRouteHarness() { - const [step, setStep] = useState<'configuration' | 'summary'>('configuration') +function WithBlueprintManifestFields({ children }: { children: JSX.Element }) { + return {children} +} + +type BlueprintRouteStep = 'service-information' | 'blueprint-setup' | 'overrides' | 'summary' + +function BlueprintFlowRouteHarness({ flowBlueprint = blueprint }: { flowBlueprint?: BlueprintItem }) { + const [step, setStep] = useState('service-information') useEffect(() => { mockNavigate.mockImplementation((options: { to?: string }) => { + if (options.to?.endsWith('/service-information')) { + setStep('service-information') + } + if (options.to?.endsWith('/blueprint-setup')) { + setStep('blueprint-setup') + } + if (options.to?.endsWith('/overrides')) { + setStep('overrides') + } if (options.to?.endsWith('/summary')) { setStep('summary') } @@ -146,8 +175,23 @@ function BlueprintFlowRouteHarness() { }, []) return ( - - {step === 'configuration' ? : } + + {step === 'service-information' && } + {step === 'blueprint-setup' && ( + + + + )} + {step === 'overrides' && ( + + + + )} + {step === 'summary' && ( + + + + )} ) } @@ -164,7 +208,10 @@ describe('BlueprintCreationFlow', () => { provider: 'AWS', serviceFamily: 'postgres', }) - mockUseBlueprintCatalogServiceManifest.mockReturnValue({ data: manifestFields }) + mockUseBlueprintCatalogServiceManifest.mockReturnValue({ + data: manifestFields, + }) + mockPrefetchBlueprintManifestFields.mockResolvedValue(undefined) mockUseBlueprintCatalogServiceReadme.mockReturnValue({ data: { content: '# AWS RDS PostgreSQL\n\nBlueprint documentation', @@ -172,13 +219,12 @@ describe('BlueprintCreationFlow', () => { }, }) mockCreateBlueprint.mockResolvedValue({ environment_id: 'env-1' }) - mockUseSearch.mockReturnValue({}) }) it('should open the blueprint details drawer from the configuration header', async () => { jest.useFakeTimers() - const { userEvent } = renderBlueprintFlow() + const { userEvent } = renderBlueprintFlow() await userEvent.click(screen.getByRole('button', { name: 'AWS RDS PostgreSQL' })) @@ -191,136 +237,94 @@ describe('BlueprintCreationFlow', () => { expect(within(dialog).queryByRole('link', { name: 'Deploy blueprint' })).not.toBeInTheDocument() }) - it('should navigate to the blueprint summary from the confirm blueprint configuration button', async () => { + it('should focus the first blueprint setup field after continuing from service information', async () => { jest.useFakeTimers() - const { userEvent } = renderBlueprintFlow() + const { userEvent } = renderWithProviders() + await userEvent.type(screen.getByLabelText('Service name'), 'custom-postgres') await userEvent.click(screen.getByRole('button', { name: /continue/i })) - await userEvent.type(await screen.findByLabelText('Db name'), 'production') - await userEvent.type(screen.getByLabelText('Db username'), 'postgres') - await userEvent.type(screen.getByLabelText('Db password'), 'super-secret') - await userEvent.click(screen.getByRole('button', { name: /continue/i })) - await userEvent.click(screen.getByRole('button', { name: /confirm blueprint configuration/i })) - expect(mockNavigate).toHaveBeenCalledWith({ - to: '/organization/org-1/project/proj-1/environment/env-1/service/create/blueprint/AWS/postgres/summary', - }) + expect(await screen.findByLabelText('Db name')).toHaveFocus() }) - it('should focus the first blueprint setup field after continuing from service information', async () => { - jest.useFakeTimers() + it('should prefetch blueprint setup fields before navigating from service information', async () => { + mockPrefetchBlueprintManifestFields.mockImplementation(async () => { + expect(mockNavigate).not.toHaveBeenCalledWith({ + to: '/organization/org-1/project/proj-1/environment/env-1/service/create/blueprint/AWS/postgres/blueprint-setup', + }) + }) - const { userEvent } = renderBlueprintFlow() + const { userEvent } = renderWithProviders() - await userEvent.type(screen.getByLabelText('Service name'), 'custom-postgres') await userEvent.click(screen.getByRole('button', { name: /continue/i })) - expect(await screen.findByLabelText('Db name')).toHaveFocus() + expect(mockPrefetchBlueprintManifestFields).toHaveBeenCalled() + expect(mockNavigate).toHaveBeenCalledWith({ + to: '/organization/org-1/project/proj-1/environment/env-1/service/create/blueprint/AWS/postgres/blueprint-setup', + }) }) - it('should not load manifest fields when a blueprint has no service family', () => { + it('should not load manifest fields from the service information route', () => { renderWithProviders( - + ) - expect(mockUseBlueprintCatalogServiceManifest).toHaveBeenCalledWith( - expect.objectContaining({ - provider: 'AWS', - serviceFamily: '', - enabled: false, - }) - ) + expect(mockUseBlueprintCatalogServiceManifest).not.toHaveBeenCalled() }) - it('should focus the first overrides field after continuing from blueprint setup', async () => { + it('should focus the first overrides field after opening overrides from blueprint setup', async () => { jest.useFakeTimers() - const { userEvent } = renderBlueprintFlow() + const { userEvent } = renderWithProviders() await userEvent.click(screen.getByRole('button', { name: /continue/i })) await userEvent.type(await screen.findByLabelText('Db name'), 'production') await userEvent.type(screen.getByLabelText('Db username'), 'postgres') await userEvent.type(screen.getByLabelText('Db password'), 'super-secret') - await userEvent.click(screen.getByRole('button', { name: /continue/i })) + await userEvent.click(screen.getByRole('button', { name: /overrides/i })) expect(await screen.findByRole('checkbox', { name: 'Skip final snapshot' })).toHaveFocus() }) - it('should open the requested configuration section from route search', async () => { - mockUseSearch.mockReturnValue({ section: 'overrides' }) - - renderBlueprintFlow() - - expect(await screen.findByText(/Use overrides to customize how your service is built or run/)).toBeInTheDocument() - expect(screen.getByRole('checkbox', { name: 'Skip final snapshot' })).toBeInTheDocument() - }) - - it('should fallback to service information when the route search section is invalid', () => { - mockUseSearch.mockReturnValue({ section: 'unknown-section' }) - - renderBlueprintFlow() - - expect(screen.getByLabelText('Service name')).toBeInTheDocument() - expect(screen.queryByLabelText('Db name')).not.toBeInTheDocument() - expect(screen.queryByText(/Use overrides to customize how your service is built or run/)).not.toBeInTheDocument() - }) - - it('should update the current section when the route search section changes to a valid section', async () => { - mockUseSearch.mockReturnValue({}) - - const { rerender } = renderBlueprintFlow() - - expect(screen.getByLabelText('Service name')).toBeInTheDocument() - - mockUseSearch.mockReturnValue({ section: 'blueprint-setup' }) - rerender( - - - - ) - - expect(await screen.findByLabelText('Db name')).toBeInTheDocument() - expect(screen.queryByLabelText('Service name')).not.toBeInTheDocument() - }) - it('should navigate to the matching configuration section from summary edit buttons', async () => { jest.useFakeTimers() const { userEvent } = renderBlueprintFlow( - - - + + + + + ) await screen.findByText(/custom-postgres/) await userEvent.click(screen.getByRole('button', { name: 'Edit service information' })) expect(mockNavigate).toHaveBeenLastCalledWith({ - to: '/organization/org-1/project/proj-1/environment/env-1/service/create/blueprint/AWS/postgres', - search: { section: 'service-information' }, + to: '/organization/org-1/project/proj-1/environment/env-1/service/create/blueprint/AWS/postgres/service-information', }) await userEvent.click(screen.getByRole('button', { name: 'Edit blueprint setup' })) expect(mockNavigate).toHaveBeenLastCalledWith({ - to: '/organization/org-1/project/proj-1/environment/env-1/service/create/blueprint/AWS/postgres', - search: { section: 'blueprint-setup' }, + to: '/organization/org-1/project/proj-1/environment/env-1/service/create/blueprint/AWS/postgres/blueprint-setup', }) await userEvent.click(screen.getByRole('button', { name: 'Edit overrides' })) expect(mockNavigate).toHaveBeenLastCalledWith({ - to: '/organization/org-1/project/proj-1/environment/env-1/service/create/blueprint/AWS/postgres', - search: { section: 'overrides' }, + to: '/organization/org-1/project/proj-1/environment/env-1/service/create/blueprint/AWS/postgres/overrides', }) }) it('should render the summary with the previously filled blueprint values', async () => { renderBlueprintFlow( - - - + + + + + ) expect(screen.getByText('Ready to create your blueprint service')).toBeInTheDocument() @@ -340,7 +344,6 @@ describe('BlueprintCreationFlow', () => { await userEvent.type(await screen.findByLabelText('Db name'), 'production') await userEvent.type(screen.getByLabelText('Db username'), 'postgres') await userEvent.type(screen.getByLabelText('Db password'), 'super-secret') - await userEvent.click(screen.getByRole('button', { name: /continue/i })) await userEvent.click(screen.getByRole('button', { name: /confirm blueprint configuration/i })) expect(await screen.findByText('Ready to create your blueprint service')).toBeInTheDocument() @@ -349,12 +352,47 @@ describe('BlueprintCreationFlow', () => { expect(screen.getByText(/••••••••/)).toBeInTheDocument() }) + it('should send default override values in the create payload when overrides were not opened', async () => { + jest.useFakeTimers() + + const { userEvent } = renderWithProviders() + + await userEvent.click(screen.getByRole('button', { name: /continue/i })) + await userEvent.type(await screen.findByLabelText('Db name'), 'production') + await userEvent.type(screen.getByLabelText('Db username'), 'postgres') + await userEvent.type(screen.getByLabelText('Db password'), 'super-secret') + await userEvent.click(screen.getByRole('button', { name: /confirm blueprint configuration/i })) + expect(await screen.findByText('Ready to create your blueprint service')).toBeInTheDocument() + + await userEvent.click(screen.getByTestId('button-create')) + + await waitFor(() => { + expect(mockCreateBlueprint).toHaveBeenCalledWith( + expect.objectContaining({ + payload: expect.objectContaining({ + variables: expect.arrayContaining([ + { + name: 'skip_final_snapshot', + value: 'true', + is_secret: false, + }, + ]), + }), + }) + ) + }) + }) + it('should redirect to configuration when summary is opened without required blueprint values', async () => { - renderBlueprintFlow() + renderBlueprintFlow( + + + + ) await waitFor(() => { expect(mockNavigate).toHaveBeenCalledWith({ - to: '/organization/org-1/project/proj-1/environment/env-1/service/create/blueprint/AWS/postgres', + to: '/organization/org-1/project/proj-1/environment/env-1/service/create/blueprint/AWS/postgres/blueprint-setup', }) }) }) @@ -363,9 +401,11 @@ describe('BlueprintCreationFlow', () => { jest.useFakeTimers() const { userEvent } = renderBlueprintFlow( - - - + + + + + ) await screen.findByText(/custom-postgres/) @@ -406,6 +446,56 @@ describe('BlueprintCreationFlow', () => { }) }) + it('should sort blueprint versions and send the selected version tag in the create payload', async () => { + jest.useFakeTimers() + const versionedBlueprint: BlueprintItem = { + ...blueprint, + majorVersions: [ + { serviceVersion: '14', latestTag: 'aws/postgres/14/1.0.0' }, + { serviceVersion: '17', latestTag: 'aws/postgres/17/1.0.0' }, + { serviceVersion: '16', latestTag: 'aws/postgres/16/1.0.0' }, + ], + } + + const { userEvent } = renderWithProviders() + + expect(screen.getByText('17')).toBeInTheDocument() + expect(mockUseBlueprintCatalogServiceManifest).not.toHaveBeenCalled() + + await selectEvent.select(screen.getByLabelText('Blueprint version'), '16') + expect(mockUseBlueprintCatalogServiceManifest).not.toHaveBeenCalled() + + await userEvent.click(screen.getByRole('button', { name: /continue/i })) + await screen.findByLabelText('Db name') + await waitFor(() => + expect(mockUseBlueprintCatalogServiceManifest).toHaveBeenCalledWith( + expect.objectContaining({ + serviceVersion: '16', + suspense: true, + }) + ) + ) + await userEvent.type(await screen.findByLabelText('Db name'), 'production') + await userEvent.type(screen.getByLabelText('Db username'), 'postgres') + await userEvent.type(screen.getByLabelText('Db password'), 'super-secret') + await userEvent.click(screen.getByRole('button', { name: /confirm blueprint configuration/i })) + + expect(await screen.findByText('Ready to create your blueprint service')).toBeInTheDocument() + expect(screen.getByText('16')).toBeInTheDocument() + + await userEvent.click(screen.getByTestId('button-create')) + + await waitFor(() => { + expect(mockCreateBlueprint).toHaveBeenCalledWith( + expect.objectContaining({ + payload: expect.objectContaining({ + tag: 'aws/postgres/16/1.0.0', + }), + }) + ) + }) + }) + it('should start listening for the blueprint service-created event before creating the blueprint', async () => { jest.useFakeTimers() let resolveCreateBlueprint: (value: { environment_id: string }) => void = jest.fn() @@ -417,9 +507,11 @@ describe('BlueprintCreationFlow', () => { ) const { userEvent } = renderBlueprintFlow( - - - + + + + + ) await screen.findByText(/custom-postgres/) @@ -503,9 +595,11 @@ describe('BlueprintCreationFlow', () => { ) const { userEvent } = renderBlueprintFlow( - - - + + + + + ) await screen.findByText(/custom-postgres/) diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/blueprint/blueprint-creation-flow.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/blueprint/blueprint-creation-flow.tsx index e062676bb7f..a234cdaf065 100644 --- a/libs/domains/services/feature/src/lib/service-creation-flow/blueprint/blueprint-creation-flow.tsx +++ b/libs/domains/services/feature/src/lib/service-creation-flow/blueprint/blueprint-creation-flow.tsx @@ -3,19 +3,12 @@ import { useMemo, useState } from 'react' import { FormProvider, useForm } from 'react-hook-form' import { FunnelFlow } from '@qovery/shared/ui' import { BlueprintDetailsPanel } from '../../blueprint-details-panel/blueprint-details-panel' -import { useBlueprintCatalogServiceManifest } from '../../hooks/use-blueprint-catalog-service-manifest/use-blueprint-catalog-service-manifest' import { BlueprintCreateContext, type BlueprintCreateFormData, type BlueprintCreationFlowProps, } from './blueprint-create-context/blueprint-create-context' -import { - getDefaultContextFieldValue, - getDefaultFieldValue, - isOptionalVariableField, - isOverridableContextVariableField, - isRequiredVariableField, -} from './blueprint-creation-utils/blueprint-creation-utils' +import { sortBlueprintMajorVersions } from './blueprint-creation-utils/blueprint-creation-utils' export { BlueprintCreateContext, @@ -24,7 +17,16 @@ export { type BlueprintCreationFlowProps, useBlueprintCreateContext, } from './blueprint-create-context/blueprint-create-context' -export { BlueprintConfigurationView } from './blueprint-configuration-view/blueprint-configuration-view' +export { + BlueprintConfigurationView, + BlueprintOverridesConfigurationSection, + BlueprintServiceInformationSection, + BlueprintSetupSection, +} from './blueprint-configuration-view/blueprint-configuration-view' +export { + BlueprintManifestFieldsProvider, + useBlueprintManifestFields, +} from './blueprint-manifest-context/blueprint-manifest-context' export { BlueprintStepSummary } from './blueprint-step-summary/blueprint-step-summary' export const blueprintCreationSteps: { title: string }[] = [{ title: 'Configuration' }, { title: 'Summary' }] @@ -32,7 +34,25 @@ export const blueprintCreationSteps: { title: string }[] = [{ title: 'Configurat export function BlueprintCreationFlow({ blueprint, children, onExit }: BlueprintCreationFlowProps) { const [currentStep, setCurrentStep] = useState(1) const [selectedBlueprint, setSelectedBlueprint] = useState(null) - const serviceVersion = blueprint.majorVersions[0]?.serviceVersion ?? 'latest' + const sortedBlueprintVersions = useMemo( + () => sortBlueprintMajorVersions(blueprint.majorVersions), + [blueprint.majorVersions] + ) + const defaultBlueprintVersion = sortedBlueprintVersions[0] + const defaultVersionTag = defaultBlueprintVersion?.latestTag ?? '' + const form = useForm({ + defaultValues: { + serviceName: blueprint.name, + versionTag: defaultVersionTag, + fields: {}, + }, + mode: 'onChange', + }) + const selectedVersionTag = form.watch('versionTag') + const selectedBlueprintVersion = + sortedBlueprintVersions.find((majorVersion) => majorVersion.latestTag === selectedVersionTag) ?? + defaultBlueprintVersion + const serviceVersion = selectedBlueprintVersion?.serviceVersion ?? 'latest' const serviceFamily = blueprint.serviceFamily ?? '' const { environmentId = '', @@ -41,45 +61,6 @@ export function BlueprintCreationFlow({ blueprint, children, onExit }: Blueprint provider = blueprint.provider, } = useParams({ strict: false }) const creationFlowUrl = `/organization/${organizationId}/project/${projectId}/environment/${environmentId}/service/create/blueprint/${encodeURIComponent(provider)}/${encodeURIComponent(serviceFamily)}` - const { data: blueprintManifestFields = [] } = useBlueprintCatalogServiceManifest({ - organizationId, - provider: blueprint.provider, - serviceFamily, - serviceVersion, - environmentId, - enabled: Boolean(serviceVersion) && Boolean(serviceFamily) && Boolean(environmentId), - suspense: true, - }) - - const requiredBlueprintFields = useMemo( - () => blueprintManifestFields.filter(isRequiredVariableField), - [blueprintManifestFields] - ) - const optionalBlueprintFields = useMemo( - () => blueprintManifestFields.filter(isOptionalVariableField), - [blueprintManifestFields] - ) - const overridableContextBlueprintFields = useMemo( - () => blueprintManifestFields.filter(isOverridableContextVariableField), - [blueprintManifestFields] - ) - const defaultBlueprintFieldValues = useMemo(() => { - const fieldsWithDefaultValue = [...requiredBlueprintFields, ...optionalBlueprintFields] - - return { - ...Object.fromEntries(fieldsWithDefaultValue.map((field) => [field.name, getDefaultFieldValue(field)])), - ...Object.fromEntries( - overridableContextBlueprintFields.map((field) => [field.name, getDefaultContextFieldValue(field)]) - ), - } - }, [optionalBlueprintFields, overridableContextBlueprintFields, requiredBlueprintFields]) - const form = useForm({ - defaultValues: { - serviceName: blueprint.name, - fields: defaultBlueprintFieldValues, - }, - mode: 'onChange', - }) return ( setSelectedBlueprint(blueprint), serviceVersion, - requiredBlueprintFields, - optionalBlueprintFields, - overridableContextBlueprintFields, }} > diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/blueprint/blueprint-creation-utils/blueprint-creation-utils.spec.ts b/libs/domains/services/feature/src/lib/service-creation-flow/blueprint/blueprint-creation-utils/blueprint-creation-utils.spec.ts index 37ad88af0cd..83a2cf41d72 100644 --- a/libs/domains/services/feature/src/lib/service-creation-flow/blueprint/blueprint-creation-utils/blueprint-creation-utils.spec.ts +++ b/libs/domains/services/feature/src/lib/service-creation-flow/blueprint/blueprint-creation-utils/blueprint-creation-utils.spec.ts @@ -1,4 +1,5 @@ import { + type BlueprintMajorVersion, type BlueprintManifestContextVariableField, type BlueprintManifestResponseResultsInner, type BlueprintManifestVariableField, @@ -22,6 +23,7 @@ import { isOverridableContextVariableField, isRequiredVariableField, isVariableField, + sortBlueprintMajorVersions, } from './blueprint-creation-utils' function createVariableField(overrides: Partial = {}): BlueprintManifestVariableField { @@ -62,6 +64,22 @@ describe('blueprint-creation-utils', () => { }) }) + describe('sortBlueprintMajorVersions', () => { + it('sorts blueprint versions from latest to oldest', () => { + const versions: BlueprintMajorVersion[] = [ + { serviceVersion: '9.6', latestTag: 'aws/postgres/9.6/1.0.0' }, + { serviceVersion: '17', latestTag: 'aws/postgres/17/1.0.0' }, + { serviceVersion: '14', latestTag: 'aws/postgres/14/1.0.0' }, + ] + + expect(sortBlueprintMajorVersions(versions)).toEqual([ + { serviceVersion: '17', latestTag: 'aws/postgres/17/1.0.0' }, + { serviceVersion: '14', latestTag: 'aws/postgres/14/1.0.0' }, + { serviceVersion: '9.6', latestTag: 'aws/postgres/9.6/1.0.0' }, + ]) + }) + }) + describe('getDefaultFieldValue', () => { it('returns the default value for string fields', () => { expect(getDefaultFieldValue(createVariableField({ default_value: 'production' }))).toBe('production') diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/blueprint/blueprint-creation-utils/blueprint-creation-utils.ts b/libs/domains/services/feature/src/lib/service-creation-flow/blueprint/blueprint-creation-utils/blueprint-creation-utils.ts index f4b5fc7d753..234434de8f5 100644 --- a/libs/domains/services/feature/src/lib/service-creation-flow/blueprint/blueprint-creation-utils/blueprint-creation-utils.ts +++ b/libs/domains/services/feature/src/lib/service-creation-flow/blueprint/blueprint-creation-utils/blueprint-creation-utils.ts @@ -1,4 +1,5 @@ import { + type BlueprintMajorVersion, type BlueprintManifestContextVariableField, type BlueprintManifestResponseResultsInner, type BlueprintManifestVariableField, @@ -22,6 +23,15 @@ export function formatFieldLabel(name: string) { return `${label.charAt(0).toUpperCase()}${label.slice(1)}` } +export function sortBlueprintMajorVersions(versions: BlueprintMajorVersion[]) { + return [...versions].sort((a, b) => + b.serviceVersion.localeCompare(a.serviceVersion, undefined, { + numeric: true, + sensitivity: 'base', + }) + ) +} + export function getDefaultFieldValue(field: BlueprintManifestVariableField): BlueprintFieldValue { if (field.type.type === 'bool') return field.default_value === 'true' return field.default_value ?? '' @@ -31,6 +41,20 @@ export function getDefaultContextFieldValue(field: BlueprintManifestContextVaria return field.value ?? '' } +export function getDefaultBlueprintFieldValues(blueprintManifestFields: BlueprintManifestResponseResultsInner[]) { + const requiredBlueprintFields = blueprintManifestFields.filter(isRequiredVariableField) + const optionalBlueprintFields = blueprintManifestFields.filter(isOptionalVariableField) + const overridableContextBlueprintFields = blueprintManifestFields.filter(isOverridableContextVariableField) + const fieldsWithDefaultValue = [...requiredBlueprintFields, ...optionalBlueprintFields] + + return { + ...Object.fromEntries(fieldsWithDefaultValue.map((field) => [field.name, getDefaultFieldValue(field)])), + ...Object.fromEntries( + overridableContextBlueprintFields.map((field) => [field.name, getDefaultContextFieldValue(field)]) + ), + } +} + export function getStringFieldValue(value: BlueprintFieldValue | undefined) { return typeof value === 'string' ? value : '' } diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/blueprint/blueprint-manifest-context/blueprint-manifest-context.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/blueprint/blueprint-manifest-context/blueprint-manifest-context.tsx new file mode 100644 index 00000000000..f1de20d906d --- /dev/null +++ b/libs/domains/services/feature/src/lib/service-creation-flow/blueprint/blueprint-manifest-context/blueprint-manifest-context.tsx @@ -0,0 +1,88 @@ +import { useParams } from '@tanstack/react-router' +import { + type BlueprintManifestResponseResultsInner, + type BlueprintManifestVariableField, +} from 'qovery-typescript-axios' +import { type PropsWithChildren, createContext, useContext, useEffect, useMemo } from 'react' +import { useBlueprintCatalogServiceManifest } from '../../../hooks/use-blueprint-catalog-service-manifest/use-blueprint-catalog-service-manifest' +import { useBlueprintCreateContext } from '../blueprint-create-context/blueprint-create-context' +import { + type OverridableBlueprintManifestContextVariableField, + getDefaultBlueprintFieldValues, + isOptionalVariableField, + isOverridableContextVariableField, + isRequiredVariableField, +} from '../blueprint-creation-utils/blueprint-creation-utils' + +interface BlueprintManifestContextInterface { + blueprintManifestFields: BlueprintManifestResponseResultsInner[] + requiredBlueprintFields: BlueprintManifestVariableField[] + optionalBlueprintFields: BlueprintManifestVariableField[] + overridableContextBlueprintFields: OverridableBlueprintManifestContextVariableField[] +} + +const BlueprintManifestContext = createContext(undefined) + +export function BlueprintManifestFieldsProvider({ children }: PropsWithChildren) { + const { blueprint, form, organizationId, serviceVersion } = useBlueprintCreateContext() + const { environmentId = '', serviceFamily = blueprint.serviceFamily ?? '' } = useParams({ strict: false }) + const versionTag = form.watch('versionTag') + const { data: blueprintManifestFields = [] } = useBlueprintCatalogServiceManifest({ + organizationId, + provider: blueprint.provider, + serviceFamily, + serviceVersion, + environmentId, + suspense: true, + }) + + const requiredBlueprintFields = useMemo( + () => blueprintManifestFields.filter(isRequiredVariableField), + [blueprintManifestFields] + ) + const optionalBlueprintFields = useMemo( + () => blueprintManifestFields.filter(isOptionalVariableField), + [blueprintManifestFields] + ) + const overridableContextBlueprintFields = useMemo( + () => blueprintManifestFields.filter(isOverridableContextVariableField), + [blueprintManifestFields] + ) + + useEffect(() => { + const formValues = form.getValues() + + if (formValues.loadedVersionTag === versionTag) { + return + } + + form.reset({ + ...formValues, + loadedVersionTag: versionTag, + fields: getDefaultBlueprintFieldValues(blueprintManifestFields), + }) + }, [blueprintManifestFields, form, versionTag]) + + return ( + + {children} + + ) +} + +export function useBlueprintManifestFields() { + const blueprintManifestContext = useContext(BlueprintManifestContext) + + if (!blueprintManifestContext) { + throw new Error('useBlueprintManifestFields must be used within a BlueprintManifestFieldsProvider') + } + + return blueprintManifestContext +} diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/blueprint/blueprint-step-summary/blueprint-step-summary.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/blueprint/blueprint-step-summary/blueprint-step-summary.tsx index 8a5328247d3..197a89d65cc 100644 --- a/libs/domains/services/feature/src/lib/service-creation-flow/blueprint/blueprint-step-summary/blueprint-step-summary.tsx +++ b/libs/domains/services/feature/src/lib/service-creation-flow/blueprint/blueprint-step-summary/blueprint-step-summary.tsx @@ -2,7 +2,6 @@ import { useNavigate, useParams } from '@tanstack/react-router' import posthog from 'posthog-js' import { type BlueprintCreateRequest } from 'qovery-typescript-axios' import { useCallback, useEffect, useRef, useState } from 'react' -import { type ServiceCreateSection } from '@qovery/shared/router' import { Button, FunnelFlowBody, Heading, Icon, Section, SummaryValue, toast } from '@qovery/shared/ui' import { useBlueprintServiceCreatedSocket } from '../../../hooks/use-blueprint-service-created-socket/use-blueprint-service-created-socket' import { useCreateBlueprint } from '../../../hooks/use-create-blueprint/use-create-blueprint' @@ -13,22 +12,18 @@ import { getSummaryFieldValue, isFieldValid, } from '../blueprint-creation-utils/blueprint-creation-utils' +import { useBlueprintManifestFields } from '../blueprint-manifest-context/blueprint-manifest-context' + +type BlueprintConfigurationSection = 'service-information' | 'blueprint-setup' | 'overrides' const BLUEPRINT_SERVICE_CREATED_FALLBACK_TIMEOUT_MS = 30_000 export function BlueprintStepSummary() { const navigate = useNavigate() const { organizationId = '', projectId = '', environmentId = '' } = useParams({ strict: false }) - const { - blueprint, - creationFlowUrl, - form, - optionalBlueprintFields, - overridableContextBlueprintFields, - requiredBlueprintFields, - serviceVersion, - setCurrentStep, - } = useBlueprintCreateContext() + const { blueprint, creationFlowUrl, form, serviceVersion, setCurrentStep } = useBlueprintCreateContext() + const { optionalBlueprintFields, overridableContextBlueprintFields, requiredBlueprintFields } = + useBlueprintManifestFields() const [submitMode, setSubmitMode] = useState<'create' | 'create-and-deploy' | null>(null) const [isWaitingForServiceCreated, setIsWaitingForServiceCreated] = useState(false) const [pendingBlueprintCreation, setPendingBlueprintCreation] = useState<{ @@ -45,8 +40,8 @@ export function BlueprintStepSummary() { const blueprintFields = [...variableFields, ...overridableContextBlueprintFields] const isBlueprintSetupValid = requiredBlueprintFields.every((field) => isFieldValid(field, fields[field.name])) - const handleEditSection = (section: ServiceCreateSection) => { - navigate({ to: creationFlowUrl, search: { section } }) + const handleEditSection = (section: BlueprintConfigurationSection) => { + navigate({ to: `${creationFlowUrl}/${section}` }) } const navigateToEnvironmentOverview = useCallback(() => { @@ -109,7 +104,7 @@ export function BlueprintStepSummary() { useEffect(() => { if (!serviceName.trim() || !isBlueprintSetupValid) { - navigate({ to: creationFlowUrl }) + navigate({ to: `${creationFlowUrl}/blueprint-setup` }) return } }, [creationFlowUrl, isBlueprintSetupValid, navigate, serviceName]) @@ -183,7 +178,7 @@ export function BlueprintStepSummary() { deploy: withDeploy, payload: { name: formValues.serviceName, - tag: blueprint.majorVersions[0]?.latestTag ?? '', + tag: formValues.versionTag, icon: blueprint.icon, variables: buildBlueprintVariables(formValues.fields, blueprintFields), }, @@ -283,7 +278,12 @@ export function BlueprintStepSummary() {
-
diff --git a/libs/domains/services/feature/src/lib/service-overview/service-header/service-header.spec.tsx b/libs/domains/services/feature/src/lib/service-overview/service-header/service-header.spec.tsx index 147f91fb1c9..91f595522d7 100644 --- a/libs/domains/services/feature/src/lib/service-overview/service-header/service-header.spec.tsx +++ b/libs/domains/services/feature/src/lib/service-overview/service-header/service-header.spec.tsx @@ -7,6 +7,7 @@ import { ServiceHeader } from './service-header' const mockCopyToClipboard = jest.fn() const mockGetDatabaseConnectionUri = jest.fn(() => 'postgres://copied-uri') +const mockUseBlueprintUpdate = jest.fn() const services = { 'application-mock': { id: 'ebb84aa8-91c2-40fb-916d-3a158db354b7', @@ -68,6 +69,28 @@ const services = { }, manifest_revision: '313a525a4ad53b37f0b33f81282c52fef5f8e7d8', }, + 'terraform-mock': { + id: 'terraform-mock', + serviceType: 'TERRAFORM', + service_type: 'TERRAFORM', + name: 'aws-s3-bucket', + description: 'Provisioned from AWS S3 Bucket blueprint', + icon_uri: null, + environment: { + id: 'environment-id', + }, + terraform_files_source: { + git: { + git_repository: { + provider: 'GITHUB', + url: 'https://github.com/qovery-blueprints/s3.git', + name: 'qovery-blueprints/s3', + branch: 'main', + }, + }, + }, + blueprint_id: 'blueprint-id', + }, } jest.mock('@tanstack/react-router', () => ({ @@ -252,6 +275,10 @@ jest.mock('../../hooks/use-master-credentials/use-master-credentials', () => ({ }), })) +jest.mock('../../hooks/use-blueprint-update/use-blueprint-update', () => ({ + useBlueprintUpdate: (props: unknown) => mockUseBlueprintUpdate(props), +})) + jest.mock('../../service-access-modal/service-access-modal', () => ({ getDatabaseConnectionUri: () => mockGetDatabaseConnectionUri(), })) @@ -290,10 +317,12 @@ describe('ServiceHeader', () => { beforeEach(() => { jest.clearAllMocks() mockGetDatabaseConnectionUri.mockReturnValue('postgres://copied-uri') + mockUseBlueprintUpdate.mockReturnValue({ data: undefined }) }) - const renderServiceHeader = (serviceId: 'application-mock' | 'database-mock' | 'job-mock' | 'argocd-mock') => - renderWithProviders() + const renderServiceHeader = ( + serviceId: 'application-mock' | 'database-mock' | 'job-mock' | 'argocd-mock' | 'terraform-mock' + ) => renderWithProviders() it('renders application details and git metadata', () => { renderServiceHeader('application-mock') @@ -341,4 +370,51 @@ describe('ServiceHeader', () => { expect(screen.getByText('main')).toBeInTheDocument() expect(screen.getByRole('button', { name: /313a525/i })).toBeInTheDocument() }) + + it('renders an up to date badge for a current blueprint service', () => { + mockUseBlueprintUpdate.mockReturnValue({ + data: { + is_up_to_date: true, + latest_tag: 'aws/s3/1.0.0', + }, + }) + + renderServiceHeader('terraform-mock') + + expect(mockUseBlueprintUpdate).toHaveBeenCalledWith({ blueprintId: 'blueprint-id', suspense: true }) + expect(screen.getByText('Up to date')).toBeInTheDocument() + expect(screen.queryByText('Update available')).not.toBeInTheDocument() + }) + + it('renders an update available badge for an outdated blueprint service', () => { + mockUseBlueprintUpdate.mockReturnValue({ + data: { + is_up_to_date: false, + latest_tag: 'aws/s3/2.0.0', + }, + }) + + renderServiceHeader('terraform-mock') + + expect(screen.getByText('Update available')).toBeInTheDocument() + expect(screen.queryByText('Up to date')).not.toBeInTheDocument() + }) + + it('renders a skeleton while the blueprint update badge is loading', () => { + mockUseBlueprintUpdate.mockImplementation(() => { + throw new Promise(() => undefined) + }) + + renderServiceHeader('terraform-mock') + + expect(screen.getByRole('generic', { busy: true })).toBeInTheDocument() + }) + + it('does not check blueprint update availability for non-blueprint services', () => { + renderServiceHeader('application-mock') + + expect(mockUseBlueprintUpdate).not.toHaveBeenCalled() + expect(screen.queryByText('Up to date')).not.toBeInTheDocument() + expect(screen.queryByText('Update available')).not.toBeInTheDocument() + }) }) diff --git a/libs/domains/services/feature/src/lib/service-overview/service-header/service-header.tsx b/libs/domains/services/feature/src/lib/service-overview/service-header/service-header.tsx index 4e8361f11d1..a8f01a93036 100644 --- a/libs/domains/services/feature/src/lib/service-overview/service-header/service-header.tsx +++ b/libs/domains/services/feature/src/lib/service-overview/service-header/service-header.tsx @@ -1,5 +1,6 @@ import { Link, useParams } from '@tanstack/react-router' import { type ApplicationGitRepository, type Credentials, type Environment } from 'qovery-typescript-axios' +import { Suspense } from 'react' import { P, match } from 'ts-pattern' import { ClusterAvatar, @@ -23,6 +24,7 @@ import { ExternalLink, Heading, Icon, + Skeleton, Tooltip, Truncate, toast, @@ -32,6 +34,7 @@ import { useCopyToClipboard } from '@qovery/shared/util-hooks' import { containerRegistryKindToIcon, upperCaseFirstLetter } from '@qovery/shared/util-js' import { ArgoCdServiceActions } from '../../argocd-service-actions/argocd-service-actions' import AutoDeployBadge from '../../auto-deploy-badge/auto-deploy-badge' +import { useBlueprintUpdate } from '../../hooks/use-blueprint-update/use-blueprint-update' import { useMasterCredentials } from '../../hooks/use-master-credentials/use-master-credentials' import { getDatabaseConnectionUri } from '../../service-access-modal/service-access-modal' import { ServiceActions } from '../../service-actions/service-actions' @@ -156,6 +159,32 @@ interface ServiceHeaderMetadataProps { service: AnyService } +function BlueprintUpdateBadgeSkeleton() { + return +} + +function BlueprintUpdateBadge({ blueprintId }: { blueprintId: string }) { + const { data: blueprintUpdate } = useBlueprintUpdate({ blueprintId, suspense: true }) + + if (!blueprintUpdate) { + return null + } + + return blueprintUpdate.is_up_to_date ? ( + + + Up to date + + ) : ( + + + + Update available + + + ) +} + function ServiceHeaderMetadata({ service }: ServiceHeaderMetadataProps) { const { organizationId = '', projectId = '', environmentId = '', serviceId = '' } = useParams({ strict: false }) const { data: masterCredentials } = useMasterCredentials({ @@ -163,6 +192,7 @@ function ServiceHeaderMetadata({ service }: ServiceHeaderMetadataProps) { serviceType: service?.service_type, }) const [, copyToClipboard] = useCopyToClipboard() + const blueprintId = 'blueprint_id' in service ? service.blueprint_id : undefined const containerImage = match(service) .with({ serviceType: ServiceTypeEnum.JOB, source: P.when(isJobContainerSource) }, ({ source }) => source.image) @@ -293,6 +323,11 @@ function ServiceHeaderMetadata({ service }: ServiceHeaderMetadataProps) { )} + {blueprintId && ( + }> + + + )} {databaseSource && ( <> diff --git a/libs/shared/router/src/lib/router-types/service-container-create-params.ts b/libs/shared/router/src/lib/router-types/service-container-create-params.ts index 941af759c58..1d31693c9b9 100644 --- a/libs/shared/router/src/lib/router-types/service-container-create-params.ts +++ b/libs/shared/router/src/lib/router-types/service-container-create-params.ts @@ -1,16 +1,11 @@ import { z } from 'zod' const serviceCreateSourceEnum = z.enum(['application', 'container']) -const serviceCreateSectionEnum = z.enum(['service-information', 'blueprint-setup', 'overrides']) - -export type ServiceCreateSection = z.infer export const serviceCreateParamsSchema = z.object({ source: serviceCreateSourceEnum.optional(), template: z.string().optional(), option: z.string().optional(), - // Used by blueprint service creation to reopen a specific configuration section from the summary. - section: serviceCreateSectionEnum.optional(), }) export type ServiceCreateParams = z.infer