From f202703b3794fd79523d47879e66f27ff39901f0 Mon Sep 17 00:00:00 2001 From: Mateusz Borowczyk Date: Wed, 29 Jan 2025 09:58:07 +0100 Subject: [PATCH] REplace service queries for react Query (Issue #5975, PR #6172) # Description Replace v1 queries related to services for React-Query closes #5975 # Self Check: Strike through any lines that are not applicable (`~~line~~`) then check the box - [ ] Attached issue to pull request - [ ] Changelog entry - [ ] Code is clear and sufficiently documented - [ ] Sufficient test cases (reproduces the bug/tests the requested feature) - [ ] Correct, in line with design - [ ] End user documentation is included or an issue is created for end-user documentation (add ref to issue here: ) --- .../5975-replace-service-queries.yml | 6 + cypress/e2e/scenario-3-service-details.cy.js | 2 +- .../Managers/V2/DELETE/DeleteService/index.ts | 1 + .../DELETE/DeleteService/useDeleteService.ts | 56 +++++ .../V2/GETTERS/GetAllServiceModels/index.ts | 1 - .../V2/GETTERS/GetServiceConfig/index.ts | 1 + .../GetServiceConfig/useGetServiceConfig.ts | 60 +++++ .../V2/GETTERS/GetServiceModel/index.ts | 2 +- ...tServiceModel.ts => useGetServiceModel.ts} | 3 +- .../V2/GETTERS/GetServiceModels/index.ts | 1 + .../useGetServiceModels.ts} | 35 ++- .../Managers/V2/POST/ExportCatalog/index.ts | 1 + .../V2/POST/ExportCatalog/useExportCatalog.ts | 53 ++++ .../CreateInstance/UI/CreateInstance.test.tsx | 76 ++++-- src/Slices/CreateInstance/UI/Page.tsx | 45 ++-- .../UI/CatalogDataList.test.tsx | 31 +-- src/Slices/ServiceCatalog/UI/Page.test.tsx | 236 ++++++++---------- src/Slices/ServiceCatalog/UI/Page.tsx | 71 +++--- src/Slices/ServiceCatalog/UI/ServiceItem.tsx | 33 ++- .../UI/Spec/CallbacksTab.spec.tsx | 70 +++--- .../ServiceDetails/UI/Spec/ConfigTab.spec.tsx | 93 ++++--- src/Slices/ServiceDetails/UI/Tabs/Config.tsx | 46 ++-- .../CatalogActions/CatalogActions.test.tsx | 131 +++++----- .../CatalogActions/CatalogActions.tsx | 43 ++-- .../Context/ComposerCreatorProvider.tsx | 4 +- .../Context/ComposerEditorProvider.tsx | 5 +- .../Components/RelatedServiceProvider.tsx | 80 +++--- .../ServiceProvider/ServiceProvider.tsx | 50 ++-- .../ServicesProvider/ServicesProvider.tsx | 49 ++-- .../Header/EnvSelector/Provider.tsx | 5 + 30 files changed, 729 insertions(+), 561 deletions(-) create mode 100644 changelogs/unreleased/5975-replace-service-queries.yml create mode 100644 src/Data/Managers/V2/DELETE/DeleteService/index.ts create mode 100644 src/Data/Managers/V2/DELETE/DeleteService/useDeleteService.ts delete mode 100644 src/Data/Managers/V2/GETTERS/GetAllServiceModels/index.ts create mode 100644 src/Data/Managers/V2/GETTERS/GetServiceConfig/index.ts create mode 100644 src/Data/Managers/V2/GETTERS/GetServiceConfig/useGetServiceConfig.ts rename src/Data/Managers/V2/GETTERS/GetServiceModel/{UseGetServiceModel.ts => useGetServiceModel.ts} (93%) create mode 100644 src/Data/Managers/V2/GETTERS/GetServiceModels/index.ts rename src/Data/Managers/V2/GETTERS/{GetAllServiceModels/useGetAllServiceModels.ts => GetServiceModels/useGetServiceModels.ts} (58%) create mode 100644 src/Data/Managers/V2/POST/ExportCatalog/index.ts create mode 100644 src/Data/Managers/V2/POST/ExportCatalog/useExportCatalog.ts diff --git a/changelogs/unreleased/5975-replace-service-queries.yml b/changelogs/unreleased/5975-replace-service-queries.yml new file mode 100644 index 000000000..27f05c967 --- /dev/null +++ b/changelogs/unreleased/5975-replace-service-queries.yml @@ -0,0 +1,6 @@ +description: REplace service queries for react Query +issue-nr: 5975 +change-type: patch +destination-branches: [master, iso8] +sections: + minor-improvement: "{{description}}" diff --git a/cypress/e2e/scenario-3-service-details.cy.js b/cypress/e2e/scenario-3-service-details.cy.js index 83ca48147..aac973698 100644 --- a/cypress/e2e/scenario-3-service-details.cy.js +++ b/cypress/e2e/scenario-3-service-details.cy.js @@ -154,7 +154,7 @@ if (Cypress.env("edition") === "iso") { cy.get("button").contains("Config").click(); // Expect it to have setting in the config - cy.get('[aria-label="ServiceConfig"]').should("be.visible"); + cy.get('[aria-label="ServiceConfig-Success"]').should("be.visible"); cy.get('[aria-label="SettingsList"]').should("have.length", 1); // Go to Callback tab diff --git a/src/Data/Managers/V2/DELETE/DeleteService/index.ts b/src/Data/Managers/V2/DELETE/DeleteService/index.ts new file mode 100644 index 000000000..a0116e1a5 --- /dev/null +++ b/src/Data/Managers/V2/DELETE/DeleteService/index.ts @@ -0,0 +1 @@ +export * from "./useDeleteService"; diff --git a/src/Data/Managers/V2/DELETE/DeleteService/useDeleteService.ts b/src/Data/Managers/V2/DELETE/DeleteService/useDeleteService.ts new file mode 100644 index 000000000..3957acd38 --- /dev/null +++ b/src/Data/Managers/V2/DELETE/DeleteService/useDeleteService.ts @@ -0,0 +1,56 @@ +import { + UseMutationResult, + useMutation, + useQueryClient, +} from "@tanstack/react-query"; +import { PrimaryBaseUrlManager } from "@/UI"; +import { useFetchHelpers } from "../../helpers"; + +/** + * React Query hook for Deleting an Service. + * + * @returns {Mutation} - The mutation object provided by `useMutation` hook. + */ +export const useDeleteService = ( + environment: string, + service_entity: string, +): UseMutationResult => { + const client = useQueryClient(); + const { createHeaders, handleErrors } = useFetchHelpers(); + const headers = createHeaders(environment); + + const baseUrlManager = new PrimaryBaseUrlManager( + globalThis.location.origin, + globalThis.location.pathname, + ); + + const baseUrl = baseUrlManager.getBaseUrl(process.env.API_BASEURL); + + /** + * Delete an Service. + * + * @returns {Promise} - A promise that resolves when the Service is successfully deleted + */ + const deleteService = async (): Promise => { + const response = await fetch( + baseUrl + `/lsm/v1/service_catalog/${service_entity}`, + { + method: "DELETE", + headers: headers, + }, + ); + + await handleErrors(response); + }; + + return useMutation({ + mutationFn: deleteService, + mutationKey: ["delete_service"], + onSuccess: () => { + client.refetchQueries({ queryKey: ["get_service_models-continuous"] }); + client.refetchQueries({ queryKey: ["get_service_models-one_time"] }); + client.refetchQueries({ queryKey: ["get_service_model-one_time"] }); + client.refetchQueries({ queryKey: ["get_service_model-continuous"] }); + }, + }); +}; diff --git a/src/Data/Managers/V2/GETTERS/GetAllServiceModels/index.ts b/src/Data/Managers/V2/GETTERS/GetAllServiceModels/index.ts deleted file mode 100644 index dafc8d005..000000000 --- a/src/Data/Managers/V2/GETTERS/GetAllServiceModels/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./useGetAllServiceModels"; diff --git a/src/Data/Managers/V2/GETTERS/GetServiceConfig/index.ts b/src/Data/Managers/V2/GETTERS/GetServiceConfig/index.ts new file mode 100644 index 000000000..e8ee635fe --- /dev/null +++ b/src/Data/Managers/V2/GETTERS/GetServiceConfig/index.ts @@ -0,0 +1 @@ +export * from "./useGetServiceConfig"; diff --git a/src/Data/Managers/V2/GETTERS/GetServiceConfig/useGetServiceConfig.ts b/src/Data/Managers/V2/GETTERS/GetServiceConfig/useGetServiceConfig.ts new file mode 100644 index 000000000..745242f95 --- /dev/null +++ b/src/Data/Managers/V2/GETTERS/GetServiceConfig/useGetServiceConfig.ts @@ -0,0 +1,60 @@ +import { UseQueryResult, useQuery } from "@tanstack/react-query"; +import { Config } from "@/Core"; +import { PrimaryBaseUrlManager } from "@/UI"; +import { useFetchHelpers } from "../../helpers"; + +/** + * Return Signature of the useGetServiceConfig React Query + */ +interface GetServiceConfig { + useOneTime: () => UseQueryResult; +} + +/** + * React Query hook to fetch the service config + * + * @param service {string} - the service entity + * @param environment {string} - the environment in which the instance belongs + * + * @returns {GetServiceConfig} An object containing the different available queries. + * @returns {UseQueryResult} returns.useOneTime - Fetch the service config with a single query. + */ +export const useGetServiceConfig = ( + service: string, + environment: string, +): GetServiceConfig => { + const { createHeaders, handleErrors } = useFetchHelpers(); + const headers = createHeaders(environment); + + const baseUrlManager = new PrimaryBaseUrlManager( + globalThis.location.origin, + globalThis.location.pathname, + ); + const baseUrl = baseUrlManager.getBaseUrl(process.env.API_BASEURL); + + const fetchInstance = async (): Promise<{ data: Config }> => { + const response = await fetch( + `${baseUrl}/lsm/v1/service_catalog/${service}/config`, + { + headers, + }, + ); + + await handleErrors( + response, + `Failed to fetch Service Config for: ${service}`, + ); + + return response.json(); + }; + + return { + useOneTime: (): UseQueryResult => + useQuery({ + queryKey: ["get_service_config-one_time", service], + queryFn: fetchInstance, + retry: false, + select: (data) => data.data, + }), + }; +}; diff --git a/src/Data/Managers/V2/GETTERS/GetServiceModel/index.ts b/src/Data/Managers/V2/GETTERS/GetServiceModel/index.ts index 81a45cf9a..e8d16ddde 100644 --- a/src/Data/Managers/V2/GETTERS/GetServiceModel/index.ts +++ b/src/Data/Managers/V2/GETTERS/GetServiceModel/index.ts @@ -1 +1 @@ -export * from "./UseGetServiceModel"; +export * from "./useGetServiceModel"; diff --git a/src/Data/Managers/V2/GETTERS/GetServiceModel/UseGetServiceModel.ts b/src/Data/Managers/V2/GETTERS/GetServiceModel/useGetServiceModel.ts similarity index 93% rename from src/Data/Managers/V2/GETTERS/GetServiceModel/UseGetServiceModel.ts rename to src/Data/Managers/V2/GETTERS/GetServiceModel/useGetServiceModel.ts index f93a8fb72..f0dc8461c 100644 --- a/src/Data/Managers/V2/GETTERS/GetServiceModel/UseGetServiceModel.ts +++ b/src/Data/Managers/V2/GETTERS/GetServiceModel/useGetServiceModel.ts @@ -15,7 +15,6 @@ interface GetServiceModel { * React Query hook to fetch the service model * * @param service {string} - the service entity - * @param instanceId {string} - the instance ID for which the data needs to be fetched. * @param environment {string} - the environment in which the instance belongs * * @returns {GetServiceModel} An object containing the different available queries. @@ -37,7 +36,7 @@ export const useGetServiceModel = ( const fetchInstance = async (): Promise<{ data: ServiceModel }> => { const response = await fetch( - `${baseUrl}/lsm/v1/service_catalog/${service}`, + `${baseUrl}/lsm/v1/service_catalog/${service}?instance_summary=True`, { headers, }, diff --git a/src/Data/Managers/V2/GETTERS/GetServiceModels/index.ts b/src/Data/Managers/V2/GETTERS/GetServiceModels/index.ts new file mode 100644 index 000000000..d0da64bb0 --- /dev/null +++ b/src/Data/Managers/V2/GETTERS/GetServiceModels/index.ts @@ -0,0 +1 @@ +export * from "./useGetServiceModels"; diff --git a/src/Data/Managers/V2/GETTERS/GetAllServiceModels/useGetAllServiceModels.ts b/src/Data/Managers/V2/GETTERS/GetServiceModels/useGetServiceModels.ts similarity index 58% rename from src/Data/Managers/V2/GETTERS/GetAllServiceModels/useGetAllServiceModels.ts rename to src/Data/Managers/V2/GETTERS/GetServiceModels/useGetServiceModels.ts index 5595518ee..b6ddcb7b5 100644 --- a/src/Data/Managers/V2/GETTERS/GetAllServiceModels/useGetAllServiceModels.ts +++ b/src/Data/Managers/V2/GETTERS/GetServiceModels/useGetServiceModels.ts @@ -4,25 +4,23 @@ import { PrimaryBaseUrlManager } from "@/UI"; import { useFetchHelpers } from "../../helpers"; /** - * Return Signature of the useGetAllServiceModels React Query + * Return Signature of the useGetServiceModel React Query */ -interface useGetAllServiceModels { +interface GetServiceModels { useOneTime: () => UseQueryResult; useContinuous: () => UseQueryResult; } /** - * React Query hook to fetch all the service models available in the given environment. + * React Query hook to fetch the service models * - * @param environment {string} - the environment in which the instance belongs + * @param environment {string} - the environment in which the services belongs * - * @returns {useGetAllServiceModels} An object containing the different available queries. + * @returns {GetServiceModels} An object containing the different available queries. * @returns {UseQueryResult} returns.useOneTime - Fetch the service models with a single query. - * @returns {UseQueryResult} returns.useContinuous - Fetch the service models with a recursive query with an interval of 5s. + * @returns {UseQueryResult} returns.useContinuous - Fetch the service models with an interval of 5s. */ -export const useGetAllServiceModels = ( - environment: string, -): useGetAllServiceModels => { +export const useGetServiceModels = (environment: string): GetServiceModels => { const { createHeaders, handleErrors } = useFetchHelpers(); const headers = createHeaders(environment); @@ -32,16 +30,13 @@ export const useGetAllServiceModels = ( ); const baseUrl = baseUrlManager.getBaseUrl(process.env.API_BASEURL); - /** - * Fetches all service models from the service catalog. - * - * @returns {Promise<{ data: ServiceModel[] }>} A promise that resolves to an object containing an array of service models. - * @throws Will throw an error if the fetch operation fails. - */ const fetchServices = async (): Promise<{ data: ServiceModel[] }> => { - const response = await fetch(`${baseUrl}/lsm/v1/service_catalog`, { - headers, - }); + const response = await fetch( + `${baseUrl}/lsm/v1/service_catalog?instance_summary=True`, + { + headers, + }, + ); await handleErrors(response, `Failed to fetch Service Models`); @@ -51,14 +46,14 @@ export const useGetAllServiceModels = ( return { useOneTime: (): UseQueryResult => useQuery({ - queryKey: ["get_all_service_models-one_time"], + queryKey: ["get_service_models-one_time"], queryFn: fetchServices, retry: false, select: (data) => data.data, }), useContinuous: (): UseQueryResult => useQuery({ - queryKey: ["get_all_service_models-continuous"], + queryKey: ["get_service_models-continuous"], queryFn: fetchServices, refetchInterval: 5000, select: (data) => data.data, diff --git a/src/Data/Managers/V2/POST/ExportCatalog/index.ts b/src/Data/Managers/V2/POST/ExportCatalog/index.ts new file mode 100644 index 000000000..4f8becc78 --- /dev/null +++ b/src/Data/Managers/V2/POST/ExportCatalog/index.ts @@ -0,0 +1 @@ +export * from "./useExportCatalog"; diff --git a/src/Data/Managers/V2/POST/ExportCatalog/useExportCatalog.ts b/src/Data/Managers/V2/POST/ExportCatalog/useExportCatalog.ts new file mode 100644 index 000000000..a6df7c7b9 --- /dev/null +++ b/src/Data/Managers/V2/POST/ExportCatalog/useExportCatalog.ts @@ -0,0 +1,53 @@ +import { + UseMutationResult, + useMutation, + useQueryClient, +} from "@tanstack/react-query"; +import { PrimaryBaseUrlManager } from "@/UI"; +import { useFetchHelpers } from "../../helpers"; + +/** + * React Query hook for updating environment catalog. + * + * @param {string} environment - The environment to use for creating headers. + * @returns {UseMutationResult}- The mutation object from `useMutation` hook. + */ +export const useExportCatalog = ( + environment: string, +): UseMutationResult => { + const client = useQueryClient(); + const baseUrlManager = new PrimaryBaseUrlManager( + globalThis.location.origin, + globalThis.location.pathname, + ); + const { createHeaders, handleErrors } = useFetchHelpers(); + const headers = createHeaders(environment); + const baseUrl = baseUrlManager.getBaseUrl(process.env.API_BASEURL); + + /** + * Update the environment catalog. + * + * @returns {Promise} - The promise object of the fetch request. + * @throws {Error} If the response is not successful, an error with the error message is thrown. + */ + const updateCatalog = async (): Promise => { + const response = await fetch( + baseUrl + `/lsm/v1/exporter/export_service_definition`, + { + method: "POST", + headers, + }, + ); + + await handleErrors(response); + }; + + return useMutation({ + mutationFn: updateCatalog, + mutationKey: ["update_catalog"], + onSuccess: () => { + client.invalidateQueries({ queryKey: ["get_service_models-one_time"] }); + client.invalidateQueries({ queryKey: ["get_service_models-continuous"] }); + }, + }); +}; diff --git a/src/Slices/CreateInstance/UI/CreateInstance.test.tsx b/src/Slices/CreateInstance/UI/CreateInstance.test.tsx index 02fa8b30b..ae1a24e50 100644 --- a/src/Slices/CreateInstance/UI/CreateInstance.test.tsx +++ b/src/Slices/CreateInstance/UI/CreateInstance.test.tsx @@ -1,9 +1,12 @@ import React, { act } from "react"; import { MemoryRouter } from "react-router-dom"; -import { render, screen, within } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { render, screen, waitFor, within } from "@testing-library/react"; import { userEvent } from "@testing-library/user-event"; import { StoreProvider } from "easy-peasy"; import { configureAxe, toHaveNoViolations } from "jest-axe"; +import { HttpResponse, http } from "msw"; +import { setupServer } from "msw/node"; import { Either } from "@/Core"; import { CommandManagerResolverImpl, @@ -24,6 +27,7 @@ import { InterServiceRelations } from "@/Test/Data/Service"; import { words } from "@/UI"; import { DependencyProvider } from "@/UI/Dependency"; import { CreateInstance } from "./CreateInstance"; + expect.extend(toHaveNoViolations); const axe = configureAxe({ @@ -33,6 +37,15 @@ const axe = configureAxe({ }, }); +const server = setupServer(); +const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, +}); + function setup(service) { const store = getStoreInstance(); const scheduler = new StaticScheduler(); @@ -47,24 +60,40 @@ function setup(service) { ); const component = ( - - - - - - - + + + + + + + + + ); return { component, apiHelper, scheduler }; } +beforeAll(() => { + server.listen(); + server.use( + http.get("/lsm/v1/service_catalog/service_name_a", () => { + return HttpResponse.json({ data: Service.withIdentity }); + }), + http.get("/lsm/v1/service_catalog/test_entity", () => { + return HttpResponse.json({ data: Service.withIdentity }); + }), + ); +}); + +afterAll(() => server.close()); + test("Given the CreateInstance View When creating an instance with attributes Then the correct request is fired", async () => { const { component, apiHelper } = setup(Service.a); @@ -134,9 +163,8 @@ test("Given the CreateInstance View When creating an instance with Inter-service render(component); - await act(async () => { - apiHelper.resolve(Either.right({ data: Service.withIdentity })); - }); + await screen.findByPlaceholderText("Select an instance of test_entity"); // await for the relation input be rendered + await act(async () => { apiHelper.resolve( Either.right({ data: [ServiceInstance.a, ServiceInstance.b] }), @@ -148,12 +176,16 @@ test("Given the CreateInstance View When creating an instance with Inter-service ); }); - const relationInputField = screen.getByPlaceholderText( - words("common.serviceInstance.relations")("test_entity"), + const relationInputField = screen.getByLabelText( + "test_entity-select-toggleFilterInput", ); await userEvent.type(relationInputField, "a"); + await waitFor(() => { + expect(apiHelper.pendingRequests.length).toBeGreaterThan(0); + }); + await act(async () => { apiHelper.resolve(Either.right({ data: [ServiceInstance.a] })); }); @@ -191,9 +223,6 @@ test("Given the CreateInstance View When creating an instance with Inter-service render(component); - await act(async () => { - apiHelper.resolve(Either.right({ data: Service.withIdentity })); - }); await act(async () => { apiHelper.resolve( Either.right({ data: [ServiceInstance.a, ServiceInstance.b] }), @@ -255,9 +284,6 @@ test("Given the CreateInstance View When creating an instance with Inter-service render(component); - await act(async () => { - apiHelper.resolve(Either.right({ data: Service.withIdentity })); - }); await act(async () => { apiHelper.resolve( Either.right({ data: [ServiceInstance.a, ServiceInstance.b] }), diff --git a/src/Slices/CreateInstance/UI/Page.tsx b/src/Slices/CreateInstance/UI/Page.tsx index adea2c1a5..3a2e843bc 100644 --- a/src/Slices/CreateInstance/UI/Page.tsx +++ b/src/Slices/CreateInstance/UI/Page.tsx @@ -1,5 +1,6 @@ import React, { useContext } from "react"; -import { PageContainer, RemoteDataView } from "@/UI/Components"; +import { useGetServiceModel } from "@/Data/Managers/V2/GETTERS/GetServiceModel"; +import { ErrorView, LoadingView, PageContainer } from "@/UI/Components"; import { DependencyContext } from "@/UI/Dependency"; import { useRouteParams } from "@/UI/Routing"; import { words } from "@/UI/words"; @@ -7,25 +8,37 @@ import { CreateInstance } from "./CreateInstance"; export const Page: React.FC = () => { const { service: serviceName } = useRouteParams<"CreateInstance">(); - const { queryResolver } = useContext(DependencyContext); + const { environmentHandler } = useContext(DependencyContext); + const env = environmentHandler.useId(); - const [data, retry] = queryResolver.useContinuous<"GetService">({ - kind: "GetService", - name: serviceName, - }); + const { data, isError, error, isSuccess, refetch } = useGetServiceModel( + serviceName, + env, + ).useContinuous(); - return ( + if (isError) { - ( -
- -
- )} + +
; + } + + if (isSuccess) { + return ( + +
+ +
+
+ ); + } + + return ( + + ); }; diff --git a/src/Slices/ServiceCatalog/UI/CatalogDataList.test.tsx b/src/Slices/ServiceCatalog/UI/CatalogDataList.test.tsx index f97032ec3..715960df9 100644 --- a/src/Slices/ServiceCatalog/UI/CatalogDataList.test.tsx +++ b/src/Slices/ServiceCatalog/UI/CatalogDataList.test.tsx @@ -1,20 +1,11 @@ import React, { act } from "react"; import { MemoryRouter } from "react-router"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { render, screen, within } from "@testing-library/react"; import { userEvent } from "@testing-library/user-event"; import { configureAxe, toHaveNoViolations } from "jest-axe"; import { ServiceModel } from "@/Core"; -import { - BaseApiHelper, - CommandResolverImpl, - DeleteServiceCommandManager, -} from "@/Data"; -import { defaultAuthContext } from "@/Data/Auth/AuthContext"; -import { - dependencies, - DynamicCommandManagerResolverImpl, - Service, -} from "@/Test"; +import { dependencies, Service } from "@/Test"; import { words } from "@/UI"; import { DependencyProvider } from "@/UI/Dependency"; import { CatalogDataList } from "./CatalogDataList"; @@ -29,18 +20,16 @@ const axe = configureAxe({ }); const Component = (services: ServiceModel[]) => { - const commandResolver = new CommandResolverImpl( - new DynamicCommandManagerResolverImpl([ - DeleteServiceCommandManager(BaseApiHelper(undefined, defaultAuthContext)), - ]), - ); + const client = new QueryClient(); return ( - - - - - + + + + + + + ); }; diff --git a/src/Slices/ServiceCatalog/UI/Page.test.tsx b/src/Slices/ServiceCatalog/UI/Page.test.tsx index dc983cef6..db9468a24 100644 --- a/src/Slices/ServiceCatalog/UI/Page.test.tsx +++ b/src/Slices/ServiceCatalog/UI/Page.test.tsx @@ -1,55 +1,30 @@ import React, { act } from "react"; -import { Link, MemoryRouter, useLocation } from "react-router-dom"; +import { MemoryRouter, useLocation } from "react-router-dom"; import { Page } from "@patternfly/react-core"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { render, screen } from "@testing-library/react"; import { userEvent } from "@testing-library/user-event"; import { StoreProvider } from "easy-peasy"; import { axe, toHaveNoViolations } from "jest-axe"; -import { Either, RemoteData } from "@/Core"; -import { - QueryResolverImpl, - ServicesQueryManager, - ServicesStateHelper, - getStoreInstance, - CommandResolverImpl, - CommandManagerResolverImpl, - defaultAuthContext, -} from "@/Data"; -import { - DeferredApiHelper, - dependencies, - DynamicQueryManagerResolverImpl, - Environment, - Service, - StaticScheduler, -} from "@/Test"; +import { HttpResponse, http } from "msw"; +import { setupServer } from "msw/node"; +import { RemoteData, ServiceModel } from "@/Core"; +import { getStoreInstance } from "@/Data"; +import { dependencies, Environment, Service } from "@/Test"; import { words } from "@/UI"; import { DependencyProvider, EnvironmentHandlerImpl } from "@/UI/Dependency"; import { ModalProvider } from "@/UI/Root/Components/ModalProvider"; import { ServiceCatalogPage } from "."; +const server = setupServer(); + expect.extend(toHaveNoViolations); -const [env1, env2] = Environment.filterable.map((env) => env.id); +const [env1] = Environment.filterable.map((env) => env.id); function setup() { + const client = new QueryClient(); const store = getStoreInstance(); - const scheduler = new StaticScheduler(); - const apiHelper = new DeferredApiHelper(); - - const servicesHelper = ServicesQueryManager( - apiHelper, - ServicesStateHelper(store), - scheduler, - ); - - const queryResolver = new QueryResolverImpl( - new DynamicQueryManagerResolverImpl([servicesHelper]), - ); - - const commandResolver = new CommandResolverImpl( - new CommandManagerResolverImpl(store, apiHelper, defaultAuthContext), - ); const environmentHandler = EnvironmentHandlerImpl( useLocation, @@ -60,45 +35,53 @@ function setup() { RemoteData.success(Environment.filterable), ); - const linkToEnv2 = ( - - - - ); - const component = ( - - - + + - - - - {linkToEnv2} - - - - - + + + + + + + + + + ); return { component, - apiHelper, - scheduler, }; } +beforeAll(() => server.listen()); -test("ServiceCatalog shows updated services", async () => { - const { component, apiHelper, scheduler } = setup(); +beforeEach(() => { + server.resetHandlers(); +}); + +afterAll(() => { + server.close(); +}); + +test("ServiceCatalog shows empty state", async () => { + server.use( + http.get("/lsm/v1/service_catalog", () => { + return HttpResponse.json({ data: [] }); + }), + ); + + const { component } = setup(); render(component); @@ -106,20 +89,9 @@ test("ServiceCatalog shows updated services", async () => { await screen.findByRole("region", { name: "ServiceCatalog-Loading" }), ).toBeInTheDocument(); - apiHelper.resolve(Either.right({ data: [] })); - expect( await screen.findByRole("generic", { name: "ServiceCatalog-Empty" }), ).toBeInTheDocument(); - expect(await screen.findByText("Update Service Catalog")).toBeInTheDocument(); - - scheduler.executeAll(); - - apiHelper.resolve(Either.right({ data: [Service.a] })); - - expect( - await screen.findByRole("generic", { name: "ServiceCatalog-Success" }), - ).toBeInTheDocument(); await act(async () => { const results = await axe(document.body); @@ -128,8 +100,14 @@ test("ServiceCatalog shows updated services", async () => { }); }); -test("ServiceCatalog shows updated empty", async () => { - const { component, apiHelper, scheduler } = setup(); +test("ServiceCatalog shows empty state", async () => { + server.use( + http.get("/lsm/v1/service_catalog", () => { + return HttpResponse.json({ data: [Service.a] }); + }), + ); + + const { component } = setup(); render(component); @@ -137,20 +115,10 @@ test("ServiceCatalog shows updated empty", async () => { await screen.findByRole("region", { name: "ServiceCatalog-Loading" }), ).toBeInTheDocument(); - apiHelper.resolve(Either.right({ data: [Service.a] })); - expect( await screen.findByRole("generic", { name: "ServiceCatalog-Success" }), ).toBeInTheDocument(); - scheduler.executeAll(); - - apiHelper.resolve(Either.right({ data: [] })); - - expect( - await screen.findByRole("generic", { name: "ServiceCatalog-Empty" }), - ).toBeInTheDocument(); - await act(async () => { const results = await axe(document.body); @@ -158,32 +126,39 @@ test("ServiceCatalog shows updated empty", async () => { }); }); -test("GIVEN ServiceCatalog WHEN new environment selected THEN new query is triggered", async () => { - const { component, apiHelper } = setup(); +test("GIVEN ServiceCatalog WHEN service is deleted THEN UI is updated", async () => { + const data = [Service.a]; + + server.use( + http.get("/lsm/v1/service_catalog", () => { + return HttpResponse.json({ data }); + }), + http.delete("/lsm/v1/service_catalog/service_name_a", () => { + data.pop(); + + return HttpResponse.json({ status: 204 }); + }), + ); + + const { component } = setup(); render(component); - expect(apiHelper.pendingRequests).toHaveLength(1); - expect(apiHelper.resolvedRequests).toHaveLength(0); - expect(apiHelper.pendingRequests[0]).toEqual({ - method: "GET", - url: "/lsm/v1/service_catalog?instance_summary=True", - environment: env1, - }); + expect( + await screen.findByRole("generic", { name: "ServiceCatalog-Success" }), + ).toBeInTheDocument(); - await act(async () => { - await apiHelper.resolve(Either.right({ data: [Service.a] })); - }); + await userEvent.click(screen.getByLabelText("Actions-dropdown")); - await userEvent.click(screen.getByText("change environment")); + await userEvent.click( + screen.getByLabelText(Service.a.name + "-deleteButton"), + ); - expect(apiHelper.pendingRequests).toHaveLength(1); - expect(apiHelper.resolvedRequests).toHaveLength(1); - expect(apiHelper.pendingRequests[0]).toEqual({ - method: "GET", - url: "/lsm/v1/service_catalog?instance_summary=True", - environment: env2, - }); + await userEvent.click(screen.getByText(words("yes"))); + + expect( + await screen.findByRole("generic", { name: "ServiceCatalog-Empty" }), + ).toBeInTheDocument(); await act(async () => { const results = await axe(document.body); @@ -192,34 +167,33 @@ test("GIVEN ServiceCatalog WHEN new environment selected THEN new query is trigg }); }); -test("GIVEN ServiceCatalog WHEN service is deleted THEN command is triggered", async () => { - const { component, apiHelper } = setup(); +test("GIVEN ServiceCatalog WHEN update fo catalog is triggered successfully THEN UI is updated", async () => { + const data: ServiceModel[] = []; - render(component); + server.use( + http.get("/lsm/v1/service_catalog", () => { + return HttpResponse.json({ data }); + }), + http.post("/lsm/v1/exporter/export_service_definition", () => { + data.push(Service.a); - await act(async () => { - await apiHelper.resolve(Either.right({ data: [Service.a] })); - }); + return HttpResponse.json({ status: 200 }); + }), + ); - await userEvent.click(screen.getByLabelText("Actions-dropdown")); + const { component } = setup(); - await userEvent.click( - screen.getByLabelText(Service.a.name + "-deleteButton"), - ); + render(component); - await userEvent.click(screen.getByText(words("yes"))); + expect( + await screen.findByRole("generic", { name: "ServiceCatalog-Empty" }), + ).toBeInTheDocument(); - expect(apiHelper.pendingRequests).toHaveLength(1); - expect(apiHelper.resolvedRequests).toHaveLength(1); - expect(apiHelper.pendingRequests[0]).toEqual({ - environment: env1, - method: "DELETE", - url: "/lsm/v1/service_catalog/" + Service.a.name, - }); + await userEvent.click(screen.getByText("Update Service Catalog")); - await act(async () => { - const results = await axe(document.body); + await userEvent.click(screen.getByText(words("yes"))); - expect(results).toHaveNoViolations(); - }); + expect( + await screen.findByRole("generic", { name: "ServiceCatalog-Success" }), + ).toBeInTheDocument(); }); diff --git a/src/Slices/ServiceCatalog/UI/Page.tsx b/src/Slices/ServiceCatalog/UI/Page.tsx index febfb05f4..a17e4e62e 100644 --- a/src/Slices/ServiceCatalog/UI/Page.tsx +++ b/src/Slices/ServiceCatalog/UI/Page.tsx @@ -1,21 +1,50 @@ -import React, { useContext, useEffect } from "react"; -import { EmptyView, PageContainer, RemoteDataView } from "@/UI/Components"; +import React, { useContext } from "react"; +import { useGetServiceModels } from "@/Data/Managers/V2/GETTERS/GetServiceModels"; +import { + EmptyView, + ErrorView, + LoadingView, + PageContainer, +} from "@/UI/Components"; import { CatalogActions } from "@/UI/Components/CatalogActions"; import { DependencyContext } from "@/UI/Dependency"; import { words } from "@/UI/words"; import { CatalogDataList } from "./CatalogDataList"; export const Page: React.FC = () => { - const { queryResolver } = useContext(DependencyContext); - const [data, retry] = queryResolver.useContinuous<"GetServices">({ - kind: "GetServices", - }); + const { environmentHandler } = useContext(DependencyContext); + const env = environmentHandler.useId(); + const { data, isError, error, isSuccess, refetch } = + useGetServiceModels(env).useContinuous(); - useEffect(() => { - document.addEventListener("service-deleted", () => { - retry(); - }); - }, [retry]); + let component: React.JSX.Element = ( + + ); + + if (isError) { + component = ( + + ); + } + if (isSuccess) { + component = + data.length <= 0 ? ( + <> + + + ) : ( +
+ +
+ ); + } return ( { } > - - services.length <= 0 ? ( - <> - - - ) : ( -
- -
- ) - } - /> + {component}
); }; diff --git a/src/Slices/ServiceCatalog/UI/ServiceItem.tsx b/src/Slices/ServiceCatalog/UI/ServiceItem.tsx index f86174698..676c7dc88 100644 --- a/src/Slices/ServiceCatalog/UI/ServiceItem.tsx +++ b/src/Slices/ServiceCatalog/UI/ServiceItem.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useRef, useState } from "react"; +import React, { useContext, useEffect, useRef, useState } from "react"; import { Link } from "react-router-dom"; import { Button, @@ -17,7 +17,8 @@ import { Content, } from "@patternfly/react-core"; import { EllipsisVIcon } from "@patternfly/react-icons"; -import { Maybe, ServiceModel } from "@/Core"; +import { ServiceModel } from "@/Core"; +import { useDeleteService } from "@/Data/Managers/V2/DELETE/DeleteService"; import { ConfirmUserActionForm, ToastAlert } from "@/UI/Components"; import { DependencyContext } from "@/UI/Dependency"; import { ModalContext } from "@/UI/Root/Components/ModalProvider"; @@ -38,31 +39,23 @@ interface Props { */ export const ServiceItem: React.FC = ({ service }) => { const { triggerModal, closeModal } = useContext(ModalContext); - const { routeManager, commandResolver } = useContext(DependencyContext); - const rowRefs = useRef>({}); + const { routeManager, environmentHandler } = useContext(DependencyContext); + const env = environmentHandler.useId(); const [isOpen, setIsOpen] = useState(false); - const serviceKey = service.name + "-item"; - const trigger = commandResolver.useGetTrigger<"DeleteService">({ - kind: "DeleteService", - name: service.name, - }); + const { mutate, isError, error } = useDeleteService(env, service.name); const [errorMessage, setErrorMessage] = useState(""); + const serviceKey = service.name + "-item"; + const rowRefs = useRef>({}); /** * Handles the submission of deleting the service. - * if there is an error, it will set the error message, otherwise it will dispatch an event to notify the service has been deleted. + * if there is an error, it will set the error message, * * @returns {Promise} A Promise that resolves when the operation is complete. */ const onSubmit = async (): Promise => { closeModal(); - const result = await trigger(); - - if (Maybe.isSome(result)) { - setErrorMessage(result.value); - } else { - document.dispatchEvent(new CustomEvent("service-deleted")); - } + await mutate(); }; /** @@ -91,6 +84,12 @@ export const ServiceItem: React.FC = ({ service }) => { }); }; + useEffect(() => { + if (isError) { + setErrorMessage(error.message); + } + }, [isError, error]); + return ( - - - - } /> - - - - + + + + + + } /> + + + + + ); return { @@ -111,17 +95,19 @@ function setup() { } test("GIVEN ServiceDetails WHEN click on callbacks tab THEN shows callbacks tab", async () => { + server.use( + http.get("/lsm/v1/service_catalog/service_name_a", () => { + return HttpResponse.json({ data: Service.a }); + }), + ); + server.listen(); const shortenUUID = getShortUuidFromRaw(Callback.list[0].callback_id); const { component, apiHelper } = setup(); render(component); - await act(async () => { - await apiHelper.resolve(Either.right({ data: Service.a })); - }); - - const callbacksButton = screen.getByRole("tab", { name: "Callbacks" }); + const callbacksButton = await screen.findByRole("tab", { name: "Callbacks" }); await userEvent.click(callbacksButton); @@ -139,4 +125,6 @@ test("GIVEN ServiceDetails WHEN click on callbacks tab THEN shows callbacks tab" expect( screen.getByRole("row", { name: "CallbackRow-" + shortenUUID }), ).toBeVisible(); + + server.close(); }); diff --git a/src/Slices/ServiceDetails/UI/Spec/ConfigTab.spec.tsx b/src/Slices/ServiceDetails/UI/Spec/ConfigTab.spec.tsx index 8a9df27b2..38353757e 100644 --- a/src/Slices/ServiceDetails/UI/Spec/ConfigTab.spec.tsx +++ b/src/Slices/ServiceDetails/UI/Spec/ConfigTab.spec.tsx @@ -1,9 +1,11 @@ -import React, { act } from "react"; +import React from "react"; import { MemoryRouter, Route, Routes } from "react-router-dom"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { render, screen } from "@testing-library/react"; import { userEvent } from "@testing-library/user-event"; import { StoreProvider } from "easy-peasy"; -import { Either } from "@/Core"; +import { HttpResponse, http } from "msw"; +import { setupServer } from "msw/node"; import { QueryResolverImpl, ServicesQueryManager, @@ -18,7 +20,6 @@ import { getStoreInstance, DeleteServiceCommandManager, BaseApiHelper, - ServiceQueryManager, } from "@/Data"; import { defaultAuthContext } from "@/Data/Auth/AuthContext"; import { @@ -34,19 +35,15 @@ import { import { DependencyProvider } from "@/UI/Dependency"; import { Page } from "@S/ServiceDetails/UI/Page"; +const server = setupServer(); + function setup() { + const client = new QueryClient(); const store = getStoreInstance(); const scheduler = new StaticScheduler(); const apiHelper = new DeferredApiHelper(); const serviceKeyMaker = new ServiceKeyMaker(); - const serviceQueryManager = ServiceQueryManager( - apiHelper, - ServiceStateHelper(store, serviceKeyMaker), - scheduler, - serviceKeyMaker, - ); - const servicesHelper = ServicesQueryManager( apiHelper, ServicesStateHelper(store), @@ -58,7 +55,6 @@ function setup() { new ServiceConfigFinalizer(ServiceStateHelper(store, serviceKeyMaker)), ); - // { data: Service.a.config } const serviceConfigCommandManager = ServiceConfigCommandManager( apiHelper, ServiceConfigStateHelper(store), @@ -70,7 +66,6 @@ function setup() { const queryResolver = new QueryResolverImpl( new DynamicQueryManagerResolverImpl([ - serviceQueryManager, servicesHelper, serviceConfigQueryManager, ]), @@ -82,42 +77,54 @@ function setup() { ]), ); const component = ( - - - - - } /> - - - - + + + + + + } /> + + + + + ); return { component, - apiHelper, - scheduler, }; } +beforeAll(() => { + server.use( + http.get("/lsm/v1/service_catalog/service_name_a", () => { + return HttpResponse.json({ data: Service.a }); + }), + http.get("/lsm/v1/service_catalog/service_name_a/config", () => { + return HttpResponse.json({ data: { test: Service.a.config } }); + }), + ); + server.listen(); +}); + +afterAll(() => { + server.close(); +}); + test("GIVEN ServiceCatalog WHEN click on config tab THEN shows config tab", async () => { - const { component, apiHelper } = setup(); + const { component } = setup(); render(component); - await act(async () => { - await apiHelper.resolve(Either.right({ data: Service.a })); - }); - - const configButton = screen.getByRole("tab", { name: "Config" }); + const configButton = await screen.findByRole("tab", { name: "Config" }); await userEvent.click(configButton); @@ -125,21 +132,13 @@ test("GIVEN ServiceCatalog WHEN click on config tab THEN shows config tab", asyn }); test("GIVEN ServiceCatalog WHEN config tab is active THEN shows SettingsList", async () => { - const { component, apiHelper } = setup(); + const { component } = setup(); render(component); - await act(async () => { - apiHelper.resolve(Either.right({ data: Service.a })); - }); - - const configButton = screen.getByRole("tab", { name: "Config" }); + const configButton = await screen.findByRole("tab", { name: "Config" }); await userEvent.click(configButton); - await act(async () => { - await apiHelper.resolve(Either.right({ data: {} })); - }); - expect( await screen.findByRole("generic", { name: "SettingsList" }), ).toBeVisible(); diff --git a/src/Slices/ServiceDetails/UI/Tabs/Config.tsx b/src/Slices/ServiceDetails/UI/Tabs/Config.tsx index b66b369c5..e74a54404 100644 --- a/src/Slices/ServiceDetails/UI/Tabs/Config.tsx +++ b/src/Slices/ServiceDetails/UI/Tabs/Config.tsx @@ -1,6 +1,7 @@ import React, { useContext } from "react"; import { Card, CardBody } from "@patternfly/react-core"; -import { RemoteDataView } from "@/UI/Components"; +import { useGetServiceConfig } from "@/Data/Managers/V2/GETTERS/GetServiceConfig/useGetServiceConfig"; +import { ErrorView, LoadingView } from "@/UI/Components"; import { DependencyContext } from "@/UI/Dependency"; import { ConfigList } from "./ConfigList"; @@ -9,23 +10,40 @@ interface Props { } export const Config: React.FC = ({ serviceName }) => { - const { queryResolver } = useContext(DependencyContext); - const [data, retry] = queryResolver.useOneTime<"GetServiceConfig">({ - kind: "GetServiceConfig", - name: serviceName, - }); + const { environmentHandler } = useContext(DependencyContext); + const env = environmentHandler.useId(); + const { data, isSuccess, isError, error, refetch } = useGetServiceConfig( + serviceName, + env, + ).useOneTime(); - return ( - + if (isError) { + - ( - - )} + + ; + } + + if (isSuccess) { + return ( + + + + + + ); + } + + return ( + + + + ); }; diff --git a/src/UI/Components/CatalogActions/CatalogActions.test.tsx b/src/UI/Components/CatalogActions/CatalogActions.test.tsx index ea1755d06..d4075433a 100644 --- a/src/UI/Components/CatalogActions/CatalogActions.test.tsx +++ b/src/UI/Components/CatalogActions/CatalogActions.test.tsx @@ -1,27 +1,18 @@ import React, { act } from "react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { render, screen, cleanup } from "@testing-library/react"; import { userEvent } from "@testing-library/user-event"; import { StoreProvider } from "easy-peasy"; import { configureAxe, toHaveNoViolations } from "jest-axe"; -import { Either } from "@/Core"; -import { - CommandManagerResolverImpl, - CommandResolverImpl, - defaultAuthContext, - getStoreInstance, - QueryManagerResolverImpl, - QueryResolverImpl, -} from "@/Data"; -import { - DeferredApiHelper, - dependencies, - MockEnvironmentModifier, - StaticScheduler, -} from "@/Test"; +import { HttpResponse, http } from "msw"; +import { setupServer } from "msw/node"; +import { getStoreInstance } from "@/Data"; +import { dependencies, MockEnvironmentModifier } from "@/Test"; import { DependencyProvider } from "@/UI/Dependency"; import { ModalProvider } from "@/UI/Root/Components/ModalProvider"; import { words } from "@/UI/words"; import { CatalogActions } from "./CatalogActions"; + expect.extend(toHaveNoViolations); const axe = configureAxe({ @@ -31,6 +22,8 @@ const axe = configureAxe({ }, }); +const server = setupServer(); + function setup( details = { halted: false, @@ -39,41 +32,41 @@ function setup( enable_lsm_expert_mode: false, }, ) { - const apiHelper = new DeferredApiHelper(); - - const scheduler = new StaticScheduler(); + const client = new QueryClient(); const store = getStoreInstance(); - const queryResolver = new QueryResolverImpl( - new QueryManagerResolverImpl(store, apiHelper, scheduler, scheduler), - ); - const commandResolver = new CommandResolverImpl( - new CommandManagerResolverImpl(store, apiHelper, defaultAuthContext), - ); - const environmentModifier = new MockEnvironmentModifier(details); const component = ( - - - - - - - + + + + + + + + + ); - return { component, apiHelper, scheduler }; + return { component }; } +beforeAll(() => server.listen()); + +beforeEach(() => { + server.resetHandlers(); +}); afterEach(cleanup); +afterAll(() => { + server.close(); +}); + test("Given CatalogUpdateButton, when user clicks on button, it should display a modal.", async () => { const { component } = setup(); @@ -99,7 +92,13 @@ test("Given CatalogUpdateButton, when user clicks on button, it should display a }); test("Given CatalogUpdateButton, when user cancels the modal, it should not fire the API call and close the modal.", async () => { - const { component, apiHelper } = setup(); + server.use( + http.post("/lsm/v1/exporter/export_service_definition", () => { + return HttpResponse.json({ status: 200 }); + }), + ); + + const { component } = setup(); render(component); @@ -121,13 +120,17 @@ test("Given CatalogUpdateButton, when user cancels the modal, it should not fire await userEvent.click(cancelButton); - expect(cancelButton).not.toBeVisible(); - expect(apiHelper.pendingRequests).toHaveLength(0); - expect(apiHelper.resolvedRequests).toHaveLength(0); + expect(await screen.queryByText(words("catalog.update.success"))).toBeNull(); }); -test("Given CatalogUpdateButton, when user confirms update, it should fire the API call, if success, show a toaster on succes and close the modal.", async () => { - const { component, apiHelper } = setup(); +test("Given CatalogUpdateButton, when user confirms update, it should fire the API call, if success, show a toaster on success and close the modal.", async () => { + const { component } = setup(); + + server.use( + http.post("/lsm/v1/exporter/export_service_definition", () => { + return HttpResponse.json({ status: 200 }); + }), + ); render(component); @@ -150,25 +153,22 @@ test("Given CatalogUpdateButton, when user confirms update, it should fire the A await userEvent.click(confirmButton); expect(confirmButton).not.toBeVisible(); - expect(apiHelper.pendingRequests).toHaveLength(1); - expect(apiHelper.pendingRequests[0]).toEqual({ - method: "POST", - url: "/lsm/v1/exporter/export_service_definition", - body: null, - environment: "env", - }); - - await act(async () => { - await apiHelper.resolve(Either.right({ data: "id" })); - }); expect( - await screen.findByText(words("catalog.update.success")), + await screen.queryByText(words("catalog.update.success")), ).toBeVisible(); }); test("Given CatalogUpdateButton, when user confirms the update, it should fire the API call, if failure, it should show an error toast and close the modal.", async () => { - const { component, apiHelper } = setup(); + server.use( + http.post("/lsm/v1/exporter/export_service_definition", () => { + return HttpResponse.json( + { message: "Something went wrong" }, + { status: 400 }, + ); + }), + ); + const { component } = setup(); render(component); @@ -184,19 +184,6 @@ test("Given CatalogUpdateButton, when user confirms the update, it should fire t await userEvent.click(confirmButton); - expect(confirmButton).not.toBeVisible(); - expect(apiHelper.pendingRequests).toHaveLength(1); - expect(apiHelper.pendingRequests[0]).toEqual({ - method: "POST", - url: "/lsm/v1/exporter/export_service_definition", - body: null, - environment: "env", - }); - - await act(async () => { - await apiHelper.resolve(Either.left("Something went wrong")); - }); - expect(await screen.findByText("Something went wrong")).toBeVisible(); }); diff --git a/src/UI/Components/CatalogActions/CatalogActions.tsx b/src/UI/Components/CatalogActions/CatalogActions.tsx index 25b5ace33..512f2ef2b 100644 --- a/src/UI/Components/CatalogActions/CatalogActions.tsx +++ b/src/UI/Components/CatalogActions/CatalogActions.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useState } from "react"; +import React, { useContext, useEffect, useState } from "react"; import { AlertVariant, Button, @@ -8,7 +8,7 @@ import { Tooltip, } from "@patternfly/react-core"; import { FileCodeIcon } from "@patternfly/react-icons"; -import { Either } from "@/Core"; +import { useExportCatalog } from "@/Data/Managers/V2/POST/ExportCatalog"; import { DependencyContext } from "@/UI/Dependency"; import { ModalContext } from "@/UI/Root/Components/ModalProvider"; import { words } from "@/UI/words"; @@ -28,12 +28,10 @@ import { ToastAlert } from "../ToastAlert"; */ export const CatalogActions: React.FC = () => { const { triggerModal, closeModal } = useContext(ModalContext); - const { commandResolver, urlManager, environmentHandler } = - useContext(DependencyContext); - - const trigger = commandResolver.useGetTrigger<"UpdateCatalog">({ - kind: "UpdateCatalog", - }); + const { urlManager, environmentHandler } = useContext(DependencyContext); + const { mutate, isError, error, isSuccess } = useExportCatalog( + environmentHandler.useId(), + ); const [message, setMessage] = useState(""); const [toastTitle, setToastTitle] = useState(""); @@ -43,25 +41,12 @@ export const CatalogActions: React.FC = () => { * Handles the submission of the form. * * This function closes the modal and triggers an asynchronous operation. - * If the operation is successful, it sets the toast title, message, and type to indicate success. - * If the operation fails, it sets the toast title, message, and type to indicate failure. - * The message in case of failure is the value of the result. * * @returns {Promise} A Promise that resolves when the operation is complete. */ - const onSubmit = async (): Promise => { + const onSubmit = (): void => { closeModal(); - const result = await trigger(); - - if (Either.isRight(result)) { - setToastTitle(words("catalog.update.success")); - setMessage(words("catalog.update.success.message")); - setToastType(AlertVariant.success); - } else { - setToastTitle(words("catalog.update.failed")); - setMessage(result.value); - setToastType(AlertVariant.danger); - } + mutate(); }; /** @@ -93,6 +78,18 @@ export const CatalogActions: React.FC = () => { }); }; + useEffect(() => { + if (isSuccess) { + setToastTitle(words("catalog.update.success")); + setMessage(words("catalog.update.success.message")); + setToastType(AlertVariant.success); + } else if (isError) { + setToastTitle(words("catalog.update.failed")); + setMessage(error.message); + setToastType(AlertVariant.danger); + } + }, [isError, error, isSuccess]); + return ( <> = ({ serviceName }) => { const { environmentHandler } = useContext(DependencyContext); const environment = environmentHandler.useId(); - const serviceModels = useGetAllServiceModels(environment).useContinuous(); + const serviceModels = useGetServiceModels(environment).useContinuous(); const relatedInventoriesQuery = useGetInventoryList( interServiceRelationNames, diff --git a/src/UI/Components/Diagram/Context/ComposerEditorProvider.tsx b/src/UI/Components/Diagram/Context/ComposerEditorProvider.tsx index 8f16dd596..445e876d8 100644 --- a/src/UI/Components/Diagram/Context/ComposerEditorProvider.tsx +++ b/src/UI/Components/Diagram/Context/ComposerEditorProvider.tsx @@ -1,8 +1,8 @@ import React, { useContext, useEffect, useMemo, useState } from "react"; import { Flex, FlexItem } from "@patternfly/react-core"; -import { useGetAllServiceModels } from "@/Data/Managers/V2/GETTERS/GetAllServiceModels"; import { useGetInstanceWithRelations } from "@/Data/Managers/V2/GETTERS/GetInstanceWithRelations"; import { useGetInventoryList } from "@/Data/Managers/V2/GETTERS/GetInventoryList"; +import { useGetServiceModels } from "@/Data/Managers/V2/GETTERS/GetServiceModels"; import { DependencyContext, words } from "@/UI"; import { ErrorView, LoadingView, PageContainer } from "@/UI/Components"; import { Canvas } from "@/UI/Components/Diagram/Canvas"; @@ -54,8 +54,7 @@ export const ComposerEditorProvider: React.FC = ({ const { environmentHandler } = useContext(DependencyContext); const environment = environmentHandler.useId(); - const serviceModelsQuery = - useGetAllServiceModels(environment).useContinuous(); + const serviceModelsQuery = useGetServiceModels(environment).useContinuous(); const mainService = useMemo(() => { const data = serviceModelsQuery.data; diff --git a/src/UI/Components/ServiceInstanceForm/Components/RelatedServiceProvider.tsx b/src/UI/Components/ServiceInstanceForm/Components/RelatedServiceProvider.tsx index f958371c9..b3edb915e 100644 --- a/src/UI/Components/ServiceInstanceForm/Components/RelatedServiceProvider.tsx +++ b/src/UI/Components/ServiceInstanceForm/Components/RelatedServiceProvider.tsx @@ -1,6 +1,6 @@ import React, { useContext } from "react"; import { Alert, Button } from "@patternfly/react-core"; -import { RemoteData } from "@/Core"; +import { useGetServiceModel } from "@/Data/Managers/V2/GETTERS/GetServiceModel"; import { DependencyContext } from "@/UI/Dependency"; import { words } from "@/UI/words"; import { AutoCompleteInputProvider } from "./AutoCompleteInputProvider"; @@ -41,45 +41,43 @@ export const RelatedServiceProvider: React.FC = ({ alreadySelected, multi, }) => { - const { queryResolver } = useContext(DependencyContext); - const [data, retry] = queryResolver.useOneTime<"GetService">({ - kind: "GetService", - name: serviceName, - }); + const { environmentHandler } = useContext(DependencyContext); + const env = environmentHandler.useId(); + const { isError, error, isSuccess, refetch } = useGetServiceModel( + serviceName, + env, + ).useContinuous(); - return RemoteData.fold( - { - notAsked: () => null, - loading: () => null, - failed: (message) => ( - - {message} -
- -
-
- ), - success: () => { - return ( - - ); - }, - }, - data, - ); + if (isError) { + return ( + + {error.message} +
+ +
+
+ ); + } + if (isSuccess) { + return ( + + ); + } + + return null; }; diff --git a/src/UI/Components/ServiceProvider/ServiceProvider.tsx b/src/UI/Components/ServiceProvider/ServiceProvider.tsx index 9741188f4..e0e887bb3 100644 --- a/src/UI/Components/ServiceProvider/ServiceProvider.tsx +++ b/src/UI/Components/ServiceProvider/ServiceProvider.tsx @@ -1,5 +1,6 @@ import React, { useContext } from "react"; -import { RemoteData, ServiceModel } from "@/Core"; +import { ServiceModel } from "@/Core"; +import { useGetServiceModel } from "@/Data/Managers/V2/GETTERS/GetServiceModel"; import { ErrorView } from "@/UI/Components/ErrorView"; import { LoadingView } from "@/UI/Components/LoadingView"; import { DependencyContext } from "@/UI/Dependency"; @@ -15,32 +16,29 @@ export const ServiceProvider: React.FunctionComponent = ({ Wrapper, Dependant, }) => { - const { queryResolver } = useContext(DependencyContext); + const { environmentHandler } = useContext(DependencyContext); + const env = environmentHandler.useId(); + const { data, isError, error, isSuccess, refetch } = useGetServiceModel( + serviceName, + env, + ).useContinuous(); - const [data, retry] = queryResolver.useContinuous<"GetService">({ - kind: "GetService", - name: serviceName, - }); + if (isError) { + + + ; + } + if (isSuccess) { + return ; + } - return RemoteData.fold( - { - notAsked: () => null, - loading: () => ( - - - - ), - failed: (error) => ( - - - - ), - success: (service) => , - }, - data, + return ( + + + ); }; diff --git a/src/UI/Components/ServicesProvider/ServicesProvider.tsx b/src/UI/Components/ServicesProvider/ServicesProvider.tsx index 3fb51b746..41df42f75 100644 --- a/src/UI/Components/ServicesProvider/ServicesProvider.tsx +++ b/src/UI/Components/ServicesProvider/ServicesProvider.tsx @@ -1,5 +1,6 @@ import React, { useContext } from "react"; -import { RemoteData, ServiceModel } from "@/Core"; +import { ServiceModel } from "@/Core"; +import { useGetServiceModels } from "@/Data/Managers/V2/GETTERS/GetServiceModels"; import { ErrorView } from "@/UI/Components/ErrorView"; import { LoadingView } from "@/UI/Components/LoadingView"; import { DependencyContext } from "@/UI/Dependency"; @@ -15,33 +16,27 @@ export const ServicesProvider: React.FunctionComponent = ({ Wrapper, Dependant, }) => { - const { queryResolver } = useContext(DependencyContext); + const { environmentHandler } = useContext(DependencyContext); + const env = environmentHandler.useId(); + const { data, isError, error, isSuccess, refetch } = + useGetServiceModels(env).useContinuous(); - const [data, retry] = queryResolver.useContinuous<"GetServices">({ - kind: "GetServices", - }); + if (isError) { + + + ; + } + if (isSuccess) { + return ; + } - return RemoteData.fold( - { - notAsked: () => null, - loading: () => ( - - - - ), - failed: (error) => ( - - - - ), - success: (services) => ( - - ), - }, - data, + return ( + + + ); }; diff --git a/src/UI/Root/Components/Header/EnvSelector/Provider.tsx b/src/UI/Root/Components/Header/EnvSelector/Provider.tsx index 9a1b55b4f..f713607c4 100644 --- a/src/UI/Root/Components/Header/EnvSelector/Provider.tsx +++ b/src/UI/Root/Components/Header/EnvSelector/Provider.tsx @@ -1,10 +1,12 @@ import React, { useContext } from "react"; import { useLocation, useNavigate } from "react-router-dom"; +import { useQueryClient } from "@tanstack/react-query"; import { DependencyContext } from "@/UI/Dependency"; import { EnvSelectorWithData } from "./EnvSelectorWithData"; import { EnvironmentSelectorItem } from "./EnvSelectorWrapper"; export const Provider: React.FC = () => { + const client = useQueryClient(); const { environmentHandler, queryResolver, routeManager, featureManager } = useContext(DependencyContext); const location = useLocation(); @@ -18,6 +20,7 @@ export const Provider: React.FC = () => { const onSelectEnvironment = (item: EnvironmentSelectorItem) => { if (selected) { environmentHandler.set(navigate, location, item.environmentId); + client.resetQueries(); return; } @@ -28,6 +31,8 @@ export const Provider: React.FC = () => { : routeManager.getUrl("CompileReports", undefined), }; + client.resetQueries(); + environmentHandler.set(navigate, newLocation, item.environmentId); };