diff --git a/airbyte-api/src/main/openapi/config.yaml b/airbyte-api/src/main/openapi/config.yaml index 2ecce6ed82d..f00262721fc 100644 --- a/airbyte-api/src/main/openapi/config.yaml +++ b/airbyte-api/src/main/openapi/config.yaml @@ -1996,6 +1996,28 @@ paths: $ref: "#/components/schemas/WebBackendConnectionReadList" "422": $ref: "#/components/responses/InvalidInputResponse" + /v1/web_backend/workspace/state: + post: + tags: + - web_backend + summary: Returns the current state of a workspace + operationId: webBackendGetWorkspaceState + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/WebBackendWorkspaceState" + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/WebBackendWorkspaceStateResult" + "404": + $ref: "#/components/responses/NotFoundResponse" + "422": + $ref: "#/components/responses/InvalidInputResponse" /v1/jobs/list: post: tags: @@ -2418,6 +2440,26 @@ components: properties: workspaceId: $ref: "#/components/schemas/WorkspaceId" + WebBackendWorkspaceState: + type: object + required: + - workspaceId + properties: + workspaceId: + $ref: "#/components/schemas/WorkspaceId" + WebBackendWorkspaceStateResult: + type: object + required: + - hasConnections + - hasSources + - hasDestinations + properties: + hasConnections: + type: boolean + hasSources: + type: boolean + hasDestinations: + type: boolean # SLUG SlugRequestBody: type: object diff --git a/airbyte-server/src/main/java/io/airbyte/server/apis/ConfigurationApi.java b/airbyte-server/src/main/java/io/airbyte/server/apis/ConfigurationApi.java index 6cd0ca648ec..299dbee2e47 100644 --- a/airbyte-server/src/main/java/io/airbyte/server/apis/ConfigurationApi.java +++ b/airbyte-server/src/main/java/io/airbyte/server/apis/ConfigurationApi.java @@ -87,6 +87,8 @@ import io.airbyte.api.model.WebBackendConnectionReadList; import io.airbyte.api.model.WebBackendConnectionRequestBody; import io.airbyte.api.model.WebBackendConnectionSearch; import io.airbyte.api.model.WebBackendConnectionUpdate; +import io.airbyte.api.model.WebBackendWorkspaceState; +import io.airbyte.api.model.WebBackendWorkspaceStateResult; import io.airbyte.api.model.WorkspaceCreate; import io.airbyte.api.model.WorkspaceGiveFeedback; import io.airbyte.api.model.WorkspaceIdRequestBody; @@ -242,7 +244,8 @@ public class ConfigurationApi implements io.airbyte.api.V1Api { schedulerHandler, operationsHandler, featureFlags, - eventRunner); + eventRunner, + configRepository); healthCheckHandler = new HealthCheckHandler(); archiveHandler = new ArchiveHandler( airbyteVersion, @@ -817,6 +820,11 @@ public class ConfigurationApi implements io.airbyte.api.V1Api { return execute(() -> webBackendConnectionsHandler.webBackendGetConnection(webBackendConnectionRequestBody)); } + @Override + public WebBackendWorkspaceStateResult webBackendGetWorkspaceState(WebBackendWorkspaceState webBackendWorkspaceState) { + return execute(() -> webBackendConnectionsHandler.getWorkspaceState(webBackendWorkspaceState)); + } + @Override public WebBackendConnectionRead webBackendCreateConnection(final WebBackendConnectionCreate webBackendConnectionCreate) { return execute(() -> webBackendConnectionsHandler.webBackendCreateConnection(webBackendConnectionCreate)); diff --git a/airbyte-server/src/main/java/io/airbyte/server/handlers/WebBackendConnectionsHandler.java b/airbyte-server/src/main/java/io/airbyte/server/handlers/WebBackendConnectionsHandler.java index fcb52c9f46d..9e29de0abb3 100644 --- a/airbyte-server/src/main/java/io/airbyte/server/handlers/WebBackendConnectionsHandler.java +++ b/airbyte-server/src/main/java/io/airbyte/server/handlers/WebBackendConnectionsHandler.java @@ -40,11 +40,14 @@ import io.airbyte.api.model.WebBackendConnectionRequestBody; import io.airbyte.api.model.WebBackendConnectionSearch; import io.airbyte.api.model.WebBackendConnectionUpdate; import io.airbyte.api.model.WebBackendOperationCreateOrUpdate; +import io.airbyte.api.model.WebBackendWorkspaceState; +import io.airbyte.api.model.WebBackendWorkspaceStateResult; import io.airbyte.api.model.WorkspaceIdRequestBody; import io.airbyte.commons.features.FeatureFlags; import io.airbyte.commons.json.Jsons; import io.airbyte.commons.lang.MoreBooleans; import io.airbyte.config.persistence.ConfigNotFoundException; +import io.airbyte.config.persistence.ConfigRepository; import io.airbyte.scheduler.client.EventRunner; import io.airbyte.validation.json.JsonValidationException; import java.io.IOException; @@ -72,6 +75,19 @@ public class WebBackendConnectionsHandler { private final OperationsHandler operationsHandler; private final FeatureFlags featureFlags; private final EventRunner eventRunner; + private final ConfigRepository configRepository; + + public WebBackendWorkspaceStateResult getWorkspaceState(final WebBackendWorkspaceState webBackendWorkspaceState) throws IOException { + final var workspaceId = webBackendWorkspaceState.getWorkspaceId(); + final var connectionCount = configRepository.countConnectionsForWorkspace(workspaceId); + final var destinationCount = configRepository.countDestinationsForWorkspace(workspaceId); + final var sourceCount = configRepository.countSourcesForWorkspace(workspaceId); + + return new WebBackendWorkspaceStateResult() + .hasConnections(connectionCount > 0) + .hasDestinations(destinationCount > 0) + .hasSources(sourceCount > 0); + } public WebBackendConnectionReadList webBackendListConnectionsForWorkspace(final WorkspaceIdRequestBody workspaceIdRequestBody) throws ConfigNotFoundException, IOException, JsonValidationException { diff --git a/airbyte-server/src/test/java/io/airbyte/server/handlers/WebBackendConnectionsHandlerTest.java b/airbyte-server/src/test/java/io/airbyte/server/handlers/WebBackendConnectionsHandlerTest.java index aed914ba80d..831bbdace6c 100644 --- a/airbyte-server/src/test/java/io/airbyte/server/handlers/WebBackendConnectionsHandlerTest.java +++ b/airbyte-server/src/test/java/io/airbyte/server/handlers/WebBackendConnectionsHandlerTest.java @@ -5,6 +5,8 @@ package io.airbyte.server.handlers; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; @@ -55,6 +57,7 @@ import io.airbyte.api.model.WebBackendConnectionRequestBody; import io.airbyte.api.model.WebBackendConnectionSearch; import io.airbyte.api.model.WebBackendConnectionUpdate; import io.airbyte.api.model.WebBackendOperationCreateOrUpdate; +import io.airbyte.api.model.WebBackendWorkspaceState; import io.airbyte.api.model.WorkspaceIdRequestBody; import io.airbyte.commons.enums.Enums; import io.airbyte.commons.features.FeatureFlags; @@ -64,6 +67,7 @@ import io.airbyte.config.StandardDestinationDefinition; import io.airbyte.config.StandardSourceDefinition; import io.airbyte.config.StandardSync; import io.airbyte.config.persistence.ConfigNotFoundException; +import io.airbyte.config.persistence.ConfigRepository; import io.airbyte.protocol.models.CatalogHelpers; import io.airbyte.protocol.models.Field; import io.airbyte.protocol.models.JsonSchemaType; @@ -103,6 +107,7 @@ class WebBackendConnectionsHandlerTest { private FeatureFlags featureFlags; private EventRunner eventRunner; private ConnectionHelper connectionHelper; + private ConfigRepository configRepository; @BeforeEach public void setup() throws IOException, JsonValidationException, ConfigNotFoundException { @@ -111,6 +116,7 @@ class WebBackendConnectionsHandlerTest { final SourceHandler sourceHandler = mock(SourceHandler.class); final DestinationHandler destinationHandler = mock(DestinationHandler.class); final JobHistoryHandler jobHistoryHandler = mock(JobHistoryHandler.class); + configRepository = mock(ConfigRepository.class); schedulerHandler = mock(SchedulerHandler.class); featureFlags = mock(FeatureFlags.class); eventRunner = mock(EventRunner.class); @@ -122,7 +128,8 @@ class WebBackendConnectionsHandlerTest { schedulerHandler, operationsHandler, featureFlags, - eventRunner); + eventRunner, + configRepository); final StandardSourceDefinition standardSourceDefinition = SourceDefinitionHelpers.generateSourceDefinition(); final SourceConnection source = SourceHelpers.generateSource(UUID.randomUUID()); @@ -234,6 +241,32 @@ class WebBackendConnectionsHandlerTest { .thenReturn(new JobInfoRead().job(new JobRead().status(JobStatus.SUCCEEDED))); } + @Test + public void testGetWorkspaceState() throws IOException { + final UUID uuid = UUID.randomUUID(); + final WebBackendWorkspaceState request = new WebBackendWorkspaceState().workspaceId(uuid); + when(configRepository.countSourcesForWorkspace(uuid)).thenReturn(5); + when(configRepository.countDestinationsForWorkspace(uuid)).thenReturn(2); + when(configRepository.countConnectionsForWorkspace(uuid)).thenReturn(8); + final var actual = wbHandler.getWorkspaceState(request); + assertTrue(actual.getHasConnections()); + assertTrue(actual.getHasDestinations()); + assertTrue((actual.getHasSources())); + } + + @Test + public void testGetWorkspaceStateEmpty() throws IOException { + final UUID uuid = UUID.randomUUID(); + final WebBackendWorkspaceState request = new WebBackendWorkspaceState().workspaceId(uuid); + when(configRepository.countSourcesForWorkspace(uuid)).thenReturn(0); + when(configRepository.countDestinationsForWorkspace(uuid)).thenReturn(0); + when(configRepository.countConnectionsForWorkspace(uuid)).thenReturn(0); + final var actual = wbHandler.getWorkspaceState(request); + assertFalse(actual.getHasConnections()); + assertFalse(actual.getHasDestinations()); + assertFalse(actual.getHasSources()); + } + @Test public void testWebBackendListConnectionsForWorkspace() throws ConfigNotFoundException, IOException, JsonValidationException { final WorkspaceIdRequestBody workspaceIdRequestBody = new WorkspaceIdRequestBody(); diff --git a/airbyte-webapp/src/core/domain/workspace/Workspace.ts b/airbyte-webapp/src/core/domain/workspace/Workspace.ts index 75b962e71e0..4e0a0b5a720 100644 --- a/airbyte-webapp/src/core/domain/workspace/Workspace.ts +++ b/airbyte-webapp/src/core/domain/workspace/Workspace.ts @@ -20,3 +20,9 @@ export interface Workspace { displaySetupWizard: boolean; notifications: Notification[]; } + +export interface WorkspaceState { + hasSources: boolean; + hasDestinations: boolean; + hasConnections: boolean; +} diff --git a/airbyte-webapp/src/core/domain/workspace/WorkspaceService.ts b/airbyte-webapp/src/core/domain/workspace/WorkspaceService.ts index 2c915c6312c..d99f8c79b2a 100644 --- a/airbyte-webapp/src/core/domain/workspace/WorkspaceService.ts +++ b/airbyte-webapp/src/core/domain/workspace/WorkspaceService.ts @@ -1,5 +1,5 @@ import { AirbyteRequestService } from "core/request/AirbyteRequestService"; -import { Workspace } from "./Workspace"; +import { Workspace, WorkspaceState } from "./Workspace"; class WorkspaceService extends AirbyteRequestService { get url() { @@ -19,6 +19,12 @@ class WorkspaceService extends AirbyteRequestService { public async update(payload: Record): Promise { return await this.fetch(`${this.url}/update`, payload); } + + public async getState(workspaceId: string): Promise { + return await this.fetch(`web_backend/workspace/state`, { + workspaceId, + }); + } } export { WorkspaceService }; diff --git a/airbyte-webapp/src/pages/OnboardingPage/OnboardingPage.tsx b/airbyte-webapp/src/pages/OnboardingPage/OnboardingPage.tsx index dbe1b4b905c..42e1d4ff8f2 100644 --- a/airbyte-webapp/src/pages/OnboardingPage/OnboardingPage.tsx +++ b/airbyte-webapp/src/pages/OnboardingPage/OnboardingPage.tsx @@ -1,19 +1,9 @@ -import React, { Suspense, useEffect, useState } from "react"; +import React, { Suspense, useState } from "react"; import styled from "styled-components"; import { FormattedMessage } from "react-intl"; import { Button } from "components"; import HeadTitle from "components/HeadTitle"; -import { useCreateSource, useSourceList } from "hooks/services/useSourceHook"; -import { - useCreateDestination, - useDestinationList, -} from "hooks/services/useDestinationHook"; -import { - useConnectionList, - useSyncConnection, -} from "hooks/services/useConnectionHook"; -import { ConnectionConfiguration } from "core/domain/connection"; import useGetStepsConfig from "./useStepsConfig"; import SourceStep from "./components/SourceStep"; import DestinationStep from "./components/DestinationStep"; @@ -27,10 +17,9 @@ import StepsCounter from "./components/StepsCounter"; import LoadingPage from "components/LoadingPage"; import useWorkspace from "hooks/services/useWorkspace"; import useRouterHook from "hooks/useRouter"; -import { JobInfo } from "core/domain/job"; import { RoutePaths } from "../routePaths"; -import { useSourceDefinitionList } from "services/connector/SourceDefinitionService"; -import { useDestinationDefinitionList } from "services/connector/DestinationDefinitionService"; +import { useCurrentWorkspaceState } from "services/workspaces/WorkspacesService"; +import { useEffectOnce } from "react-use"; const Content = styled.div<{ big?: boolean; medium?: boolean }>` width: 100%; @@ -64,39 +53,27 @@ const OnboardingPage: React.FC = () => { const analyticsService = useAnalyticsService(); const { push } = useRouterHook(); - useEffect(() => { + useEffectOnce(() => { analyticsService.page("Onboarding Page"); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }); - const { sources } = useSourceList(); - const { destinations } = useDestinationList(); - const { connections } = useConnectionList(); - const { sourceDefinitions } = useSourceDefinitionList(); - const { destinationDefinitions } = useDestinationDefinitionList(); - - const { mutateAsync: syncConnection } = useSyncConnection(); - - const { mutateAsync: createSource } = useCreateSource(); - const { mutateAsync: createDestination } = useCreateDestination(); const { finishOnboarding } = useWorkspace(); + const { + hasConnections, + hasDestinations, + hasSources, + } = useCurrentWorkspaceState(); - const [successRequest, setSuccessRequest] = useState(false); - const [errorStatusRequest, setErrorStatusRequest] = useState<{ - status: number; - response: JobInfo; - message: string; - } | null>(null); + const [animateExit, setAnimateExit] = useState(false); const afterUpdateStep = () => { - setSuccessRequest(false); - setErrorStatusRequest(null); + setAnimateExit(false); }; const { currentStep, setCurrentStep, steps } = useGetStepsConfig( - !!sources.length, - !!destinations.length, - !!connections.length, + hasSources, + hasDestinations, + hasConnections, afterUpdateStep ); @@ -105,114 +82,12 @@ const OnboardingPage: React.FC = () => { push(RoutePaths.Connections); }; - const renderStep = () => { - if (currentStep === StepType.INSTRUCTION) { - const onStart = () => setCurrentStep(StepType.CREATE_SOURCE); - //TODO: add username - return ; - } - if (currentStep === StepType.CREATE_SOURCE) { - const getSourceDefinitionById = (id: string) => - sourceDefinitions.find((item) => item.sourceDefinitionId === id); - - const onSubmitSourceStep = async (values: { - name: string; - serviceType: string; - sourceId?: string; - connectionConfiguration?: ConnectionConfiguration; - }) => { - setErrorStatusRequest(null); - const sourceConnector = getSourceDefinitionById(values.serviceType); - - try { - await createSource({ values, sourceConnector }); - - setSuccessRequest(true); - setTimeout(() => { - setSuccessRequest(false); - setCurrentStep(StepType.CREATE_DESTINATION); - }, 2000); - } catch (e) { - setErrorStatusRequest(e); - } - }; - return ( - setErrorStatusRequest(null)} - onSubmit={onSubmitSourceStep} - availableServices={sourceDefinitions} - hasSuccess={successRequest} - error={errorStatusRequest} - /> - ); - } - if (currentStep === StepType.CREATE_DESTINATION) { - const getDestinationDefinitionById = (id: string) => - destinationDefinitions.find( - (item) => item.destinationDefinitionId === id - ); - - const onSubmitDestinationStep = async (values: { - name: string; - serviceType: string; - destinationDefinitionId?: string; - connectionConfiguration?: ConnectionConfiguration; - }) => { - setErrorStatusRequest(null); - const destinationConnector = getDestinationDefinitionById( - values.serviceType - ); - - try { - await createDestination({ - values, - destinationConnector, - }); - - setSuccessRequest(true); - setTimeout(() => { - setSuccessRequest(false); - setCurrentStep(StepType.SET_UP_CONNECTION); - }, 2000); - } catch (e) { - setErrorStatusRequest(e); - } - }; - return ( - setErrorStatusRequest(null)} - onSubmit={onSubmitDestinationStep} - availableServices={destinationDefinitions} - hasSuccess={successRequest} - error={errorStatusRequest} - /> - ); - } - - if (currentStep === StepType.SET_UP_CONNECTION) { - return ( - setCurrentStep(StepType.FINAl)} - /> - ); - } - - const onSync = () => syncConnection(connections[0]); - - return ( - - ); - }; - return ( {currentStep === StepType.CREATE_SOURCE ? ( - + ) : currentStep === StepType.CREATE_DESTINATION ? ( - + ) : null} { - }>{renderStep()} + }> + {currentStep === StepType.INSTRUCTION && ( + setCurrentStep(StepType.CREATE_SOURCE)} + /> + )} + {currentStep === StepType.CREATE_SOURCE && ( + setAnimateExit(true)} + onNextStep={() => setCurrentStep(StepType.CREATE_DESTINATION)} + /> + )} + {currentStep === StepType.CREATE_DESTINATION && ( + setAnimateExit(true)} + onNextStep={() => setCurrentStep(StepType.SET_UP_CONNECTION)} + /> + )} + {currentStep === StepType.SET_UP_CONNECTION && ( + setCurrentStep(StepType.FINAl)} /> + )} + {currentStep === StepType.FINAl && } +