1
0
mirror of synced 2025-12-25 02:09:19 -05:00

Improve onboarding performance (#11682)

* Improve onboarding performance

* Fix auto import issues

* Fix minor issues

* Regenerate index.html

* Add JUnit tests
This commit is contained in:
Tim Roes
2022-04-04 18:49:22 -07:00
committed by GitHub
parent df55c9135d
commit 25b65c6bb1
16 changed files with 384 additions and 233 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,3 +20,9 @@ export interface Workspace {
displaySetupWizard: boolean;
notifications: Notification[];
}
export interface WorkspaceState {
hasSources: boolean;
hasDestinations: boolean;
hasConnections: boolean;
}

View File

@@ -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<string, unknown>): Promise<Workspace> {
return await this.fetch<Workspace>(`${this.url}/update`, payload);
}
public async getState(workspaceId: string): Promise<WorkspaceState> {
return await this.fetch<WorkspaceState>(`web_backend/workspace/state`, {
workspaceId,
});
}
}
export { WorkspaceService };

View File

@@ -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 <WelcomeStep onSubmit={onStart} userName="" />;
}
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 (
<SourceStep
afterSelectConnector={() => 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 (
<DestinationStep
afterSelectConnector={() => setErrorStatusRequest(null)}
onSubmit={onSubmitDestinationStep}
availableServices={destinationDefinitions}
hasSuccess={successRequest}
error={errorStatusRequest}
/>
);
}
if (currentStep === StepType.SET_UP_CONNECTION) {
return (
<ConnectionStep
errorStatus={errorStatusRequest?.status}
source={sources[0]}
destination={destinations[0]}
afterSubmitConnection={() => setCurrentStep(StepType.FINAl)}
/>
);
}
const onSync = () => syncConnection(connections[0]);
return (
<FinalStep connectionId={connections[0].connectionId} onSync={onSync} />
);
};
return (
<ScreenContent>
{currentStep === StepType.CREATE_SOURCE ? (
<LetterLine exit={successRequest} />
<LetterLine exit={animateExit} />
) : currentStep === StepType.CREATE_DESTINATION ? (
<LetterLine onRight exit={successRequest} />
<LetterLine onRight exit={animateExit} />
) : null}
<Content
big={currentStep === StepType.SET_UP_CONNECTION}
@@ -223,7 +98,29 @@ const OnboardingPage: React.FC = () => {
<HeadTitle titles={[{ id: "onboarding.headTitle" }]} />
<StepsCounter steps={steps} currentStep={currentStep} />
<Suspense fallback={<LoadingPage />}>{renderStep()}</Suspense>
<Suspense fallback={<LoadingPage />}>
{currentStep === StepType.INSTRUCTION && (
<WelcomeStep
onNextStep={() => setCurrentStep(StepType.CREATE_SOURCE)}
/>
)}
{currentStep === StepType.CREATE_SOURCE && (
<SourceStep
onSuccess={() => setAnimateExit(true)}
onNextStep={() => setCurrentStep(StepType.CREATE_DESTINATION)}
/>
)}
{currentStep === StepType.CREATE_DESTINATION && (
<DestinationStep
onSuccess={() => setAnimateExit(true)}
onNextStep={() => setCurrentStep(StepType.SET_UP_CONNECTION)}
/>
)}
{currentStep === StepType.SET_UP_CONNECTION && (
<ConnectionStep onNextStep={() => setCurrentStep(StepType.FINAl)} />
)}
{currentStep === StepType.FINAl && <FinalStep />}
</Suspense>
<Footer>
<Button secondary onClick={() => handleFinishOnboarding()}>

View File

@@ -2,22 +2,21 @@ import React from "react";
import { FormattedMessage } from "react-intl";
import CreateConnectionContent from "components/CreateConnectionContent";
import { Destination, Source } from "core/domain/connector";
import TitlesBlock from "./TitlesBlock";
import HighlightedText from "./HighlightedText";
import { useSourceList } from "hooks/services/useSourceHook";
import { useDestinationList } from "hooks/services/useDestinationHook";
type IProps = {
errorStatus?: number;
source: Source;
destination: Destination;
afterSubmitConnection: () => void;
onNextStep: () => void;
};
const ConnectionStep: React.FC<IProps> = ({
source,
destination,
afterSubmitConnection,
onNextStep: afterSubmitConnection,
}) => {
const { sources } = useSourceList();
const { destinations } = useDestinationList();
return (
<>
<TitlesBlock
@@ -36,8 +35,8 @@ const ConnectionStep: React.FC<IProps> = ({
</TitlesBlock>
<CreateConnectionContent
noTitles
source={source}
destination={destination}
source={sources[0]}
destination={destinations[0]}
afterSubmitConnection={afterSubmitConnection}
/>
</>

View File

@@ -3,34 +3,22 @@ import { FormattedMessage } from "react-intl";
import { createFormErrorMessage } from "utils/errorStatusMessage";
import { ConnectionConfiguration } from "core/domain/connection";
import { DestinationDefinition } from "core/domain/connector";
import { ConnectorCard } from "views/Connector/ConnectorCard";
import TitlesBlock from "./TitlesBlock";
import HighlightedText from "./HighlightedText";
import { useAnalyticsService } from "hooks/services/Analytics/useAnalyticsService";
import { useGetDestinationDefinitionSpecificationAsync } from "services/connector/DestinationDefinitionSpecificationService";
import { useDestinationDefinitionList } from "services/connector/DestinationDefinitionService";
import { useCreateDestination } from "hooks/services/useDestinationHook";
import { JobInfo } from "core/domain/job";
type IProps = {
availableServices: DestinationDefinition[];
onSubmit: (values: {
name: string;
serviceType: string;
destinationDefinitionId?: string;
connectionConfiguration?: ConnectionConfiguration;
}) => void;
hasSuccess?: boolean;
error?: null | { message?: string; status?: number };
afterSelectConnector?: () => void;
type Props = {
onNextStep: () => void;
onSuccess: () => void;
};
const DestinationStep: React.FC<IProps> = ({
onSubmit,
availableServices,
hasSuccess,
error,
afterSelectConnector,
}) => {
const DestinationStep: React.FC<Props> = ({ onNextStep, onSuccess }) => {
const [destinationDefinitionId, setDestinationDefinitionId] = useState<
string | null
>(null);
@@ -38,12 +26,51 @@ const DestinationStep: React.FC<IProps> = ({
data: destinationDefinitionSpecification,
isLoading,
} = useGetDestinationDefinitionSpecificationAsync(destinationDefinitionId);
const { destinationDefinitions } = useDestinationDefinitionList();
const [successRequest, setSuccessRequest] = useState(false);
const [error, setError] = useState<{
status: number;
response: JobInfo;
message: string;
} | null>(null);
const { mutateAsync: createDestination } = useCreateDestination();
const analyticsService = useAnalyticsService();
const onDropDownSelect = (destinationDefinition: string) => {
const destinationConnector = availableServices.find(
(s) => s.destinationDefinitionId === destinationDefinition
const getDestinationDefinitionById = (id: string) =>
destinationDefinitions.find((item) => item.destinationDefinitionId === id);
const onSubmitDestinationStep = async (values: {
name: string;
serviceType: string;
destinationDefinitionId?: string;
connectionConfiguration?: ConnectionConfiguration;
}) => {
setError(null);
const destinationConnector = getDestinationDefinitionById(
values.serviceType
);
try {
await createDestination({
values,
destinationConnector,
});
setSuccessRequest(true);
onSuccess();
setTimeout(() => {
setSuccessRequest(false);
onNextStep();
}, 2000);
} catch (e) {
setError(e);
}
};
const onDropDownSelect = (destinationDefinitionId: string) => {
const destinationConnector = getDestinationDefinitionById(
destinationDefinitionId
);
analyticsService.track("New Destination - Action", {
action: "Select a connector",
@@ -52,17 +79,14 @@ const DestinationStep: React.FC<IProps> = ({
destinationConnector?.destinationDefinitionId,
});
if (afterSelectConnector) {
afterSelectConnector();
}
setDestinationDefinitionId(destinationDefinition);
setError(null);
setDestinationDefinitionId(destinationDefinitionId);
};
const onSubmitForm = async (values: {
name: string;
serviceType: string;
}) => {
await onSubmit({
await onSubmitDestinationStep({
...values,
destinationDefinitionId:
destinationDefinitionSpecification?.destinationDefinitionId,
@@ -92,8 +116,8 @@ const DestinationStep: React.FC<IProps> = ({
formType="destination"
onServiceSelect={onDropDownSelect}
onSubmit={onSubmitForm}
hasSuccess={hasSuccess}
availableServices={availableServices}
hasSuccess={successRequest}
availableServices={destinationDefinitions}
errorMessage={errorMessage}
selectedConnectorDefinitionSpecification={
destinationDefinitionSpecification

View File

@@ -13,12 +13,11 @@ import SyncCompletedModal from "views/Feedback/SyncCompletedModal";
import { useOnboardingService } from "hooks/services/Onboarding/OnboardingService";
import Status from "core/statuses";
import useWorkspace from "hooks/services/useWorkspace";
import { useGetConnection } from "hooks/services/useConnectionHook";
type FinalStepProps = {
connectionId: string;
onSync: () => void;
};
import {
useConnectionList,
useGetConnection,
useSyncConnection,
} from "hooks/services/useConnectionHook";
const Title = styled(H1)`
margin: 21px 0;
@@ -35,7 +34,7 @@ const Videos = styled.div`
padding: 0 27px;
`;
const FinalStep: React.FC<FinalStepProps> = ({ connectionId, onSync }) => {
const FinalStep: React.FC = () => {
const config = useConfig();
const { sendFeedback } = useWorkspace();
const {
@@ -45,7 +44,9 @@ const FinalStep: React.FC<FinalStepProps> = ({ connectionId, onSync }) => {
useCaseLinks,
skipCase,
} = useOnboardingService();
const connection = useGetConnection(connectionId, {
const { mutateAsync: syncConnection } = useSyncConnection();
const { connections } = useConnectionList();
const connection = useGetConnection(connections[0].connectionId, {
refetchInterval: 2500,
});
const [isOpen, setIsOpen] = useState(false);
@@ -74,6 +75,8 @@ const FinalStep: React.FC<FinalStepProps> = ({ connectionId, onSync }) => {
setIsOpen(false);
};
const onSync = () => syncConnection(connections[0]);
return (
<>
<Videos>

View File

@@ -39,12 +39,12 @@ export const ExitAnimation = keyframes`
}
`;
const Line = styled.div<{ onRight?: boolean }>`
const Line = styled.div<{ $onRight?: boolean }>`
position: absolute;
width: calc(50% - 275px);
z-index: 1;
top: 382px;
left: ${({ onRight }) => (onRight ? "calc(50% + 275px)" : 0)};
left: ${({ $onRight }) => ($onRight ? "calc(50% + 275px)" : 0)};
`;
const Path = styled.div<{ exit?: boolean }>`
width: 100%;
@@ -70,7 +70,7 @@ type LetterLineProps = {
const LetterLine: React.FC<LetterLineProps> = ({ onRight, exit }) => {
return (
<Line onRight={onRight}>
<Line $onRight={onRight}>
<Path exit={exit} />
<Img
src="/newsletter.png"

View File

@@ -8,43 +8,65 @@ import { createFormErrorMessage } from "utils/errorStatusMessage";
import { useAnalyticsService } from "hooks/services/Analytics/useAnalyticsService";
import HighlightedText from "./HighlightedText";
import TitlesBlock from "./TitlesBlock";
import { SourceDefinition } from "core/domain/connector";
import { useGetSourceDefinitionSpecificationAsync } from "services/connector/SourceDefinitionSpecificationService";
import { useSourceDefinitionList } from "services/connector/SourceDefinitionService";
import { useCreateSource } from "hooks/services/useSourceHook";
import { JobInfo } from "core/domain/job";
type IProps = {
onSubmit: (values: {
name: string;
serviceType: string;
sourceDefinitionId?: string;
connectionConfiguration?: ConnectionConfiguration;
}) => void;
availableServices: SourceDefinition[];
hasSuccess?: boolean;
error?: null | { message?: string; status?: number };
afterSelectConnector?: () => void;
onSuccess: () => void;
onNextStep: () => void;
};
const SourceStep: React.FC<IProps> = ({
onSubmit,
availableServices,
hasSuccess,
error,
afterSelectConnector,
}) => {
const SourceStep: React.FC<IProps> = ({ onNextStep, onSuccess }) => {
const { sourceDefinitions } = useSourceDefinitionList();
const [sourceDefinitionId, setSourceDefinitionId] = useState<string | null>(
null
);
const [successRequest, setSuccessRequest] = useState(false);
const [error, setError] = useState<{
status: number;
response: JobInfo;
message: string;
} | null>(null);
const { mutateAsync: createSource } = useCreateSource();
const analyticsService = useAnalyticsService();
const getSourceDefinitionById = (id: string) =>
sourceDefinitions.find((item) => item.sourceDefinitionId === id);
const {
data: sourceDefinitionSpecification,
isLoading,
} = useGetSourceDefinitionSpecificationAsync(sourceDefinitionId);
const onSubmitSourceStep = async (values: {
name: string;
serviceType: string;
sourceId?: string;
connectionConfiguration?: ConnectionConfiguration;
}) => {
setError(null);
const sourceConnector = getSourceDefinitionById(values.serviceType);
try {
await createSource({ values, sourceConnector });
setSuccessRequest(true);
onSuccess();
setTimeout(() => {
setSuccessRequest(false);
onNextStep();
}, 2000);
} catch (e) {
setError(e);
}
};
const onServiceSelect = (sourceId: string) => {
const sourceDefinition = availableServices.find(
(s) => s.sourceDefinitionId === sourceId
);
const sourceDefinition = getSourceDefinitionById(sourceId);
analyticsService.track("New Source - Action", {
action: "Select a connector",
@@ -52,17 +74,13 @@ const SourceStep: React.FC<IProps> = ({
connector_source_id: sourceDefinition?.sourceDefinitionId,
});
if (afterSelectConnector) {
afterSelectConnector();
}
setError(null);
setSourceDefinitionId(sourceId);
};
const onSubmitForm = async (values: { name: string; serviceType: string }) =>
onSubmit({
onSubmitSourceStep({
...values,
sourceDefinitionId: sourceDefinitionSpecification?.sourceDefinitionId,
});
const errorMessage = error ? createFormErrorMessage(error) : "";
@@ -89,8 +107,8 @@ const SourceStep: React.FC<IProps> = ({
onServiceSelect={onServiceSelect}
onSubmit={onSubmitForm}
formType="source"
availableServices={availableServices}
hasSuccess={hasSuccess}
availableServices={sourceDefinitions}
hasSuccess={successRequest}
errorMessage={errorMessage}
selectedConnectorDefinitionSpecification={sourceDefinitionSpecification}
isLoading={isLoading}

View File

@@ -9,7 +9,7 @@ import { BigButton } from "components/CenteredPageComponents";
import { useConfig } from "config";
type WelcomeStepProps = {
onSubmit: () => void;
onNextStep: () => void;
userName?: string;
};
@@ -23,7 +23,7 @@ const Videos = styled.div`
margin: 20px 0 67px;
`;
const WelcomeStep: React.FC<WelcomeStepProps> = ({ userName, onSubmit }) => {
const WelcomeStep: React.FC<WelcomeStepProps> = ({ userName, onNextStep }) => {
const config = useConfig();
return (
@@ -66,7 +66,7 @@ const WelcomeStep: React.FC<WelcomeStepProps> = ({ userName, onSubmit }) => {
link={config.ui.demoLink}
/>
</Videos>
<BigButton onClick={onSubmit} shadow>
<BigButton onClick={onNextStep} shadow>
<FormattedMessage id="onboarding.firstConnection" />
</BigButton>
</>

View File

@@ -29,9 +29,7 @@ const useStepsConfig = (
const updateStep = useCallback(
(step: StepType) => {
setCurrentStep(step);
if (afterUpdateStep) {
afterUpdateStep();
}
afterUpdateStep?.();
},
[setCurrentStep, afterUpdateStep]
);

View File

@@ -7,7 +7,11 @@ import {
} from "react-query";
import useRouter from "hooks/useRouter";
import { Workspace, WorkspaceService } from "core/domain/workspace";
import {
Workspace,
WorkspaceService,
WorkspaceState,
} from "core/domain/workspace";
import { RoutePaths } from "pages/routePaths";
import { useConfig } from "config";
import { useDefaultRequestMiddlewares } from "../useDefaultRequestMiddlewares";
@@ -20,6 +24,8 @@ export const workspaceKeys = {
list: (filters: string) => [...workspaceKeys.lists(), { filters }] as const,
detail: (workspaceId: string) =>
[...workspaceKeys.all, "details", workspaceId] as const,
state: (workspaceId: string) =>
[...workspaceKeys.all, "state", workspaceId] as const,
};
type Context = {
@@ -106,6 +112,22 @@ export const useCurrentWorkspace = (): Workspace => {
});
};
export const useCurrentWorkspaceState = (): WorkspaceState => {
const workspaceId = useCurrentWorkspaceId();
const service = useWorkspaceApiService();
return (useQuery(
workspaceKeys.state(workspaceId),
() => service.getState(workspaceId),
{
// We want to keep this query only shortly in cache, so we refetch
// the data whenever the user might have changed sources/destinations/connections
// without requiring to manually invalidate that query on each change.
cacheTime: 5 * 1000,
}
) as QueryObserverSuccessResult<WorkspaceState>).data;
};
export const useListWorkspaces = (): Workspace[] => {
const service = useWorkspaceApiService();

View File

@@ -363,6 +363,7 @@ font-style: italic;
<ul>
<li><a href="#webBackendCreateConnection"><code><span class="http-method">post</span> /v1/web_backend/connections/create</code></a></li>
<li><a href="#webBackendGetConnection"><code><span class="http-method">post</span> /v1/web_backend/connections/get</code></a></li>
<li><a href="#webBackendGetWorkspaceState"><code><span class="http-method">post</span> /v1/web_backend/workspace/state</code></a></li>
<li><a href="#webBackendListAllConnectionsForWorkspace"><code><span class="http-method">post</span> /v1/web_backend/connections/list_all</code></a></li>
<li><a href="#webBackendListConnectionsForWorkspace"><code><span class="http-method">post</span> /v1/web_backend/connections/list</code></a></li>
<li><a href="#webBackendSearchConnections"><code><span class="http-method">post</span> /v1/web_backend/connections/search</code></a></li>
@@ -8096,6 +8097,66 @@ font-style: italic;
<a href="#InvalidInputExceptionInfo">InvalidInputExceptionInfo</a>
</div> <!-- method -->
<hr/>
<div class="method"><a name="webBackendGetWorkspaceState"/>
<div class="method-path">
<a class="up" href="#__Methods">Up</a>
<pre class="post"><code class="huge"><span class="http-method">post</span> /v1/web_backend/workspace/state</code></pre></div>
<div class="method-summary">Returns the current state of a workspace (<span class="nickname">webBackendGetWorkspaceState</span>)</div>
<div class="method-notes"></div>
<h3 class="field-label">Consumes</h3>
This API call consumes the following media types via the <span class="header">Content-Type</span> request header:
<ul>
<li><code>application/json</code></li>
</ul>
<h3 class="field-label">Request body</h3>
<div class="field-items">
<div class="param">WebBackendWorkspaceState <a href="#WebBackendWorkspaceState">WebBackendWorkspaceState</a> (optional)</div>
<div class="param-desc"><span class="param-type">Body Parameter</span> &mdash; </div>
</div> <!-- field-items -->
<h3 class="field-label">Return type</h3>
<div class="return-type">
<a href="#WebBackendWorkspaceStateResult">WebBackendWorkspaceStateResult</a>
</div>
<!--Todo: process Response Object and its headers, schema, examples -->
<h3 class="field-label">Example data</h3>
<div class="example-data-content-type">Content-Type: application/json</div>
<pre class="example"><code>{
"hasDestinations" : true,
"hasConnections" : true,
"hasSources" : true
}</code></pre>
<h3 class="field-label">Produces</h3>
This API call produces the following media types according to the <span class="header">Accept</span> request header;
the media type will be conveyed by the <span class="header">Content-Type</span> response header.
<ul>
<li><code>application/json</code></li>
</ul>
<h3 class="field-label">Responses</h3>
<h4 class="field-label">200</h4>
Successful operation
<a href="#WebBackendWorkspaceStateResult">WebBackendWorkspaceStateResult</a>
<h4 class="field-label">404</h4>
Object with given id was not found.
<a href="#NotFoundKnownExceptionInfo">NotFoundKnownExceptionInfo</a>
<h4 class="field-label">422</h4>
Input failed validation
<a href="#InvalidInputExceptionInfo">InvalidInputExceptionInfo</a>
</div> <!-- method -->
<hr/>
<div class="method"><a name="webBackendListAllConnectionsForWorkspace"/>
<div class="method-path">
<a class="up" href="#__Methods">Up</a>
@@ -9792,6 +9853,8 @@ font-style: italic;
<li><a href="#WebBackendConnectionSearch"><code>WebBackendConnectionSearch</code> - </a></li>
<li><a href="#WebBackendConnectionUpdate"><code>WebBackendConnectionUpdate</code> - </a></li>
<li><a href="#WebBackendOperationCreateOrUpdate"><code>WebBackendOperationCreateOrUpdate</code> - </a></li>
<li><a href="#WebBackendWorkspaceState"><code>WebBackendWorkspaceState</code> - </a></li>
<li><a href="#WebBackendWorkspaceStateResult"><code>WebBackendWorkspaceStateResult</code> - </a></li>
<li><a href="#WorkspaceCreate"><code>WorkspaceCreate</code> - </a></li>
<li><a href="#WorkspaceGiveFeedback"><code>WorkspaceGiveFeedback</code> - </a></li>
<li><a href="#WorkspaceIdRequestBody"><code>WorkspaceIdRequestBody</code> - </a></li>
@@ -11032,6 +11095,22 @@ if oauth parameters were contained inside the top level, rootObject=[] If they w
<div class="param">operatorConfiguration </div><div class="param-desc"><span class="param-type"><a href="#OperatorConfiguration">OperatorConfiguration</a></span> </div>
</div> <!-- field-items -->
</div>
<div class="model">
<h3><a name="WebBackendWorkspaceState"><code>WebBackendWorkspaceState</code> - </a> <a class="up" href="#__Models">Up</a></h3>
<div class='model-description'></div>
<div class="field-items">
<div class="param">workspaceId </div><div class="param-desc"><span class="param-type"><a href="#UUID">UUID</a></span> format: uuid</div>
</div> <!-- field-items -->
</div>
<div class="model">
<h3><a name="WebBackendWorkspaceStateResult"><code>WebBackendWorkspaceStateResult</code> - </a> <a class="up" href="#__Models">Up</a></h3>
<div class='model-description'></div>
<div class="field-items">
<div class="param">hasConnections </div><div class="param-desc"><span class="param-type"><a href="#boolean">Boolean</a></span> </div>
<div class="param">hasSources </div><div class="param-desc"><span class="param-type"><a href="#boolean">Boolean</a></span> </div>
<div class="param">hasDestinations </div><div class="param-desc"><span class="param-type"><a href="#boolean">Boolean</a></span> </div>
</div> <!-- field-items -->
</div>
<div class="model">
<h3><a name="WorkspaceCreate"><code>WorkspaceCreate</code> - </a> <a class="up" href="#__Models">Up</a></h3>
<div class='model-description'></div>