Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable the backfill counter to be manually incremented in edit workflows #956

Merged
merged 13 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/components/collection/ResourceConfig.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { Box, Typography } from '@mui/material';
import ResourceConfigForm from 'components/collection/ResourceConfigForm';
import FieldSelectionViewer from 'components/editor/Bindings/FieldSelection';
import ManualBackfill from 'components/editor/Bindings/ManualBackfill';
import TimeTravel from 'components/editor/Bindings/TimeTravel';
import { useEditorStore_queryResponse_draftedBindingIndex } from 'components/editor/Store/hooks';
import { useEntityType } from 'context/EntityContext';
import { useEntityWorkflow_Editing } from 'context/Workflow';
import { FormattedMessage } from 'react-intl';
import {
useResourceConfig_hydrated,
useResourceConfig_resourceConfigOfCollectionProperty,
} from 'stores/ResourceConfig/hooks';
import TimeTravel from 'components/editor/Bindings/TimeTravel';
import { BindingsEditorConfigSkeleton } from './CollectionSkeletons';

interface Props {
Expand All @@ -17,9 +20,13 @@ interface Props {

function ResourceConfig({ collectionName, readOnly = false }: Props) {
const entityType = useEntityType();
const isEdit = useEntityWorkflow_Editing();

const hydrated = useResourceConfig_hydrated();

const draftedBindingIndex =
useEditorStore_queryResponse_draftedBindingIndex(collectionName);

// If the collection is disabled then it will not come back in the built spec
// binding list. This means the user could end up clicking "See Fields" button
// forever and never get fields listed.
Expand All @@ -46,6 +53,10 @@ function ResourceConfig({ collectionName, readOnly = false }: Props) {
)}
</Box>

{isEdit && draftedBindingIndex > -1 && !collectionDisabled ? (
<ManualBackfill bindingIndex={draftedBindingIndex} />
) : null}

{entityType === 'materialization' && !collectionDisabled ? (
<FieldSelectionViewer collectionName={collectionName} />
) : null}
Expand Down
154 changes: 154 additions & 0 deletions src/components/editor/Bindings/ManualBackfill.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { Box, Stack, Typography } from '@mui/material';
import OutlinedToggleButton from 'components/shared/OutlinedToggleButton';
import { useEntityType } from 'context/EntityContext';
import { Check } from 'iconoir-react';
import { useEffect, useMemo, useState } from 'react';
import { FormattedMessage } from 'react-intl';
import {
useFormStateStore_isActive,
useFormStateStore_setFormState,
} from 'stores/FormState/hooks';
import { FormStatus } from 'stores/FormState/types';
import {
useResourceConfig_addBackfilledCollection,
useResourceConfig_backfilledCollections,
useResourceConfig_currentCollection,
useResourceConfig_removeBackfilledCollection,
} from 'stores/ResourceConfig/hooks';
import { useEditorStore_queryResponse_draftSpecs } from '../Store/hooks';
import useUpdateBackfillCounter from './useUpdateBackfillCounter';

export type BooleanString = 'true' | 'false';

interface Props {
bindingIndex: number;
}

function ManualBackfill({ bindingIndex }: Props) {
const entityType = useEntityType();
const { updateBackfillCounter } = useUpdateBackfillCounter();

// Draft Editor Store
const draftSpecs = useEditorStore_queryResponse_draftSpecs();

// Form State Store
const formActive = useFormStateStore_isActive();
const setFormState = useFormStateStore_setFormState();

// Resource Config Store
const currentCollection = useResourceConfig_currentCollection();
const backfilledCollections = useResourceConfig_backfilledCollections();
const addBackfilledCollection = useResourceConfig_addBackfilledCollection();
const removeBackfilledCollection =
useResourceConfig_removeBackfilledCollection();

const selected = useMemo(
() =>
currentCollection
? backfilledCollections.includes(currentCollection)
: false,
[backfilledCollections, currentCollection]
);

const [increment, setIncrement] = useState<BooleanString | 'undefined'>(
selected ? 'true' : 'undefined'
);

const serverUpdateRequired = useMemo(() => {
if (currentCollection && increment !== 'undefined') {
return increment === 'true'
? !backfilledCollections.includes(currentCollection)
: backfilledCollections.includes(currentCollection);
}

return false;
}, [backfilledCollections, currentCollection, increment]);

const draftSpec = useMemo(
() =>
draftSpecs.length > 0 && draftSpecs[0].spec ? draftSpecs[0] : null,
[draftSpecs]
);

useEffect(() => {
if (
draftSpec &&
currentCollection &&
serverUpdateRequired &&
increment !== 'undefined'
) {
setFormState({ status: FormStatus.UPDATING });

updateBackfillCounter(
draftSpec,
bindingIndex,
increment,
currentCollection
).then(
() => {
increment === 'true'
? addBackfilledCollection(currentCollection)
: removeBackfilledCollection(currentCollection);

setFormState({ status: FormStatus.UPDATED });
},
(error) =>
setFormState({
status: FormStatus.FAILED,
error: {
title: 'workflows.collectionSelector.manualBackfill.error.title',
error,
},
})
);
}
}, [
addBackfilledCollection,
bindingIndex,
currentCollection,
draftSpec,
increment,
removeBackfilledCollection,
serverUpdateRequired,
setFormState,
updateBackfillCounter,
]);

return (
<Box sx={{ mt: 3 }}>
<Stack direction="row" spacing={1} sx={{ mb: 2 }}>
<Stack spacing={1}>
<Typography variant="h6" sx={{ mr: 0.5 }}>
<FormattedMessage id="workflows.collectionSelector.manualBackfill.header" />
</Typography>

<Typography component="div">
<FormattedMessage
id={`workflows.collectionSelector.manualBackfill.message.${entityType}`}
/>
</Typography>
</Stack>
</Stack>

<OutlinedToggleButton
value={increment}
selected={selected}
disabled={formActive}
onClick={(event, checked: string) => {
event.preventDefault();
event.stopPropagation();

setIncrement(checked === 'true' ? 'false' : 'true');
}}
>
<FormattedMessage id="workflows.collectionSelector.manualBackfill.cta.backfill" />

{selected ? (
<Check style={{ marginLeft: 8, fontSize: 13 }} />
) : null}
</OutlinedToggleButton>
</Box>
);
}

export default ManualBackfill;
84 changes: 84 additions & 0 deletions src/components/editor/Bindings/useUpdateBackfillCounter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { modifyDraftSpec } from 'api/draftSpecs';
import { useEntityType } from 'context/EntityContext';
import { DraftSpecQuery } from 'hooks/useDraftSpecs';
import { omit } from 'lodash';
import { useCallback } from 'react';
import { useIntl } from 'react-intl';
import { Schema } from 'types';
import { getBackfillCounter } from 'utils/workflow-utils';
import {
useEditorStore_persistedDraftId,
useEditorStore_queryResponse_mutate,
} from '../Store/hooks';
import { BooleanString } from './ManualBackfill';

function useUpdateBackfillCounter() {
const intl = useIntl();
const entityType = useEntityType();

// Draft Editor Store
const draftId = useEditorStore_persistedDraftId();
const mutateDraftSpecs = useEditorStore_queryResponse_mutate();

const updateBackfillCounter = useCallback(
async (
draftSpec: DraftSpecQuery,
bindingIndex: number,
increment: BooleanString,
collection: string
) => {
if (!mutateDraftSpecs || bindingIndex === -1) {
// TODO: Extend the type of the error object accepted by setFormState to include strings.
// Currently, this action only accepts a PostgresError object.
return Promise.reject({
message: intl.formatMessage(
{
id: 'workflows.collectionSelector.manualBackfill.error.message',
},
{ collection }
),
details: '',
hint: '',
code: '',
});
}

const spec: Schema = draftSpec.spec;
let counter = getBackfillCounter(spec.bindings[bindingIndex]);

if (increment === 'true') {
counter = counter + 1;
} else if (counter > 0) {
counter = counter - 1;
}

// TODO: Determine if setting the backfill counter to zero is the same as removing the property entirely.
if (counter > 0) {
spec.bindings[bindingIndex].backfill = counter;
} else {
// Remove the backfill property from the specification if it equates to default behavior.
spec.bindings[bindingIndex] = omit(
spec.bindings[bindingIndex],
'backfill'
);
}

const updateResponse = await modifyDraftSpec(spec, {
draft_id: draftId,
catalog_name: draftSpec.catalog_name,
spec_type: entityType,
});

if (updateResponse.error) {
return Promise.reject(updateResponse.error);
}

return mutateDraftSpecs();
},
[draftId, entityType, intl, mutateDraftSpecs]
);

return { updateBackfillCounter };
}

export default useUpdateBackfillCounter;
26 changes: 26 additions & 0 deletions src/components/editor/Store/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { LiveSpecsQuery_spec } from 'hooks/useLiveSpecs';
import { useEffect } from 'react';
import { EditorStoreNames } from 'stores/names';
import { Entity } from 'types';
import { hasLength } from 'utils/misc-utils';
import { getBindingIndex } from 'utils/workflow-utils';
import { EditorStoreState } from './types';

interface SelectorParams {
Expand Down Expand Up @@ -529,6 +531,30 @@ export const useEditorStore_queryResponse_mutate = (
>(storeName(entityType, localScope), (state) => state.queryResponse.mutate);
};

export const useEditorStore_queryResponse_draftedBindingIndex = (
collection: string | null,
params?: SelectorParams | undefined
) => {
const localScope = params?.localScope;

const useZustandStore = localScope
? useLocalZustandStore
: useGlobalZustandStore;

const entityType = useEntityType();

return useZustandStore<EditorStoreState<DraftSpecQuery>, number>(
storeName(entityType, localScope),
(state) =>
collection && hasLength(state.queryResponse.draftSpecs)
? getBindingIndex(
state.queryResponse.draftSpecs[0].spec.bindings,
collection
)
: -1
);
};

export const useEditorStore_resetState = (
params?: SelectorParams | undefined
) => {
Expand Down
75 changes: 75 additions & 0 deletions src/components/shared/OutlinedToggleButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import {
SxProps,
Theme,
ToggleButton,
ToggleButtonProps,
toggleButtonClasses,
useTheme,
} from '@mui/material';
import {
intensifiedOutline,
outlinedButtonBackground,
primaryColoredOutline,
primaryColoredOutline_hovered,
} from 'context/Theme';

interface Props extends ToggleButtonProps {
defaultStateSx?: SxProps<Theme>;
disabledStateSx?: SxProps<Theme>;
selectedStateSx?: SxProps<Theme>;
}

function OutlinedToggleButton({
children,
defaultStateSx,
disabled,
disabledStateSx,
selectedStateSx,
selected,
value,
onChange,
onClick,
...props
}: Props) {
const theme = useTheme();

let sx: SxProps<Theme> = {
px: '9px',
py: '3px',
border: intensifiedOutline[theme.palette.mode],
borderRadius: 2,
[`&.${toggleButtonClasses.selected}`]: selectedStateSx ?? {
'backgroundColor': outlinedButtonBackground[theme.palette.mode],
'border': primaryColoredOutline[theme.palette.mode],
'color': theme.palette.primary.main,
'&:hover': {
border: primaryColoredOutline_hovered[theme.palette.mode],
},
},
};

if (defaultStateSx) {
sx = { ...sx, ...defaultStateSx };
}

if (disabledStateSx) {
sx = { ...sx, [`&.${toggleButtonClasses.disabled}`]: disabledStateSx };
}

return (
<ToggleButton
{...props}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine now but this should be at the end to allow a user to override whatever they want.

size="small"
value={value}
selected={selected}
disabled={disabled}
onChange={onChange}
onClick={onClick}
sx={sx}
>
{children}
</ToggleButton>
);
}

export default OutlinedToggleButton;
Loading