From a4c4b0285eda664a035e03b4ee9ebbf44da771a8 Mon Sep 17 00:00:00 2001 From: Travis Jenkins Date: Mon, 16 Sep 2024 12:48:49 -0400 Subject: [PATCH] Full entity refresh start (not live) (#1257) * adding back in the data flow reset stuff defaulting to true making sure we only show the review when there are backfills * Moving away from chip list to a drop down as that is more usable * Fixing some typing on auto completes Updating store with target value Controlling continue button * wiring up passing around settings cleaning up logging ordering props to make them a bit more readable * Making shared function for disabling * Creating a handler hook for processing update * Handling "empty" value * Moved the pop up to a stand alone dialog to keep things simpler. This new flow is complex enough trying to work it into existing code was just too much. Saving off the live spec we check if it exists to show review Starting to flesh out the data flow dialog Renaming in case we move away from a "modal" approach * moving buttons to the bottom * Language tweaks adding where logs will be * Adding fitwidth to alerts so we don't have to keep wrapping it * Using the fitWidth prop * Disabling the editor when backfill data flow is enabled Showing alert informing user why the editor is disabled * Do not want the dialog closing when clicking outside of it accidentally * Adding missing keywords * typing passing entire option to make life easier * This should close when selected as you can only select one * The default should be only on multiples and not the root setting * Okay so this is A LOT. But overall all this is doing is taking the work that was previously committed and breaking it up a little. The idea here is similar to how tables store columns. * Some renaming to remove `Step` from names * Commenting out work so this stuff can be merged now * Removing the onClick stuff from the chips * Commenting out alert * cleaning up logging * don't need to save right now * noting work * Adding error handling * notes on the data * PR: cleaning up spread that is no longer needed * pr: typos * PR: typing * PR: typing * Removing needless type --- src/app/guards/EntityExistenceGuard.tsx | 6 ++ .../Backfill/BackfillDataFlowOption.tsx | 3 +- .../editor/Bindings/Backfill/index.tsx | 9 +- src/components/shared/AlertBox.tsx | 25 ++---- .../shared/AutoComplete/DefaultProps.tsx | 18 +++- src/components/shared/Entity/Actions/Save.tsx | 47 +++++------ .../shared/Entity/CatalogEditor.tsx | 21 +++++ .../DataflowResetModal/BindingReview.tsx | 19 ----- .../shared/Entity/DataflowResetModal/types.ts | 3 - .../shared/Entity/DetailsForm/Form.tsx | 4 +- src/components/shared/Entity/Edit/index.tsx | 3 + .../Entity/hooks/useDataFlowResetHandler.tsx | 40 +++++++++ .../Entity/hooks/useDataFlowResetPrompt.tsx | 45 ---------- .../shared/Entity/prompts/PreSave/Actions.tsx | 22 +++++ .../shared/Entity/prompts/PreSave/Content.tsx | 29 +++++++ .../shared/Entity/prompts/PreSave/Title.tsx | 41 ++++++++++ .../shared/Entity/prompts/PreSave/index.tsx | 26 ++++++ .../dataFlowReset/DisableCapture/index.tsx | 26 ++++++ .../dataFlowReset/EnableCapture/index.tsx | 15 ++++ .../SelectMaterialization/BindingReview.tsx | 47 +++++++++++ .../Materializations.tsx} | 42 +++++----- .../SelectMaterialization/Selector.tsx | 82 +++++++++++++++++++ .../SelectMaterialization/SelectorOption.tsx | 20 +++++ .../SelectMaterialization/index.tsx | 18 ++++ .../SelectMaterialization/types.ts | 20 +++++ .../UpdateMaterialization/index.tsx | 17 ++++ .../WaitForCaptureStop/index.tsx | 18 ++++ .../prompts/steps/dataFlowReset/shared.ts | 14 ++++ .../steps/preSave/ChangeReview/DiffViewer.tsx | 32 ++++++++ .../steps/preSave/ChangeReview/index.tsx | 18 ++++ .../prompts/steps/preSave/Done/index.tsx | 17 ++++ .../prompts/steps/preSave/Publish/index.tsx | 15 ++++ .../steps/preSave/usePreSavePromptSteps.tsx | 56 +++++++++++++ src/components/shared/ErrorBoundryWrapper.tsx | 9 +- src/components/shared/types.ts | 13 +++ .../RowActions/DisableEnable/Button.tsx | 46 +---------- src/context/Confirmation/index.tsx | 2 +- src/hooks/useLiveSpecsExt.ts | 24 ++++++ src/lang/en-US/Workflows.ts | 21 ++++- src/services/ajv.ts | 2 + src/stores/Binding/Store.ts | 16 +++- src/stores/Binding/types.ts | 5 ++ src/stores/FormState/Store.ts | 30 ++++++- src/stores/FormState/hooks.ts | 36 ++++++++ src/stores/FormState/types.ts | 8 +- src/utils/entity-utils.ts | 35 ++++++++ 46 files changed, 862 insertions(+), 203 deletions(-) delete mode 100644 src/components/shared/Entity/DataflowResetModal/BindingReview.tsx delete mode 100644 src/components/shared/Entity/DataflowResetModal/types.ts create mode 100644 src/components/shared/Entity/hooks/useDataFlowResetHandler.tsx delete mode 100644 src/components/shared/Entity/hooks/useDataFlowResetPrompt.tsx create mode 100644 src/components/shared/Entity/prompts/PreSave/Actions.tsx create mode 100644 src/components/shared/Entity/prompts/PreSave/Content.tsx create mode 100644 src/components/shared/Entity/prompts/PreSave/Title.tsx create mode 100644 src/components/shared/Entity/prompts/PreSave/index.tsx create mode 100644 src/components/shared/Entity/prompts/steps/dataFlowReset/DisableCapture/index.tsx create mode 100644 src/components/shared/Entity/prompts/steps/dataFlowReset/EnableCapture/index.tsx create mode 100644 src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/BindingReview.tsx rename src/components/shared/Entity/{DataflowResetModal/RelatedMaterializations.tsx => prompts/steps/dataFlowReset/SelectMaterialization/Materializations.tsx} (52%) create mode 100644 src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/Selector.tsx create mode 100644 src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/SelectorOption.tsx create mode 100644 src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/index.tsx create mode 100644 src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/types.ts create mode 100644 src/components/shared/Entity/prompts/steps/dataFlowReset/UpdateMaterialization/index.tsx create mode 100644 src/components/shared/Entity/prompts/steps/dataFlowReset/WaitForCaptureStop/index.tsx create mode 100644 src/components/shared/Entity/prompts/steps/dataFlowReset/shared.ts create mode 100644 src/components/shared/Entity/prompts/steps/preSave/ChangeReview/DiffViewer.tsx create mode 100644 src/components/shared/Entity/prompts/steps/preSave/ChangeReview/index.tsx create mode 100644 src/components/shared/Entity/prompts/steps/preSave/Done/index.tsx create mode 100644 src/components/shared/Entity/prompts/steps/preSave/Publish/index.tsx create mode 100644 src/components/shared/Entity/prompts/steps/preSave/usePreSavePromptSteps.tsx create mode 100644 src/components/shared/types.ts create mode 100644 src/utils/entity-utils.ts diff --git a/src/app/guards/EntityExistenceGuard.tsx b/src/app/guards/EntityExistenceGuard.tsx index 16f64584c..fc80f8be5 100644 --- a/src/app/guards/EntityExistenceGuard.tsx +++ b/src/app/guards/EntityExistenceGuard.tsx @@ -12,6 +12,9 @@ function EntityExistenceGuard({ children }: BaseComponentProps) { const entityType = useEntityType(); + // TODO (data flow reset) + // const setLiveSpec = useFormStateStore_setLiveSpec(); + const { liveSpecs, isValidating: checkingEntityExistence } = useLiveSpecsExtWithSpec(liveSpecId, entityType); @@ -20,6 +23,9 @@ function EntityExistenceGuard({ children }: BaseComponentProps) { } else if (liveSpecs.length === 0) { return ; } else { + // TODO (data flow reset) + // setLiveSpec(liveSpecs[0].spec); + // eslint-disable-next-line react/jsx-no-useless-fragment return <>{children}; } diff --git a/src/components/editor/Bindings/Backfill/BackfillDataFlowOption.tsx b/src/components/editor/Bindings/Backfill/BackfillDataFlowOption.tsx index b1e5ead36..ea53730ff 100644 --- a/src/components/editor/Bindings/Backfill/BackfillDataFlowOption.tsx +++ b/src/components/editor/Bindings/Backfill/BackfillDataFlowOption.tsx @@ -28,8 +28,9 @@ function BackfillDataFlowOption({ disabled }: BackfillDataflowOptionProps) { } return ( - + - {/* // TODO (reset dataflow) - {bindingIndex === -1 && workflow === 'capture_edit' ? ( + {/*TODO (data flow reset)*/} + {/* {bindingIndex === -1 && workflow === 'capture_edit' ? ( - ) : null} - */} + ) : null}*/} ); } diff --git a/src/components/shared/AlertBox.tsx b/src/components/shared/AlertBox.tsx index e1a87a10e..03b5c1473 100644 --- a/src/components/shared/AlertBox.tsx +++ b/src/components/shared/AlertBox.tsx @@ -1,10 +1,4 @@ -import { - Alert, - AlertColor, - AlertTitle, - Typography, - useTheme, -} from '@mui/material'; +import { Alert, AlertTitle, Typography, useTheme } from '@mui/material'; import { alertBackground, alertTextPrimary } from 'context/Theme'; import { CheckCircle, @@ -12,17 +6,9 @@ import { WarningCircle, XmarkCircle, } from 'iconoir-react'; -import { forwardRef, ReactNode, useMemo } from 'react'; +import { forwardRef, useMemo } from 'react'; import { FormattedMessage } from 'react-intl'; -import { BaseComponentProps } from 'types'; - -interface Props extends BaseComponentProps { - severity: AlertColor; - short?: boolean; - hideIcon?: boolean; - title?: string | ReactNode; - onClose?: () => void; -} +import { AlertBoxProps } from './types'; const SHARED_STYLING = { borderRadius: 2, @@ -37,8 +23,8 @@ const HEADER_MESSAGE = { error: 'alert.error', }; -const AlertBox = forwardRef(function NavLinkRef( - { short, severity, hideIcon, title, children, onClose }, +const AlertBox = forwardRef(function NavLinkRef( + { short, severity, hideIcon, title, children, onClose, fitWidth }, ref ) { const theme = useTheme(); @@ -92,6 +78,7 @@ const AlertBox = forwardRef(function NavLinkRef( 'color': alertTextPrimary[theme.palette.mode], 'borderColor': theme.palette[severity][theme.palette.mode], 'padding': 0, + 'maxWidth': fitWidth ? 'fit-content' : undefined, 'pl': hideIcon ? 2 : undefined, '& > .MuiAlert-message': { p: 1, diff --git a/src/components/shared/AutoComplete/DefaultProps.tsx b/src/components/shared/AutoComplete/DefaultProps.tsx index de384b0af..97534ee30 100644 --- a/src/components/shared/AutoComplete/DefaultProps.tsx +++ b/src/components/shared/AutoComplete/DefaultProps.tsx @@ -13,18 +13,16 @@ const PopperComponent = styled(Popper)({ }, }) as any; -export const autoCompleteDefaults_Virtual_Multiple: AutocompleteProps< +export const autoCompleteDefaults_Virtual: AutocompleteProps< + any, any, - true, false, false, 'div' > = { ListboxComponent, PopperComponent, - blurOnSelect: false, disableCloseOnSelect: true, - multiple: true, options: [], // You MUST provide these yourself size: 'small', renderInput: () => null, // You MUST provide these yourself @@ -33,3 +31,15 @@ export const autoCompleteDefaults_Virtual_Multiple: AutocompleteProps< return [props, option, state.selected] as React.ReactNode; }, }; + +export const autoCompleteDefaults_Virtual_Multiple: AutocompleteProps< + any, + true, + false, + false, + 'div' +> = { + ...autoCompleteDefaults_Virtual, + multiple: true, + blurOnSelect: false, +}; diff --git a/src/components/shared/Entity/Actions/Save.tsx b/src/components/shared/Entity/Actions/Save.tsx index 50f18008c..923f5c80d 100644 --- a/src/components/shared/Entity/Actions/Save.tsx +++ b/src/components/shared/Entity/Actions/Save.tsx @@ -4,7 +4,7 @@ import { useEditorStore_isSaving, } from 'components/editor/Store/hooks'; import { buttonSx } from 'components/shared/Entity/Header'; -import { FormattedMessage } from 'react-intl'; +import { useIntl } from 'react-intl'; import { useFormStateStore_isActive } from 'stores/FormState/hooks'; import { EntityCreateSaveButtonProps } from './types'; @@ -18,44 +18,41 @@ function EntityCreateSave({ logEvent, onFailure, }: EntityCreateSaveButtonProps) { + const intl = useIntl(); + const save = useSave(logEvent, onFailure, dryRun); const isSaving = useEditorStore_isSaving(); const formActive = useFormStateStore_isActive(); const draftId = useEditorStore_id(); - // TODO (reset dataflow) - // const showDataFlowResetPrompt = useDataFlowResetPrompt(); - // const [backfillDataflow] = useBindingStore((state) => [ - // state.backfillDataFlow, - // ]); - - const labelId = buttonLabelId - ? buttonLabelId - : dryRun === true - ? `cta.testConfig${loading ? '.active' : ''}` - : `cta.saveEntity${loading ? '.active' : ''}`; + // TODO (data flow reset) + // const setShowPreSavePrompt = useFormStateStore_setShowPreSavePrompt(); + // const backfillDataflow = useBindingStore((state) => state.backfillDataFlow); + // const needsBackfilled = useBinding_backfilledBindings_count(); return ( ); } diff --git a/src/components/shared/Entity/CatalogEditor.tsx b/src/components/shared/Entity/CatalogEditor.tsx index 506c0c787..42b88bd2c 100644 --- a/src/components/shared/Entity/CatalogEditor.tsx +++ b/src/components/shared/Entity/CatalogEditor.tsx @@ -20,6 +20,11 @@ function CatalogEditor({ messageId }: Props) { const formStatus = useFormStateStore_status(); const formActive = useFormStateStore_isActive(); + // TODO (data flow reset) + // const intl = useIntl(); + // const backfillDataflow = useBindingStore((state) => state.backfillDataFlow); + // const needsBackfilled = useBinding_backfilledBindings_count(); + if (draftId && formStatus !== FormStatus.INIT) { return ( + {/*TODO (data flow reset) - also make sure editor is disabled*/} + {/* {backfillDataFlow && needsBackfilled ? ( + + {intl.formatMessage({ + id: 'dataflowReset.editor.warning.message', + })} + + ) : null}*/} + - - {selected.length} Collections that will be backfilled - - - - - - ); -} - -export default BindingReview; diff --git a/src/components/shared/Entity/DataflowResetModal/types.ts b/src/components/shared/Entity/DataflowResetModal/types.ts deleted file mode 100644 index 3e25d8d9a..000000000 --- a/src/components/shared/Entity/DataflowResetModal/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface BindingReviewProps { - selected: string[]; -} diff --git a/src/components/shared/Entity/DetailsForm/Form.tsx b/src/components/shared/Entity/DetailsForm/Form.tsx index 23f6dd0be..2e27ea9d9 100644 --- a/src/components/shared/Entity/DetailsForm/Form.tsx +++ b/src/components/shared/Entity/DetailsForm/Form.tsx @@ -231,8 +231,8 @@ function DetailsFormForm({ connectorTags, entityType, readOnly }: Props) { return ( <> {readOnly ? ( - - + + {intl.formatMessage({ id: 'entityEdit.alert.detailsFormDisabled', })} diff --git a/src/components/shared/Entity/Edit/index.tsx b/src/components/shared/Entity/Edit/index.tsx index dff717b71..fea1d3fc5 100644 --- a/src/components/shared/Entity/Edit/index.tsx +++ b/src/components/shared/Entity/Edit/index.tsx @@ -212,6 +212,9 @@ function EntityEdit({ + + {/*TODO (data flow reset)*/} + {/**/} )} diff --git a/src/components/shared/Entity/hooks/useDataFlowResetHandler.tsx b/src/components/shared/Entity/hooks/useDataFlowResetHandler.tsx new file mode 100644 index 000000000..d8c24cfbc --- /dev/null +++ b/src/components/shared/Entity/hooks/useDataFlowResetHandler.tsx @@ -0,0 +1,40 @@ +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/useDataFlowResetPrompt.tsx b/src/components/shared/Entity/hooks/useDataFlowResetPrompt.tsx deleted file mode 100644 index 533891919..000000000 --- a/src/components/shared/Entity/hooks/useDataFlowResetPrompt.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { useConfirmationModalContext } from 'context/Confirmation'; -import { useCallback } from 'react'; -import { useBindingStore } from 'stores/Binding/Store'; -import { useShallow } from 'zustand/react/shallow'; -import BindingReview from '../DataflowResetModal/BindingReview'; - -function useDataFlowResetPrompt() { - const confirmationModalContext = useConfirmationModalContext(); - - const collectionsBeingBackfilled = useBindingStore( - useShallow((state) => { - return state.backfilledBindings.map((backfilledBinding) => { - return state.resourceConfigs[backfilledBinding].meta - .collectionName; - }); - }) - ); - - return useCallback( - (callback: (data: any) => void) => { - confirmationModalContext - ?.showConfirmation( - { - message: ( - - ), - title: 'workflows.save.review.header', - }, - false - ) - .then((data) => { - console.log('then', data); - callback(data); - }) - .catch((err) => { - console.log('catch', err); - }); - }, - [collectionsBeingBackfilled, confirmationModalContext] - ); -} - -export default useDataFlowResetPrompt; diff --git a/src/components/shared/Entity/prompts/PreSave/Actions.tsx b/src/components/shared/Entity/prompts/PreSave/Actions.tsx new file mode 100644 index 000000000..779d768b1 --- /dev/null +++ b/src/components/shared/Entity/prompts/PreSave/Actions.tsx @@ -0,0 +1,22 @@ +import { Button, DialogActions, Stack } from '@mui/material'; +import usePreSavePromptSteps from '../steps/preSave/usePreSavePromptSteps'; + +function Actions() { + const { handleBack, handleNext } = usePreSavePromptSteps(); + + return ( + + + + + + + + ); +} + +export default Actions; diff --git a/src/components/shared/Entity/prompts/PreSave/Content.tsx b/src/components/shared/Entity/prompts/PreSave/Content.tsx new file mode 100644 index 000000000..e8d0607b1 --- /dev/null +++ b/src/components/shared/Entity/prompts/PreSave/Content.tsx @@ -0,0 +1,29 @@ +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/Title.tsx b/src/components/shared/Entity/prompts/PreSave/Title.tsx new file mode 100644 index 000000000..b81be8dff --- /dev/null +++ b/src/components/shared/Entity/prompts/PreSave/Title.tsx @@ -0,0 +1,41 @@ +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'; + +function Title() { + const intl = useIntl(); + const theme = useTheme(); + + const { activeStep, setActiveStep } = usePreSavePromptSteps(); + const setShowPreSavePrompt = useFormStateStore_setShowPreSavePrompt(); + + const closeDialog = () => { + setActiveStep(0); + setShowPreSavePrompt(false); + }; + + return ( + + Please review your changes + 2} onClick={closeDialog}> + + + + ); +} + +export default Title; diff --git a/src/components/shared/Entity/prompts/PreSave/index.tsx b/src/components/shared/Entity/prompts/PreSave/index.tsx new file mode 100644 index 000000000..8275b582e --- /dev/null +++ b/src/components/shared/Entity/prompts/PreSave/index.tsx @@ -0,0 +1,26 @@ +import { Dialog } from '@mui/material'; +import { useFormStateStore_showPreSavePrompt } from 'stores/FormState/hooks'; +import usePreSavePromptSteps from '../steps/preSave/usePreSavePromptSteps'; +import Actions from './Actions'; +import Content from './Content'; +import Title from './Title'; + +function PreSave() { + const { activeStep } = usePreSavePromptSteps(); + + const showPreSavePrompt = useFormStateStore_showPreSavePrompt(); + + return ( + + + <Content /> + <Actions /> + </Dialog> + ); +} + +export default PreSave; diff --git a/src/components/shared/Entity/prompts/steps/dataFlowReset/DisableCapture/index.tsx b/src/components/shared/Entity/prompts/steps/dataFlowReset/DisableCapture/index.tsx new file mode 100644 index 000000000..b85016ce2 --- /dev/null +++ b/src/components/shared/Entity/prompts/steps/dataFlowReset/DisableCapture/index.tsx @@ -0,0 +1,26 @@ +import { StepContent, StepLabel } from '@mui/material'; +import ErrorBoundryWrapper from 'components/shared/ErrorBoundryWrapper'; +import Logs from 'components/logs'; + +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> + </> + ); +} + +export default DisableCapture; diff --git a/src/components/shared/Entity/prompts/steps/dataFlowReset/EnableCapture/index.tsx b/src/components/shared/Entity/prompts/steps/dataFlowReset/EnableCapture/index.tsx new file mode 100644 index 000000000..6678a5797 --- /dev/null +++ b/src/components/shared/Entity/prompts/steps/dataFlowReset/EnableCapture/index.tsx @@ -0,0 +1,15 @@ +import { StepContent, StepLabel } from '@mui/material'; +import ErrorBoundryWrapper from 'components/shared/ErrorBoundryWrapper'; + +function EnableCapture() { + return ( + <> + <StepLabel>Enable capture</StepLabel> + <StepContent> + <ErrorBoundryWrapper>Logs</ErrorBoundryWrapper> + </StepContent> + </> + ); +} + +export default EnableCapture; diff --git a/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/BindingReview.tsx b/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/BindingReview.tsx new file mode 100644 index 000000000..7275a9aa3 --- /dev/null +++ b/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/BindingReview.tsx @@ -0,0 +1,47 @@ +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/DataflowResetModal/RelatedMaterializations.tsx b/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/Materializations.tsx similarity index 52% rename from src/components/shared/Entity/DataflowResetModal/RelatedMaterializations.tsx rename to src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/Materializations.tsx index 2d976f600..a77b4d8fa 100644 --- a/src/components/shared/Entity/DataflowResetModal/RelatedMaterializations.tsx +++ b/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/Materializations.tsx @@ -1,36 +1,35 @@ import { Box, LinearProgress, Typography } from '@mui/material'; -import { useQuery } from '@supabase-cache-helpers/postgrest-swr'; -import ChipList from 'components/shared/ChipList'; import Error from 'components/shared/Error'; import { useConfirmationModalContext } from 'context/Confirmation'; -import { supabaseClient } from 'context/GlobalProviders'; +import { useLiveSpecsExt_related } from 'hooks/useLiveSpecsExt'; import { useEffect, useMemo } from 'react'; import { useIntl } from 'react-intl'; -import { TABLES } from 'services/supabase'; +import { useBindingStore } from 'stores/Binding/Store'; import { hasLength } from 'utils/misc-utils'; +import Selector from './Selector'; import { BindingReviewProps } from './types'; -function RelatedMaterializations({ selected }: BindingReviewProps) { +function Materializations({ selected }: BindingReviewProps) { const intl = useIntl(); - const { data, error, isValidating } = useQuery( - supabaseClient - .from(TABLES.LIVE_SPECS_EXT) - .select('catalog_name') - .eq('spec_type', 'materialization') - .overlaps('reads_from', selected), - {} - ); + const { related, error, isValidating } = useLiveSpecsExt_related(selected); const confirmationModal = useConfirmationModalContext(); - const foundData = useMemo(() => hasLength(data), [data]); + const foundData = useMemo(() => hasLength(related), [related]); + + const [backfillDataFlowTarget] = useBindingStore((state) => [ + state.backfillDataFlowTarget, + ]); useEffect(() => { - if (!isValidating && !foundData) { - confirmationModal?.setContinueAllowed(true); - } - }, [foundData, isValidating, confirmationModal]); + // 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> @@ -45,10 +44,7 @@ function RelatedMaterializations({ selected }: BindingReviewProps) { {error ? <Error error={error} condensed /> : null} {!error && foundData ? ( - <ChipList - values={data ? data.map((datum) => datum.catalog_name) : []} - maxChips={10} - /> + <Selector keys={related} value={null} /> ) : ( <Box> {intl.formatMessage({ @@ -60,4 +56,4 @@ function RelatedMaterializations({ selected }: BindingReviewProps) { ); } -export default RelatedMaterializations; +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 new file mode 100644 index 000000000..298da3f8b --- /dev/null +++ b/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/Selector.tsx @@ -0,0 +1,82 @@ +import { Autocomplete, Grid, TextField } from '@mui/material'; +import { autoCompleteDefaults_Virtual } from 'components/shared/AutoComplete/DefaultProps'; +import { ReactNode, useState } from 'react'; +import { useIntl } from 'react-intl'; +import { useBindingStore } from 'stores/Binding/Store'; +import { RelatedMaterializationSelectorProps } from './types'; +import SelectorOption from './SelectorOption'; + +const getValue = (option: any) => + typeof option === 'string' ? option : option?.catalog_name; + +function Selector({ disabled, keys }: RelatedMaterializationSelectorProps) { + const intl = useIntl(); + + const [inputValue, setInputValue] = useState(''); + + const [backfillDataFlowTarget, setBackfillDataFlowTarget] = useBindingStore( + (state) => [ + state.backfillDataFlowTarget, + state.setBackfillDataFlowTarget, + ] + ); + + if (keys.length === 0) { + return null; + } + + return ( + <Grid item xs={12}> + <Autocomplete + {...autoCompleteDefaults_Virtual} + disabled={disabled} + getOptionLabel={getValue} + inputValue={inputValue} + isOptionEqualToValue={(option, optionValue) => { + return option.catalog_name === optionValue; + }} + options={keys} + value={backfillDataFlowTarget} + onChange={(_event, newValue) => { + setBackfillDataFlowTarget( + newValue ? newValue.catalog_name : null + ); + }} + onInputChange={(_event, newInputValue) => { + setInputValue(newInputValue); + }} + renderInput={(params) => { + return ( + <TextField + {...params} + disabled={disabled} + helperText={intl.formatMessage({ + id: 'resetDataFlow.materializations.selector.helper', + })} + label={intl.formatMessage({ + id: 'resetDataFlow.materializations.selector.label', + })} + variant="standard" + /> + ); + }} + renderOption={(renderOptionProps, option, state) => { + const RowContent = ( + <SelectorOption + option={option} + x-react-window-item-height={75} + /> + ); + + return [ + renderOptionProps, + RowContent, + state.selected, + ] as ReactNode; + }} + /> + </Grid> + ); +} + +export default Selector; diff --git a/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/SelectorOption.tsx b/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/SelectorOption.tsx new file mode 100644 index 000000000..c1995e4d9 --- /dev/null +++ b/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/SelectorOption.tsx @@ -0,0 +1,20 @@ +import { Stack, Typography } from '@mui/material'; +import ChipList from 'components/shared/ChipList'; +import { truncateTextSx } from 'context/Theme'; +import { MaterializationSelectorOptionProps } from './types'; + +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> + + <ChipList values={reads_from} maxChips={3} /> + </Stack> + ); +} + +export default SelectorOption; diff --git a/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/index.tsx b/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/index.tsx new file mode 100644 index 000000000..8e69e0e99 --- /dev/null +++ b/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/index.tsx @@ -0,0 +1,18 @@ +import { StepContent, StepLabel } from '@mui/material'; +import ErrorBoundryWrapper from 'components/shared/ErrorBoundryWrapper'; +import BindingReview from './BindingReview'; + +function SelectMaterialization() { + return ( + <> + <StepLabel>Select materialization for data flow reset</StepLabel> + <StepContent> + <ErrorBoundryWrapper> + <BindingReview /> + </ErrorBoundryWrapper> + </StepContent> + </> + ); +} + +export default SelectMaterialization; diff --git a/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/types.ts b/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/types.ts new file mode 100644 index 000000000..b2e63da83 --- /dev/null +++ b/src/components/shared/Entity/prompts/steps/dataFlowReset/SelectMaterialization/types.ts @@ -0,0 +1,20 @@ +import { LiveSpecsExt_Related } from 'hooks/useLiveSpecsExt'; + +export interface BindingReviewProps { + selected: string[]; +} + +export interface RelatedMaterializationSelectorProps { + keys: LiveSpecsExt_Related[]; + value: string | null; + disabled?: boolean; + onChange?: ( + event: any, + newValue: string[], + reason: string + ) => PromiseLike<any>; +} + +export interface MaterializationSelectorOptionProps { + option: LiveSpecsExt_Related; +} diff --git a/src/components/shared/Entity/prompts/steps/dataFlowReset/UpdateMaterialization/index.tsx b/src/components/shared/Entity/prompts/steps/dataFlowReset/UpdateMaterialization/index.tsx new file mode 100644 index 000000000..efe8f9739 --- /dev/null +++ b/src/components/shared/Entity/prompts/steps/dataFlowReset/UpdateMaterialization/index.tsx @@ -0,0 +1,17 @@ +import { StepContent, StepLabel } from '@mui/material'; +import ErrorBoundryWrapper from 'components/shared/ErrorBoundryWrapper'; + +function MarkMaterialization() { + return ( + <> + <StepLabel>Mark materialization notBefore</StepLabel> + <StepContent> + <ErrorBoundryWrapper> + describe what we're doing + </ErrorBoundryWrapper> + </StepContent> + </> + ); +} + +export default MarkMaterialization; diff --git a/src/components/shared/Entity/prompts/steps/dataFlowReset/WaitForCaptureStop/index.tsx b/src/components/shared/Entity/prompts/steps/dataFlowReset/WaitForCaptureStop/index.tsx new file mode 100644 index 000000000..fcb43169e --- /dev/null +++ b/src/components/shared/Entity/prompts/steps/dataFlowReset/WaitForCaptureStop/index.tsx @@ -0,0 +1,18 @@ +import { LinearProgress, StepContent, StepLabel } from '@mui/material'; +import ErrorBoundryWrapper from 'components/shared/ErrorBoundryWrapper'; + +function WaitForCaptureStop() { + return ( + <> + <StepLabel>Wait for capture data to stop</StepLabel> + <StepContent> + <ErrorBoundryWrapper> + <LinearProgress /> + No logs... but show something + </ErrorBoundryWrapper> + </StepContent> + </> + ); +} + +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 new file mode 100644 index 000000000..7fdf66b9e --- /dev/null +++ b/src/components/shared/Entity/prompts/steps/dataFlowReset/shared.ts @@ -0,0 +1,14 @@ +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'; + +export const DataFlowResetSteps: (() => ReactJSXElement)[] = [ + SelectMaterialization, + DisableCapture, + WaitForCaptureStop, + MarkMaterialization, + EnableCapture, +]; diff --git a/src/components/shared/Entity/prompts/steps/preSave/ChangeReview/DiffViewer.tsx b/src/components/shared/Entity/prompts/steps/preSave/ChangeReview/DiffViewer.tsx new file mode 100644 index 000000000..c4c25abd7 --- /dev/null +++ b/src/components/shared/Entity/prompts/steps/preSave/ChangeReview/DiffViewer.tsx @@ -0,0 +1,32 @@ +import { DiffEditor } from '@monaco-editor/react'; +import { useTheme } from '@mui/material'; +import { monacoEditorComponentBackground } from 'context/Theme'; + +import useDraftSpecEditor from 'hooks/useDraftSpecEditor'; +import { stringifyJSON } from 'services/stringify'; +import { useDetailsFormStore } from 'stores/DetailsForm/Store'; +import { useFormStateStore_liveSpec } from 'stores/FormState/hooks'; + +const HEIGHT = 400; + +function DiffViewer() { + const theme = useTheme(); + + const entityName = useDetailsFormStore((state) => state.draftedEntityName); + const { defaultValue: draftSpec, isValidating } = + useDraftSpecEditor(entityName); + + const liveSpec = useFormStateStore_liveSpec(); + + return ( + <DiffEditor + height={`${HEIGHT}px`} + original={liveSpec ? stringifyJSON(liveSpec) : 'loading...'} + modified={isValidating ? 'loading...' : draftSpec} + theme={monacoEditorComponentBackground[theme.palette.mode]} + options={{ readOnly: true }} + /> + ); +} + +export default DiffViewer; diff --git a/src/components/shared/Entity/prompts/steps/preSave/ChangeReview/index.tsx b/src/components/shared/Entity/prompts/steps/preSave/ChangeReview/index.tsx new file mode 100644 index 000000000..d9d6e9346 --- /dev/null +++ b/src/components/shared/Entity/prompts/steps/preSave/ChangeReview/index.tsx @@ -0,0 +1,18 @@ +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> + </> + ); +} + +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 new file mode 100644 index 000000000..a458f6a87 --- /dev/null +++ b/src/components/shared/Entity/prompts/steps/preSave/Done/index.tsx @@ -0,0 +1,17 @@ +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/index.tsx b/src/components/shared/Entity/prompts/steps/preSave/Publish/index.tsx new file mode 100644 index 000000000..12b11fbfa --- /dev/null +++ b/src/components/shared/Entity/prompts/steps/preSave/Publish/index.tsx @@ -0,0 +1,15 @@ +import { StepContent, StepLabel } from '@mui/material'; +import ErrorBoundryWrapper from 'components/shared/ErrorBoundryWrapper'; + +function Publish() { + return ( + <> + <StepLabel>Publishing</StepLabel> + <StepContent> + <ErrorBoundryWrapper>Logs</ErrorBoundryWrapper> + </StepContent> + </> + ); +} + +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 new file mode 100644 index 000000000..82d6ceb57 --- /dev/null +++ b/src/components/shared/Entity/prompts/steps/preSave/usePreSavePromptSteps.tsx @@ -0,0 +1,56 @@ +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/ErrorBoundryWrapper.tsx b/src/components/shared/ErrorBoundryWrapper.tsx index 0d7e416f7..858680172 100644 --- a/src/components/shared/ErrorBoundryWrapper.tsx +++ b/src/components/shared/ErrorBoundryWrapper.tsx @@ -1,16 +1,13 @@ import { Collapse, Divider, IconButton, Paper, useTheme } from '@mui/material'; import { NavArrowDown } from 'iconoir-react'; -import { ReactNode, useState } from 'react'; +import { useState } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; import { FormattedMessage, useIntl } from 'react-intl'; import { logRocketEvent } from 'services/shared'; import { CustomEvents } from 'services/types'; +import { BaseComponentProps } from 'types'; import AlertBox from './AlertBox'; -interface Props { - children: ReactNode; -} - const logErrorToLogRocket = (error: Error) => { // Let LogRocket know this was rendered since there is almost never // a time where it is good that this was shown to a user. @@ -71,7 +68,7 @@ function ErrorFallback({ error }: { error: Error }): JSX.Element { ); } -const ErrorBoundryWrapper = ({ children }: Props) => { +const ErrorBoundryWrapper = ({ children }: BaseComponentProps) => { return ( <ErrorBoundary FallbackComponent={ErrorFallback} diff --git a/src/components/shared/types.ts b/src/components/shared/types.ts new file mode 100644 index 000000000..b7ea58765 --- /dev/null +++ b/src/components/shared/types.ts @@ -0,0 +1,13 @@ +import { AlertColor } from '@mui/material'; + +import { ReactNode } from 'react'; +import { BaseComponentProps } from 'types'; + +export interface AlertBoxProps extends BaseComponentProps { + severity: AlertColor; + fitWidth?: boolean; + hideIcon?: boolean; + onClose?: () => void; + short?: boolean; + title?: string | ReactNode; +} diff --git a/src/components/tables/RowActions/DisableEnable/Button.tsx b/src/components/tables/RowActions/DisableEnable/Button.tsx index 403a9ef34..c5c6ae80c 100644 --- a/src/components/tables/RowActions/DisableEnable/Button.tsx +++ b/src/components/tables/RowActions/DisableEnable/Button.tsx @@ -1,16 +1,10 @@ import DisableEnableConfirmation from 'components/tables/RowActions/DisableEnable/Confirmation'; import RowActionButton from 'components/tables/RowActions/Shared/Button'; import UpdateEntity from 'components/tables/RowActions/Shared/UpdateEntity'; -import produce from 'immer'; import { SelectTableStoreNames } from 'stores/names'; -import { specContainsDerivation } from 'utils/misc-utils'; +import { generateDisabledSpec } from 'utils/entity-utils'; import { DisableEnableButtonProps } from './types'; -const updateShardDisabled = (draftSpec: any, enabling: boolean) => { - draftSpec.shards ??= {}; - draftSpec.shards.disable = !enabling; -}; - function DisableEnableButton({ enabling, selectableTableStoreName, @@ -43,41 +37,9 @@ function DisableEnableButton({ skippedMessageID={messages.skipped} successMessageID={messages.success} runningMessageID={messages.running} - generateNewSpec={(spec) => { - // Make sure we have a spec to update - if (spec) { - // Check if we need to place the settings deeper (collections) - if (shardsAreNested) { - const { isDerivation, derivationKey } = - specContainsDerivation(spec); - - // Check if there is a derivation key we can update (derivations) - // if the collection is not a derivation then we cannot enable/disable - if (isDerivation) { - return produce<typeof spec>( - spec, - (draftSpec) => { - updateShardDisabled( - draftSpec[derivationKey], - enabling - ); - } - ); - } - } else { - // Not nested so we can update the root (captures and materializations) - return produce<typeof spec>( - spec, - (draftSpec) => { - updateShardDisabled( - draftSpec, - enabling - ); - } - ); - } - } - }} + generateNewSpec={(spec) => + generateDisabledSpec(spec, enabling, shardsAreNested) + } generateNewSpecType={(entity) => entity.spec_type} selectableStoreName={selectableTableStoreName} /> diff --git a/src/context/Confirmation/index.tsx b/src/context/Confirmation/index.tsx index c2390c287..5c74ab983 100644 --- a/src/context/Confirmation/index.tsx +++ b/src/context/Confirmation/index.tsx @@ -87,7 +87,7 @@ const ConfirmationModalContextProvider = ({ children }: BaseComponentProps) => { </Box> </DialogContent> - <DialogActions sx={{ p: '16px 24px' }}> + <DialogActions style={{ padding: '16px 24px' }}> <Button variant="text" onClick={handlers.dismiss}> <FormattedMessage id={settings.cancelText} /> </Button> diff --git a/src/hooks/useLiveSpecsExt.ts b/src/hooks/useLiveSpecsExt.ts index f2543d06c..b0f56894d 100644 --- a/src/hooks/useLiveSpecsExt.ts +++ b/src/hooks/useLiveSpecsExt.ts @@ -109,3 +109,27 @@ export function useLiveSpecsExtWithOutSpec( ): Response<LiveSpecsExtQuery> { 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[]) { + const { data, error, isValidating } = useQuery( + supabaseClient + .from(TABLES.LIVE_SPECS_EXT) + .select(liveSpecsExtRelatedQuery) + .eq('spec_type', 'materialization') + .overlaps('reads_from', selected) + .returns<LiveSpecsExt_Related[]>() + ); + + return { + related: data ?? [], + error, + isValidating, + }; +} diff --git a/src/lang/en-US/Workflows.ts b/src/lang/en-US/Workflows.ts index b21a86204..80f42885f 100644 --- a/src/lang/en-US/Workflows.ts +++ b/src/lang/en-US/Workflows.ts @@ -86,10 +86,6 @@ export const Workflows: Record<string, string> = { 'workflows.collectionSelector.manualBackfill.count.disabled': `no {itemType} available to backfill`, 'workflows.collectionSelector.manualBackfill.count.aria': `Backfill count`, - '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.`, - 'workflows.collectionSelector.manualBackfill.error.title': `Backfill update failed`, 'workflows.collectionSelector.manualBackfill.error.message.singleCollection': `There was an issue updating the backfill counter for one or more bindings associated with collection, {collection}.`, 'workflows.collectionSelector.manualBackfill.error.message.allBindings': `There was an issue updating the backfill counter for one or more bindings.`, @@ -131,6 +127,21 @@ 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.`, + // 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.logs.spinner.stopped': `done`, + 'dataflowReset.logs.spinner.running': `loading...`, + // Field Selection 'fieldSelection.header': `Field Selection`, 'fieldSelection.table.label': `Field Selection Editor`, @@ -335,4 +346,6 @@ export const Workflows: Record<string, string> = { // 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`, }; diff --git a/src/services/ajv.ts b/src/services/ajv.ts index db9da8aa2..51549ae82 100644 --- a/src/services/ajv.ts +++ b/src/services/ajv.ts @@ -42,6 +42,8 @@ export const addKeywords = (ajv: Ajv) => { ajv.addKeyword('x-collection-name'); // Used to default name in resource configs ajv.addKeyword('discriminator'); // Used to know what field in a complex oneOf should be unique (ex: parser) ajv.addKeyword('x-infer-schema'); // Indicates that schema inference should be enabled in the UI + ajv.addKeyword('x-delta-updates'); // Backend only + ajv.addKeyword('x-schema-name'); // Backend only return ajv; }; diff --git a/src/stores/Binding/Store.ts b/src/stores/Binding/Store.ts index e261af89b..fd425459d 100644 --- a/src/stores/Binding/Store.ts +++ b/src/stores/Binding/Store.ts @@ -232,6 +232,7 @@ const getInitialMiscData = (): Pick< | 'backfilledBindings' | 'backfillAllBindings' | 'backfillDataFlow' + | 'backfillDataFlowTarget' | 'backfillSupported' | 'collectionsRequiringRediscovery' | 'disabledCollections' @@ -245,7 +246,8 @@ const getInitialMiscData = (): Pick< | 'serverUpdateRequired' > => ({ backfillAllBindings: false, - backfillDataFlow: false, + backfillDataFlowTarget: null, + backfillDataFlow: true, backfillSupported: true, backfilledBindings: [], collectionsRequiringRediscovery: [], @@ -1024,13 +1026,23 @@ const getInitialState = ( ); }, + setBackfillDataFlowTarget: (value) => { + set( + produce((state: BindingState) => { + state.backfillDataFlowTarget = value; + }), + false, + 'Backfill data flow target changed' + ); + }, + setBackfillSupported: (value) => { set( produce((state: BindingState) => { state.backfillSupported = value; }), false, - 'Backfill Disabled Changed' + 'Backfill supported changed' ); }, diff --git a/src/stores/Binding/types.ts b/src/stores/Binding/types.ts index e2d01712a..ae30fe154 100644 --- a/src/stores/Binding/types.ts +++ b/src/stores/Binding/types.ts @@ -92,6 +92,11 @@ export interface BindingState backfillDataFlow: boolean; setBackfillDataFlow: (val: BindingState['backfillDataFlow']) => void; + backfillDataFlowTarget: string | null; + setBackfillDataFlowTarget: ( + val: BindingState['backfillDataFlowTarget'] + ) => void; + // Resource Schema resourceSchema: Schema; setResourceSchema: (val: BindingState['resourceSchema']) => Promise<void>; diff --git a/src/stores/FormState/Store.ts b/src/stores/FormState/Store.ts index 42a28355d..c48474fde 100644 --- a/src/stores/FormState/Store.ts +++ b/src/stores/FormState/Store.ts @@ -83,13 +83,21 @@ const getInitialStateData = ( messagePrefix: MessagePrefixes ): Pick< EntityFormState, - 'formState' | 'isIdle' | 'isActive' | 'messagePrefix' + | 'formState' + | 'isIdle' + | 'isActive' + | 'messagePrefix' + | 'showPreSavePrompt' + | 'liveSpec' > => ({ formState: initialFormState, isIdle: true, isActive: false, + liveSpec: null, + showPreSavePrompt: false, + messagePrefix, }); @@ -170,6 +178,26 @@ const getInitialState = ( ); }, + setShowPreSavePrompt: (newVal) => { + set( + produce((state: EntityFormState) => { + state.showPreSavePrompt = newVal; + }), + false, + 'Show Change Review Updated' + ); + }, + + setLiveSpec: (newVal) => { + set( + produce((state: EntityFormState) => { + state.liveSpec = newVal; + }), + false, + 'Live Spec Updated' + ); + }, + resetState: () => { set( getInitialStateData(messagePrefix), diff --git a/src/stores/FormState/hooks.ts b/src/stores/FormState/hooks.ts index febf8cdec..40cd08391 100644 --- a/src/stores/FormState/hooks.ts +++ b/src/stores/FormState/hooks.ts @@ -138,3 +138,39 @@ export const useFormStateStore_messagePrefix = () => { (state) => state.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(); + + return useZustandStore<EntityFormState, EntityFormState['liveSpec']>( + storeName(workflow), + (state) => state.liveSpec + ); +}; + +export const useFormStateStore_setLiveSpec = () => { + const workflow = useEntityWorkflow(); + + return useZustandStore<EntityFormState, EntityFormState['setLiveSpec']>( + storeName(workflow), + (state) => state.setLiveSpec + ); +}; diff --git a/src/stores/FormState/types.ts b/src/stores/FormState/types.ts index e5beac14c..04600783e 100644 --- a/src/stores/FormState/types.ts +++ b/src/stores/FormState/types.ts @@ -1,6 +1,6 @@ import { AlertColor } from '@mui/material'; import { PostgrestError } from '@supabase/postgrest-js'; -import { MessagePrefixes } from 'types'; +import { MessagePrefixes, Schema } from 'types'; export interface FormState { displayValidation: boolean; @@ -50,6 +50,12 @@ export interface EntityFormState { isIdle: boolean; isActive: boolean; + showPreSavePrompt: boolean; + setShowPreSavePrompt: (data: EntityFormState['showPreSavePrompt']) => void; + + liveSpec: Schema | null; + setLiveSpec: (data: EntityFormState['liveSpec']) => void; + updateStatus: (status: FormStatus, background?: boolean) => void; // Misc. diff --git a/src/utils/entity-utils.ts b/src/utils/entity-utils.ts new file mode 100644 index 000000000..50fc32a18 --- /dev/null +++ b/src/utils/entity-utils.ts @@ -0,0 +1,35 @@ +import produce from 'immer'; +import { specContainsDerivation } from 'utils/misc-utils'; + +export const updateShardDisabled = (draftSpec: any, enabling: boolean) => { + draftSpec.shards ??= {}; + draftSpec.shards.disable = !enabling; +}; + +export const generateDisabledSpec = ( + spec: any, + enabling: boolean, + shardsAreNested: boolean +) => { + // Make sure we have a spec to update + if (spec) { + // Check if we need to place the settings deeper (collections) + if (shardsAreNested) { + const { isDerivation, derivationKey } = + specContainsDerivation(spec); + + // Check if there is a derivation key we can update (derivations) + // if the collection is not a derivation then we cannot enable/disable + if (isDerivation) { + return produce<typeof spec>(spec, (draftSpec) => { + updateShardDisabled(draftSpec[derivationKey], enabling); + }); + } + } else { + // Not nested so we can update the root (captures and materializations) + return produce<typeof spec>(spec, (draftSpec) => { + updateShardDisabled(draftSpec, enabling); + }); + } + } +};