From 7bbb7d62a09772c107a91290538e5f5f01c4ad1a Mon Sep 17 00:00:00 2001 From: Travis Jenkins Date: Wed, 25 Sep 2024 13:20:47 -0400 Subject: [PATCH] DataFlow backfill - disabled but most of the functionality is there (#1261) * Turning everything back on * Moving steps stuff into a manual machine state in a store * Getting actions buttons wired up Calling intl for translations * Moving more stuff into content Moving label into store to reduce duplication Adding a running handler into parent * checking first step * Updating typing a lot Switching to uses as single status property Getting capture disable wired up * typing tweaks handling errors a bit better updating enum * Storing off step definition * Updating key to keep consistent * Adding support for a context to store data... but this feels wrong * adding quick support to store off things in a keyed / machine way * cleaning up handled as we are not using this * Forcing to pass an index when updating a step * quick hack and typing update * Checking locked to skip showing the load bar Closing when "back" button is clicked on first step No longer making steps options just defaulting to empty Just setting show to false is enough for closing dialog Checking the shallow `onFirstStep` to reduce renders Moving context out to the main store Only running initialize once per hydration * Adding handled to get no response when an insert is done to reduce network Fetching live spec based on spec id * Storing stuff to re-enable the capture Wiring up most of the steps (without waiting for no data) * marking valid so the next step works * Starting to wire up the last step * Updating ChipList to: Support passing in 'sx' so we can override styling when needed Scrolling the element down a little bit when all items are shown * Making code a bit safer * Typing for chiplist changes * styling the options better so the collections don't overlap the option below * Reset the state when leaving the flow * Probably best to just store the publication status as a whole Making consts for important keys * Adding a "review selections" step as an idea Starting work on handling errors better Trying to get publication populated correctly * Refactoring to reduce need of functions * Resetting when closing as doing it in store is not working * Moving hydrator within modal so it is always reset when opening and closing * Breaking out the review table * Adding more plural support * Updating content as requested * styling tweaks * Putting routes nested Moving the show prompt flag over to the form state store Linking to the capture and materialization details Have LinkWrapper handle newWindow links Allowing path overrides on the details navigator * Sharing the link to details Making code more null/empty * A bit better error handling and messaging Removing some filler content Adding support for valid and error to be handled by the store * clearing our errors when success * Using related collections list Allowing related collections to open in a new window Changing query to just find materializations with Source Capture Updating content to use the right terms Removing log token from the state * Making sure we only count the enabled bindings in the list * cleaning up hook that is not used * Fixing the warning to display correctly Moving the hook fetch into the component to reduce extra renders * Making the save and publish logging and warning shared Moving the LogDialog into a component folder * Moved to a component fold * Updating handling Showing proper error icon in stepper * Ended up not needing the hook Starting to figure out how to process skipping steps * Updating messaging * Not show logs right away Moving files around a bit to reduce nesting Move the loading indicator into the selector * Adding a uuid so make things a bit more traceable * Adding a LR event to try to make these sessions easier to find * Cleaning up comments Adding event to store to make it consistent * Cleaning up comments * Adding some handling for draft errors * Wiring up to exit the flow when complete Adding in a real time to prevent build from breaking Removing draft errors from alert * Updating details message * Disabling all the new stuff so this can be merged * Removing `detail` from the forms. * Removing code that is not needed * PR: comments * PR: commenting out function that isn't used * PR: commenting out to disable this * Making code a bit simpler Typing the ref Including the current top height when scrolling * Getting the chips aligned * PR: putting original count back in * Updating content to be more clear Switching out format components * PR: typo and small content change * PR: content typo --- src/api/draftSpecs.ts | 5 +- src/api/liveSpecsExt.ts | 49 ++++- .../editor/Bindings/Backfill/index.tsx | 11 +- src/components/editor/Bindings/index.tsx | 8 +- .../editor/Shards/Disable/Warning.tsx | 89 ++++++---- .../editor/Shards/Disable/index.tsx | 10 +- .../EntityStatOverview/StatOverview.tsx | 72 +------- src/components/shared/ChipList/Wrapper.tsx | 18 +- src/components/shared/ChipList/index.tsx | 27 ++- src/components/shared/ChipList/types.ts | 5 + src/components/shared/Entity/Actions/Save.tsx | 8 +- .../shared/Entity/Actions/useSave.ts | 6 +- .../shared/Entity/CatalogEditor.tsx | 13 +- .../Entity/DetailsForm/useFormFields.ts | 23 +-- src/components/shared/Entity/Edit/index.tsx | 9 +- .../Entity/EntityNameDetailsLink/index.tsx | 33 ++++ .../Entity/EntityNameDetailsLink/types.ts | 5 + src/components/shared/Entity/Header.tsx | 3 +- src/components/shared/Entity/LogDialog.tsx | 70 -------- .../shared/Entity/LogDialog/Content.tsx | 34 ++++ .../shared/Entity/LogDialog/index.tsx | 49 +++++ .../shared/Entity/LogDialog/types.ts | 15 ++ .../shared/Entity/RelatedCollections.tsx | 4 +- .../Entity/hooks/useDataFlowResetHandler.tsx | 40 ----- .../Entity/hooks/useEntityWorkflowHelpers.ts | 7 + .../shared/Entity/prompts/PreSave/Actions.tsx | 46 ++++- .../shared/Entity/prompts/PreSave/Content.tsx | 29 --- .../Entity/prompts/PreSave/Content/index.tsx | 145 +++++++++++++++ .../shared/Entity/prompts/PreSave/Title.tsx | 23 ++- .../shared/Entity/prompts/PreSave/index.tsx | 26 +-- .../DisableCapture/definition.ts | 14 ++ .../dataFlowReset/DisableCapture/index.tsx | 145 ++++++++++++--- .../dataFlowReset/EnableCapture/definition.ts | 9 + .../dataFlowReset/EnableCapture/index.tsx | 67 +++++-- .../ReviewSelection/ReviewTable.tsx | 123 +++++++++++++ .../ReviewSelection/definition.ts | 12 ++ .../dataFlowReset/ReviewSelection/index.tsx | 33 ++++ .../SelectMaterialization/BindingReview.tsx | 47 ----- .../Materializations.tsx | 59 ------- .../SelectMaterialization/Selector.tsx | 76 ++++++-- .../SelectMaterialization/SelectorOption.tsx | 26 ++- .../SelectMaterialization/definition.ts | 18 ++ .../SelectMaterialization/index.tsx | 45 +++-- .../SelectMaterialization/types.ts | 11 +- .../UpdateMaterialization/definition.ts | 15 ++ .../UpdateMaterialization/index.tsx | 124 +++++++++++-- .../WaitForCaptureStop/definition.ts | 16 ++ .../WaitForCaptureStop/index.tsx | 85 +++++++-- .../prompts/steps/dataFlowReset/shared.ts | 31 ++-- .../steps/preSave/ChangeReview/definition.ts | 12 ++ .../steps/preSave/ChangeReview/index.tsx | 13 +- .../prompts/steps/preSave/Done/index.tsx | 17 -- .../steps/preSave/Publish/definition.ts | 9 + .../prompts/steps/preSave/Publish/index.tsx | 94 +++++++++- .../steps/preSave/usePreSavePromptSteps.tsx | 56 ------ .../shared/Entity/prompts/store/Hydrator.tsx | 23 +++ .../shared/Entity/prompts/store/shared.ts | 9 + .../shared/Entity/prompts/store/types.ts | 21 +++ .../prompts/store/usePreSavePromptStore.ts | 167 ++++++++++++++++++ src/components/shared/Entity/prompts/types.ts | 26 +++ src/components/shared/LinkWrapper.tsx | 12 +- .../RowActions/AccessGrants/Progress.tsx | 2 +- .../RowActions/AccessGrants/RevokeGrant.tsx | 2 +- .../AccessLinks/DisableDirective.tsx | 2 +- .../RowActions/AccessLinks/Progress.tsx | 2 +- .../tables/RowActions/Shared/Progress.tsx | 21 +-- .../tables/RowActions/Shared/UpdateEntity.tsx | 6 +- .../tables/RowActions/Shared/types.ts | 27 +++ .../tables/cells/EntityNameLink.tsx | 25 +-- src/context/LoopIndex/index.tsx | 13 ++ src/context/LoopIndex/shared.ts | 3 + src/context/LoopIndex/types.ts | 5 + src/context/LoopIndex/useLoopIndex.tsx | 14 ++ src/hooks/useDetailsNavigator.ts | 8 +- src/hooks/useLiveSpecsExt.ts | 16 +- src/lang/en-US/CommonMessages.ts | 4 + src/lang/en-US/Workflows.ts | 47 +++-- src/services/supabase.ts | 34 +++- src/services/types.ts | 1 + src/stores/Binding/Store.ts | 5 + src/stores/Binding/hooks.ts | 15 ++ src/stores/Binding/types.ts | 1 + src/stores/DetailsForm/Store.ts | 2 - src/stores/DetailsForm/types.ts | 1 - src/stores/FormState/Store.ts | 36 +++- src/stores/FormState/hooks.ts | 36 ++-- src/stores/FormState/types.ts | 9 +- src/utils/entity-utils.ts | 44 +++++ src/utils/workflow-utils.ts | 11 ++ 89 files changed, 1928 insertions(+), 761 deletions(-) create mode 100644 src/components/shared/Entity/EntityNameDetailsLink/index.tsx create mode 100644 src/components/shared/Entity/EntityNameDetailsLink/types.ts delete mode 100644 src/components/shared/Entity/LogDialog.tsx create mode 100644 src/components/shared/Entity/LogDialog/Content.tsx create mode 100644 src/components/shared/Entity/LogDialog/index.tsx create mode 100644 src/components/shared/Entity/LogDialog/types.ts delete mode 100644 src/components/shared/Entity/hooks/useDataFlowResetHandler.tsx delete mode 100644 src/components/shared/Entity/prompts/PreSave/Content.tsx create mode 100644 src/components/shared/Entity/prompts/PreSave/Content/index.tsx create mode 100644 src/components/shared/Entity/prompts/steps/dataFlowReset/DisableCapture/definition.ts create mode 100644 src/components/shared/Entity/prompts/steps/dataFlowReset/EnableCapture/definition.ts create mode 100644 src/components/shared/Entity/prompts/steps/dataFlowReset/ReviewSelection/ReviewTable.tsx create mode 100644 src/components/shared/Entity/prompts/steps/dataFlowReset/ReviewSelection/definition.ts create mode 100644 src/components/shared/Entity/prompts/steps/dataFlowReset/ReviewSelection/index.tsx delete mode 100644 src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/BindingReview.tsx delete mode 100644 src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/Materializations.tsx create mode 100644 src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/definition.ts create mode 100644 src/components/shared/Entity/prompts/steps/dataFlowReset/UpdateMaterialization/definition.ts create mode 100644 src/components/shared/Entity/prompts/steps/dataFlowReset/WaitForCaptureStop/definition.ts create mode 100644 src/components/shared/Entity/prompts/steps/preSave/ChangeReview/definition.ts delete mode 100644 src/components/shared/Entity/prompts/steps/preSave/Done/index.tsx create mode 100644 src/components/shared/Entity/prompts/steps/preSave/Publish/definition.ts delete mode 100644 src/components/shared/Entity/prompts/steps/preSave/usePreSavePromptSteps.tsx create mode 100644 src/components/shared/Entity/prompts/store/Hydrator.tsx create mode 100644 src/components/shared/Entity/prompts/store/shared.ts create mode 100644 src/components/shared/Entity/prompts/store/types.ts create mode 100644 src/components/shared/Entity/prompts/store/usePreSavePromptStore.ts create mode 100644 src/components/shared/Entity/prompts/types.ts create mode 100644 src/components/tables/RowActions/Shared/types.ts create mode 100644 src/context/LoopIndex/index.tsx create mode 100644 src/context/LoopIndex/shared.ts create mode 100644 src/context/LoopIndex/types.ts create mode 100644 src/context/LoopIndex/useLoopIndex.tsx diff --git a/src/api/draftSpecs.ts b/src/api/draftSpecs.ts index faa348b98..1c7d38bd6 100644 --- a/src/api/draftSpecs.ts +++ b/src/api/draftSpecs.ts @@ -42,7 +42,8 @@ export const createDraftSpec = ( catalogName: string, draftSpec: any, specType?: Entity | null, - lastPubId?: string | null + lastPubId?: string | null, + noResponse?: boolean ) => { let matchData: CreateMatchData = { draft_id: draftId, @@ -55,7 +56,7 @@ export const createDraftSpec = ( matchData = { ...matchData, expect_pub_id: lastPubId }; } - return insertSupabase(TABLES.DRAFT_SPECS, matchData); + return insertSupabase(TABLES.DRAFT_SPECS, matchData, noResponse); }; export const modifyDraftSpec = ( diff --git a/src/api/liveSpecsExt.ts b/src/api/liveSpecsExt.ts index 71dbbcdee..509cf5ae3 100644 --- a/src/api/liveSpecsExt.ts +++ b/src/api/liveSpecsExt.ts @@ -206,7 +206,6 @@ export interface LiveSpecsExtQuery_DetailsForm { spec_type: Entity; spec: any; data_plane_id: string; - detail: string | null; connector_tag_id: string; connector_image_name: string; connector_image_tag: string; @@ -219,7 +218,6 @@ const DETAILS_FORM_QUERY = ` spec_type, spec, data_plane_id, - detail, connector_tag_id, connector_image_name, connector_image_tag, @@ -372,6 +370,14 @@ const getLiveSpecsByLiveSpecId = async (liveSpecId: string) => { return data; }; +const getLiveSpecSpec = (liveSpecId: string) => { + return supabaseClient + .from(TABLES.LIVE_SPECS_EXT) + .select(`spec`) + .eq('id', liveSpecId) + .single(); +}; + const getLiveSpecShards = (tenant: string, entityType: Entity) => { return supabaseClient .from(TABLES.LIVE_SPECS_EXT) @@ -380,6 +386,44 @@ const getLiveSpecShards = (tenant: string, entityType: Entity) => { .eq('spec_type', entityType); }; +const liveSpecsExtRelatedColumns = ['catalog_name', 'reads_from', 'id']; +export const liveSpecsExtRelatedQuery = liveSpecsExtRelatedColumns.join(','); +export interface LiveSpecsExt_Related { + catalog_name: string; + reads_from: string[]; + id: string; +} +// const getLiveSpecsRelatedToMaterialization = async ( +// collectionNames: string[] +// ) => { +// const limiter = pLimit(3); +// const promises = []; +// let index = 0; + +// // TODO (retry) promise generator +// const promiseGenerator = (idx: number) => { +// return supabaseClient +// .from(TABLES.LIVE_SPECS_EXT) +// .select(liveSpecsExtRelatedQuery) +// .eq('spec_type', 'materialization') +// .overlaps( +// 'reads_from', +// collectionNames.slice(idx, idx + CHUNK_SIZE) +// ) +// .returns(); +// }; + +// while (index < collectionNames.length) { +// const prom = promiseGenerator(index); +// promises.push(limiter(() => prom)); +// index = index + CHUNK_SIZE; +// } + +// const response = await Promise.all(promises); +// const errors = response.filter((r) => r.error); +// return errors[0] ?? response[0]; +// }; + export { getLiveSpecs_captures, getLiveSpecs_collections, @@ -392,4 +436,5 @@ export { getLiveSpecsByConnectorId, getLiveSpecsByLiveSpecId, getLiveSpecShards, + getLiveSpecSpec, }; diff --git a/src/components/editor/Bindings/Backfill/index.tsx b/src/components/editor/Bindings/Backfill/index.tsx index ad0cc6176..1d08dff21 100644 --- a/src/components/editor/Bindings/Backfill/index.tsx +++ b/src/components/editor/Bindings/Backfill/index.tsx @@ -10,8 +10,8 @@ import { useBinding_currentCollection, useBinding_currentBindingUUID, useBinding_setBackfilledBindings, - useBinding_collections_count, useBinding_backfillSupported, + useBinding_enabledCollections_count, } from 'stores/Binding/hooks'; import { useFormStateStore_isActive, @@ -36,7 +36,7 @@ function Backfill({ description, bindingIndex = -1 }: BackfillProps) { // Binding Store const currentCollection = useBinding_currentCollection(); const currentBindingUUID = useBinding_currentBindingUUID(); - const collectionsCount = useBinding_collections_count(); + const collectionsCount = useBinding_enabledCollections_count(); const allBindingsDisabled = useBinding_allBindingsDisabled(); const backfillAllBindings = useBinding_backfillAllBindings(); @@ -199,10 +199,11 @@ function Backfill({ description, bindingIndex = -1 }: BackfillProps) { ) : null} - {/*TODO (data flow reset)*/} - {/* {bindingIndex === -1 && workflow === 'capture_edit' ? ( + {/*TODO (data flow reset) + {bindingIndex === -1 && workflow === 'capture_edit' ? ( - ) : null}*/} + ) : null} + */} ); } diff --git a/src/components/editor/Bindings/index.tsx b/src/components/editor/Bindings/index.tsx index dd415d0b1..30399e778 100644 --- a/src/components/editor/Bindings/index.tsx +++ b/src/components/editor/Bindings/index.tsx @@ -110,11 +110,9 @@ function BindingsMultiEditor({ {workflow === 'capture_edit' || workflow === 'materialization_edit' ? ( - } + description={intl.formatMessage({ + id: `workflows.collectionSelector.manualBackfill.message.${entityType}.allBindings`, + })} /> ) : null} diff --git a/src/components/editor/Shards/Disable/Warning.tsx b/src/components/editor/Shards/Disable/Warning.tsx index 324b288e4..6d4c174eb 100644 --- a/src/components/editor/Shards/Disable/Warning.tsx +++ b/src/components/editor/Shards/Disable/Warning.tsx @@ -3,50 +3,61 @@ import { FormattedMessage } from 'react-intl'; import { useEntityType } from 'context/EntityContext'; import AlertBox from 'components/shared/AlertBox'; +import useGlobalSearchParams, { + GlobalSearchParams, +} from 'hooks/searchParams/useGlobalSearchParams'; function ShardsDisableWarning() { + const forcedToEnable = useGlobalSearchParams( + GlobalSearchParams.FORCED_SHARD_ENABLE + ); + const entityType = useEntityType(); - return ( - - - } + if (forcedToEnable === 0) { + return ( + - - - - - - - - - ); + + } + > + + + + + + + + + ); + } + + return null; } export default ShardsDisableWarning; diff --git a/src/components/editor/Shards/Disable/index.tsx b/src/components/editor/Shards/Disable/index.tsx index 03b5b192c..33da93072 100644 --- a/src/components/editor/Shards/Disable/index.tsx +++ b/src/components/editor/Shards/Disable/index.tsx @@ -2,17 +2,11 @@ import { Stack, Typography } from '@mui/material'; import { FormattedMessage } from 'react-intl'; import { useEntityType } from 'context/EntityContext'; import { useEditorStore_persistedDraftId } from 'components/editor/Store/hooks'; -import useGlobalSearchParams, { - GlobalSearchParams, -} from 'hooks/searchParams/useGlobalSearchParams'; + import ShardsDisableForm from './Form'; import ShardsDisableWarning from './Warning'; function ShardsDisable() { - const forcedToEnable = useGlobalSearchParams( - GlobalSearchParams.FORCED_SHARD_ENABLE - ); - const entityType = useEntityType(); const draftId = useEditorStore_persistedDraftId(); @@ -23,7 +17,7 @@ function ShardsDisable() { return ( <> - {forcedToEnable ? : null} + diff --git a/src/components/home/dashboard/EntityStatOverview/StatOverview.tsx b/src/components/home/dashboard/EntityStatOverview/StatOverview.tsx index 0c0d1e153..61f4fa8ab 100644 --- a/src/components/home/dashboard/EntityStatOverview/StatOverview.tsx +++ b/src/components/home/dashboard/EntityStatOverview/StatOverview.tsx @@ -1,15 +1,10 @@ import { Stack, Typography } from '@mui/material'; import { PostgrestError } from '@supabase/postgrest-js'; -import { authenticatedRoutes } from 'app/routes'; import LinkWrapper from 'components/shared/LinkWrapper'; -import { - semiTransparentBackground_blue, - semiTransparentBackground_purple, - semiTransparentBackground_teal, -} from 'context/Theme'; -import { CloudDownload, CloudUpload, DatabaseScript } from 'iconoir-react'; + import { FormattedMessage, useIntl } from 'react-intl'; import { Entity } from 'types'; +import { ENTITY_SETTINGS } from 'utils/entity-utils'; import ActiveEntityCount from './ActiveEntityCount'; import Statistic from './Statistic'; @@ -21,56 +16,6 @@ interface Props { monthlyUsageLoading?: boolean; } -const getEntityPageURLPath = (entityType: string): string => { - if (entityType === 'collection') { - return authenticatedRoutes.collections.fullPath; - } - - if (entityType === 'capture') { - return authenticatedRoutes.captures.fullPath; - } - - return authenticatedRoutes.materializations.fullPath; -}; - -const getTitleId = (entityType: string): string => { - if (entityType === 'collection') { - return 'terms.collections'; - } - - if (entityType === 'capture') { - return 'terms.sources'; - } - - return 'terms.destinations'; -}; - -const getBackgroundColor = ( - entityType: string -): { dark: string; light: string } => { - if (entityType === 'collection') { - return semiTransparentBackground_blue; - } - - if (entityType === 'capture') { - return semiTransparentBackground_teal; - } - - return semiTransparentBackground_purple; -}; - -const getEntityIcon = (entityType: string) => { - if (entityType === 'collection') { - return DatabaseScript; - } - - if (entityType === 'capture') { - return CloudUpload; - } - - return CloudDownload; -}; - export default function StatOverview({ entityType, monthlyUsage, @@ -80,11 +25,12 @@ export default function StatOverview({ }: Props) { const intl = useIntl(); - const route = getEntityPageURLPath(entityType); - const titleId = getTitleId(entityType); - - const background = getBackgroundColor(entityType); - const Icon = getEntityIcon(entityType); + const { + routes: { viewAll }, + termId: titleId, + background, + Icon, + } = ENTITY_SETTINGS[entityType]; return ( - + diff --git a/src/components/shared/ChipList/Wrapper.tsx b/src/components/shared/ChipList/Wrapper.tsx index 79743e2af..310025366 100644 --- a/src/components/shared/ChipList/Wrapper.tsx +++ b/src/components/shared/ChipList/Wrapper.tsx @@ -80,7 +80,15 @@ function ChipWrapper({ size="small" variant="outlined" disabled={disabled} - onClick={onClick} + onClick={ + onClick + ? (event) => { + event.stopPropagation(); + event.preventDefault(); + onClick(); + } + : undefined + } sx={chipSX} /> ); @@ -88,10 +96,14 @@ function ChipWrapper({ const wrappedChip = useMemo(() => { if (val.link) { - return {chip}; + return ( + + {chip} + + ); } return chip; - }, [chip, val.link]); + }, [chip, val.link, val.newWindow]); return ( diff --git a/src/components/shared/ChipList/index.tsx b/src/components/shared/ChipList/index.tsx index d0e041686..2587e48c7 100644 --- a/src/components/shared/ChipList/index.tsx +++ b/src/components/shared/ChipList/index.tsx @@ -1,12 +1,20 @@ import { Box } from '@mui/material'; -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { useIntl } from 'react-intl'; import { ChipListProps } from './types'; import ChipWrapper from './Wrapper'; -function ChipList({ values, disabled, maxChips, stripPath }: ChipListProps) { +function ChipList({ + values, + disabled, + maxChips, + stripPath, + sx, +}: ChipListProps) { const intl = useIntl(); + const listScroller = useRef(null); + // Format data coming in so we can still pass in a list of strings const formattedValues = useMemo(() => { return values.map((value) => { @@ -37,17 +45,30 @@ function ChipList({ values, disabled, maxChips, stripPath }: ChipListProps) { setMaxRender(valueLength); }; + // When all chips are shown scroll down just a hair to try to make + // sure the user knows that the list is scrollable + useEffect(() => { + if (!listScroller.current || maxRender !== valueLength) { + return; + } + + listScroller.current.scrollTop += 20; + }, [maxRender, valueLength]); + return ( diff --git a/src/components/shared/ChipList/types.ts b/src/components/shared/ChipList/types.ts index 449caf57c..70f1cc7b5 100644 --- a/src/components/shared/ChipList/types.ts +++ b/src/components/shared/ChipList/types.ts @@ -1,6 +1,9 @@ +import { SxProps, Theme } from '@mui/material'; + export interface ChipDisplay { display: string; link?: string; + newWindow?: boolean; title?: string; } @@ -16,5 +19,7 @@ export interface ChipListProps { values: string[] | ChipDisplay[]; disabled?: boolean; maxChips?: number; + newWindow?: boolean; stripPath?: boolean; + sx?: SxProps; } diff --git a/src/components/shared/Entity/Actions/Save.tsx b/src/components/shared/Entity/Actions/Save.tsx index 923f5c80d..efa99406f 100644 --- a/src/components/shared/Entity/Actions/Save.tsx +++ b/src/components/shared/Entity/Actions/Save.tsx @@ -5,7 +5,6 @@ import { } from 'components/editor/Store/hooks'; import { buttonSx } from 'components/shared/Entity/Header'; import { useIntl } from 'react-intl'; - import { useFormStateStore_isActive } from 'stores/FormState/hooks'; import { EntityCreateSaveButtonProps } from './types'; import useSave from './useSave'; @@ -23,11 +22,12 @@ function EntityCreateSave({ const save = useSave(logEvent, onFailure, dryRun); const isSaving = useEditorStore_isSaving(); - const formActive = useFormStateStore_isActive(); const draftId = useEditorStore_id(); + const formActive = useFormStateStore_isActive(); + // TODO (data flow reset) - // const setShowPreSavePrompt = useFormStateStore_setShowPreSavePrompt(); + // const setShowSavePrompt = useFormStateStore_setShowSavePrompt(); // const backfillDataflow = useBindingStore((state) => state.backfillDataFlow); // const needsBackfilled = useBinding_backfilledBindings_count(); @@ -38,7 +38,7 @@ function EntityCreateSave({ onClick={async () => { // TODO (data flow reset) // if (!dryRun && backfillDataflow && needsBackfilled) { - // setShowPreSavePrompt(true); + // setShowSavePrompt(true); // } else { // await save(draftId); // } diff --git a/src/components/shared/Entity/Actions/useSave.ts b/src/components/shared/Entity/Actions/useSave.ts index b82b1f279..c510d3ece 100644 --- a/src/components/shared/Entity/Actions/useSave.ts +++ b/src/components/shared/Entity/Actions/useSave.ts @@ -60,9 +60,6 @@ function useSave( const setDiscoveredDraftId = useEditorStore_setDiscoveredDraftId(); const mutateDraftSpecs = useEditorStore_queryResponse_mutate(); - const entityDescription = useDetailsFormStore( - (state) => state.details.data.description - ); const dataPlaneName = useDetailsFormStore( (state) => state.details.data.dataPlane?.dataPlaneName ); @@ -263,7 +260,7 @@ function useSave( const response = await createPublication( draftId, dryRun ?? false, - entityDescription, + undefined, dataPlaneName?.whole ); if (response.error) { @@ -288,7 +285,6 @@ function useSave( dataPlaneName?.whole, disabledBindings, dryRun, - entityDescription, fullSourceErrorsExist, intl, messagePrefix, diff --git a/src/components/shared/Entity/CatalogEditor.tsx b/src/components/shared/Entity/CatalogEditor.tsx index 42b88bd2c..cfa3ce457 100644 --- a/src/components/shared/Entity/CatalogEditor.tsx +++ b/src/components/shared/Entity/CatalogEditor.tsx @@ -22,7 +22,7 @@ function CatalogEditor({ messageId }: Props) { // TODO (data flow reset) // const intl = useIntl(); - // const backfillDataflow = useBindingStore((state) => state.backfillDataFlow); + // const backfillDataFlow = useBindingStore((state) => state.backfillDataFlow); // const needsBackfilled = useBinding_backfilledBindings_count(); if (draftId && formStatus !== FormStatus.INIT) { @@ -41,21 +41,22 @@ function CatalogEditor({ messageId }: Props) { - {/*TODO (data flow reset) - also make sure editor is disabled*/} - {/* {backfillDataFlow && needsBackfilled ? ( + {/*TODO (data flow reset) - also make sure editor is disabled + {backfillDataFlow && needsBackfilled ? ( {intl.formatMessage({ - id: 'dataflowReset.editor.warning.message', + id: 'dataFlowReset.editor.warning.message', })} - ) : null}*/} + ) : null} + */} { const catalogNameUISchema = { @@ -74,14 +68,6 @@ export default function useFormFields( type: 'Control', }; - const descriptionUISchema = { - label: intl.formatMessage({ - id: 'description.label', - }), - scope: '#/properties/description', - type: 'Control', - }; - return { elements: [ { @@ -91,13 +77,8 @@ export default function useFormFields( connectorUISchema, catalogNameUISchema, dataPlaneUISchema, - descriptionUISchema, ] - : [ - connectorUISchema, - catalogNameUISchema, - descriptionUISchema, - ], + : [connectorUISchema, catalogNameUISchema], type: 'HorizontalLayout', }, ], diff --git a/src/components/shared/Entity/Edit/index.tsx b/src/components/shared/Entity/Edit/index.tsx index fea1d3fc5..594df46d9 100644 --- a/src/components/shared/Entity/Edit/index.tsx +++ b/src/components/shared/Entity/Edit/index.tsx @@ -147,6 +147,9 @@ function EntityEdit({ entityType={entityType} entityName={entityName} > + {/*TODO (data flow reset) + + */} {formSubmitError ? ( - {/*TODO (data flow reset)*/} - {/**/} + {/*TODO (data flow reset) + + + */} )} diff --git a/src/components/shared/Entity/EntityNameDetailsLink/index.tsx b/src/components/shared/Entity/EntityNameDetailsLink/index.tsx new file mode 100644 index 000000000..8a0b9e83e --- /dev/null +++ b/src/components/shared/Entity/EntityNameDetailsLink/index.tsx @@ -0,0 +1,33 @@ +import LinkWrapper from 'components/shared/LinkWrapper'; +import { Box, Tooltip } from '@mui/material'; +import { useIntl } from 'react-intl'; +import { ViewDetailsProps } from './types'; + +function EntityNameDetailsLink({ name, path, newWindow }: ViewDetailsProps) { + const intl = useIntl(); + + return ( + + + + {name} + + + + ); +} + +export default EntityNameDetailsLink; diff --git a/src/components/shared/Entity/EntityNameDetailsLink/types.ts b/src/components/shared/Entity/EntityNameDetailsLink/types.ts new file mode 100644 index 000000000..82d24ae3a --- /dev/null +++ b/src/components/shared/Entity/EntityNameDetailsLink/types.ts @@ -0,0 +1,5 @@ +export interface ViewDetailsProps { + name: string; + path: string; + newWindow?: boolean; +} diff --git a/src/components/shared/Entity/Header.tsx b/src/components/shared/Entity/Header.tsx index 542978f76..d0d601c76 100644 --- a/src/components/shared/Entity/Header.tsx +++ b/src/components/shared/Entity/Header.tsx @@ -48,6 +48,7 @@ function EntityToolbar({ const formStatus = useFormStateStore_status(); const discovering = !draftId && formStatus === FormStatus.GENERATING; const saved = formStatus === FormStatus.SAVED; + const locked = formStatus === FormStatus.LOCKED; const PrimaryButton = PrimaryButtonComponent ?? EntitySaveButton; const SecondaryButton = SecondaryButtonComponent ?? EntityTestButton; @@ -90,7 +91,7 @@ function EntityToolbar({ }} > diff --git a/src/components/shared/Entity/LogDialog.tsx b/src/components/shared/Entity/LogDialog.tsx deleted file mode 100644 index 68c9da0fc..000000000 --- a/src/components/shared/Entity/LogDialog.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { - Dialog, - DialogActions, - DialogContent, - DialogTitle, -} from '@mui/material'; -import Logs from 'components/logs'; -import { ReactNode } from 'react'; -import { useFormStateStore_message } from 'stores/FormState/hooks'; -import ErrorBoundryWrapper from '../ErrorBoundryWrapper'; -import EntityWarnings from './Warnings'; - -interface Props { - open: boolean; - token: string | null; - title: ReactNode; - actionComponent: ReactNode; -} - -const TITLE_ID = 'logs-dialog-title'; - -function LogDialog({ open, token, actionComponent, title }: Props) { - const { key, severity } = useFormStateStore_message(); - - return ( - theme.breakpoints.values.sm, - }} - > - {title} - - - - - - - - - - - {actionComponent} - - - ); -} - -export default LogDialog; diff --git a/src/components/shared/Entity/LogDialog/Content.tsx b/src/components/shared/Entity/LogDialog/Content.tsx new file mode 100644 index 000000000..da0601f82 --- /dev/null +++ b/src/components/shared/Entity/LogDialog/Content.tsx @@ -0,0 +1,34 @@ +import Logs from 'components/logs'; +import ErrorBoundryWrapper from 'components/shared/ErrorBoundryWrapper'; +import EntityWarnings from '../Warnings'; +import { LogDialogContentProps } from './types'; + +function LogDialogContent({ + spinnerMessageId, + severity, + token, +}: LogDialogContentProps) { + return ( + <> + + + + + + + ); +} + +export default LogDialogContent; diff --git a/src/components/shared/Entity/LogDialog/index.tsx b/src/components/shared/Entity/LogDialog/index.tsx new file mode 100644 index 000000000..a011d8fbd --- /dev/null +++ b/src/components/shared/Entity/LogDialog/index.tsx @@ -0,0 +1,49 @@ +import { + Dialog, + DialogActions, + DialogContent, + DialogTitle, +} from '@mui/material'; +import { useFormStateStore_message } from 'stores/FormState/hooks'; +import LogDialogContent from './Content'; +import { LogDialogProps } from './types'; + +const TITLE_ID = 'logs-dialog-title'; + +function LogDialog({ open, token, actionComponent, title }: LogDialogProps) { + const { key, severity } = useFormStateStore_message(); + + return ( + theme.breakpoints.values.sm, + }} + > + {title} + + + + + + + {actionComponent} + + + ); +} + +export default LogDialog; diff --git a/src/components/shared/Entity/LogDialog/types.ts b/src/components/shared/Entity/LogDialog/types.ts new file mode 100644 index 000000000..0183893c0 --- /dev/null +++ b/src/components/shared/Entity/LogDialog/types.ts @@ -0,0 +1,15 @@ +import { AlertColor } from '@mui/material'; +import { ReactNode } from 'react'; + +export interface LogDialogProps { + open: boolean; + token: string | null; + title: ReactNode; + actionComponent: ReactNode; +} + +export interface LogDialogContentProps { + spinnerMessageId: string | null; + severity: AlertColor | null; + token: string | null; +} diff --git a/src/components/shared/Entity/RelatedCollections.tsx b/src/components/shared/Entity/RelatedCollections.tsx index d5b552940..ea18cbbd1 100644 --- a/src/components/shared/Entity/RelatedCollections.tsx +++ b/src/components/shared/Entity/RelatedCollections.tsx @@ -5,9 +5,10 @@ import { useIntl } from 'react-intl'; interface Props { collections: string[] | null; + newWindow?: boolean; } -function RelatedCollections({ collections }: Props) { +function RelatedCollections({ collections, newWindow }: Props) { const intl = useIntl(); const { generatePath } = useDetailsNavigator( @@ -24,6 +25,7 @@ function RelatedCollections({ collections }: Props) { link: generatePath({ catalog_name: collection, }), + newWindow, title: intl.formatMessage( { id: 'detailsPanel.details.linkToCollection', diff --git a/src/components/shared/Entity/hooks/useDataFlowResetHandler.tsx b/src/components/shared/Entity/hooks/useDataFlowResetHandler.tsx deleted file mode 100644 index d8c24cfbc..000000000 --- a/src/components/shared/Entity/hooks/useDataFlowResetHandler.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { createPublication } from 'api/publications'; -import { useEditorStore_id } from 'components/editor/Store/hooks'; -import { useCallback } from 'react'; -import { useBindingStore } from 'stores/Binding/Store'; -import { generateDisabledSpec } from 'utils/entity-utils'; - -function useDataFlowResetHandler() { - const [backfillDataFlowTarget] = useBindingStore((state) => [ - state.backfillDataFlowTarget, - ]); - - const draftId = useEditorStore_id(); - - return useCallback(async () => { - console.log('Starting'); - - // Capture - Disable - generateDisabledSpec({}, false, false); - - // Capture - Publish - const publishResponse = await createPublication(draftId, false); - if (publishResponse.error) { - console.log('publishResponse.error', publishResponse.error); - // return failed(publishResponse); - } - - // waitForPublishToFinish(publishResponse.data[0].id, false); - - // Runtime must stop 100% and is done writing documents (wait for publication to succeed and then wait… or keep looking for shards) - // (IMPORTANT) - save current time - // Bindings - update backfill property - // Capture - enable - // Materialization - update notBefore property - // Capture & Materialization - Publish - - console.log('backfillDataFlowTarget', backfillDataFlowTarget); - }, [backfillDataFlowTarget, draftId]); -} - -export default useDataFlowResetHandler; diff --git a/src/components/shared/Entity/hooks/useEntityWorkflowHelpers.ts b/src/components/shared/Entity/hooks/useEntityWorkflowHelpers.ts index 335f0336b..a8b4a1d80 100644 --- a/src/components/shared/Entity/hooks/useEntityWorkflowHelpers.ts +++ b/src/components/shared/Entity/hooks/useEntityWorkflowHelpers.ts @@ -70,6 +70,12 @@ function useEntityWorkflowHelpers() { // Transformation Create Store const resetTransformationCreateState = useTransformationCreate_resetState(); + // TODO (data flow reset) + // PreSave Prompt Store + // const resetPreSavePrompt = usePreSavePromptStore( + // (state) => state.resetState + // ); + const resetState = useCallback(() => { resetFormState(); resetEndpointConfigState(); @@ -80,6 +86,7 @@ function useEntityWorkflowHelpers() { resetSchemaEvolutionState(); resetSourceCapture(); resetTransformationCreateState(); + // resetPreSavePrompt(); }, [ resetBindingState, resetBindingsEditorStore, diff --git a/src/components/shared/Entity/prompts/PreSave/Actions.tsx b/src/components/shared/Entity/prompts/PreSave/Actions.tsx index 779d768b1..aa2c2bba5 100644 --- a/src/components/shared/Entity/prompts/PreSave/Actions.tsx +++ b/src/components/shared/Entity/prompts/PreSave/Actions.tsx @@ -1,18 +1,52 @@ import { Button, DialogActions, Stack } from '@mui/material'; -import usePreSavePromptSteps from '../steps/preSave/usePreSavePromptSteps'; +import { useIntl } from 'react-intl'; +import { useFormStateStore_setShowSavePrompt } from 'stores/FormState/hooks'; +import useEntityWorkflowHelpers from '../../hooks/useEntityWorkflowHelpers'; +import { + usePreSavePromptStore, + usePreSavePromptStore_onFirstStep, + usePreSavePromptStore_onLastStep, + usePreSavePromptStore_stepValid, +} from '../store/usePreSavePromptStore'; function Actions() { - const { handleBack, handleNext } = usePreSavePromptSteps(); + const intl = useIntl(); + + const setShowSavePrompt = useFormStateStore_setShowSavePrompt(); + + const [activeStep, nextStep, previousStep] = usePreSavePromptStore( + (state) => [state.activeStep, state.nextStep, state.previousStep] + ); + + const canContinue = usePreSavePromptStore_stepValid(); + const onFirstStep = usePreSavePromptStore_onFirstStep(); + const onLastStep = usePreSavePromptStore_onLastStep(); + + const { exit } = useEntityWorkflowHelpers(); return ( - - diff --git a/src/components/shared/Entity/prompts/PreSave/Content.tsx b/src/components/shared/Entity/prompts/PreSave/Content.tsx deleted file mode 100644 index e8d0607b1..000000000 --- a/src/components/shared/Entity/prompts/PreSave/Content.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { DialogContent, Step, Stepper } from '@mui/material'; -import { useMemo } from 'react'; -import usePreSavePromptSteps from '../steps/preSave/usePreSavePromptSteps'; - -function Content() { - const { activeStep, steps } = usePreSavePromptSteps(); - - const renderedSteps = useMemo( - () => - steps.map((StepComponent, index) => { - return ( - - - - ); - }), - [steps] - ); - - return ( - - - {renderedSteps} - - - ); -} - -export default Content; diff --git a/src/components/shared/Entity/prompts/PreSave/Content/index.tsx b/src/components/shared/Entity/prompts/PreSave/Content/index.tsx new file mode 100644 index 000000000..01e4b56b6 --- /dev/null +++ b/src/components/shared/Entity/prompts/PreSave/Content/index.tsx @@ -0,0 +1,145 @@ +import { + Box, + DialogContent, + LinearProgress, + Stack, + Step, + StepContent, + StepLabel, + Stepper, +} from '@mui/material'; +import ErrorBoundryWrapper from 'components/shared/ErrorBoundryWrapper'; +import { + ProgressFinished, + ProgressStates, +} from 'components/tables/RowActions/Shared/types'; +import { useMemo } from 'react'; +import { useIntl } from 'react-intl'; +import Error from 'components/shared/Error'; +import ErrorLogs from 'components/shared/Entity/Error/Logs'; +import { LoopIndexContextProvider } from 'context/LoopIndex'; +import DraftErrors from 'components/shared/Entity/Error/DraftErrors'; +import { usePreSavePromptStore } from '../../store/usePreSavePromptStore'; + +function Content() { + const intl = useIntl(); + const [activeStep, steps, backfilledDraftId] = usePreSavePromptStore( + (state) => [ + state.activeStep, + state.steps, + state.context?.backfilledDraftId, + ] + ); + + const renderedSteps = useMemo( + () => + steps.map( + ( + { + StepComponent, + stepLabelMessageId, + state: { error, progress, publicationStatus }, + }, + index + ) => { + const hasError = Boolean(error); + const stepCompleted = progress >= ProgressFinished; + + return ( + + + {intl.formatMessage({ + id: stepLabelMessageId, + })} + + + + + + {progress === + ProgressStates.RUNNING ? ( + + ) : null} + + {backfilledDraftId ? ( + + ) : null} + {/* TODO (data flow reset) + + + {intl.formatMessage({ + id: 'preSavePrompt.draftErrors.message', + })} + + + + + */} + + {progress === + ProgressStates.FAILED ? ( + + + + + + + ) : null} + + + + + + + + ); + } + ), + [backfilledDraftId, intl, steps] + ); + + return ( + + + {renderedSteps} + + + ); +} + +export default Content; diff --git a/src/components/shared/Entity/prompts/PreSave/Title.tsx b/src/components/shared/Entity/prompts/PreSave/Title.tsx index b81be8dff..e1863f359 100644 --- a/src/components/shared/Entity/prompts/PreSave/Title.tsx +++ b/src/components/shared/Entity/prompts/PreSave/Title.tsx @@ -1,20 +1,19 @@ import { DialogTitle, IconButton, useTheme } from '@mui/material'; import { Xmark } from 'iconoir-react'; import { useIntl } from 'react-intl'; -import { useFormStateStore_setShowPreSavePrompt } from 'stores/FormState/hooks'; -import usePreSavePromptSteps from '../steps/preSave/usePreSavePromptSteps'; +import { useFormStateStore_setShowSavePrompt } from 'stores/FormState/hooks'; +import { usePreSavePromptStore } from '../store/usePreSavePromptStore'; function Title() { const intl = useIntl(); const theme = useTheme(); - const { activeStep, setActiveStep } = usePreSavePromptSteps(); - const setShowPreSavePrompt = useFormStateStore_setShowPreSavePrompt(); + const setShowSavePrompt = useFormStateStore_setShowSavePrompt(); - const closeDialog = () => { - setActiveStep(0); - setShowPreSavePrompt(false); - }; + const [activeStep, resetState] = usePreSavePromptStore((state) => [ + state.activeStep, + state.resetState, + ]); return ( Please review your changes - 2} onClick={closeDialog}> + 3} + onClick={() => { + resetState(); + setShowSavePrompt(false); + }} + > - - <Content /> - <Actions /> + <PromptsHydrator> + <Title /> + <Content /> + <Actions /> + </PromptsHydrator> </Dialog> ); } -export default PreSave; +export default PreSavePrompt; diff --git a/src/components/shared/Entity/prompts/steps/dataFlowReset/DisableCapture/definition.ts b/src/components/shared/Entity/prompts/steps/dataFlowReset/DisableCapture/definition.ts new file mode 100644 index 000000000..0c66b7983 --- /dev/null +++ b/src/components/shared/Entity/prompts/steps/dataFlowReset/DisableCapture/definition.ts @@ -0,0 +1,14 @@ +import { defaultStepState } from '../../../store/shared'; +import { PromptStep } from '../../../types'; +import DisableCapture from '.'; + +// interface DisableCaptureStepContext { +// pubId: string | null; +// captureSpec: JSON | null; +// captureName: string | null; +// } +export const DisableCaptureStep: PromptStep = { + StepComponent: DisableCapture, + stepLabelMessageId: 'dataFlowReset.disableCapture.title', + state: defaultStepState, +}; diff --git a/src/components/shared/Entity/prompts/steps/dataFlowReset/DisableCapture/index.tsx b/src/components/shared/Entity/prompts/steps/dataFlowReset/DisableCapture/index.tsx index b85016ce2..99061cee2 100644 --- a/src/components/shared/Entity/prompts/steps/dataFlowReset/DisableCapture/index.tsx +++ b/src/components/shared/Entity/prompts/steps/dataFlowReset/DisableCapture/index.tsx @@ -1,26 +1,129 @@ -import { StepContent, StepLabel } from '@mui/material'; -import ErrorBoundryWrapper from 'components/shared/ErrorBoundryWrapper'; -import Logs from 'components/logs'; +import { modifyDraftSpec } from 'api/draftSpecs'; +import { + createPublication, + getPublicationByIdQuery, + PublicationJobStatus, +} from 'api/publications'; +import { + useEditorStore_id, + useEditorStore_queryResponse_draftSpecs, +} from 'components/editor/Store/hooks'; +import { ProgressStates } from 'components/tables/RowActions/Shared/types'; +import { useLoopIndex } from 'context/LoopIndex/useLoopIndex'; +import useJobStatusPoller from 'hooks/useJobStatusPoller'; +import { useMount } from 'react-use'; +import { useFormStateStore_setFormState } from 'stores/FormState/hooks'; +import { FormStatus } from 'stores/FormState/types'; +import { generateDisabledSpec } from 'utils/entity-utils'; +import { usePreSavePromptStore } from '../../../store/usePreSavePromptStore'; function DisableCapture() { - return ( - <> - <StepLabel>Disable capture</StepLabel> - <StepContent> - <ErrorBoundryWrapper> - <Logs - token={null} - height={350} - loadingLineSeverity="info" - spinnerMessages={{ - stoppedKey: 'dataflowReset.logs.spinner.stopped', - runningKey: 'dataflowReset.logs.spinner.running', - }} - /> - </ErrorBoundryWrapper> - </StepContent> - </> - ); + const draftId = useEditorStore_id(); + const draftSpecs = useEditorStore_queryResponse_draftSpecs(); + const { jobStatusPoller } = useJobStatusPoller(); + + const stepIndex = useLoopIndex(); + const thisStep = usePreSavePromptStore((state) => state.steps[stepIndex]); + + const [updateStep, updateContext, nextStep, initUUID] = + usePreSavePromptStore((state) => [ + state.updateStep, + state.updateContext, + state.nextStep, + state.initUUID, + ]); + + const setFormState = useFormStateStore_setFormState(); + + useMount(() => { + if (thisStep.state.progress === ProgressStates.IDLE) { + setFormState({ + status: FormStatus.LOCKED, + exitWhenLogsClose: true, + }); + + updateStep(stepIndex, { + progress: ProgressStates.RUNNING, + }); + + const disableCaptureAndPublish = async () => { + const newSpec = generateDisabledSpec( + draftSpecs[0].spec, + false, + false + ); + + const captureName = draftSpecs[0].catalog_name; + + // Update the Capture to be disabled + const updateResponse = await modifyDraftSpec( + newSpec, + { + draft_id: draftId, + catalog_name: captureName, + spec_type: 'capture', + }, + undefined, + undefined, + `data flow backfill : disable : ${initUUID}` + ); + + if (updateResponse.error) { + updateStep(stepIndex, { + error: updateResponse.error, + progress: ProgressStates.FAILED, + }); + return; + } + + // Start publishing it + const publishResponse = await createPublication(draftId, false); + + if (publishResponse.error || !publishResponse.data) { + updateStep(stepIndex, { + error: publishResponse.error, + progress: ProgressStates.FAILED, + }); + return; + } + + updateContext({ + captureName, + captureSpec: newSpec, + pubId: publishResponse.data[0].id, + }); + + jobStatusPoller( + getPublicationByIdQuery(publishResponse.data[0].id), + async (successResponse: PublicationJobStatus) => { + updateStep(stepIndex, { + publicationStatus: successResponse, + }); + + nextStep(); + }, + async ( + failedResponse: any //PublicationJobStatus | PostgrestError + ) => { + updateStep(stepIndex, { + error: failedResponse.error ? failedResponse : null, + publicationStatus: !failedResponse.error + ? failedResponse + : null, + }); + // logRocketEvent(CustomEvents.REPUBLISH_PREFIX_FAILED); + } + ); + }; + + void disableCaptureAndPublish(); + } else { + console.log('TODO: need to handle showing previous state?'); + } + }); + + // eslint-disable-next-line react/jsx-no-useless-fragment + return <></>; } export default DisableCapture; diff --git a/src/components/shared/Entity/prompts/steps/dataFlowReset/EnableCapture/definition.ts b/src/components/shared/Entity/prompts/steps/dataFlowReset/EnableCapture/definition.ts new file mode 100644 index 000000000..7df104044 --- /dev/null +++ b/src/components/shared/Entity/prompts/steps/dataFlowReset/EnableCapture/definition.ts @@ -0,0 +1,9 @@ +import { defaultStepState } from '../../../store/shared'; +import { PromptStep } from '../../../types'; +import EnableCapture from '.'; + +export const EnableCaptureStep: PromptStep = { + StepComponent: EnableCapture, + stepLabelMessageId: 'dataFlowReset.enableCapture.title', + state: defaultStepState, +}; diff --git a/src/components/shared/Entity/prompts/steps/dataFlowReset/EnableCapture/index.tsx b/src/components/shared/Entity/prompts/steps/dataFlowReset/EnableCapture/index.tsx index 6678a5797..d7cefbc10 100644 --- a/src/components/shared/Entity/prompts/steps/dataFlowReset/EnableCapture/index.tsx +++ b/src/components/shared/Entity/prompts/steps/dataFlowReset/EnableCapture/index.tsx @@ -1,15 +1,62 @@ -import { StepContent, StepLabel } from '@mui/material'; -import ErrorBoundryWrapper from 'components/shared/ErrorBoundryWrapper'; +import { createDraftSpec } from 'api/draftSpecs'; + +import { ProgressStates } from 'components/tables/RowActions/Shared/types'; +import { useLoopIndex } from 'context/LoopIndex/useLoopIndex'; +import { useMount } from 'react-use'; +import { generateDisabledSpec } from 'utils/entity-utils'; +import { usePreSavePromptStore } from '../../../store/usePreSavePromptStore'; function EnableCapture() { - return ( - <> - <StepLabel>Enable capture</StepLabel> - <StepContent> - <ErrorBoundryWrapper>Logs</ErrorBoundryWrapper> - </StepContent> - </> - ); + const stepIndex = useLoopIndex(); + const thisStep = usePreSavePromptStore((state) => state.steps[stepIndex]); + + const [updateStep, nextStep, context] = usePreSavePromptStore((state) => [ + state.updateStep, + state.nextStep, + state.context, + ]); + + useMount(() => { + if (thisStep.state.progress === ProgressStates.IDLE) { + updateStep(stepIndex, { + progress: ProgressStates.RUNNING, + }); + + const enableCapture = async () => { + // Update the Capture to be disabled + const updateResponse = await createDraftSpec( + context.backfilledDraftId, + context.captureName, + generateDisabledSpec(context.captureSpec, true, false), + 'capture', + undefined, + false + ); + + if (updateResponse.error) { + updateStep(stepIndex, { + error: updateResponse.error, + progress: ProgressStates.FAILED, + }); + return; + } + + updateStep(stepIndex, { + progress: ProgressStates.SUCCESS, + valid: true, + }); + + nextStep(); + }; + + void enableCapture(); + } else { + console.log('TODO: need to handle showing previous state?'); + } + }); + + // eslint-disable-next-line react/jsx-no-useless-fragment + return <></>; } export default EnableCapture; diff --git a/src/components/shared/Entity/prompts/steps/dataFlowReset/ReviewSelection/ReviewTable.tsx b/src/components/shared/Entity/prompts/steps/dataFlowReset/ReviewSelection/ReviewTable.tsx new file mode 100644 index 000000000..005699b45 --- /dev/null +++ b/src/components/shared/Entity/prompts/steps/dataFlowReset/ReviewSelection/ReviewTable.tsx @@ -0,0 +1,123 @@ +import { + Box, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableRow, +} from '@mui/material'; +import { useEditorStore_queryResponse_draftSpecs } from 'components/editor/Store/hooks'; +import EntityNameDetailsLink from 'components/shared/Entity/EntityNameDetailsLink'; +import RelatedCollections from 'components/shared/Entity/RelatedCollections'; +import useDetailsNavigator from 'hooks/useDetailsNavigator'; +import { useIntl } from 'react-intl'; +import { useBinding_collectionsBeingBackfilled } from 'stores/Binding/hooks'; +import { ENTITY_SETTINGS } from 'utils/entity-utils'; +import { usePreSavePromptStore } from '../../../store/usePreSavePromptStore'; + +function ReviewTable() { + const intl = useIntl(); + + const { generatePath } = useDetailsNavigator(''); + + const draftSpecs = useEditorStore_queryResponse_draftSpecs(); + const collectionsBeingBackfilled = useBinding_collectionsBeingBackfilled(); + const [materializationName] = usePreSavePromptStore((state) => [ + state.context?.backfillTarget?.catalog_name, + ]); + + const tableRows = [ + { + entityType: 'capture', + cell: ( + <EntityNameDetailsLink + newWindow + name={draftSpecs[0].catalog_name} + path={generatePath( + { + catalog_name: draftSpecs[0].catalog_name, + }, + ENTITY_SETTINGS.capture.routes.details + )} + /> + ), + count: 1, + }, + { + entityType: 'collection', + cell: ( + <RelatedCollections + collections={collectionsBeingBackfilled} + newWindow + /> + ), + count: collectionsBeingBackfilled.length, + }, + { + entityType: 'materialization', + cell: ( + <EntityNameDetailsLink + newWindow + name={materializationName} + path={generatePath( + { + catalog_name: materializationName, + }, + ENTITY_SETTINGS.materialization.routes.details + )} + /> + ), + count: 1, + }, + ]; + + return ( + <TableContainer> + <Table size="small"> + <TableBody> + {tableRows.map(({ cell, count, entityType }) => { + const { pluralId, Icon } = ENTITY_SETTINGS[entityType]; + + return ( + <TableRow + key={`review-table-row-${pluralId}`} + sx={{ height: 50 }} + > + <TableCell + sx={{ + minWidth: 'fit-content', + width: 0, + background: (theme) => + theme.palette.background.default, + }} + > + <Stack + direction="row" + spacing={1} + style={{ + alignItems: 'center', + textTransform: 'capitalize', + }} + > + <Icon style={{ fontSize: 12 }} /> + + <Box> + {intl.formatMessage( + { id: pluralId }, + { count } + )} + </Box> + </Stack> + </TableCell> + <TableCell>{cell}</TableCell> + </TableRow> + ); + })} + </TableBody> + </Table> + </TableContainer> + ); +} + +export default ReviewTable; diff --git a/src/components/shared/Entity/prompts/steps/dataFlowReset/ReviewSelection/definition.ts b/src/components/shared/Entity/prompts/steps/dataFlowReset/ReviewSelection/definition.ts new file mode 100644 index 000000000..96a8aa95b --- /dev/null +++ b/src/components/shared/Entity/prompts/steps/dataFlowReset/ReviewSelection/definition.ts @@ -0,0 +1,12 @@ +import { defaultStepState } from '../../../store/shared'; +import { PromptStep } from '../../../types'; +import ReviewSelection from '.'; + +export const ReviewSelectionStep: PromptStep = { + StepComponent: ReviewSelection, + stepLabelMessageId: 'dataFlowReset.reviewSelection.title', + state: { + ...defaultStepState, + valid: true, + }, +}; diff --git a/src/components/shared/Entity/prompts/steps/dataFlowReset/ReviewSelection/index.tsx b/src/components/shared/Entity/prompts/steps/dataFlowReset/ReviewSelection/index.tsx new file mode 100644 index 000000000..04d8248f0 --- /dev/null +++ b/src/components/shared/Entity/prompts/steps/dataFlowReset/ReviewSelection/index.tsx @@ -0,0 +1,33 @@ +import { Stack, Typography } from '@mui/material'; +import MessageWithLink from 'components/content/MessageWithLink'; +import AlertBox from 'components/shared/AlertBox'; +import { useIntl } from 'react-intl'; +import ReviewTable from './ReviewTable'; + +function ReviewSelection() { + const intl = useIntl(); + + return ( + <Stack spacing={2}> + <AlertBox + short + severity="warning" + title={intl.formatMessage({ + id: 'dataFlowReset.reviewSelection.warning.title', + })} + > + <MessageWithLink messageID="dataFlowReset.reviewSelection.warning.message" /> + </AlertBox> + + <Typography> + {intl.formatMessage({ + id: 'dataFlowReset.reviewSelection.instructions', + })} + </Typography> + + <ReviewTable /> + </Stack> + ); +} + +export default ReviewSelection; diff --git a/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/BindingReview.tsx b/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/BindingReview.tsx deleted file mode 100644 index 7275a9aa3..000000000 --- a/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/BindingReview.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { Stack, Typography } from '@mui/material'; -import AlertBox from 'components/shared/AlertBox'; -import ChipList from 'components/shared/ChipList'; -import { useIntl } from 'react-intl'; -import { useBindingStore } from 'stores/Binding/Store'; -import { useShallow } from 'zustand/react/shallow'; -import Materializations from './Materializations'; - -function BindingReview() { - const intl = useIntl(); - - const collectionsBeingBackfilled = useBindingStore( - useShallow((state) => { - return state.backfilledBindings.map((backfilledBinding) => { - return state.resourceConfigs[backfilledBinding].meta - .collectionName; - }); - }) - ); - return ( - <Stack direction="column" spacing={2}> - <AlertBox - short - severity="warning" - title={intl.formatMessage({ - id: 'dataflowReset.warning.title', - })} - > - {intl.formatMessage({ id: 'dataflowReset.warning.message' })} - </AlertBox> - - <Typography> - {intl.formatMessage( - { id: 'dataflowReset.step1.message' }, - { - entityCount: collectionsBeingBackfilled.length, - } - )} - </Typography> - <ChipList values={collectionsBeingBackfilled} maxChips={10} /> - - <Materializations selected={collectionsBeingBackfilled} /> - </Stack> - ); -} - -export default BindingReview; diff --git a/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/Materializations.tsx b/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/Materializations.tsx deleted file mode 100644 index a77b4d8fa..000000000 --- a/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/Materializations.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { Box, LinearProgress, Typography } from '@mui/material'; -import Error from 'components/shared/Error'; -import { useConfirmationModalContext } from 'context/Confirmation'; -import { useLiveSpecsExt_related } from 'hooks/useLiveSpecsExt'; -import { useEffect, useMemo } from 'react'; -import { useIntl } from 'react-intl'; -import { useBindingStore } from 'stores/Binding/Store'; -import { hasLength } from 'utils/misc-utils'; -import Selector from './Selector'; -import { BindingReviewProps } from './types'; - -function Materializations({ selected }: BindingReviewProps) { - const intl = useIntl(); - - const { related, error, isValidating } = useLiveSpecsExt_related(selected); - - const confirmationModal = useConfirmationModalContext(); - - const foundData = useMemo(() => hasLength(related), [related]); - - const [backfillDataFlowTarget] = useBindingStore((state) => [ - state.backfillDataFlowTarget, - ]); - - useEffect(() => { - // TODO (data flow reset) - // This needs to get worked into the steps somehow.... the steps need to be able to say - // they are "allowed to continue" - confirmationModal?.setContinueAllowed( - Boolean((!isValidating && !foundData) || backfillDataFlowTarget) - ); - }, [foundData, isValidating, confirmationModal, backfillDataFlowTarget]); - - return ( - <Box> - <Typography> - {intl.formatMessage({ - id: 'resetDataFlow.materializations.header', - })} - </Typography> - - {isValidating ? <LinearProgress /> : null} - - {error ? <Error error={error} condensed /> : null} - - {!error && foundData ? ( - <Selector keys={related} value={null} /> - ) : ( - <Box> - {intl.formatMessage({ - id: 'resetDataFlow.materializations.empty', - })} - </Box> - )} - </Box> - ); -} - -export default Materializations; diff --git a/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/Selector.tsx b/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/Selector.tsx index 298da3f8b..ab5cafac6 100644 --- a/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/Selector.tsx +++ b/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/Selector.tsx @@ -1,28 +1,60 @@ -import { Autocomplete, Grid, TextField } from '@mui/material'; +import { Autocomplete, Grid, Skeleton, TextField } from '@mui/material'; import { autoCompleteDefaults_Virtual } from 'components/shared/AutoComplete/DefaultProps'; -import { ReactNode, useState } from 'react'; +import { ReactNode, useEffect, useState } from 'react'; import { useIntl } from 'react-intl'; -import { useBindingStore } from 'stores/Binding/Store'; +import { useLoopIndex } from 'context/LoopIndex/useLoopIndex'; +import { LiveSpecsExt_Related } from 'api/liveSpecsExt'; +import AlertBox from 'components/shared/AlertBox'; +import { usePreSavePromptStore } from '../../../store/usePreSavePromptStore'; import { RelatedMaterializationSelectorProps } from './types'; import SelectorOption from './SelectorOption'; const getValue = (option: any) => typeof option === 'string' ? option : option?.catalog_name; -function Selector({ disabled, keys }: RelatedMaterializationSelectorProps) { +function Selector({ + disabled, + keys, + loading, +}: RelatedMaterializationSelectorProps) { const intl = useIntl(); + const stepIndex = useLoopIndex(); const [inputValue, setInputValue] = useState(''); - const [backfillDataFlowTarget, setBackfillDataFlowTarget] = useBindingStore( - (state) => [ - state.backfillDataFlowTarget, - state.setBackfillDataFlowTarget, - ] - ); + const [updateStep, updateContext] = usePreSavePromptStore((state) => [ + state.updateStep, + state.updateContext, + ]); + + const backfillTarget = usePreSavePromptStore((state) => { + state.context.backfillTarget; + }); + + useEffect(() => { + updateContext({ + noMaterializations: Boolean(!loading && keys.length === 0), + }); + }, [keys.length, loading, updateContext]); + + if (loading) { + return <Skeleton />; + } if (keys.length === 0) { - return null; + return ( + <AlertBox + severity="info" + short + title={intl.formatMessage({ + id: 'resetDataFlow.materializations.empty.header', + })} + > + {intl.formatMessage({ + id: 'resetDataFlow.materializations.empty.message', + })} + </AlertBox> + ); } return ( @@ -33,14 +65,22 @@ function Selector({ disabled, keys }: RelatedMaterializationSelectorProps) { getOptionLabel={getValue} inputValue={inputValue} isOptionEqualToValue={(option, optionValue) => { - return option.catalog_name === optionValue; + return option.catalog_name === optionValue.catalog_name; }} options={keys} - value={backfillDataFlowTarget} - onChange={(_event, newValue) => { - setBackfillDataFlowTarget( - newValue ? newValue.catalog_name : null - ); + value={backfillTarget} + onChange={(_event, newValue: LiveSpecsExt_Related | null) => { + const newBackfillTarget = newValue ? newValue : null; + + console.log('newBackfillTarget', newBackfillTarget); + + updateStep(stepIndex, { + valid: Boolean(newBackfillTarget), + }); + + updateContext({ + backfillTarget: newBackfillTarget, + }); }} onInputChange={(_event, newInputValue) => { setInputValue(newInputValue); @@ -64,7 +104,7 @@ function Selector({ disabled, keys }: RelatedMaterializationSelectorProps) { const RowContent = ( <SelectorOption option={option} - x-react-window-item-height={75} + x-react-window-item-height={100} /> ); diff --git a/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/SelectorOption.tsx b/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/SelectorOption.tsx index c1995e4d9..a1f8ff2a8 100644 --- a/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/SelectorOption.tsx +++ b/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/SelectorOption.tsx @@ -1,4 +1,4 @@ -import { Stack, Typography } from '@mui/material'; +import { Box, Stack, Typography } from '@mui/material'; import ChipList from 'components/shared/ChipList'; import { truncateTextSx } from 'context/Theme'; import { MaterializationSelectorOptionProps } from './types'; @@ -7,12 +7,26 @@ function SelectorOption({ option }: MaterializationSelectorOptionProps) { const { catalog_name, reads_from } = option; return ( - <Stack component="span" direction="column" spacing={1}> - <Typography component="span" sx={truncateTextSx}> - {catalog_name} - </Typography> + <Stack component="span" direction="column" spacing={2}> + <Box + sx={{ + maxHeight: 20, + }} + > + <Typography component="span" sx={truncateTextSx}> + {catalog_name} + </Typography> + </Box> - <ChipList values={reads_from} maxChips={3} /> + <ChipList + values={reads_from} + maxChips={5} + sx={{ + pl: 10, + minHeight: 65, + maxHeight: 65, + }} + /> </Stack> ); } diff --git a/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/definition.ts b/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/definition.ts new file mode 100644 index 000000000..f87b4fa11 --- /dev/null +++ b/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/definition.ts @@ -0,0 +1,18 @@ +import { defaultStepState } from '../../../store/shared'; +import { PromptStep } from '../../../types'; +import SelectMaterialization from '.'; + +// interface SelectMaterializationStepContext { +// backfillTarget: LiveSpecsExt_Related | null; +// noMaterializations: boolean | null; +// relatedMaterializations: LiveSpecsExt_Related[] | null; +// } + +export const SelectMaterializationStep: PromptStep = { + StepComponent: SelectMaterialization, + stepLabelMessageId: 'dataFlowReset.selectMaterialization.title', + state: { + ...defaultStepState, + valid: false, + }, +}; diff --git a/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/index.tsx b/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/index.tsx index 8e69e0e99..ccc175679 100644 --- a/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/index.tsx +++ b/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/index.tsx @@ -1,17 +1,40 @@ -import { StepContent, StepLabel } from '@mui/material'; -import ErrorBoundryWrapper from 'components/shared/ErrorBoundryWrapper'; -import BindingReview from './BindingReview'; +import { Box, Typography } from '@mui/material'; +import { useEditorStore_queryResponse_draftSpecs } from 'components/editor/Store/hooks'; +import Error from 'components/shared/Error'; +import { useLiveSpecsExt_related } from 'hooks/useLiveSpecsExt'; +import { useIntl } from 'react-intl'; +import Selector from './Selector'; function SelectMaterialization() { + const intl = useIntl(); + + const draftSpecs = useEditorStore_queryResponse_draftSpecs(); + + // TODO (data flow backfill) + // Go ahead and work this into the hydration of the store... I think + const { related, error, isValidating } = useLiveSpecsExt_related( + draftSpecs[0].catalog_name + ); + return ( - <> - <StepLabel>Select materialization for data flow reset</StepLabel> - <StepContent> - <ErrorBoundryWrapper> - <BindingReview /> - </ErrorBoundryWrapper> - </StepContent> - </> + <Box> + <Typography> + {intl.formatMessage( + { + id: 'resetDataFlow.materializations.header', + }, + { + captureName: draftSpecs[0].catalog_name, + } + )} + </Typography> + + {error ? ( + <Error error={error} condensed /> + ) : ( + <Selector keys={related} value={null} loading={isValidating} /> + )} + </Box> ); } diff --git a/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/types.ts b/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/types.ts index b2e63da83..ea2530239 100644 --- a/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/types.ts +++ b/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/types.ts @@ -1,13 +1,16 @@ -import { LiveSpecsExt_Related } from 'hooks/useLiveSpecsExt'; +import { LiveSpecsExt_Related } from 'api/liveSpecsExt'; -export interface BindingReviewProps { - selected: string[]; -} +// export interface BindingReviewProps { +// selected: string[]; +// } + +// export type MaterializationsProps = BindingReviewProps; export interface RelatedMaterializationSelectorProps { keys: LiveSpecsExt_Related[]; value: string | null; disabled?: boolean; + loading?: boolean; onChange?: ( event: any, newValue: string[], diff --git a/src/components/shared/Entity/prompts/steps/dataFlowReset/UpdateMaterialization/definition.ts b/src/components/shared/Entity/prompts/steps/dataFlowReset/UpdateMaterialization/definition.ts new file mode 100644 index 000000000..0b8046b96 --- /dev/null +++ b/src/components/shared/Entity/prompts/steps/dataFlowReset/UpdateMaterialization/definition.ts @@ -0,0 +1,15 @@ +import { defaultStepState } from '../../../store/shared'; +import { PromptStep } from '../../../types'; +import UpdateMaterialization from '.'; + +// interface UpdateMaterializationStepContext { +// backfilledDraftId: string | null; +// } + +export const UpdateMaterializationStep: PromptStep = { + StepComponent: UpdateMaterialization, + stepLabelMessageId: 'dataFlowReset.updateMaterialization.title', + state: { + ...defaultStepState, + }, +}; diff --git a/src/components/shared/Entity/prompts/steps/dataFlowReset/UpdateMaterialization/index.tsx b/src/components/shared/Entity/prompts/steps/dataFlowReset/UpdateMaterialization/index.tsx index efe8f9739..bd14fee85 100644 --- a/src/components/shared/Entity/prompts/steps/dataFlowReset/UpdateMaterialization/index.tsx +++ b/src/components/shared/Entity/prompts/steps/dataFlowReset/UpdateMaterialization/index.tsx @@ -1,17 +1,117 @@ -import { StepContent, StepLabel } from '@mui/material'; -import ErrorBoundryWrapper from 'components/shared/ErrorBoundryWrapper'; +import { createEntityDraft } from 'api/drafts'; +import { createDraftSpec } from 'api/draftSpecs'; +import { getLiveSpecSpec } from 'api/liveSpecsExt'; + +import { ProgressStates } from 'components/tables/RowActions/Shared/types'; +import { useLoopIndex } from 'context/LoopIndex/useLoopIndex'; +import { useMount } from 'react-use'; +import { useBinding_collectionsBeingBackfilled } from 'stores/Binding/hooks'; +import { + getBindingAsFullSource, + getCollectionName, +} from 'utils/workflow-utils'; +import { usePreSavePromptStore } from '../../../store/usePreSavePromptStore'; function MarkMaterialization() { - return ( - <> - <StepLabel>Mark materialization notBefore</StepLabel> - <StepContent> - <ErrorBoundryWrapper> - describe what we're doing - </ErrorBoundryWrapper> - </StepContent> - </> - ); + const stepIndex = useLoopIndex(); + const thisStep = usePreSavePromptStore((state) => state.steps[stepIndex]); + + const collectionsBeingBackfilled = useBinding_collectionsBeingBackfilled(); + + const [updateStep, updateContext, nextStep, context, initUUID] = + usePreSavePromptStore((state) => [ + state.updateStep, + state.updateContext, + state.nextStep, + state.context, + state.initUUID, + ]); + + useMount(() => { + if (thisStep.state.progress === ProgressStates.IDLE) { + updateStep(stepIndex, { + progress: ProgressStates.RUNNING, + }); + + const updateMaterializationTimestamp = async () => { + const liveSpecResponse = await getLiveSpecSpec( + context.backfillTarget.id + ); + + if (liveSpecResponse.error) { + updateStep(stepIndex, { + error: liveSpecResponse.error, + progress: ProgressStates.FAILED, + }); + return; + } + + const draftsResponse = await createEntityDraft( + `data flow backfill : update : ${initUUID}` + ); + const backfilledDraftId = draftsResponse.data?.[0].id; + + if (draftsResponse.error || !backfilledDraftId) { + updateStep(stepIndex, { + error: draftsResponse.error, + progress: ProgressStates.FAILED, + }); + return; + } + + updateContext({ + backfilledDraftId, + }); + + // Update the spec here + const updatedSpec = { + ...liveSpecResponse.data.spec, + }; + updatedSpec.bindings.forEach((binding: any) => { + if ( + collectionsBeingBackfilled.includes( + getCollectionName(binding) + ) + ) { + binding.source = getBindingAsFullSource(binding); + binding.source.notBefore = context.timeStopped; + } + }); + + // Add to the draft + const draftedMaterialization = await createDraftSpec( + backfilledDraftId, + context.backfillTarget.catalog_name, + updatedSpec, + 'materialization', + undefined, + false + ); + + if (draftedMaterialization.error) { + updateStep(stepIndex, { + error: draftedMaterialization.error, + progress: ProgressStates.FAILED, + }); + return; + } + + updateStep(stepIndex, { + progress: ProgressStates.SUCCESS, + valid: true, + }); + + nextStep(); + }; + + void updateMaterializationTimestamp(); + } else { + console.log('TODO: need to handle showing previous state?'); + } + }); + + // eslint-disable-next-line react/jsx-no-useless-fragment + return <></>; } export default MarkMaterialization; diff --git a/src/components/shared/Entity/prompts/steps/dataFlowReset/WaitForCaptureStop/definition.ts b/src/components/shared/Entity/prompts/steps/dataFlowReset/WaitForCaptureStop/definition.ts new file mode 100644 index 000000000..177e0a8dd --- /dev/null +++ b/src/components/shared/Entity/prompts/steps/dataFlowReset/WaitForCaptureStop/definition.ts @@ -0,0 +1,16 @@ +import { defaultStepState } from '../../../store/shared'; +import { PromptStep } from '../../../types'; +import WaitForCapture from '.'; + +// interface WaitForCaptureStepContext { +// liveSpecId: string | null; +// timeStopped: string | null; +// } + +export const WaitForCaptureStep: PromptStep = { + StepComponent: WaitForCapture, + stepLabelMessageId: 'dataFlowReset.waitForCapture.title', + state: { + ...defaultStepState, + }, +}; diff --git a/src/components/shared/Entity/prompts/steps/dataFlowReset/WaitForCaptureStop/index.tsx b/src/components/shared/Entity/prompts/steps/dataFlowReset/WaitForCaptureStop/index.tsx index fcb43169e..93564f208 100644 --- a/src/components/shared/Entity/prompts/steps/dataFlowReset/WaitForCaptureStop/index.tsx +++ b/src/components/shared/Entity/prompts/steps/dataFlowReset/WaitForCaptureStop/index.tsx @@ -1,18 +1,77 @@ -import { LinearProgress, StepContent, StepLabel } from '@mui/material'; -import ErrorBoundryWrapper from 'components/shared/ErrorBoundryWrapper'; +import { getLiveSpecIdByPublication } from 'api/publicationSpecsExt'; +import { useEditorStore_catalogName } from 'components/editor/Store/hooks'; +import { ProgressStates } from 'components/tables/RowActions/Shared/types'; +import { useLoopIndex } from 'context/LoopIndex/useLoopIndex'; +import { useMount } from 'react-use'; +import { DateTime } from 'luxon'; +import { usePreSavePromptStore } from '../../../store/usePreSavePromptStore'; function WaitForCaptureStop() { - return ( - <> - <StepLabel>Wait for capture data to stop</StepLabel> - <StepContent> - <ErrorBoundryWrapper> - <LinearProgress /> - No logs... but show something - </ErrorBoundryWrapper> - </StepContent> - </> - ); + const catalogName = useEditorStore_catalogName(); + + const stepIndex = useLoopIndex(); + const thisStep = usePreSavePromptStore((state) => state.steps[stepIndex]); + + const [context, updateStep, updateContext, nextStep] = + usePreSavePromptStore((state) => [ + state.context, + state.updateStep, + state.updateContext, + state.nextStep, + ]); + + useMount(() => { + if (thisStep.state.progress === ProgressStates.IDLE) { + updateStep(stepIndex, { + progress: ProgressStates.RUNNING, + }); + + const waitForSpecToFullyStop = async () => { + const liveSpecResponse = await getLiveSpecIdByPublication( + context.pubId, + catalogName + ); + + const liveSpecId = liveSpecResponse.data?.[0].live_spec_id; + + if (liveSpecResponse.error || !liveSpecId) { + updateStep(stepIndex, { + error: liveSpecResponse.error, + progress: ProgressStates.FAILED, + }); + return; + } + + // TODO (data flow) this needs to actually fetch the time for this + // Start calling for shards + // Loop over it until we see nothing is coming + // Snag time + + const timeStopped = DateTime.utc().toFormat( + `yyyy-MM-dd'T'HH:mm:ss'Z'` + ); + updateContext({ + liveSpecId, + timeStopped, + }); + + // Fake timeout to make it feel more async + setTimeout(() => { + updateStep(stepIndex, { + valid: true, + }); + nextStep(); + }, 1000); + }; + + void waitForSpecToFullyStop(); + } else { + console.log('TODO: need to handle showing previous state?'); + } + }); + + // eslint-disable-next-line react/jsx-no-useless-fragment + return <></>; } export default WaitForCaptureStop; diff --git a/src/components/shared/Entity/prompts/steps/dataFlowReset/shared.ts b/src/components/shared/Entity/prompts/steps/dataFlowReset/shared.ts index 7fdf66b9e..13bbfaff5 100644 --- a/src/components/shared/Entity/prompts/steps/dataFlowReset/shared.ts +++ b/src/components/shared/Entity/prompts/steps/dataFlowReset/shared.ts @@ -1,14 +1,19 @@ -import { ReactJSXElement } from '@emotion/react/types/jsx-namespace'; -import DisableCapture from './DisableCapture'; -import EnableCapture from './EnableCapture'; -import SelectMaterialization from './SelectMaterialization'; -import MarkMaterialization from './UpdateMaterialization'; -import WaitForCaptureStop from './WaitForCaptureStop'; +import { DisableCaptureStep } from './DisableCapture/definition'; +import { EnableCaptureStep } from './EnableCapture/definition'; +import { ReviewSelectionStep } from './ReviewSelection/definition'; +import { SelectMaterializationStep } from './SelectMaterialization/definition'; +import { UpdateMaterializationStep } from './UpdateMaterialization/definition'; +import { WaitForCaptureStep } from './WaitForCaptureStop/definition'; -export const DataFlowResetSteps: (() => ReactJSXElement)[] = [ - SelectMaterialization, - DisableCapture, - WaitForCaptureStop, - MarkMaterialization, - EnableCapture, -]; +export const DataFlowSteps = { + selectMaterialization: SelectMaterializationStep, + reviewSelection: ReviewSelectionStep, + disableCapture: DisableCaptureStep, + waitForCapture: WaitForCaptureStep, + updateMaterialization: UpdateMaterializationStep, + enableCapture: EnableCaptureStep, +}; + +// !!!!!!!!!ORDER IS IMPORTANT!!!!!!!!!!!! +// We run through steps in order +export const DataFlowResetSteps = Object.values(DataFlowSteps); diff --git a/src/components/shared/Entity/prompts/steps/preSave/ChangeReview/definition.ts b/src/components/shared/Entity/prompts/steps/preSave/ChangeReview/definition.ts new file mode 100644 index 000000000..7586c885e --- /dev/null +++ b/src/components/shared/Entity/prompts/steps/preSave/ChangeReview/definition.ts @@ -0,0 +1,12 @@ +import { defaultStepState } from '../../../store/shared'; +import { PromptStep } from '../../../types'; +import ChangeReview from '.'; + +export const ChangeReviewStep: PromptStep = { + StepComponent: ChangeReview, + stepLabelMessageId: 'preSavePrompt.changeReview.title', + state: { + ...defaultStepState, + valid: true, + }, +}; diff --git a/src/components/shared/Entity/prompts/steps/preSave/ChangeReview/index.tsx b/src/components/shared/Entity/prompts/steps/preSave/ChangeReview/index.tsx index d9d6e9346..177768eab 100644 --- a/src/components/shared/Entity/prompts/steps/preSave/ChangeReview/index.tsx +++ b/src/components/shared/Entity/prompts/steps/preSave/ChangeReview/index.tsx @@ -1,18 +1,7 @@ -import { StepContent, StepLabel } from '@mui/material'; -import ErrorBoundryWrapper from 'components/shared/ErrorBoundryWrapper'; import DiffViewer from './DiffViewer'; function ChangeReview() { - return ( - <> - <StepLabel>How the spec is changing</StepLabel> - <StepContent> - <ErrorBoundryWrapper> - <DiffViewer /> - </ErrorBoundryWrapper> - </StepContent> - </> - ); + return <DiffViewer />; } export default ChangeReview; diff --git a/src/components/shared/Entity/prompts/steps/preSave/Done/index.tsx b/src/components/shared/Entity/prompts/steps/preSave/Done/index.tsx deleted file mode 100644 index a458f6a87..000000000 --- a/src/components/shared/Entity/prompts/steps/preSave/Done/index.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { StepContent, StepLabel } from '@mui/material'; -import ErrorBoundryWrapper from 'components/shared/ErrorBoundryWrapper'; - -function Done() { - return ( - <> - <StepLabel>Done</StepLabel> - <StepContent> - <ErrorBoundryWrapper> - Congrats you are done! - </ErrorBoundryWrapper> - </StepContent> - </> - ); -} - -export default Done; diff --git a/src/components/shared/Entity/prompts/steps/preSave/Publish/definition.ts b/src/components/shared/Entity/prompts/steps/preSave/Publish/definition.ts new file mode 100644 index 000000000..5b0f4f9fa --- /dev/null +++ b/src/components/shared/Entity/prompts/steps/preSave/Publish/definition.ts @@ -0,0 +1,9 @@ +import { defaultStepState } from '../../../store/shared'; +import { PromptStep } from '../../../types'; +import Publish from '.'; + +export const PublishStep: PromptStep = { + StepComponent: Publish, + stepLabelMessageId: 'preSavePrompt.publish.title', + state: defaultStepState, +}; diff --git a/src/components/shared/Entity/prompts/steps/preSave/Publish/index.tsx b/src/components/shared/Entity/prompts/steps/preSave/Publish/index.tsx index 12b11fbfa..75867d502 100644 --- a/src/components/shared/Entity/prompts/steps/preSave/Publish/index.tsx +++ b/src/components/shared/Entity/prompts/steps/preSave/Publish/index.tsx @@ -1,15 +1,91 @@ -import { StepContent, StepLabel } from '@mui/material'; -import ErrorBoundryWrapper from 'components/shared/ErrorBoundryWrapper'; +import { + createPublication, + getPublicationByIdQuery, + PublicationJobStatus, +} from 'api/publications'; +import { ProgressStates } from 'components/tables/RowActions/Shared/types'; +import { useLoopIndex } from 'context/LoopIndex/useLoopIndex'; +import useJobStatusPoller from 'hooks/useJobStatusPoller'; +import { useMount } from 'react-use'; +import { useDetailsFormStore } from 'stores/DetailsForm/Store'; +import { usePreSavePromptStore } from '../../../store/usePreSavePromptStore'; function Publish() { - return ( - <> - <StepLabel>Publishing</StepLabel> - <StepContent> - <ErrorBoundryWrapper>Logs</ErrorBoundryWrapper> - </StepContent> - </> + const { jobStatusPoller } = useJobStatusPoller(); + + const stepIndex = useLoopIndex(); + const thisStep = usePreSavePromptStore((state) => state.steps[stepIndex]); + + const [updateStep, context, updateContext, initUUID] = + usePreSavePromptStore((state) => [ + state.updateStep, + state.context, + state.updateContext, + state.initUUID, + ]); + + // TODO (data flow reset) need to plumb this through correctly + const dataPlaneName = useDetailsFormStore( + (state) => state.details.data.dataPlane?.dataPlaneName ); + + useMount(() => { + if (thisStep.state.progress === ProgressStates.IDLE) { + updateStep(stepIndex, { + progress: ProgressStates.RUNNING, + }); + + const saveAndPublish = async () => { + // Start publishing it + const publishResponse = await createPublication( + context.backfilledDraftId, + false, + `data flow backfill : publish : ${initUUID}`, + dataPlaneName?.whole + ); + + if (publishResponse.error || !publishResponse.data) { + updateStep(stepIndex, { + error: publishResponse.error, + progress: ProgressStates.FAILED, + }); + return; + } + + updateContext({ + pubId: publishResponse.data[0].id, + }); + + jobStatusPoller( + getPublicationByIdQuery(publishResponse.data[0].id), + async (successResponse: PublicationJobStatus) => { + updateStep(stepIndex, { + publicationStatus: successResponse, + }); + }, + async ( + failedResponse: any //PublicationJobStatus | PostgrestError + ) => { + updateStep(stepIndex, { + error: failedResponse.error ? failedResponse : null, + publicationStatus: !failedResponse.error + ? failedResponse + : null, + progress: ProgressStates.FAILED, + valid: false, + }); + } + ); + }; + + void saveAndPublish(); + } else { + console.log('TODO: need to handle showing previous state?'); + } + }); + + // eslint-disable-next-line react/jsx-no-useless-fragment + return <></>; } export default Publish; diff --git a/src/components/shared/Entity/prompts/steps/preSave/usePreSavePromptSteps.tsx b/src/components/shared/Entity/prompts/steps/preSave/usePreSavePromptSteps.tsx deleted file mode 100644 index 82d6ceb57..000000000 --- a/src/components/shared/Entity/prompts/steps/preSave/usePreSavePromptSteps.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { ReactJSXElement } from '@emotion/react/types/jsx-namespace'; -import { useMemo } from 'react'; -import { createGlobalState } from 'react-use'; -import { useBinding_backfilledBindings_count } from 'stores/Binding/hooks'; -import { useBindingStore } from 'stores/Binding/Store'; -import { DataFlowResetSteps } from '../dataFlowReset/shared'; -import ChangeReviewStep from './ChangeReview'; -import Done from './Done'; -import Publish from './Publish'; - -// TODO (data flow reset) this stuff should go into a store -// also we probably need to keep if a step is done within the step itself -// that way a user could go back and view the outcome of a state while -// other states are running. - -// step data sketch -// label - string or node -// content - react node -// done - boolean -// errors - array of issues (cannot continue until gone) -// status? - maybe... need to know if it is running or not? -// action? - maybe... storing off the functions/hooks/whatevs that will run on this step -export const useGlobalValue = createGlobalState<number>(0); - -function usePreSavePromptSteps() { - const [activeStep, setActiveStep] = useGlobalValue(); - - const backfillDataflow = useBindingStore((state) => state.backfillDataFlow); - const needsBackfilled = useBinding_backfilledBindings_count(); - - const steps = useMemo(() => { - const response: (() => ReactJSXElement)[] = [ChangeReviewStep]; - - if (backfillDataflow && needsBackfilled) { - response.push(...DataFlowResetSteps); - } - - response.push(Publish, Done); - - return response; - }, [backfillDataflow, needsBackfilled]); - - return { - steps, - activeStep, - setActiveStep, - handleBack: () => { - setActiveStep((prevActiveStep) => prevActiveStep - 1); - }, - handleNext: () => { - setActiveStep((prevActiveStep) => prevActiveStep + 1); - }, - }; -} - -export default usePreSavePromptSteps; diff --git a/src/components/shared/Entity/prompts/store/Hydrator.tsx b/src/components/shared/Entity/prompts/store/Hydrator.tsx new file mode 100644 index 000000000..539763da9 --- /dev/null +++ b/src/components/shared/Entity/prompts/store/Hydrator.tsx @@ -0,0 +1,23 @@ +import { BaseComponentProps } from 'types'; +import { useEffect } from 'react'; +import { useBinding_backfilledBindings_count } from 'stores/Binding/hooks'; +import { useBindingStore } from 'stores/Binding/Store'; +import { usePreSavePromptStore } from './usePreSavePromptStore'; + +function PromptsHydrator({ children }: BaseComponentProps) { + const [initializeSteps] = usePreSavePromptStore((state) => [ + state.initializeSteps, + ]); + + const backfillDataflow = useBindingStore((state) => state.backfillDataFlow); + const needsBackfilled = useBinding_backfilledBindings_count(); + + useEffect(() => { + initializeSteps(Boolean(backfillDataflow && needsBackfilled)); + }, [backfillDataflow, initializeSteps, needsBackfilled]); + + // eslint-disable-next-line react/jsx-no-useless-fragment + return <>{children}</>; +} + +export default PromptsHydrator; diff --git a/src/components/shared/Entity/prompts/store/shared.ts b/src/components/shared/Entity/prompts/store/shared.ts new file mode 100644 index 000000000..bb65eab60 --- /dev/null +++ b/src/components/shared/Entity/prompts/store/shared.ts @@ -0,0 +1,9 @@ +import { ProgressStates } from 'components/tables/RowActions/Shared/types'; +import { PromptStepState } from '../types'; + +export const defaultStepState: PromptStepState = { + error: null, + progress: ProgressStates.IDLE, + started: false, + valid: false, +}; diff --git a/src/components/shared/Entity/prompts/store/types.ts b/src/components/shared/Entity/prompts/store/types.ts new file mode 100644 index 000000000..c6884f8b8 --- /dev/null +++ b/src/components/shared/Entity/prompts/store/types.ts @@ -0,0 +1,21 @@ +import { PromptStep, PromptStepState } from '../types'; + +// TODO (typing) would like to auto generate the `context` type somehow +// by combining multiple steps contexts +export interface PreSavePromptStore<T = any> { + steps: PromptStep[]; + + context: T; + updateContext: (setting: any) => void; + + updateStep: (step: number, settings: Partial<PromptStepState>) => void; + initializeSteps: (backfillEnabled: boolean) => void; + initUUID: string | null; + + activeStep: number; + setActiveStep: (val: PreSavePromptStore['activeStep']) => void; + nextStep: () => void; + previousStep: () => void; + + resetState: () => void; +} diff --git a/src/components/shared/Entity/prompts/store/usePreSavePromptStore.ts b/src/components/shared/Entity/prompts/store/usePreSavePromptStore.ts new file mode 100644 index 000000000..9524ee776 --- /dev/null +++ b/src/components/shared/Entity/prompts/store/usePreSavePromptStore.ts @@ -0,0 +1,167 @@ +import produce from 'immer'; +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import { devtoolsOptions } from 'utils/store-utils'; +import { useShallow } from 'zustand/react/shallow'; +import { ProgressStates } from 'components/tables/RowActions/Shared/types'; +import { JOB_STATUS_FAILURE, JOB_STATUS_SUCCESS } from 'services/supabase'; +import { logRocketEvent } from 'services/shared'; +import { CustomEvents } from 'services/types'; +import { PromptStep } from '../types'; +import { DataFlowResetSteps } from '../steps/dataFlowReset/shared'; +import { ChangeReviewStep } from '../steps/preSave/ChangeReview/definition'; +import { PublishStep } from '../steps/preSave/Publish/definition'; +import { PreSavePromptStore } from './types'; + +const getInitialState = (): Pick< + PreSavePromptStore, + 'activeStep' | 'steps' | 'context' | 'initUUID' +> => ({ + activeStep: 0, + initUUID: null, + steps: [], + context: {}, +}); + +export const usePreSavePromptStore = create<PreSavePromptStore>()( + devtools((set) => { + return { + ...getInitialState(), + resetState: () => set(getInitialState(), false, 'resetState'), + + initializeSteps: (backfillEnabled) => + set( + produce((state: PreSavePromptStore) => { + const initUUID = crypto.randomUUID(); + const newSteps: PromptStep[] = [ChangeReviewStep]; + + if (backfillEnabled) { + newSteps.push(...DataFlowResetSteps); + } + + newSteps.push(PublishStep); + + state.steps = newSteps; + state.initUUID = initUUID; + logRocketEvent(CustomEvents.BACKFILL_DATAFLOW, { + step: 'init', + initUUID, + }); + }), + false, + 'initializeSteps' + ), + + updateStep: (stepToUpdate, settings) => + set( + produce((state: PreSavePromptStore) => { + const updating = state.steps[stepToUpdate]; + + updating.state = { + ...updating.state, + ...settings, + }; + + const newStatus = + settings.publicationStatus?.job_status.type; + if (newStatus) { + if (JOB_STATUS_SUCCESS.includes(newStatus)) { + updating.state.progress = + ProgressStates.SUCCESS; + updating.state.valid = true; + updating.state.error = null; + } else if (JOB_STATUS_FAILURE.includes(newStatus)) { + updating.state.progress = ProgressStates.FAILED; + updating.state.valid = false; + updating.state.error = { + message: + 'dataFlowReset.errors.publishFailed', + }; + } + } + + logRocketEvent(CustomEvents.BACKFILL_DATAFLOW, { + step: updating.StepComponent.name, + progress: updating.state.progress, + }); + }), + false, + 'setActiveStep' + ), + + updateContext: (settings) => + set( + produce((state: PreSavePromptStore) => { + state.context = { + ...state.context, + ...settings, + }; + }), + false, + 'updateContext' + ), + + setActiveStep: (value) => + set( + produce((state: PreSavePromptStore) => { + state.activeStep = value; + }), + false, + 'setActiveStep' + ), + + nextStep: () => + set( + produce((state: PreSavePromptStore) => { + if (state.steps[state.activeStep].state.valid) { + state.steps[state.activeStep].state.progress = + ProgressStates.SUCCESS; + state.activeStep = state.activeStep + 1; + } + }), + false, + 'nextStep' + ), + + previousStep: () => + set( + produce((state: PreSavePromptStore) => { + const newVal = state.activeStep - 1; + state.activeStep = newVal >= 0 ? newVal : 0; + }), + false, + 'previousStep' + ), + }; + }, devtoolsOptions('estuary.presave-prompt-store')) +); + +export const usePreSavePromptStore_activeStep = () => { + return usePreSavePromptStore( + useShallow((state) => { + return state.steps[state.activeStep]?.state; + }) + ); +}; + +export const usePreSavePromptStore_stepValid = () => { + return usePreSavePromptStore( + useShallow((state) => state.steps[state.activeStep]?.state.valid) + ); +}; + +export const usePreSavePromptStore_onFirstStep = () => { + return usePreSavePromptStore( + useShallow((state) => { + return state.activeStep === 0; + }) + ); +}; + +export const usePreSavePromptStore_onLastStep = () => { + return usePreSavePromptStore( + useShallow((state) => { + return state.activeStep === state.steps.length - 1; + }) + ); +}; diff --git a/src/components/shared/Entity/prompts/types.ts b/src/components/shared/Entity/prompts/types.ts new file mode 100644 index 000000000..5c6032118 --- /dev/null +++ b/src/components/shared/Entity/prompts/types.ts @@ -0,0 +1,26 @@ +import { EmotionJSX } from '@emotion/react/types/jsx-namespace'; +import { PublicationJobStatus } from 'api/publications'; +import { ProgressStates } from 'components/tables/RowActions/Shared/types'; + +export interface PromptStepState { + // Both server and client side error + error: any | null; // PostgrestError + + // Stores what the step is currently doing + progress: ProgressStates; + + // Stores if we have ever tried _once_ + started: boolean; + + // Controls if the user can continue on from this step + valid: boolean; + + publicationStatus?: PublicationJobStatus; +} + +// TODO (dataflow typing) should try to get typing working with the context +export interface PromptStep { + StepComponent: () => EmotionJSX.Element; + stepLabelMessageId: string; + state: PromptStepState; +} diff --git a/src/components/shared/LinkWrapper.tsx b/src/components/shared/LinkWrapper.tsx index 25ed53c88..dbefebe10 100644 --- a/src/components/shared/LinkWrapper.tsx +++ b/src/components/shared/LinkWrapper.tsx @@ -1,13 +1,15 @@ import { Link, useMediaQuery, useTheme } from '@mui/material'; import { Link as ReactRouterLink } from 'react-router-dom'; import { BaseComponentProps } from 'types'; +import { OpenNewWindow } from 'iconoir-react'; interface Props extends BaseComponentProps { link: string; ariaLabel?: string; + newWindow?: boolean; } -function LinkWrapper({ ariaLabel, children, link }: Props) { +function LinkWrapper({ ariaLabel, children, link, newWindow }: Props) { const theme = useTheme(); const belowMd = useMediaQuery(theme.breakpoints.down('md')); @@ -15,9 +17,12 @@ function LinkWrapper({ ariaLabel, children, link }: Props) { <Link reloadDocument={false} component={ReactRouterLink} + target={newWindow ? '_blank' : undefined} to={link} aria-label={ariaLabel} sx={{ + display: 'flex', + alignItems: 'center', padding: 1, pl: 0, overflowWrap: belowMd ? 'break-word' : undefined, @@ -25,6 +30,11 @@ function LinkWrapper({ ariaLabel, children, link }: Props) { }} > {children} + {newWindow ? ( + <OpenNewWindow + style={{ height: 15, width: 15, marginLeft: 5 }} + /> + ) : null} </Link> ); } diff --git a/src/components/tables/RowActions/AccessGrants/Progress.tsx b/src/components/tables/RowActions/AccessGrants/Progress.tsx index 2769e4430..6e7415b0c 100644 --- a/src/components/tables/RowActions/AccessGrants/Progress.tsx +++ b/src/components/tables/RowActions/AccessGrants/Progress.tsx @@ -8,7 +8,7 @@ import { import Error from 'components/shared/Error'; import { CheckCircle, WarningCircle } from 'iconoir-react'; import { FormattedMessage } from 'react-intl'; -import { ProgressStates } from '../Shared/Progress'; +import { ProgressStates } from '../Shared/types'; interface Props { error: any | null; diff --git a/src/components/tables/RowActions/AccessGrants/RevokeGrant.tsx b/src/components/tables/RowActions/AccessGrants/RevokeGrant.tsx index d3d3988bf..c3a1a7426 100644 --- a/src/components/tables/RowActions/AccessGrants/RevokeGrant.tsx +++ b/src/components/tables/RowActions/AccessGrants/RevokeGrant.tsx @@ -1,6 +1,6 @@ import { deleteRoleGrant } from 'api/roleGrants'; import { deleteUserGrant } from 'api/userGrants'; -import { ProgressStates } from 'components/tables/RowActions/Shared/Progress'; +import { ProgressStates } from 'components/tables/RowActions/Shared/types'; import { useZustandStore } from 'context/Zustand/provider'; import { Dispatch, SetStateAction, useEffect, useState } from 'react'; import { useMount } from 'react-use'; diff --git a/src/components/tables/RowActions/AccessLinks/DisableDirective.tsx b/src/components/tables/RowActions/AccessLinks/DisableDirective.tsx index 00663a4f3..d72333c1e 100644 --- a/src/components/tables/RowActions/AccessLinks/DisableDirective.tsx +++ b/src/components/tables/RowActions/AccessLinks/DisableDirective.tsx @@ -1,6 +1,6 @@ import { disableDirective } from 'api/directives'; import Progress from 'components/tables/RowActions/AccessLinks/Progress'; -import { ProgressStates } from 'components/tables/RowActions/Shared/Progress'; +import { ProgressStates } from 'components/tables/RowActions/Shared/types'; import { useZustandStore } from 'context/Zustand/provider'; import { Dispatch, SetStateAction, useEffect, useState } from 'react'; import { SelectTableStoreNames } from 'stores/names'; diff --git a/src/components/tables/RowActions/AccessLinks/Progress.tsx b/src/components/tables/RowActions/AccessLinks/Progress.tsx index 4a6d908bb..919a76088 100644 --- a/src/components/tables/RowActions/AccessLinks/Progress.tsx +++ b/src/components/tables/RowActions/AccessLinks/Progress.tsx @@ -8,7 +8,7 @@ import { import Error from 'components/shared/Error'; import { CheckCircle, WarningCircle } from 'iconoir-react'; import { FormattedMessage } from 'react-intl'; -import { ProgressStates } from '../Shared/Progress'; +import { ProgressStates } from '../Shared/types'; interface Props { progress: ProgressStates; diff --git a/src/components/tables/RowActions/Shared/Progress.tsx b/src/components/tables/RowActions/Shared/Progress.tsx index f5dbe5155..ae7b55226 100644 --- a/src/components/tables/RowActions/Shared/Progress.tsx +++ b/src/components/tables/RowActions/Shared/Progress.tsx @@ -9,27 +9,8 @@ import { import ErrorLogs from 'components/shared/Entity/Error/Logs'; import Error from 'components/shared/Error'; import { CheckCircle, InfoCircle, WarningCircle } from 'iconoir-react'; -import { ReactNode } from 'react'; import { FormattedMessage } from 'react-intl'; - -export enum ProgressStates { - RUNNING = 1, - SKIPPED = 2, - FAILED = 3, - SUCCESS = 4, -} - -export interface SharedProgressProps { - name: string; - error: any | null; - logToken?: string | null; - renderError?: (error: any, progressState: ProgressStates) => ReactNode; - renderLogs?: Function | boolean; - skippedMessageID?: string; - runningMessageID: string; - successMessageID: string; - state: ProgressStates; -} +import { ProgressStates, SharedProgressProps } from './types'; const wrapperStyling = { mb: 1, ml: 3, width: '100%' }; diff --git a/src/components/tables/RowActions/Shared/UpdateEntity.tsx b/src/components/tables/RowActions/Shared/UpdateEntity.tsx index db453694d..b02b8f0ac 100644 --- a/src/components/tables/RowActions/Shared/UpdateEntity.tsx +++ b/src/components/tables/RowActions/Shared/UpdateEntity.tsx @@ -8,10 +8,6 @@ import { createPublication } from 'api/publications'; import AlertBox from 'components/shared/AlertBox'; import DraftErrors from 'components/shared/Entity/Error/DraftErrors'; import Error from 'components/shared/Error'; -import SharedProgress, { - ProgressStates, - SharedProgressProps, -} from 'components/tables/RowActions/Shared/Progress'; import { useZustandStore } from 'context/Zustand/provider'; import { LiveSpecsExtQueryWithSpec, @@ -26,6 +22,8 @@ import { selectableTableStoreSelectors, } from 'stores/Tables/Store'; import { Entity } from 'types'; +import SharedProgress from './Progress'; +import { ProgressStates, SharedProgressProps } from './types'; export interface UpdateEntityProps { entity: CaptureQuery; diff --git a/src/components/tables/RowActions/Shared/types.ts b/src/components/tables/RowActions/Shared/types.ts new file mode 100644 index 000000000..eeaff023d --- /dev/null +++ b/src/components/tables/RowActions/Shared/types.ts @@ -0,0 +1,27 @@ +import { ReactNode } from 'react'; + +export const ProgressFinished = 200; + +// Making these high numbers so we have room to put extras between +export enum ProgressStates { + IDLE = -100, // new for steps + PAUSED = 0, // Not used right now + RUNNING = 100, + + // Here and above is considered finished + SKIPPED = 200, + SUCCESS = 300, + FAILED = 400, +} + +export interface SharedProgressProps { + name: string; + error: any | null; + logToken?: string | null; + renderError?: (error: any, progressState: ProgressStates) => ReactNode; + renderLogs?: Function | boolean; + skippedMessageID?: string; + runningMessageID: string; + successMessageID: string; + state: ProgressStates; +} diff --git a/src/components/tables/cells/EntityNameLink.tsx b/src/components/tables/cells/EntityNameLink.tsx index f4b16ba4d..c89217b5f 100644 --- a/src/components/tables/cells/EntityNameLink.tsx +++ b/src/components/tables/cells/EntityNameLink.tsx @@ -1,7 +1,6 @@ -import { Box, Stack, TableCell, Tooltip } from '@mui/material'; -import LinkWrapper from 'components/shared/LinkWrapper'; +import { Stack, TableCell } from '@mui/material'; +import EntityNameDetailsLink from 'components/shared/Entity/EntityNameDetailsLink'; import EntityStatus from 'components/tables/cells/EntityStatus'; -import { useIntl } from 'react-intl'; import { ShardEntityTypes } from 'stores/ShardDetail/types'; interface Props { @@ -17,8 +16,6 @@ function EntityNameLink({ entityStatusTypes, showEntityStatus, }: Props) { - const intl = useIntl(); - return ( <TableCell sx={{ @@ -36,23 +33,7 @@ function EntityNameLink({ <EntityStatus name={name} taskTypes={entityStatusTypes} /> ) : null} - <Tooltip - title={intl.formatMessage({ - id: 'entityTable.detailsLink', - })} - > - <Box> - <LinkWrapper - ariaLabel={intl.formatMessage( - { id: 'entityTable.viewDetails.aria' }, - { name } - )} - link={detailsLink} - > - {name} - </LinkWrapper> - </Box> - </Tooltip> + <EntityNameDetailsLink name={name} path={detailsLink} /> </Stack> </TableCell> ); diff --git a/src/context/LoopIndex/index.tsx b/src/context/LoopIndex/index.tsx new file mode 100644 index 000000000..c1b3676b3 --- /dev/null +++ b/src/context/LoopIndex/index.tsx @@ -0,0 +1,13 @@ +import { LoopIndexContext } from './shared'; +import { LoopIndexContextProps } from './types'; + +export const LoopIndexContextProvider = ({ + children, + value, +}: LoopIndexContextProps) => { + return ( + <LoopIndexContext.Provider value={value}> + {children} + </LoopIndexContext.Provider> + ); +}; diff --git a/src/context/LoopIndex/shared.ts b/src/context/LoopIndex/shared.ts new file mode 100644 index 000000000..421ff6818 --- /dev/null +++ b/src/context/LoopIndex/shared.ts @@ -0,0 +1,3 @@ +import { createContext } from 'react'; + +export const LoopIndexContext = createContext<number | null>(null); diff --git a/src/context/LoopIndex/types.ts b/src/context/LoopIndex/types.ts new file mode 100644 index 000000000..cc81167d2 --- /dev/null +++ b/src/context/LoopIndex/types.ts @@ -0,0 +1,5 @@ +import { BaseComponentProps } from 'types'; + +export interface LoopIndexContextProps extends BaseComponentProps { + value: number; +} diff --git a/src/context/LoopIndex/useLoopIndex.tsx b/src/context/LoopIndex/useLoopIndex.tsx new file mode 100644 index 000000000..a9f3d6a77 --- /dev/null +++ b/src/context/LoopIndex/useLoopIndex.tsx @@ -0,0 +1,14 @@ +import { useContext } from 'react'; +import { LoopIndexContext } from './shared'; + +export const useLoopIndex = () => { + const context = useContext(LoopIndexContext); + + if (context === null) { + throw new Error( + 'useLoopIndex must be used within a LoopIndexContextProvider' + ); + } + + return context; +}; diff --git a/src/hooks/useDetailsNavigator.ts b/src/hooks/useDetailsNavigator.ts index 65ab69afe..dffc28cc4 100644 --- a/src/hooks/useDetailsNavigator.ts +++ b/src/hooks/useDetailsNavigator.ts @@ -11,8 +11,8 @@ function useDetailsNavigator(path: string) { const navigate = useNavigate(); const generatePath = useCallback( - (data: Data) => { - return getPathWithParams(path, { + (data: Data, pathOverride?: string) => { + return getPathWithParams(pathOverride ?? path, { [GlobalSearchParams.CATALOG_NAME]: data.catalog_name, }); }, @@ -20,9 +20,9 @@ function useDetailsNavigator(path: string) { ); const navigateToPath = useCallback( - (data: Data) => { + (data: Data, pathOverride?: string) => { navigate( - getPathWithParams(path, { + getPathWithParams(pathOverride ?? path, { [GlobalSearchParams.CATALOG_NAME]: data.catalog_name, }) ); diff --git a/src/hooks/useLiveSpecsExt.ts b/src/hooks/useLiveSpecsExt.ts index b0f56894d..0a3af6a45 100644 --- a/src/hooks/useLiveSpecsExt.ts +++ b/src/hooks/useLiveSpecsExt.ts @@ -1,5 +1,9 @@ import { useQuery } from '@supabase-cache-helpers/postgrest-swr'; import { PostgrestError } from '@supabase/postgrest-js'; +import { + liveSpecsExtRelatedQuery, + LiveSpecsExt_Related, +} from 'api/liveSpecsExt'; import { supabaseClient } from 'context/GlobalProviders'; import { useMemo } from 'react'; import { TABLES } from 'services/supabase'; @@ -110,21 +114,15 @@ export function useLiveSpecsExtWithOutSpec( return useLiveSpecsExt(draftId, specType, false); } -const liveSpecsExtRelatedColumns = ['catalog_name', 'reads_from', 'id']; -const liveSpecsExtRelatedQuery = liveSpecsExtRelatedColumns.join(','); -export interface LiveSpecsExt_Related { - catalog_name: string; - reads_from: string[]; - id: string; -} -export function useLiveSpecsExt_related(selected: string[]) { +export function useLiveSpecsExt_related(captureName: string) { const { data, error, isValidating } = useQuery( supabaseClient .from(TABLES.LIVE_SPECS_EXT) .select(liveSpecsExtRelatedQuery) .eq('spec_type', 'materialization') - .overlaps('reads_from', selected) + .or(`spec->>sourceCapture.eq.${captureName}`) .returns<LiveSpecsExt_Related[]>() + // getLiveSpecsRelatedToMaterialization(collectionName) ); return { diff --git a/src/lang/en-US/CommonMessages.ts b/src/lang/en-US/CommonMessages.ts index a9539d334..87b96a679 100644 --- a/src/lang/en-US/CommonMessages.ts +++ b/src/lang/en-US/CommonMessages.ts @@ -63,7 +63,9 @@ export const CommonMessages: Record<string, string> = { 'terms.documentation': `Docs`, 'terms.entity': `Entity`, 'terms.dataFlow': `Data Flow`, + 'terms.source': `Source`, 'terms.sources': `Sources`, + 'terms.destination': `Destination`, 'terms.destinations': `Destinations`, // Terms V2 @@ -72,6 +74,8 @@ export const CommonMessages: Record<string, string> = { // is just how react-intl handles it and we might end up rolling our own. 'terms.bindings.plural': `{count, plural, one {binding} other {bindings}}`, 'terms.collections.plural': `{count, plural, one {collection} other {collections}}`, + 'terms.destinations.plural': `{count, plural, one {destination} other {destinations}}`, + 'terms.sources.plural': `{count, plural, one {source} other {sources}}`, // Common fields 'entityPrefix.label': `Prefix`, diff --git a/src/lang/en-US/Workflows.ts b/src/lang/en-US/Workflows.ts index 59517f816..ba7a2888a 100644 --- a/src/lang/en-US/Workflows.ts +++ b/src/lang/en-US/Workflows.ts @@ -77,9 +77,9 @@ export const Workflows: Record<string, string> = { 'workflows.collectionSelector.manualBackfill.notSupported.title': `This {entityType} doesn’t support backfills.`, 'workflows.collectionSelector.manualBackfill.notSupported.message': `To backfill, disable each binding, save and then re-enable and save.`, 'workflows.collectionSelector.manualBackfill.message.capture': `Trigger a backfill of this collection from the source when published.`, - 'workflows.collectionSelector.manualBackfill.message.capture.allBindings': `Trigger a backfill of all collections from the source when published.`, + 'workflows.collectionSelector.manualBackfill.message.capture.allBindings': `Trigger a backfill of all collections from the source when published. The UI will mark all collections to be backfilled but the server will filter out those that cannot be backfilled (e.g. disabled collections).`, 'workflows.collectionSelector.manualBackfill.message.materialization': `Trigger a backfill from the source collection to its materialized resource when published.`, - 'workflows.collectionSelector.manualBackfill.message.materialization.allBindings': `Trigger a backfill from all source collections to their materialized resource when published.`, + 'workflows.collectionSelector.manualBackfill.message.materialization.allBindings': `Trigger a backfill from all source collections to their materialized resource when published. The UI will mark all collections to be backfilled but the server will filter out those that cannot be backfilled (e.g. disabled collections).`, 'workflows.collectionSelector.manualBackfill.cta.backfill': `Backfill`, 'workflows.collectionSelector.manualBackfill.count': `{backfillCount} of {bindingsTotal} {itemType} marked for backfill`, 'workflows.collectionSelector.manualBackfill.count.empty': `no {itemType} marked for backfill`, @@ -127,20 +127,37 @@ export const Workflows: Record<string, string> = { 'workflows.disable.message': `Control whether your {entityType} is disabled. This setting takes effect when your changes are published.`, 'workflows.disable.update.error': `Failed to update {entityType}. Please check your network connection and try again.`, + // PreSave prompts + 'preSavePrompt.changeReview.title': `How the spec is changing`, + 'preSavePrompt.publish.title': `Save and publish`, + 'preSavePrompt.logs.spinner.stopped': `done`, + 'preSavePrompt.logs.spinner.running': `loading...`, + + 'preSavePrompt.draftErrors.title': `Draft Errors`, + 'preSavePrompt.draftErrors.message': `There is an issue with the drafted version of your entity. Please contact support immediately.`, + + 'dataFlowReset.selectMaterialization.title': `Select materialization for data flow reset`, + 'dataFlowReset.reviewSelection.title': `Review your selections`, + 'dataFlowReset.disableCapture.title': `Disable capture`, + 'dataFlowReset.waitForCapture.title': `Wait for capture to fully stop`, + 'dataFlowReset.updateMaterialization.title': `Update Materialization`, + 'dataFlowReset.enableCapture.title': `Enable capture`, + + 'dataFlowReset.errors.publishFailed': `There was a build failure on the server.`, + // Dataflow reset 'workflows.collectionSelector.dataFlowBackfill.header': `Choose to backfill just your capture or the entire ${CommonMessages['terms.dataFlow']}.`, 'workflows.collectionSelector.dataFlowBackfill.option': `Backfill Capture`, 'workflows.collectionSelector.dataFlowBackfill.message': `Backfill capture and reset corresponding tables in materialization.`, - 'dataflowReset.warning.title': `This cannot be undone or stopped`, - 'dataflowReset.warning.message': `Once this process is started you must stay on this page. Do not click away or reload the page. If you have any issues please contact support immediately as we may need to assist you in recovery.`, - 'dataflowReset.step1.message': `The {entityCount} collections to be backfilled`, - - 'dataflowReset.editor.warning.title': `Editing disabled`, - 'dataflowReset.editor.warning.message': `While backfilling the ${CommonMessages['terms.dataFlow']} you cannot manually edit your spec.`, + 'dataFlowReset.reviewSelection.warning.title': `Once this process starts, you must stay on the page`, + 'dataFlowReset.reviewSelection.warning.message': `Do not navigate away or reload. If you have any issues, please contact {docLink}`, + 'dataFlowReset.reviewSelection.warning.message.docLink': `support@estuary.dev`, + 'dataFlowReset.reviewSelection.warning.message.docPath': `${CommonMessages['support.email']}`, + 'dataFlowReset.reviewSelection.instructions': `Please confirm you’d like to reset this data flow:`, - 'dataflowReset.logs.spinner.stopped': `done`, - 'dataflowReset.logs.spinner.running': `loading...`, + 'dataFlowReset.editor.warning.title': `Editing disabled`, + 'dataFlowReset.editor.warning.message': `While backfilling the ${CommonMessages['terms.dataFlow']} you cannot manually edit your spec.`, 'workflows.dataPlane.description': `Choose the data plane you would like to use.`, 'workflows.dataPlane.label': `Data Plane`, @@ -342,13 +359,15 @@ export const Workflows: Record<string, string> = { // Logs Dialog 'logs.default': ` `, 'logs.paused': `paused`, + 'logs.noLogs': `no logs were found`, 'logs.restartLink': `click here`, 'logs.tooManyEmpty': `Logs for this build may have ended. {restartCTA} to start waiting for new logs again.`, 'logs.networkFailure': `We encountered a problem streaming logs. Please check your network connection and {restartCTA} to start waiting for new logs again.`, // Reset Data Flow - 'resetDataFlow.materializations.header': `Select which materialization you want backfilled`, - 'resetDataFlow.materializations.empty': `No related materializations`, - 'resetDataFlow.materializations.selector.label': `Materialization to backfill`, - 'resetDataFlow.materializations.selector.helper': `Select one (1) materialization`, + 'resetDataFlow.materializations.header': `Below are ${CommonMessages['terms.sources']} that are linked to "{captureName}" via the Source Capture property.`, + 'resetDataFlow.materializations.empty.header': `No related ${CommonMessages['terms.sources']}`, + 'resetDataFlow.materializations.empty.message': `We currently only support doing a data flow backfill on Capture and Materializations that are linked through the Source Capture property on the Materialization.`, + 'resetDataFlow.materializations.selector.label': `${CommonMessages['terms.destination']} to backfill`, + 'resetDataFlow.materializations.selector.helper': `Select one (1) ${CommonMessages['terms.destination']}`, }; diff --git a/src/services/supabase.ts b/src/services/supabase.ts index 6266002de..694721609 100644 --- a/src/services/supabase.ts +++ b/src/services/supabase.ts @@ -311,18 +311,35 @@ export function invokeSupabase<T>(fn: FUNCTIONS, body: any) { ); } -export const insertSupabase = ( +export const insertSupabase = <T = any>( + table: TABLES, + data: any, + noResponse?: boolean +): PromiseLike<CallSupabaseResponse<T>> => { + return supabaseRetry(() => { + const query = supabaseClient + .from(table) + .insert(Array.isArray(data) ? data : [data]); + + if (!noResponse) { + return query.select(); + } + + return query; + }, 'insert').then(handleSuccess<T>, handleFailure); +}; + +export const insertSupabase_noResponse = <T = any>( table: TABLES, data: any -): PromiseLike<CallSupabaseResponse<any>> => { +): PromiseLike<CallSupabaseResponse<T>> => { return supabaseRetry( () => supabaseClient .from(table) - .insert(Array.isArray(data) ? data : [data]) - .select(), + .insert(Array.isArray(data) ? data : [data]), 'insert' - ).then(handleSuccess, handleFailure); + ).then(handleSuccess<T>, handleFailure); }; // Makes update calls. Mainly consumed in the src/api folder @@ -440,7 +457,12 @@ export const DEFAULT_POLLER_ERROR = { }, }; +export const JOB_TYPE_EMPTY = 'emptyDraft'; +export const JOB_TYPE_FAILURE = 'buildFailed'; +export const JOB_TYPE_SUCCESS = 'success'; + // These columns are not always what you want... but okay for a "default" constant -export const JOB_STATUS_SUCCESS = ['emptyDraft', 'success']; +export const JOB_STATUS_FAILURE = [JOB_TYPE_FAILURE]; +export const JOB_STATUS_SUCCESS = [JOB_TYPE_EMPTY, JOB_TYPE_SUCCESS]; export const JOB_STATUS_COLUMNS = `job_status, logs_token, id`; // END: Poller diff --git a/src/services/types.ts b/src/services/types.ts index 83849a250..112504a50 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -2,6 +2,7 @@ import { PostgrestResponse } from '@supabase/postgrest-js'; export enum CustomEvents { AUTH_SIGNOUT = 'Auth_Signout', + BACKFILL_DATAFLOW = 'Backfill_Dataflow', BINDINGS_EXPECTED_MISSING = 'Bindings_Expected_Missing', BINDINGS_RESOURCE_CONFIG_MISSING = 'Bindings_Resource_Config_Missing', CAPTURE_CREATE = 'Capture_Create', diff --git a/src/stores/Binding/Store.ts b/src/stores/Binding/Store.ts index fd425459d..0e2ac464a 100644 --- a/src/stores/Binding/Store.ts +++ b/src/stores/Binding/Store.ts @@ -418,6 +418,11 @@ const getInitialState = ( ({ meta }) => meta.collectionName ), + getEnabledCollections: () => + Object.values(get().resourceConfigs) + .filter(({ meta }) => !meta.disable) + .map(({ meta }) => meta.collectionName), + hydrateState: async ( editWorkflow, entityType, diff --git a/src/stores/Binding/hooks.ts b/src/stores/Binding/hooks.ts index 0f7e79bf0..cc6ca61d9 100644 --- a/src/stores/Binding/hooks.ts +++ b/src/stores/Binding/hooks.ts @@ -125,6 +125,11 @@ export const useBinding_collections = () => { export const useBinding_collections_count = () => useBindingStore(useShallow((state) => state.getCollections().length)); +export const useBinding_enabledCollections_count = () => + useBindingStore( + useShallow((state) => state.getEnabledCollections().length) + ); + export const useBinding_toggleDisable = () => { return useBindingStore((state) => state.toggleDisable); }; @@ -320,3 +325,13 @@ export const useBinding_backfillSupported = () => useBindingStore((state) => { return state.backfillSupported; }); + +export const useBinding_collectionsBeingBackfilled = () => + useBindingStore( + useShallow((state) => { + return state.backfilledBindings.map((backfilledBinding) => { + return state.resourceConfigs[backfilledBinding].meta + .collectionName; + }); + }) + ); diff --git a/src/stores/Binding/types.ts b/src/stores/Binding/types.ts index ae30fe154..4ffb902d5 100644 --- a/src/stores/Binding/types.ts +++ b/src/stores/Binding/types.ts @@ -143,6 +143,7 @@ export interface BindingState // Computed Values getCollections: () => string[]; + getEnabledCollections: () => string[]; // Misc. hydrateState: ( diff --git a/src/stores/DetailsForm/Store.ts b/src/stores/DetailsForm/Store.ts index b28f29d97..c9e92f86e 100644 --- a/src/stores/DetailsForm/Store.ts +++ b/src/stores/DetailsForm/Store.ts @@ -337,7 +337,6 @@ export const getInitialState = ( connector_image_tag, connector_tag_id, data_plane_id, - detail, } = data[0]; const connectorImage = await getConnectorImage( @@ -362,7 +361,6 @@ export const getInitialState = ( entityName: catalog_name, connectorImage, dataPlane, - description: detail ?? '', }, }; diff --git a/src/stores/DetailsForm/types.ts b/src/stores/DetailsForm/types.ts index c6fd0515d..9971d0fb1 100644 --- a/src/stores/DetailsForm/types.ts +++ b/src/stores/DetailsForm/types.ts @@ -28,7 +28,6 @@ export interface Details extends Pick<JsonFormsCore, 'data' | 'errors'> { }; entityName: string; dataPlane?: DataPlaneOption; - description?: string; }; } diff --git a/src/stores/FormState/Store.ts b/src/stores/FormState/Store.ts index c48474fde..b3c6748de 100644 --- a/src/stores/FormState/Store.ts +++ b/src/stores/FormState/Store.ts @@ -2,6 +2,7 @@ import produce from 'immer'; import { logRocketConsole } from 'services/shared'; import { CustomEvents } from 'services/types'; import { MessagePrefixes } from 'types'; +import { hasLength } from 'utils/misc-utils'; import { devtoolsOptions } from 'utils/store-utils'; import { create, StoreApi } from 'zustand'; import { devtools, NamedSet } from 'zustand/middleware'; @@ -17,7 +18,10 @@ const formActive = (status: FormStatus) => { status === FormStatus.UPDATING || // TODO (workflow stores) need to manage form state better // This is crappy - sorry. But if we have saved we want to disable everything and this is quickest way - status === FormStatus.SAVED + status === FormStatus.SAVED || + // This is like 'saved' but a bit different. With PreSavePrompt we need a way to make sure the user + // never is able to get back out of that ever + status === FormStatus.LOCKED ); }; @@ -87,8 +91,8 @@ const getInitialStateData = ( | 'isIdle' | 'isActive' | 'messagePrefix' - | 'showPreSavePrompt' | 'liveSpec' + | 'showSavePrompt' > => ({ formState: initialFormState, @@ -96,7 +100,8 @@ const getInitialStateData = ( isActive: false, liveSpec: null, - showPreSavePrompt: false, + + showSavePrompt: false, messagePrefix, }); @@ -140,6 +145,19 @@ const getInitialState = ( return; } + if ( + formState.status === FormStatus.LOCKED && + hasLength(newState.status) + ) { + // If we are here this means somehow the user is trying to take an action + // AFTER we have locked it and that should not happen ever. It does not matter + // what state it wants to go do - after being 'locked' it cannot go back. + logRocketConsole(CustomEvents.FORM_STATE_PREVENTED, { + type: 'locked', + }); + return; + } + state.formState = { ...formState, ...newState }; state.isIdle = formIdle(state.formState.status); @@ -178,23 +196,23 @@ const getInitialState = ( ); }, - setShowPreSavePrompt: (newVal) => { + setLiveSpec: (newVal) => { set( produce((state: EntityFormState) => { - state.showPreSavePrompt = newVal; + state.liveSpec = newVal; }), false, - 'Show Change Review Updated' + 'Live Spec Updated' ); }, - setLiveSpec: (newVal) => { + setShowSavePrompt: (newVal) => { set( produce((state: EntityFormState) => { - state.liveSpec = newVal; + state.showSavePrompt = newVal; }), false, - 'Live Spec Updated' + 'setShowSavePrompt' ); }, diff --git a/src/stores/FormState/hooks.ts b/src/stores/FormState/hooks.ts index 40cd08391..06c87a3b0 100644 --- a/src/stores/FormState/hooks.ts +++ b/src/stores/FormState/hooks.ts @@ -139,24 +139,6 @@ export const useFormStateStore_messagePrefix = () => { ); }; -export const useFormStateStore_showPreSavePrompt = () => { - const workflow = useEntityWorkflow(); - - return useZustandStore< - EntityFormState, - EntityFormState['showPreSavePrompt'] - >(storeName(workflow), (state) => state.showPreSavePrompt); -}; - -export const useFormStateStore_setShowPreSavePrompt = () => { - const workflow = useEntityWorkflow(); - - return useZustandStore< - EntityFormState, - EntityFormState['setShowPreSavePrompt'] - >(storeName(workflow), (state) => state.setShowPreSavePrompt); -}; - export const useFormStateStore_liveSpec = () => { const workflow = useEntityWorkflow(); @@ -174,3 +156,21 @@ export const useFormStateStore_setLiveSpec = () => { (state) => state.setLiveSpec ); }; + +export const useFormStateStore_showSavePrompt = () => { + const workflow = useEntityWorkflow(); + + return useZustandStore<EntityFormState, EntityFormState['showSavePrompt']>( + storeName(workflow), + (state) => state.showSavePrompt + ); +}; + +export const useFormStateStore_setShowSavePrompt = () => { + const workflow = useEntityWorkflow(); + + return useZustandStore< + EntityFormState, + EntityFormState['setShowSavePrompt'] + >(storeName(workflow), (state) => state.setShowSavePrompt); +}; diff --git a/src/stores/FormState/types.ts b/src/stores/FormState/types.ts index 04600783e..caed62d6d 100644 --- a/src/stores/FormState/types.ts +++ b/src/stores/FormState/types.ts @@ -39,6 +39,9 @@ export enum FormStatus { UPDATED = 'UPDATED', FAILED = 'FAILED', + + // USE WITH CAUTION - only for prompts right now (Q3 2024) + LOCKED = 'LOCKED', } export interface EntityFormState { @@ -50,12 +53,12 @@ export interface EntityFormState { isIdle: boolean; isActive: boolean; - showPreSavePrompt: boolean; - setShowPreSavePrompt: (data: EntityFormState['showPreSavePrompt']) => void; - liveSpec: Schema | null; setLiveSpec: (data: EntityFormState['liveSpec']) => void; + showSavePrompt: boolean; + setShowSavePrompt: (data: EntityFormState['showSavePrompt']) => void; + updateStatus: (status: FormStatus, background?: boolean) => void; // Misc. diff --git a/src/utils/entity-utils.ts b/src/utils/entity-utils.ts index 50fc32a18..b202b1740 100644 --- a/src/utils/entity-utils.ts +++ b/src/utils/entity-utils.ts @@ -1,6 +1,50 @@ +import { authenticatedRoutes } from 'app/routes'; +import { + semiTransparentBackground_blue, + semiTransparentBackground_purple, + semiTransparentBackground_teal, +} from 'context/Theme'; +import { CloudDownload, CloudUpload, DatabaseScript } from 'iconoir-react'; import produce from 'immer'; import { specContainsDerivation } from 'utils/misc-utils'; +// Eventually we'll probably move this out of here as it feels it is beyond the scope +// of "utils". Also, we'll probably end up nesting message keys together and stuff like that +// to keep it a bit easier to visual skim. +export const ENTITY_SETTINGS = { + collection: { + Icon: DatabaseScript, + background: semiTransparentBackground_blue, + pluralId: 'terms.collections.plural', + routes: { + details: authenticatedRoutes.collections.details.overview.fullPath, + viewAll: authenticatedRoutes.collections.fullPath, + }, + termId: 'terms.collections', + }, + capture: { + Icon: CloudUpload, + background: semiTransparentBackground_teal, + pluralId: 'terms.sources.plural', + routes: { + details: authenticatedRoutes.captures.details.overview.fullPath, + viewAll: authenticatedRoutes.captures.fullPath, + }, + termId: 'terms.sources', + }, + materialization: { + Icon: CloudDownload, + background: semiTransparentBackground_purple, + pluralId: 'terms.destinations.plural', + routes: { + details: + authenticatedRoutes.materializations.details.overview.fullPath, + viewAll: authenticatedRoutes.materializations.fullPath, + }, + termId: 'terms.destinations', + }, +}; + export const updateShardDisabled = (draftSpec: any, enabling: boolean) => { draftSpec.shards ??= {}; draftSpec.shards.disable = !enabling; diff --git a/src/utils/workflow-utils.ts b/src/utils/workflow-utils.ts index 9578ad852..e0daf46b1 100644 --- a/src/utils/workflow-utils.ts +++ b/src/utils/workflow-utils.ts @@ -38,6 +38,17 @@ export const getSourceOrTarget = (binding: any) => { : binding; }; +export const getBindingAsFullSource = (binding: any) => { + const response = getSourceOrTarget(binding); + if (typeof response === 'string') { + return { + name: response, + }; + } + + return getSourceOrTarget(binding); +}; + export const getCollectionNameProp = (entityType: Entity) => { return entityType === 'materialization' ? 'source' : 'target'; };