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/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( - defaultTableFilter( - 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, handleFailure); - - return data; -}; - -const getSingleConnectorWithTag = async (connectorId: string) => { - const data = await supabaseRetry( - () => - requiredConnectorColumnsExist( - supabaseClient - .from(TABLES.CONNECTORS) - .select(CONNECTOR_WITH_TAG_QUERY) - .eq('id', connectorId), - 'connector_tags' - ), - 'getSingleConnectorWithTag' - ).then(handleSuccess, handleFailure); - - return data; -}; - -export { - getConnectors, - getConnectors_detailsFormTestPage, - getSingleConnectorWithTag, -}; diff --git a/src/api/gql/connectors.ts b/src/api/gql/connectors.ts new file mode 100644 index 0000000000..f4973d32a8 --- /dev/null +++ b/src/api/gql/connectors.ts @@ -0,0 +1,59 @@ +import type { ConnectorsGridQuery } from 'src/gql-types/graphql'; + +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: 500, after: $after, filter: $filter) { + edges { + cursor + node { + id + imageName + logoUrl + title + recommended + shortDescription + connectorTag(orDefault: true) { + id + connectorId + imageTag + documentationUrl + protocol + } + } + } + pageInfo { + hasNextPage + endCursor + } + } + } +`); + +export const CONNECTOR_BY_ID_QUERY = graphql(` + query SingleConnector($id: Id!, $imageTag: String) { + connector(id: $id) { + id + imageName + logoUrl + title + connectorTag(imageTag: $imageTag, orDefault: true) { + id + connectorId + imageTag + defaultCaptureInterval + disableBackfill + documentationUrl + endpointSpecSchema + resourceSpecSchema + protocol + } + } + } +`); 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, 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/liveSpecsExt.ts b/src/api/liveSpecsExt.ts index 9d504dcbb9..ac1b213bb2 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/api/types.ts b/src/api/types.ts index 304858ab84..11d406c4a7 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -1,53 +1,6 @@ -import type { CONNECTOR_NAME } from 'src/api/shared'; -import type { Entity, EntityWithCreateWorkflow, Schema } from 'src/types'; +import type { Entity } from 'src/types'; import type { AlertSubscription } from 'src/types/gql'; -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; -} - -// 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; - 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[]; -} - export interface DraftSpecData { spec: any; catalog_name?: string; diff --git a/src/components/capture/Create/index.tsx b/src/components/capture/Create/index.tsx index 87691f6ab7..a2035ea88b 100644 --- a/src/components/capture/Create/index.tsx +++ b/src/components/capture/Create/index.tsx @@ -7,12 +7,10 @@ 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'; 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'; @@ -28,19 +26,13 @@ function CaptureCreate() { 'https://docs.estuary.dev/guides/create-dataflow/#create-a-capture', }); - 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,12 +54,6 @@ function CaptureCreate() { } }, [mutateDraftSpecs, mutate_advancedEditor]); - // Reset the catalog if the connector changes - useEffect(() => { - setDraftId(null); - setInitiateDiscovery(true); - }, [setDraftId, setInitiateDiscovery, imageTag]); - // If the name changed we need to make sure we run discovery again useEffect(() => { if (entityNameChanged) { @@ -91,13 +77,11 @@ function CaptureCreate() { logEvent: CustomEvents.CAPTURE_CREATE, }} secondaryButtonProps={{ - disabled: !hasConnectors, logEvent: CustomEvents.CAPTURE_TEST, }} GenerateButton={ } RediscoverButton={ - + } /> diff --git a/src/components/capture/Edit.tsx b/src/components/capture/Edit.tsx index 3ed4292f9e..b2cac794af 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,22 +64,17 @@ function CaptureEdit() { logEvent: CustomEvents.CAPTURE_EDIT, }} secondaryButtonProps={{ - disabled: !hasConnectors, logEvent: CustomEvents.CAPTURE_TEST, }} GenerateButton={ } /> } RediscoverButton={ - + } /> 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 {connectorTitle}; + return {connectorTag.connector.title}; }; 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={ >; }; } -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} > 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 ( { - return getConnectors(searchQuery, 'asc', protocol); - }, [searchQuery, protocol]); + const variables = useMemo( + () => ({ + filter: protocol + ? { protocol: { eq: protocol as ConnectorProto } } + : undefined, + }), + [protocol] + ); - const { data: selectResponse, isValidating, error } = useQuery(query); + const [{ data: queryData, fetching, error }] = useQuery({ + query: CONNECTORS_QUERY, + variables, + }); - const selectData = useMemo(() => selectResponse ?? [], [selectResponse]); + const selectData = useMemo(() => { + const nodes = (queryData?.connectors.edges ?? []) + .map((edge) => edge.node) + .filter( + ( + node + ): node is typeof node & { + connectorTag: NonNullable; + } => node.connectorTag !== null + ); + + 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 +81,12 @@ export default function ConnectorCards({ ); - const primaryCtaClick = (row: ConnectorWithTagQuery) => { - navigateToCreate(row.connector_tags[0].protocol, { - id: row.connector_tags[0].connector_id, + const primaryCtaClick = ( + entityType: EntityWithCreateWorkflow, + connectorId: string + ) => { + navigateToCreate(entityType, { + id: connectorId, advanceToForm: true, expressWorkflow: condensed, }); @@ -72,9 +102,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 +144,40 @@ export default function ConnectorCards({ return ( <> {selectData - .map((row) => { + .map((node) => { + const { connectorTag } = node; const ConnectorCard = condensed ? Card : LegacyCard; + const entityType = connectorTag.protocol; + + // TODO (GQL:connector) how to better handle with typing? + if (!entityType) { + return null; + } return ( primaryCtaClick(row)} - Detail={} - entityType={row.connector_tags[0].protocol} + key={`connector-card-${node.id}`} + clickHandler={() => + primaryCtaClick( + entityType, + connectorTag.connectorId + ) + } + docsUrl={connectorTag.documentationUrl ?? ''} + entityType={entityType} + recommended={node.recommended} + Detail={ + + } Logo={ } - recommended={row.recommended} Title={ } 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; } diff --git a/src/components/materialization/Create/index.tsx b/src/components/materialization/Create/index.tsx index b608f02557..4d0187b76c 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 @@ -66,17 +62,12 @@ function MaterializationCreate() { draftSpecMetadata={draftSpecsMetadata} Toolbar={ <EntityToolbar - GenerateButton={ - <MaterializeGenerateButton - disabled={!hasConnectors} - /> - } + GenerateButton={<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..c3bdd3be99 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(); @@ -58,17 +54,12 @@ function MaterializationEdit() { draftSpecMetadata={draftSpecsMetadata} toolbar={ <EntityToolbar - GenerateButton={ - <MaterializeGenerateButton - disabled={!hasConnectors} - /> - } + GenerateButton={<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/components/shared/Entity/DetailsForm/useConnectorField.ts b/src/components/shared/Entity/DetailsForm/useConnectorField.ts index b85c15698b..c1a7460c2a 100644 --- a/src/components/shared/Entity/DetailsForm/useConnectorField.ts +++ b/src/components/shared/Entity/DetailsForm/useConnectorField.ts @@ -1,104 +1,34 @@ -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 { 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_RE } from 'src/validation'; +import { buildConnectorImageFromTag } from 'src/utils/connector-utils'; export default function useConnectorField( entityType: EntityWithCreateWorkflow ) { - const connectorId = useGlobalSearchParams(GlobalSearchParams.CONNECTOR_ID); - 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); - - useEffect(() => { - if (connectorId && hasLength(connectorTags) && connectorIdChanged) { - connectorTags.find((connector) => { - const connectorTag = evaluateConnectorVersions(connector); - - const connectorLocated = - connectorTag.connector_id === connectorId; - - if (connectorLocated) { - setDetails_connector(getConnectorMetadata(connector)); - } - - 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) - ? { - connectorId, - existingImageTag: connectorImageTag, - } - : undefined; - }, [connectorId, connectorImageTag, isEdit]); + const connectorTag = useWorkflowStore((state) => state.connectorMetadata); 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, - }); - }); + if (!connectorTag) { + return []; } - return response; - }, [connectorTags, versionEvaluationOptions]); + return [ + { + const: buildConnectorImageFromTag(connectorTag), + title: + connectorTag.connector.title ?? + connectorTag.connector.imageName, + }, + ]; + }, [connectorTag]); const connectorSchema = useMemo( () => ({ @@ -122,37 +52,5 @@ export default function useConnectorField( }, }; - const setConnector = useCallback( - (details: Details, selectedDataPlaneId: string | undefined) => { - const selectedConnectorId = details.data.connectorImage.connectorId; - - if ( - MAC_ADDR_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, - }); - } - } - }, - [ - connectorId, - entityType, - navigateToCreate, - originalConnectorImage, - setDetails_connector, - 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..dec51be965 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 } = @@ -81,14 +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); - setConnector(details, dataPlaneOption?.id); - setCatalogName({ root: details.data.entityName.substring(tenant.length), tenant, diff --git a/src/components/shared/Entity/Edit/useInitializeTaskDraft.ts b/src/components/shared/Entity/Edit/useInitializeTaskDraft.ts index 6e7888dcab..f72619c3d2 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,9 @@ function useInitializeTaskDraft() { { [GlobalSearchParams.LIVE_SPEC_ID]: liveSpecId, [GlobalSearchParams.CONNECTOR_ID]: - task.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/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/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/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/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/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/ConnectorTag.tsx b/src/context/ConnectorTag.tsx new file mode 100644 index 0000000000..77fe9cd537 --- /dev/null +++ b/src/context/ConnectorTag.tsx @@ -0,0 +1,122 @@ +import type { SingleConnectorQuery } from 'src/gql-types/graphql'; +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'; +import ErrorComponent from 'src/components/shared/Error'; +import useGlobalSearchParams, { + GlobalSearchParams, +} from 'src/hooks/searchParams/useGlobalSearchParams'; +import { logRocketEvent } 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 imageTag = useGlobalSearchParams( + GlobalSearchParams.CONNECTOR_IMAGE_TAG + ); + const client = useClient(); + const intl = useIntl(); + + const [connectorTag, setConnectorTag] = useState<ConnectorTagData | null>( + null + ); + const [fetchError, setFetchError] = useState<boolean | null>(false); + + useEffect(() => { + if (!hasLength(connectorId)) { + return; + } + + client + .query( + CONNECTOR_BY_ID_QUERY, + { id: connectorId, imageTag }, + { requestPolicy: 'network-only' } + ) + .toPromise() + .then(({ data, error }) => { + const connector = data?.connector; + const connectorTagData = connector?.connectorTag; + + if (error || !connector || !connectorTagData) { + logRocketEvent('Connectors:fetch', { + noConnectors: !connector, + noConnectorTags: !connectorTagData, + status: 'failure', + }); + setFetchError(true); + return; + } + + setConnectorTag({ + ...connectorTagData, + connector: { + id: connector.id, + imageName: connector.imageName, + logoUrl: connector.logoUrl, + title: connector.title, + }, + }); + }) + .catch((e) => { + logRocketEvent('Connectors:fetch', { + status: 'exception', + }); + setFetchError(true); + }); + }, [client, connectorId, imageTag]); + + if (fetchError) { + return ( + <ErrorComponent + condensed + error={{ + ...BASE_ERROR, + message: intl.formatMessage({ + id: 'workflow.connectorTag.error.message', + }), + }} + /> + ); + } + + // While loading we don't want to show anything + 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/context/Router/index.tsx b/src/context/Router/index.tsx index 87cd00530a..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'; @@ -753,23 +754,25 @@ const router = createBrowserRouter( /> </Route> - {!isProduction ? ( - <> - <Route - path="test/jsonforms" - element={ - <ErrorBoundary - FallbackComponent={ErrorImporting} - > + <Route path="test"> + <Route + path="jsonforms" + element={ + <ErrorBoundary + FallbackComponent={ErrorImporting} + > + <HasSupportRoleGuard> <EntityContextProvider value="capture"> <TestJsonForms /> </EntityContextProvider> - </ErrorBoundary> - } - /> + </HasSupportRoleGuard> + </ErrorBoundary> + } + /> + {!isProduction ? ( <Route - path="test/gql" + path="gql" element={ <ErrorBoundary FallbackComponent={ErrorImporting} @@ -778,8 +781,8 @@ const router = createBrowserRouter( </ErrorBoundary> } /> - </> - ) : null} + ) : null} + </Route> <Route path={authenticatedRoutes.pageNotFound.path} 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>; diff --git a/src/gql-types/schema.graphql b/src/gql-types/schema.graphql index 2b38a2d8c6..350d2a232d 100644 --- a/src/gql-types/schema.graphql +++ b/src/gql-types/schema.graphql @@ -284,6 +284,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 { """ @@ -304,6 +387,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.""" @@ -811,6 +955,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""" @@ -891,6 +1039,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/hooks/connectors/shared.ts b/src/hooks/connectors/shared.ts deleted file mode 100644 index 2e7c554e7e..0000000000 --- a/src/hooks/connectors/shared.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { CONNECTOR_NAME, CONNECTOR_RECOMMENDED } from 'src/api/shared'; - -////////////////////////// -// useConnectors -////////////////////////// -export interface Connector { - id: string; - title: { 'en-US': string }; - image_name: string; -} - -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/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/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; 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', 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/pages/dev/TestJsonForms.tsx b/src/pages/dev/TestJsonForms.tsx index 588c359292..0ca1785ba9 100644 --- a/src/pages/dev/TestJsonForms.tsx +++ b/src/pages/dev/TestJsonForms.tsx @@ -1,100 +1,74 @@ -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, + Typography, } from '@mui/material'; import { JsonForms } from '@jsonforms/react'; import Editor from '@monaco-editor/react'; -import { useIntl } from 'react-intl'; +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 { 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 { TABLES } from 'src/services/supabase'; import { useDetailsFormStore } from 'src/stores/DetailsForm/Store'; const TestJsonForms = () => { - const intl = useIntl(); - const { connectors } = useConnectors(); + const { data, error: serverError } = 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>(''); 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 = () => - 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) { @@ -105,6 +79,7 @@ const TestJsonForms = () => { try { setFormData({}); setSchema(null); + setError(null); const parsedSchema = JSON.parse(schemaInput); const resolved = await getDereffedSchema(parsedSchema); @@ -128,8 +103,6 @@ const TestJsonForms = () => { } }; - const searchParams = new URLSearchParams(window.location.search); - useUnmount(() => { resetDetailsForm(); }); @@ -143,17 +116,32 @@ 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} <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 +158,43 @@ 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} + > + <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> + </FormControl> <Editor height="500px" @@ -223,28 +215,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/services/types.ts b/src/services/types.ts index e6b0594edb..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,8 +34,6 @@ export enum CustomEvents { CAPTURE_TEST = 'Capture_Test', 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/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..bea7d874ad 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,23 @@ 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)); + } else { + get().setHydrationErrorsExist(true); } - 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>; 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}</>; -}; diff --git a/src/stores/DetailsForm/Store.ts b/src/stores/DetailsForm/Store.ts index 36dd2cc550..5a660589e8 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,80 +20,12 @@ 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 getInitialStateData = (): Pick< DetailsFormState, | 'connectors' @@ -115,7 +36,6 @@ const getInitialStateData = (): Pick< | 'draftedEntityName' | 'entityNameChanged' | 'previousDetails' - | 'unsupportedConnectorVersion' > => ({ connectors: [], @@ -124,7 +44,6 @@ const getInitialStateData = (): Pick< details: initialDetails, errorsExist: true, - unsupportedConnectorVersion: false, draftedEntityName: '', entityNameChanged: false, @@ -234,25 +153,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) => { @@ -306,113 +206,10 @@ 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, - connector_tag_id, - 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().setUnsupportedConnectorVersion( - connectorImage.id, - connector_tag_id - ); - - 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( 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 - ) - ); -}; 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 f17bfbc5c0..0d6c6ea1b4 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,11 @@ 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); -}; +import { buildConnectorImageFromTag } from 'src/utils/connector-utils'; export const useDetailsFormHydrator = () => { + const connectorTag = useConnectorTag(); + const dataPlaneId = useGlobalSearchParams(GlobalSearchParams.DATA_PLANE_ID); const liveSpecId = useGlobalSearchParams(GlobalSearchParams.LIVE_SPEC_ID); @@ -44,19 +34,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 +47,12 @@ export const useDetailsFormHydrator = () => { workflow === 'materialization_create'; if (createWorkflow) { - const connectorImage = await getConnectorImage( - connectorId, - connectorMetadata - ); + const connectorImage = buildConnectorImageFromTag(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 +70,7 @@ export const useDetailsFormHydrator = () => { setHydrated(true); - return Promise.resolve({ - connectorTagId: hydratedDetails.data.connectorImage.id, - }); + return Promise.resolve(); } if (liveSpecId) { @@ -108,23 +80,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 = buildConnectorImageFromTag(connectorTag); const dataPlaneOptions = await evaluateDataPlaneOptions( catalog_name, @@ -136,12 +102,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 +110,24 @@ export const useDetailsFormHydrator = () => { }, }; - setUnsupportedConnectorVersion( - connectorImage.id, - connector_tag_id - ); - 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, - iconPath: '', - 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 +138,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 deleted file mode 100644 index a457b306cb..0000000000 --- a/src/stores/Workflow/hooks.ts +++ /dev/null @@ -1,47 +0,0 @@ -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 => { - return useWorkflowStore( - useShallow((state) => { - if (!connectorId || !hasLength(state.connectorMetadata)) { - return undefined; - } - - return state.connectorMetadata.find( - (connector) => connector.id === connectorId - )?.[property]; - }) - ); -}; - -export const useWorkflowStore_connectorTagProperty = < - K extends keyof ConnectorTag, ->( - connectorId: string | null | undefined, - connectorTagId: string | null | undefined, - property: K -): ConnectorTag[K] | undefined => { - return useWorkflowStore( - useShallow((state) => { - if (!connectorId || !hasLength(state.connectorMetadata)) { - return undefined; - } - - return state.connectorMetadata - .find((connector) => connector.id === connectorId) - ?.connector_tags.find((tag) => tag.id === connectorTagId)?.[ - property - ]; - }) - ); -}; 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 f8027c2531..d2c250e331 100644 --- a/src/stores/Workflow/useWorkflowHydrator.ts +++ b/src/stores/Workflow/useWorkflowHydrator.ts @@ -1,19 +1,14 @@ import { useCallback } from 'react'; -import { getSingleConnectorWithTag } from 'src/api/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(); @@ -39,39 +34,15 @@ export const useWorkflowHydrator = (expressWorkflow: boolean | undefined) => { ); const hydrateWorkflow = useCallback(async () => { - const { data: connectorMetadata, error: connectorError } = - await getSingleConnectorWithTag(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); } @@ -80,7 +51,7 @@ export const useWorkflowHydrator = (expressWorkflow: boolean | undefined) => { }, [ baseCatalogPrefix, catalogName, - connectorId, + connectorTag, expressWorkflow, hydrateDetailsForm, hydrateEndpointConfig, diff --git a/src/utils/connector-utils.ts b/src/utils/connector-utils.ts index cb2638fa64..e1a1cff811 100644 --- a/src/utils/connector-utils.ts +++ b/src/utils/connector-utils.ts @@ -1,28 +1,15 @@ -// 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'; -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 +20,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[] ) => @@ -118,3 +45,34 @@ export const requiredConnectorColumnsExist = <Response>( null ); }; + +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; +export function formatOldUuidToGql( + id: string | null | undefined +): string | null | undefined; +export function formatOldUuidToGql(id: string | null | undefined) { + return typeof id === 'string' ? id.replaceAll(':', '') : id; +} diff --git a/src/validation/index.ts b/src/validation/index.ts index 2ca298035f..8f6ffa6d9d 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 UNDERSCORE_RE = new RegExp(/_+/g);