diff --git a/src/components/Misc/CreatableSelect.tsx b/src/components/Misc/CreatableSelect.tsx index 3f26b10a..0093798c 100644 --- a/src/components/Misc/CreatableSelect.tsx +++ b/src/components/Misc/CreatableSelect.tsx @@ -8,6 +8,7 @@ type Props = { setValue: (val: string) => void; placeholder: string; error: string; + style?: Record }; export function CreatableSelect(props: Props) { @@ -15,7 +16,7 @@ export function CreatableSelect(props: Props) { onDropdownClose: () => combobox.resetSelectedOption(), }); - const { data, setData, value, setValue } = props; + const { data, setData, value, setValue, style = {}} = props; const [search, setSearch] = useState(''); const exactOptionMatch = data.some((item) => item === search); @@ -71,7 +72,8 @@ export function CreatableSelect(props: Props) { } combobox.closeDropdown(); - }}> + }} + > } @@ -90,6 +92,7 @@ export function CreatableSelect(props: Props) { placeholder={props.placeholder || 'Search or Type'} rightSectionPointerEvents="none" error={props.error} + style={style} /> diff --git a/src/pages/Home/CreateStreamModal.tsx b/src/pages/Home/CreateStreamModal.tsx index 0f9961f6..6ac48b80 100644 --- a/src/pages/Home/CreateStreamModal.tsx +++ b/src/pages/Home/CreateStreamModal.tsx @@ -4,8 +4,10 @@ import { Button, CloseIcon, Modal, + NumberInput, Select, Stack, + TagsInput, Text, TextInput, ThemeIcon, @@ -98,7 +100,8 @@ const AddFieldButton = ({ onClick }: { onClick: () => void }) => { } - onClick={onClick}> + onClick={onClick} + style={{ padding: 12, fontSize: '0.8rem' }}> Add Field @@ -122,7 +125,29 @@ const SchemaTypeField = (props: { inputProps: GetInputPropsReturnType }) => { Choose dynamic, evolving schema or fixed, static schema. - + + ); +}; + +const PartitionLimitField = (props: { inputProps: GetInputPropsReturnType }) => { + return ( + + + + Partition Limit + + + + + Default value is set to 30 days. + + Days}/> ); }; @@ -171,6 +196,50 @@ const PartitionField = (props: { setValue={(value: string) => onFieldChange(value)} placeholder="Select or Add" error={props.error} + style={{width: 200}} + /> + + ); +}; + +const CustomPartitionField = (props: { + partitionFields: string[]; + onChangeValue: (key: string, field: string[]) => void; + isStaticSchema: boolean; + error: string; + value: string[]; +}) => { + const shouldDisable = _.isEmpty(props.partitionFields); + + return ( + + + + Custom Partition Field + + + + + Upto 3 columns + + props.onChangeValue('customPartitionFields', val)} + maxTags={3} + error={props.error} /> ); @@ -197,6 +266,8 @@ const useCreateStreamForm = () => { fields: [defaultFieldValue], schemaType: dynamicType, partitionField: defaultPartitionField, + partitionLimit: 30, + customPartitionFields: [], }, validate: { name: (value) => isValidStreamName(value), @@ -217,6 +288,16 @@ const useCreateStreamForm = () => { return schemaType === staticType && !_.includes(allStringFieldNames, val) ? 'Unknown Field' : null; }, schemaType: (val) => (_.includes([dynamicType, staticType], val) ? null : 'Choose either Dynamic or Static'), + customPartitionFields: (val, allValues) => { + if (_.isEmpty(val) || allValues.schemaType !== staticType) { + return null; + } else { + const allFieldNames = _.map(allValues.fields, (field) => field.name); + const invalidColumnNames = _.difference(val, allFieldNames); + return !_.isEmpty(invalidColumnNames) ? 'Unknown Field Included' : null; + } + }, + partitionLimit: (val) => (!_.isInteger(val) ? 'Must be a number' : null), }, validateInputOnChange: true, validateInputOnBlur: true, @@ -238,6 +319,7 @@ const useCreateStreamForm = () => { const onChangeValue = useCallback((key: string, value: any) => { form.setFieldValue(key, value); + form.validateField(key); }, []); return { form, onAddField, onRemoveField, onChangeValue }; @@ -248,6 +330,12 @@ const CreateStreamForm = (props: { toggleModal: () => void }) => { const stringFields = getStringFieldNames(form.values.fields); const isStaticSchema = form.values.schemaType === staticType; const partitionFields = [defaultPartitionField, ...(isStaticSchema ? stringFields : [])]; + const customPartitionFields = !isStaticSchema + ? [] + : _.chain(form.values.fields) + .map((field) => field.name) + .compact() + .value(); const { createLogStreamMutation } = useLogStream(); const { getLogStreamListRefetch } = useLogStream(); const onSuccessCallback = useCallback(() => { @@ -257,13 +345,15 @@ const CreateStreamForm = (props: { toggleModal: () => void }) => { const onSubmit = useCallback(() => { const { hasErrors } = form.validate(); - const { schemaType, fields, partitionField } = form.values; + const { schemaType, fields, partitionField, customPartitionFields, partitionLimit } = form.values; const isStatic = schemaType === staticType; if (hasErrors || (isStatic && _.isEmpty(fields))) return; const headers = { ...(partitionField !== defaultPartitionField ? { 'X-P-Time-Partition': partitionField } : {}), ...(isStatic ? { 'X-P-Static-Schema-Flag': true } : {}), + ...(_.isEmpty(customPartitionFields) ? {} : { 'X-P-Custom-Partition': _.join(customPartitionFields, ',') }), + ...(partitionField === defaultPartitionField || !_.isInteger(partitionLimit) ? {} : { 'X-P-Time-Partition-Limit': `${partitionLimit}d` }), }; const schmaFields = isStatic ? fields : {}; createLogStreamMutation({ @@ -305,6 +395,16 @@ const CreateStreamForm = (props: { toggleModal: () => void }) => { value={form.values.partitionField} error={_.toString(form.errors.partitionField)} /> + {form.values.partitionField !== defaultPartitionField && ( + + )} + diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx index 2ffc2eff..bba92ad7 100644 --- a/src/pages/Home/index.tsx +++ b/src/pages/Home/index.tsx @@ -36,11 +36,11 @@ const EmptyStreamsView: FC = () => { const Home: FC = () => { useDocumentTitle('Parseable | Streams'); const classes = homeStyles; - const { container } = classes; + const { container, createStreamButton } = classes; const navigate = useNavigate(); const { getStreamMetadata, metaData } = useGetStreamMetadata(); const [userSpecificStreams, setAppStore] = useAppStore((store) => store.userSpecificStreams); - const [userAccessMap] = useAppStore((store) => store.userAccessMap); + const [userAccessMap] = useAppStore((store) => store.userAccessMap); useEffect(() => { if (!Array.isArray(userSpecificStreams) || userSpecificStreams.length === 0) return; @@ -54,7 +54,7 @@ const Home: FC = () => { const displayEmptyPlaceholder = Array.isArray(userSpecificStreams) && userSpecificStreams.length === 0; const openCreateStreamModal = useCallback(() => { - setAppStore(store => toggleCreateStreamModal(store)) + setAppStore((store) => toggleCreateStreamModal(store)); }, []); return ( @@ -74,7 +74,7 @@ const Home: FC = () => { {userAccessMap.hasCreateStreamAccess && (