From e8526613251b27be2f5235dc4f989e0c198001c0 Mon Sep 17 00:00:00 2001 From: Travis Jenkins Date: Fri, 23 Aug 2024 12:59:41 -0400 Subject: [PATCH 01/13] Making a shared hydrator that calls all the hydrators we need --- src/components/capture/Create/index.tsx | 78 ++++++++--------- src/components/capture/Edit.tsx | 87 +++++++++---------- .../materialization/Create/index.tsx | 56 +++++------- src/components/materialization/Edit.tsx | 69 +++++++-------- src/hooks/connectors/shared.ts | 4 +- src/stores/Workflow/Hydrator.tsx | 26 ++++++ 6 files changed, 157 insertions(+), 163 deletions(-) create mode 100644 src/stores/Workflow/Hydrator.tsx diff --git a/src/components/capture/Create/index.tsx b/src/components/capture/Create/index.tsx index 4a96e9819..5d8fb2864 100644 --- a/src/components/capture/Create/index.tsx +++ b/src/components/capture/Create/index.tsx @@ -15,10 +15,8 @@ import useDraftSpecs from 'hooks/useDraftSpecs'; import usePageTitle from 'hooks/usePageTitle'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { CustomEvents } from 'services/types'; -import BindingHydrator from 'stores/Binding/Hydrator'; -import { DetailsFormHydrator } from 'stores/DetailsForm/Hydrator'; import { useDetailsFormStore } from 'stores/DetailsForm/Store'; -import { EndpointConfigHydrator } from 'stores/EndpointConfig/Hydrator'; +import WorkflowHydrator from 'stores/Workflow/Hydrator'; import { MAX_DISCOVER_TIME } from 'utils/misc-utils'; function CaptureCreate() { @@ -84,50 +82,46 @@ function CaptureCreate() { ); return ( - - - - - - } - /> - } - RediscoverButton={ - + + } /> - - - - + } + RediscoverButton={ + + } + /> + + ); } diff --git a/src/components/capture/Edit.tsx b/src/components/capture/Edit.tsx index e9fb40c2e..986cd6bb5 100644 --- a/src/components/capture/Edit.tsx +++ b/src/components/capture/Edit.tsx @@ -7,7 +7,6 @@ import { useEditorStore_queryResponse_mutate, } from 'components/editor/Store/hooks'; import EntityEdit from 'components/shared/Entity/Edit'; -import DraftInitializer from 'components/shared/Entity/Edit/DraftInitializer'; import EntityToolbar from 'components/shared/Entity/Header'; import { MutateDraftSpecProvider } from 'components/shared/Entity/MutateDraftSpecContext'; import useGlobalSearchParams, { @@ -17,11 +16,9 @@ import { useDraftSpecs_editWorkflow } from 'hooks/useDraftSpecs'; import usePageTitle from 'hooks/usePageTitle'; import { useCallback, useMemo } from 'react'; import { CustomEvents } from 'services/types'; -import BindingHydrator from 'stores/Binding/Hydrator'; -import { DetailsFormHydrator } from 'stores/DetailsForm/Hydrator'; -import { EndpointConfigHydrator } from 'stores/EndpointConfig/Hydrator'; import { MAX_DISCOVER_TIME } from 'utils/misc-utils'; import useValidConnectorsExist from 'hooks/connectors/useHasConnectors'; +import WorkflowHydrator from 'stores/Workflow/Hydrator'; const entityType = 'capture'; function CaptureEdit() { @@ -59,50 +56,44 @@ function CaptureEdit() { ); return ( - - - - - - - } - /> - } - RediscoverButton={ - - } - /> - - - - - + + + + } + /> + } + RediscoverButton={ + + } + /> + + ); } diff --git a/src/components/materialization/Create/index.tsx b/src/components/materialization/Create/index.tsx index 7cf0d254d..802691427 100644 --- a/src/components/materialization/Create/index.tsx +++ b/src/components/materialization/Create/index.tsx @@ -14,10 +14,8 @@ import useDraftSpecs from 'hooks/useDraftSpecs'; import usePageTitle from 'hooks/usePageTitle'; import { useCallback, useEffect, useMemo } from 'react'; import { CustomEvents } from 'services/types'; -import BindingHydrator from 'stores/Binding/Hydrator'; -import { DetailsFormHydrator } from 'stores/DetailsForm/Hydrator'; import { useDetailsFormStore } from 'stores/DetailsForm/Store'; -import { EndpointConfigHydrator } from 'stores/EndpointConfig/Hydrator'; +import WorkflowHydrator from 'stores/Workflow/Hydrator'; function MaterializationCreate() { usePageTitle({ @@ -66,38 +64,32 @@ function MaterializationCreate() { }, [imageTag, setDraftId]); return ( - - - - - - } - primaryButtonProps={{ - disabled: !draftId, - logEvent: - CustomEvents.MATERIALIZATION_CREATE, - }} - secondaryButtonProps={{ - disabled: !hasConnectors, - logEvent: - CustomEvents.MATERIALIZATION_TEST, - }} + + + } + 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 2e793b269..5503503b3 100644 --- a/src/components/materialization/Edit.tsx +++ b/src/components/materialization/Edit.tsx @@ -6,7 +6,6 @@ import { } from 'components/editor/Store/hooks'; import MaterializeGenerateButton from 'components/materialization/GenerateButton'; import EntityEdit from 'components/shared/Entity/Edit'; -import DraftInitializer from 'components/shared/Entity/Edit/DraftInitializer'; import EntityToolbar from 'components/shared/Entity/Header'; import { MutateDraftSpecProvider } from 'components/shared/Entity/MutateDraftSpecContext'; import useValidConnectorsExist from 'hooks/connectors/useHasConnectors'; @@ -17,9 +16,7 @@ import { useDraftSpecs_editWorkflow } from 'hooks/useDraftSpecs'; import usePageTitle from 'hooks/usePageTitle'; import { useCallback, useMemo } from 'react'; import { CustomEvents } from 'services/types'; -import BindingHydrator from 'stores/Binding/Hydrator'; -import { DetailsFormHydrator } from 'stores/DetailsForm/Hydrator'; -import { EndpointConfigHydrator } from 'stores/EndpointConfig/Hydrator'; +import WorkflowHydrator from 'stores/Workflow/Hydrator'; function MaterializationEdit() { usePageTitle({ @@ -57,42 +54,34 @@ function MaterializationEdit() { ); return ( - - - - - - - } - primaryButtonProps={{ - disabled: !draftId, - logEvent: - CustomEvents.MATERIALIZATION_EDIT, - }} - secondaryButtonProps={{ - disabled: !hasConnectors, - logEvent: - CustomEvents.MATERIALIZATION_TEST, - }} - /> - } - /> - - - - - + + + + } + primaryButtonProps={{ + disabled: !draftId, + logEvent: CustomEvents.MATERIALIZATION_EDIT, + }} + secondaryButtonProps={{ + disabled: !hasConnectors, + logEvent: CustomEvents.MATERIALIZATION_TEST, + }} + /> + } + /> + + ); } diff --git a/src/hooks/connectors/shared.ts b/src/hooks/connectors/shared.ts index 30189a729..957d74be8 100644 --- a/src/hooks/connectors/shared.ts +++ b/src/hooks/connectors/shared.ts @@ -42,6 +42,7 @@ export const CONNECTOR_TAG_QUERY = ` ), id, connector_id, + default_capture_interval, image_tag, endpoint_spec_schema, resource_spec_schema, @@ -54,9 +55,10 @@ export interface ConnectorTag { }; id: string; connector_id: string; + default_capture_interval: any; //interval image_tag: string; endpoint_spec_schema: Schema; - resource_spec_schema: string; + resource_spec_schema: Schema; documentation_url: string; } diff --git a/src/stores/Workflow/Hydrator.tsx b/src/stores/Workflow/Hydrator.tsx new file mode 100644 index 000000000..2727cbefa --- /dev/null +++ b/src/stores/Workflow/Hydrator.tsx @@ -0,0 +1,26 @@ +import DraftInitializer from 'components/shared/Entity/Edit/DraftInitializer'; +import { useEntityWorkflow_Editing } from 'context/Workflow'; +import { Fragment } from 'react'; +import BindingHydrator from 'stores/Binding/Hydrator'; +import { DetailsFormHydrator } from 'stores/DetailsForm/Hydrator'; +import { EndpointConfigHydrator } from 'stores/EndpointConfig/Hydrator'; +import { BaseComponentProps } from 'types'; + +// 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({ children }: BaseComponentProps) { + const isEdit = useEntityWorkflow_Editing(); + const InitializerComponent = isEdit ? DraftInitializer : Fragment; + + return ( + + + + {children} + + + + ); +} + +export default WorkflowHydrator; From 0a5daae0527a8c42bab7f1e21565ef28c2e61680 Mon Sep 17 00:00:00 2001 From: Travis Jenkins Date: Fri, 23 Aug 2024 13:05:44 -0400 Subject: [PATCH 02/13] Starting to wire up the CaptureInterval component --- src/components/capture/Interval/index.tsx | 119 ++++++++++++++++++++++ src/components/editor/Bindings/index.tsx | 3 + src/lang/en-US.ts | 7 ++ 3 files changed, 129 insertions(+) create mode 100644 src/components/capture/Interval/index.tsx diff --git a/src/components/capture/Interval/index.tsx b/src/components/capture/Interval/index.tsx new file mode 100644 index 000000000..ee52c827c --- /dev/null +++ b/src/components/capture/Interval/index.tsx @@ -0,0 +1,119 @@ +import { + FormControl, + FormHelperText, + InputAdornment, + InputLabel, + MenuItem, + OutlinedInput, + Select, + Stack, + Typography, +} from '@mui/material'; +import { FormattedMessage, useIntl } from 'react-intl'; + +const DESCRIPTION_ID = 'capture-interval-description'; +const INPUT_ID = 'capture-interval-input'; +const INPUT_SIZE = 'small'; +interface Props { + readOnly?: boolean; +} + +function CaptureInterval({ readOnly }: Props) { + const intl = useIntl(); + const label = intl.formatMessage({ + id: 'workflows.interval.input.label', + }); + + // Need to check if Capture Interval is there + + return ( + + + + + + + + + + + + {label} + + + { + console.log('change', event.target.value); + }} + endAdornment={ + + + + } + /> + + errors go here + + + + ); +} + +export default CaptureInterval; diff --git a/src/components/editor/Bindings/index.tsx b/src/components/editor/Bindings/index.tsx index 3d888b8be..76c45166c 100644 --- a/src/components/editor/Bindings/index.tsx +++ b/src/components/editor/Bindings/index.tsx @@ -1,5 +1,6 @@ import { Stack, Typography, useTheme } from '@mui/material'; import AutoDiscoverySettings from 'components/capture/AutoDiscoverySettings'; +import CaptureInterval from 'components/capture/Interval'; import BindingsEditor from 'components/editor/Bindings/Editor'; import BindingSelector from 'components/editor/Bindings/Selector'; import ListAndDetails from 'components/editor/ListAndDetails'; @@ -103,6 +104,8 @@ function BindingsMultiEditor({ {entityType === 'capture' ? : null} + {entityType === 'capture' ? : null} + {entityType === 'materialization' ? : null} {workflow === 'capture_edit' || diff --git a/src/lang/en-US.ts b/src/lang/en-US.ts index 240047e47..9caa91e95 100644 --- a/src/lang/en-US.ts +++ b/src/lang/en-US.ts @@ -1110,6 +1110,13 @@ const Workflows: ResolvedIntlConfig['messages'] = { 'workflows.autoDiscovery.label.evolveIncompatibleCollection': `Changing primary keys re-versions collections`, 'workflows.autoDiscovery.update.failed': `Schema evolution update failed`, + 'workflows.interval.header': `Polling Interval`, + 'workflows.interval.message': `Allows controlling how often the Capture will check for new data. Intervals are relative to the start of an invocation and not its completion. For example, if the interval is five minutes, and an invocation of the capture finishes after two minutes, then the next invocation will be started after three additional minutes.`, + 'workflows.interval.input.label': `Interval`, + 'workflows.interval.input.seconds': `seconds`, + 'workflows.interval.input.minutes': `minutes`, + 'workflows.interval.input.hours': `hours`, + 'workflows.sourceCapture.header': `Link Capture`, 'workflows.sourceCapture.cta': `Source From Capture`, 'workflows.sourceCapture.cta.edit': `Edit Source Capture`, From dd5f6d32f6d4636068934d9aa682b3261f802405 Mon Sep 17 00:00:00 2001 From: Travis Jenkins Date: Fri, 23 Aug 2024 14:32:58 -0400 Subject: [PATCH 03/13] Commenting out interval stuff to work on merging Breaking up the cols to make manipulation a bit easier --- src/components/editor/Bindings/index.tsx | 3 +-- src/hooks/connectors/shared.ts | 25 ++++++++++++------------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/components/editor/Bindings/index.tsx b/src/components/editor/Bindings/index.tsx index 76c45166c..dd415d0b1 100644 --- a/src/components/editor/Bindings/index.tsx +++ b/src/components/editor/Bindings/index.tsx @@ -1,6 +1,5 @@ import { Stack, Typography, useTheme } from '@mui/material'; import AutoDiscoverySettings from 'components/capture/AutoDiscoverySettings'; -import CaptureInterval from 'components/capture/Interval'; import BindingsEditor from 'components/editor/Bindings/Editor'; import BindingSelector from 'components/editor/Bindings/Selector'; import ListAndDetails from 'components/editor/ListAndDetails'; @@ -104,7 +103,7 @@ function BindingsMultiEditor({ {entityType === 'capture' ? : null} - {entityType === 'capture' ? : null} + {/*{entityType === 'capture' ? : null}*/} {entityType === 'materialization' ? : null} diff --git a/src/hooks/connectors/shared.ts b/src/hooks/connectors/shared.ts index 957d74be8..0fb9ac4e4 100644 --- a/src/hooks/connectors/shared.ts +++ b/src/hooks/connectors/shared.ts @@ -36,18 +36,19 @@ export const CONNECTORS_EXIST_QUERY = ` ////////////////////////////// // useConnectorTag ////////////////////////////// -export const CONNECTOR_TAG_QUERY = ` - connectors( +export const CONNECTOR_TAG_COLS = [ + `connectors( image_name - ), - id, - connector_id, - default_capture_interval, - image_tag, - endpoint_spec_schema, - resource_spec_schema, - documentation_url -`; + )`, + 'id', + 'connector_id', + // 'default_capture_interval', + 'image_tag', + 'endpoint_spec_schema', + 'resource_spec_schema', + 'documentation_url', +]; +export const CONNECTOR_TAG_QUERY = CONNECTOR_TAG_COLS.join(','); export interface ConnectorTag { connectors: { @@ -55,7 +56,7 @@ export interface ConnectorTag { }; id: string; connector_id: string; - default_capture_interval: any; //interval + // default_capture_interval: any; //interval image_tag: string; endpoint_spec_schema: Schema; resource_spec_schema: Schema; From b61ba20873c1bdc3ffcfe145096233b00a5051f8 Mon Sep 17 00:00:00 2001 From: Travis Jenkins Date: Fri, 23 Aug 2024 15:10:02 -0400 Subject: [PATCH 04/13] Using a shared function that already exists for this Making the function a bit safer --- .../shared/Entity/EndpointConfig/index.tsx | 13 ++++++------- src/utils/misc-utils.ts | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/components/shared/Entity/EndpointConfig/index.tsx b/src/components/shared/Entity/EndpointConfig/index.tsx index 12aa5681a..58b7cc972 100644 --- a/src/components/shared/Entity/EndpointConfig/index.tsx +++ b/src/components/shared/Entity/EndpointConfig/index.tsx @@ -8,7 +8,7 @@ import Error from 'components/shared/Error'; import ErrorBoundryWrapper from 'components/shared/ErrorBoundryWrapper'; import { useEntityWorkflow } from 'context/Workflow'; import useConnectorTag from 'hooks/connectors/useConnectorTag'; -import { isEmpty, isEqual } from 'lodash'; +import { isEqual } from 'lodash'; import { useEffect, useMemo } from 'react'; import { useIntl } from 'react-intl'; import { useMount, useUnmount } from 'react-use'; @@ -27,6 +27,7 @@ import { useEndpointConfig_setServerUpdateRequired, } from 'stores/EndpointConfig/hooks'; import { useSidePanelDocsStore } from 'stores/SidePanelDocs/Store'; +import { configCanBeEmpty } from 'utils/misc-utils'; interface Props { connectorImage: string; @@ -83,12 +84,10 @@ function EndpointConfig({ // Storing if this endpointConfig can be empty or not // If so we know there will never be a "change" to the endpoint config - const canBeEmpty = useMemo(() => { - return ( - !connectorTag?.endpoint_spec_schema.properties || - isEmpty(connectorTag.endpoint_spec_schema.properties) - ); - }, [connectorTag?.endpoint_spec_schema]); + const canBeEmpty = useMemo( + () => configCanBeEmpty(connectorTag?.endpoint_spec_schema), + [connectorTag?.endpoint_spec_schema] + ); useEffect(() => { setEndpointCanBeEmpty(canBeEmpty); diff --git a/src/utils/misc-utils.ts b/src/utils/misc-utils.ts index 68088477e..82c354903 100644 --- a/src/utils/misc-utils.ts +++ b/src/utils/misc-utils.ts @@ -193,5 +193,5 @@ export const getDereffedSchema = async (val: any) => { }; export const configCanBeEmpty = (schema: any) => { - return Boolean(!schema.properties || isEmpty(schema.properties)); + return Boolean(!schema?.properties || isEmpty(schema?.properties)); }; From 7c8d9d593ac0cb0c8d01b9dd8fec3611e8d5de60 Mon Sep 17 00:00:00 2001 From: Travis Jenkins Date: Fri, 23 Aug 2024 15:10:13 -0400 Subject: [PATCH 05/13] Removing resource as it is not consumed currently --- src/hooks/connectors/shared.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/connectors/shared.ts b/src/hooks/connectors/shared.ts index 0fb9ac4e4..dc2aabfc2 100644 --- a/src/hooks/connectors/shared.ts +++ b/src/hooks/connectors/shared.ts @@ -45,7 +45,7 @@ export const CONNECTOR_TAG_COLS = [ // 'default_capture_interval', 'image_tag', 'endpoint_spec_schema', - 'resource_spec_schema', + // 'resource_spec_schema', //not used right now (Q3 2024) 'documentation_url', ]; export const CONNECTOR_TAG_QUERY = CONNECTOR_TAG_COLS.join(','); From bbbf31f8ea90d433f028b89b3db2ff83ab0c91d6 Mon Sep 17 00:00:00 2001 From: Travis Jenkins Date: Fri, 23 Aug 2024 15:24:16 -0400 Subject: [PATCH 06/13] Adding a guard to ensure we have a connector selected --- .../shared/guards/ConnectorSelected.tsx | 26 +++++++++++++++++++ src/context/Router/CaptureCreateNew.tsx | 10 ++++++- .../Router/MaterializationCreateNew.tsx | 10 ++++++- 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 src/components/shared/guards/ConnectorSelected.tsx diff --git a/src/components/shared/guards/ConnectorSelected.tsx b/src/components/shared/guards/ConnectorSelected.tsx new file mode 100644 index 000000000..368633b33 --- /dev/null +++ b/src/components/shared/guards/ConnectorSelected.tsx @@ -0,0 +1,26 @@ +import useGlobalSearchParams, { + GlobalSearchParams, +} from 'hooks/searchParams/useGlobalSearchParams'; +import { Navigate } from 'react-router-dom'; +import { BaseComponentProps } from 'types'; + +// 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 +interface Props extends BaseComponentProps { + navigateToPath: string; +} + +// This is quick - but really we should just make a general "SearchParamGuard" that +// can easily check we have everything we need. +function ConnectorSelectedGuard({ children, navigateToPath }: Props) { + const connectorId = useGlobalSearchParams(GlobalSearchParams.CONNECTOR_ID); + + if (!connectorId) { + return ; + } + + // eslint-disable-next-line react/jsx-no-useless-fragment + return <>{children}; +} + +export default ConnectorSelectedGuard; diff --git a/src/context/Router/CaptureCreateNew.tsx b/src/context/Router/CaptureCreateNew.tsx index c404b8e75..dbf21758f 100644 --- a/src/context/Router/CaptureCreateNew.tsx +++ b/src/context/Router/CaptureCreateNew.tsx @@ -1,5 +1,7 @@ +import { authenticatedRoutes } from 'app/routes'; import CaptureCreate from 'components/capture/Create'; import AdminCapabilityGuard from 'components/shared/guards/AdminCapability'; +import ConnectorSelectedGuard from 'components/shared/guards/ConnectorSelected'; import { EntityContextProvider } from 'context/EntityContext'; import { WorkflowContextProvider } from 'context/Workflow'; @@ -8,7 +10,13 @@ function CaptureCreateNewRoute() { - + + + diff --git a/src/context/Router/MaterializationCreateNew.tsx b/src/context/Router/MaterializationCreateNew.tsx index 347843bcb..10446b370 100644 --- a/src/context/Router/MaterializationCreateNew.tsx +++ b/src/context/Router/MaterializationCreateNew.tsx @@ -1,5 +1,7 @@ +import { authenticatedRoutes } from 'app/routes'; import MaterializationCreate from 'components/materialization/Create'; import AdminCapabilityGuard from 'components/shared/guards/AdminCapability'; +import ConnectorSelectedGuard from 'components/shared/guards/ConnectorSelected'; import { EntityContextProvider } from 'context/EntityContext'; import { WorkflowContextProvider } from 'context/Workflow'; @@ -8,7 +10,13 @@ function MaterializationCreateNewRoute() { - + + + From cc41febfec7eae911002129a857acae48fdc8c6c Mon Sep 17 00:00:00 2001 From: Travis Jenkins Date: Fri, 23 Aug 2024 15:50:18 -0400 Subject: [PATCH 07/13] Adding some validation to the guard to make it a bit safer --- src/components/shared/guards/ConnectorSelected.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/shared/guards/ConnectorSelected.tsx b/src/components/shared/guards/ConnectorSelected.tsx index 368633b33..408120572 100644 --- a/src/components/shared/guards/ConnectorSelected.tsx +++ b/src/components/shared/guards/ConnectorSelected.tsx @@ -10,12 +10,14 @@ interface Props extends BaseComponentProps { navigateToPath: string; } +const MAC_ADDR_RE = new RegExp(/^([0-9A-F]{2}:){7}([0-9A-F]{2})$/i); + // This is quick - but really we should just make a general "SearchParamGuard" that // can easily check we have everything we need. function ConnectorSelectedGuard({ children, navigateToPath }: Props) { const connectorId = useGlobalSearchParams(GlobalSearchParams.CONNECTOR_ID); - if (!connectorId) { + if (!MAC_ADDR_RE.test(connectorId)) { return ; } From e70a14ce0c96cb772913963c16654be6ebf6701c Mon Sep 17 00:00:00 2001 From: Travis Jenkins Date: Fri, 23 Aug 2024 16:27:19 -0400 Subject: [PATCH 08/13] Moving some validation into a new folder for validation Fixing issues with broken reg ex Checking catalog name is valid while loading details --- .../Dialog/useConfigurationSchema.ts | 2 +- src/components/capture/Details.tsx | 5 +++- src/components/collection/Details.tsx | 5 +++- .../inputs/PrefixedName/useValidatePrefix.ts | 2 +- src/components/materialization/Details.tsx | 5 +++- .../shared/Entity/DetailsForm/Form.tsx | 5 ++-- src/components/shared/guards/CatalogName.tsx | 25 +++++++++++++++++++ src/components/shared/pickers/shared.ts | 4 +-- .../DataSharing/Dialog/GenerateGrant.tsx | 7 ++---- src/directives/Onboard/Store/create.ts | 3 ++- src/lang/en-US.ts | 2 ++ src/services/types.ts | 1 + src/stores/DetailsForm/Store.ts | 4 +-- src/utils/misc-utils.ts | 9 ------- .../shared.ts => validation/index.ts} | 22 +++++++++++++--- 15 files changed, 69 insertions(+), 32 deletions(-) create mode 100644 src/components/shared/guards/CatalogName.tsx rename src/{components/inputs/PrefixedName/shared.ts => validation/index.ts} (52%) diff --git a/src/components/admin/Settings/StorageMappings/Dialog/useConfigurationSchema.ts b/src/components/admin/Settings/StorageMappings/Dialog/useConfigurationSchema.ts index 7db480745..f3159f660 100644 --- a/src/components/admin/Settings/StorageMappings/Dialog/useConfigurationSchema.ts +++ b/src/components/admin/Settings/StorageMappings/Dialog/useConfigurationSchema.ts @@ -4,7 +4,7 @@ import { isEmpty } from 'lodash'; import { useMemo } from 'react'; import { custom_generateDefaultUISchema } from 'services/jsonforms'; import { EnumDictionary } from 'types/utils'; -import { PREFIX_NAME_PATTERN } from 'utils/misc-utils'; +import { PREFIX_NAME_PATTERN } from 'validation'; export enum CloudProviderCodes { GCS = 'GCS', diff --git a/src/components/capture/Details.tsx b/src/components/capture/Details.tsx index 0f01f8e86..03fe63fac 100644 --- a/src/components/capture/Details.tsx +++ b/src/components/capture/Details.tsx @@ -1,5 +1,6 @@ import { authenticatedRoutes } from 'app/routes'; import EntityDetails from 'components/shared/Entity/Details'; +import CatalogNameGuard from 'components/shared/guards/CatalogName'; import { EntityContextProvider } from 'context/EntityContext'; import usePageTitle from 'hooks/usePageTitle'; @@ -10,7 +11,9 @@ function CaptureDetails() { return ( - + + + ); } diff --git a/src/components/collection/Details.tsx b/src/components/collection/Details.tsx index 39cd069f1..beebf3da6 100644 --- a/src/components/collection/Details.tsx +++ b/src/components/collection/Details.tsx @@ -1,5 +1,6 @@ import { authenticatedRoutes } from 'app/routes'; import EntityDetails from 'components/shared/Entity/Details'; +import CatalogNameGuard from 'components/shared/guards/CatalogName'; import { EntityContextProvider } from 'context/EntityContext'; import usePageTitle from 'hooks/usePageTitle'; @@ -9,7 +10,9 @@ function CollectionDetails() { }); return ( - + + + ); } diff --git a/src/components/inputs/PrefixedName/useValidatePrefix.ts b/src/components/inputs/PrefixedName/useValidatePrefix.ts index 85c74786e..b1a836121 100644 --- a/src/components/inputs/PrefixedName/useValidatePrefix.ts +++ b/src/components/inputs/PrefixedName/useValidatePrefix.ts @@ -1,4 +1,3 @@ -import { validateCatalogName } from 'components/inputs/PrefixedName/shared'; import { PrefixedName_Change, PrefixedName_Errors, @@ -7,6 +6,7 @@ import { useState } from 'react'; import { useIntl } from 'react-intl'; import { useEntitiesStore_capabilities_adminable } from 'stores/Entities/hooks'; import { hasLength } from 'utils/misc-utils'; +import { validateCatalogName } from 'validation'; interface Options { allowBlankName?: boolean; diff --git a/src/components/materialization/Details.tsx b/src/components/materialization/Details.tsx index 7bc6ebf0a..5ecaad5bb 100644 --- a/src/components/materialization/Details.tsx +++ b/src/components/materialization/Details.tsx @@ -1,5 +1,6 @@ import { authenticatedRoutes } from 'app/routes'; import EntityDetails from 'components/shared/Entity/Details'; +import CatalogNameGuard from 'components/shared/guards/CatalogName'; import { EntityContextProvider } from 'context/EntityContext'; import usePageTitle from 'hooks/usePageTitle'; @@ -9,7 +10,9 @@ function MaterializationDetails() { }); return ( - + + + ); } diff --git a/src/components/shared/Entity/DetailsForm/Form.tsx b/src/components/shared/Entity/DetailsForm/Form.tsx index 6f72f2d20..23f6dd0be 100644 --- a/src/components/shared/Entity/DetailsForm/Form.tsx +++ b/src/components/shared/Entity/DetailsForm/Form.tsx @@ -29,6 +29,7 @@ import { ConnectorVersionEvaluationOptions, evaluateConnectorVersions, } from 'utils/workflow-utils'; +import { MAC_ADDR_RE } from 'validation'; export const CONFIG_EDITOR_ID = 'endpointConfigEditor'; @@ -195,9 +196,7 @@ function DetailsFormForm({ connectorTags, entityType, readOnly }: Props) { const updateDetails = (details: Details) => { if ( - // TODO (Validators) we need to build out validators for specific types of data - details.data.connectorImage.connectorId && - details.data.connectorImage.connectorId.length === 23 && + MAC_ADDR_RE.test(details.data.connectorImage.connectorId) && details.data.connectorImage.connectorId !== originalConnectorImage.connectorId ) { diff --git a/src/components/shared/guards/CatalogName.tsx b/src/components/shared/guards/CatalogName.tsx new file mode 100644 index 000000000..c035c8470 --- /dev/null +++ b/src/components/shared/guards/CatalogName.tsx @@ -0,0 +1,25 @@ +import useGlobalSearchParams, { + GlobalSearchParams, +} from 'hooks/searchParams/useGlobalSearchParams'; +import EntityNotFound from 'pages/error/EntityNotFound'; +import { logRocketEvent } from 'services/shared'; +import { CustomEvents } from 'services/types'; +import { BaseComponentProps } from 'types'; +import { hasLength } from 'utils/misc-utils'; +import { validateCatalogName } from 'validation'; + +function CatalogNameGuard({ children }: BaseComponentProps) { + const catalogName = useGlobalSearchParams(GlobalSearchParams.CATALOG_NAME); + + if (hasLength(validateCatalogName(catalogName))) { + logRocketEvent(CustomEvents.ERROR_INVALID_CATALOG_NAME, { + catalogName, + }); + return ; + } + + // eslint-disable-next-line react/jsx-no-useless-fragment + return <>{children}; +} + +export default CatalogNameGuard; diff --git a/src/components/shared/pickers/shared.ts b/src/components/shared/pickers/shared.ts index c81690b96..cb8f389b9 100644 --- a/src/components/shared/pickers/shared.ts +++ b/src/components/shared/pickers/shared.ts @@ -1,5 +1,6 @@ import { PopoverOrigin } from '@mui/material'; -import { DATE_TIME_PATTERN, hasLength } from 'utils/misc-utils'; +import { hasLength } from 'utils/misc-utils'; +import { DATE_TIME_RE } from 'validation'; export const CLEAR_BUTTON_ID_SUFFIX = '__clear-button'; export const INVALID_DATE = 'Invalid Date'; @@ -25,7 +26,6 @@ export const validateDateTime = (value: string, allowBlank?: boolean): any => { } // Check the date is the correct format - const DATE_TIME_RE = new RegExp(`^(${DATE_TIME_PATTERN}$`); if (!isBlank && !DATE_TIME_RE.test(value)) { return ['invalid']; } diff --git a/src/components/tables/AccessGrants/DataSharing/Dialog/GenerateGrant.tsx b/src/components/tables/AccessGrants/DataSharing/Dialog/GenerateGrant.tsx index 448693f4c..01b80a051 100644 --- a/src/components/tables/AccessGrants/DataSharing/Dialog/GenerateGrant.tsx +++ b/src/components/tables/AccessGrants/DataSharing/Dialog/GenerateGrant.tsx @@ -21,11 +21,8 @@ import { selectableTableStoreSelectors, } from 'stores/Tables/Store'; import { Capability } from 'types'; -import { - appendWithForwardSlash, - hasLength, - PREFIX_NAME_PATTERN, -} from 'utils/misc-utils'; +import { appendWithForwardSlash, hasLength } from 'utils/misc-utils'; +import { PREFIX_NAME_PATTERN } from 'validation'; interface Props { serverError: PostgrestError | null; diff --git a/src/directives/Onboard/Store/create.ts b/src/directives/Onboard/Store/create.ts index 272878169..ad1db1d93 100644 --- a/src/directives/Onboard/Store/create.ts +++ b/src/directives/Onboard/Store/create.ts @@ -1,8 +1,9 @@ import { OnboardingState } from 'directives/Onboard/Store/types'; import produce from 'immer'; import { OnboardingStoreNames } from 'stores/names'; -import { hasLength, PREFIX_NAME_PATTERN } from 'utils/misc-utils'; +import { hasLength } from 'utils/misc-utils'; import { devtoolsOptions } from 'utils/store-utils'; +import { PREFIX_NAME_PATTERN } from 'validation'; import { create, StoreApi } from 'zustand'; import { devtools, NamedSet } from 'zustand/middleware'; diff --git a/src/lang/en-US.ts b/src/lang/en-US.ts index 9caa91e95..2ca65ba00 100644 --- a/src/lang/en-US.ts +++ b/src/lang/en-US.ts @@ -205,6 +205,8 @@ const Error: ResolvedIntlConfig['messages'] = { 'error.descriptionLabel': `Description:`, 'error.tryAgain': `Try again and if the issue persists please contact support.`, + 'error.catalogNameInvalid.message': `{catalogName} is not a valid entity name.`, + 'error.fallBack': `no error details to display`, }; diff --git a/src/services/types.ts b/src/services/types.ts index 3f824d93d..a739be0e6 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -22,6 +22,7 @@ export enum CustomEvents { ERROR_BOUNDARY_PAYMENT_METHODS = 'Error_Boundary_Displayed:PaymentMethods', ERROR_DISPLAYED = 'Error_Displayed', ERROR_MISSING_MESSAGE = 'Error_Missing_Message', + ERROR_INVALID_CATALOG_NAME = 'Error_Invalid_Catalog_Name', FIELD_SELECTION_REFRESH_AUTO = 'Field_Selection_Refresh:Auto', FIELD_SELECTION_REFRESH_MANUAL = 'Field_Selection_Refresh:Manual', FORM_STATE_PREVENTED = 'FormState:Prevented', diff --git a/src/stores/DetailsForm/Store.ts b/src/stores/DetailsForm/Store.ts index 6f6db7d36..9d13331f6 100644 --- a/src/stores/DetailsForm/Store.ts +++ b/src/stores/DetailsForm/Store.ts @@ -17,12 +17,12 @@ import { getInitialHydrationData, getStoreWithHydrationSettings, } from 'stores/extensions/Hydration'; -import { CATALOG_NAME_PATTERN } from 'utils/misc-utils'; import { devtoolsOptions } from 'utils/store-utils'; import { ConnectorVersionEvaluationOptions, evaluateConnectorVersions, } from 'utils/workflow-utils'; +import { NAME_RE } from 'validation'; import { StoreApi, create } from 'zustand'; import { NamedSet, devtools } from 'zustand/middleware'; @@ -114,8 +114,6 @@ export const getInitialState = ( // Run validation on the name. This is done inside the input but // having the input set custom errors causes issues as we basically // make two near identical calls to the store and that causes problems. - const NAME_RE = new RegExp(CATALOG_NAME_PATTERN); - const nameValidation = NAME_RE.test( state.details.data.entityName ) diff --git a/src/utils/misc-utils.ts b/src/utils/misc-utils.ts index 82c354903..9456c19f6 100644 --- a/src/utils/misc-utils.ts +++ b/src/utils/misc-utils.ts @@ -6,15 +6,6 @@ import { derefSchema } from 'services/jsonforms'; export const ESTUARY_SUPPORT_ROLE = 'estuary_support/'; export const DEMO_TENANT = 'demo/'; -// Based on pattern taken from -// https://github.com/estuary/animated-carnival/blob/main/supabase/migrations/03_catalog-types.sql -export const PREFIX_NAME_PATTERN = `[a-zA-Z0-9-_.]+`; -export const CATALOG_NAME_PATTERN = `^(${PREFIX_NAME_PATTERN}/)+${PREFIX_NAME_PATTERN}$`; - -// Based on the patterns connectors use for date time -// eslint-disable-next-line no-useless-escape -export const DATE_TIME_PATTERN = `[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z`; - // Default size used when splitting up larged promises export const CHUNK_SIZE = 10; diff --git a/src/components/inputs/PrefixedName/shared.ts b/src/validation/index.ts similarity index 52% rename from src/components/inputs/PrefixedName/shared.ts rename to src/validation/index.ts index 95d3e0966..2e87c2384 100644 --- a/src/components/inputs/PrefixedName/shared.ts +++ b/src/validation/index.ts @@ -1,5 +1,19 @@ -import { hasLength, PREFIX_NAME_PATTERN } from 'utils/misc-utils'; -import { PrefixedName_Errors } from './types'; +// Based on pattern taken from + +import { PrefixedName_Errors } from 'components/inputs/PrefixedName/types'; +import { hasLength } from 'utils/misc-utils'; + +// https://github.com/estuary/animated-carnival/blob/main/supabase/migrations/03_catalog-types.sql +export const PREFIX_NAME_PATTERN = `[a-zA-Z0-9-_.]+`; +export const CATALOG_NAME_PATTERN = `^(${PREFIX_NAME_PATTERN}/)+${PREFIX_NAME_PATTERN}$`; +export const NAME_RE = new RegExp(CATALOG_NAME_PATTERN); + +// Based on the patterns connectors use for date time +// eslint-disable-next-line no-useless-escape +export const DATE_TIME_PATTERN = `[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z`; +export const DATE_TIME_RE = new RegExp(`^(${DATE_TIME_PATTERN})$`); + +export const MAC_ADDR_RE = new RegExp(/^([0-9A-F]{2}:){7}([0-9A-F]{2})$/i); export const validateCatalogName = ( value: string, @@ -19,12 +33,12 @@ export const validateCatalogName = ( } // Check the name is the correct format - const NAME_RE = new RegExp( + const DYNAMIC_NAME_RE = new RegExp( `^(${PREFIX_NAME_PATTERN}/)*${PREFIX_NAME_PATTERN}${ allowEndSlash ? '/?' : '' }$` ); - if (!isBlank && !NAME_RE.test(value)) { + if (!isBlank && !DYNAMIC_NAME_RE.test(value)) { return ['invalid']; } From b65a5d3922e70227a69f1ac2ddc38dba7da7b000 Mon Sep 17 00:00:00 2001 From: Travis Jenkins Date: Fri, 23 Aug 2024 16:30:13 -0400 Subject: [PATCH 09/13] Making sure we show the name on error page --- src/components/shared/guards/CatalogName.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/shared/guards/CatalogName.tsx b/src/components/shared/guards/CatalogName.tsx index c035c8470..f91c0bea3 100644 --- a/src/components/shared/guards/CatalogName.tsx +++ b/src/components/shared/guards/CatalogName.tsx @@ -15,7 +15,7 @@ function CatalogNameGuard({ children }: BaseComponentProps) { logRocketEvent(CustomEvents.ERROR_INVALID_CATALOG_NAME, { catalogName, }); - return ; + return ; } // eslint-disable-next-line react/jsx-no-useless-fragment From 37b1fde16647e95aaee985c8eea95ef3c5491158 Mon Sep 17 00:00:00 2001 From: Travis Jenkins Date: Fri, 23 Aug 2024 16:32:20 -0400 Subject: [PATCH 10/13] Putting eventing in common place --- src/components/shared/guards/CatalogName.tsx | 7 +------ src/pages/error/EntityNotFound.tsx | 9 +++++++++ src/services/types.ts | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/components/shared/guards/CatalogName.tsx b/src/components/shared/guards/CatalogName.tsx index f91c0bea3..39c721a6c 100644 --- a/src/components/shared/guards/CatalogName.tsx +++ b/src/components/shared/guards/CatalogName.tsx @@ -2,8 +2,6 @@ import useGlobalSearchParams, { GlobalSearchParams, } from 'hooks/searchParams/useGlobalSearchParams'; import EntityNotFound from 'pages/error/EntityNotFound'; -import { logRocketEvent } from 'services/shared'; -import { CustomEvents } from 'services/types'; import { BaseComponentProps } from 'types'; import { hasLength } from 'utils/misc-utils'; import { validateCatalogName } from 'validation'; @@ -11,10 +9,7 @@ import { validateCatalogName } from 'validation'; function CatalogNameGuard({ children }: BaseComponentProps) { const catalogName = useGlobalSearchParams(GlobalSearchParams.CATALOG_NAME); - if (hasLength(validateCatalogName(catalogName))) { - logRocketEvent(CustomEvents.ERROR_INVALID_CATALOG_NAME, { - catalogName, - }); + if (hasLength(validateCatalogName(catalogName, false, true))) { return ; } diff --git a/src/pages/error/EntityNotFound.tsx b/src/pages/error/EntityNotFound.tsx index 992b97372..6deeb6291 100644 --- a/src/pages/error/EntityNotFound.tsx +++ b/src/pages/error/EntityNotFound.tsx @@ -1,6 +1,9 @@ import { Typography } from '@mui/material'; import usePageTitle from 'hooks/usePageTitle'; import { FormattedMessage } from 'react-intl'; +import { useMount } from 'react-use'; +import { logRocketEvent } from 'services/shared'; +import { CustomEvents } from 'services/types'; interface Props { catalogName?: string; @@ -11,6 +14,12 @@ const EntityNotFound = ({ catalogName }: Props) => { header: 'routeTitle.error.entityNotFound', }); + useMount(() => { + logRocketEvent(CustomEvents.ENTITY_NOT_FOUND, { + catalogName, + }); + }); + return ( <> diff --git a/src/services/types.ts b/src/services/types.ts index a739be0e6..719107bce 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -18,11 +18,11 @@ export enum CustomEvents { DIRECTIVE = 'Directive', DIRECTIVE_EXCHANGE_TOKEN = 'Directive:ExchangeToken', DIRECTIVE_GUARD_STATE = 'Directive:Guard:State', + ENTITY_NOT_FOUND = 'Entity_Not_Found', ERROR_BOUNDARY_DISPLAYED = 'Error_Boundary_Displayed', ERROR_BOUNDARY_PAYMENT_METHODS = 'Error_Boundary_Displayed:PaymentMethods', ERROR_DISPLAYED = 'Error_Displayed', ERROR_MISSING_MESSAGE = 'Error_Missing_Message', - ERROR_INVALID_CATALOG_NAME = 'Error_Invalid_Catalog_Name', FIELD_SELECTION_REFRESH_AUTO = 'Field_Selection_Refresh:Auto', FIELD_SELECTION_REFRESH_MANUAL = 'Field_Selection_Refresh:Manual', FORM_STATE_PREVENTED = 'FormState:Prevented', From 0e2db98e4e632bb0b630278dc4ce091b329bc9ad Mon Sep 17 00:00:00 2001 From: Travis Jenkins Date: Fri, 23 Aug 2024 16:34:32 -0400 Subject: [PATCH 11/13] using common value --- src/components/shared/guards/ConnectorSelected.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/shared/guards/ConnectorSelected.tsx b/src/components/shared/guards/ConnectorSelected.tsx index 408120572..9826e1490 100644 --- a/src/components/shared/guards/ConnectorSelected.tsx +++ b/src/components/shared/guards/ConnectorSelected.tsx @@ -3,6 +3,7 @@ import useGlobalSearchParams, { } from 'hooks/searchParams/useGlobalSearchParams'; import { Navigate } from 'react-router-dom'; import { BaseComponentProps } from 'types'; +import { MAC_ADDR_RE } from '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 @@ -10,8 +11,6 @@ interface Props extends BaseComponentProps { navigateToPath: string; } -const MAC_ADDR_RE = new RegExp(/^([0-9A-F]{2}:){7}([0-9A-F]{2})$/i); - // This is quick - but really we should just make a general "SearchParamGuard" that // can easily check we have everything we need. function ConnectorSelectedGuard({ children, navigateToPath }: Props) { From 19d371a682220cd68fd6c2040c1b66d4c81e36f1 Mon Sep 17 00:00:00 2001 From: Travis Jenkins Date: Fri, 23 Aug 2024 16:44:58 -0400 Subject: [PATCH 12/13] The pattern is no longer being used --- src/validation/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/validation/index.ts b/src/validation/index.ts index 2e87c2384..0d3f027e2 100644 --- a/src/validation/index.ts +++ b/src/validation/index.ts @@ -10,8 +10,9 @@ export const NAME_RE = new RegExp(CATALOG_NAME_PATTERN); // Based on the patterns connectors use for date time // eslint-disable-next-line no-useless-escape -export const DATE_TIME_PATTERN = `[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z`; -export const DATE_TIME_RE = new RegExp(`^(${DATE_TIME_PATTERN})$`); +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); From 58243a6d1dd980c2420c347934682e85716e9454 Mon Sep 17 00:00:00 2001 From: Travis Jenkins Date: Mon, 26 Aug 2024 09:38:11 -0400 Subject: [PATCH 13/13] PR: content and comments --- src/lang/en-US.ts | 2 +- src/validation/index.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/lang/en-US.ts b/src/lang/en-US.ts index 2ca65ba00..6ea285112 100644 --- a/src/lang/en-US.ts +++ b/src/lang/en-US.ts @@ -1113,7 +1113,7 @@ const Workflows: ResolvedIntlConfig['messages'] = { 'workflows.autoDiscovery.update.failed': `Schema evolution update failed`, 'workflows.interval.header': `Polling Interval`, - 'workflows.interval.message': `Allows controlling how often the Capture will check for new data. Intervals are relative to the start of an invocation and not its completion. For example, if the interval is five minutes, and an invocation of the capture finishes after two minutes, then the next invocation will be started after three additional minutes.`, + 'workflows.interval.message': `Controls how often the Capture will check for new data. Intervals are relative to the start of an invocation and not its completion. For example, if the interval is five minutes, and an invocation of the capture finishes after two minutes, then the next invocation will be started after three additional minutes.`, 'workflows.interval.input.label': `Interval`, 'workflows.interval.input.seconds': `seconds`, 'workflows.interval.input.minutes': `minutes`, diff --git a/src/validation/index.ts b/src/validation/index.ts index 0d3f027e2..af6cc91c2 100644 --- a/src/validation/index.ts +++ b/src/validation/index.ts @@ -1,8 +1,7 @@ -// Based on pattern taken from - import { PrefixedName_Errors } from 'components/inputs/PrefixedName/types'; import { hasLength } from 'utils/misc-utils'; +// Based on pattern taken from // https://github.com/estuary/animated-carnival/blob/main/supabase/migrations/03_catalog-types.sql export const PREFIX_NAME_PATTERN = `[a-zA-Z0-9-_.]+`; export const CATALOG_NAME_PATTERN = `^(${PREFIX_NAME_PATTERN}/)+${PREFIX_NAME_PATTERN}$`;