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

Extend capabilities of the hidden, capture interval field #1319

Merged
merged 30 commits into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
e440048
Display a capture interval field
kiahna-tucker Oct 14, 2024
be155c1
Await async function in binding store hydrator
kiahna-tucker Oct 14, 2024
19df13a
Hydrate captureInterval with spec value if exists
kiahna-tucker Oct 17, 2024
09b7d4e
Update capture interval state when field value changes
kiahna-tucker Oct 17, 2024
a0ec88f
Unset the time unit when a postgres interval is entered
kiahna-tucker Oct 17, 2024
7617b25
Apply the stored interval to the drafted spec
kiahna-tucker Oct 17, 2024
586a352
Correct and apply the capture interval reg ex
kiahna-tucker Oct 18, 2024
5d97b3c
Constrain field width
kiahna-tucker Oct 18, 2024
94d14bc
Merge branch 'main' into kiahna-tucker/capture-interval/init-field
kiahna-tucker Oct 23, 2024
fa8b9fc
Remove code duplicated by merge
kiahna-tucker Oct 23, 2024
a6f59cc
Debounce server patches
kiahna-tucker Oct 24, 2024
90024e3
Do not use interval fallback for single unit intervals
kiahna-tucker Oct 24, 2024
c104bb2
Support intervals that evaluate to zero
kiahna-tucker Oct 24, 2024
81e2862
Ignore zeros in postgres intervals
kiahna-tucker Oct 24, 2024
dcb9d4a
Remove addressed comment
kiahna-tucker Oct 24, 2024
90a324a
Prevent unnecessary server patches
kiahna-tucker Oct 24, 2024
f25aa6a
Move example from description to tooltip
kiahna-tucker Oct 24, 2024
6a57350
Move interval utils into new file and expand test coverage
kiahna-tucker Oct 25, 2024
4c5a00f
Merge branch 'main' into kiahna-tucker/capture-interval/init-field
kiahna-tucker Oct 25, 2024
91f595c
Update page-level error header
kiahna-tucker Oct 25, 2024
83bd272
Remove page-level error on successful submisson
kiahna-tucker Oct 25, 2024
597c05c
Narrow default_capture_interval typ in ConnectorTag interface
kiahna-tucker Oct 25, 2024
c250cfc
Merge branch 'main' into kiahna-tucker/capture-interval/init-field
kiahna-tucker Oct 30, 2024
5414095
Refactor a series of code fragments
kiahna-tucker Oct 30, 2024
afc70e7
Merge branch 'main' into kiahna-tucker/capture-interval/init-field
kiahna-tucker Oct 31, 2024
91040d4
Move capture interval component props into types file
kiahna-tucker Nov 1, 2024
a25c32d
Display the default interval in field description
kiahna-tucker Nov 1, 2024
b6f0213
Hide capture interval field in all workflows
kiahna-tucker Nov 1, 2024
2113a41
Track capture interval LogRocket events
kiahna-tucker Nov 1, 2024
5a42fea
Merge branch 'main' into kiahna-tucker/capture-interval/init-field
kiahna-tucker Nov 1, 2024
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
11 changes: 8 additions & 3 deletions src/api/hydration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,21 @@ import {
LiveSpecsExt_MaterializeOrTransform,
} from 'hooks/useLiveSpecsExt';
import {
TABLES,
handleFailure,
handleSuccess,
supabaseRetry,
TABLES,
} from 'services/supabase';
import { Entity } from 'types';

// TODO (optimization): Consider removing he tight coupling between this file and the stores.
// These APIs are truly general purpose. Perhaps break them out by supabase table.
type ConnectorTagResourceData = Pick<
ConnectorTag,
'connector_id' | 'resource_spec_schema' | 'disable_backfill'
| 'connector_id'
| 'default_capture_interval'
| 'disable_backfill'
| 'resource_spec_schema'
>;

