From 7e09fcc8628c4e5bc053e068380ceddc6957a148 Mon Sep 17 00:00:00 2001 From: Travis Jenkins Date: Tue, 31 Mar 2026 15:36:01 -0400 Subject: [PATCH 01/30] Initial switching over to use GQL for connectors on the card list page --- src/api/gql/connectors.ts | 122 +++++++++++ src/components/capture/Create/index.tsx | 15 +- .../connectors/Grid/ConnectorCards.tsx | 80 +++++-- .../Entity/DetailsForm/useConnectorField.ts | 4 +- .../shared/guards/ConnectorSelected.tsx | 4 +- src/gql-types/gql.ts | 12 ++ src/gql-types/graphql.ts | 181 ++++++++++++++++ src/gql-types/schema.graphql | 196 ++++++++++++++++++ src/stores/Workflow/useWorkflowHydrator.ts | 6 +- src/validation/index.ts | 2 +- 10 files changed, 584 insertions(+), 38 deletions(-) create mode 100644 src/api/gql/connectors.ts diff --git a/src/api/gql/connectors.ts b/src/api/gql/connectors.ts new file mode 100644 index 0000000000..538e67834a --- /dev/null +++ b/src/api/gql/connectors.ts @@ -0,0 +1,122 @@ +import type { ConnectorTag, ConnectorWithTagQuery } from 'src/api/types'; +import type { + SingleConnectorQuery, + ConnectorsGridQuery, +} from 'src/gql-types/graphql'; +import type { EntityWithCreateWorkflow } from 'src/types'; + +import { useCallback } from 'react'; + +import { useClient } from 'urql'; + +import { graphql } from 'src/gql-types'; + +export type ConnectorGridNode = + ConnectorsGridQuery['connectors']['edges'][number]['node']; + +export const CONNECTORS_QUERY = graphql(` + query ConnectorsGrid($filter: ConnectorsFilter, $after: String) { + connectors(first: 100, after: $after, filter: $filter) { + edges { + cursor + node { + id + imageName + logoUrl + title + recommended + shortDescription + connectorTag(orDefault: true) { + id + connectorId + imageTag + documentationUrl + protocol + } + } + } + pageInfo { + hasNextPage + endCursor + } + } + } +`); + +const CONNECTOR_BY_ID_QUERY = graphql(` + query SingleConnector($id: Id!) { + connector(id: $id) { + id + imageName + logoUrl + title + recommended + shortDescription + connectorTag(orDefault: true) { + id + connectorId + imageTag + documentationUrl + endpointSpecSchema + protocol + } + } + } +`); + +// Maps a GQL SingleConnector result to the legacy ConnectorWithTagQuery shape +// used by workflow hydration and downstream stores. +export const toConnectorWithTagQuery = ( + node: NonNullable +): ConnectorWithTagQuery | null => { + const { connectorTag } = node; + if (!connectorTag) return null; + + const tag: ConnectorTag = { + id: connectorTag.id, + connector_id: connectorTag.connectorId, + image_tag: connectorTag.imageTag, + documentation_url: connectorTag.documentationUrl ?? '', + endpoint_spec_schema: connectorTag.endpointSpecSchema, + image_name: node.imageName, + protocol: (connectorTag.protocol ?? 'capture') as EntityWithCreateWorkflow, + title: node.title ?? '', + }; + + return { + id: node.id, + detail: node.shortDescription ?? '', + image_name: node.imageName, + image: node.logoUrl ?? '', + recommended: node.recommended, + title: node.title ?? '', + connector_tags: [tag], + } as ConnectorWithTagQuery; +}; + +export function useGetSingleConnector() { + const client = useClient(); + + return useCallback( + async (connectorId: string) => { + const result = await client + .query( + CONNECTOR_BY_ID_QUERY, + { id: connectorId }, + { requestPolicy: 'network-only' } + ) + .toPromise(); + + if (result.error || !result.data?.connector) { + return { + data: null, + error: result.error ?? 'Connector not found', + }; + } + + const mapped = toConnectorWithTagQuery(result.data.connector); + return { data: mapped ? [mapped] : [], error: null }; + }, + [client] + ); +} diff --git a/src/components/capture/Create/index.tsx b/src/components/capture/Create/index.tsx index 87691f6ab7..834aebf9ce 100644 --- a/src/components/capture/Create/index.tsx +++ b/src/components/capture/Create/index.tsx @@ -7,7 +7,6 @@ import { useEditorStore_id, useEditorStore_persistedDraftId, useEditorStore_queryResponse_mutate, - useEditorStore_setId, } from 'src/components/editor/Store/hooks'; import EntityCreate from 'src/components/shared/Entity/Create'; import EntityToolbar from 'src/components/shared/Entity/Header'; @@ -31,16 +30,12 @@ function CaptureCreate() { const hasConnectors = useValidConnectorsExist(entityType); // Details Form Store - const imageTag = useDetailsFormStore( - (state) => state.details.data.connectorImage - ); const entityNameChanged = useDetailsFormStore( (state) => state.entityNameChanged ); // Draft Editor Store const draftId = useEditorStore_id(); - const setDraftId = useEditorStore_setId(); const persistedDraftId = useEditorStore_persistedDraftId(); // Endpoint Config Store @@ -62,11 +57,13 @@ function CaptureCreate() { } }, [mutateDraftSpecs, mutate_advancedEditor]); + // TODO (GQL:connector) - we do not allow changing I think we're good to remove this // Reset the catalog if the connector changes - useEffect(() => { - setDraftId(null); - setInitiateDiscovery(true); - }, [setDraftId, setInitiateDiscovery, imageTag]); + // useEffect(() => { + // console.log('sup >>>>> '); + // setDraftId(null); + // setInitiateDiscovery(true); + // }, [setDraftId, setInitiateDiscovery, imageTag]); // If the name changed we need to make sure we run discovery again useEffect(() => { diff --git a/src/components/connectors/Grid/ConnectorCards.tsx b/src/components/connectors/Grid/ConnectorCards.tsx index eea4560339..c23235c06c 100644 --- a/src/components/connectors/Grid/ConnectorCards.tsx +++ b/src/components/connectors/Grid/ConnectorCards.tsx @@ -1,15 +1,15 @@ -import type { ConnectorWithTagQuery } from 'src/api/types'; import type { ConnectorCardsProps } from 'src/components/connectors/Grid/types'; +import type { ConnectorProto } from 'src/gql-types/graphql'; import type { TableState } from 'src/types'; import { useEffect, useMemo, useRef, useState } from 'react'; import { Grid, Paper, Typography } from '@mui/material'; -import { useQuery } from '@supabase-cache-helpers/postgrest-swr'; import { FormattedMessage } from 'react-intl'; +import { useQuery } from 'urql'; -import { getConnectors } from 'src/api/connectors'; +import { CONNECTORS_QUERY } from 'src/api/gql/connectors'; import Card from 'src/components/connectors/Grid/cards/Card'; import ConnectorRequestCard from 'src/components/connectors/Grid/cards/ConnectorRequestCard'; import Detail from 'src/components/connectors/Grid/cards/Detail'; @@ -40,13 +40,35 @@ export default function ConnectorCards({ status: TableStatuses.LOADING, }); - const query = useMemo(() => { - return getConnectors(searchQuery, 'asc', protocol); - }, [searchQuery, protocol]); + const variables = useMemo( + () => ({ + filter: protocol + ? { protocol: { eq: protocol as ConnectorProto } } + : undefined, + }), + [protocol] + ); + + const [{ data: queryData, fetching, error }] = useQuery({ + query: CONNECTORS_QUERY, + variables, + }); - const { data: selectResponse, isValidating, error } = useQuery(query); + const selectData = useMemo(() => { + const nodes = (queryData?.connectors.edges ?? []) + .map((edge) => edge.node) + // TODO (gql:connector) - I think we can remove this + .filter((node) => node.connectorTag !== null); - const selectData = useMemo(() => selectResponse ?? [], [selectResponse]); + if (!searchQuery) return nodes; + + const q = searchQuery.toLowerCase(); + return nodes.filter( + (node) => + node.title?.toLowerCase().includes(q) || + node.shortDescription?.toLowerCase().includes(q) + ); + }, [queryData, searchQuery]); const RequestCard = condensed ? (
@@ -54,9 +76,9 @@ export default function ConnectorCards({ ); - const primaryCtaClick = (row: ConnectorWithTagQuery) => { - navigateToCreate(row.connector_tags[0].protocol, { - id: row.connector_tags[0].connector_id, + const primaryCtaClick = (entityType: any, connectorId: string) => { + navigateToCreate(entityType, { + id: connectorId, advanceToForm: true, expressWorkflow: condensed, }); @@ -72,9 +94,9 @@ export default function ConnectorCards({ } else { setTableState({ status: TableStatuses.NO_EXISTING_DATA }); } - }, [selectData, isValidating, error?.message]); + }, [selectData, fetching, error?.message]); - if (isValidating || tableState.status === TableStatuses.LOADING) { + if (fetching || tableState.status === TableStatuses.LOADING) { return ; } @@ -114,26 +136,40 @@ export default function ConnectorCards({ return ( <> {selectData - .map((row) => { + .map((node) => { + // TODO (gql:connector) - doubt we need this + if (!node.connectorTag) { + return null; + } + const ConnectorCard = condensed ? Card : LegacyCard; + const entityType = node.connectorTag?.protocol ?? 'capture'; + return ( primaryCtaClick(row)} - Detail={} - entityType={row.connector_tags[0].protocol} + key={`connector-card-${node.id}`} + clickHandler={() => + primaryCtaClick( + entityType, + node.connectorTag?.connectorId + ) + } + docsUrl={node.connectorTag?.documentationUrl ?? ''} + entityType={entityType} + recommended={node.recommended} + Detail={ + + } Logo={ } - recommended={row.recommended} Title={ } diff --git a/src/components/shared/Entity/DetailsForm/useConnectorField.ts b/src/components/shared/Entity/DetailsForm/useConnectorField.ts index b85c15698b..93ba28c7ed 100644 --- a/src/components/shared/Entity/DetailsForm/useConnectorField.ts +++ b/src/components/shared/Entity/DetailsForm/useConnectorField.ts @@ -20,7 +20,7 @@ import { getConnectorMetadata, } from 'src/utils/connector-utils'; import { hasLength } from 'src/utils/misc-utils'; -import { MAC_ADDR_RE } from 'src/validation'; +import { MAC_ADDR_LIKE_RE } from 'src/validation'; export default function useConnectorField( entityType: EntityWithCreateWorkflow @@ -127,7 +127,7 @@ export default function useConnectorField( const selectedConnectorId = details.data.connectorImage.connectorId; if ( - MAC_ADDR_RE.test(selectedConnectorId) && + MAC_ADDR_LIKE_RE.test(selectedConnectorId) && selectedConnectorId !== originalConnectorImage.connectorId ) { if (selectedConnectorId === connectorId) { diff --git a/src/components/shared/guards/ConnectorSelected.tsx b/src/components/shared/guards/ConnectorSelected.tsx index 9951b7fe78..d5f78e602a 100644 --- a/src/components/shared/guards/ConnectorSelected.tsx +++ b/src/components/shared/guards/ConnectorSelected.tsx @@ -5,7 +5,7 @@ import { Navigate } from 'react-router-dom'; import useGlobalSearchParams, { GlobalSearchParams, } from 'src/hooks/searchParams/useGlobalSearchParams'; -import { MAC_ADDR_RE } from 'src/validation'; +import { MAC_ADDR_LIKE_RE } from 'src/validation'; // This 'navigateToPath' is so stupid and so annoying. However, for whatever reason // if you have the navigate to equal to '..' it threw you back up too many levels @@ -18,7 +18,7 @@ interface Props extends BaseComponentProps { function ConnectorSelectedGuard({ children, navigateToPath }: Props) { const connectorId = useGlobalSearchParams(GlobalSearchParams.CONNECTOR_ID); - if (!MAC_ADDR_RE.test(connectorId)) { + if (!MAC_ADDR_LIKE_RE.test(connectorId)) { return <Navigate to={navigateToPath} replace />; } diff --git a/src/gql-types/gql.ts b/src/gql-types/gql.ts index 4f877a5ce6..edc811d858 100644 --- a/src/gql-types/gql.ts +++ b/src/gql-types/gql.ts @@ -14,6 +14,8 @@ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/ * Learn more about it here: https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#reducing-bundle-size */ type Documents = { + "\n query ConnectorsGrid($filter: ConnectorsFilter, $after: String) {\n connectors(first: 100, after: $after, filter: $filter) {\n edges {\n cursor\n node {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n documentationUrl\n protocol\n }\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n": typeof types.ConnectorsGridDocument, + "\n query SingleConnector($id: Id!) {\n connector(id: $id) {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n documentationUrl\n endpointSpecSchema\n protocol\n }\n }\n }\n": typeof types.SingleConnectorDocument, "\n query DataPlanes($after: String) {\n dataPlanes(first: 100, after: $after) {\n edges {\n node {\n name\n cloudProvider\n region\n isPublic\n fqdn\n cidrBlocks\n awsIamUserArn\n gcpServiceAccountEmail\n azureApplicationClientId\n azureApplicationName\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n": typeof types.DataPlanesDocument, "\n query InviteLinks($first: Int, $after: String) {\n inviteLinks(first: $first, after: $after) {\n edges {\n node {\n token\n ssoProviderId\n catalogPrefix\n capability\n singleUse\n detail\n createdAt\n }\n cursor\n }\n pageInfo {\n ...PageInfoFields\n }\n }\n }\n": typeof types.InviteLinksDocument, "\n mutation CreateInviteLink(\n $catalogPrefix: Prefix!\n $capability: Capability!\n $singleUse: Boolean!\n $detail: String\n ) {\n createInviteLink(\n catalogPrefix: $catalogPrefix\n capability: $capability\n singleUse: $singleUse\n detail: $detail\n ) {\n token\n catalogPrefix\n capability\n singleUse\n detail\n createdAt\n }\n }\n": typeof types.CreateInviteLinkDocument, @@ -32,6 +34,8 @@ type Documents = { "\n query AuthRolesQuery($after: String) {\n prefixes(by: { minCapability: read }, first: 7500, after: $after) {\n edges {\n node {\n prefix\n userCapability\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n": typeof types.AuthRolesQueryDocument, }; const documents: Documents = { + "\n query ConnectorsGrid($filter: ConnectorsFilter, $after: String) {\n connectors(first: 100, after: $after, filter: $filter) {\n edges {\n cursor\n node {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n documentationUrl\n protocol\n }\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n": types.ConnectorsGridDocument, + "\n query SingleConnector($id: Id!) {\n connector(id: $id) {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n documentationUrl\n endpointSpecSchema\n protocol\n }\n }\n }\n": types.SingleConnectorDocument, "\n query DataPlanes($after: String) {\n dataPlanes(first: 100, after: $after) {\n edges {\n node {\n name\n cloudProvider\n region\n isPublic\n fqdn\n cidrBlocks\n awsIamUserArn\n gcpServiceAccountEmail\n azureApplicationClientId\n azureApplicationName\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n": types.DataPlanesDocument, "\n query InviteLinks($first: Int, $after: String) {\n inviteLinks(first: $first, after: $after) {\n edges {\n node {\n token\n ssoProviderId\n catalogPrefix\n capability\n singleUse\n detail\n createdAt\n }\n cursor\n }\n pageInfo {\n ...PageInfoFields\n }\n }\n }\n": types.InviteLinksDocument, "\n mutation CreateInviteLink(\n $catalogPrefix: Prefix!\n $capability: Capability!\n $singleUse: Boolean!\n $detail: String\n ) {\n createInviteLink(\n catalogPrefix: $catalogPrefix\n capability: $capability\n singleUse: $singleUse\n detail: $detail\n ) {\n token\n catalogPrefix\n capability\n singleUse\n detail\n createdAt\n }\n }\n": types.CreateInviteLinkDocument, @@ -64,6 +68,14 @@ const documents: Documents = { */ export function graphql(source: string): unknown; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "\n query ConnectorsGrid($filter: ConnectorsFilter, $after: String) {\n connectors(first: 100, after: $after, filter: $filter) {\n edges {\n cursor\n node {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n documentationUrl\n protocol\n }\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n"): (typeof documents)["\n query ConnectorsGrid($filter: ConnectorsFilter, $after: String) {\n connectors(first: 100, after: $after, filter: $filter) {\n edges {\n cursor\n node {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n documentationUrl\n protocol\n }\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "\n query SingleConnector($id: Id!) {\n connector(id: $id) {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n documentationUrl\n endpointSpecSchema\n protocol\n }\n }\n }\n"): (typeof documents)["\n query SingleConnector($id: Id!) {\n connector(id: $id) {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n documentationUrl\n endpointSpecSchema\n protocol\n }\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/src/gql-types/graphql.ts b/src/gql-types/graphql.ts index 1568eddcb5..3c430e409f 100644 --- a/src/gql-types/graphql.ts +++ b/src/gql-types/graphql.ts @@ -318,6 +318,70 @@ export type ConnectionHealthTestResult = { results: Array<StorageHealthItem>; }; +export type Connector = { + __typename?: 'Connector'; + /** Returns the ConnectorTag object for the given image tag, which must begin with a `:`. */ + connectorTag?: Maybe<ConnectorTag>; + /** Timestamp of when the connector was first created */ + createdAt: Scalars['DateTime']['output']; + /** + * Returns the default `ConnectorTag` for this connector. This is the one + * that should be used by default when publishing new tasks for this + * connector. There will only be a default image tag if at least one tag + * has successfully completed the connector Spec RPC. + */ + defaultImageTag?: Maybe<Scalars['String']['output']>; + /** Link to an external site with more information about the endpoint */ + externalUrl: Scalars['String']['output']; + /** Unique id of the connector */ + id: Scalars['Id']['output']; + /** Name of the conector's OCI (Docker) Container image, for example "ghcr.io/estuary/source-postgres" */ + imageName: Scalars['String']['output']; + /** The connector's logo image, represented as a URL per locale */ + logoUrl?: Maybe<Scalars['String']['output']>; + /** A longform description of this connector */ + longDescription?: Maybe<Scalars['String']['output']>; + /** Does Estuary's marketing team want this one to appear at the top of the results? */ + recommended: Scalars['Boolean']['output']; + /** Brief human readable description, at most a few sentences */ + shortDescription?: Maybe<Scalars['String']['output']>; + /** All the tags that are available for this connector. */ + tags: Array<ConnectorTagRef>; + /** The title, a few words at most */ + title?: Maybe<Scalars['String']['output']>; +}; + + +export type ConnectorConnectorTagArgs = { + imageTag?: InputMaybe<Scalars['String']['input']>; + orDefault: Scalars['Boolean']['input']; +}; + +export type ConnectorConnection = { + __typename?: 'ConnectorConnection'; + /** A list of edges. */ + edges: Array<ConnectorEdge>; + /** Information to aid in pagination. */ + pageInfo: PageInfo; +}; + +/** An edge in a connection. */ +export type ConnectorEdge = { + __typename?: 'ConnectorEdge'; + /** A cursor for use in pagination */ + cursor: Scalars['String']['output']; + /** The item at the end of the edge */ + node: Connector; +}; + +/** + * The type of task that the connector is used for. Note that derivation + * connectors do exist, but aren't yet represented in `connector_tags`. + */ +export type ConnectorProto = + | 'capture' + | 'materialization'; + /** The shape of a connector status, which matches that of an ops::Log. */ export type ConnectorStatus = { __typename?: 'ConnectorStatus'; @@ -334,6 +398,56 @@ export type ConnectorStatus = { ts: Scalars['DateTime']['output']; }; +export type ConnectorTag = { + __typename?: 'ConnectorTag'; + /** The id of the connector this tag relates to. */ + connectorId: Scalars['Id']['output']; + /** Time at which the ConnectorTag was created */ + createdAt: Scalars['DateTime']['output']; + /** + * The default interval between invocations of a capture using this connector tag. + * Formatted as HH:MM:SS. Only applicable to non-streaming (polling) capture connectors. + */ + defaultCaptureInterval?: Maybe<Scalars['String']['output']>; + /** Whether the UI should hide the backfill button for this connector */ + disableBackfill: Scalars['Boolean']['output']; + /** URL pointing to the documentation page for this connector */ + documentationUrl?: Maybe<Scalars['String']['output']>; + /** Endpoint specification JSON-Schema of the tagged connector */ + endpointSpecSchema?: Maybe<Scalars['JSON']['output']>; + /** Unique id of the connector tag */ + id: Scalars['Id']['output']; + /** The OCI Image tag value, including the leading `:`. For example `:v1` */ + imageTag: Scalars['String']['output']; + /** The protocol of the connector with this tag value */ + protocol?: Maybe<ConnectorProto>; + /** Resource specification JSON-Schema of the tagged connector */ + resourceSpecSchema?: Maybe<Scalars['JSON']['output']>; + /** Time at which the ConnectorTag was last updated */ + updatedAt: Scalars['DateTime']['output']; +}; + +export type ConnectorTagRef = { + __typename?: 'ConnectorTagRef'; + /** The canonical id of this connector tag */ + id: Scalars['Id']['output']; + /** The OCI image tag, includeing the leading `:`, for example `:v2` */ + imageTag: Scalars['String']['output']; + /** The protocol of this connector tag, if known */ + protocol?: Maybe<ConnectorProto>; + /** + * Returns whether a connector Spec RPC has ever been successful for this tag. + * Concretely, this is used to determine whether the tag could be used by the + * UI or flowctl for publishing tasks, because the Spec RPC populates the + * `endpointSpecSchema`, `resourceSpecSchema`, `protocol`, etc. + */ + specSucceeded: Scalars['Boolean']['output']; +}; + +export type ConnectorsFilter = { + protocol?: InputMaybe<ProtocolFilter>; +}; + /** Status info related to the controller */ export type Controller = { __typename?: 'Controller'; @@ -880,6 +994,10 @@ export type PrefixesBy = { minCapability: Capability; }; +export type ProtocolFilter = { + eq: ConnectorProto; +}; + /** Summary of a publication that was attempted by a controller. */ export type PublicationInfo = { __typename?: 'PublicationInfo'; @@ -949,6 +1067,31 @@ export type QueryRoot = { * prefixes. */ alerts: AlertConnection; + /** + * Returns information about a single connector, which may or may not have + * had a successful Spec RPC, and thus may or may not be usable in the + * Estuary UI. At least one parameter must be provided. If multiple + * parameters are provided, then the connector must match _both_ the image + * name and id parameters in order to be returned. + */ + connector?: Maybe<Connector>; + /** + * Returns the ConnectorTag for a given full (including the version) OCI + * image name. The returned tag may be different from the version in the + * image name. This would happen if there is no connector spec for the + * given tag, but one exists for a different tag. The return value will be + * null if either the connector image is unkown, or if there has not been a + * successful Spec for any version of that image. + */ + connectorTag?: Maybe<ConnectorTag>; + /** + * Returns a paginated list of connectors. This query only returns + * connectors that have at least one `ConnectorTag` that has had a + * successful Spec RPC. Connectors that have not had at least one + * successful Spec RPC cannot be used by the Estuary UI, and so are + * excluded here. + */ + connectors: ConnectorConnection; /** * Returns data planes accessible to the current user. * @@ -999,6 +1142,27 @@ export type QueryRootAlertsArgs = { }; +export type QueryRootConnectorArgs = { + id?: InputMaybe<Scalars['Id']['input']>; + imageName?: InputMaybe<Scalars['String']['input']>; +}; + + +export type QueryRootConnectorTagArgs = { + fullImageName?: InputMaybe<Scalars['String']['input']>; + id?: InputMaybe<Scalars['Id']['input']>; +}; + + +export type QueryRootConnectorsArgs = { + after?: InputMaybe<Scalars['String']['input']>; + before?: InputMaybe<Scalars['String']['input']>; + filter?: InputMaybe<ConnectorsFilter>; + first?: InputMaybe<Scalars['Int']['input']>; + last?: InputMaybe<Scalars['Int']['input']>; +}; + + export type QueryRootDataPlanesArgs = { after?: InputMaybe<Scalars['String']['input']>; before?: InputMaybe<Scalars['String']['input']>; @@ -1329,6 +1493,21 @@ export type UpdateStorageMappingResult = { republish: Scalars['Boolean']['output']; }; +export type ConnectorsGridQueryVariables = Exact<{ + filter?: InputMaybe<ConnectorsFilter>; + after?: InputMaybe<Scalars['String']['input']>; +}>; + + +export type ConnectorsGridQuery = { __typename?: 'QueryRoot', connectors: { __typename?: 'ConnectorConnection', edges: Array<{ __typename?: 'ConnectorEdge', cursor: string, node: { __typename?: 'Connector', id: any, imageName: string, logoUrl?: string | null, title?: string | null, recommended: boolean, shortDescription?: string | null, connectorTag?: { __typename?: 'ConnectorTag', id: any, connectorId: any, imageTag: string, documentationUrl?: string | null, protocol?: ConnectorProto | null } | null } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null } } }; + +export type SingleConnectorQueryVariables = Exact<{ + id: Scalars['Id']['input']; +}>; + + +export type SingleConnectorQuery = { __typename?: 'QueryRoot', connector?: { __typename?: 'Connector', id: any, imageName: string, logoUrl?: string | null, title?: string | null, recommended: boolean, shortDescription?: string | null, connectorTag?: { __typename?: 'ConnectorTag', id: any, connectorId: any, imageTag: string, documentationUrl?: string | null, endpointSpecSchema?: any | null, protocol?: ConnectorProto | null } | null } | null }; + export type DataPlanesQueryVariables = Exact<{ after?: InputMaybe<Scalars['String']['input']>; }>; @@ -1450,6 +1629,8 @@ export type AuthRolesQueryQueryVariables = Exact<{ export type AuthRolesQueryQuery = { __typename?: 'QueryRoot', prefixes: { __typename?: 'PrefixRefConnection', edges: Array<{ __typename?: 'PrefixRefEdge', node: { __typename?: 'PrefixRef', prefix: any, userCapability: Capability } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null } } }; export const PageInfoFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PageInfoFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PageInfo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]} as unknown as DocumentNode<PageInfoFieldsFragment, unknown>; +export const ConnectorsGridDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ConnectorsGrid"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ConnectorsFilter"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"connectors"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"100"}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"imageName"}},{"kind":"Field","name":{"kind":"Name","value":"logoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"recommended"}},{"kind":"Field","name":{"kind":"Name","value":"shortDescription"}},{"kind":"Field","name":{"kind":"Name","value":"connectorTag"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orDefault"},"value":{"kind":"BooleanValue","value":true}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"connectorId"}},{"kind":"Field","name":{"kind":"Name","value":"imageTag"}},{"kind":"Field","name":{"kind":"Name","value":"documentationUrl"}},{"kind":"Field","name":{"kind":"Name","value":"protocol"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]} as unknown as DocumentNode<ConnectorsGridQuery, ConnectorsGridQueryVariables>; +export const SingleConnectorDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"SingleConnector"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Id"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"connector"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"imageName"}},{"kind":"Field","name":{"kind":"Name","value":"logoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"recommended"}},{"kind":"Field","name":{"kind":"Name","value":"shortDescription"}},{"kind":"Field","name":{"kind":"Name","value":"connectorTag"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orDefault"},"value":{"kind":"BooleanValue","value":true}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"connectorId"}},{"kind":"Field","name":{"kind":"Name","value":"imageTag"}},{"kind":"Field","name":{"kind":"Name","value":"documentationUrl"}},{"kind":"Field","name":{"kind":"Name","value":"endpointSpecSchema"}},{"kind":"Field","name":{"kind":"Name","value":"protocol"}}]}}]}}]}}]} as unknown as DocumentNode<SingleConnectorQuery, SingleConnectorQueryVariables>; export const DataPlanesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"DataPlanes"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"dataPlanes"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"100"}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"cloudProvider"}},{"kind":"Field","name":{"kind":"Name","value":"region"}},{"kind":"Field","name":{"kind":"Name","value":"isPublic"}},{"kind":"Field","name":{"kind":"Name","value":"fqdn"}},{"kind":"Field","name":{"kind":"Name","value":"cidrBlocks"}},{"kind":"Field","name":{"kind":"Name","value":"awsIamUserArn"}},{"kind":"Field","name":{"kind":"Name","value":"gcpServiceAccountEmail"}},{"kind":"Field","name":{"kind":"Name","value":"azureApplicationClientId"}},{"kind":"Field","name":{"kind":"Name","value":"azureApplicationName"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]} as unknown as DocumentNode<DataPlanesQuery, DataPlanesQueryVariables>; export const InviteLinksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"InviteLinks"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"inviteLinks"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"ssoProviderId"}},{"kind":"Field","name":{"kind":"Name","value":"catalogPrefix"}},{"kind":"Field","name":{"kind":"Name","value":"capability"}},{"kind":"Field","name":{"kind":"Name","value":"singleUse"}},{"kind":"Field","name":{"kind":"Name","value":"detail"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PageInfoFields"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PageInfoFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PageInfo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]} as unknown as DocumentNode<InviteLinksQuery, InviteLinksQueryVariables>; export const CreateInviteLinkDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateInviteLink"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"catalogPrefix"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Prefix"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"capability"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Capability"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"singleUse"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"detail"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createInviteLink"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"catalogPrefix"},"value":{"kind":"Variable","name":{"kind":"Name","value":"catalogPrefix"}}},{"kind":"Argument","name":{"kind":"Name","value":"capability"},"value":{"kind":"Variable","name":{"kind":"Name","value":"capability"}}},{"kind":"Argument","name":{"kind":"Name","value":"singleUse"},"value":{"kind":"Variable","name":{"kind":"Name","value":"singleUse"}}},{"kind":"Argument","name":{"kind":"Name","value":"detail"},"value":{"kind":"Variable","name":{"kind":"Name","value":"detail"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"catalogPrefix"}},{"kind":"Field","name":{"kind":"Name","value":"capability"}},{"kind":"Field","name":{"kind":"Name","value":"singleUse"}},{"kind":"Field","name":{"kind":"Name","value":"detail"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]} as unknown as DocumentNode<CreateInviteLinkMutation, CreateInviteLinkMutationVariables>; diff --git a/src/gql-types/schema.graphql b/src/gql-types/schema.graphql index 5d88e64886..9618f36278 100644 --- a/src/gql-types/schema.graphql +++ b/src/gql-types/schema.graphql @@ -328,6 +328,89 @@ type ConnectionHealthTestResult { results: [StorageHealthItem!]! } +type Connector { + """ + Returns the ConnectorTag object for the given image tag, which must begin with a `:`. + """ + connectorTag( + """the OCI Image tag, including the leading ':', e.g. ':v1'""" + imageTag: String + + """ + Whether to return the default connector tag instead when the requested tag is not present or has not had a successful Spec RPC + """ + orDefault: Boolean! + ): ConnectorTag + + """Timestamp of when the connector was first created""" + createdAt: DateTime! + + """ + Returns the default `ConnectorTag` for this connector. This is the one + that should be used by default when publishing new tasks for this + connector. There will only be a default image tag if at least one tag + has successfully completed the connector Spec RPC. + """ + defaultImageTag: String + + """Link to an external site with more information about the endpoint""" + externalUrl: String! + + """Unique id of the connector""" + id: Id! + + """ + Name of the conector's OCI (Docker) Container image, for example "ghcr.io/estuary/source-postgres" + """ + imageName: String! + + """The connector's logo image, represented as a URL per locale""" + logoUrl: String + + """A longform description of this connector""" + longDescription: String + + """ + Does Estuary's marketing team want this one to appear at the top of the results? + """ + recommended: Boolean! + + """Brief human readable description, at most a few sentences""" + shortDescription: String + + """All the tags that are available for this connector.""" + tags: [ConnectorTagRef!]! + + """The title, a few words at most""" + title: String +} + +type ConnectorConnection { + """A list of edges.""" + edges: [ConnectorEdge!]! + + """Information to aid in pagination.""" + pageInfo: PageInfo! +} + +"""An edge in a connection.""" +type ConnectorEdge { + """A cursor for use in pagination""" + cursor: String! + + """The item at the end of the edge""" + node: Connector! +} + +""" +The type of task that the connector is used for. Note that derivation +connectors do exist, but aren't yet represented in `connector_tags`. +""" +enum ConnectorProto { + capture + materialization +} + """The shape of a connector status, which matches that of an ops::Log.""" type ConnectorStatus { """ @@ -348,6 +431,67 @@ type ConnectorStatus { ts: DateTime! } +type ConnectorTag { + """The id of the connector this tag relates to.""" + connectorId: Id! + + """Time at which the ConnectorTag was created""" + createdAt: DateTime! + + """ + The default interval between invocations of a capture using this connector tag. + Formatted as HH:MM:SS. Only applicable to non-streaming (polling) capture connectors. + """ + defaultCaptureInterval: String + + """Whether the UI should hide the backfill button for this connector""" + disableBackfill: Boolean! + + """URL pointing to the documentation page for this connector""" + documentationUrl: String + + """Endpoint specification JSON-Schema of the tagged connector""" + endpointSpecSchema: JSON + + """Unique id of the connector tag""" + id: Id! + + """The OCI Image tag value, including the leading `:`. For example `:v1`""" + imageTag: String! + + """The protocol of the connector with this tag value""" + protocol: ConnectorProto + + """Resource specification JSON-Schema of the tagged connector""" + resourceSpecSchema: JSON + + """Time at which the ConnectorTag was last updated""" + updatedAt: DateTime! +} + +type ConnectorTagRef { + """The canonical id of this connector tag""" + id: Id! + + """The OCI image tag, includeing the leading `:`, for example `:v2`""" + imageTag: String! + + """The protocol of this connector tag, if known""" + protocol: ConnectorProto + + """ + Returns whether a connector Spec RPC has ever been successful for this tag. + Concretely, this is used to determine whether the tag could be used by the + UI or flowctl for publishing tasks, because the Spec RPC populates the + `endpointSpecSchema`, `resourceSpecSchema`, `protocol`, etc. + """ + specSucceeded: Boolean! +} + +input ConnectorsFilter { + protocol: ProtocolFilter +} + """Status info related to the controller""" type Controller { """Present for captures, collections, and materializations.""" @@ -855,6 +999,10 @@ input PrefixesBy { minCapability: Capability! } +input ProtocolFilter { + eq: ConnectorProto! +} + """Summary of a publication that was attempted by a controller.""" type PublicationInfo { """Time at which the publication was completed""" @@ -932,6 +1080,54 @@ type QueryRoot { """ alerts(after: String, before: String, by: AlertsBy!, first: Int, last: Int): AlertConnection! + """ + Returns information about a single connector, which may or may not have + had a successful Spec RPC, and thus may or may not be usable in the + Estuary UI. At least one parameter must be provided. If multiple + parameters are provided, then the connector must match _both_ the image + name and id parameters in order to be returned. + """ + connector( + """ + the id of the connector, with or without ':' separators, e.g. '1122334455aabbcc' + """ + id: Id + + """ + the OCI image name, without a version tag, e.g. 'ghcr.io/estuary/source-foo' + """ + imageName: String + ): Connector + + """ + Returns the ConnectorTag for a given full (including the version) OCI + image name. The returned tag may be different from the version in the + image name. This would happen if there is no connector spec for the + given tag, but one exists for a different tag. The return value will be + null if either the connector image is unkown, or if there has not been a + successful Spec for any version of that image. + """ + connectorTag( + """ + the full OCI image name, including the version tag, e.g. 'ghcr.io/estuary/source-foo:v1' + """ + fullImageName: String + + """ + the id of the connectorTag, with or without ':' separators, e.g. '1122334455aabbcc' + """ + id: Id + ): ConnectorTag + + """ + Returns a paginated list of connectors. This query only returns + connectors that have at least one `ConnectorTag` that has had a + successful Spec RPC. Connectors that have not had at least one + successful Spec RPC cannot be used by the Estuary UI, and so are + excluded here. + """ + connectors(after: String, before: String, filter: ConnectorsFilter, first: Int, last: Int): ConnectorConnection! + """ Returns data planes accessible to the current user. diff --git a/src/stores/Workflow/useWorkflowHydrator.ts b/src/stores/Workflow/useWorkflowHydrator.ts index f8027c2531..22447ddba2 100644 --- a/src/stores/Workflow/useWorkflowHydrator.ts +++ b/src/stores/Workflow/useWorkflowHydrator.ts @@ -1,6 +1,6 @@ import { useCallback } from 'react'; -import { getSingleConnectorWithTag } from 'src/api/connectors'; +import { useGetSingleConnector } from 'src/api/gql/connectors'; import useGlobalSearchParams, { GlobalSearchParams, } from 'src/hooks/searchParams/useGlobalSearchParams'; @@ -17,6 +17,7 @@ export const useWorkflowHydrator = (expressWorkflow: boolean | undefined) => { const { hydrateDetailsForm } = useDetailsFormHydrator(); const { hydrateEndpointConfig } = useEndpointConfigHydrator(); + const getSingleConnector = useGetSingleConnector(); const baseCatalogPrefix = useEntitiesStore((state) => { const storageMappingPrefixes = Object.keys(state.storageMappings); @@ -40,7 +41,7 @@ export const useWorkflowHydrator = (expressWorkflow: boolean | undefined) => { const hydrateWorkflow = useCallback(async () => { const { data: connectorMetadata, error: connectorError } = - await getSingleConnectorWithTag(connectorId); + await getSingleConnector(connectorId); if ( !hasLength(connectorId) || @@ -82,6 +83,7 @@ export const useWorkflowHydrator = (expressWorkflow: boolean | undefined) => { catalogName, connectorId, expressWorkflow, + getSingleConnector, hydrateDetailsForm, hydrateEndpointConfig, setConnectorMetadata, diff --git a/src/validation/index.ts b/src/validation/index.ts index cc6db79cae..5b941a0ca0 100644 --- a/src/validation/index.ts +++ b/src/validation/index.ts @@ -29,7 +29,7 @@ export const DATE_TIME_RE = new RegExp( /^([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z)$/ ); -export const MAC_ADDR_RE = new RegExp(/^([0-9A-F]{2}:){7}([0-9A-F]{2})$/i); +export const MAC_ADDR_LIKE_RE = new RegExp(/^([0-9A-F]{2}){7}([0-9A-F]{2})$/i); export const validateCatalogName = ( value: string, From e6b270fc8e926e150018cf7fbf5580e8207f799a Mon Sep 17 00:00:00 2001 From: Travis Jenkins <travis@estuary.dev> Date: Tue, 31 Mar 2026 16:08:06 -0400 Subject: [PATCH 02/30] Switching over to storing the connector stuff in context --- src/api/gql/connectors.ts | 5 +- .../Entity/DetailsForm/useConnectorField.ts | 128 +++++------------- src/context/ConnectorTag.tsx | 101 ++++++++++++++ src/gql-types/gql.ts | 6 +- src/gql-types/graphql.ts | 4 +- .../DetailsForm/useDetailsFormHydrator.ts | 98 ++++++-------- .../useEndpointConfigHydrator.ts | 122 +++++++---------- src/stores/Workflow/Hydrator.tsx | 16 ++- src/stores/Workflow/Store.ts | 2 +- src/stores/Workflow/hooks.ts | 64 ++++----- src/stores/Workflow/types.ts | 4 +- src/stores/Workflow/useWorkflowHydrator.ts | 45 +----- 12 files changed, 288 insertions(+), 307 deletions(-) create mode 100644 src/context/ConnectorTag.tsx diff --git a/src/api/gql/connectors.ts b/src/api/gql/connectors.ts index 538e67834a..adfb9c5fd9 100644 --- a/src/api/gql/connectors.ts +++ b/src/api/gql/connectors.ts @@ -43,7 +43,7 @@ export const CONNECTORS_QUERY = graphql(` } `); -const CONNECTOR_BY_ID_QUERY = graphql(` +export const CONNECTOR_BY_ID_QUERY = graphql(` query SingleConnector($id: Id!) { connector(id: $id) { id @@ -56,8 +56,11 @@ const CONNECTOR_BY_ID_QUERY = graphql(` id connectorId imageTag + defaultCaptureInterval + disableBackfill documentationUrl endpointSpecSchema + resourceSpecSchema protocol } } diff --git a/src/components/shared/Entity/DetailsForm/useConnectorField.ts b/src/components/shared/Entity/DetailsForm/useConnectorField.ts index 93ba28c7ed..882d60ec05 100644 --- a/src/components/shared/Entity/DetailsForm/useConnectorField.ts +++ b/src/components/shared/Entity/DetailsForm/useConnectorField.ts @@ -1,26 +1,20 @@ import type { Details } from 'src/stores/DetailsForm/types'; import type { EntityWithCreateWorkflow } from 'src/types'; -import type { ConnectorVersionEvaluationOptions } from 'src/utils/connector-utils'; -import { useCallback, useEffect, useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import { useIntl } from 'react-intl'; import useEntityCreateNavigate from 'src/components/shared/Entity/hooks/useEntityCreateNavigate'; -import { useEntityWorkflow_Editing } from 'src/context/Workflow'; import { CONNECTOR_IMAGE_SCOPE } from 'src/forms/renderers/Connectors'; import useGlobalSearchParams, { GlobalSearchParams, } from 'src/hooks/searchParams/useGlobalSearchParams'; -import { useDetailsForm_changed_connectorId } from 'src/stores/DetailsForm/hooks'; import { useDetailsFormStore } from 'src/stores/DetailsForm/Store'; import { useWorkflowStore } from 'src/stores/Workflow/Store'; -import { - evaluateConnectorVersions, - getConnectorMetadata, -} from 'src/utils/connector-utils'; import { hasLength } from 'src/utils/misc-utils'; -import { MAC_ADDR_LIKE_RE } from 'src/validation'; + +const DEKAF_IMAGE_PREFIX = 'ghcr.io/estuary/dekaf-'; export default function useConnectorField( entityType: EntityWithCreateWorkflow @@ -30,75 +24,40 @@ export default function useConnectorField( const intl = useIntl(); const navigateToCreate = useEntityCreateNavigate(); - const isEdit = useEntityWorkflow_Editing(); - const originalConnectorImage = useDetailsFormStore( - (state) => state.details.data.connectorImage - ); - const connectorImageTag = useDetailsFormStore( - (state) => state.details.data.connectorImage.imageTag - ); - const connectorIdChanged = useDetailsForm_changed_connectorId(); - const setDetails_connector = useDetailsFormStore( - (state) => state.setDetails_connector - ); const setEntityNameChanged = useDetailsFormStore( (state) => state.setEntityNameChanged ); - const connectorTags = useWorkflowStore((state) => state.connectorMetadata); + const connectorTag = useWorkflowStore((state) => state.connectorMetadata); - useEffect(() => { - if (connectorId && hasLength(connectorTags) && connectorIdChanged) { - connectorTags.find((connector) => { - const connectorTag = evaluateConnectorVersions(connector); + const connectorsOneOf = useMemo(() => { + if (!connectorTag) { + return []; + } - const connectorLocated = - connectorTag.connector_id === connectorId; + const { id, connectorId: tagConnectorId, imageTag, connector } = + connectorTag; - if (connectorLocated) { - setDetails_connector(getConnectorMetadata(connector)); - } + const base = { + connectorId: tagConnectorId, + iconPath: connector.logoUrl ?? '', + id, + imageName: connector.imageName, + imageTag, + }; - return connectorLocated; - }); - } - }, [setDetails_connector, connectorId, connectorIdChanged, connectorTags]); - - const versionEvaluationOptions: - | ConnectorVersionEvaluationOptions - | undefined = useMemo(() => { - // This is rare but can happen so being safe. - // If you remove the connector id from the create URL - if (!connectorImageTag) { - return undefined; - } - - return isEdit && hasLength(connectorId) + const connectorImage = connector.imageName.startsWith(DEKAF_IMAGE_PREFIX) ? { - connectorId, - existingImageTag: connectorImageTag, + ...base, + variant: connector.imageName.substring( + DEKAF_IMAGE_PREFIX.length + ), } - : undefined; - }, [connectorId, connectorImageTag, isEdit]); - - const connectorsOneOf = useMemo(() => { - const response = [] as { title: string; const: Object }[]; - - if (connectorTags.length > 0) { - connectorTags.forEach((connector) => { - response.push({ - const: getConnectorMetadata( - connector, - versionEvaluationOptions - ), - title: connector.title, - }); - }); - } + : { ...base, imagePath: `${connector.imageName}${imageTag}` }; - return response; - }, [connectorTags, versionEvaluationOptions]); + return [{ const: connectorImage, title: connector.title ?? connector.imageName }]; + }, [connectorTag]); const connectorSchema = useMemo( () => ({ @@ -124,34 +83,19 @@ export default function useConnectorField( const setConnector = useCallback( (details: Details, selectedDataPlaneId: string | undefined) => { - const selectedConnectorId = details.data.connectorImage.connectorId; - - if ( - MAC_ADDR_LIKE_RE.test(selectedConnectorId) && - selectedConnectorId !== originalConnectorImage.connectorId - ) { - if (selectedConnectorId === connectorId) { - setDetails_connector(details.data.connectorImage); - } else { - setEntityNameChanged(details.data.entityName); - - // TODO (data-plane): Set search param of interest instead of using navigate function. - navigateToCreate(entityType, { - id: selectedConnectorId, - advanceToForm: true, - dataPlaneId: selectedDataPlaneId ?? null, - }); - } + if (!hasLength(connectorId)) { + return; } + + setEntityNameChanged(details.data.entityName); + + navigateToCreate(entityType, { + id: connectorId, + advanceToForm: true, + dataPlaneId: selectedDataPlaneId ?? null, + }); }, - [ - connectorId, - entityType, - navigateToCreate, - originalConnectorImage, - setDetails_connector, - setEntityNameChanged, - ] + [connectorId, entityType, navigateToCreate, setEntityNameChanged] ); return { connectorSchema, connectorUISchema, setConnector }; diff --git a/src/context/ConnectorTag.tsx b/src/context/ConnectorTag.tsx new file mode 100644 index 0000000000..3f2f6894cc --- /dev/null +++ b/src/context/ConnectorTag.tsx @@ -0,0 +1,101 @@ +import type { SingleConnectorQuery } from 'src/gql-types/graphql'; +import type { BaseComponentProps } from 'src/types'; + +import { createContext, useContext, useEffect, useState } from 'react'; + +import { useClient } from 'urql'; + +import { CONNECTOR_BY_ID_QUERY } from 'src/api/gql/connectors'; +import ErrorComponent from 'src/components/shared/Error'; +import useGlobalSearchParams, { + GlobalSearchParams, +} from 'src/hooks/searchParams/useGlobalSearchParams'; +import { logRocketConsole } from 'src/services/shared'; +import { BASE_ERROR } from 'src/services/supabase'; +import { hasLength } from 'src/utils/misc-utils'; + +export type ConnectorTagData = NonNullable< + NonNullable<SingleConnectorQuery['connector']>['connectorTag'] +> & { + connector: Pick< + NonNullable<SingleConnectorQuery['connector']>, + 'id' | 'imageName' | 'logoUrl' | 'title' + >; +}; + +const ConnectorTagContext = createContext<ConnectorTagData | null>(null); + +export const ConnectorTagProvider = ({ children }: BaseComponentProps) => { + const connectorId = useGlobalSearchParams(GlobalSearchParams.CONNECTOR_ID); + const client = useClient(); + + const [connectorTag, setConnectorTag] = useState<ConnectorTagData | null>( + null + ); + const [fetchError, setFetchError] = useState<string | null>(null); + + useEffect(() => { + if (!hasLength(connectorId)) { + return; + } + + client + .query( + CONNECTOR_BY_ID_QUERY, + { id: connectorId }, + { requestPolicy: 'network-only' } + ) + .toPromise() + .then(({ data, error }) => { + const connector = data?.connector; + const connectorTagData = connector?.connectorTag; + + if (error || !connector || !connectorTagData) { + logRocketConsole('Failed to fetch connector tag', error); + setFetchError('Failed to load connector information'); + return; + } + + setConnectorTag({ + ...connectorTagData, + connector: { + id: connector.id, + imageName: connector.imageName, + logoUrl: connector.logoUrl, + title: connector.title, + }, + }); + }); + }, [client, connectorId]); + + if (fetchError) { + return ( + <ErrorComponent + condensed + error={{ ...BASE_ERROR, message: fetchError }} + /> + ); + } + + if (!connectorTag) { + return null; + } + + return ( + <ConnectorTagContext.Provider value={connectorTag}> + {children} + </ConnectorTagContext.Provider> + ); +}; + +export const useConnectorTag = (): ConnectorTagData => { + const context = useContext(ConnectorTagContext); + + if (!context) { + throw new Error( + 'useConnectorTag must be used within a ConnectorTagProvider' + ); + } + + return context; +}; diff --git a/src/gql-types/gql.ts b/src/gql-types/gql.ts index edc811d858..8bf18de2e9 100644 --- a/src/gql-types/gql.ts +++ b/src/gql-types/gql.ts @@ -15,7 +15,7 @@ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/ */ type Documents = { "\n query ConnectorsGrid($filter: ConnectorsFilter, $after: String) {\n connectors(first: 100, after: $after, filter: $filter) {\n edges {\n cursor\n node {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n documentationUrl\n protocol\n }\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n": typeof types.ConnectorsGridDocument, - "\n query SingleConnector($id: Id!) {\n connector(id: $id) {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n documentationUrl\n endpointSpecSchema\n protocol\n }\n }\n }\n": typeof types.SingleConnectorDocument, + "\n query SingleConnector($id: Id!) {\n connector(id: $id) {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n defaultCaptureInterval\n disableBackfill\n documentationUrl\n endpointSpecSchema\n resourceSpecSchema\n protocol\n }\n }\n }\n": typeof types.SingleConnectorDocument, "\n query DataPlanes($after: String) {\n dataPlanes(first: 100, after: $after) {\n edges {\n node {\n name\n cloudProvider\n region\n isPublic\n fqdn\n cidrBlocks\n awsIamUserArn\n gcpServiceAccountEmail\n azureApplicationClientId\n azureApplicationName\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n": typeof types.DataPlanesDocument, "\n query InviteLinks($first: Int, $after: String) {\n inviteLinks(first: $first, after: $after) {\n edges {\n node {\n token\n ssoProviderId\n catalogPrefix\n capability\n singleUse\n detail\n createdAt\n }\n cursor\n }\n pageInfo {\n ...PageInfoFields\n }\n }\n }\n": typeof types.InviteLinksDocument, "\n mutation CreateInviteLink(\n $catalogPrefix: Prefix!\n $capability: Capability!\n $singleUse: Boolean!\n $detail: String\n ) {\n createInviteLink(\n catalogPrefix: $catalogPrefix\n capability: $capability\n singleUse: $singleUse\n detail: $detail\n ) {\n token\n catalogPrefix\n capability\n singleUse\n detail\n createdAt\n }\n }\n": typeof types.CreateInviteLinkDocument, @@ -35,7 +35,7 @@ type Documents = { }; const documents: Documents = { "\n query ConnectorsGrid($filter: ConnectorsFilter, $after: String) {\n connectors(first: 100, after: $after, filter: $filter) {\n edges {\n cursor\n node {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n documentationUrl\n protocol\n }\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n": types.ConnectorsGridDocument, - "\n query SingleConnector($id: Id!) {\n connector(id: $id) {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n documentationUrl\n endpointSpecSchema\n protocol\n }\n }\n }\n": types.SingleConnectorDocument, + "\n query SingleConnector($id: Id!) {\n connector(id: $id) {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n defaultCaptureInterval\n disableBackfill\n documentationUrl\n endpointSpecSchema\n resourceSpecSchema\n protocol\n }\n }\n }\n": types.SingleConnectorDocument, "\n query DataPlanes($after: String) {\n dataPlanes(first: 100, after: $after) {\n edges {\n node {\n name\n cloudProvider\n region\n isPublic\n fqdn\n cidrBlocks\n awsIamUserArn\n gcpServiceAccountEmail\n azureApplicationClientId\n azureApplicationName\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n": types.DataPlanesDocument, "\n query InviteLinks($first: Int, $after: String) {\n inviteLinks(first: $first, after: $after) {\n edges {\n node {\n token\n ssoProviderId\n catalogPrefix\n capability\n singleUse\n detail\n createdAt\n }\n cursor\n }\n pageInfo {\n ...PageInfoFields\n }\n }\n }\n": types.InviteLinksDocument, "\n mutation CreateInviteLink(\n $catalogPrefix: Prefix!\n $capability: Capability!\n $singleUse: Boolean!\n $detail: String\n ) {\n createInviteLink(\n catalogPrefix: $catalogPrefix\n capability: $capability\n singleUse: $singleUse\n detail: $detail\n ) {\n token\n catalogPrefix\n capability\n singleUse\n detail\n createdAt\n }\n }\n": types.CreateInviteLinkDocument, @@ -75,7 +75,7 @@ export function graphql(source: "\n query ConnectorsGrid($filter: ConnectorsF /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n query SingleConnector($id: Id!) {\n connector(id: $id) {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n documentationUrl\n endpointSpecSchema\n protocol\n }\n }\n }\n"): (typeof documents)["\n query SingleConnector($id: Id!) {\n connector(id: $id) {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n documentationUrl\n endpointSpecSchema\n protocol\n }\n }\n }\n"]; +export function graphql(source: "\n query SingleConnector($id: Id!) {\n connector(id: $id) {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n defaultCaptureInterval\n disableBackfill\n documentationUrl\n endpointSpecSchema\n resourceSpecSchema\n protocol\n }\n }\n }\n"): (typeof documents)["\n query SingleConnector($id: Id!) {\n connector(id: $id) {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n defaultCaptureInterval\n disableBackfill\n documentationUrl\n endpointSpecSchema\n resourceSpecSchema\n protocol\n }\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/src/gql-types/graphql.ts b/src/gql-types/graphql.ts index 3c430e409f..a519242106 100644 --- a/src/gql-types/graphql.ts +++ b/src/gql-types/graphql.ts @@ -1506,7 +1506,7 @@ export type SingleConnectorQueryVariables = Exact<{ }>; -export type SingleConnectorQuery = { __typename?: 'QueryRoot', connector?: { __typename?: 'Connector', id: any, imageName: string, logoUrl?: string | null, title?: string | null, recommended: boolean, shortDescription?: string | null, connectorTag?: { __typename?: 'ConnectorTag', id: any, connectorId: any, imageTag: string, documentationUrl?: string | null, endpointSpecSchema?: any | null, protocol?: ConnectorProto | null } | null } | null }; +export type SingleConnectorQuery = { __typename?: 'QueryRoot', connector?: { __typename?: 'Connector', id: any, imageName: string, logoUrl?: string | null, title?: string | null, recommended: boolean, shortDescription?: string | null, connectorTag?: { __typename?: 'ConnectorTag', id: any, connectorId: any, imageTag: string, defaultCaptureInterval?: string | null, disableBackfill: boolean, documentationUrl?: string | null, endpointSpecSchema?: any | null, resourceSpecSchema?: any | null, protocol?: ConnectorProto | null } | null } | null }; export type DataPlanesQueryVariables = Exact<{ after?: InputMaybe<Scalars['String']['input']>; @@ -1630,7 +1630,7 @@ export type AuthRolesQueryQuery = { __typename?: 'QueryRoot', prefixes: { __type export const PageInfoFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PageInfoFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PageInfo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]} as unknown as DocumentNode<PageInfoFieldsFragment, unknown>; export const ConnectorsGridDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ConnectorsGrid"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ConnectorsFilter"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"connectors"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"100"}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"imageName"}},{"kind":"Field","name":{"kind":"Name","value":"logoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"recommended"}},{"kind":"Field","name":{"kind":"Name","value":"shortDescription"}},{"kind":"Field","name":{"kind":"Name","value":"connectorTag"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orDefault"},"value":{"kind":"BooleanValue","value":true}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"connectorId"}},{"kind":"Field","name":{"kind":"Name","value":"imageTag"}},{"kind":"Field","name":{"kind":"Name","value":"documentationUrl"}},{"kind":"Field","name":{"kind":"Name","value":"protocol"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]} as unknown as DocumentNode<ConnectorsGridQuery, ConnectorsGridQueryVariables>; -export const SingleConnectorDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"SingleConnector"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Id"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"connector"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"imageName"}},{"kind":"Field","name":{"kind":"Name","value":"logoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"recommended"}},{"kind":"Field","name":{"kind":"Name","value":"shortDescription"}},{"kind":"Field","name":{"kind":"Name","value":"connectorTag"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orDefault"},"value":{"kind":"BooleanValue","value":true}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"connectorId"}},{"kind":"Field","name":{"kind":"Name","value":"imageTag"}},{"kind":"Field","name":{"kind":"Name","value":"documentationUrl"}},{"kind":"Field","name":{"kind":"Name","value":"endpointSpecSchema"}},{"kind":"Field","name":{"kind":"Name","value":"protocol"}}]}}]}}]}}]} as unknown as DocumentNode<SingleConnectorQuery, SingleConnectorQueryVariables>; +export const SingleConnectorDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"SingleConnector"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Id"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"connector"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"imageName"}},{"kind":"Field","name":{"kind":"Name","value":"logoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"recommended"}},{"kind":"Field","name":{"kind":"Name","value":"shortDescription"}},{"kind":"Field","name":{"kind":"Name","value":"connectorTag"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orDefault"},"value":{"kind":"BooleanValue","value":true}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"connectorId"}},{"kind":"Field","name":{"kind":"Name","value":"imageTag"}},{"kind":"Field","name":{"kind":"Name","value":"defaultCaptureInterval"}},{"kind":"Field","name":{"kind":"Name","value":"disableBackfill"}},{"kind":"Field","name":{"kind":"Name","value":"documentationUrl"}},{"kind":"Field","name":{"kind":"Name","value":"endpointSpecSchema"}},{"kind":"Field","name":{"kind":"Name","value":"resourceSpecSchema"}},{"kind":"Field","name":{"kind":"Name","value":"protocol"}}]}}]}}]}}]} as unknown as DocumentNode<SingleConnectorQuery, SingleConnectorQueryVariables>; export const DataPlanesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"DataPlanes"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"dataPlanes"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"100"}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"cloudProvider"}},{"kind":"Field","name":{"kind":"Name","value":"region"}},{"kind":"Field","name":{"kind":"Name","value":"isPublic"}},{"kind":"Field","name":{"kind":"Name","value":"fqdn"}},{"kind":"Field","name":{"kind":"Name","value":"cidrBlocks"}},{"kind":"Field","name":{"kind":"Name","value":"awsIamUserArn"}},{"kind":"Field","name":{"kind":"Name","value":"gcpServiceAccountEmail"}},{"kind":"Field","name":{"kind":"Name","value":"azureApplicationClientId"}},{"kind":"Field","name":{"kind":"Name","value":"azureApplicationName"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]} as unknown as DocumentNode<DataPlanesQuery, DataPlanesQueryVariables>; export const InviteLinksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"InviteLinks"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"inviteLinks"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"ssoProviderId"}},{"kind":"Field","name":{"kind":"Name","value":"catalogPrefix"}},{"kind":"Field","name":{"kind":"Name","value":"capability"}},{"kind":"Field","name":{"kind":"Name","value":"singleUse"}},{"kind":"Field","name":{"kind":"Name","value":"detail"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PageInfoFields"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PageInfoFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PageInfo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]} as unknown as DocumentNode<InviteLinksQuery, InviteLinksQueryVariables>; export const CreateInviteLinkDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateInviteLink"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"catalogPrefix"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Prefix"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"capability"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Capability"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"singleUse"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"detail"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createInviteLink"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"catalogPrefix"},"value":{"kind":"Variable","name":{"kind":"Name","value":"catalogPrefix"}}},{"kind":"Argument","name":{"kind":"Name","value":"capability"},"value":{"kind":"Variable","name":{"kind":"Name","value":"capability"}}},{"kind":"Argument","name":{"kind":"Name","value":"singleUse"},"value":{"kind":"Variable","name":{"kind":"Name","value":"singleUse"}}},{"kind":"Argument","name":{"kind":"Name","value":"detail"},"value":{"kind":"Variable","name":{"kind":"Name","value":"detail"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"catalogPrefix"}},{"kind":"Field","name":{"kind":"Name","value":"capability"}},{"kind":"Field","name":{"kind":"Name","value":"singleUse"}},{"kind":"Field","name":{"kind":"Name","value":"detail"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]} as unknown as DocumentNode<CreateInviteLinkMutation, CreateInviteLinkMutationVariables>; diff --git a/src/stores/DetailsForm/useDetailsFormHydrator.ts b/src/stores/DetailsForm/useDetailsFormHydrator.ts index f17bfbc5c0..b6dbd50b6a 100644 --- a/src/stores/DetailsForm/useDetailsFormHydrator.ts +++ b/src/stores/DetailsForm/useDetailsFormHydrator.ts @@ -1,10 +1,9 @@ -import type { ConnectorWithTagQuery } from 'src/api/types'; import type { Details } from 'src/stores/DetailsForm/types'; -import type { ConnectorVersionEvaluationOptions } from 'src/utils/connector-utils'; import { useCallback } from 'react'; import { getLiveSpecs_detailsForm } from 'src/api/liveSpecsExt'; +import { useConnectorTag } from 'src/context/ConnectorTag'; import { useEntityWorkflow } from 'src/context/Workflow'; import { useEvaluateDataPlaneOptions } from 'src/hooks/dataPlanes/useEvaluateDataPlaneOptions'; import useGetDataPlane from 'src/hooks/dataPlanes/useGetDataPlane'; @@ -13,20 +12,33 @@ import useGlobalSearchParams, { } from 'src/hooks/searchParams/useGlobalSearchParams'; import { initialDetails } from 'src/stores/DetailsForm/shared'; import { useDetailsFormStore } from 'src/stores/DetailsForm/Store'; -import { getConnectorMetadata } from 'src/utils/connector-utils'; -const getConnectorImage = async ( - connectorId: string, - connectorMetadata: ConnectorWithTagQuery, - existingImageTag?: ConnectorVersionEvaluationOptions['existingImageTag'] -): Promise<Details['data']['connectorImage'] | null> => { - const options: ConnectorVersionEvaluationOptions | undefined = - existingImageTag ? { connectorId, existingImageTag } : undefined; - - return getConnectorMetadata(connectorMetadata, options); +const DEKAF_IMAGE_PREFIX = 'ghcr.io/estuary/dekaf-'; + +const buildConnectorImage = ( + connectorTag: ReturnType<typeof useConnectorTag> +): Details['data']['connectorImage'] => { + const { id, connectorId, imageTag, connector } = connectorTag; + + const base = { + connectorId, + iconPath: connector.logoUrl ?? '', + id, + imageName: connector.imageName, + imageTag, + }; + + return connector.imageName.startsWith(DEKAF_IMAGE_PREFIX) + ? { + ...base, + variant: connector.imageName.substring(DEKAF_IMAGE_PREFIX.length), + } + : { ...base, imagePath: `${connector.imageName}${imageTag}` }; }; export const useDetailsFormHydrator = () => { + const connectorTag = useConnectorTag(); + const dataPlaneId = useGlobalSearchParams(GlobalSearchParams.DATA_PLANE_ID); const liveSpecId = useGlobalSearchParams(GlobalSearchParams.LIVE_SPEC_ID); @@ -44,19 +56,12 @@ export const useDetailsFormHydrator = () => { const setPreviousDetails = useDetailsFormStore( (state) => state.setPreviousDetails ); - const setUnsupportedConnectorVersion = useDetailsFormStore( - (state) => state.setUnsupportedConnectorVersion - ); const evaluateDataPlaneOptions = useEvaluateDataPlaneOptions(); const getDataPlane = useGetDataPlane(); const hydrateDetailsForm = useCallback( - async ( - connectorId: string, - connectorMetadata: ConnectorWithTagQuery, - baseEntityName?: string - ) => { + async (baseEntityName?: string) => { setActive(true); const createWorkflow = @@ -64,21 +69,12 @@ export const useDetailsFormHydrator = () => { workflow === 'materialization_create'; if (createWorkflow) { - const connectorImage = await getConnectorImage( - connectorId, - connectorMetadata - ); + const connectorImage = buildConnectorImage(connectorTag); const dataPlaneOptions = await evaluateDataPlaneOptions(baseEntityName); const dataPlane = getDataPlane(dataPlaneOptions, dataPlaneId); - if (!connectorImage) { - setHydrationErrorsExist(true); - - return Promise.reject({ connectorTagId: null }); - } - setDetails_connector(connectorImage); const { data } = initialDetails; @@ -96,9 +92,7 @@ export const useDetailsFormHydrator = () => { setHydrated(true); - return Promise.resolve({ - connectorTagId: hydratedDetails.data.connectorImage.id, - }); + return Promise.resolve(); } if (liveSpecId) { @@ -108,23 +102,17 @@ export const useDetailsFormHydrator = () => { if (error || !data || data.length === 0) { setHydrationErrorsExist(true); - return Promise.reject({ connectorTagId: null }); + return Promise.reject(); } const { catalog_name, - connector_image_tag, - connector_tag_id, data_plane_id, data_plane_name, reactor_address, } = data[0]; - const connectorImage = await getConnectorImage( - connectorId, - connectorMetadata, - connector_image_tag - ); + const connectorImage = buildConnectorImage(connectorTag); const dataPlaneOptions = await evaluateDataPlaneOptions( catalog_name, @@ -136,12 +124,6 @@ export const useDetailsFormHydrator = () => { ); const dataPlane = getDataPlane(dataPlaneOptions, data_plane_id); - if (!connectorImage) { - setHydrationErrorsExist(true); - - return Promise.reject({ connectorTagId: null }); - } - const hydratedDetails: Details = { data: { entityName: catalog_name, @@ -150,41 +132,40 @@ export const useDetailsFormHydrator = () => { }, }; - setUnsupportedConnectorVersion( - connectorImage.id, - connector_tag_id - ); + // TODO (gql:connector) - connector tag UUID not available via GQL; + // setUnsupportedConnectorVersion cannot be called accurately. + // Previously compared connectorImage.id against connector_tag_id + // from live_specs. setDetails(hydratedDetails); setPreviousDetails(hydratedDetails); setHydrated(true); - return Promise.resolve({ - connectorTagId: hydratedDetails.data.connectorImage.id, - }); + return Promise.resolve(); } if (workflow === 'test_json_forms') { setDetails_connector({ - id: connectorId, + connectorId: '', iconPath: '', + id: '', imageName: '', imagePath: '', imageTag: '', - connectorId, }); setHydrationErrorsExist(true); setHydrated(true); - return Promise.resolve({ connectorTagId: connectorId }); + return Promise.resolve(); } - return Promise.resolve({ connectorTagId: null }); + return Promise.resolve(); }, [ + connectorTag, dataPlaneId, evaluateDataPlaneOptions, getDataPlane, @@ -195,7 +176,6 @@ export const useDetailsFormHydrator = () => { setHydrated, setHydrationErrorsExist, setPreviousDetails, - setUnsupportedConnectorVersion, workflow, ] ); diff --git a/src/stores/EndpointConfig/useEndpointConfigHydrator.ts b/src/stores/EndpointConfig/useEndpointConfigHydrator.ts index 0826d6a2bb..25d3c548a4 100644 --- a/src/stores/EndpointConfig/useEndpointConfigHydrator.ts +++ b/src/stores/EndpointConfig/useEndpointConfigHydrator.ts @@ -1,11 +1,11 @@ import type { PostgrestError } from '@supabase/postgrest-js'; -import type { ConnectorTag } from 'src/api/types'; import type { Schema } from 'src/types'; import { useCallback } from 'react'; import { getDraftSpecsByDraftId } from 'src/api/draftSpecs'; import { getLiveSpecsByLiveSpecId } from 'src/api/hydration'; +import { useConnectorTag } from 'src/context/ConnectorTag'; import { useEntityType } from 'src/context/EntityContext'; import { useEntityWorkflow } from 'src/context/Workflow'; import useGlobalSearchParams, { @@ -26,28 +26,19 @@ const useStoreEndpointSchema = () => { (state) => state.setEndpointCanBeEmpty ); - const storeEndpointSchema = async ( - connectorTagId: string, - connectorTags: ConnectorTag[] - ) => { - const endpointSchema = connectorTags.find( - (tag) => tag.id === connectorTagId - )?.endpoint_spec_schema; - - if (!endpointSchema) { + const storeEndpointSchema = async (endpointSpecSchema: any) => { + if (!endpointSpecSchema) { return { endpointSchema: null, error: { ...BASE_ERROR, message: 'endpoint schema not found' }, }; } - await setEndpointSchema(endpointSchema); + await setEndpointSchema(endpointSpecSchema); - // Storing if this endpointConfig can be empty or not - // If so we know there will never be a "change" to the endpoint config - setEndpointCanBeEmpty(configCanBeEmpty(endpointSchema)); + setEndpointCanBeEmpty(configCanBeEmpty(endpointSpecSchema)); - return { endpointSchema, error: null }; + return { endpointSchema: endpointSpecSchema, error: null }; }; return { storeEndpointSchema }; @@ -117,6 +108,8 @@ const useHydrateEndpointConfigDependentState = () => { }; export const useEndpointConfigHydrator = () => { + const connectorTag = useConnectorTag(); + const draftId = useGlobalSearchParams(GlobalSearchParams.DRAFT_ID); const liveSpecId = useGlobalSearchParams(GlobalSearchParams.LIVE_SPEC_ID); @@ -135,73 +128,60 @@ export const useEndpointConfigHydrator = () => { const { hydrateEndpointConfigDependentState } = useHydrateEndpointConfigDependentState(); - const hydrateEndpointConfig = useCallback( - async ( - connectorTagId: string | null, - connectorTags: ConnectorTag[] - ) => { - setActive(true); + const hydrateEndpointConfig = useCallback(async () => { + setActive(true); + + if ( + workflow === 'capture_create' || + workflow === 'materialization_create' + ) { + logRocketEvent('EndpointConfig', { + hydrationDefault: true, + }); + setServerUpdateRequired(true); + } - if (!connectorTagId || connectorTagId.length === 0) { - // TODO: Add a Log Rocket event. - setHydrated(true); - setHydrationErrorsExist(true); + const { endpointSchema, error: endpointSchemaError } = + await storeEndpointSchema(connectorTag.endpointSpecSchema); - return Promise.reject(); - } + if (endpointSchemaError || !endpointSchema) { + setHydrated(true); + setHydrationErrorsExist(true); - if ( - workflow === 'capture_create' || - workflow === 'materialization_create' - ) { - logRocketEvent('EndpointConfig', { - hydrationDefault: true, - }); - setServerUpdateRequired(true); - } + return Promise.reject(); + } - const { endpointSchema, error: endpointSchemaError } = - await storeEndpointSchema(connectorTagId, connectorTags); + if (liveSpecId) { + const { error: endpointConfigError } = + await hydrateEndpointConfigDependentState( + draftId, + liveSpecId, + endpointSchema + ); - if (endpointSchemaError || !endpointSchema) { + if (endpointConfigError) { setHydrated(true); setHydrationErrorsExist(true); return Promise.reject(); } + } - if (liveSpecId) { - const { error: endpointConfigError } = - await hydrateEndpointConfigDependentState( - draftId, - liveSpecId, - endpointSchema - ); - - if (endpointConfigError) { - setHydrated(true); - setHydrationErrorsExist(true); - - return Promise.reject(); - } - } - - setHydrated(true); - - return Promise.resolve(); - }, - [ - draftId, - hydrateEndpointConfigDependentState, - liveSpecId, - setActive, - setHydrated, - setHydrationErrorsExist, - setServerUpdateRequired, - storeEndpointSchema, - workflow, - ] - ); + setHydrated(true); + + return Promise.resolve(); + }, [ + connectorTag, + draftId, + hydrateEndpointConfigDependentState, + liveSpecId, + setActive, + setHydrated, + setHydrationErrorsExist, + setServerUpdateRequired, + storeEndpointSchema, + workflow, + ]); return { hydrateEndpointConfig }; }; diff --git a/src/stores/Workflow/Hydrator.tsx b/src/stores/Workflow/Hydrator.tsx index d71f2e7216..48fc6e73f4 100644 --- a/src/stores/Workflow/Hydrator.tsx +++ b/src/stores/Workflow/Hydrator.tsx @@ -3,6 +3,7 @@ import type { WorkflowInitializerProps } from 'src/components/shared/Entity/type import { useEffectOnce } from 'react-use'; import Error from 'src/components/shared/Error'; +import { ConnectorTagProvider } from 'src/context/ConnectorTag'; import { BASE_ERROR } from 'src/services/supabase'; import BindingHydrator from 'src/stores/Binding/Hydrator'; import { useWorkflowStore } from 'src/stores/Workflow/Store'; @@ -10,7 +11,7 @@ import { useWorkflowHydrator } from 'src/stores/Workflow/useWorkflowHydrator'; // This hydrator is here without a store so that we can start working on moving a lot of // these separate stores into a single "Workflow" store for Create and Edit. -function WorkflowHydrator({ +function WorkflowHydratorInner({ children, expressWorkflow, }: WorkflowInitializerProps) { @@ -48,4 +49,17 @@ function WorkflowHydrator({ return <BindingHydrator>{children}</BindingHydrator>; } +function WorkflowHydrator({ + children, + expressWorkflow, +}: WorkflowInitializerProps) { + return ( + <ConnectorTagProvider> + <WorkflowHydratorInner expressWorkflow={expressWorkflow}> + {children} + </WorkflowHydratorInner> + </ConnectorTagProvider> + ); +} + export default WorkflowHydrator; diff --git a/src/stores/Workflow/Store.ts b/src/stores/Workflow/Store.ts index 89d899397a..a1277c0971 100644 --- a/src/stores/Workflow/Store.ts +++ b/src/stores/Workflow/Store.ts @@ -28,7 +28,7 @@ const getInitialStateData = (): Pick< | 'storageMappingPrefix' > => ({ catalogName: { root: '', suffix: '', tenant: '', whole: '' }, - connectorMetadata: [], + connectorMetadata: null, customerId: '', redirectUrl: '', storageMappingPrefix: '', diff --git a/src/stores/Workflow/hooks.ts b/src/stores/Workflow/hooks.ts index a457b306cb..b715f373a8 100644 --- a/src/stores/Workflow/hooks.ts +++ b/src/stores/Workflow/hooks.ts @@ -1,47 +1,37 @@ -import type { ConnectorTag, ConnectorWithTag } from 'src/api/types'; - -import { useShallow } from 'zustand/react/shallow'; - import { useWorkflowStore } from 'src/stores/Workflow/Store'; -import { hasLength } from 'src/utils/misc-utils'; -export const useWorkflowStore_connectorMetadataProperty = < - K extends keyof ConnectorWithTag, ->( - connectorId: string | null | undefined, - property: K -): ConnectorWithTag[K] | undefined => { +// Returns the title of the connector stored in workflow metadata +export const useWorkflowStore_connectorTitle = (): string | undefined => { return useWorkflowStore( - useShallow((state) => { - if (!connectorId || !hasLength(state.connectorMetadata)) { - return undefined; - } - - return state.connectorMetadata.find( - (connector) => connector.id === connectorId - )?.[property]; - }) + (state) => state.connectorMetadata?.connector.title ?? undefined ); }; -export const useWorkflowStore_connectorTagProperty = < - K extends keyof ConnectorTag, ->( - connectorId: string | null | undefined, - connectorTagId: string | null | undefined, - property: K -): ConnectorTag[K] | undefined => { +// Returns the documentation URL of the connector tag stored in workflow metadata +export const useWorkflowStore_connectorTagDocumentationUrl = + (): string | undefined => { + return useWorkflowStore( + (state) => state.connectorMetadata?.documentationUrl ?? undefined + ); + }; + +// Legacy hook names preserved for backward compatibility +// TODO (gql:connector) - update callers to use the new named hooks above +export const useWorkflowStore_connectorMetadataProperty = ( + _connectorId: string | null | undefined, + _property: string +): string | undefined => { return useWorkflowStore( - useShallow((state) => { - if (!connectorId || !hasLength(state.connectorMetadata)) { - return undefined; - } + (state) => state.connectorMetadata?.connector.title ?? undefined + ); +}; - return state.connectorMetadata - .find((connector) => connector.id === connectorId) - ?.connector_tags.find((tag) => tag.id === connectorTagId)?.[ - property - ]; - }) +export const useWorkflowStore_connectorTagProperty = ( + _connectorId: string | null | undefined, + _connectorTagId: string | null | undefined, + _property: string +): string | undefined => { + return useWorkflowStore( + (state) => state.connectorMetadata?.documentationUrl ?? undefined ); }; diff --git a/src/stores/Workflow/types.ts b/src/stores/Workflow/types.ts index 6f40c6ce74..fba3c5f9cb 100644 --- a/src/stores/Workflow/types.ts +++ b/src/stores/Workflow/types.ts @@ -1,4 +1,4 @@ -import type { ConnectorWithTagQuery } from 'src/api/types'; +import type { ConnectorTagData } from 'src/context/ConnectorTag'; import type { StoreWithHydration } from 'src/stores/extensions/Hydration'; import type { StoreWithCollections } from 'src/stores/Workflow/slices/Collections'; import type { StoreWithProjections } from 'src/stores/Workflow/slices/Projections'; @@ -15,7 +15,7 @@ export interface WorkflowState StoreWithProjections, StoreWithCollections { catalogName: CatalogName; - connectorMetadata: ConnectorWithTagQuery[]; + connectorMetadata: ConnectorTagData | null; customerId: string; redirectUrl: string; resetState: () => void; diff --git a/src/stores/Workflow/useWorkflowHydrator.ts b/src/stores/Workflow/useWorkflowHydrator.ts index 22447ddba2..d2c250e331 100644 --- a/src/stores/Workflow/useWorkflowHydrator.ts +++ b/src/stores/Workflow/useWorkflowHydrator.ts @@ -1,23 +1,17 @@ import { useCallback } from 'react'; -import { useGetSingleConnector } from 'src/api/gql/connectors'; -import useGlobalSearchParams, { - GlobalSearchParams, -} from 'src/hooks/searchParams/useGlobalSearchParams'; -import { logRocketConsole, logRocketEvent } from 'src/services/shared'; -import { CustomEvents } from 'src/services/types'; +import { useConnectorTag } from 'src/context/ConnectorTag'; +import { logRocketConsole } from 'src/services/shared'; import { useDetailsFormHydrator } from 'src/stores/DetailsForm/useDetailsFormHydrator'; import { useEndpointConfigHydrator } from 'src/stores/EndpointConfig/useEndpointConfigHydrator'; import { useEntitiesStore } from 'src/stores/Entities/Store'; import { useWorkflowStore } from 'src/stores/Workflow/Store'; -import { hasLength } from 'src/utils/misc-utils'; export const useWorkflowHydrator = (expressWorkflow: boolean | undefined) => { - const connectorId = useGlobalSearchParams(GlobalSearchParams.CONNECTOR_ID); + const connectorTag = useConnectorTag(); const { hydrateDetailsForm } = useDetailsFormHydrator(); const { hydrateEndpointConfig } = useEndpointConfigHydrator(); - const getSingleConnector = useGetSingleConnector(); const baseCatalogPrefix = useEntitiesStore((state) => { const storageMappingPrefixes = Object.keys(state.storageMappings); @@ -40,39 +34,15 @@ export const useWorkflowHydrator = (expressWorkflow: boolean | undefined) => { ); const hydrateWorkflow = useCallback(async () => { - const { data: connectorMetadata, error: connectorError } = - await getSingleConnector(connectorId); - - if ( - !hasLength(connectorId) || - connectorError || - !connectorMetadata || - connectorMetadata.length === 0 - ) { - logRocketEvent(CustomEvents.CONNECTOR_VERSION_MISSING); - - return Promise.reject( - connectorError ?? 'Connector information not found' - ); - } - - setConnectorMetadata(connectorMetadata); + setConnectorMetadata(connectorTag); const baseEntityName = expressWorkflow ? catalogName : baseCatalogPrefix; try { - const { connectorTagId } = await hydrateDetailsForm( - connectorId, - connectorMetadata[0], - baseEntityName - ); - - await hydrateEndpointConfig( - connectorTagId, - connectorMetadata[0].connector_tags - ); + await hydrateDetailsForm(baseEntityName); + await hydrateEndpointConfig(); } catch (error: unknown) { return Promise.reject(error); } @@ -81,9 +51,8 @@ export const useWorkflowHydrator = (expressWorkflow: boolean | undefined) => { }, [ baseCatalogPrefix, catalogName, - connectorId, + connectorTag, expressWorkflow, - getSingleConnector, hydrateDetailsForm, hydrateEndpointConfig, setConnectorMetadata, From f2de20c4e9b0fde4dded4c8db6bb7732422a13a0 Mon Sep 17 00:00:00 2001 From: Travis Jenkins <travis@estuary.dev> Date: Tue, 31 Mar 2026 16:17:58 -0400 Subject: [PATCH 03/30] Formatting to GQL format of UUIDs --- .../shared/Entity/Edit/useInitializeTaskDraft.ts | 3 ++- .../shared/Entity/hooks/useEntityCreateNavigate.ts | 4 +++- src/utils/connector-utils.ts | 10 ++++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/components/shared/Entity/Edit/useInitializeTaskDraft.ts b/src/components/shared/Entity/Edit/useInitializeTaskDraft.ts index 6e7888dcab..1824937456 100644 --- a/src/components/shared/Entity/Edit/useInitializeTaskDraft.ts +++ b/src/components/shared/Entity/Edit/useInitializeTaskDraft.ts @@ -28,6 +28,7 @@ import { logRocketEvent } from 'src/services/shared'; import { CustomEvents } from 'src/services/types'; import { useFormStateStore_setFormState } from 'src/stores/FormState/hooks'; import { FormStatus } from 'src/stores/FormState/types'; +import { formatOldUuidToGql } from 'src/utils/connector-utils'; import { isTaskDisabled } from 'src/utils/entity-utils'; interface SupabaseConfig { @@ -254,7 +255,7 @@ function useInitializeTaskDraft() { { [GlobalSearchParams.LIVE_SPEC_ID]: liveSpecId, [GlobalSearchParams.CONNECTOR_ID]: - task.connector_id, + formatOldUuidToGql(task.connector_id), [GlobalSearchParams.LAST_PUB_ID]: task.last_pub_id, }, diff --git a/src/components/shared/Entity/hooks/useEntityCreateNavigate.ts b/src/components/shared/Entity/hooks/useEntityCreateNavigate.ts index 88e05f5e4f..53599430bb 100644 --- a/src/components/shared/Entity/hooks/useEntityCreateNavigate.ts +++ b/src/components/shared/Entity/hooks/useEntityCreateNavigate.ts @@ -8,6 +8,7 @@ import { useNavigate } from 'react-router'; import { GlobalSearchParams } from 'src/hooks/searchParams/useGlobalSearchParams'; import useSearchParamAppend from 'src/hooks/searchParams/useSearchParamAppend'; import { ENTITY_SETTINGS } from 'src/settings/entity'; +import { formatOldUuidToGql } from 'src/utils/connector-utils'; import { getPathWithParams, hasLength } from 'src/utils/misc-utils'; export interface HookEntityCreateNavigateProps { @@ -34,7 +35,8 @@ export default function useEntityCreateNavigate() { const searchParamConfig: { [param: string]: any } = {}; if (hasLength(id)) { - searchParamConfig[GlobalSearchParams.CONNECTOR_ID] = id; + searchParamConfig[GlobalSearchParams.CONNECTOR_ID] = + formatOldUuidToGql(id); } if (typeof dataPlaneId !== 'undefined') { diff --git a/src/utils/connector-utils.ts b/src/utils/connector-utils.ts index cb2638fa64..cf43173f8d 100644 --- a/src/utils/connector-utils.ts +++ b/src/utils/connector-utils.ts @@ -118,3 +118,13 @@ export const requiredConnectorColumnsExist = <Response>( null ); }; + +// TODO (GQL:live specs) - once we get live specs fetched with GQL we don't need to worry about this +export function formatOldUuidToGql(id: string): string; +export function formatOldUuidToGql(id: null | undefined): null | undefined; +export function formatOldUuidToGql( + id: string | null | undefined +): string | null | undefined; +export function formatOldUuidToGql(id: string | null | undefined) { + return typeof id === 'string' ? id.replaceAll(':', '') : id; +} From d10afa272d7441e55073b714f4d417d4325a7a63 Mon Sep 17 00:00:00 2001 From: Travis Jenkins <travis@estuary.dev> Date: Wed, 1 Apr 2026 16:13:54 -0400 Subject: [PATCH 04/30] Starting to clean up when we allowed connector to be changed --- .../Entity/DetailsForm/useConnectorField.ts | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/src/components/shared/Entity/DetailsForm/useConnectorField.ts b/src/components/shared/Entity/DetailsForm/useConnectorField.ts index 882d60ec05..9818c5a67e 100644 --- a/src/components/shared/Entity/DetailsForm/useConnectorField.ts +++ b/src/components/shared/Entity/DetailsForm/useConnectorField.ts @@ -36,18 +36,24 @@ export default function useConnectorField( return []; } - const { id, connectorId: tagConnectorId, imageTag, connector } = - connectorTag; + const { + id, + connectorId: connectorTagId, + imageTag, + connector, + } = connectorTag; const base = { - connectorId: tagConnectorId, + connectorId: connectorTagId, iconPath: connector.logoUrl ?? '', id, imageName: connector.imageName, imageTag, }; - const connectorImage = connector.imageName.startsWith(DEKAF_IMAGE_PREFIX) + const connectorImage = connector.imageName.startsWith( + DEKAF_IMAGE_PREFIX + ) ? { ...base, variant: connector.imageName.substring( @@ -56,7 +62,12 @@ export default function useConnectorField( } : { ...base, imagePath: `${connector.imageName}${imageTag}` }; - return [{ const: connectorImage, title: connector.title ?? connector.imageName }]; + return [ + { + const: connectorImage, + title: connector.title ?? connector.imageName, + }, + ]; }, [connectorTag]); const connectorSchema = useMemo( @@ -83,12 +94,19 @@ export default function useConnectorField( const setConnector = useCallback( (details: Details, selectedDataPlaneId: string | undefined) => { + console.log('setConnector >>>>>', { + connectorId, + details, + selectedDataPlaneId, + }); + if (!hasLength(connectorId)) { return; } setEntityNameChanged(details.data.entityName); + // TODO (GQL:connectors) - this is breaking edit navigateToCreate(entityType, { id: connectorId, advanceToForm: true, From 6b957cf16ba59981260d601976678d3e64ccf912 Mon Sep 17 00:00:00 2001 From: Travis Jenkins <travis@estuary.dev> Date: Wed, 1 Apr 2026 16:19:32 -0400 Subject: [PATCH 05/30] We do not need to enable setting this as we force the option to be selected and the user is not allowed to change it later --- .../Entity/DetailsForm/useConnectorField.ts | 43 +------------------ .../Entity/DetailsForm/useFormFields.ts | 3 +- 2 files changed, 3 insertions(+), 43 deletions(-) diff --git a/src/components/shared/Entity/DetailsForm/useConnectorField.ts b/src/components/shared/Entity/DetailsForm/useConnectorField.ts index 9818c5a67e..c25a718c80 100644 --- a/src/components/shared/Entity/DetailsForm/useConnectorField.ts +++ b/src/components/shared/Entity/DetailsForm/useConnectorField.ts @@ -1,34 +1,19 @@ -import type { Details } from 'src/stores/DetailsForm/types'; import type { EntityWithCreateWorkflow } from 'src/types'; -import { useCallback, useMemo } from 'react'; +import { useMemo } from 'react'; import { useIntl } from 'react-intl'; -import useEntityCreateNavigate from 'src/components/shared/Entity/hooks/useEntityCreateNavigate'; import { CONNECTOR_IMAGE_SCOPE } from 'src/forms/renderers/Connectors'; -import useGlobalSearchParams, { - GlobalSearchParams, -} from 'src/hooks/searchParams/useGlobalSearchParams'; -import { useDetailsFormStore } from 'src/stores/DetailsForm/Store'; import { useWorkflowStore } from 'src/stores/Workflow/Store'; -import { hasLength } from 'src/utils/misc-utils'; const DEKAF_IMAGE_PREFIX = 'ghcr.io/estuary/dekaf-'; export default function useConnectorField( entityType: EntityWithCreateWorkflow ) { - const connectorId = useGlobalSearchParams(GlobalSearchParams.CONNECTOR_ID); - const intl = useIntl(); - const navigateToCreate = useEntityCreateNavigate(); - - const setEntityNameChanged = useDetailsFormStore( - (state) => state.setEntityNameChanged - ); - const connectorTag = useWorkflowStore((state) => state.connectorMetadata); const connectorsOneOf = useMemo(() => { @@ -92,29 +77,5 @@ export default function useConnectorField( }, }; - const setConnector = useCallback( - (details: Details, selectedDataPlaneId: string | undefined) => { - console.log('setConnector >>>>>', { - connectorId, - details, - selectedDataPlaneId, - }); - - if (!hasLength(connectorId)) { - return; - } - - setEntityNameChanged(details.data.entityName); - - // TODO (GQL:connectors) - this is breaking edit - navigateToCreate(entityType, { - id: connectorId, - advanceToForm: true, - dataPlaneId: selectedDataPlaneId ?? null, - }); - }, - [connectorId, entityType, navigateToCreate, setEntityNameChanged] - ); - - return { connectorSchema, connectorUISchema, setConnector }; + return { connectorSchema, connectorUISchema }; } diff --git a/src/components/shared/Entity/DetailsForm/useFormFields.ts b/src/components/shared/Entity/DetailsForm/useFormFields.ts index b730c04092..451f17a503 100644 --- a/src/components/shared/Entity/DetailsForm/useFormFields.ts +++ b/src/components/shared/Entity/DetailsForm/useFormFields.ts @@ -31,7 +31,7 @@ export default function useFormFields(entityType: EntityWithCreateWorkflow) { const { evaluateStorageMapping } = useEvaluateStorageMapping(); - const { connectorSchema, connectorUISchema, setConnector } = + const { connectorSchema, connectorUISchema } = useConnectorField(entityType); const { dataPlaneSchema, dataPlaneUISchema, setDataPlane } = @@ -87,7 +87,6 @@ export default function useFormFields(entityType: EntityWithCreateWorkflow) { setDetails(details); setDataPlane(details, dataPlaneOption); - setConnector(details, dataPlaneOption?.id); setCatalogName({ root: details.data.entityName.substring(tenant.length), From c86d33a3154fc5362b1cd4860f117f87e3315f52 Mon Sep 17 00:00:00 2001 From: Travis Jenkins <travis@estuary.dev> Date: Wed, 1 Apr 2026 16:23:21 -0400 Subject: [PATCH 06/30] formatting stuff --- src/components/shared/Entity/DetailsForm/useFormFields.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/components/shared/Entity/DetailsForm/useFormFields.ts b/src/components/shared/Entity/DetailsForm/useFormFields.ts index 451f17a503..dec51be965 100644 --- a/src/components/shared/Entity/DetailsForm/useFormFields.ts +++ b/src/components/shared/Entity/DetailsForm/useFormFields.ts @@ -81,13 +81,10 @@ export default function useFormFields(entityType: EntityWithCreateWorkflow) { details.data.dataPlane ); - // The field-specific functions below, `setDataPlane` and `setConnector`, - // set details form state that can be overridden by `setDetails`. Consequently, - // `setDetails` should always be called first. + // Some field specific functions `setDataPlane` and `setCatalogName` + // are meant to be called _after_ the more general `setDetails`. setDetails(details); - setDataPlane(details, dataPlaneOption); - setCatalogName({ root: details.data.entityName.substring(tenant.length), tenant, From e63dc7070174b21563db9704404d654d99d79035 Mon Sep 17 00:00:00 2001 From: Travis Jenkins <travis@estuary.dev> Date: Wed, 1 Apr 2026 16:30:47 -0400 Subject: [PATCH 07/30] Wiring back up the check for unsupported connectors --- src/stores/DetailsForm/useDetailsFormHydrator.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/stores/DetailsForm/useDetailsFormHydrator.ts b/src/stores/DetailsForm/useDetailsFormHydrator.ts index b6dbd50b6a..fc7b50fd93 100644 --- a/src/stores/DetailsForm/useDetailsFormHydrator.ts +++ b/src/stores/DetailsForm/useDetailsFormHydrator.ts @@ -56,6 +56,9 @@ export const useDetailsFormHydrator = () => { const setPreviousDetails = useDetailsFormStore( (state) => state.setPreviousDetails ); + const setUnsupportedConnectorVersion = useDetailsFormStore( + (state) => state.setUnsupportedConnectorVersion + ); const evaluateDataPlaneOptions = useEvaluateDataPlaneOptions(); const getDataPlane = useGetDataPlane(); @@ -107,6 +110,7 @@ export const useDetailsFormHydrator = () => { const { catalog_name, + connector_tag_id, data_plane_id, data_plane_name, reactor_address, @@ -132,10 +136,10 @@ export const useDetailsFormHydrator = () => { }, }; - // TODO (gql:connector) - connector tag UUID not available via GQL; - // setUnsupportedConnectorVersion cannot be called accurately. - // Previously compared connectorImage.id against connector_tag_id - // from live_specs. + setUnsupportedConnectorVersion( + connectorImage.id, + connector_tag_id + ); setDetails(hydratedDetails); setPreviousDetails(hydratedDetails); @@ -176,6 +180,7 @@ export const useDetailsFormHydrator = () => { setHydrated, setHydrationErrorsExist, setPreviousDetails, + setUnsupportedConnectorVersion, workflow, ] ); From 4550725afaa9193e1670948248d493fee0c5a60d Mon Sep 17 00:00:00 2001 From: Travis Jenkins <travis@estuary.dev> Date: Mon, 6 Apr 2026 13:44:43 -0400 Subject: [PATCH 08/30] Cleaning up old code/approach Handling errors when fetching Cleaning up the unsupported stuff --- src/api/gql/connectors.ts | 68 +------------------ src/context/ConnectorTag.tsx | 10 ++- src/lang/en-US/Workflows.ts | 2 + src/services/types.ts | 1 - src/stores/DetailsForm/Store.ts | 27 -------- src/stores/DetailsForm/types.ts | 6 -- .../DetailsForm/useDetailsFormHydrator.ts | 10 --- 7 files changed, 12 insertions(+), 112 deletions(-) diff --git a/src/api/gql/connectors.ts b/src/api/gql/connectors.ts index adfb9c5fd9..3a379f9d8b 100644 --- a/src/api/gql/connectors.ts +++ b/src/api/gql/connectors.ts @@ -1,13 +1,4 @@ -import type { ConnectorTag, ConnectorWithTagQuery } from 'src/api/types'; -import type { - SingleConnectorQuery, - ConnectorsGridQuery, -} from 'src/gql-types/graphql'; -import type { EntityWithCreateWorkflow } from 'src/types'; - -import { useCallback } from 'react'; - -import { useClient } from 'urql'; +import type { ConnectorsGridQuery } from 'src/gql-types/graphql'; import { graphql } from 'src/gql-types'; @@ -66,60 +57,3 @@ export const CONNECTOR_BY_ID_QUERY = graphql(` } } `); - -// Maps a GQL SingleConnector result to the legacy ConnectorWithTagQuery shape -// used by workflow hydration and downstream stores. -export const toConnectorWithTagQuery = ( - node: NonNullable<SingleConnectorQuery['connector']> -): ConnectorWithTagQuery | null => { - const { connectorTag } = node; - if (!connectorTag) return null; - - const tag: ConnectorTag = { - id: connectorTag.id, - connector_id: connectorTag.connectorId, - image_tag: connectorTag.imageTag, - documentation_url: connectorTag.documentationUrl ?? '', - endpoint_spec_schema: connectorTag.endpointSpecSchema, - image_name: node.imageName, - protocol: (connectorTag.protocol ?? 'capture') as EntityWithCreateWorkflow, - title: node.title ?? '', - }; - - return { - id: node.id, - detail: node.shortDescription ?? '', - image_name: node.imageName, - image: node.logoUrl ?? '', - recommended: node.recommended, - title: node.title ?? '', - connector_tags: [tag], - } as ConnectorWithTagQuery; -}; - -export function useGetSingleConnector() { - const client = useClient(); - - return useCallback( - async (connectorId: string) => { - const result = await client - .query( - CONNECTOR_BY_ID_QUERY, - { id: connectorId }, - { requestPolicy: 'network-only' } - ) - .toPromise(); - - if (result.error || !result.data?.connector) { - return { - data: null, - error: result.error ?? 'Connector not found', - }; - } - - const mapped = toConnectorWithTagQuery(result.data.connector); - return { data: mapped ? [mapped] : [], error: null }; - }, - [client] - ); -} diff --git a/src/context/ConnectorTag.tsx b/src/context/ConnectorTag.tsx index 3f2f6894cc..bd14659c16 100644 --- a/src/context/ConnectorTag.tsx +++ b/src/context/ConnectorTag.tsx @@ -3,6 +3,7 @@ import type { BaseComponentProps } from 'src/types'; import { createContext, useContext, useEffect, useState } from 'react'; +import { useIntl } from 'react-intl'; import { useClient } from 'urql'; import { CONNECTOR_BY_ID_QUERY } from 'src/api/gql/connectors'; @@ -28,6 +29,7 @@ const ConnectorTagContext = createContext<ConnectorTagData | null>(null); export const ConnectorTagProvider = ({ children }: BaseComponentProps) => { const connectorId = useGlobalSearchParams(GlobalSearchParams.CONNECTOR_ID); const client = useClient(); + const intl = useIntl(); const [connectorTag, setConnectorTag] = useState<ConnectorTagData | null>( null @@ -72,11 +74,17 @@ export const ConnectorTagProvider = ({ children }: BaseComponentProps) => { return ( <ErrorComponent condensed - error={{ ...BASE_ERROR, message: fetchError }} + error={{ + ...BASE_ERROR, + message: intl.formatMessage({ + id: 'workflow.connectorTag.error.message', + }), + }} /> ); } + // While loading we don't want to show anything if (!connectorTag) { return null; } diff --git a/src/lang/en-US/Workflows.ts b/src/lang/en-US/Workflows.ts index c08facb302..da4a1ab35b 100644 --- a/src/lang/en-US/Workflows.ts +++ b/src/lang/en-US/Workflows.ts @@ -502,4 +502,6 @@ export const Workflows: Record<string, string> = { 'schemaManagement.options.manual.label': `Manually manage schemas`, 'schemaManagement.options.auto.description': `Estuary infers the schema based on the data. With automatically handle changes.`, 'schemaManagement.options.auto.label': `Let Estuary control schemas`, + + 'workflow.connectorTag.error.message': `Failed to fetch connector details`, }; diff --git a/src/services/types.ts b/src/services/types.ts index 58f72fd7c5..94970b458f 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -33,7 +33,6 @@ export enum CustomEvents { COLLECTION_CREATE = 'Collection_Create', COLLECTION_SCHEMA = 'CollectionSchema', CONNECTOR_VERSION_MISSING = 'Connector_Version:Missing', - CONNECTOR_VERSION_UNSUPPORTED = 'Connector_Version:Unsupported', DATA_PLANE_SELECTOR = 'Data_Plane_Selector', DATE_TIME_PICKER_CHANGE = 'Date_Time_Picker:Change', DIRECTIVE = 'Directive', diff --git a/src/stores/DetailsForm/Store.ts b/src/stores/DetailsForm/Store.ts index 36dd2cc550..69785ae6fe 100644 --- a/src/stores/DetailsForm/Store.ts +++ b/src/stores/DetailsForm/Store.ts @@ -115,7 +115,6 @@ const getInitialStateData = (): Pick< | 'draftedEntityName' | 'entityNameChanged' | 'previousDetails' - | 'unsupportedConnectorVersion' > => ({ connectors: [], @@ -124,7 +123,6 @@ const getInitialStateData = (): Pick< details: initialDetails, errorsExist: true, - unsupportedConnectorVersion: false, draftedEntityName: '', entityNameChanged: false, @@ -234,25 +232,6 @@ export const getInitialState = ( ); }, - setUnsupportedConnectorVersion: (evaluatedId, existingId) => { - set( - produce((state: DetailsFormState) => { - const unsupported = evaluatedId !== existingId; - - if (unsupported) { - logRocketEvent(CustomEvents.CONNECTOR_VERSION_UNSUPPORTED, { - evaluatedId, - existingId, - }); - } - - state.unsupportedConnectorVersion = unsupported; - }), - false, - 'Unsupported Connector Version Flag Changed' - ); - }, - setDraftedEntityName: (value) => { set( produce((state: DetailsFormState) => { @@ -357,7 +336,6 @@ export const getInitialState = ( const { catalog_name, connector_image_tag, - connector_tag_id, data_plane_id, } = data[0]; @@ -380,11 +358,6 @@ export const getInitialState = ( }, }; - get().setUnsupportedConnectorVersion( - connectorImage.id, - connector_tag_id - ); - get().setDetails(hydratedDetails); get().setPreviousDetails(hydratedDetails); } else { diff --git a/src/stores/DetailsForm/types.ts b/src/stores/DetailsForm/types.ts index 4214b24bee..2908f08775 100644 --- a/src/stores/DetailsForm/types.ts +++ b/src/stores/DetailsForm/types.ts @@ -76,12 +76,6 @@ export interface DetailsFormState connectors: { [key: string]: any }[]; setConnectors: (val: DetailsFormState['connectors']) => void; - unsupportedConnectorVersion: boolean; - setUnsupportedConnectorVersion: ( - evaluatedId: string, - existingId: string - ) => void; - // Misc. draftedEntityName: string; setDraftedEntityName: ( diff --git a/src/stores/DetailsForm/useDetailsFormHydrator.ts b/src/stores/DetailsForm/useDetailsFormHydrator.ts index fc7b50fd93..ee4046707b 100644 --- a/src/stores/DetailsForm/useDetailsFormHydrator.ts +++ b/src/stores/DetailsForm/useDetailsFormHydrator.ts @@ -56,9 +56,6 @@ export const useDetailsFormHydrator = () => { const setPreviousDetails = useDetailsFormStore( (state) => state.setPreviousDetails ); - const setUnsupportedConnectorVersion = useDetailsFormStore( - (state) => state.setUnsupportedConnectorVersion - ); const evaluateDataPlaneOptions = useEvaluateDataPlaneOptions(); const getDataPlane = useGetDataPlane(); @@ -110,7 +107,6 @@ export const useDetailsFormHydrator = () => { const { catalog_name, - connector_tag_id, data_plane_id, data_plane_name, reactor_address, @@ -136,11 +132,6 @@ export const useDetailsFormHydrator = () => { }, }; - setUnsupportedConnectorVersion( - connectorImage.id, - connector_tag_id - ); - setDetails(hydratedDetails); setPreviousDetails(hydratedDetails); @@ -180,7 +171,6 @@ export const useDetailsFormHydrator = () => { setHydrated, setHydrationErrorsExist, setPreviousDetails, - setUnsupportedConnectorVersion, workflow, ] ); From 92c4708c138b45df57fa0e8388eda09bca3add3f Mon Sep 17 00:00:00 2001 From: Travis Jenkins <travis@estuary.dev> Date: Mon, 6 Apr 2026 14:08:44 -0400 Subject: [PATCH 09/30] fetching binding stuff from new context --- src/api/hydration.ts | 19 ------------------- src/api/types.ts | 8 -------- src/stores/Binding/Hydrator.tsx | 15 +++++---------- src/stores/Binding/Store.ts | 10 +++++----- src/stores/Binding/shared.ts | 24 ++++++++++-------------- src/stores/Binding/types.ts | 3 ++- 6 files changed, 22 insertions(+), 57 deletions(-) diff --git a/src/api/hydration.ts b/src/api/hydration.ts index a105b7957a..31f47abdf6 100644 --- a/src/api/hydration.ts +++ b/src/api/hydration.ts @@ -1,4 +1,3 @@ -import type { ConnectorTagResourceData } from 'src/api/types'; import type { LiveSpecsExt_MaterializeOrTransform, LiveSpecsExtQuery, @@ -13,24 +12,6 @@ import { TABLES, } from 'src/services/supabase'; -// TODO (optimization): Consider removing the tight coupling between this file and the stores. -// These APIs are truly general purpose. Perhaps break them out by supabase table. -export const getSchema_Resource = async (connectorTagId: string | null) => { - const resourceSchema = await supabaseRetry( - () => - supabaseClient - .from(TABLES.CONNECTOR_TAGS) - .select( - `default_capture_interval,disable_backfill,resource_spec_schema` - ) - .eq('id', connectorTagId) - .single(), - 'getSchema_Resource' - ).then(handleSuccess<ConnectorTagResourceData>, handleFailure); - - return resourceSchema; -}; - const liveSpecColumns = `id,spec_type,spec,writes_to,reads_from,last_pub_id,updated_at`; export const getLiveSpecsByLiveSpecId = async ( diff --git a/src/api/types.ts b/src/api/types.ts index 6e32641276..70836a6a61 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -15,14 +15,6 @@ export interface ConnectorTag extends BaseConnectorTag { title: string; } -// This interface is only used to type the data returned by getSchema_Resource. -export interface ConnectorTagResourceData { - connector_id: string; - default_capture_interval: string | null; - disable_backfill: boolean; - resource_spec_schema: Schema; -} - export interface ConnectorWithTag { connector_tags: ConnectorTag[]; id: string; diff --git a/src/stores/Binding/Hydrator.tsx b/src/stores/Binding/Hydrator.tsx index 08275a10b9..d4c6ba70bc 100644 --- a/src/stores/Binding/Hydrator.tsx +++ b/src/stores/Binding/Hydrator.tsx @@ -2,6 +2,7 @@ import type { BaseComponentProps } from 'src/types'; import { useEffect, useRef } from 'react'; +import { useConnectorTag } from 'src/context/ConnectorTag'; import { useEntityType } from 'src/context/EntityContext'; import { useEntityWorkflow, @@ -16,7 +17,6 @@ import { useBinding_setHydrated, useBinding_setHydrationErrorsExist, } from 'src/stores/Binding/hooks'; -import { useDetailsFormStore } from 'src/stores/DetailsForm/Store'; import { useSourceCaptureStore } from 'src/stores/SourceCapture/Store'; export const BindingHydrator = ({ children }: BaseComponentProps) => { @@ -30,9 +30,7 @@ export const BindingHydrator = ({ children }: BaseComponentProps) => { const getTrialPrefixes = useTrialPrefixes(); - const connectorTagId = useDetailsFormStore( - (state) => state.details.data.connectorImage.id - ); + const connectorTag = useConnectorTag(); const hydrated = useBinding_hydrated(); const setHydrated = useBinding_setHydrated(); @@ -45,10 +43,7 @@ export const BindingHydrator = ({ children }: BaseComponentProps) => { ); useEffect(() => { - if ( - (workflow && connectorTagId.length > 0) || - workflow === 'collection_create' - ) { + if ((workflow && connectorTag) || workflow === 'collection_create') { setActive(true); // TODO (Workflow Hydrator) - when moving bindings into the parent hydrator @@ -57,7 +52,7 @@ export const BindingHydrator = ({ children }: BaseComponentProps) => { hydrateState( editWorkflow, entityType, - connectorTagId, + connectorTag, getTrialPrefixes, rehydrating.current ) @@ -89,7 +84,7 @@ export const BindingHydrator = ({ children }: BaseComponentProps) => { }); } }, [ - connectorTagId, + connectorTag, editWorkflow, entityType, getTrialPrefixes, diff --git a/src/stores/Binding/Store.ts b/src/stores/Binding/Store.ts index 437d1b2b6a..fb2b63c5bb 100644 --- a/src/stores/Binding/Store.ts +++ b/src/stores/Binding/Store.ts @@ -230,7 +230,7 @@ const getInitialState = ( hydrateState: async ( editWorkflow, entityType, - connectorTagId, + connectorTag, getTrialOnlyPrefixes, rehydrating ) => { @@ -248,13 +248,13 @@ const getInitialState = ( get().resetState(materializationRehydrating); const connectorTagResponse = await hydrateConnectorTagDependentState( - connectorTagId, + connectorTag, get ); const fallbackInterval = entityType === 'capture' && - typeof connectorTagResponse?.default_capture_interval === 'string' + typeof connectorTagResponse?.defaultCaptureInterval === 'string' ? '' : null; @@ -273,7 +273,7 @@ const getInitialState = ( const specHydrationResponse = await hydrateSpecificationDependentState( - connectorTagResponse?.default_capture_interval, + connectorTagResponse?.defaultCaptureInterval, entityType, fallbackInterval, get, @@ -305,7 +305,7 @@ const getInitialState = ( } else { get().setCaptureInterval( fallbackInterval, - connectorTagResponse?.default_capture_interval + connectorTagResponse?.defaultCaptureInterval ); } diff --git a/src/stores/Binding/shared.ts b/src/stores/Binding/shared.ts index 7718ffca93..d61ad34b6b 100644 --- a/src/stores/Binding/shared.ts +++ b/src/stores/Binding/shared.ts @@ -1,4 +1,5 @@ import type { PostgrestError } from '@supabase/postgrest-js'; +import type { ConnectorTagData } from 'src/context/ConnectorTag'; import type { LiveSpecsExtQuery } from 'src/hooks/useLiveSpecsExt'; import type { BindingFieldSelectionDictionary, @@ -17,7 +18,6 @@ import type { StoreApi } from 'zustand'; import { difference, intersection, isEmpty, omit } from 'lodash'; import { getDraftSpecsByDraftId } from 'src/api/draftSpecs'; -import { getSchema_Resource } from 'src/api/hydration'; import { GlobalSearchParams } from 'src/hooks/searchParams/useGlobalSearchParams'; import { BASE_ERROR } from 'src/services/supabase'; import { getInitialBackfillData } from 'src/stores/Binding/slices/Backfill'; @@ -28,7 +28,7 @@ import { import { getInitialTimeTravelData } from 'src/stores/Binding/slices/TimeTravel'; import { getInitialHydrationData } from 'src/stores/extensions/Hydration'; import { populateErrors } from 'src/stores/utils'; -import { hasLength, hasOwnProperty } from 'src/utils/misc-utils'; +import { hasOwnProperty } from 'src/utils/misc-utils'; import { formatCaptureInterval } from 'src/utils/time-utils'; import { getCollectionName, getDisableProps } from 'src/utils/workflow-utils'; @@ -333,25 +333,21 @@ export const stubBindingFieldSelection = ( export const STORE_KEY = 'Bindings'; export const hydrateConnectorTagDependentState = async ( - connectorTagId: string, + connectorTag: ConnectorTagData | null, get: StoreApi<BindingState>['getState'] -): Promise<Schema | null> => { - if (!hasLength(connectorTagId)) { +): Promise<ConnectorTagData | null> => { + if (!connectorTag) { return null; } - const { data, error } = await getSchema_Resource(connectorTagId); - - if (error) { - get().setHydrationErrorsExist(true); - } else if (data?.resource_spec_schema) { - const schema = data.resource_spec_schema as unknown as Schema; + if (connectorTag.resourceSpecSchema) { + const schema = connectorTag.resourceSpecSchema as unknown as Schema; await get().setResourceSchema(schema); - - get().setBackfillSupported(!Boolean(data.disable_backfill)); } - return data; + get().setBackfillSupported(!Boolean(connectorTag.disableBackfill)); + + return connectorTag; }; export const hydrateSpecificationDependentState = async ( diff --git a/src/stores/Binding/types.ts b/src/stores/Binding/types.ts index cfb96d0992..427b6de8a7 100644 --- a/src/stores/Binding/types.ts +++ b/src/stores/Binding/types.ts @@ -1,5 +1,6 @@ import type { DurationObjectUnits } from 'luxon'; import type { TrialCollectionQuery } from 'src/api/liveSpecsExt'; +import type { ConnectorTagData } from 'src/context/ConnectorTag'; import type { LiveSpecsExt_MaterializeOrTransform } from 'src/hooks/useLiveSpecsExt'; import type { ResourceConfigPointers } from 'src/services/ajv'; import type { CallSupabaseResponse } from 'src/services/supabase'; @@ -210,7 +211,7 @@ export interface BindingState hydrateState: ( editWorkflow: boolean, entityType: Entity, - connectorTagId: string, + connectorTag: ConnectorTagData | null, getTrialOnlyPrefixes: (prefixes: string[]) => Promise<string[]>, rehydrating?: boolean ) => Promise<LiveSpecsExt_MaterializeOrTransform[] | null>; From e4003beb7ef0ac719a8e4148f3c245086017a46d Mon Sep 17 00:00:00 2001 From: Travis Jenkins <travis@estuary.dev> Date: Mon, 6 Apr 2026 14:21:40 -0400 Subject: [PATCH 10/30] cleaning up the valid connector check - the backend checks this and the new context blocks showing the entire flow if there isn't a connector --- src/components/capture/Create/index.tsx | 10 +----- src/components/capture/Edit.tsx | 7 ---- .../capture/ExpressCreate/index.tsx | 4 --- src/components/capture/GenerateButton.tsx | 9 ++--- src/components/capture/RediscoverButton.tsx | 5 ++- .../materialization/Create/index.tsx | 9 +---- src/components/materialization/Edit.tsx | 9 +---- .../materialization/GenerateButton.tsx | 8 ++--- src/hooks/connectors/shared.ts | 20 ----------- src/hooks/connectors/useHasConnectors.ts | 36 ------------------- 10 files changed, 9 insertions(+), 108 deletions(-) delete mode 100644 src/hooks/connectors/useHasConnectors.ts diff --git a/src/components/capture/Create/index.tsx b/src/components/capture/Create/index.tsx index 834aebf9ce..dada996db1 100644 --- a/src/components/capture/Create/index.tsx +++ b/src/components/capture/Create/index.tsx @@ -11,7 +11,6 @@ import { import EntityCreate from 'src/components/shared/Entity/Create'; import EntityToolbar from 'src/components/shared/Entity/Header'; import { MutateDraftSpecProvider } from 'src/components/shared/Entity/MutateDraftSpecContext'; -import useValidConnectorsExist from 'src/hooks/connectors/useHasConnectors'; import useDraftSpecs from 'src/hooks/useDraftSpecs'; import usePageTitle from 'src/hooks/usePageTitle'; import { CustomEvents } from 'src/services/types'; @@ -27,8 +26,6 @@ function CaptureCreate() { 'https://docs.estuary.dev/guides/create-dataflow/#create-a-capture', }); - const hasConnectors = useValidConnectorsExist(entityType); - // Details Form Store const entityNameChanged = useDetailsFormStore( (state) => state.entityNameChanged @@ -88,13 +85,11 @@ function CaptureCreate() { logEvent: CustomEvents.CAPTURE_CREATE, }} secondaryButtonProps={{ - disabled: !hasConnectors, logEvent: CustomEvents.CAPTURE_TEST, }} GenerateButton={ <CaptureGenerateButton entityType={entityType} - disabled={!hasConnectors} createWorkflowMetadata={{ initiateDiscovery, setInitiateDiscovery, @@ -104,10 +99,7 @@ function CaptureCreate() { /> } RediscoverButton={ - <RediscoverButton - entityType={entityType} - disabled={!hasConnectors} - /> + <RediscoverButton entityType={entityType} /> } /> </MutateDraftSpecProvider> diff --git a/src/components/capture/Edit.tsx b/src/components/capture/Edit.tsx index 3ed4292f9e..ecc019a9fe 100644 --- a/src/components/capture/Edit.tsx +++ b/src/components/capture/Edit.tsx @@ -12,7 +12,6 @@ import EntityEdit from 'src/components/shared/Entity/Edit'; import DraftInitializer from 'src/components/shared/Entity/Edit/DraftInitializer'; import EntityToolbar from 'src/components/shared/Entity/Header'; import { MutateDraftSpecProvider } from 'src/components/shared/Entity/MutateDraftSpecContext'; -import useValidConnectorsExist from 'src/hooks/connectors/useHasConnectors'; import useGlobalSearchParams, { GlobalSearchParams, } from 'src/hooks/searchParams/useGlobalSearchParams'; @@ -30,9 +29,6 @@ function CaptureEdit() { const lastPubId = useGlobalSearchParams(GlobalSearchParams.LAST_PUB_ID); - // Supabase - const hasConnectors = useValidConnectorsExist(entityType); - // Draft Editor Store const draftId = useEditorStore_id(); const persistedDraftId = useEditorStore_persistedDraftId(); @@ -68,13 +64,11 @@ function CaptureEdit() { logEvent: CustomEvents.CAPTURE_EDIT, }} secondaryButtonProps={{ - disabled: !hasConnectors, logEvent: CustomEvents.CAPTURE_TEST, }} GenerateButton={ <CaptureGenerateButton entityType={entityType} - disabled={!hasConnectors} /> } /> @@ -82,7 +76,6 @@ function CaptureEdit() { RediscoverButton={ <RediscoverButton entityType={entityType} - disabled={!hasConnectors} /> } /> diff --git a/src/components/capture/ExpressCreate/index.tsx b/src/components/capture/ExpressCreate/index.tsx index 296ae71a5f..b32f293e80 100644 --- a/src/components/capture/ExpressCreate/index.tsx +++ b/src/components/capture/ExpressCreate/index.tsx @@ -11,7 +11,6 @@ import EntityCreateExpress from 'src/components/shared/Entity/Create/Express'; import EntityToolbar from 'src/components/shared/Entity/Header'; import { MutateDraftSpecProvider } from 'src/components/shared/Entity/MutateDraftSpecContext'; import { useEntityType } from 'src/context/EntityContext'; -import useValidConnectorsExist from 'src/hooks/connectors/useHasConnectors'; import useDraftSpecs from 'src/hooks/useDraftSpecs'; import { CustomEvents } from 'src/services/types'; import { useDetailsFormStore } from 'src/stores/DetailsForm/Store'; @@ -20,7 +19,6 @@ import { MAX_DISCOVER_TIME } from 'src/utils/misc-utils'; export default function ExpressCaptureCreate() { const entityType = useEntityType(); - const hasConnectors = useValidConnectorsExist(entityType); // Details Form Store const imageTag = useDetailsFormStore( @@ -87,13 +85,11 @@ export default function ExpressCaptureCreate() { logEvent: CustomEvents.CAPTURE_CREATE, }} secondaryButtonProps={{ - disabled: !hasConnectors, logEvent: CustomEvents.CAPTURE_TEST, }} GenerateButton={ <CaptureGenerateButton entityType={entityType} - disabled={!hasConnectors} createWorkflowMetadata={{ initiateDiscovery, setInitiateDiscovery, diff --git a/src/components/capture/GenerateButton.tsx b/src/components/capture/GenerateButton.tsx index eba0ee5910..fe51224731 100644 --- a/src/components/capture/GenerateButton.tsx +++ b/src/components/capture/GenerateButton.tsx @@ -17,18 +17,13 @@ import { FormStatus } from 'src/stores/FormState/types'; interface Props { entityType: Entity; - disabled: boolean; createWorkflowMetadata?: { initiateDiscovery: boolean; setInitiateDiscovery: Dispatch<SetStateAction<boolean>>; }; } -function CaptureGenerateButton({ - entityType, - disabled, - createWorkflowMetadata, -}: Props) { +function CaptureGenerateButton({ entityType, createWorkflowMetadata }: Props) { const isEdit = useEntityWorkflow_Editing(); const rediscoveryRequired = useBinding_rediscoveryRequired(); @@ -84,7 +79,7 @@ function CaptureGenerateButton({ void generateCatalog(); }} - disabled={disabled || isSaving || formActive} + disabled={isSaving || formActive} sx={entityHeaderButtonSx} > <FormattedMessage id="cta.generateCatalog.capture" /> diff --git a/src/components/capture/RediscoverButton.tsx b/src/components/capture/RediscoverButton.tsx index bb36c40469..c05f960175 100644 --- a/src/components/capture/RediscoverButton.tsx +++ b/src/components/capture/RediscoverButton.tsx @@ -10,10 +10,9 @@ import { disabledButtonText } from 'src/context/Theme'; interface Props { entityType: Entity; - disabled: boolean; } -function RediscoverButton({ entityType, disabled }: Props) { +function RediscoverButton({ entityType }: Props) { const { generateCatalog, isSaving, formActive } = useDiscoverCapture( entityType, { initiateRediscovery: true } @@ -22,7 +21,7 @@ function RediscoverButton({ entityType, disabled }: Props) { const intl = useIntl(); const theme = useTheme(); - const disable = disabled || isSaving || formActive; + const disable = isSaving || formActive; return ( <Tooltip diff --git a/src/components/materialization/Create/index.tsx b/src/components/materialization/Create/index.tsx index b608f02557..8d832d0cd3 100644 --- a/src/components/materialization/Create/index.tsx +++ b/src/components/materialization/Create/index.tsx @@ -11,7 +11,6 @@ import MaterializeGenerateButton from 'src/components/materialization/GenerateBu import EntityCreate from 'src/components/shared/Entity/Create'; import EntityToolbar from 'src/components/shared/Entity/Header'; import { MutateDraftSpecProvider } from 'src/components/shared/Entity/MutateDraftSpecContext'; -import useValidConnectorsExist from 'src/hooks/connectors/useHasConnectors'; import useDraftSpecs from 'src/hooks/useDraftSpecs'; import usePageTitle from 'src/hooks/usePageTitle'; import { CustomEvents } from 'src/services/types'; @@ -27,9 +26,6 @@ function MaterializationCreate() { const entityType = 'materialization'; - // Supabase - const hasConnectors = useValidConnectorsExist(entityType); - // Details Form Store const imageTag = useDetailsFormStore( (state) => state.details.data.connectorImage @@ -67,16 +63,13 @@ function MaterializationCreate() { Toolbar={ <EntityToolbar GenerateButton={ - <MaterializeGenerateButton - disabled={!hasConnectors} - /> + <MaterializeGenerateButton /> } primaryButtonProps={{ disabled: !draftId, logEvent: CustomEvents.MATERIALIZATION_CREATE, }} secondaryButtonProps={{ - disabled: !hasConnectors, logEvent: CustomEvents.MATERIALIZATION_TEST, }} /> diff --git a/src/components/materialization/Edit.tsx b/src/components/materialization/Edit.tsx index b31880adf7..fcd6783e2a 100644 --- a/src/components/materialization/Edit.tsx +++ b/src/components/materialization/Edit.tsx @@ -11,7 +11,6 @@ import EntityEdit from 'src/components/shared/Entity/Edit'; import DraftInitializer from 'src/components/shared/Entity/Edit/DraftInitializer'; import EntityToolbar from 'src/components/shared/Entity/Header'; import { MutateDraftSpecProvider } from 'src/components/shared/Entity/MutateDraftSpecContext'; -import useValidConnectorsExist from 'src/hooks/connectors/useHasConnectors'; import useGlobalSearchParams, { GlobalSearchParams, } from 'src/hooks/searchParams/useGlobalSearchParams'; @@ -28,9 +27,6 @@ function MaterializationEdit() { const entityType = 'materialization'; - // Supabase - const hasConnectors = useValidConnectorsExist(entityType); - // Draft Editor Store const draftId = useEditorStore_id(); const persistedDraftId = useEditorStore_persistedDraftId(); @@ -59,16 +55,13 @@ function MaterializationEdit() { toolbar={ <EntityToolbar GenerateButton={ - <MaterializeGenerateButton - disabled={!hasConnectors} - /> + <MaterializeGenerateButton /> } primaryButtonProps={{ disabled: !draftId, logEvent: CustomEvents.MATERIALIZATION_EDIT, }} secondaryButtonProps={{ - disabled: !hasConnectors, logEvent: CustomEvents.MATERIALIZATION_TEST, }} /> diff --git a/src/components/materialization/GenerateButton.tsx b/src/components/materialization/GenerateButton.tsx index 0de06048ba..85c6224677 100644 --- a/src/components/materialization/GenerateButton.tsx +++ b/src/components/materialization/GenerateButton.tsx @@ -8,11 +8,7 @@ import { useMutateDraftSpec } from 'src/components/shared/Entity/MutateDraftSpec import { entityHeaderButtonSx } from 'src/context/Theme'; import { useFormStateStore_isActive } from 'src/stores/FormState/hooks'; -interface Props { - disabled: boolean; -} - -function MaterializeGenerateButton({ disabled }: Props) { +function MaterializeGenerateButton() { const intl = useIntl(); const generateCatalog = useGenerateCatalog(); const isSaving = useEditorStore_isSaving(); @@ -24,7 +20,7 @@ function MaterializeGenerateButton({ disabled }: Props) { onClick={() => { void generateCatalog(mutateDraftSpecs); }} - disabled={disabled || isSaving || formActive} + disabled={isSaving || formActive} sx={entityHeaderButtonSx} > {intl.formatMessage({ id: 'cta.generateCatalog.materialization' })} diff --git a/src/hooks/connectors/shared.ts b/src/hooks/connectors/shared.ts index 2e7c554e7e..65c2a7f20e 100644 --- a/src/hooks/connectors/shared.ts +++ b/src/hooks/connectors/shared.ts @@ -1,5 +1,3 @@ -import type { CONNECTOR_NAME, CONNECTOR_RECOMMENDED } from 'src/api/shared'; - ////////////////////////// // useConnectors ////////////////////////// @@ -12,21 +10,3 @@ export interface Connector { export const CONNECTOR_QUERY = ` id, title, image_name `; - -///////////////////////////////// -// useConnectorsExist -///////////////////////////////// -export interface ConnectorsExist { - id: string; - // FILTERING TYPES HACK - ['connector_tags.protocol']: undefined; - [CONNECTOR_RECOMMENDED]: undefined; - [CONNECTOR_NAME]: undefined; -} - -export const CONNECTORS_EXIST_QUERY = ` - image_name, - connector_tags !inner( - protocol - ) -`; diff --git a/src/hooks/connectors/useHasConnectors.ts b/src/hooks/connectors/useHasConnectors.ts deleted file mode 100644 index 9964100fae..0000000000 --- a/src/hooks/connectors/useHasConnectors.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { ConnectorsExist } from 'src/hooks/connectors/shared'; - -import { useMemo } from 'react'; - -import { useQuery } from '@supabase-cache-helpers/postgrest-swr'; - -import { supabaseClient } from 'src/context/GlobalProviders'; -import { CONNECTORS_EXIST_QUERY } from 'src/hooks/connectors/shared'; -import { TABLES } from 'src/services/supabase'; -import { requiredConnectorColumnsExist } from 'src/utils/connector-utils'; - -// TODO (connectors store) - this is temporary -// We used to check if connectors exist with a query that returned a bunch -// of data and sorting. However, we then go and fetch that again a second -// later. So this query is super small to reduce the amount of data and just -// making sure there are connectors. -// We should just make a store that fetches all the needed connectors once -// then always pull from that. The store could also cache the schemas, etc. -// when the user selects a connector. -function useValidConnectorsExist(protocol: string | null) { - const { data } = useQuery( - protocol - ? requiredConnectorColumnsExist<ConnectorsExist[]>( - supabaseClient - .from(TABLES.CONNECTORS) - .select(CONNECTORS_EXIST_QUERY) - .eq('connector_tags.protocol', protocol), - 'connector_tags' - ) - : null - ); - - return useMemo(() => (data ? data.length > 0 : false), [data]); -} - -export default useValidConnectorsExist; From 58a91188a68416a42961b98b464a0065419afb45 Mon Sep 17 00:00:00 2001 From: Travis Jenkins <travis@estuary.dev> Date: Mon, 6 Apr 2026 14:33:55 -0400 Subject: [PATCH 11/30] cleaning up some typing --- .../connectors/Grid/ConnectorCards.tsx | 32 ++++++++++++------- src/components/connectors/Grid/cards/types.ts | 2 +- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/components/connectors/Grid/ConnectorCards.tsx b/src/components/connectors/Grid/ConnectorCards.tsx index c23235c06c..de0bfa73fd 100644 --- a/src/components/connectors/Grid/ConnectorCards.tsx +++ b/src/components/connectors/Grid/ConnectorCards.tsx @@ -1,6 +1,6 @@ import type { ConnectorCardsProps } from 'src/components/connectors/Grid/types'; import type { ConnectorProto } from 'src/gql-types/graphql'; -import type { TableState } from 'src/types'; +import type { EntityWithCreateWorkflow, TableState } from 'src/types'; import { useEffect, useMemo, useRef, useState } from 'react'; @@ -57,8 +57,13 @@ export default function ConnectorCards({ const selectData = useMemo(() => { const nodes = (queryData?.connectors.edges ?? []) .map((edge) => edge.node) - // TODO (gql:connector) - I think we can remove this - .filter((node) => node.connectorTag !== null); + .filter( + ( + node + ): node is typeof node & { + connectorTag: NonNullable<typeof node.connectorTag>; + } => node.connectorTag !== null + ); if (!searchQuery) return nodes; @@ -76,7 +81,10 @@ export default function ConnectorCards({ <ConnectorRequestCard key="connector-tile-request" /> ); - const primaryCtaClick = (entityType: any, connectorId: string) => { + const primaryCtaClick = ( + entityType: EntityWithCreateWorkflow, + connectorId: string + ) => { navigateToCreate(entityType, { id: connectorId, advanceToForm: true, @@ -137,14 +145,14 @@ export default function ConnectorCards({ <> {selectData .map((node) => { - // TODO (gql:connector) - doubt we need this - if (!node.connectorTag) { - return null; - } - + const { connectorTag } = node; const ConnectorCard = condensed ? Card : LegacyCard; + const entityType = connectorTag.protocol; - const entityType = node.connectorTag?.protocol ?? 'capture'; + // TODO (GQL:connector) how to better handle with typing? + if (!entityType) { + return null; + } return ( <ConnectorCard @@ -152,10 +160,10 @@ export default function ConnectorCards({ clickHandler={() => primaryCtaClick( entityType, - node.connectorTag?.connectorId + connectorTag.connectorId ) } - docsUrl={node.connectorTag?.documentationUrl ?? ''} + docsUrl={connectorTag.documentationUrl ?? ''} entityType={entityType} recommended={node.recommended} Detail={ diff --git a/src/components/connectors/Grid/cards/types.ts b/src/components/connectors/Grid/cards/types.ts index 5ef9e2f09d..9fb802279c 100644 --- a/src/components/connectors/Grid/cards/types.ts +++ b/src/components/connectors/Grid/cards/types.ts @@ -8,7 +8,7 @@ export interface CardProps { clickHandler?: () => void; CTA?: ReactNode; docsUrl?: string; - entityType?: string; + entityType?: string | null; externalLink?: TileProps['externalLink']; recommended?: boolean; } From 7a152b2c93285f65430c6475aa295c27be307a6d Mon Sep 17 00:00:00 2001 From: Travis Jenkins <travis@estuary.dev> Date: Mon, 6 Apr 2026 16:11:08 -0400 Subject: [PATCH 12/30] Cleaning up stuff we should not need anymore Adding in fetching 500 so we don't need pagination Updating typing now that my local has latest merged on flow --- src/api/gql/connectors.ts | 4 +- src/components/capture/Create/index.tsx | 8 --- src/gql-types/gql.ts | 6 +- src/gql-types/graphql.ts | 72 +++++++++++++++------ src/gql-types/schema.graphql | 83 ++++++++++++++++++------- 5 files changed, 122 insertions(+), 51 deletions(-) diff --git a/src/api/gql/connectors.ts b/src/api/gql/connectors.ts index 3a379f9d8b..9b7c344378 100644 --- a/src/api/gql/connectors.ts +++ b/src/api/gql/connectors.ts @@ -5,9 +5,11 @@ import { graphql } from 'src/gql-types'; export type ConnectorGridNode = ConnectorsGridQuery['connectors']['edges'][number]['node']; +// TODO (GQL:Connector) - fine for now but this ignores pagination and just +// fetches 500 all at once export const CONNECTORS_QUERY = graphql(` query ConnectorsGrid($filter: ConnectorsFilter, $after: String) { - connectors(first: 100, after: $after, filter: $filter) { + connectors(first: 500, after: $after, filter: $filter) { edges { cursor node { diff --git a/src/components/capture/Create/index.tsx b/src/components/capture/Create/index.tsx index dada996db1..a2035ea88b 100644 --- a/src/components/capture/Create/index.tsx +++ b/src/components/capture/Create/index.tsx @@ -54,14 +54,6 @@ function CaptureCreate() { } }, [mutateDraftSpecs, mutate_advancedEditor]); - // TODO (GQL:connector) - we do not allow changing I think we're good to remove this - // Reset the catalog if the connector changes - // useEffect(() => { - // console.log('sup >>>>> '); - // setDraftId(null); - // setInitiateDiscovery(true); - // }, [setDraftId, setInitiateDiscovery, imageTag]); - // If the name changed we need to make sure we run discovery again useEffect(() => { if (entityNameChanged) { diff --git a/src/gql-types/gql.ts b/src/gql-types/gql.ts index 8bf18de2e9..33ad1e0e0e 100644 --- a/src/gql-types/gql.ts +++ b/src/gql-types/gql.ts @@ -14,7 +14,7 @@ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/ * Learn more about it here: https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#reducing-bundle-size */ type Documents = { - "\n query ConnectorsGrid($filter: ConnectorsFilter, $after: String) {\n connectors(first: 100, after: $after, filter: $filter) {\n edges {\n cursor\n node {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n documentationUrl\n protocol\n }\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n": typeof types.ConnectorsGridDocument, + "\n query ConnectorsGrid($filter: ConnectorsFilter, $after: String) {\n connectors(first: 500, after: $after, filter: $filter) {\n edges {\n cursor\n node {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n documentationUrl\n protocol\n }\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n": typeof types.ConnectorsGridDocument, "\n query SingleConnector($id: Id!) {\n connector(id: $id) {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n defaultCaptureInterval\n disableBackfill\n documentationUrl\n endpointSpecSchema\n resourceSpecSchema\n protocol\n }\n }\n }\n": typeof types.SingleConnectorDocument, "\n query DataPlanes($after: String) {\n dataPlanes(first: 100, after: $after) {\n edges {\n node {\n name\n cloudProvider\n region\n isPublic\n fqdn\n cidrBlocks\n awsIamUserArn\n gcpServiceAccountEmail\n azureApplicationClientId\n azureApplicationName\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n": typeof types.DataPlanesDocument, "\n query InviteLinks($first: Int, $after: String) {\n inviteLinks(first: $first, after: $after) {\n edges {\n node {\n token\n ssoProviderId\n catalogPrefix\n capability\n singleUse\n detail\n createdAt\n }\n cursor\n }\n pageInfo {\n ...PageInfoFields\n }\n }\n }\n": typeof types.InviteLinksDocument, @@ -34,7 +34,7 @@ type Documents = { "\n query AuthRolesQuery($after: String) {\n prefixes(by: { minCapability: read }, first: 7500, after: $after) {\n edges {\n node {\n prefix\n userCapability\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n": typeof types.AuthRolesQueryDocument, }; const documents: Documents = { - "\n query ConnectorsGrid($filter: ConnectorsFilter, $after: String) {\n connectors(first: 100, after: $after, filter: $filter) {\n edges {\n cursor\n node {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n documentationUrl\n protocol\n }\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n": types.ConnectorsGridDocument, + "\n query ConnectorsGrid($filter: ConnectorsFilter, $after: String) {\n connectors(first: 500, after: $after, filter: $filter) {\n edges {\n cursor\n node {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n documentationUrl\n protocol\n }\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n": types.ConnectorsGridDocument, "\n query SingleConnector($id: Id!) {\n connector(id: $id) {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n defaultCaptureInterval\n disableBackfill\n documentationUrl\n endpointSpecSchema\n resourceSpecSchema\n protocol\n }\n }\n }\n": types.SingleConnectorDocument, "\n query DataPlanes($after: String) {\n dataPlanes(first: 100, after: $after) {\n edges {\n node {\n name\n cloudProvider\n region\n isPublic\n fqdn\n cidrBlocks\n awsIamUserArn\n gcpServiceAccountEmail\n azureApplicationClientId\n azureApplicationName\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n": types.DataPlanesDocument, "\n query InviteLinks($first: Int, $after: String) {\n inviteLinks(first: $first, after: $after) {\n edges {\n node {\n token\n ssoProviderId\n catalogPrefix\n capability\n singleUse\n detail\n createdAt\n }\n cursor\n }\n pageInfo {\n ...PageInfoFields\n }\n }\n }\n": types.InviteLinksDocument, @@ -71,7 +71,7 @@ export function graphql(source: string): unknown; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n query ConnectorsGrid($filter: ConnectorsFilter, $after: String) {\n connectors(first: 100, after: $after, filter: $filter) {\n edges {\n cursor\n node {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n documentationUrl\n protocol\n }\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n"): (typeof documents)["\n query ConnectorsGrid($filter: ConnectorsFilter, $after: String) {\n connectors(first: 100, after: $after, filter: $filter) {\n edges {\n cursor\n node {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n documentationUrl\n protocol\n }\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n"]; +export function graphql(source: "\n query ConnectorsGrid($filter: ConnectorsFilter, $after: String) {\n connectors(first: 500, after: $after, filter: $filter) {\n edges {\n cursor\n node {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n documentationUrl\n protocol\n }\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n"): (typeof documents)["\n query ConnectorsGrid($filter: ConnectorsFilter, $after: String) {\n connectors(first: 500, after: $after, filter: $filter) {\n edges {\n cursor\n node {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n documentationUrl\n protocol\n }\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/src/gql-types/graphql.ts b/src/gql-types/graphql.ts index c0949d210b..7924512cba 100644 --- a/src/gql-types/graphql.ts +++ b/src/gql-types/graphql.ts @@ -161,34 +161,72 @@ export type AlertSubscriptionsBy = { }; export type AlertType = + /** + * Triggers when the automated background discovery process fails. If this + * alert is firing, it means that the Capture may be unable to respond to + * schema changes in the source system. + */ | 'auto_discover_failed' + /** + * Triggers when an automated background process needs to publish a spec, + * but is unable to because of publication errors. Background publications + * are peformed on all specs for a variety of reasons. For example, + * updating inferred schemas, or updating materialization bindings to match + * the source capture. When these publications fail, tasks are likely to + * stop functioning correctly until the issue can be addressed. + */ | 'background_publication_failed' + /** + * Triggers when there has been no data successfully processed by the task during + * the configured alert interval. + */ | 'data_movement_stalled' + /** + * Triggers automatically for every tenant that begins a free + * trial, and resolves when the trial period ends. + */ | 'free_trial' + /** Triggers when the free trial is getting close to expiring. */ | 'free_trial_ending' + /** + * Triggers after the free trial period has expired, and still no payment info + * has been added. + */ | 'free_trial_stalled' + /** + * Triggers for any tenants that do not have a payment method, and resolves when + * a payment method is added. + */ | 'missing_payment_method' + /** + * Triggers after repeated task failures have been observed. The task may or may not + * continue to make progress in between failures, but at a minimum, performance will + * be degraded. And in many scenarios, the task will be unable to process data at all. + */ | 'shard_failed' + /** + * The task was automatically disabled because its shards have been + * failing continuously for an extended period without any user intervention. + */ | 'task_auto_disabled_failing' + /** + * The task was automatically disabled because it had not processed any + * data for an extended period and had not been modified recently. + */ | 'task_auto_disabled_idle' + /** + * Warning that a task has been unable to run for an extended period. It will + * be automatically disabled unless the issue is addressed or a new version + * of the spec is published. + */ | 'task_chronically_failing' + /** + * Warning that a task has not processed any data for an extended period + * and has not been modified recently. It will be automatically disabled + * unless a new version of the spec is published. + */ | 'task_idle'; -/** Describes an alert type with user-facing metadata. */ -export type AlertTypeInfo = { - __typename?: 'AlertTypeInfo'; - /** The alert type identifier. */ - alertType: AlertType; - /** A user-facing description of what this alert type means. */ - description: Scalars['String']['output']; - /** A short, user-facing alert type name. */ - displayName: Scalars['String']['output']; - /** An indication of whether the alert type is subscribed to by default. */ - isDefault: Scalars['Boolean']['output']; - /** An indication of whether the alert type is considered to be a system alert. */ - isSystem: Scalars['Boolean']['output']; -}; - export type AlertsBy = { /** * Optionally filter alerts by active status. If unspecified, both active @@ -1024,8 +1062,6 @@ export type QueryRoot = { __typename?: 'QueryRoot'; /** Returns a complete list of alert subscriptions. */ alertSubscriptions: Array<AlertSubscription>; - /** Returns all possible alert types with their user-facing metadata. */ - alertTypes: Array<AlertTypeInfo>; /** * Returns a list of alerts that are currently active for the given catalog * prefixes. @@ -1593,7 +1629,7 @@ export type AuthRolesQueryQueryVariables = Exact<{ export type AuthRolesQueryQuery = { __typename?: 'QueryRoot', prefixes: { __typename?: 'PrefixRefConnection', edges: Array<{ __typename?: 'PrefixRefEdge', node: { __typename?: 'PrefixRef', prefix: any, userCapability: Capability } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null } } }; export const PageInfoFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PageInfoFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PageInfo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]} as unknown as DocumentNode<PageInfoFieldsFragment, unknown>; -export const ConnectorsGridDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ConnectorsGrid"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ConnectorsFilter"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"connectors"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"100"}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"imageName"}},{"kind":"Field","name":{"kind":"Name","value":"logoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"recommended"}},{"kind":"Field","name":{"kind":"Name","value":"shortDescription"}},{"kind":"Field","name":{"kind":"Name","value":"connectorTag"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orDefault"},"value":{"kind":"BooleanValue","value":true}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"connectorId"}},{"kind":"Field","name":{"kind":"Name","value":"imageTag"}},{"kind":"Field","name":{"kind":"Name","value":"documentationUrl"}},{"kind":"Field","name":{"kind":"Name","value":"protocol"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]} as unknown as DocumentNode<ConnectorsGridQuery, ConnectorsGridQueryVariables>; +export const ConnectorsGridDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ConnectorsGrid"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ConnectorsFilter"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"connectors"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"500"}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"imageName"}},{"kind":"Field","name":{"kind":"Name","value":"logoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"recommended"}},{"kind":"Field","name":{"kind":"Name","value":"shortDescription"}},{"kind":"Field","name":{"kind":"Name","value":"connectorTag"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orDefault"},"value":{"kind":"BooleanValue","value":true}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"connectorId"}},{"kind":"Field","name":{"kind":"Name","value":"imageTag"}},{"kind":"Field","name":{"kind":"Name","value":"documentationUrl"}},{"kind":"Field","name":{"kind":"Name","value":"protocol"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]} as unknown as DocumentNode<ConnectorsGridQuery, ConnectorsGridQueryVariables>; export const SingleConnectorDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"SingleConnector"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Id"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"connector"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"imageName"}},{"kind":"Field","name":{"kind":"Name","value":"logoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"recommended"}},{"kind":"Field","name":{"kind":"Name","value":"shortDescription"}},{"kind":"Field","name":{"kind":"Name","value":"connectorTag"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orDefault"},"value":{"kind":"BooleanValue","value":true}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"connectorId"}},{"kind":"Field","name":{"kind":"Name","value":"imageTag"}},{"kind":"Field","name":{"kind":"Name","value":"defaultCaptureInterval"}},{"kind":"Field","name":{"kind":"Name","value":"disableBackfill"}},{"kind":"Field","name":{"kind":"Name","value":"documentationUrl"}},{"kind":"Field","name":{"kind":"Name","value":"endpointSpecSchema"}},{"kind":"Field","name":{"kind":"Name","value":"resourceSpecSchema"}},{"kind":"Field","name":{"kind":"Name","value":"protocol"}}]}}]}}]}}]} as unknown as DocumentNode<SingleConnectorQuery, SingleConnectorQueryVariables>; export const DataPlanesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"DataPlanes"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"dataPlanes"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"100"}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"cloudProvider"}},{"kind":"Field","name":{"kind":"Name","value":"region"}},{"kind":"Field","name":{"kind":"Name","value":"isPublic"}},{"kind":"Field","name":{"kind":"Name","value":"fqdn"}},{"kind":"Field","name":{"kind":"Name","value":"cidrBlocks"}},{"kind":"Field","name":{"kind":"Name","value":"awsIamUserArn"}},{"kind":"Field","name":{"kind":"Name","value":"gcpServiceAccountEmail"}},{"kind":"Field","name":{"kind":"Name","value":"azureApplicationClientId"}},{"kind":"Field","name":{"kind":"Name","value":"azureApplicationName"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]} as unknown as DocumentNode<DataPlanesQuery, DataPlanesQueryVariables>; export const InviteLinksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"InviteLinks"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"inviteLinks"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"ssoProviderId"}},{"kind":"Field","name":{"kind":"Name","value":"catalogPrefix"}},{"kind":"Field","name":{"kind":"Name","value":"capability"}},{"kind":"Field","name":{"kind":"Name","value":"singleUse"}},{"kind":"Field","name":{"kind":"Name","value":"detail"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PageInfoFields"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PageInfoFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PageInfo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]} as unknown as DocumentNode<InviteLinksQuery, InviteLinksQueryVariables>; diff --git a/src/gql-types/schema.graphql b/src/gql-types/schema.graphql index 350d2a232d..9618f36278 100644 --- a/src/gql-types/schema.graphql +++ b/src/gql-types/schema.graphql @@ -133,38 +133,82 @@ input AlertSubscriptionsBy { } enum AlertType { + """ + Triggers when the automated background discovery process fails. If this + alert is firing, it means that the Capture may be unable to respond to + schema changes in the source system. + """ auto_discover_failed + + """ + Triggers when an automated background process needs to publish a spec, + but is unable to because of publication errors. Background publications + are peformed on all specs for a variety of reasons. For example, + updating inferred schemas, or updating materialization bindings to match + the source capture. When these publications fail, tasks are likely to + stop functioning correctly until the issue can be addressed. + """ background_publication_failed + + """ + Triggers when there has been no data successfully processed by the task during + the configured alert interval. + """ data_movement_stalled + + """ + Triggers automatically for every tenant that begins a free + trial, and resolves when the trial period ends. + """ free_trial + + """Triggers when the free trial is getting close to expiring.""" free_trial_ending + + """ + Triggers after the free trial period has expired, and still no payment info + has been added. + """ free_trial_stalled + + """ + Triggers for any tenants that do not have a payment method, and resolves when + a payment method is added. + """ missing_payment_method - shard_failed - task_auto_disabled_failing - task_auto_disabled_idle - task_chronically_failing - task_idle -} -"""Describes an alert type with user-facing metadata.""" -type AlertTypeInfo { - """The alert type identifier.""" - alertType: AlertType! + """ + Triggers after repeated task failures have been observed. The task may or may not + continue to make progress in between failures, but at a minimum, performance will + be degraded. And in many scenarios, the task will be unable to process data at all. + """ + shard_failed - """A user-facing description of what this alert type means.""" - description: String! + """ + The task was automatically disabled because its shards have been + failing continuously for an extended period without any user intervention. + """ + task_auto_disabled_failing - """A short, user-facing alert type name.""" - displayName: String! + """ + The task was automatically disabled because it had not processed any + data for an extended period and had not been modified recently. + """ + task_auto_disabled_idle - """An indication of whether the alert type is subscribed to by default.""" - isDefault: Boolean! + """ + Warning that a task has been unable to run for an extended period. It will + be automatically disabled unless the issue is addressed or a new version + of the spec is published. + """ + task_chronically_failing """ - An indication of whether the alert type is considered to be a system alert. + Warning that a task has not processed any data for an extended period + and has not been modified recently. It will be automatically disabled + unless a new version of the spec is published. """ - isSystem: Boolean! + task_idle } input AlertsBy { @@ -1030,9 +1074,6 @@ type QueryRoot { """Returns a complete list of alert subscriptions.""" alertSubscriptions(by: AlertSubscriptionsBy!): [AlertSubscription!]! - """Returns all possible alert types with their user-facing metadata.""" - alertTypes: [AlertTypeInfo!]! - """ Returns a list of alerts that are currently active for the given catalog prefixes. From 8b8f4dc91a54c48a460b5198695c3ddf6194a856 Mon Sep 17 00:00:00 2001 From: Travis Jenkins <travis@estuary.dev> Date: Tue, 7 Apr 2026 14:04:01 -0400 Subject: [PATCH 13/30] Removing the old details hydrator for details as only test page used it --- src/pages/dev/TestJsonForms.tsx | 180 ++++++++++------------------ src/stores/DetailsForm/Hydrator.tsx | 52 -------- 2 files changed, 66 insertions(+), 166 deletions(-) delete mode 100644 src/stores/DetailsForm/Hydrator.tsx diff --git a/src/pages/dev/TestJsonForms.tsx b/src/pages/dev/TestJsonForms.tsx index 588c359292..445dcdcf2e 100644 --- a/src/pages/dev/TestJsonForms.tsx +++ b/src/pages/dev/TestJsonForms.tsx @@ -1,9 +1,15 @@ -import { useMemo, useState } from 'react'; +import type { SelectChangeEvent } from '@mui/material'; + +import { useState } from 'react'; import { Box, Button, Divider, + FormControl, + InputLabel, + MenuItem, + Select, Stack, StyledEngineProvider, } from '@mui/material'; @@ -11,7 +17,6 @@ import { import { JsonForms } from '@jsonforms/react'; import Editor from '@monaco-editor/react'; -import { useIntl } from 'react-intl'; import { useUnmount } from 'react-use'; import AlertBox from 'src/components/shared/AlertBox'; @@ -19,76 +24,40 @@ import WrapperWithHeader from 'src/components/shared/Entity/WrapperWithHeader'; import PageContainer from 'src/components/shared/PageContainer'; import { jsonFormsPadding } from 'src/context/Theme'; import { WorkflowContextProvider } from 'src/context/Workflow'; -import { CONNECTOR_IMAGE_SCOPE } from 'src/forms/renderers/Connectors'; import useConnectors from 'src/hooks/connectors/useConnectors'; -import { GlobalSearchParams } from 'src/hooks/searchParams/useGlobalSearchParams'; import { custom_generateDefaultUISchema, getDereffedSchema, } from 'src/services/jsonforms'; import { jsonFormsDefaults } from 'src/services/jsonforms/defaults'; -import { DetailsFormHydrator } from 'src/stores/DetailsForm/Hydrator'; import { useDetailsFormStore } from 'src/stores/DetailsForm/Store'; const TestJsonForms = () => { - const intl = useIntl(); const { connectors } = useConnectors(); + const [connectorId, setConnectorId] = useState(''); const [error, setError] = useState<string | null>(null); const [schemaInput, setSchemaInput] = useState<string | undefined>(''); const [schema, setSchema] = useState<any | null>(null); const [uiSchema, setUiSchema] = useState<any | null>(null); const [formData, setFormData] = useState({}); + const setDetails_connector = useDetailsFormStore( + (state) => state.setDetails_connector + ); + const setHydrated = useDetailsFormStore((state) => state.setHydrated); const resetDetailsForm = useDetailsFormStore((state) => state.resetState); - const connectorsOneOf = useMemo(() => { - const response = [] as { title: string; const: Object }[]; - - if (connectors.length > 0) { - connectors.forEach((connector) => { - response.push({ - const: { id: connector.id }, - title: connector.title['en-US'], - }); - }); - } - - return response; - }, [connectors]); - - const topSchema = useMemo(() => { - return { - properties: { - [CONNECTOR_IMAGE_SCOPE]: { - description: intl.formatMessage({ - id: 'connector.description', - }), - oneOf: connectorsOneOf, - type: 'object', - }, - }, - required: [CONNECTOR_IMAGE_SCOPE], - type: 'object', - }; - }, [connectorsOneOf, intl]); - console.log(connectorsOneOf); - - const topUiSchema = { - elements: [ - { - elements: [ - { - label: intl.formatMessage({ - id: 'entityCreate.connector.label', - }), - scope: `#/properties/${CONNECTOR_IMAGE_SCOPE}`, - type: 'Control', - }, - ], - type: 'HorizontalLayout', - }, - ], - type: 'VerticalLayout', + const applyConnectorId = (id: string) => { + setConnectorId(id); + setDetails_connector({ + id, + iconPath: '', + imageName: '', + imagePath: '', + imageTag: '', + connectorId: id, + }); + setHydrated(true); }; const failed = () => @@ -128,8 +97,6 @@ const TestJsonForms = () => { } }; - const searchParams = new URLSearchParams(window.location.search); - useUnmount(() => { resetDetailsForm(); }); @@ -151,9 +118,10 @@ const TestJsonForms = () => { <AlertBox severity="info" short title="Instructions"> <Box> - 1. Select a connector in the dropdown. This does not - load in anything - just sets a property in the URL - and sets some stuff behind the scenes. + 1. TESTING OAUTH - Select a connector in the + dropdown. This will be the connector that is looked + up in the DB for the <code>authURL</code> and{' '} + <code>accessToken</code>. </Box> <Box> 2. Paste a JSONSchema into the text area below and @@ -170,39 +138,25 @@ const TestJsonForms = () => { </Box> </AlertBox> - <JsonForms - {...jsonFormsDefaults} - schema={topSchema} - uischema={topUiSchema} - data={{ - connectorImage: { - id: searchParams.get( - GlobalSearchParams.CONNECTOR_ID - ), - }, - }} - validationMode="ValidateAndShow" - onChange={(state) => { - console.log( - 'This is the new state of the form', - state - ); - const connectorId = state.data?.connectorImage?.id; - if ( - connectorId && - searchParams.get( - GlobalSearchParams.CONNECTOR_ID - ) !== connectorId - ) { - searchParams.set( - GlobalSearchParams.CONNECTOR_ID, - connectorId - ); - window.location.search = - searchParams.toString(); - } - }} - /> + <FormControl fullWidth> + <InputLabel>Connector</InputLabel> + <Select + label="Connector" + value={connectorId} + onChange={(e: SelectChangeEvent) => { + applyConnectorId(e.target.value); + }} + > + {connectors.map((connector) => ( + <MenuItem + key={connector.id} + value={connector.id} + > + {connector.title['en-US']} ({connector.id}) + </MenuItem> + ))} + </Select> + </FormControl> <Editor height="500px" @@ -223,28 +177,26 @@ const TestJsonForms = () => { ...jsonFormsPadding, }} > - <DetailsFormHydrator> - {schema !== null && uiSchema !== null ? ( - <JsonForms - {...jsonFormsDefaults} - schema={schema} - uischema={uiSchema} - data={formData} - validationMode="ValidateAndShow" - onChange={(state) => { - console.log( - 'This is the new state of the form', - state - ); - }} - /> - ) : ( - <> - To render form enter a schema above and - click the Render button - </> - )} - </DetailsFormHydrator> + {schema !== null && uiSchema !== null ? ( + <JsonForms + {...jsonFormsDefaults} + schema={schema} + uischema={uiSchema} + data={formData} + validationMode="ValidateAndShow" + onChange={(state) => { + console.log( + 'This is the new state of the form', + state + ); + }} + /> + ) : ( + <> + To render form enter a schema above and + click the Render button + </> + )} </Box> </StyledEngineProvider> </WrapperWithHeader> diff --git a/src/stores/DetailsForm/Hydrator.tsx b/src/stores/DetailsForm/Hydrator.tsx deleted file mode 100644 index c0570e49ae..0000000000 --- a/src/stores/DetailsForm/Hydrator.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import type { BaseComponentProps } from 'src/types'; - -import { useEffectOnce } from 'react-use'; - -import { useEntityType } from 'src/context/EntityContext'; -import { useEntityWorkflow } from 'src/context/Workflow'; -import { logRocketConsole } from 'src/services/shared'; -import { useDetailsFormStore } from 'src/stores/DetailsForm/Store'; - -// TODO: Remove details form store hydrator and hydrateState action. -// It is only used by the test JSON forms page. -export const DetailsFormHydrator = ({ children }: BaseComponentProps) => { - const entityType = useEntityType(); - const workflow = useEntityWorkflow(); - - const hydrated = useDetailsFormStore((state) => state.hydrated); - const setHydrated = useDetailsFormStore((state) => state.setHydrated); - const setActive = useDetailsFormStore((state) => state.setActive); - const [setHydrationErrorsExist, dataPlaneOptions] = useDetailsFormStore( - (state) => [state.setHydrationErrorsExist, state.dataPlaneOptions] - ); - - const hydrateState = useDetailsFormStore((state) => state.hydrateState); - - useEffectOnce(() => { - if ( - !hydrated && - (entityType === 'capture' || entityType === 'materialization') - ) { - setActive(true); - hydrateState(workflow, dataPlaneOptions).then( - () => { - setHydrated(true); - }, - (error) => { - setHydrated(true); - setHydrationErrorsExist(true); - - logRocketConsole('Failed to hydrate details form', error); - } - ); - } - }); - - // Until details is hydrated we should wait to load in the other hydrator children - if (!hydrated) { - return null; - } - - // eslint-disable-next-line react/jsx-no-useless-fragment - return <>{children}</>; -}; From 3621d534d9275d380ebaaa5898c6d3a83813cd08 Mon Sep 17 00:00:00 2001 From: Travis Jenkins <travis@estuary.dev> Date: Tue, 7 Apr 2026 14:14:10 -0400 Subject: [PATCH 14/30] Cleaning up a lot of code just for the test json forms page --- src/hooks/connectors/shared.ts | 12 ----------- src/hooks/connectors/useConnectors.ts | 21 ------------------ src/pages/dev/TestJsonForms.tsx | 31 ++++++++++++++++++++++++--- 3 files changed, 28 insertions(+), 36 deletions(-) delete mode 100644 src/hooks/connectors/shared.ts delete mode 100644 src/hooks/connectors/useConnectors.ts diff --git a/src/hooks/connectors/shared.ts b/src/hooks/connectors/shared.ts deleted file mode 100644 index 65c2a7f20e..0000000000 --- a/src/hooks/connectors/shared.ts +++ /dev/null @@ -1,12 +0,0 @@ -////////////////////////// -// useConnectors -////////////////////////// -export interface Connector { - id: string; - title: { 'en-US': string }; - image_name: string; -} - -export const CONNECTOR_QUERY = ` - id, title, image_name -`; diff --git a/src/hooks/connectors/useConnectors.ts b/src/hooks/connectors/useConnectors.ts deleted file mode 100644 index 175764371a..0000000000 --- a/src/hooks/connectors/useConnectors.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { useQuery } from '@supabase-cache-helpers/postgrest-swr'; - -import { supabaseClient } from 'src/context/GlobalProviders'; -import { CONNECTOR_QUERY } from 'src/hooks/connectors/shared'; -import { TABLES } from 'src/services/supabase'; - -// A hook for fetching connectors directory from -// their own table, without any association with -// connector_tags. Made for the jsonForms test page (as of Q2 2023) -function useConnectors() { - const { data, error } = useQuery( - supabaseClient.from(TABLES.CONNECTORS).select(CONNECTOR_QUERY) - ); - - return { - connectors: data ?? [], - error, - }; -} - -export default useConnectors; diff --git a/src/pages/dev/TestJsonForms.tsx b/src/pages/dev/TestJsonForms.tsx index 445dcdcf2e..5758f28a85 100644 --- a/src/pages/dev/TestJsonForms.tsx +++ b/src/pages/dev/TestJsonForms.tsx @@ -12,28 +12,35 @@ import { Select, Stack, StyledEngineProvider, + Typography, } from '@mui/material'; import { JsonForms } from '@jsonforms/react'; import Editor from '@monaco-editor/react'; +import { useQuery } from '@supabase-cache-helpers/postgrest-swr'; import { useUnmount } from 'react-use'; import AlertBox from 'src/components/shared/AlertBox'; import WrapperWithHeader from 'src/components/shared/Entity/WrapperWithHeader'; import PageContainer from 'src/components/shared/PageContainer'; +import { supabaseClient } from 'src/context/GlobalProviders'; import { jsonFormsPadding } from 'src/context/Theme'; import { WorkflowContextProvider } from 'src/context/Workflow'; -import useConnectors from 'src/hooks/connectors/useConnectors'; import { custom_generateDefaultUISchema, getDereffedSchema, } from 'src/services/jsonforms'; import { jsonFormsDefaults } from 'src/services/jsonforms/defaults'; +import { TABLES } from 'src/services/supabase'; import { useDetailsFormStore } from 'src/stores/DetailsForm/Store'; const TestJsonForms = () => { - const { connectors } = useConnectors(); + const { data } = useQuery( + supabaseClient.from(TABLES.CONNECTORS).select(`id, title, image_name`) + ); + const connectors = data ?? []; + const [connectorId, setConnectorId] = useState(''); const [error, setError] = useState<string | null>(null); const [schemaInput, setSchemaInput] = useState<string | undefined>(''); @@ -152,7 +159,25 @@ const TestJsonForms = () => { key={connector.id} value={connector.id} > - {connector.title['en-US']} ({connector.id}) + <Stack direction="row" spacing={1}> + <Typography fontWeight="700"> + {connector.title['en-US']} + </Typography> + <Divider + orientation="vertical" + flexItem + /> + <Typography> + {connector.image_name} + </Typography> + <Divider + orientation="vertical" + flexItem + /> + <Typography> + ({connector.id}) + </Typography> + </Stack> </MenuItem> ))} </Select> From a93da555f35ac64ad05b103f48198726c087d8c0 Mon Sep 17 00:00:00 2001 From: Travis Jenkins <travis@estuary.dev> Date: Tue, 7 Apr 2026 14:20:20 -0400 Subject: [PATCH 15/30] Adding a bit more error handling --- src/pages/dev/TestJsonForms.tsx | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/pages/dev/TestJsonForms.tsx b/src/pages/dev/TestJsonForms.tsx index 5758f28a85..0ca1785ba9 100644 --- a/src/pages/dev/TestJsonForms.tsx +++ b/src/pages/dev/TestJsonForms.tsx @@ -36,7 +36,7 @@ import { TABLES } from 'src/services/supabase'; import { useDetailsFormStore } from 'src/stores/DetailsForm/Store'; const TestJsonForms = () => { - const { data } = useQuery( + const { data, error: serverError } = useQuery( supabaseClient.from(TABLES.CONNECTORS).select(`id, title, image_name`) ); const connectors = data ?? []; @@ -68,9 +68,7 @@ const TestJsonForms = () => { }; const failed = () => - setError( - 'Failed to parse input. Make sure it is valid JSON and then click button again' - ); + setError('Make sure it is valid JSON and then click `Render` again'); const parseSchema = async () => { if (!schemaInput) { @@ -81,6 +79,7 @@ const TestJsonForms = () => { try { setFormData({}); setSchema(null); + setError(null); const parsedSchema = JSON.parse(schemaInput); const resolved = await getDereffedSchema(parsedSchema); @@ -117,8 +116,22 @@ const TestJsonForms = () => { justifyContent: 'center', }} > + {serverError ? ( + <AlertBox + short + severity="error" + title="Failed to fetch list of connectors" + > + {serverError.message} + </AlertBox> + ) : null} + {error !== null ? ( - <AlertBox short={false} severity="error"> + <AlertBox + short + severity="error" + title="Failed to parse input" + > {error} </AlertBox> ) : null} From b00508f7e0f50c3bdfb5af45d3a14aeca591a421 Mon Sep 17 00:00:00 2001 From: Travis Jenkins <travis@estuary.dev> Date: Tue, 7 Apr 2026 16:11:56 -0400 Subject: [PATCH 16/30] Updating query so we fetch the image tag if available --- src/api/gql/connectors.ts | 6 +- src/api/liveSpecsExt.ts | 3 +- .../Entity/Edit/useInitializeTaskDraft.ts | 2 + .../Entity/hooks/useEntityEditNavigate.ts | 1 + src/context/ConnectorTag.tsx | 7 +- src/gql-types/gql.ts | 6 +- src/gql-types/graphql.ts | 75 +++++------------ src/gql-types/schema.graphql | 83 +++++-------------- .../searchParams/useGlobalSearchParams.ts | 1 + 9 files changed, 57 insertions(+), 127 deletions(-) diff --git a/src/api/gql/connectors.ts b/src/api/gql/connectors.ts index 9b7c344378..f4973d32a8 100644 --- a/src/api/gql/connectors.ts +++ b/src/api/gql/connectors.ts @@ -37,15 +37,13 @@ export const CONNECTORS_QUERY = graphql(` `); export const CONNECTOR_BY_ID_QUERY = graphql(` - query SingleConnector($id: Id!) { + query SingleConnector($id: Id!, $imageTag: String) { connector(id: $id) { id imageName logoUrl title - recommended - shortDescription - connectorTag(orDefault: true) { + connectorTag(imageTag: $imageTag, orDefault: true) { id connectorId imageTag diff --git a/src/api/liveSpecsExt.ts b/src/api/liveSpecsExt.ts index 5b9d267660..5cc930cd8e 100644 --- a/src/api/liveSpecsExt.ts +++ b/src/api/liveSpecsExt.ts @@ -422,6 +422,7 @@ export interface LiveSpecsExtQuery_ByLiveSpecId { last_pub_id: string; spec: any; connector_id: string; + connector_image_tag: string; } const getLiveSpecsByLiveSpecId = async (liveSpecId: string) => { @@ -430,7 +431,7 @@ const getLiveSpecsByLiveSpecId = async (liveSpecId: string) => { supabaseClient .from(TABLES.LIVE_SPECS_EXT) .select( - 'built_spec,catalog_name,id,spec_type,last_pub_id,spec,connector_id' + 'built_spec,catalog_name,id,spec_type,last_pub_id,spec,connector_id,connector_image_tag' ) .eq('id', liveSpecId), 'getLiveSpecsByLiveSpecId' diff --git a/src/components/shared/Entity/Edit/useInitializeTaskDraft.ts b/src/components/shared/Entity/Edit/useInitializeTaskDraft.ts index 1824937456..f72619c3d2 100644 --- a/src/components/shared/Entity/Edit/useInitializeTaskDraft.ts +++ b/src/components/shared/Entity/Edit/useInitializeTaskDraft.ts @@ -256,6 +256,8 @@ function useInitializeTaskDraft() { [GlobalSearchParams.LIVE_SPEC_ID]: liveSpecId, [GlobalSearchParams.CONNECTOR_ID]: formatOldUuidToGql(task.connector_id), + [GlobalSearchParams.CONNECTOR_IMAGE_TAG]: + task.connector_image_tag, [GlobalSearchParams.LAST_PUB_ID]: task.last_pub_id, }, diff --git a/src/components/shared/Entity/hooks/useEntityEditNavigate.ts b/src/components/shared/Entity/hooks/useEntityEditNavigate.ts index cb55174f04..aba332866c 100644 --- a/src/components/shared/Entity/hooks/useEntityEditNavigate.ts +++ b/src/components/shared/Entity/hooks/useEntityEditNavigate.ts @@ -12,6 +12,7 @@ import { getPathWithParams } from 'src/utils/misc-utils'; interface BaseSearchParams { [GlobalSearchParams.CONNECTOR_ID]: string; + [GlobalSearchParams.CONNECTOR_IMAGE_TAG]: string; [GlobalSearchParams.LIVE_SPEC_ID]: string; [GlobalSearchParams.LAST_PUB_ID]: string; } diff --git a/src/context/ConnectorTag.tsx b/src/context/ConnectorTag.tsx index bd14659c16..0c81869954 100644 --- a/src/context/ConnectorTag.tsx +++ b/src/context/ConnectorTag.tsx @@ -28,6 +28,9 @@ const ConnectorTagContext = createContext<ConnectorTagData | null>(null); export const ConnectorTagProvider = ({ children }: BaseComponentProps) => { const connectorId = useGlobalSearchParams(GlobalSearchParams.CONNECTOR_ID); + const imageTag = useGlobalSearchParams( + GlobalSearchParams.CONNECTOR_IMAGE_TAG + ); const client = useClient(); const intl = useIntl(); @@ -44,7 +47,7 @@ export const ConnectorTagProvider = ({ children }: BaseComponentProps) => { client .query( CONNECTOR_BY_ID_QUERY, - { id: connectorId }, + { id: connectorId, imageTag }, { requestPolicy: 'network-only' } ) .toPromise() @@ -68,7 +71,7 @@ export const ConnectorTagProvider = ({ children }: BaseComponentProps) => { }, }); }); - }, [client, connectorId]); + }, [client, connectorId, imageTag]); if (fetchError) { return ( diff --git a/src/gql-types/gql.ts b/src/gql-types/gql.ts index 33ad1e0e0e..9a7ef030f8 100644 --- a/src/gql-types/gql.ts +++ b/src/gql-types/gql.ts @@ -15,7 +15,7 @@ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/ */ type Documents = { "\n query ConnectorsGrid($filter: ConnectorsFilter, $after: String) {\n connectors(first: 500, after: $after, filter: $filter) {\n edges {\n cursor\n node {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n documentationUrl\n protocol\n }\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n": typeof types.ConnectorsGridDocument, - "\n query SingleConnector($id: Id!) {\n connector(id: $id) {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n defaultCaptureInterval\n disableBackfill\n documentationUrl\n endpointSpecSchema\n resourceSpecSchema\n protocol\n }\n }\n }\n": typeof types.SingleConnectorDocument, + "\n query SingleConnector($id: Id!, $imageTag: String) {\n connector(id: $id) {\n id\n imageName\n logoUrl\n title\n connectorTag(imageTag: $imageTag, orDefault: true) {\n id\n connectorId\n imageTag\n defaultCaptureInterval\n disableBackfill\n documentationUrl\n endpointSpecSchema\n resourceSpecSchema\n protocol\n }\n }\n }\n": typeof types.SingleConnectorDocument, "\n query DataPlanes($after: String) {\n dataPlanes(first: 100, after: $after) {\n edges {\n node {\n name\n cloudProvider\n region\n isPublic\n fqdn\n cidrBlocks\n awsIamUserArn\n gcpServiceAccountEmail\n azureApplicationClientId\n azureApplicationName\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n": typeof types.DataPlanesDocument, "\n query InviteLinks($first: Int, $after: String) {\n inviteLinks(first: $first, after: $after) {\n edges {\n node {\n token\n ssoProviderId\n catalogPrefix\n capability\n singleUse\n detail\n createdAt\n }\n cursor\n }\n pageInfo {\n ...PageInfoFields\n }\n }\n }\n": typeof types.InviteLinksDocument, "\n mutation CreateInviteLink(\n $catalogPrefix: Prefix!\n $capability: Capability!\n $singleUse: Boolean!\n $detail: String\n ) {\n createInviteLink(\n catalogPrefix: $catalogPrefix\n capability: $capability\n singleUse: $singleUse\n detail: $detail\n ) {\n token\n catalogPrefix\n capability\n singleUse\n detail\n createdAt\n }\n }\n": typeof types.CreateInviteLinkDocument, @@ -35,7 +35,7 @@ type Documents = { }; const documents: Documents = { "\n query ConnectorsGrid($filter: ConnectorsFilter, $after: String) {\n connectors(first: 500, after: $after, filter: $filter) {\n edges {\n cursor\n node {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n documentationUrl\n protocol\n }\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n": types.ConnectorsGridDocument, - "\n query SingleConnector($id: Id!) {\n connector(id: $id) {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n defaultCaptureInterval\n disableBackfill\n documentationUrl\n endpointSpecSchema\n resourceSpecSchema\n protocol\n }\n }\n }\n": types.SingleConnectorDocument, + "\n query SingleConnector($id: Id!, $imageTag: String) {\n connector(id: $id) {\n id\n imageName\n logoUrl\n title\n connectorTag(imageTag: $imageTag, orDefault: true) {\n id\n connectorId\n imageTag\n defaultCaptureInterval\n disableBackfill\n documentationUrl\n endpointSpecSchema\n resourceSpecSchema\n protocol\n }\n }\n }\n": types.SingleConnectorDocument, "\n query DataPlanes($after: String) {\n dataPlanes(first: 100, after: $after) {\n edges {\n node {\n name\n cloudProvider\n region\n isPublic\n fqdn\n cidrBlocks\n awsIamUserArn\n gcpServiceAccountEmail\n azureApplicationClientId\n azureApplicationName\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n": types.DataPlanesDocument, "\n query InviteLinks($first: Int, $after: String) {\n inviteLinks(first: $first, after: $after) {\n edges {\n node {\n token\n ssoProviderId\n catalogPrefix\n capability\n singleUse\n detail\n createdAt\n }\n cursor\n }\n pageInfo {\n ...PageInfoFields\n }\n }\n }\n": types.InviteLinksDocument, "\n mutation CreateInviteLink(\n $catalogPrefix: Prefix!\n $capability: Capability!\n $singleUse: Boolean!\n $detail: String\n ) {\n createInviteLink(\n catalogPrefix: $catalogPrefix\n capability: $capability\n singleUse: $singleUse\n detail: $detail\n ) {\n token\n catalogPrefix\n capability\n singleUse\n detail\n createdAt\n }\n }\n": types.CreateInviteLinkDocument, @@ -75,7 +75,7 @@ export function graphql(source: "\n query ConnectorsGrid($filter: ConnectorsF /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n query SingleConnector($id: Id!) {\n connector(id: $id) {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n defaultCaptureInterval\n disableBackfill\n documentationUrl\n endpointSpecSchema\n resourceSpecSchema\n protocol\n }\n }\n }\n"): (typeof documents)["\n query SingleConnector($id: Id!) {\n connector(id: $id) {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n defaultCaptureInterval\n disableBackfill\n documentationUrl\n endpointSpecSchema\n resourceSpecSchema\n protocol\n }\n }\n }\n"]; +export function graphql(source: "\n query SingleConnector($id: Id!, $imageTag: String) {\n connector(id: $id) {\n id\n imageName\n logoUrl\n title\n connectorTag(imageTag: $imageTag, orDefault: true) {\n id\n connectorId\n imageTag\n defaultCaptureInterval\n disableBackfill\n documentationUrl\n endpointSpecSchema\n resourceSpecSchema\n protocol\n }\n }\n }\n"): (typeof documents)["\n query SingleConnector($id: Id!, $imageTag: String) {\n connector(id: $id) {\n id\n imageName\n logoUrl\n title\n connectorTag(imageTag: $imageTag, orDefault: true) {\n id\n connectorId\n imageTag\n defaultCaptureInterval\n disableBackfill\n documentationUrl\n endpointSpecSchema\n resourceSpecSchema\n protocol\n }\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/src/gql-types/graphql.ts b/src/gql-types/graphql.ts index 7924512cba..5839ef6019 100644 --- a/src/gql-types/graphql.ts +++ b/src/gql-types/graphql.ts @@ -161,72 +161,34 @@ export type AlertSubscriptionsBy = { }; export type AlertType = - /** - * Triggers when the automated background discovery process fails. If this - * alert is firing, it means that the Capture may be unable to respond to - * schema changes in the source system. - */ | 'auto_discover_failed' - /** - * Triggers when an automated background process needs to publish a spec, - * but is unable to because of publication errors. Background publications - * are peformed on all specs for a variety of reasons. For example, - * updating inferred schemas, or updating materialization bindings to match - * the source capture. When these publications fail, tasks are likely to - * stop functioning correctly until the issue can be addressed. - */ | 'background_publication_failed' - /** - * Triggers when there has been no data successfully processed by the task during - * the configured alert interval. - */ | 'data_movement_stalled' - /** - * Triggers automatically for every tenant that begins a free - * trial, and resolves when the trial period ends. - */ | 'free_trial' - /** Triggers when the free trial is getting close to expiring. */ | 'free_trial_ending' - /** - * Triggers after the free trial period has expired, and still no payment info - * has been added. - */ | 'free_trial_stalled' - /** - * Triggers for any tenants that do not have a payment method, and resolves when - * a payment method is added. - */ | 'missing_payment_method' - /** - * Triggers after repeated task failures have been observed. The task may or may not - * continue to make progress in between failures, but at a minimum, performance will - * be degraded. And in many scenarios, the task will be unable to process data at all. - */ | 'shard_failed' - /** - * The task was automatically disabled because its shards have been - * failing continuously for an extended period without any user intervention. - */ | 'task_auto_disabled_failing' - /** - * The task was automatically disabled because it had not processed any - * data for an extended period and had not been modified recently. - */ | 'task_auto_disabled_idle' - /** - * Warning that a task has been unable to run for an extended period. It will - * be automatically disabled unless the issue is addressed or a new version - * of the spec is published. - */ | 'task_chronically_failing' - /** - * Warning that a task has not processed any data for an extended period - * and has not been modified recently. It will be automatically disabled - * unless a new version of the spec is published. - */ | 'task_idle'; +/** Describes an alert type with user-facing metadata. */ +export type AlertTypeInfo = { + __typename?: 'AlertTypeInfo'; + /** The alert type identifier. */ + alertType: AlertType; + /** A user-facing description of what this alert type means. */ + description: Scalars['String']['output']; + /** A short, user-facing alert type name. */ + displayName: Scalars['String']['output']; + /** An indication of whether the alert type is subscribed to by default. */ + isDefault: Scalars['Boolean']['output']; + /** An indication of whether the alert type is considered to be a system alert. */ + isSystem: Scalars['Boolean']['output']; +}; + export type AlertsBy = { /** * Optionally filter alerts by active status. If unspecified, both active @@ -1062,6 +1024,8 @@ export type QueryRoot = { __typename?: 'QueryRoot'; /** Returns a complete list of alert subscriptions. */ alertSubscriptions: Array<AlertSubscription>; + /** Returns all possible alert types with their user-facing metadata. */ + alertTypes: Array<AlertTypeInfo>; /** * Returns a list of alerts that are currently active for the given catalog * prefixes. @@ -1503,10 +1467,11 @@ export type ConnectorsGridQuery = { __typename?: 'QueryRoot', connectors: { __ty export type SingleConnectorQueryVariables = Exact<{ id: Scalars['Id']['input']; + imageTag?: InputMaybe<Scalars['String']['input']>; }>; -export type SingleConnectorQuery = { __typename?: 'QueryRoot', connector?: { __typename?: 'Connector', id: any, imageName: string, logoUrl?: string | null, title?: string | null, recommended: boolean, shortDescription?: string | null, connectorTag?: { __typename?: 'ConnectorTag', id: any, connectorId: any, imageTag: string, defaultCaptureInterval?: string | null, disableBackfill: boolean, documentationUrl?: string | null, endpointSpecSchema?: any | null, resourceSpecSchema?: any | null, protocol?: ConnectorProto | null } | null } | null }; +export type SingleConnectorQuery = { __typename?: 'QueryRoot', connector?: { __typename?: 'Connector', id: any, imageName: string, logoUrl?: string | null, title?: string | null, connectorTag?: { __typename?: 'ConnectorTag', id: any, connectorId: any, imageTag: string, defaultCaptureInterval?: string | null, disableBackfill: boolean, documentationUrl?: string | null, endpointSpecSchema?: any | null, resourceSpecSchema?: any | null, protocol?: ConnectorProto | null } | null } | null }; export type DataPlanesQueryVariables = Exact<{ after?: InputMaybe<Scalars['String']['input']>; @@ -1630,7 +1595,7 @@ export type AuthRolesQueryQuery = { __typename?: 'QueryRoot', prefixes: { __type export const PageInfoFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PageInfoFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PageInfo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]} as unknown as DocumentNode<PageInfoFieldsFragment, unknown>; export const ConnectorsGridDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ConnectorsGrid"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ConnectorsFilter"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"connectors"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"500"}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"imageName"}},{"kind":"Field","name":{"kind":"Name","value":"logoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"recommended"}},{"kind":"Field","name":{"kind":"Name","value":"shortDescription"}},{"kind":"Field","name":{"kind":"Name","value":"connectorTag"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orDefault"},"value":{"kind":"BooleanValue","value":true}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"connectorId"}},{"kind":"Field","name":{"kind":"Name","value":"imageTag"}},{"kind":"Field","name":{"kind":"Name","value":"documentationUrl"}},{"kind":"Field","name":{"kind":"Name","value":"protocol"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]} as unknown as DocumentNode<ConnectorsGridQuery, ConnectorsGridQueryVariables>; -export const SingleConnectorDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"SingleConnector"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Id"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"connector"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"imageName"}},{"kind":"Field","name":{"kind":"Name","value":"logoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"recommended"}},{"kind":"Field","name":{"kind":"Name","value":"shortDescription"}},{"kind":"Field","name":{"kind":"Name","value":"connectorTag"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orDefault"},"value":{"kind":"BooleanValue","value":true}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"connectorId"}},{"kind":"Field","name":{"kind":"Name","value":"imageTag"}},{"kind":"Field","name":{"kind":"Name","value":"defaultCaptureInterval"}},{"kind":"Field","name":{"kind":"Name","value":"disableBackfill"}},{"kind":"Field","name":{"kind":"Name","value":"documentationUrl"}},{"kind":"Field","name":{"kind":"Name","value":"endpointSpecSchema"}},{"kind":"Field","name":{"kind":"Name","value":"resourceSpecSchema"}},{"kind":"Field","name":{"kind":"Name","value":"protocol"}}]}}]}}]}}]} as unknown as DocumentNode<SingleConnectorQuery, SingleConnectorQueryVariables>; +export const SingleConnectorDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"SingleConnector"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Id"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"imageTag"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"connector"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"imageName"}},{"kind":"Field","name":{"kind":"Name","value":"logoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"connectorTag"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"imageTag"},"value":{"kind":"Variable","name":{"kind":"Name","value":"imageTag"}}},{"kind":"Argument","name":{"kind":"Name","value":"orDefault"},"value":{"kind":"BooleanValue","value":true}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"connectorId"}},{"kind":"Field","name":{"kind":"Name","value":"imageTag"}},{"kind":"Field","name":{"kind":"Name","value":"defaultCaptureInterval"}},{"kind":"Field","name":{"kind":"Name","value":"disableBackfill"}},{"kind":"Field","name":{"kind":"Name","value":"documentationUrl"}},{"kind":"Field","name":{"kind":"Name","value":"endpointSpecSchema"}},{"kind":"Field","name":{"kind":"Name","value":"resourceSpecSchema"}},{"kind":"Field","name":{"kind":"Name","value":"protocol"}}]}}]}}]}}]} as unknown as DocumentNode<SingleConnectorQuery, SingleConnectorQueryVariables>; export const DataPlanesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"DataPlanes"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"dataPlanes"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"100"}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"cloudProvider"}},{"kind":"Field","name":{"kind":"Name","value":"region"}},{"kind":"Field","name":{"kind":"Name","value":"isPublic"}},{"kind":"Field","name":{"kind":"Name","value":"fqdn"}},{"kind":"Field","name":{"kind":"Name","value":"cidrBlocks"}},{"kind":"Field","name":{"kind":"Name","value":"awsIamUserArn"}},{"kind":"Field","name":{"kind":"Name","value":"gcpServiceAccountEmail"}},{"kind":"Field","name":{"kind":"Name","value":"azureApplicationClientId"}},{"kind":"Field","name":{"kind":"Name","value":"azureApplicationName"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]} as unknown as DocumentNode<DataPlanesQuery, DataPlanesQueryVariables>; export const InviteLinksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"InviteLinks"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"inviteLinks"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"ssoProviderId"}},{"kind":"Field","name":{"kind":"Name","value":"catalogPrefix"}},{"kind":"Field","name":{"kind":"Name","value":"capability"}},{"kind":"Field","name":{"kind":"Name","value":"singleUse"}},{"kind":"Field","name":{"kind":"Name","value":"detail"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PageInfoFields"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PageInfoFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PageInfo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]} as unknown as DocumentNode<InviteLinksQuery, InviteLinksQueryVariables>; export const CreateInviteLinkDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateInviteLink"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"catalogPrefix"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Prefix"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"capability"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Capability"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"singleUse"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"detail"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createInviteLink"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"catalogPrefix"},"value":{"kind":"Variable","name":{"kind":"Name","value":"catalogPrefix"}}},{"kind":"Argument","name":{"kind":"Name","value":"capability"},"value":{"kind":"Variable","name":{"kind":"Name","value":"capability"}}},{"kind":"Argument","name":{"kind":"Name","value":"singleUse"},"value":{"kind":"Variable","name":{"kind":"Name","value":"singleUse"}}},{"kind":"Argument","name":{"kind":"Name","value":"detail"},"value":{"kind":"Variable","name":{"kind":"Name","value":"detail"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"catalogPrefix"}},{"kind":"Field","name":{"kind":"Name","value":"capability"}},{"kind":"Field","name":{"kind":"Name","value":"singleUse"}},{"kind":"Field","name":{"kind":"Name","value":"detail"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]} as unknown as DocumentNode<CreateInviteLinkMutation, CreateInviteLinkMutationVariables>; diff --git a/src/gql-types/schema.graphql b/src/gql-types/schema.graphql index 9618f36278..350d2a232d 100644 --- a/src/gql-types/schema.graphql +++ b/src/gql-types/schema.graphql @@ -133,82 +133,38 @@ input AlertSubscriptionsBy { } enum AlertType { - """ - Triggers when the automated background discovery process fails. If this - alert is firing, it means that the Capture may be unable to respond to - schema changes in the source system. - """ auto_discover_failed - - """ - Triggers when an automated background process needs to publish a spec, - but is unable to because of publication errors. Background publications - are peformed on all specs for a variety of reasons. For example, - updating inferred schemas, or updating materialization bindings to match - the source capture. When these publications fail, tasks are likely to - stop functioning correctly until the issue can be addressed. - """ background_publication_failed - - """ - Triggers when there has been no data successfully processed by the task during - the configured alert interval. - """ data_movement_stalled - - """ - Triggers automatically for every tenant that begins a free - trial, and resolves when the trial period ends. - """ free_trial - - """Triggers when the free trial is getting close to expiring.""" free_trial_ending - - """ - Triggers after the free trial period has expired, and still no payment info - has been added. - """ free_trial_stalled - - """ - Triggers for any tenants that do not have a payment method, and resolves when - a payment method is added. - """ missing_payment_method - - """ - Triggers after repeated task failures have been observed. The task may or may not - continue to make progress in between failures, but at a minimum, performance will - be degraded. And in many scenarios, the task will be unable to process data at all. - """ shard_failed - - """ - The task was automatically disabled because its shards have been - failing continuously for an extended period without any user intervention. - """ task_auto_disabled_failing - - """ - The task was automatically disabled because it had not processed any - data for an extended period and had not been modified recently. - """ task_auto_disabled_idle - - """ - Warning that a task has been unable to run for an extended period. It will - be automatically disabled unless the issue is addressed or a new version - of the spec is published. - """ task_chronically_failing + task_idle +} + +"""Describes an alert type with user-facing metadata.""" +type AlertTypeInfo { + """The alert type identifier.""" + alertType: AlertType! + + """A user-facing description of what this alert type means.""" + description: String! + + """A short, user-facing alert type name.""" + displayName: String! + + """An indication of whether the alert type is subscribed to by default.""" + isDefault: Boolean! """ - Warning that a task has not processed any data for an extended period - and has not been modified recently. It will be automatically disabled - unless a new version of the spec is published. + An indication of whether the alert type is considered to be a system alert. """ - task_idle + isSystem: Boolean! } input AlertsBy { @@ -1074,6 +1030,9 @@ type QueryRoot { """Returns a complete list of alert subscriptions.""" alertSubscriptions(by: AlertSubscriptionsBy!): [AlertSubscription!]! + """Returns all possible alert types with their user-facing metadata.""" + alertTypes: [AlertTypeInfo!]! + """ Returns a list of alerts that are currently active for the given catalog prefixes. diff --git a/src/hooks/searchParams/useGlobalSearchParams.ts b/src/hooks/searchParams/useGlobalSearchParams.ts index e7e00e942b..d81404e784 100644 --- a/src/hooks/searchParams/useGlobalSearchParams.ts +++ b/src/hooks/searchParams/useGlobalSearchParams.ts @@ -5,6 +5,7 @@ import { useSearchParams } from 'react-router-dom'; export enum GlobalSearchParams { CATALOG_NAME = 'catalogName', CONNECTOR_ID = 'connectorId', + CONNECTOR_IMAGE_TAG = 'connectorImageTag', DATA_PLANE_ID = 'dataPlaneId', DIFF_VIEW_ORIGINAL = 'diff_o', DIFF_VIEW_MODIFIED = 'diff_m', From 326faa2b53c7b9877d355f9690088ebdc355de5a Mon Sep 17 00:00:00 2001 From: Travis Jenkins <travis@estuary.dev> Date: Tue, 7 Apr 2026 16:29:23 -0400 Subject: [PATCH 17/30] I think we are good to remove this - gonna test it out --- src/stores/DetailsForm/Store.ts | 344 +++++++++++++++----------------- 1 file changed, 166 insertions(+), 178 deletions(-) diff --git a/src/stores/DetailsForm/Store.ts b/src/stores/DetailsForm/Store.ts index 69785ae6fe..e7bcacc020 100644 --- a/src/stores/DetailsForm/Store.ts +++ b/src/stores/DetailsForm/Store.ts @@ -1,9 +1,4 @@ -import type { - DataPlaneOption, - Details, - DetailsFormState, -} from 'src/stores/DetailsForm/types'; -import type { ConnectorVersionEvaluationOptions } from 'src/utils/connector-utils'; +import type { DetailsFormState } from 'src/stores/DetailsForm/types'; import type { StoreApi } from 'zustand'; import type { NamedSet } from 'zustand/middleware'; @@ -13,12 +8,6 @@ import { devtools } from 'zustand/middleware'; import produce from 'immer'; import { isEmpty } from 'lodash'; -import { getConnectors_detailsFormTestPage } from 'src/api/connectors'; -import { getLiveSpecs_detailsForm } from 'src/api/liveSpecsExt'; -import { GlobalSearchParams } from 'src/hooks/searchParams/useGlobalSearchParams'; -import { logRocketEvent } from 'src/services/shared'; -import { CustomEvents } from 'src/services/types'; -import { DATA_PLANE_SETTINGS } from 'src/settings/dataPlanes'; import { initialDetails } from 'src/stores/DetailsForm/shared'; import { fetchErrors, @@ -31,79 +20,77 @@ import { getInitialHydrationData, getStoreWithHydrationSettings, } from 'src/stores/extensions/Hydration'; -import { getConnectorMetadata } from 'src/utils/connector-utils'; -import { defaultDataPlaneSuffix } from 'src/utils/env-utils'; import { hasLength } from 'src/utils/misc-utils'; import { devtoolsOptions } from 'src/utils/store-utils'; import { NAME_RE } from 'src/validation'; const STORE_KEY = 'Details Form'; -const getConnectorImage = async ( - connectorId: string, - existingImageTag?: ConnectorVersionEvaluationOptions['existingImageTag'] -): Promise<Details['data']['connectorImage'] | null> => { - const { data, error } = - await getConnectors_detailsFormTestPage(connectorId); - - if (!error && data && data.length > 0) { - const connector = data[0]; - - const options: ConnectorVersionEvaluationOptions | undefined = - existingImageTag ? { connectorId, existingImageTag } : undefined; - - return getConnectorMetadata(connector, options); - } - - return null; -}; - -const getDataPlane = ( - dataPlaneOptions: DataPlaneOption[], - dataPlaneId: string | null -): Details['data']['dataPlane'] | null => { - const selectedOption = dataPlaneId - ? dataPlaneOptions.find(({ id }) => id === dataPlaneId) - : undefined; - - if (selectedOption) { - return selectedOption; - } - - // TODO (private data plane) - we need to add support for allowing tenants to configure their - // preferred data plane. - - // If we are not trying to find a specific data plane and there is only one option - // and it is private we are pretty safe in prefilling that one. - if ( - !dataPlaneId && - dataPlaneOptions.length === 1 && - dataPlaneOptions[0].dataPlaneName.whole.includes( - DATA_PLANE_SETTINGS.private.prefix - ) - ) { - logRocketEvent(CustomEvents.DATA_PLANE_SELECTOR, { - defaultedPrivate: true, - }); - return dataPlaneOptions[0]; - } - - // Try to find the default public data plane - const defaultOption = dataPlaneOptions.find( - ({ dataPlaneName }) => - dataPlaneName.whole === - `${DATA_PLANE_SETTINGS.public.prefix}${defaultDataPlaneSuffix}` - ); - - if (dataPlaneId) { - logRocketEvent(CustomEvents.DATA_PLANE_SELECTOR, { - targetDataPlaneId: dataPlaneId, - defaultDataPlaneId: defaultOption?.id, - }); - } - - return defaultOption ?? null; -}; +// const getConnectorImage = async ( +// connectorId: string, +// existingImageTag?: ConnectorVersionEvaluationOptions['existingImageTag'] +// ): Promise<Details['data']['connectorImage'] | null> => { +// const { data, error } = +// await getConnectors_detailsFormTestPage(connectorId); + +// if (!error && data && data.length > 0) { +// const connector = data[0]; + +// const options: ConnectorVersionEvaluationOptions | undefined = +// existingImageTag ? { connectorId, existingImageTag } : undefined; + +// return getConnectorMetadata(connector, options); +// } + +// return null; +// }; + +// const getDataPlane = ( +// dataPlaneOptions: DataPlaneOption[], +// dataPlaneId: string | null +// ): Details['data']['dataPlane'] | null => { +// const selectedOption = dataPlaneId +// ? dataPlaneOptions.find(({ id }) => id === dataPlaneId) +// : undefined; + +// if (selectedOption) { +// return selectedOption; +// } + +// // TODO (private data plane) - we need to add support for allowing tenants to configure their +// // preferred data plane. + +// // If we are not trying to find a specific data plane and there is only one option +// // and it is private we are pretty safe in prefilling that one. +// if ( +// !dataPlaneId && +// dataPlaneOptions.length === 1 && +// dataPlaneOptions[0].dataPlaneName.whole.includes( +// DATA_PLANE_SETTINGS.private.prefix +// ) +// ) { +// logRocketEvent(CustomEvents.DATA_PLANE_SELECTOR, { +// defaultedPrivate: true, +// }); +// return dataPlaneOptions[0]; +// } + +// // Try to find the default public data plane +// const defaultOption = dataPlaneOptions.find( +// ({ dataPlaneName }) => +// dataPlaneName.whole === +// `${DATA_PLANE_SETTINGS.public.prefix}${defaultDataPlaneSuffix}` +// ); + +// if (dataPlaneId) { +// logRocketEvent(CustomEvents.DATA_PLANE_SELECTOR, { +// targetDataPlaneId: dataPlaneId, +// defaultDataPlaneId: defaultOption?.id, +// }); +// } + +// return defaultOption ?? null; +// }; const getInitialStateData = (): Pick< DetailsFormState, @@ -286,105 +273,106 @@ export const getInitialState = ( }, hydrateState: async (workflow, dataPlaneOptions): Promise<void> => { - const searchParams = new URLSearchParams(window.location.search); - const connectorId = searchParams.get(GlobalSearchParams.CONNECTOR_ID); - const dataPlaneId = searchParams.get(GlobalSearchParams.DATA_PLANE_ID); - const liveSpecId = searchParams.get(GlobalSearchParams.LIVE_SPEC_ID); - - const createWorkflow = - workflow === 'capture_create' || - workflow === 'materialization_create'; - - if (connectorId) { - if (createWorkflow) { - const connectorImage = await getConnectorImage(connectorId); - const dataPlane = getDataPlane(dataPlaneOptions, dataPlaneId); - - if (connectorImage && dataPlane === null) { - get().setDetails_connector(connectorImage); - - const { - data: { entityName }, - errors, - } = initialDetails; - - get().setPreviousDetails({ - data: { entityName, connectorImage }, - errors, - }); - } else if (connectorImage && dataPlane !== null) { - get().setDetails_connector(connectorImage); - - const { - data: { entityName }, - errors, - } = initialDetails; - - get().setDetails_dataPlane(dataPlane); - get().setPreviousDetails({ - data: { entityName, connectorImage, dataPlane }, - errors, - }); - } else { - get().setHydrationErrorsExist(true); - } - } else if (liveSpecId) { - const { data, error } = - await getLiveSpecs_detailsForm(liveSpecId); - - if (!error && data && data.length > 0) { - const { - catalog_name, - connector_image_tag, - data_plane_id, - } = data[0]; - - const connectorImage = await getConnectorImage( - connectorId, - connector_image_tag - ); - - const dataPlane = getDataPlane( - dataPlaneOptions, - data_plane_id - ); - - if (connectorImage && dataPlane !== null) { - const hydratedDetails: Details = { - data: { - entityName: catalog_name, - connectorImage, - dataPlane, - }, - }; - - get().setDetails(hydratedDetails); - get().setPreviousDetails(hydratedDetails); - } else { - get().setHydrationErrorsExist(true); - } - } else { - get().setHydrationErrorsExist(true); - } - } else if (workflow === 'test_json_forms') { - get().setDetails_connector({ - id: connectorId, - iconPath: '', - imageName: '', - imagePath: '', - imageTag: '', - connectorId, - }); - get().setHydrationErrorsExist(true); - } - } else { - logRocketEvent(CustomEvents.CONNECTOR_VERSION_MISSING); - // TODO (details hydration) should really show an error here - // get().setHydrationError( - // 'Unable to locate selected connector. If the issue persists, please contact support.' - // ); - // get().setHydrationErrorsExist(true); - } + console.error('DETAILS FORM HYDRATE STATE WAS CALLED'); + // const searchParams = new URLSearchParams(window.location.search); + // const connectorId = searchParams.get(GlobalSearchParams.CONNECTOR_ID); + // const dataPlaneId = searchParams.get(GlobalSearchParams.DATA_PLANE_ID); + // const liveSpecId = searchParams.get(GlobalSearchParams.LIVE_SPEC_ID); + + // const createWorkflow = + // workflow === 'capture_create' || + // workflow === 'materialization_create'; + + // if (connectorId) { + // if (createWorkflow) { + // const connectorImage = await getConnectorImage(connectorId); + // const dataPlane = getDataPlane(dataPlaneOptions, dataPlaneId); + + // if (connectorImage && dataPlane === null) { + // get().setDetails_connector(connectorImage); + + // const { + // data: { entityName }, + // errors, + // } = initialDetails; + + // get().setPreviousDetails({ + // data: { entityName, connectorImage }, + // errors, + // }); + // } else if (connectorImage && dataPlane !== null) { + // get().setDetails_connector(connectorImage); + + // const { + // data: { entityName }, + // errors, + // } = initialDetails; + + // get().setDetails_dataPlane(dataPlane); + // get().setPreviousDetails({ + // data: { entityName, connectorImage, dataPlane }, + // errors, + // }); + // } else { + // get().setHydrationErrorsExist(true); + // } + // } else if (liveSpecId) { + // const { data, error } = + // await getLiveSpecs_detailsForm(liveSpecId); + + // if (!error && data && data.length > 0) { + // const { + // catalog_name, + // connector_image_tag, + // data_plane_id, + // } = data[0]; + + // const connectorImage = await getConnectorImage( + // connectorId, + // connector_image_tag + // ); + + // const dataPlane = getDataPlane( + // dataPlaneOptions, + // data_plane_id + // ); + + // if (connectorImage && dataPlane !== null) { + // const hydratedDetails: Details = { + // data: { + // entityName: catalog_name, + // connectorImage, + // dataPlane, + // }, + // }; + + // get().setDetails(hydratedDetails); + // get().setPreviousDetails(hydratedDetails); + // } else { + // get().setHydrationErrorsExist(true); + // } + // } else { + // get().setHydrationErrorsExist(true); + // } + // } else if (workflow === 'test_json_forms') { + // get().setDetails_connector({ + // id: connectorId, + // iconPath: '', + // imageName: '', + // imagePath: '', + // imageTag: '', + // connectorId, + // }); + // get().setHydrationErrorsExist(true); + // } + // } else { + // logRocketEvent(CustomEvents.CONNECTOR_VERSION_MISSING); + // // TODO (details hydration) should really show an error here + // // get().setHydrationError( + // // 'Unable to locate selected connector. If the issue persists, please contact support.' + // // ); + // // get().setHydrationErrorsExist(true); + // } }, resetState: () => { From b21977f5c656ea82fb43aa886ddb1f9f47105ba8 Mon Sep 17 00:00:00 2001 From: Travis Jenkins <travis@estuary.dev> Date: Tue, 7 Apr 2026 16:34:46 -0400 Subject: [PATCH 18/30] We can remove this as it was only around for test/jsonforms page and that has been handled --- src/stores/DetailsForm/Store.ts | 172 +------------------------------- 1 file changed, 4 insertions(+), 168 deletions(-) diff --git a/src/stores/DetailsForm/Store.ts b/src/stores/DetailsForm/Store.ts index e7bcacc020..5a660589e8 100644 --- a/src/stores/DetailsForm/Store.ts +++ b/src/stores/DetailsForm/Store.ts @@ -26,72 +26,6 @@ import { NAME_RE } from 'src/validation'; const STORE_KEY = 'Details Form'; -// const getConnectorImage = async ( -// connectorId: string, -// existingImageTag?: ConnectorVersionEvaluationOptions['existingImageTag'] -// ): Promise<Details['data']['connectorImage'] | null> => { -// const { data, error } = -// await getConnectors_detailsFormTestPage(connectorId); - -// if (!error && data && data.length > 0) { -// const connector = data[0]; - -// const options: ConnectorVersionEvaluationOptions | undefined = -// existingImageTag ? { connectorId, existingImageTag } : undefined; - -// return getConnectorMetadata(connector, options); -// } - -// return null; -// }; - -// const getDataPlane = ( -// dataPlaneOptions: DataPlaneOption[], -// dataPlaneId: string | null -// ): Details['data']['dataPlane'] | null => { -// const selectedOption = dataPlaneId -// ? dataPlaneOptions.find(({ id }) => id === dataPlaneId) -// : undefined; - -// if (selectedOption) { -// return selectedOption; -// } - -// // TODO (private data plane) - we need to add support for allowing tenants to configure their -// // preferred data plane. - -// // If we are not trying to find a specific data plane and there is only one option -// // and it is private we are pretty safe in prefilling that one. -// if ( -// !dataPlaneId && -// dataPlaneOptions.length === 1 && -// dataPlaneOptions[0].dataPlaneName.whole.includes( -// DATA_PLANE_SETTINGS.private.prefix -// ) -// ) { -// logRocketEvent(CustomEvents.DATA_PLANE_SELECTOR, { -// defaultedPrivate: true, -// }); -// return dataPlaneOptions[0]; -// } - -// // Try to find the default public data plane -// const defaultOption = dataPlaneOptions.find( -// ({ dataPlaneName }) => -// dataPlaneName.whole === -// `${DATA_PLANE_SETTINGS.public.prefix}${defaultDataPlaneSuffix}` -// ); - -// if (dataPlaneId) { -// logRocketEvent(CustomEvents.DATA_PLANE_SELECTOR, { -// targetDataPlaneId: dataPlaneId, -// defaultDataPlaneId: defaultOption?.id, -// }); -// } - -// return defaultOption ?? null; -// }; - const getInitialStateData = (): Pick< DetailsFormState, | 'connectors' @@ -272,108 +206,10 @@ export const getInitialState = ( ); }, - hydrateState: async (workflow, dataPlaneOptions): Promise<void> => { - console.error('DETAILS FORM HYDRATE STATE WAS CALLED'); - // const searchParams = new URLSearchParams(window.location.search); - // const connectorId = searchParams.get(GlobalSearchParams.CONNECTOR_ID); - // const dataPlaneId = searchParams.get(GlobalSearchParams.DATA_PLANE_ID); - // const liveSpecId = searchParams.get(GlobalSearchParams.LIVE_SPEC_ID); - - // const createWorkflow = - // workflow === 'capture_create' || - // workflow === 'materialization_create'; - - // if (connectorId) { - // if (createWorkflow) { - // const connectorImage = await getConnectorImage(connectorId); - // const dataPlane = getDataPlane(dataPlaneOptions, dataPlaneId); - - // if (connectorImage && dataPlane === null) { - // get().setDetails_connector(connectorImage); - - // const { - // data: { entityName }, - // errors, - // } = initialDetails; - - // get().setPreviousDetails({ - // data: { entityName, connectorImage }, - // errors, - // }); - // } else if (connectorImage && dataPlane !== null) { - // get().setDetails_connector(connectorImage); - - // const { - // data: { entityName }, - // errors, - // } = initialDetails; - - // get().setDetails_dataPlane(dataPlane); - // get().setPreviousDetails({ - // data: { entityName, connectorImage, dataPlane }, - // errors, - // }); - // } else { - // get().setHydrationErrorsExist(true); - // } - // } else if (liveSpecId) { - // const { data, error } = - // await getLiveSpecs_detailsForm(liveSpecId); - - // if (!error && data && data.length > 0) { - // const { - // catalog_name, - // connector_image_tag, - // data_plane_id, - // } = data[0]; - - // const connectorImage = await getConnectorImage( - // connectorId, - // connector_image_tag - // ); - - // const dataPlane = getDataPlane( - // dataPlaneOptions, - // data_plane_id - // ); - - // if (connectorImage && dataPlane !== null) { - // const hydratedDetails: Details = { - // data: { - // entityName: catalog_name, - // connectorImage, - // dataPlane, - // }, - // }; - - // get().setDetails(hydratedDetails); - // get().setPreviousDetails(hydratedDetails); - // } else { - // get().setHydrationErrorsExist(true); - // } - // } else { - // get().setHydrationErrorsExist(true); - // } - // } else if (workflow === 'test_json_forms') { - // get().setDetails_connector({ - // id: connectorId, - // iconPath: '', - // imageName: '', - // imagePath: '', - // imageTag: '', - // connectorId, - // }); - // get().setHydrationErrorsExist(true); - // } - // } else { - // logRocketEvent(CustomEvents.CONNECTOR_VERSION_MISSING); - // // TODO (details hydration) should really show an error here - // // get().setHydrationError( - // // 'Unable to locate selected connector. If the issue persists, please contact support.' - // // ); - // // get().setHydrationErrorsExist(true); - // } - }, + // This is blank on purpose - this is handled by useDetailsFormHydrator + // this is just here to handling typing and we want the other parts + // of the "Hydration" slice. + hydrateState: async () => {}, resetState: () => { set( From 3aabcde08d0d541cdf4965c33dca2ec65c47301a Mon Sep 17 00:00:00 2001 From: Travis Jenkins <travis@estuary.dev> Date: Wed, 8 Apr 2026 10:18:44 -0400 Subject: [PATCH 19/30] jsonforms test page will help connector team with quicker testing and it it a pretty safe page to allow hitting. --- src/context/Router/index.tsx | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/context/Router/index.tsx b/src/context/Router/index.tsx index 87cd00530a..bad40abae7 100644 --- a/src/context/Router/index.tsx +++ b/src/context/Router/index.tsx @@ -753,23 +753,23 @@ const router = createBrowserRouter( /> </Route> - {!isProduction ? ( - <> - <Route - path="test/jsonforms" - element={ - <ErrorBoundary - FallbackComponent={ErrorImporting} - > - <EntityContextProvider value="capture"> - <TestJsonForms /> - </EntityContextProvider> - </ErrorBoundary> - } - /> + <Route path="test"> + <Route + path="jsonforms" + element={ + <ErrorBoundary + FallbackComponent={ErrorImporting} + > + <EntityContextProvider value="capture"> + <TestJsonForms /> + </EntityContextProvider> + </ErrorBoundary> + } + /> + {!isProduction ? ( <Route - path="test/gql" + path="gql" element={ <ErrorBoundary FallbackComponent={ErrorImporting} @@ -778,8 +778,8 @@ const router = createBrowserRouter( </ErrorBoundary> } /> - </> - ) : null} + ) : null} + </Route> <Route path={authenticatedRoutes.pageNotFound.path} From abc5ea234dd84573242c40cbda568a2ec5aead34 Mon Sep 17 00:00:00 2001 From: Travis Jenkins <travis@estuary.dev> Date: Thu, 9 Apr 2026 12:15:19 -0400 Subject: [PATCH 20/30] Cleaning up some old code to move towards using the new context --- src/api/connectors.ts | 102 ------------------ .../capture/ExpressCreate/HeaderText.tsx | 14 +-- .../Entity/DetailsForm/useConnectorField.ts | 33 +----- .../Entity/EndpointConfig/SectionContent.tsx | 26 ++--- src/stores/Binding/shared.ts | 2 + .../DetailsForm/useDetailsFormHydrator.ts | 28 +---- src/stores/Workflow/hooks.ts | 37 ------- src/utils/connector-utils.ts | 90 ++++------------ 8 files changed, 39 insertions(+), 293 deletions(-) delete mode 100644 src/api/connectors.ts delete mode 100644 src/stores/Workflow/hooks.ts diff --git a/src/api/connectors.ts b/src/api/connectors.ts deleted file mode 100644 index fef3287164..0000000000 --- a/src/api/connectors.ts +++ /dev/null @@ -1,102 +0,0 @@ -import type { - ConnectorsQuery_DetailsForm, - ConnectorWithTagQuery, -} from 'src/api/types'; -import type { SortDirection } from 'src/types'; - -import { - CONNECTOR_DETAILS, - CONNECTOR_NAME, - CONNECTOR_RECOMMENDED, - CONNECTOR_WITH_TAG_QUERY, -} from 'src/api/shared'; -import { supabaseClient } from 'src/context/GlobalProviders'; -import { - defaultTableFilter, - handleFailure, - handleSuccess, - supabaseRetry, - TABLES, -} from 'src/services/supabase'; -import { requiredConnectorColumnsExist } from 'src/utils/connector-utils'; - -// Table-specific queries -const getConnectors = ( - searchQuery: any, - sortDirection: SortDirection, - protocol: string | null -) => { - // TODO (V2 typing) - need a way to handle single vs multiple responses - return requiredConnectorColumnsExist<ConnectorWithTagQuery[]>( - defaultTableFilter<ConnectorWithTagQuery>( - supabaseClient - .from(TABLES.CONNECTORS) - .select(CONNECTOR_WITH_TAG_QUERY), - [CONNECTOR_NAME, CONNECTOR_DETAILS], - searchQuery, - [ - { - col: CONNECTOR_RECOMMENDED, - direction: 'desc', - }, - { - col: CONNECTOR_NAME, - direction: sortDirection, - }, - ], - undefined, - { column: 'connector_tags.protocol', value: protocol } - ), - 'connector_tags' - ); -}; - -// Hydration-specific queries -const DETAILS_FORM_QUERY = ` - id, - image_name, - image:logo_url->>en-US::text, - connector_tags !inner( - id, - connector_id, - image_tag - ) -`; - -// TODO: Remove getConnectors_detailsForm and related assets. -// It is only used by the test JSON forms page. -const getConnectors_detailsFormTestPage = async (connectorId: string) => { - const data = await supabaseRetry( - () => - supabaseClient - .from(TABLES.CONNECTORS) - .select(DETAILS_FORM_QUERY) - .eq('id', connectorId) - .eq('connector_tags.connector_id', connectorId), - 'getConnectors_detailsFormTestPage' - ).then(handleSuccess<ConnectorsQuery_DetailsForm[]>, handleFailure); - - return data; -}; - -const getSingleConnectorWithTag = async (connectorId: string) => { - const data = await supabaseRetry( - () => - requiredConnectorColumnsExist<ConnectorWithTagQuery[]>( - supabaseClient - .from(TABLES.CONNECTORS) - .select(CONNECTOR_WITH_TAG_QUERY) - .eq('id', connectorId), - 'connector_tags' - ), - 'getSingleConnectorWithTag' - ).then(handleSuccess<ConnectorWithTagQuery[]>, handleFailure); - - return data; -}; - -export { - getConnectors, - getConnectors_detailsFormTestPage, - getSingleConnectorWithTag, -}; diff --git a/src/components/capture/ExpressCreate/HeaderText.tsx b/src/components/capture/ExpressCreate/HeaderText.tsx index d17c4ba4a4..755488db10 100644 --- a/src/components/capture/ExpressCreate/HeaderText.tsx +++ b/src/components/capture/ExpressCreate/HeaderText.tsx @@ -1,17 +1,9 @@ import { Typography } from '@mui/material'; -import useGlobalSearchParams, { - GlobalSearchParams, -} from 'src/hooks/searchParams/useGlobalSearchParams'; -import { useWorkflowStore_connectorMetadataProperty } from 'src/stores/Workflow/hooks'; +import { useConnectorTag } from 'src/context/ConnectorTag'; export const ExpressHeaderText = () => { - const connectorId = useGlobalSearchParams(GlobalSearchParams.CONNECTOR_ID); + const connectorTag = useConnectorTag(); - const connectorTitle = useWorkflowStore_connectorMetadataProperty( - connectorId, - 'title' - ); - - return <Typography variant="h6">{connectorTitle}</Typography>; + return <Typography variant="h6">{connectorTag.connector.title}</Typography>; }; diff --git a/src/components/shared/Entity/DetailsForm/useConnectorField.ts b/src/components/shared/Entity/DetailsForm/useConnectorField.ts index c25a718c80..2497dd5f36 100644 --- a/src/components/shared/Entity/DetailsForm/useConnectorField.ts +++ b/src/components/shared/Entity/DetailsForm/useConnectorField.ts @@ -6,8 +6,7 @@ import { useIntl } from 'react-intl'; import { CONNECTOR_IMAGE_SCOPE } from 'src/forms/renderers/Connectors'; import { useWorkflowStore } from 'src/stores/Workflow/Store'; - -const DEKAF_IMAGE_PREFIX = 'ghcr.io/estuary/dekaf-'; +import { buildConnectorImageFromTag } from 'src/utils/connector-utils'; export default function useConnectorField( entityType: EntityWithCreateWorkflow @@ -21,36 +20,10 @@ export default function useConnectorField( return []; } - const { - id, - connectorId: connectorTagId, - imageTag, - connector, - } = connectorTag; - - const base = { - connectorId: connectorTagId, - iconPath: connector.logoUrl ?? '', - id, - imageName: connector.imageName, - imageTag, - }; - - const connectorImage = connector.imageName.startsWith( - DEKAF_IMAGE_PREFIX - ) - ? { - ...base, - variant: connector.imageName.substring( - DEKAF_IMAGE_PREFIX.length - ), - } - : { ...base, imagePath: `${connector.imageName}${imageTag}` }; - return [ { - const: connectorImage, - title: connector.title ?? connector.imageName, + const: buildConnectorImageFromTag(connectorTag), + title: connectorTag.connector.title ?? connectorTag.connector.imageName, }, ]; }, [connectorTag]); diff --git a/src/components/shared/Entity/EndpointConfig/SectionContent.tsx b/src/components/shared/Entity/EndpointConfig/SectionContent.tsx index a9733388fe..8efe5b1cdb 100644 --- a/src/components/shared/Entity/EndpointConfig/SectionContent.tsx +++ b/src/components/shared/Entity/EndpointConfig/SectionContent.tsx @@ -13,9 +13,9 @@ import EndpointConfigForm from 'src/components/shared/Entity/EndpointConfig/Form import { DOCUSAURUS_THEME } from 'src/components/shared/Entity/EndpointConfig/shared'; import ErrorBoundryWrapper from 'src/components/shared/ErrorBoundryWrapper'; import HydrationError from 'src/components/shared/HydrationError'; +import { useConnectorTag } from 'src/context/ConnectorTag'; import { useEntityWorkflow_Editing } from 'src/context/Workflow'; import { logRocketEvent } from 'src/services/shared'; -import { useDetailsFormStore } from 'src/stores/DetailsForm/Store'; import { useEndpointConfig_setServerUpdateRequired, useEndpointConfigStore_endpointConfig_data, @@ -23,18 +23,13 @@ import { } from 'src/stores/EndpointConfig/hooks'; import { useEndpointConfigStore } from 'src/stores/EndpointConfig/Store'; import { useSidePanelDocsStore } from 'src/stores/SidePanelDocs/Store'; -import { useWorkflowStore_connectorTagProperty } from 'src/stores/Workflow/hooks'; const SectionContent = ({ readOnly = false }: SectionContentProps) => { // General hooks const intl = useIntl(); const theme = useTheme(); - // Detail Form Store - const [connectorId, connectorTagId] = useDetailsFormStore((state) => [ - state.details.data.connectorImage.connectorId, - state.details.data.connectorImage.id, - ]); + const connectorTag = useConnectorTag(); // Endpoint Config Store const [endpointCanBeEmpty, hydrationErrorsExist] = useEndpointConfigStore( @@ -45,13 +40,6 @@ const SectionContent = ({ readOnly = false }: SectionContentProps) => { useEndpointConfigStore_previousEndpointConfig_data(); const setServerUpdateRequired = useEndpointConfig_setServerUpdateRequired(); - // Workflow Store - const documentationURL = useWorkflowStore_connectorTagProperty( - connectorId, - connectorTagId, - 'documentation_url' - ); - // Workflow related props const editWorkflow = useEntityWorkflow_Editing(); @@ -87,18 +75,20 @@ const SectionContent = ({ readOnly = false }: SectionContentProps) => { sidePanelResetState(); }); useEffect(() => { - if (documentationURL) { - const concatSymbol = documentationURL.includes('?') ? '&' : '?'; + if (connectorTag.documentationUrl) { + const concatSymbol = connectorTag.documentationUrl.includes('?') + ? '&' + : '?'; setDocsURL( - `${documentationURL}${concatSymbol}${DOCUSAURUS_THEME}=${theme.palette.mode}` + `${connectorTag.documentationUrl}${concatSymbol}${DOCUSAURUS_THEME}=${theme.palette.mode}` ); } // We do not want to trigger this if the theme changes so we just use the theme at load // because we fire a message to the docs when the theme changes // eslint-disable-next-line react-hooks/exhaustive-deps - }, [documentationURL, setDocsURL]); + }, [connectorTag.documentationUrl, setDocsURL]); // Default serverUpdateRequired for Create // This prevents us from sending the empty object to get encrypted diff --git a/src/stores/Binding/shared.ts b/src/stores/Binding/shared.ts index d61ad34b6b..bea7d874ad 100644 --- a/src/stores/Binding/shared.ts +++ b/src/stores/Binding/shared.ts @@ -343,6 +343,8 @@ export const hydrateConnectorTagDependentState = async ( if (connectorTag.resourceSpecSchema) { const schema = connectorTag.resourceSpecSchema as unknown as Schema; await get().setResourceSchema(schema); + } else { + get().setHydrationErrorsExist(true); } get().setBackfillSupported(!Boolean(connectorTag.disableBackfill)); diff --git a/src/stores/DetailsForm/useDetailsFormHydrator.ts b/src/stores/DetailsForm/useDetailsFormHydrator.ts index ee4046707b..e388d997db 100644 --- a/src/stores/DetailsForm/useDetailsFormHydrator.ts +++ b/src/stores/DetailsForm/useDetailsFormHydrator.ts @@ -12,29 +12,7 @@ import useGlobalSearchParams, { } from 'src/hooks/searchParams/useGlobalSearchParams'; import { initialDetails } from 'src/stores/DetailsForm/shared'; import { useDetailsFormStore } from 'src/stores/DetailsForm/Store'; - -const DEKAF_IMAGE_PREFIX = 'ghcr.io/estuary/dekaf-'; - -const buildConnectorImage = ( - connectorTag: ReturnType<typeof useConnectorTag> -): Details['data']['connectorImage'] => { - const { id, connectorId, imageTag, connector } = connectorTag; - - const base = { - connectorId, - iconPath: connector.logoUrl ?? '', - id, - imageName: connector.imageName, - imageTag, - }; - - return connector.imageName.startsWith(DEKAF_IMAGE_PREFIX) - ? { - ...base, - variant: connector.imageName.substring(DEKAF_IMAGE_PREFIX.length), - } - : { ...base, imagePath: `${connector.imageName}${imageTag}` }; -}; +import { buildConnectorImageFromTag } from 'src/utils/connector-utils'; export const useDetailsFormHydrator = () => { const connectorTag = useConnectorTag(); @@ -69,7 +47,7 @@ export const useDetailsFormHydrator = () => { workflow === 'materialization_create'; if (createWorkflow) { - const connectorImage = buildConnectorImage(connectorTag); + const connectorImage = buildConnectorImageFromTag(connectorTag); const dataPlaneOptions = await evaluateDataPlaneOptions(baseEntityName); @@ -112,7 +90,7 @@ export const useDetailsFormHydrator = () => { reactor_address, } = data[0]; - const connectorImage = buildConnectorImage(connectorTag); + const connectorImage = buildConnectorImageFromTag(connectorTag); const dataPlaneOptions = await evaluateDataPlaneOptions( catalog_name, diff --git a/src/stores/Workflow/hooks.ts b/src/stores/Workflow/hooks.ts deleted file mode 100644 index b715f373a8..0000000000 --- a/src/stores/Workflow/hooks.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { useWorkflowStore } from 'src/stores/Workflow/Store'; - -// Returns the title of the connector stored in workflow metadata -export const useWorkflowStore_connectorTitle = (): string | undefined => { - return useWorkflowStore( - (state) => state.connectorMetadata?.connector.title ?? undefined - ); -}; - -// Returns the documentation URL of the connector tag stored in workflow metadata -export const useWorkflowStore_connectorTagDocumentationUrl = - (): string | undefined => { - return useWorkflowStore( - (state) => state.connectorMetadata?.documentationUrl ?? undefined - ); - }; - -// Legacy hook names preserved for backward compatibility -// TODO (gql:connector) - update callers to use the new named hooks above -export const useWorkflowStore_connectorMetadataProperty = ( - _connectorId: string | null | undefined, - _property: string -): string | undefined => { - return useWorkflowStore( - (state) => state.connectorMetadata?.connector.title ?? undefined - ); -}; - -export const useWorkflowStore_connectorTagProperty = ( - _connectorId: string | null | undefined, - _connectorTagId: string | null | undefined, - _property: string -): string | undefined => { - return useWorkflowStore( - (state) => state.connectorMetadata?.documentationUrl ?? undefined - ); -}; diff --git a/src/utils/connector-utils.ts b/src/utils/connector-utils.ts index cf43173f8d..74b15e5ec1 100644 --- a/src/utils/connector-utils.ts +++ b/src/utils/connector-utils.ts @@ -6,23 +6,15 @@ import type { PostgrestFilterBuilder } from '@supabase/postgrest-js'; import type { ConnectorConfig } from 'deps/flow/flow'; import type { DraftSpecsExtQuery_ByDraftId } from 'src/api/draftSpecs'; -import type { - BaseConnectorTag, - ConnectorsQuery_DetailsForm, - ConnectorWithTagQuery, -} from 'src/api/types'; +import type { ConnectorTagData } from 'src/context/ConnectorTag'; import type { LiveSpecsExtQuery } from 'src/hooks/useLiveSpecsExt'; import type { - ConnectorMetadata, DekafConnectorMetadata, - Details, StandardConnectorMetadata, } from 'src/stores/DetailsForm/types'; import type { DekafConfig } from 'src/types'; -import { hasLength } from 'src/utils/misc-utils'; - -const DEKAF_IMAGE_PREFIX = 'ghcr.io/estuary/dekaf-'; +export const DEKAF_IMAGE_PREFIX = 'ghcr.io/estuary/dekaf-'; const DEKAF_VARIANT_PROPERTY = 'variant'; export const isDekafConnector = ( @@ -33,66 +25,6 @@ export const isDekafEndpointConfig = ( value: ConnectorConfig | DekafConfig ): value is DekafConfig => DEKAF_VARIANT_PROPERTY in value; -export interface ConnectorVersionEvaluationOptions { - connectorId: string; - existingImageTag: string; -} - -export function evaluateConnectorVersions( - connector: ConnectorWithTagQuery | ConnectorsQuery_DetailsForm, - options?: ConnectorVersionEvaluationOptions -): BaseConnectorTag { - // Return the version of the connector that is used by the existing task in an edit workflow. - if (options && options.connectorId === connector.id) { - const connectorsInUse = connector.connector_tags.filter( - (version) => version.image_tag === options.existingImageTag - ); - - if (hasLength(connectorsInUse)) { - return connectorsInUse[0]; - } - } - - // Return the latest version of a given connector. - const { connector_id, id, image_tag } = connector.connector_tags.sort( - (a, b) => b.image_tag.localeCompare(a.image_tag) - )[0]; - - return { connector_id, id, image_tag }; -} - -// TODO (typing): Align `connectors` and `connector_tags` query interfaces. -// Renamed table columns need to be given the same name to avoid type conflicts. -export function getConnectorMetadata( - connector: ConnectorsQuery_DetailsForm | ConnectorWithTagQuery, - options?: ConnectorVersionEvaluationOptions -): Details['data']['connectorImage'] { - const { id: connectorTagId, image_tag } = evaluateConnectorVersions( - connector, - options - ); - - const { id: connectorId, image: iconPath, image_name } = connector; - - const connectorMetadata: ConnectorMetadata = { - connectorId, - iconPath, - id: connectorTagId, - imageName: image_name, - imageTag: image_tag, - }; - - return image_name.startsWith(DEKAF_IMAGE_PREFIX) - ? { - ...connectorMetadata, - variant: image_name.substring(DEKAF_IMAGE_PREFIX.length), - } - : { - ...connectorMetadata, - imagePath: `${image_name}${image_tag}`, - }; -} - export const getEndpointConfig = ( data: DraftSpecsExtQuery_ByDraftId[] | LiveSpecsExtQuery[] ) => @@ -119,6 +51,24 @@ export const requiredConnectorColumnsExist = <Response>( ); }; +export const buildConnectorImageFromTag = ( + connectorTag: ConnectorTagData +): StandardConnectorMetadata | DekafConnectorMetadata => { + const { id, connectorId, imageTag, connector } = connectorTag; + + const base = { + connectorId, + iconPath: connector.logoUrl ?? '', + id, + imageName: connector.imageName, + imageTag, + }; + + return connector.imageName.startsWith(DEKAF_IMAGE_PREFIX) + ? { ...base, variant: connector.imageName.substring(DEKAF_IMAGE_PREFIX.length) } + : { ...base, imagePath: `${connector.imageName}${imageTag}` }; +}; + // TODO (GQL:live specs) - once we get live specs fetched with GQL we don't need to worry about this export function formatOldUuidToGql(id: string): string; export function formatOldUuidToGql(id: null | undefined): null | undefined; From f03eca2ff77a22abec35c6b322813580df76e8b8 Mon Sep 17 00:00:00 2001 From: Travis Jenkins <travis@estuary.dev> Date: Thu, 9 Apr 2026 12:20:11 -0400 Subject: [PATCH 21/30] Some final cleanup of hanging code --- src/api/types.ts | 41 +----------------------------------- src/utils/connector-utils.ts | 10 ++++----- 2 files changed, 5 insertions(+), 46 deletions(-) diff --git a/src/api/types.ts b/src/api/types.ts index 70836a6a61..5fb4684f2b 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -1,43 +1,4 @@ -import type { CONNECTOR_NAME } from 'src/api/shared'; -import type { Entity, EntityWithCreateWorkflow, Schema } from 'src/types'; - -export interface BaseConnectorTag { - id: string; - connector_id: string; - image_tag: string; -} - -export interface ConnectorTag extends BaseConnectorTag { - documentation_url: string; - endpoint_spec_schema: Schema; - image_name: string; - protocol: EntityWithCreateWorkflow; - title: string; -} - -export interface ConnectorWithTag { - connector_tags: ConnectorTag[]; - id: string; - detail: string; - image_name: string; - image: string; - recommended: boolean; - title: string; -} - -export interface ConnectorWithTagQuery extends ConnectorWithTag { - // FILTERING TYPES HACK - ['connector_tags.protocol']: undefined; - ['connector_tags.image_tag']: undefined; - [CONNECTOR_NAME]: undefined; -} - -export interface ConnectorsQuery_DetailsForm { - id: string; - image_name: string; - image: string; - connector_tags: BaseConnectorTag[]; -} +import type { Entity } from 'src/types'; export interface DraftSpecData { spec: any; diff --git a/src/utils/connector-utils.ts b/src/utils/connector-utils.ts index 74b15e5ec1..e1a1cff811 100644 --- a/src/utils/connector-utils.ts +++ b/src/utils/connector-utils.ts @@ -1,8 +1,3 @@ -// TODO (Typing) -// Since the typing looks at columns it was a pain to make this -// truly reusable. So marking the query as `any` even thogh -// it is PostgrestFilterBuilder<ConnectorTag |ConnectorWithTagDetailQuery> - import type { PostgrestFilterBuilder } from '@supabase/postgrest-js'; import type { ConnectorConfig } from 'deps/flow/flow'; import type { DraftSpecsExtQuery_ByDraftId } from 'src/api/draftSpecs'; @@ -65,7 +60,10 @@ export const buildConnectorImageFromTag = ( }; return connector.imageName.startsWith(DEKAF_IMAGE_PREFIX) - ? { ...base, variant: connector.imageName.substring(DEKAF_IMAGE_PREFIX.length) } + ? { + ...base, + variant: connector.imageName.substring(DEKAF_IMAGE_PREFIX.length), + } : { ...base, imagePath: `${connector.imageName}${imageTag}` }; }; From 679d56b3544cfe1ce0b2fd12603d49546094bd93 Mon Sep 17 00:00:00 2001 From: Travis Jenkins <travis@estuary.dev> Date: Fri, 10 Apr 2026 11:26:17 -0400 Subject: [PATCH 22/30] Post merge codegen run --- src/gql-types/gql.ts | 12 +++ src/gql-types/graphql.ts | 182 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 194 insertions(+) diff --git a/src/gql-types/gql.ts b/src/gql-types/gql.ts index e942300f5b..4fd93f6335 100644 --- a/src/gql-types/gql.ts +++ b/src/gql-types/gql.ts @@ -19,6 +19,8 @@ type Documents = { "\n query AlertSubscriptions($prefix: Prefix!) {\n alertSubscriptions(by: { prefix: $prefix }) {\n alertTypes\n catalogPrefix\n email\n updatedAt\n }\n }\n": typeof types.AlertSubscriptionsDocument, "\n mutation UpdateAlertSubscriptionMutation(\n $prefix: Prefix!\n $email: String!\n $alertTypes: [AlertType!]\n $detail: String\n ) {\n updateAlertSubscription(\n prefix: $prefix\n email: $email\n alertTypes: $alertTypes\n detail: $detail\n ) {\n catalogPrefix\n email\n }\n }\n": typeof types.UpdateAlertSubscriptionMutationDocument, "\n query AlertType {\n alertTypes {\n alertType\n description\n displayName\n isDefault\n isSystem\n }\n }\n": typeof types.AlertTypeDocument, + "\n query ConnectorsGrid($filter: ConnectorsFilter, $after: String) {\n connectors(first: 500, after: $after, filter: $filter) {\n edges {\n cursor\n node {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n documentationUrl\n protocol\n }\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n": typeof types.ConnectorsGridDocument, + "\n query SingleConnector($id: Id!, $imageTag: String) {\n connector(id: $id) {\n id\n imageName\n logoUrl\n title\n connectorTag(imageTag: $imageTag, orDefault: true) {\n id\n connectorId\n imageTag\n defaultCaptureInterval\n disableBackfill\n documentationUrl\n endpointSpecSchema\n resourceSpecSchema\n protocol\n }\n }\n }\n": typeof types.SingleConnectorDocument, "\n query DataPlanes($after: String) {\n dataPlanes(first: 100, after: $after) {\n edges {\n node {\n name\n cloudProvider\n region\n isPublic\n fqdn\n cidrBlocks\n awsIamUserArn\n gcpServiceAccountEmail\n azureApplicationClientId\n azureApplicationName\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n": typeof types.DataPlanesDocument, "\n query InviteLinks($first: Int, $after: String) {\n inviteLinks(first: $first, after: $after) {\n edges {\n node {\n token\n ssoProviderId\n catalogPrefix\n capability\n singleUse\n detail\n createdAt\n }\n cursor\n }\n pageInfo {\n ...PageInfoFields\n }\n }\n }\n": typeof types.InviteLinksDocument, "\n mutation CreateInviteLink(\n $catalogPrefix: Prefix!\n $capability: Capability!\n $singleUse: Boolean!\n $detail: String\n ) {\n createInviteLink(\n catalogPrefix: $catalogPrefix\n capability: $capability\n singleUse: $singleUse\n detail: $detail\n ) {\n token\n catalogPrefix\n capability\n singleUse\n detail\n createdAt\n }\n }\n": typeof types.CreateInviteLinkDocument, @@ -42,6 +44,8 @@ const documents: Documents = { "\n query AlertSubscriptions($prefix: Prefix!) {\n alertSubscriptions(by: { prefix: $prefix }) {\n alertTypes\n catalogPrefix\n email\n updatedAt\n }\n }\n": types.AlertSubscriptionsDocument, "\n mutation UpdateAlertSubscriptionMutation(\n $prefix: Prefix!\n $email: String!\n $alertTypes: [AlertType!]\n $detail: String\n ) {\n updateAlertSubscription(\n prefix: $prefix\n email: $email\n alertTypes: $alertTypes\n detail: $detail\n ) {\n catalogPrefix\n email\n }\n }\n": types.UpdateAlertSubscriptionMutationDocument, "\n query AlertType {\n alertTypes {\n alertType\n description\n displayName\n isDefault\n isSystem\n }\n }\n": types.AlertTypeDocument, + "\n query ConnectorsGrid($filter: ConnectorsFilter, $after: String) {\n connectors(first: 500, after: $after, filter: $filter) {\n edges {\n cursor\n node {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n documentationUrl\n protocol\n }\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n": types.ConnectorsGridDocument, + "\n query SingleConnector($id: Id!, $imageTag: String) {\n connector(id: $id) {\n id\n imageName\n logoUrl\n title\n connectorTag(imageTag: $imageTag, orDefault: true) {\n id\n connectorId\n imageTag\n defaultCaptureInterval\n disableBackfill\n documentationUrl\n endpointSpecSchema\n resourceSpecSchema\n protocol\n }\n }\n }\n": types.SingleConnectorDocument, "\n query DataPlanes($after: String) {\n dataPlanes(first: 100, after: $after) {\n edges {\n node {\n name\n cloudProvider\n region\n isPublic\n fqdn\n cidrBlocks\n awsIamUserArn\n gcpServiceAccountEmail\n azureApplicationClientId\n azureApplicationName\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n": types.DataPlanesDocument, "\n query InviteLinks($first: Int, $after: String) {\n inviteLinks(first: $first, after: $after) {\n edges {\n node {\n token\n ssoProviderId\n catalogPrefix\n capability\n singleUse\n detail\n createdAt\n }\n cursor\n }\n pageInfo {\n ...PageInfoFields\n }\n }\n }\n": types.InviteLinksDocument, "\n mutation CreateInviteLink(\n $catalogPrefix: Prefix!\n $capability: Capability!\n $singleUse: Boolean!\n $detail: String\n ) {\n createInviteLink(\n catalogPrefix: $catalogPrefix\n capability: $capability\n singleUse: $singleUse\n detail: $detail\n ) {\n token\n catalogPrefix\n capability\n singleUse\n detail\n createdAt\n }\n }\n": types.CreateInviteLinkDocument, @@ -94,6 +98,14 @@ export function graphql(source: "\n mutation UpdateAlertSubscriptionMutation( * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql(source: "\n query AlertType {\n alertTypes {\n alertType\n description\n displayName\n isDefault\n isSystem\n }\n }\n"): (typeof documents)["\n query AlertType {\n alertTypes {\n alertType\n description\n displayName\n isDefault\n isSystem\n }\n }\n"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "\n query ConnectorsGrid($filter: ConnectorsFilter, $after: String) {\n connectors(first: 500, after: $after, filter: $filter) {\n edges {\n cursor\n node {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n documentationUrl\n protocol\n }\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n"): (typeof documents)["\n query ConnectorsGrid($filter: ConnectorsFilter, $after: String) {\n connectors(first: 500, after: $after, filter: $filter) {\n edges {\n cursor\n node {\n id\n imageName\n logoUrl\n title\n recommended\n shortDescription\n connectorTag(orDefault: true) {\n id\n connectorId\n imageTag\n documentationUrl\n protocol\n }\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "\n query SingleConnector($id: Id!, $imageTag: String) {\n connector(id: $id) {\n id\n imageName\n logoUrl\n title\n connectorTag(imageTag: $imageTag, orDefault: true) {\n id\n connectorId\n imageTag\n defaultCaptureInterval\n disableBackfill\n documentationUrl\n endpointSpecSchema\n resourceSpecSchema\n protocol\n }\n }\n }\n"): (typeof documents)["\n query SingleConnector($id: Id!, $imageTag: String) {\n connector(id: $id) {\n id\n imageName\n logoUrl\n title\n connectorTag(imageTag: $imageTag, orDefault: true) {\n id\n connectorId\n imageTag\n defaultCaptureInterval\n disableBackfill\n documentationUrl\n endpointSpecSchema\n resourceSpecSchema\n protocol\n }\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/src/gql-types/graphql.ts b/src/gql-types/graphql.ts index f4e3bfb999..dc1254daa3 100644 --- a/src/gql-types/graphql.ts +++ b/src/gql-types/graphql.ts @@ -280,6 +280,70 @@ export type ConnectionHealthTestResult = { results: Array<StorageHealthItem>; }; +export type Connector = { + __typename?: 'Connector'; + /** Returns the ConnectorTag object for the given image tag, which must begin with a `:`. */ + connectorTag?: Maybe<ConnectorTag>; + /** Timestamp of when the connector was first created */ + createdAt: Scalars['DateTime']['output']; + /** + * Returns the default `ConnectorTag` for this connector. This is the one + * that should be used by default when publishing new tasks for this + * connector. There will only be a default image tag if at least one tag + * has successfully completed the connector Spec RPC. + */ + defaultImageTag?: Maybe<Scalars['String']['output']>; + /** Link to an external site with more information about the endpoint */ + externalUrl: Scalars['String']['output']; + /** Unique id of the connector */ + id: Scalars['Id']['output']; + /** Name of the conector's OCI (Docker) Container image, for example "ghcr.io/estuary/source-postgres" */ + imageName: Scalars['String']['output']; + /** The connector's logo image, represented as a URL per locale */ + logoUrl?: Maybe<Scalars['String']['output']>; + /** A longform description of this connector */ + longDescription?: Maybe<Scalars['String']['output']>; + /** Does Estuary's marketing team want this one to appear at the top of the results? */ + recommended: Scalars['Boolean']['output']; + /** Brief human readable description, at most a few sentences */ + shortDescription?: Maybe<Scalars['String']['output']>; + /** All the tags that are available for this connector. */ + tags: Array<ConnectorTagRef>; + /** The title, a few words at most */ + title?: Maybe<Scalars['String']['output']>; +}; + + +export type ConnectorConnectorTagArgs = { + imageTag?: InputMaybe<Scalars['String']['input']>; + orDefault: Scalars['Boolean']['input']; +}; + +export type ConnectorConnection = { + __typename?: 'ConnectorConnection'; + /** A list of edges. */ + edges: Array<ConnectorEdge>; + /** Information to aid in pagination. */ + pageInfo: PageInfo; +}; + +/** An edge in a connection. */ +export type ConnectorEdge = { + __typename?: 'ConnectorEdge'; + /** A cursor for use in pagination */ + cursor: Scalars['String']['output']; + /** The item at the end of the edge */ + node: Connector; +}; + +/** + * The type of task that the connector is used for. Note that derivation + * connectors do exist, but aren't yet represented in `connector_tags`. + */ +export type ConnectorProto = + | 'capture' + | 'materialization'; + /** The shape of a connector status, which matches that of an ops::Log. */ export type ConnectorStatus = { __typename?: 'ConnectorStatus'; @@ -296,6 +360,56 @@ export type ConnectorStatus = { ts: Scalars['DateTime']['output']; }; +export type ConnectorTag = { + __typename?: 'ConnectorTag'; + /** The id of the connector this tag relates to. */ + connectorId: Scalars['Id']['output']; + /** Time at which the ConnectorTag was created */ + createdAt: Scalars['DateTime']['output']; + /** + * The default interval between invocations of a capture using this connector tag. + * Formatted as HH:MM:SS. Only applicable to non-streaming (polling) capture connectors. + */ + defaultCaptureInterval?: Maybe<Scalars['String']['output']>; + /** Whether the UI should hide the backfill button for this connector */ + disableBackfill: Scalars['Boolean']['output']; + /** URL pointing to the documentation page for this connector */ + documentationUrl?: Maybe<Scalars['String']['output']>; + /** Endpoint specification JSON-Schema of the tagged connector */ + endpointSpecSchema?: Maybe<Scalars['JSON']['output']>; + /** Unique id of the connector tag */ + id: Scalars['Id']['output']; + /** The OCI Image tag value, including the leading `:`. For example `:v1` */ + imageTag: Scalars['String']['output']; + /** The protocol of the connector with this tag value */ + protocol?: Maybe<ConnectorProto>; + /** Resource specification JSON-Schema of the tagged connector */ + resourceSpecSchema?: Maybe<Scalars['JSON']['output']>; + /** Time at which the ConnectorTag was last updated */ + updatedAt: Scalars['DateTime']['output']; +}; + +export type ConnectorTagRef = { + __typename?: 'ConnectorTagRef'; + /** The canonical id of this connector tag */ + id: Scalars['Id']['output']; + /** The OCI image tag, includeing the leading `:`, for example `:v2` */ + imageTag: Scalars['String']['output']; + /** The protocol of this connector tag, if known */ + protocol?: Maybe<ConnectorProto>; + /** + * Returns whether a connector Spec RPC has ever been successful for this tag. + * Concretely, this is used to determine whether the tag could be used by the + * UI or flowctl for publishing tasks, because the Spec RPC populates the + * `endpointSpecSchema`, `resourceSpecSchema`, `protocol`, etc. + */ + specSucceeded: Scalars['Boolean']['output']; +}; + +export type ConnectorsFilter = { + protocol?: InputMaybe<ProtocolFilter>; +}; + /** Status info related to the controller */ export type Controller = { __typename?: 'Controller'; @@ -842,6 +956,10 @@ export type PrefixesBy = { minCapability: Capability; }; +export type ProtocolFilter = { + eq: ConnectorProto; +}; + /** Summary of a publication that was attempted by a controller. */ export type PublicationInfo = { __typename?: 'PublicationInfo'; @@ -913,6 +1031,31 @@ export type QueryRoot = { * prefixes. */ alerts: AlertConnection; + /** + * Returns information about a single connector, which may or may not have + * had a successful Spec RPC, and thus may or may not be usable in the + * Estuary UI. At least one parameter must be provided. If multiple + * parameters are provided, then the connector must match _both_ the image + * name and id parameters in order to be returned. + */ + connector?: Maybe<Connector>; + /** + * Returns the ConnectorTag for a given full (including the version) OCI + * image name. The returned tag may be different from the version in the + * image name. This would happen if there is no connector spec for the + * given tag, but one exists for a different tag. The return value will be + * null if either the connector image is unkown, or if there has not been a + * successful Spec for any version of that image. + */ + connectorTag?: Maybe<ConnectorTag>; + /** + * Returns a paginated list of connectors. This query only returns + * connectors that have at least one `ConnectorTag` that has had a + * successful Spec RPC. Connectors that have not had at least one + * successful Spec RPC cannot be used by the Estuary UI, and so are + * excluded here. + */ + connectors: ConnectorConnection; /** * Returns data planes accessible to the current user. * @@ -963,6 +1106,27 @@ export type QueryRootAlertsArgs = { }; +export type QueryRootConnectorArgs = { + id?: InputMaybe<Scalars['Id']['input']>; + imageName?: InputMaybe<Scalars['String']['input']>; +}; + + +export type QueryRootConnectorTagArgs = { + fullImageName?: InputMaybe<Scalars['String']['input']>; + id?: InputMaybe<Scalars['Id']['input']>; +}; + + +export type QueryRootConnectorsArgs = { + after?: InputMaybe<Scalars['String']['input']>; + before?: InputMaybe<Scalars['String']['input']>; + filter?: InputMaybe<ConnectorsFilter>; + first?: InputMaybe<Scalars['Int']['input']>; + last?: InputMaybe<Scalars['Int']['input']>; +}; + + export type QueryRootDataPlanesArgs = { after?: InputMaybe<Scalars['String']['input']>; before?: InputMaybe<Scalars['String']['input']>; @@ -1333,6 +1497,22 @@ export type AlertTypeQueryVariables = Exact<{ [key: string]: never; }>; export type AlertTypeQuery = { __typename?: 'QueryRoot', alertTypes: Array<{ __typename?: 'AlertTypeInfo', alertType: AlertType, description: string, displayName: string, isDefault: boolean, isSystem: boolean }> }; +export type ConnectorsGridQueryVariables = Exact<{ + filter?: InputMaybe<ConnectorsFilter>; + after?: InputMaybe<Scalars['String']['input']>; +}>; + + +export type ConnectorsGridQuery = { __typename?: 'QueryRoot', connectors: { __typename?: 'ConnectorConnection', edges: Array<{ __typename?: 'ConnectorEdge', cursor: string, node: { __typename?: 'Connector', id: any, imageName: string, logoUrl?: string | null, title?: string | null, recommended: boolean, shortDescription?: string | null, connectorTag?: { __typename?: 'ConnectorTag', id: any, connectorId: any, imageTag: string, documentationUrl?: string | null, protocol?: ConnectorProto | null } | null } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null } } }; + +export type SingleConnectorQueryVariables = Exact<{ + id: Scalars['Id']['input']; + imageTag?: InputMaybe<Scalars['String']['input']>; +}>; + + +export type SingleConnectorQuery = { __typename?: 'QueryRoot', connector?: { __typename?: 'Connector', id: any, imageName: string, logoUrl?: string | null, title?: string | null, connectorTag?: { __typename?: 'ConnectorTag', id: any, connectorId: any, imageTag: string, defaultCaptureInterval?: string | null, disableBackfill: boolean, documentationUrl?: string | null, endpointSpecSchema?: any | null, resourceSpecSchema?: any | null, protocol?: ConnectorProto | null } | null } | null }; + export type DataPlanesQueryVariables = Exact<{ after?: InputMaybe<Scalars['String']['input']>; }>; @@ -1459,6 +1639,8 @@ export const DeleteAlertSubscriptionMutationDocument = {"kind":"Document","defin export const AlertSubscriptionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AlertSubscriptions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"prefix"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Prefix"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"alertSubscriptions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"by"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"prefix"},"value":{"kind":"Variable","name":{"kind":"Name","value":"prefix"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"alertTypes"}},{"kind":"Field","name":{"kind":"Name","value":"catalogPrefix"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<AlertSubscriptionsQuery, AlertSubscriptionsQueryVariables>; export const UpdateAlertSubscriptionMutationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateAlertSubscriptionMutation"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"prefix"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Prefix"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"email"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"alertTypes"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"AlertType"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"detail"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateAlertSubscription"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"prefix"},"value":{"kind":"Variable","name":{"kind":"Name","value":"prefix"}}},{"kind":"Argument","name":{"kind":"Name","value":"email"},"value":{"kind":"Variable","name":{"kind":"Name","value":"email"}}},{"kind":"Argument","name":{"kind":"Name","value":"alertTypes"},"value":{"kind":"Variable","name":{"kind":"Name","value":"alertTypes"}}},{"kind":"Argument","name":{"kind":"Name","value":"detail"},"value":{"kind":"Variable","name":{"kind":"Name","value":"detail"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"catalogPrefix"}},{"kind":"Field","name":{"kind":"Name","value":"email"}}]}}]}}]} as unknown as DocumentNode<UpdateAlertSubscriptionMutationMutation, UpdateAlertSubscriptionMutationMutationVariables>; export const AlertTypeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AlertType"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"alertTypes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"alertType"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"isDefault"}},{"kind":"Field","name":{"kind":"Name","value":"isSystem"}}]}}]}}]} as unknown as DocumentNode<AlertTypeQuery, AlertTypeQueryVariables>; +export const ConnectorsGridDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ConnectorsGrid"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ConnectorsFilter"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"connectors"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"500"}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"imageName"}},{"kind":"Field","name":{"kind":"Name","value":"logoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"recommended"}},{"kind":"Field","name":{"kind":"Name","value":"shortDescription"}},{"kind":"Field","name":{"kind":"Name","value":"connectorTag"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orDefault"},"value":{"kind":"BooleanValue","value":true}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"connectorId"}},{"kind":"Field","name":{"kind":"Name","value":"imageTag"}},{"kind":"Field","name":{"kind":"Name","value":"documentationUrl"}},{"kind":"Field","name":{"kind":"Name","value":"protocol"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]} as unknown as DocumentNode<ConnectorsGridQuery, ConnectorsGridQueryVariables>; +export const SingleConnectorDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"SingleConnector"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Id"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"imageTag"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"connector"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"imageName"}},{"kind":"Field","name":{"kind":"Name","value":"logoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"connectorTag"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"imageTag"},"value":{"kind":"Variable","name":{"kind":"Name","value":"imageTag"}}},{"kind":"Argument","name":{"kind":"Name","value":"orDefault"},"value":{"kind":"BooleanValue","value":true}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"connectorId"}},{"kind":"Field","name":{"kind":"Name","value":"imageTag"}},{"kind":"Field","name":{"kind":"Name","value":"defaultCaptureInterval"}},{"kind":"Field","name":{"kind":"Name","value":"disableBackfill"}},{"kind":"Field","name":{"kind":"Name","value":"documentationUrl"}},{"kind":"Field","name":{"kind":"Name","value":"endpointSpecSchema"}},{"kind":"Field","name":{"kind":"Name","value":"resourceSpecSchema"}},{"kind":"Field","name":{"kind":"Name","value":"protocol"}}]}}]}}]}}]} as unknown as DocumentNode<SingleConnectorQuery, SingleConnectorQueryVariables>; export const DataPlanesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"DataPlanes"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"dataPlanes"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"100"}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"cloudProvider"}},{"kind":"Field","name":{"kind":"Name","value":"region"}},{"kind":"Field","name":{"kind":"Name","value":"isPublic"}},{"kind":"Field","name":{"kind":"Name","value":"fqdn"}},{"kind":"Field","name":{"kind":"Name","value":"cidrBlocks"}},{"kind":"Field","name":{"kind":"Name","value":"awsIamUserArn"}},{"kind":"Field","name":{"kind":"Name","value":"gcpServiceAccountEmail"}},{"kind":"Field","name":{"kind":"Name","value":"azureApplicationClientId"}},{"kind":"Field","name":{"kind":"Name","value":"azureApplicationName"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]} as unknown as DocumentNode<DataPlanesQuery, DataPlanesQueryVariables>; export const InviteLinksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"InviteLinks"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"inviteLinks"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"ssoProviderId"}},{"kind":"Field","name":{"kind":"Name","value":"catalogPrefix"}},{"kind":"Field","name":{"kind":"Name","value":"capability"}},{"kind":"Field","name":{"kind":"Name","value":"singleUse"}},{"kind":"Field","name":{"kind":"Name","value":"detail"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PageInfoFields"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PageInfoFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PageInfo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]} as unknown as DocumentNode<InviteLinksQuery, InviteLinksQueryVariables>; export const CreateInviteLinkDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateInviteLink"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"catalogPrefix"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Prefix"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"capability"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Capability"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"singleUse"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"detail"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createInviteLink"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"catalogPrefix"},"value":{"kind":"Variable","name":{"kind":"Name","value":"catalogPrefix"}}},{"kind":"Argument","name":{"kind":"Name","value":"capability"},"value":{"kind":"Variable","name":{"kind":"Name","value":"capability"}}},{"kind":"Argument","name":{"kind":"Name","value":"singleUse"},"value":{"kind":"Variable","name":{"kind":"Name","value":"singleUse"}}},{"kind":"Argument","name":{"kind":"Name","value":"detail"},"value":{"kind":"Variable","name":{"kind":"Name","value":"detail"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"catalogPrefix"}},{"kind":"Field","name":{"kind":"Name","value":"capability"}},{"kind":"Field","name":{"kind":"Name","value":"singleUse"}},{"kind":"Field","name":{"kind":"Name","value":"detail"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]} as unknown as DocumentNode<CreateInviteLinkMutation, CreateInviteLinkMutationVariables>; From 2bc46b325d208548e9856a8ddfea59cb8c570032 Mon Sep 17 00:00:00 2001 From: Travis Jenkins <travis@estuary.dev> Date: Fri, 10 Apr 2026 11:49:47 -0400 Subject: [PATCH 23/30] Cleaning up some old stuff Adding basic eventing in Handling exceptions --- .../Entity/Edit/useInitializeTaskDraft.ts | 2 -- .../Entity/hooks/useEntityEditNavigate.ts | 1 - src/context/ConnectorTag.tsx | 18 ++++++++++++++---- src/services/types.ts | 2 +- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/components/shared/Entity/Edit/useInitializeTaskDraft.ts b/src/components/shared/Entity/Edit/useInitializeTaskDraft.ts index f72619c3d2..1824937456 100644 --- a/src/components/shared/Entity/Edit/useInitializeTaskDraft.ts +++ b/src/components/shared/Entity/Edit/useInitializeTaskDraft.ts @@ -256,8 +256,6 @@ function useInitializeTaskDraft() { [GlobalSearchParams.LIVE_SPEC_ID]: liveSpecId, [GlobalSearchParams.CONNECTOR_ID]: formatOldUuidToGql(task.connector_id), - [GlobalSearchParams.CONNECTOR_IMAGE_TAG]: - task.connector_image_tag, [GlobalSearchParams.LAST_PUB_ID]: task.last_pub_id, }, diff --git a/src/components/shared/Entity/hooks/useEntityEditNavigate.ts b/src/components/shared/Entity/hooks/useEntityEditNavigate.ts index aba332866c..cb55174f04 100644 --- a/src/components/shared/Entity/hooks/useEntityEditNavigate.ts +++ b/src/components/shared/Entity/hooks/useEntityEditNavigate.ts @@ -12,7 +12,6 @@ import { getPathWithParams } from 'src/utils/misc-utils'; interface BaseSearchParams { [GlobalSearchParams.CONNECTOR_ID]: string; - [GlobalSearchParams.CONNECTOR_IMAGE_TAG]: string; [GlobalSearchParams.LIVE_SPEC_ID]: string; [GlobalSearchParams.LAST_PUB_ID]: string; } diff --git a/src/context/ConnectorTag.tsx b/src/context/ConnectorTag.tsx index 0c81869954..77fe9cd537 100644 --- a/src/context/ConnectorTag.tsx +++ b/src/context/ConnectorTag.tsx @@ -11,7 +11,7 @@ import ErrorComponent from 'src/components/shared/Error'; import useGlobalSearchParams, { GlobalSearchParams, } from 'src/hooks/searchParams/useGlobalSearchParams'; -import { logRocketConsole } from 'src/services/shared'; +import { logRocketEvent } from 'src/services/shared'; import { BASE_ERROR } from 'src/services/supabase'; import { hasLength } from 'src/utils/misc-utils'; @@ -37,7 +37,7 @@ export const ConnectorTagProvider = ({ children }: BaseComponentProps) => { const [connectorTag, setConnectorTag] = useState<ConnectorTagData | null>( null ); - const [fetchError, setFetchError] = useState<string | null>(null); + const [fetchError, setFetchError] = useState<boolean | null>(false); useEffect(() => { if (!hasLength(connectorId)) { @@ -56,8 +56,12 @@ export const ConnectorTagProvider = ({ children }: BaseComponentProps) => { const connectorTagData = connector?.connectorTag; if (error || !connector || !connectorTagData) { - logRocketConsole('Failed to fetch connector tag', error); - setFetchError('Failed to load connector information'); + logRocketEvent('Connectors:fetch', { + noConnectors: !connector, + noConnectorTags: !connectorTagData, + status: 'failure', + }); + setFetchError(true); return; } @@ -70,6 +74,12 @@ export const ConnectorTagProvider = ({ children }: BaseComponentProps) => { title: connector.title, }, }); + }) + .catch((e) => { + logRocketEvent('Connectors:fetch', { + status: 'exception', + }); + setFetchError(true); }); }, [client, connectorId, imageTag]); diff --git a/src/services/types.ts b/src/services/types.ts index a167a0a587..da1cc6e8ff 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -4,6 +4,7 @@ export type KnownEvents = | 'AlertSubscription' | 'Auth' | 'Confirmation' + | 'Connectors' | 'DataPlaneGateway' | 'Data_Flow_Reset' | 'EndpointConfig' @@ -33,7 +34,6 @@ export enum CustomEvents { CAPTURE_TEST = 'Capture_Test', COLLECTION_CREATE = 'Collection_Create', COLLECTION_SCHEMA = 'CollectionSchema', - CONNECTOR_VERSION_MISSING = 'Connector_Version:Missing', DATA_PLANE_SELECTOR = 'Data_Plane_Selector', DATE_TIME_PICKER_CHANGE = 'Date_Time_Picker:Change', DIRECTIVE = 'Directive', From c2d3084000971023923590bfaf6c90b824af6663 Mon Sep 17 00:00:00 2001 From: Travis Jenkins <travis@estuary.dev> Date: Fri, 10 Apr 2026 11:54:42 -0400 Subject: [PATCH 24/30] More cleanup --- src/stores/DetailsForm/hooks.ts | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 src/stores/DetailsForm/hooks.ts diff --git a/src/stores/DetailsForm/hooks.ts b/src/stores/DetailsForm/hooks.ts deleted file mode 100644 index 30ba3db771..0000000000 --- a/src/stores/DetailsForm/hooks.ts +++ /dev/null @@ -1,20 +0,0 @@ -import useGlobalSearchParams, { - GlobalSearchParams, -} from 'src/hooks/searchParams/useGlobalSearchParams'; -import { useDetailsFormStore } from 'src/stores/DetailsForm/Store'; - -// Selector hooks -export const useDetailsForm_changed_connectorId = () => { - const connectorId = useGlobalSearchParams(GlobalSearchParams.CONNECTOR_ID); - - return useDetailsFormStore( - (state) => - state.details.data.connectorImage.connectorId !== - state.previousDetails.data.connectorImage.connectorId || - Boolean( - connectorId && - connectorId !== - state.details.data.connectorImage.connectorId - ) - ); -}; From 131486273059d817fe8385e4a41283dff90ab95d Mon Sep 17 00:00:00 2001 From: Travis Jenkins <travis@estuary.dev> Date: Fri, 10 Apr 2026 12:45:31 -0400 Subject: [PATCH 25/30] Hook no longer needed --- src/stores/DetailsForm/hooks.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/stores/DetailsForm/hooks.ts diff --git a/src/stores/DetailsForm/hooks.ts b/src/stores/DetailsForm/hooks.ts new file mode 100644 index 0000000000..25c7496401 --- /dev/null +++ b/src/stores/DetailsForm/hooks.ts @@ -0,0 +1,20 @@ +import { useConnectorTag } from 'src/context/ConnectorTag'; +import useGlobalSearchParams, { + GlobalSearchParams, +} from 'src/hooks/searchParams/useGlobalSearchParams'; +import { useDetailsFormStore } from 'src/stores/DetailsForm/Store'; + +export const useDetailsForm_changed_connectorId = (): boolean => { + const { connectorId: currentConnectorId } = useConnectorTag(); + + const previousConnectorId = useDetailsFormStore( + (state) => state.previousConnectorId + ); + + const urlConnectorId = useGlobalSearchParams(GlobalSearchParams.CONNECTOR_ID); + + return ( + currentConnectorId !== previousConnectorId || + currentConnectorId !== urlConnectorId + ); +}; From 44a142723571197a5cd40c4384a0bbe5661bd50a Mon Sep 17 00:00:00 2001 From: Travis Jenkins <travis@estuary.dev> Date: Fri, 10 Apr 2026 12:56:08 -0400 Subject: [PATCH 26/30] No clue why I thought we could remove this --- src/components/shared/Entity/Edit/useInitializeTaskDraft.ts | 2 ++ src/components/shared/Entity/hooks/useEntityEditNavigate.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/src/components/shared/Entity/Edit/useInitializeTaskDraft.ts b/src/components/shared/Entity/Edit/useInitializeTaskDraft.ts index 1824937456..f72619c3d2 100644 --- a/src/components/shared/Entity/Edit/useInitializeTaskDraft.ts +++ b/src/components/shared/Entity/Edit/useInitializeTaskDraft.ts @@ -256,6 +256,8 @@ function useInitializeTaskDraft() { [GlobalSearchParams.LIVE_SPEC_ID]: liveSpecId, [GlobalSearchParams.CONNECTOR_ID]: formatOldUuidToGql(task.connector_id), + [GlobalSearchParams.CONNECTOR_IMAGE_TAG]: + task.connector_image_tag, [GlobalSearchParams.LAST_PUB_ID]: task.last_pub_id, }, diff --git a/src/components/shared/Entity/hooks/useEntityEditNavigate.ts b/src/components/shared/Entity/hooks/useEntityEditNavigate.ts index cb55174f04..aba332866c 100644 --- a/src/components/shared/Entity/hooks/useEntityEditNavigate.ts +++ b/src/components/shared/Entity/hooks/useEntityEditNavigate.ts @@ -12,6 +12,7 @@ import { getPathWithParams } from 'src/utils/misc-utils'; interface BaseSearchParams { [GlobalSearchParams.CONNECTOR_ID]: string; + [GlobalSearchParams.CONNECTOR_IMAGE_TAG]: string; [GlobalSearchParams.LIVE_SPEC_ID]: string; [GlobalSearchParams.LAST_PUB_ID]: string; } From 90bbaa639419101b596fbfb2c30bd2a290ca0de5 Mon Sep 17 00:00:00 2001 From: Travis Jenkins <travis@estuary.dev> Date: Fri, 10 Apr 2026 12:58:45 -0400 Subject: [PATCH 27/30] Adding support check wrapper around json test forms --- src/components/shared/guards/SupportRole.tsx | 19 +++++++++++++++++++ src/context/Router/index.tsx | 9 ++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 src/components/shared/guards/SupportRole.tsx diff --git a/src/components/shared/guards/SupportRole.tsx b/src/components/shared/guards/SupportRole.tsx new file mode 100644 index 0000000000..4c624091b3 --- /dev/null +++ b/src/components/shared/guards/SupportRole.tsx @@ -0,0 +1,19 @@ +import type { InputBaseComponentProps } from '@mui/material'; + +import { useUserInfoSummaryStore } from 'src/context/UserInfoSummary/useUserInfoSummaryStore'; +import { isProduction } from 'src/utils/env-utils'; + +function HasSupportRoleGuard({ children }: InputBaseComponentProps) { + const hasSupportAccess = useUserInfoSummaryStore( + (state) => state.hasSupportAccess + ); + + if (!hasSupportAccess && isProduction) { + return null; + } + + // eslint-disable-next-line react/jsx-no-useless-fragment + return <>{children}</>; +} + +export default HasSupportRoleGuard; diff --git a/src/context/Router/index.tsx b/src/context/Router/index.tsx index bad40abae7..7fe20af556 100644 --- a/src/context/Router/index.tsx +++ b/src/context/Router/index.tsx @@ -16,6 +16,7 @@ import AdminBilling from 'src/components/admin/Billing'; import AdminConnectors from 'src/components/admin/Connectors'; import AdminSettings from 'src/components/admin/Settings'; import { ErrorImporting } from 'src/components/shared/ErrorImporting'; +import HasSupportRoleGuard from 'src/components/shared/guards/SupportRole'; import { AuthenticatedOnlyContext } from 'src/context/Authenticated'; import { DashboardWelcomeProvider } from 'src/context/DashboardWelcome'; import { EntityContextProvider } from 'src/context/EntityContext'; @@ -760,9 +761,11 @@ const router = createBrowserRouter( <ErrorBoundary FallbackComponent={ErrorImporting} > - <EntityContextProvider value="capture"> - <TestJsonForms /> - </EntityContextProvider> + <HasSupportRoleGuard> + <EntityContextProvider value="capture"> + <TestJsonForms /> + </EntityContextProvider> + </HasSupportRoleGuard> </ErrorBoundary> } /> From ee2a6c5a05105351573c3d106c794e84973bcaec Mon Sep 17 00:00:00 2001 From: Travis Jenkins <travis@estuary.dev> Date: Fri, 10 Apr 2026 13:04:58 -0400 Subject: [PATCH 28/30] Hook not needed anymore --- src/stores/DetailsForm/hooks.ts | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 src/stores/DetailsForm/hooks.ts diff --git a/src/stores/DetailsForm/hooks.ts b/src/stores/DetailsForm/hooks.ts deleted file mode 100644 index 25c7496401..0000000000 --- a/src/stores/DetailsForm/hooks.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { useConnectorTag } from 'src/context/ConnectorTag'; -import useGlobalSearchParams, { - GlobalSearchParams, -} from 'src/hooks/searchParams/useGlobalSearchParams'; -import { useDetailsFormStore } from 'src/stores/DetailsForm/Store'; - -export const useDetailsForm_changed_connectorId = (): boolean => { - const { connectorId: currentConnectorId } = useConnectorTag(); - - const previousConnectorId = useDetailsFormStore( - (state) => state.previousConnectorId - ); - - const urlConnectorId = useGlobalSearchParams(GlobalSearchParams.CONNECTOR_ID); - - return ( - currentConnectorId !== previousConnectorId || - currentConnectorId !== urlConnectorId - ); -}; From 13f2697d09cd24ed33b4ef6e75cec195d87514b2 Mon Sep 17 00:00:00 2001 From: Travis Jenkins <travis@estuary.dev> Date: Fri, 10 Apr 2026 13:11:21 -0400 Subject: [PATCH 29/30] This is not needed for the test pages --- src/stores/DetailsForm/useDetailsFormHydrator.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/stores/DetailsForm/useDetailsFormHydrator.ts b/src/stores/DetailsForm/useDetailsFormHydrator.ts index e388d997db..0d6c6ea1b4 100644 --- a/src/stores/DetailsForm/useDetailsFormHydrator.ts +++ b/src/stores/DetailsForm/useDetailsFormHydrator.ts @@ -119,17 +119,6 @@ export const useDetailsFormHydrator = () => { } if (workflow === 'test_json_forms') { - setDetails_connector({ - connectorId: '', - iconPath: '', - id: '', - imageName: '', - imagePath: '', - imageTag: '', - }); - - setHydrationErrorsExist(true); - setHydrated(true); return Promise.resolve(); From 23997dfd3323081a0f4315ac78c31d30ea22e2af Mon Sep 17 00:00:00 2001 From: Travis Jenkins <travis@estuary.dev> Date: Fri, 10 Apr 2026 13:26:34 -0400 Subject: [PATCH 30/30] some formatting issues and ignoring storybook --- .eslintrc.cjs | 1 + src/components/capture/Edit.tsx | 4 +--- src/components/materialization/Create/index.tsx | 4 +--- src/components/materialization/Edit.tsx | 4 +--- src/components/shared/Entity/DetailsForm/useConnectorField.ts | 4 +++- 5 files changed, 7 insertions(+), 10 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index b03f4b6131..4675da6187 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -11,6 +11,7 @@ module.exports = { '__mocks__', 'playwright-tests/', 'src/gql-types/', + 'storybook-static/', ], plugins: ['formatjs', 'unused-imports', 'no-relative-import-paths'], parserOptions: { diff --git a/src/components/capture/Edit.tsx b/src/components/capture/Edit.tsx index ecc019a9fe..b2cac794af 100644 --- a/src/components/capture/Edit.tsx +++ b/src/components/capture/Edit.tsx @@ -74,9 +74,7 @@ function CaptureEdit() { /> } RediscoverButton={ - <RediscoverButton - entityType={entityType} - /> + <RediscoverButton entityType={entityType} /> } /> </MutateDraftSpecProvider> diff --git a/src/components/materialization/Create/index.tsx b/src/components/materialization/Create/index.tsx index 8d832d0cd3..4d0187b76c 100644 --- a/src/components/materialization/Create/index.tsx +++ b/src/components/materialization/Create/index.tsx @@ -62,9 +62,7 @@ function MaterializationCreate() { draftSpecMetadata={draftSpecsMetadata} Toolbar={ <EntityToolbar - GenerateButton={ - <MaterializeGenerateButton /> - } + GenerateButton={<MaterializeGenerateButton />} primaryButtonProps={{ disabled: !draftId, logEvent: CustomEvents.MATERIALIZATION_CREATE, diff --git a/src/components/materialization/Edit.tsx b/src/components/materialization/Edit.tsx index fcd6783e2a..c3bdd3be99 100644 --- a/src/components/materialization/Edit.tsx +++ b/src/components/materialization/Edit.tsx @@ -54,9 +54,7 @@ function MaterializationEdit() { draftSpecMetadata={draftSpecsMetadata} toolbar={ <EntityToolbar - GenerateButton={ - <MaterializeGenerateButton /> - } + GenerateButton={<MaterializeGenerateButton />} primaryButtonProps={{ disabled: !draftId, logEvent: CustomEvents.MATERIALIZATION_EDIT, diff --git a/src/components/shared/Entity/DetailsForm/useConnectorField.ts b/src/components/shared/Entity/DetailsForm/useConnectorField.ts index 2497dd5f36..c1a7460c2a 100644 --- a/src/components/shared/Entity/DetailsForm/useConnectorField.ts +++ b/src/components/shared/Entity/DetailsForm/useConnectorField.ts @@ -23,7 +23,9 @@ export default function useConnectorField( return [ { const: buildConnectorImageFromTag(connectorTag), - title: connectorTag.connector.title ?? connectorTag.connector.imageName, + title: + connectorTag.connector.title ?? + connectorTag.connector.imageName, }, ]; }, [connectorTag]);