diff --git a/package.json b/package.json index 0bef214d..0a483e0b 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@tanstack/react-query": "5.90.21", "@tanstack/react-query-devtools": "5.91.3", "@xyflow/react": "12.10.1", + "ace-builds": "1.43.6", "antd": "4.24.16", "axios": "1.13.5", "clsx": "2.1.1", @@ -32,6 +33,7 @@ "i18next": "24.2.3", "rc-picker": "4.11.3", "react": "18.3.1", + "react-ace": "14.0.1", "react-dom": "18.3.1", "react-error-boundary": "4.1.2", "react-i18next": "15.7.4", diff --git a/src/app/styles/variables.less b/src/app/styles/variables.less index f0ad101e..d1596a24 100644 --- a/src/app/styles/variables.less +++ b/src/app/styles/variables.less @@ -18,6 +18,7 @@ @typography-title-margin-bottom: 0; @input-bg: #f1f3f5; +@input-border: 1px solid #d9d9d9; @select-background: #f1f3f5; @picker-bg: #f1f3f5; @tooltip-bg: #000000; diff --git a/src/entities/transformation/constants.ts b/src/entities/transformation/constants.ts index 272f5db8..4b09662f 100644 --- a/src/entities/transformation/constants.ts +++ b/src/entities/transformation/constants.ts @@ -21,6 +21,7 @@ export const TRANSFORMATIONS_FORM_DEFAULT_VALUE: TransformationsForm = { [TransformationType.FILTER_FILE]: [], [TransformationType.FILTER_ROWS]: [], [TransformationType.FILTER_COLUMNS]: [], + [TransformationType.FILTER_SQL]: [], }; export const TRANSFORMATIONS_REQUEST_DEFAULT_VALUE: Transformations = [ @@ -71,6 +72,8 @@ const FILE_TRANSFORMATION_TYPES = Object.values(TransformationType); const DB_TRANSFORMATION_TYPES = FILE_TRANSFORMATION_TYPES.filter((type) => type !== TransformationType.FILTER_FILE); +export const DEFAULT_TRANSFORMATION_TYPES = DB_TRANSFORMATION_TYPES; + export const CONNECTION_TYPE_SUPPORT_TRANSFORMATION_TYPES: Record = { [ConnectionType.CLICKHOUSE]: DB_TRANSFORMATION_TYPES, [ConnectionType.FTP]: FILE_TRANSFORMATION_TYPES, diff --git a/src/entities/transformation/types.ts b/src/entities/transformation/types.ts index 2e81e09e..748a097f 100644 --- a/src/entities/transformation/types.ts +++ b/src/entities/transformation/types.ts @@ -4,6 +4,7 @@ export enum TransformationType { FILTER_ROWS = 'dataframe_rows_filter', FILTER_COLUMNS = 'dataframe_columns_filter', FILTER_FILE = 'file_metadata_filter', + FILTER_SQL = 'sql', } export enum TransformationFilterRowsType { @@ -107,15 +108,43 @@ export interface TransformationFilterFileForm { filters: Array; } -export type Transformations = Array; +export enum TransformationFilterSqlDialect { + SPARK = 'spark', +} + +export type TransformationFilterSql = { + type: TransformationType.FILTER_SQL; + dialect: TransformationFilterSqlDialect; + query: string; +}; + +export type Transformations = Array< + TransformationFilterRows | TransformationFilterColumns | TransformationFilterFile | TransformationFilterSql +>; + +export interface TransformationFilterSqlSparkItemForm { + dialect: TransformationFilterSqlDialect; + query: string; +} + +export type TransformationFormFilterSql = { + type: TransformationType.FILTER_SQL; + filters: Array; +}; export interface TransformationsForm { [TransformationType.FILTER_FILE]?: TransformationFilterFileForm['filters']; [TransformationType.FILTER_ROWS]?: TransformationFilterRows['filters']; [TransformationType.FILTER_COLUMNS]?: TransformationFilterColumns['filters']; + [TransformationType.FILTER_SQL]?: TransformationFormFilterSql['filters']; } -export type TransformationsFormNestedType = +export type TransformationsFormWithNestedType = + | TransformationType.FILTER_FILE + | TransformationType.FILTER_ROWS + | TransformationType.FILTER_COLUMNS; + +export type TransformationsFormNestedType = Required[T][number]['type']; export interface ShowButtonsContextProps { diff --git a/src/entities/transformation/ui/TransformationForm/TransformationForm.tsx b/src/entities/transformation/ui/TransformationForm/TransformationForm.tsx index 0642b9f0..6dd8bf3b 100644 --- a/src/entities/transformation/ui/TransformationForm/TransformationForm.tsx +++ b/src/entities/transformation/ui/TransformationForm/TransformationForm.tsx @@ -1,9 +1,9 @@ import { Button, Form, FormListFieldData } from 'antd'; -import React, { memo, useLayoutEffect } from 'react'; +import { memo, useLayoutEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useShowButtons } from '../../hooks'; -import { Transformations, TransformationType } from '../../types'; +import { TransformationsForm, TransformationType } from '../../types'; import { TransformationFormItem } from './components'; import { TransformationFormProps } from './types'; @@ -17,20 +17,24 @@ const TransformationFormComponent = ({ const { isDisplayed } = useShowButtons(); const formInstance = Form.useFormInstance(); - const filtersValues: Transformations[number]['filters'] | undefined = formInstance.getFieldValue([ + const filtersValues: TransformationsForm[T] | undefined = formInstance.getFieldValue([ 'transformations', transformationType, ]); + const isFilterEmpty = !filtersValues?.length; /** Add at least one element to array form value here, * because it is inconvenient to check for the presence of a default value of this array, * when forming a request to backend or initial form values */ useLayoutEffect(() => { - const needFillEmpty = !canHaveEmptyRecordsList && !filtersValues?.length; + const needFillEmpty = !canHaveEmptyRecordsList && isFilterEmpty; if (needFillEmpty) { formInstance.setFieldValue(['transformations', transformationType], [{}]); } - }, [formInstance, filtersValues, transformationType, canHaveEmptyRecordsList]); + }, [formInstance, isFilterEmpty, transformationType, canHaveEmptyRecordsList]); + + const isNeedShowAddNew = (fields: FormListFieldData[]) => + isDisplayed && !(transformationType === TransformationType.FILTER_SQL && fields.length); const canRemoveItem = ({ name }: FormListFieldData) => (name || canHaveEmptyRecordsList) && isDisplayed; @@ -47,9 +51,11 @@ const TransformationFormComponent = ({ key={field.key} /> ))} - + {isNeedShowAddNew(fields) && ( + + )} )} diff --git a/src/entities/transformation/ui/TransformationForm/components/FilterComponent/index.tsx b/src/entities/transformation/ui/TransformationForm/components/FilterComponent/index.tsx index 8f24e0c8..a7151704 100644 --- a/src/entities/transformation/ui/TransformationForm/components/FilterComponent/index.tsx +++ b/src/entities/transformation/ui/TransformationForm/components/FilterComponent/index.tsx @@ -1,5 +1,8 @@ -import React from 'react'; -import { TransformationsFormNestedType, TransformationType } from '@entities/transformation'; +import { + TransformationsFormNestedType, + TransformationsFormWithNestedType, + TransformationType, +} from '@entities/transformation'; import { FilterColumnsValue } from '../FilterColumnsValue'; import { FilterFileValue } from '../FilterFileValue'; @@ -7,7 +10,7 @@ import { FilterRowsValue } from '../FilterRowsValue'; import { FilterComponentProps } from './types'; -export const FilterComponent = ({ +export const FilterComponent = ({ transformationType, nestedType, name, diff --git a/src/entities/transformation/ui/TransformationForm/components/FilterComponent/types.ts b/src/entities/transformation/ui/TransformationForm/components/FilterComponent/types.ts index 1e08f86b..5ee599ca 100644 --- a/src/entities/transformation/ui/TransformationForm/components/FilterComponent/types.ts +++ b/src/entities/transformation/ui/TransformationForm/components/FilterComponent/types.ts @@ -1,6 +1,6 @@ -import { TransformationsFormNestedType, TransformationType } from '@entities/transformation'; +import { TransformationsFormNestedType, TransformationsFormWithNestedType } from '@entities/transformation'; -export interface FilterComponentProps { +export interface FilterComponentProps { name: number; transformationType: T; nestedType?: TransformationsFormNestedType; diff --git a/src/entities/transformation/ui/TransformationForm/components/FilterSql/constants.ts b/src/entities/transformation/ui/TransformationForm/components/FilterSql/constants.ts new file mode 100644 index 00000000..4652a7cb --- /dev/null +++ b/src/entities/transformation/ui/TransformationForm/components/FilterSql/constants.ts @@ -0,0 +1,7 @@ +export const LINE_HEIGHT = 1.7; + +export const WIDTH = '100%'; + +export const HEIGHT = '100px'; + +export const FONT_SIZE = 1.5; diff --git a/src/entities/transformation/ui/TransformationForm/components/FilterSql/index.tsx b/src/entities/transformation/ui/TransformationForm/components/FilterSql/index.tsx new file mode 100644 index 00000000..77f61f99 --- /dev/null +++ b/src/entities/transformation/ui/TransformationForm/components/FilterSql/index.tsx @@ -0,0 +1,25 @@ +import { AceSqlEditor } from '@shared/ui/AceSqlEditor'; +import { getCountLines } from '@shared/utils'; + +import { FilterSqlProps } from './types'; +import { FONT_SIZE, LINE_HEIGHT, WIDTH } from './constants'; +import * as classes from './styles.module.less'; + +export const FilterSql = ({ autoHeightMaxLineCount, value, onChange }: FilterSqlProps) => { + /** Calc the number of lines in the SQL expression to set the height of the editor */ + const linesCount = Math.max(1, getCountLines(value as string)); + const heightLinesCount = autoHeightMaxLineCount ? Math.min(linesCount, autoHeightMaxLineCount) : linesCount; + + return ( + + ); +}; diff --git a/src/entities/transformation/ui/TransformationForm/components/FilterSql/styles.module.less b/src/entities/transformation/ui/TransformationForm/components/FilterSql/styles.module.less new file mode 100644 index 00000000..d4f8be73 --- /dev/null +++ b/src/entities/transformation/ui/TransformationForm/components/FilterSql/styles.module.less @@ -0,0 +1,5 @@ +.root { + border: @input-border; + background-color: @input-bg; + border-radius: 8px; +} diff --git a/src/entities/transformation/ui/TransformationForm/components/FilterSql/types.ts b/src/entities/transformation/ui/TransformationForm/components/FilterSql/types.ts new file mode 100644 index 00000000..dc139f00 --- /dev/null +++ b/src/entities/transformation/ui/TransformationForm/components/FilterSql/types.ts @@ -0,0 +1,5 @@ +export interface FilterSqlProps { + autoHeightMaxLineCount: number; + value?: string; + onChange?: (value: string) => void; +} diff --git a/src/entities/transformation/ui/TransformationForm/components/TransformationFormItem/TransformationFormItem.tsx b/src/entities/transformation/ui/TransformationForm/components/TransformationFormItem/TransformationFormItem.tsx index 57c96044..9f0b0c42 100644 --- a/src/entities/transformation/ui/TransformationForm/components/TransformationFormItem/TransformationFormItem.tsx +++ b/src/entities/transformation/ui/TransformationForm/components/TransformationFormItem/TransformationFormItem.tsx @@ -1,31 +1,49 @@ import { Button, Form, Input } from 'antd'; -import React, { useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import { DeleteOutlined } from '@ant-design/icons'; import { Select } from '@shared/ui'; import { useTranslation } from 'react-i18next'; import clsx from 'clsx'; -import { TransformationsFormNestedType, TransformationType } from '../../../../types'; +import { + TransformationsFormNestedType, + TransformationsFormWithNestedType, + TransformationType, +} from '../../../../types'; import { FilterComponent } from '../FilterComponent'; +import { FilterSql } from '../FilterSql'; import { TransformationFormItemProps } from './types'; import { useGetNestedTypesSelectOptions } from './hooks'; import * as classes from './styles.module.less'; +import { SQL_AUTO_HEIGHT_MAX_LINE_COUNT } from './constants'; export const TransformationFormItem = ({ name, transformationType, nestedTypeSelectLabel, hasColumnField, + hasNestedTypeSelectField, + hasFilterComponent, + hasSqlField, onRemove, }: TransformationFormItemProps) => { const { t } = useTranslation('transformation'); - const nestedTypesSelectOptions = useGetNestedTypesSelectOptions(transformationType); + const nestedTypesSelectOptions = useGetNestedTypesSelectOptions( + transformationType as TransformationsFormWithNestedType, + ); const formInstance = Form.useFormInstance(); - const initialType: TransformationsFormNestedType | undefined = useMemo(() => { - return formInstance.getFieldValue(['transformations', transformationType, name, 'type']); - }, [formInstance, name, transformationType]); + const initialType = useMemo(() => { + if (!hasNestedTypeSelectField) return undefined; + + return formInstance.getFieldValue([ + 'transformations', + transformationType as TransformationsFormWithNestedType, + name, + 'type', + ]) as TransformationsFormNestedType; + }, [formInstance, hasNestedTypeSelectField, name, transformationType]); /** Use custom type state, because Form.useWatch doesn't support dynamic fieldname like in Form.List */ const [type, setType] = useState(() => initialType); @@ -41,23 +59,36 @@ export const TransformationFormItem = ({ )} - - + + )} + {hasNestedTypeSelectField && ( + - - + )} + {hasSqlField && ( + + + + )} {onRemove && (