type ConnectorTagEndpointData = Pick<
Expand All @@ -43,7 +46,9 @@ export const getSchema_Resource = async (connectorTagId: string | null) => {
() =>
supabaseClient
.from(TABLES.CONNECTOR_TAGS)
.select(`resource_spec_schema, disable_backfill`)
.select(
`default_capture_interval,disable_backfill,resource_spec_schema`
)
.eq('id', connectorTagId)
.single(),
'getSchema_Resource'
Expand Down
211 changes: 173 additions & 38 deletions src/components/capture/Interval/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,30 @@ import {
OutlinedInput,
Select,
Stack,
Tooltip,
Typography,
} from '@mui/material';
import {
primaryButtonText,
primaryColoredBackground_hovered,
} from 'context/Theme';
import useCaptureInterval from 'hooks/captureInterval/useCaptureInterval';
import { HelpCircle } from 'iconoir-react';
import { useMemo, useState } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import { useBindingStore } from 'stores/Binding/Store';
import {
useFormStateStore_isActive,
useFormStateStore_status,
} from 'stores/FormState/hooks';
import { FormStatus } from 'stores/FormState/types';
import { hasLength } from 'utils/misc-utils';
import { getCaptureIntervalSegment } from 'utils/time-utils';
import {
CAPTURE_INTERVAL_RE,
NUMERIC_RE,
POSTGRES_INTERVAL_RE,
} from 'validation';

const DESCRIPTION_ID = 'capture-interval-description';
const INPUT_ID = 'capture-interval-input';
Expand All @@ -21,23 +42,72 @@ interface Props {
function CaptureInterval({ readOnly }: Props) {
const intl = useIntl();
const label = intl.formatMessage({
id: 'workflows.interval.input.label',
id: 'captureInterval.input.label',
});

// Need to check if Capture Interval is there
const { updateStoredInterval } = useCaptureInterval();

// Binding Store
const interval = useBindingStore((state) => state.captureInterval);

// Form State Store
const formActive = useFormStateStore_isActive();
const formStatus = useFormStateStore_status();

const lastIntervalChar = interval?.at(-1) ?? '';
const singleUnit = ['h', 'i', 'm', 's'].some(
(symbol) => lastIntervalChar === symbol
);

const [unit, setUnit] = useState(singleUnit ? lastIntervalChar : '');
const [input, setInput] = useState(
singleUnit ? interval?.substring(0, interval.length - 1) : interval
);

const loading = formActive || formStatus === FormStatus.TESTING_BACKGROUND;

const errorsExist = useMemo(() => {
const intervalErrorsExist =
input &&
hasLength(input) &&
!POSTGRES_INTERVAL_RE.test(input) &&
!CAPTURE_INTERVAL_RE.test(input);

return Boolean(
unit === 'i'
? intervalErrorsExist
: intervalErrorsExist && !NUMERIC_RE.test(input)
);
}, [input, unit]);

if (typeof input !== 'string') {
return null;
}

return (
<Stack spacing={1}>
<Typography variant="formSectionHeader">
<FormattedMessage id="workflows.interval.header" />
</Typography>
<Stack direction="row" spacing={1} style={{ alignItems: 'center' }}>
<Typography variant="formSectionHeader">
<FormattedMessage id="captureInterval.header" />
</Typography>

<Tooltip
placement="right-start"
title={intl.formatMessage({
id: 'captureInterval.tooltip',
})}
>
<HelpCircle style={{ fontSize: 11 }} />
</Tooltip>
</Stack>

<Typography>
<FormattedMessage id="workflows.interval.message" />
<Typography style={{ marginBottom: 16 }}>
<FormattedMessage id="captureInterval.message" />
</Typography>

<FormControl
error={false}
disabled={readOnly ?? loading}
error={errorsExist}
fullWidth={false}
size={INPUT_SIZE}
variant="outlined"
Expand All @@ -48,7 +118,7 @@ function CaptureInterval({ readOnly }: Props) {
}}
>
<InputLabel
disabled={readOnly}
disabled={readOnly ?? loading}
focused
htmlFor={INPUT_ID}
variant="outlined"
Expand All @@ -58,59 +128,124 @@ function CaptureInterval({ readOnly }: Props) {

<OutlinedInput
aria-describedby={DESCRIPTION_ID}
disabled={readOnly}
error={false}
id={INPUT_ID}
label={label}
size={INPUT_SIZE}
sx={{ borderRadius: 3 }}
onChange={(event) => {
console.log('change', event.target.value);
}}
disabled={readOnly ?? loading}
endAdornment={
<InputAdornment position="start">
<InputAdornment
position="end"
style={{ marginRight: 1 }}
>
<Select
disabled={readOnly}
disabled={readOnly ?? loading}
disableUnderline
error={false}
onChange={(event) => {
const value = event.target.value;
let evaluatedInterval = input;

if (unit === 'i' && value !== 'i') {
const intervalSegment =
getCaptureIntervalSegment(
input,
value
);

evaluatedInterval =
intervalSegment > -1
? intervalSegment.toString()
: '';

setInput(evaluatedInterval);
}

setUnit(value);

updateStoredInterval(
evaluatedInterval,
value
);
}}
required
size={INPUT_SIZE}
variant="standard"
sx={{
'backgroundColor': (theme) =>
theme.palette.primary.main,
'borderBottomLeftRadius': 0,
'borderBottomRightRadius': 5,
'borderTopLeftRadius': 0,
'borderTopRightRadius': 5,
'color': (theme) =>
primaryButtonText[theme.palette.mode],
'maxWidth': 100,
'minWidth': 100,
'& .MuiSelect-select': {
paddingBottom: 0.2,
'&.Mui-focused,&:hover': {
backgroundColor: (theme) =>
primaryColoredBackground_hovered[
theme.palette.mode
],
},
'& .MuiFilledInput-input': {
py: '8px',
},
'& .MuiSelect-iconFilled': {
color: (theme) =>
primaryButtonText[
theme.palette.mode
],
},
}}
onChange={(event) => {
console.log(
'select change',
event.target.value
);
}}
value={unit}
variant="filled"
>
<MenuItem value="s" selected>
<FormattedMessage id="workflows.interval.input.seconds" />
<FormattedMessage id="captureInterval.input.seconds" />
</MenuItem>

<MenuItem value="m">
<FormattedMessage id="workflows.interval.input.minutes" />
<FormattedMessage id="captureInterval.input.minutes" />
</MenuItem>

<MenuItem value="h">
<FormattedMessage id="workflows.interval.input.hours" />
<FormattedMessage id="captureInterval.input.hours" />
</MenuItem>

<MenuItem value="i">
<FormattedMessage id="captureInterval.input.interval" />
</MenuItem>
</Select>
</InputAdornment>
}
error={errorsExist}
id={INPUT_ID}
label={label}
onChange={(event) => {
const value = event.target.value;

setInput(value);
updateStoredInterval(value, unit, setUnit);
}}
size={INPUT_SIZE}
sx={{
borderRadius: 3,
pr: 0,
width: 400,
}}
value={input}
/>
<FormHelperText
id={DESCRIPTION_ID}
// error={showErrors ? !description : undefined}
>
errors go here
</FormHelperText>

{errorsExist ? (
<FormHelperText
id={DESCRIPTION_ID}
error={errorsExist}
style={{ marginLeft: 0 }}
>
{intl.formatMessage({
id:
unit === 'i'
? 'captureInterval.error.intervalFormat'
: 'captureInterval.error.generalFormat',
})}
</FormHelperText>
) : null}
</FormControl>
</Stack>
);
Expand Down
3 changes: 2 additions & 1 deletion src/components/editor/Bindings/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Stack, Typography, useTheme } from '@mui/material';
import AutoDiscoverySettings from 'components/capture/AutoDiscoverySettings';
import CaptureInterval from 'components/capture/Interval';
import BindingsEditor from 'components/editor/Bindings/Editor';
import BindingSelector from 'components/editor/Bindings/Selector';
import ListAndDetails from 'components/editor/ListAndDetails';
Expand Down Expand Up @@ -103,7 +104,7 @@ function BindingsMultiEditor({
<Stack spacing={3} sx={{ mb: 5 }}>
{entityType === 'capture' ? <AutoDiscoverySettings /> : null}

{/*{entityType === 'capture' ? <CaptureInterval /> : null}*/}
{entityType === 'capture' ? <CaptureInterval /> : null}

{entityType === 'materialization' ? <SourceCapture /> : null}

Expand Down
16 changes: 13 additions & 3 deletions src/context/Theme.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { ThemeKeys } from '@microlink/react-json-view';
import {
ThemeProvider as MUIThemeProvider,
createTheme,
PaletteOptions,
SxProps,
Theme,
ThemeOptions,
ThemeProvider as MUIThemeProvider,
TypographyProps,
createTheme,
useMediaQuery,
} from '@mui/material';
import CssBaseline from '@mui/material/CssBaseline';
import { Square, XmarkCircle, Check, Copy, WarningCircle } from 'iconoir-react';
import { Check, Copy, Square, WarningCircle, XmarkCircle } from 'iconoir-react';
import CheckSquare from 'icons/CheckSquare';
import React from 'react';
import { useLocalStorage } from 'react-use';
Expand Down Expand Up @@ -378,6 +378,11 @@ export const semiTransparentBackground_purple = {
dark: 'rgba(214, 194, 255, 0.12)',
};

export const primaryColoredBackground_hovered = {
light: '#3149AB',
dark: '#9EAED7',
};

export const textLoadingColor = {
light: 'rgba(11, 19, 30, 0.4)',
dark: 'rgba(247, 249, 252, 0.4)',
Expand Down Expand Up @@ -668,6 +673,11 @@ export const successButtonText = {
dark: '#66BB6A',
};

export const primaryButtonText = {
light: 'white',
dark: 'rgba(0, 0, 0, 0.87)',
};

// Light is an RGB translation of #2A7942; Dark is an RGB translation of #66BB6A.
export const disabledButtonText_success = {
light: `rgba(42, 121, 66, 0.26)`,
Expand Down
Loading