diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 0853fc59..7492f92b 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -6,4 +6,6 @@ window.env = { }; EOF +sed -i '/<\/head>/i \ ' /usr/share/nginx/html/index.html + exec "$@" \ No newline at end of file diff --git a/src/app/config/router/instance.tsx b/src/app/config/router/instance.tsx index 5a416218..1045d0b4 100644 --- a/src/app/config/router/instance.tsx +++ b/src/app/config/router/instance.tsx @@ -6,7 +6,12 @@ import { AuthLayout, ErrorLayout, PrivateLayout } from '@app/layouts'; import { CreateGroupPage, GroupDetailPage, GroupListPage, UpdateGroupPage } from '@pages/group'; import { AuthProvider } from '@entities/auth'; import { CreateQueuePage, QueueDetailPage, QueueListPage, UpdateQueuePage } from '@pages/queue'; -import { ConnectionDetailPage, ConnectionListPage } from '@pages/connection'; +import { + ConnectionDetailPage, + ConnectionListPage, + CreateConnectionPage, + UpdateConnectionPage, +} from '@pages/connection'; import { TransferDetailPage, TransferListPage } from '@pages/transfer'; import { RunDetailPage } from '@pages/run'; @@ -92,6 +97,14 @@ export const router = createBrowserRouter([ path: '/connections/:id', element: , }, + { + path: '/connections/create', + element: , + }, + { + path: '/connections/:id/update', + element: , + }, { path: '/transfers', element: , diff --git a/src/app/styles/antd.less b/src/app/styles/antd.less index e326b010..b10f3b3f 100644 --- a/src/app/styles/antd.less +++ b/src/app/styles/antd.less @@ -3,10 +3,6 @@ border-radius: 0; } -.ant-descriptions { - width: 100%; -} - .ant-descriptions-item-label { width: 250px; font-weight: 700; @@ -16,7 +12,9 @@ background-color: @white; } -.ant-select { +.ant-descriptions, +.ant-select, +.ant-input-number { width: 100%; } diff --git a/src/entities/auth/api/types.ts b/src/entities/auth/api/types.ts index aea1434c..27c01598 100644 --- a/src/entities/auth/api/types.ts +++ b/src/entities/auth/api/types.ts @@ -11,5 +11,5 @@ export interface LoginRequest { export interface LoginResponse { access_token: string; - refresh_token: string; + token_type: string; } diff --git a/src/entities/connection/api/connectionService.ts b/src/entities/connection/api/connectionService.ts index d335c04d..19d90050 100644 --- a/src/entities/connection/api/connectionService.ts +++ b/src/entities/connection/api/connectionService.ts @@ -1,7 +1,14 @@ import { axiosInstance } from '@shared/config'; import { PaginationResponse } from '@shared/types'; -import { GetConnectionsRequest, Connection, GetConnectionRequest } from './types'; +import { + GetConnectionsRequest, + Connection, + GetConnectionRequest, + CreateConnectionRequest, + UpdateConnectionRequest, + DeleteConnectionRequest, +} from './types'; export const connectionService = { getConnections: (params: GetConnectionsRequest): Promise> => { @@ -11,4 +18,16 @@ export const connectionService = { getConnection: ({ id }: GetConnectionRequest): Promise => { return axiosInstance.get(`connections/${id}`); }, + + createConnection: (data: CreateConnectionRequest): Promise => { + return axiosInstance.post(`connections`, data); + }, + + updateConnection: ({ id, ...data }: UpdateConnectionRequest): Promise => { + return axiosInstance.patch(`connections/${id}`, data); + }, + + deleteConnection: ({ id }: DeleteConnectionRequest): Promise => { + return axiosInstance.delete(`connections/${id}`); + }, }; diff --git a/src/entities/connection/api/hooks/index.ts b/src/entities/connection/api/hooks/index.ts index aad81f64..276dc7f8 100644 --- a/src/entities/connection/api/hooks/index.ts +++ b/src/entities/connection/api/hooks/index.ts @@ -1 +1,2 @@ export * from './useGetConnection'; +export * from './useDeleteConnection'; diff --git a/src/entities/connection/api/hooks/useDeleteConnection/index.ts b/src/entities/connection/api/hooks/useDeleteConnection/index.ts new file mode 100644 index 00000000..071aaa99 --- /dev/null +++ b/src/entities/connection/api/hooks/useDeleteConnection/index.ts @@ -0,0 +1,25 @@ +import { useMutation, UseMutationResult, useQueryClient } from '@tanstack/react-query'; +import { notification } from 'antd'; +import { getErrorMessage } from '@shared/config'; + +import { connectionService } from '../../connectionService'; +import { DeleteConnectionRequest } from '../../types'; +import { ConnectionQueryKey } from '../../keys'; + +/** Hook for deleting connection */ +export const useDeleteConnection = (data: DeleteConnectionRequest): UseMutationResult => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: () => connectionService.deleteConnection(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: [ConnectionQueryKey.GET_CONNECTIONS] }); + queryClient.removeQueries({ queryKey: [ConnectionQueryKey.GET_CONNECTION, data.id] }); + }, + onError: (error) => { + notification.error({ + message: getErrorMessage(error), + }); + }, + }); +}; diff --git a/src/entities/connection/api/types.ts b/src/entities/connection/api/types.ts index 0eb964a4..15339496 100644 --- a/src/entities/connection/api/types.ts +++ b/src/entities/connection/api/types.ts @@ -7,12 +7,17 @@ export type Connection = { description: string; } & ConnectionData; -type ConnectionData = ConnectionHive | ConnectionHdfs | ConnectionOracle | ConnectionPostgres | ConnectionS3; +export type ConnectionData = ConnectionHive | ConnectionHdfs | ConnectionOracle | ConnectionPostgres | ConnectionS3; -interface ConnectionHive { +export type ConnectionBucketStyle = 'domain' | 'path'; + +export type ConnectionProtocol = 'https' | 'http'; + +export interface ConnectionHive { auth_data: { type: ConnectionType.HIVE; user: string; + password?: string; }; connection_data: { type: ConnectionType.HIVE; @@ -20,10 +25,11 @@ interface ConnectionHive { }; } -interface ConnectionHdfs { +export interface ConnectionHdfs { auth_data: { type: ConnectionType.HDFS; user: string; + password?: string; }; connection_data: { type: ConnectionType.HDFS; @@ -31,10 +37,11 @@ interface ConnectionHdfs { }; } -interface ConnectionOracle { +export interface ConnectionOracle { auth_data: { type: ConnectionType.ORACLE; user: string; + password?: string; }; connection_data: { type: ConnectionType.ORACLE; @@ -42,37 +49,37 @@ interface ConnectionOracle { port: number; service_name: string | null; sid: string | null; - additional_params: object; }; } -interface ConnectionPostgres { +export interface ConnectionPostgres { auth_data: { type: ConnectionType.POSTGRES; user: string; + password?: string; }; connection_data: { type: ConnectionType.POSTGRES; host: string; port: number; database_name: string; - additional_params: object; }; } -interface ConnectionS3 { +export interface ConnectionS3 { auth_data: { type: ConnectionType.S3; access_key: string; + secret_key?: string; }; connection_data: { type: ConnectionType.S3; host: string; bucket: string; - bucket_style: 'domain' | 'path'; + bucket_style: ConnectionBucketStyle; port: number | null; region: string | null; - protocol: 'http' | 'https'; + protocol: ConnectionProtocol; }; } @@ -83,3 +90,19 @@ export interface GetConnectionsRequest extends PaginationRequest { export interface GetConnectionRequest { id: number; } + +export type CreateConnectionRequest = { + group_id: number; + name: string; + description: string; +} & ConnectionData; + +export type UpdateConnectionRequest = { + id: number; + name: string; + description: string; +} & ConnectionData; + +export interface DeleteConnectionRequest { + id: number; +} diff --git a/src/entities/connection/constants.ts b/src/entities/connection/constants.ts new file mode 100644 index 00000000..d25edff7 --- /dev/null +++ b/src/entities/connection/constants.ts @@ -0,0 +1,22 @@ +import { ConnectionType } from '@shared/types'; +import { prepareOptionsForSelect } from '@shared/ui'; + +import { ConnectionBucketStyle, ConnectionProtocol } from './api'; + +export const CONNECTION_TYPE_SELECT_OPTIONS = prepareOptionsForSelect({ + data: Object.values(ConnectionType), + renderLabel: (data) => data, + renderValue: (data) => data, +}); + +export const CONNECTION_BUCKET_STYLE_SELECT_OPTIONS = prepareOptionsForSelect({ + data: ['domain', 'path'], + renderLabel: (data) => data, + renderValue: (data) => data, +}); + +export const CONNECTION_PROTOCOL_SELECT_OPTIONS = prepareOptionsForSelect({ + data: ['https', 'http'], + renderLabel: (data) => data, + renderValue: (data) => data, +}); diff --git a/src/entities/connection/index.ts b/src/entities/connection/index.ts index b1c13e73..6b9a58e4 100644 --- a/src/entities/connection/index.ts +++ b/src/entities/connection/index.ts @@ -1 +1,4 @@ export * from './api'; +export * from './constants'; +export * from './ui'; +export * from './utils'; diff --git a/src/entities/connection/ui/ConnectionTypeForm/components/ConnectionHdfs/index.tsx b/src/entities/connection/ui/ConnectionTypeForm/components/ConnectionHdfs/index.tsx new file mode 100644 index 00000000..1aebf2e3 --- /dev/null +++ b/src/entities/connection/ui/ConnectionTypeForm/components/ConnectionHdfs/index.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { Form, Input } from 'antd'; + +import { useSensitiveFields } from '../../hooks'; + +export const ConnectionHdfs = () => { + const { isRequired } = useSensitiveFields(); + + return ( + <> + + + + + + + + + + + ); +}; diff --git a/src/entities/connection/ui/ConnectionTypeForm/components/ConnectionHive/index.tsx b/src/entities/connection/ui/ConnectionTypeForm/components/ConnectionHive/index.tsx new file mode 100644 index 00000000..4d7c5726 --- /dev/null +++ b/src/entities/connection/ui/ConnectionTypeForm/components/ConnectionHive/index.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { Form, Input } from 'antd'; + +import { useSensitiveFields } from '../../hooks'; + +export const ConnectionHive = () => { + const { isRequired } = useSensitiveFields(); + + return ( + <> + + + + + + + + + + + ); +}; diff --git a/src/entities/connection/ui/ConnectionTypeForm/components/ConnectionOracle/index.tsx b/src/entities/connection/ui/ConnectionTypeForm/components/ConnectionOracle/index.tsx new file mode 100644 index 00000000..68d31f53 --- /dev/null +++ b/src/entities/connection/ui/ConnectionTypeForm/components/ConnectionOracle/index.tsx @@ -0,0 +1,71 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { Form, Input, InputNumber } from 'antd'; + +import { useSensitiveFields } from '../../hooks'; +import { MAX_ALLOWED_PORT, MIN_ALLOWED_PORT } from '../../constants'; + +export const ConnectionOracle = () => { + const { isRequired } = useSensitiveFields(); + const form = Form.useFormInstance(); + + const [isServiceNameDisabled, setServiceNameDisabled] = useState(false); + const [isSidDisabled, setSidDisabled] = useState(false); + + const changeDisabledFields = useCallback(() => { + const serviceName = form.getFieldValue('service_name'); + const sid = form.getFieldValue('sid'); + setServiceNameDisabled(!!sid); + setSidDisabled(!!serviceName); + }, [form]); + + const handleFieldChange = () => { + changeDisabledFields(); + form.validateFields(['service_name', 'sid']); + }; + + //* It needs to validate required fields service_name and sid correctly if they have initial values + useEffect(() => { + changeDisabledFields(); + }, [changeDisabledFields]); + + return ( + <> + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/entities/connection/ui/ConnectionTypeForm/components/ConnectionPostgres/index.tsx b/src/entities/connection/ui/ConnectionTypeForm/components/ConnectionPostgres/index.tsx new file mode 100644 index 00000000..3405f75f --- /dev/null +++ b/src/entities/connection/ui/ConnectionTypeForm/components/ConnectionPostgres/index.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { Form, Input, InputNumber } from 'antd'; + +import { useSensitiveFields } from '../../hooks'; +import { MAX_ALLOWED_PORT, MIN_ALLOWED_PORT } from '../../constants'; + +export const ConnectionPostgres = () => { + const { isRequired } = useSensitiveFields(); + + return ( + <> + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/entities/connection/ui/ConnectionTypeForm/components/ConnectionS3/index.tsx b/src/entities/connection/ui/ConnectionTypeForm/components/ConnectionS3/index.tsx new file mode 100644 index 00000000..35b454d0 --- /dev/null +++ b/src/entities/connection/ui/ConnectionTypeForm/components/ConnectionS3/index.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { Form, Input, InputNumber, Select } from 'antd'; +import { CONNECTION_BUCKET_STYLE_SELECT_OPTIONS, CONNECTION_PROTOCOL_SELECT_OPTIONS } from '@entities/connection'; + +import { useSensitiveFields } from '../../hooks'; +import { MAX_ALLOWED_PORT, MIN_ALLOWED_PORT } from '../../constants'; + +export const ConnectionS3 = () => { + const { isRequired } = useSensitiveFields(); + + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/entities/connection/ui/ConnectionTypeForm/components/index.ts b/src/entities/connection/ui/ConnectionTypeForm/components/index.ts new file mode 100644 index 00000000..22c76bdb --- /dev/null +++ b/src/entities/connection/ui/ConnectionTypeForm/components/index.ts @@ -0,0 +1,5 @@ +export * from './ConnectionHdfs'; +export * from './ConnectionHive'; +export * from './ConnectionOracle'; +export * from './ConnectionPostgres'; +export * from './ConnectionS3'; diff --git a/src/entities/connection/ui/ConnectionTypeForm/constants.tsx b/src/entities/connection/ui/ConnectionTypeForm/constants.tsx new file mode 100644 index 00000000..32780b31 --- /dev/null +++ b/src/entities/connection/ui/ConnectionTypeForm/constants.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { createContext } from 'react'; +import { ConnectionType } from '@shared/types'; + +import { SensitiveFieldsContextProps } from './types'; +import { ConnectionHdfs, ConnectionHive, ConnectionOracle, ConnectionPostgres, ConnectionS3 } from './components'; + +const SENSITIVE_FIELDS_CONTEXT_INITIAL_VALUE: SensitiveFieldsContextProps = { + isRequired: true, +}; + +export const SensitiveFieldsContext = createContext( + SENSITIVE_FIELDS_CONTEXT_INITIAL_VALUE, +); + +export const MIN_ALLOWED_PORT = 1; +export const MAX_ALLOWED_PORT = 65535; + +export const CONNECTION_TYPE_COMPONENT = { + [ConnectionType.HDFS]: , + [ConnectionType.HIVE]: , + [ConnectionType.ORACLE]: , + [ConnectionType.POSTGRES]: , + [ConnectionType.S3]: , +}; diff --git a/src/entities/connection/ui/ConnectionTypeForm/hooks/index.ts b/src/entities/connection/ui/ConnectionTypeForm/hooks/index.ts new file mode 100644 index 00000000..93d5003c --- /dev/null +++ b/src/entities/connection/ui/ConnectionTypeForm/hooks/index.ts @@ -0,0 +1 @@ +export * from './useIsRequiredSensitiveFields'; diff --git a/src/entities/connection/ui/ConnectionTypeForm/hooks/useIsRequiredSensitiveFields/index.ts b/src/entities/connection/ui/ConnectionTypeForm/hooks/useIsRequiredSensitiveFields/index.ts new file mode 100644 index 00000000..062564ee --- /dev/null +++ b/src/entities/connection/ui/ConnectionTypeForm/hooks/useIsRequiredSensitiveFields/index.ts @@ -0,0 +1,5 @@ +import { useContext } from 'react'; + +import { SensitiveFieldsContext } from '../../constants'; + +export const useSensitiveFields = () => useContext(SensitiveFieldsContext); diff --git a/src/entities/connection/ui/ConnectionTypeForm/index.tsx b/src/entities/connection/ui/ConnectionTypeForm/index.tsx new file mode 100644 index 00000000..23e090f2 --- /dev/null +++ b/src/entities/connection/ui/ConnectionTypeForm/index.tsx @@ -0,0 +1,31 @@ +import React, { useState } from 'react'; +import { ConnectionType } from '@shared/types'; +import { Form, Select } from 'antd'; +import { CONNECTION_TYPE_SELECT_OPTIONS } from '@entities/connection'; + +import { ConnectionTypeFormProps } from './types'; +import { CONNECTION_TYPE_COMPONENT, SensitiveFieldsContext } from './constants'; + +export const ConnectionTypeForm = ({ initialType, isRequiredSensitiveFields = true }: ConnectionTypeFormProps) => { + const [selectedConnectionType, setConnectionType] = useState(initialType); + + const handleSelectConnectionType = (type: ConnectionType) => { + setConnectionType(type); + }; + + return ( + <> + + + + + + + + + + + + ); +}; diff --git a/src/features/connection/CreateConnection/types.ts b/src/features/connection/CreateConnection/types.ts new file mode 100644 index 00000000..86e3ccc1 --- /dev/null +++ b/src/features/connection/CreateConnection/types.ts @@ -0,0 +1,10 @@ +import { CreateConnectionRequest } from '@entities/connection'; +import { GroupData } from '@entities/group'; + +export type CreateConnectionForm = Omit & + CreateConnectionRequest['connection_data'] & + CreateConnectionRequest['auth_data']; + +export interface CreateConnectionProps { + group: GroupData; +} diff --git a/src/features/connection/DeleteConnection/index.tsx b/src/features/connection/DeleteConnection/index.tsx new file mode 100644 index 00000000..67ba0a71 --- /dev/null +++ b/src/features/connection/DeleteConnection/index.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { ControlButtons } from '@shared/ui'; +import { Typography } from 'antd'; +import { WarningOutlined } from '@ant-design/icons'; +import { useDeleteConnection } from '@entities/connection'; + +import classes from './styles.module.less'; +import { DeleteConnectionProps } from './types'; + +const { Text } = Typography; + +export const DeleteConnection = ({ connection, onSuccess, onCancel }: DeleteConnectionProps) => { + const { mutate: deleteConnection, isPending } = useDeleteConnection({ id: connection.id }); + + const handleSubmit = () => { + deleteConnection(null, { onSuccess }); + }; + + return ( +
+
+ + + Do you really want to delete connection «{connection.name}»? + +
+ +
+ ); +}; diff --git a/src/features/connection/DeleteConnection/styles.module.less b/src/features/connection/DeleteConnection/styles.module.less new file mode 100644 index 00000000..4341ad51 --- /dev/null +++ b/src/features/connection/DeleteConnection/styles.module.less @@ -0,0 +1,20 @@ +.root { + display: flex; + flex-direction: column; + gap: 24px; + + .main { + display: flex; + align-items: flex-start; + gap: 24px; + + .icon { + color: @red-6; + + svg { + width: 24px; + height: 24px; + } + } + } +} diff --git a/src/features/connection/DeleteConnection/types.ts b/src/features/connection/DeleteConnection/types.ts new file mode 100644 index 00000000..251fdc7d --- /dev/null +++ b/src/features/connection/DeleteConnection/types.ts @@ -0,0 +1,7 @@ +import { Connection } from '@entities/connection'; + +export interface DeleteConnectionProps { + connection: Connection; + onSuccess: () => void; + onCancel: () => void; +} diff --git a/src/features/connection/UpdateConnection/index.tsx b/src/features/connection/UpdateConnection/index.tsx new file mode 100644 index 00000000..34b21958 --- /dev/null +++ b/src/features/connection/UpdateConnection/index.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { ControlButtons, FormCurrentGroupDescription, ManagedForm } from '@shared/ui'; +import { Form, Input } from 'antd'; +import { useNavigate } from 'react-router-dom'; +import { Connection, ConnectionQueryKey, connectionService, ConnectionTypeForm } from '@entities/connection'; + +import { adaptConnectionTypeRequest } from '../utils'; + +import { UpdateConnectionForm, UpdateConnectionProps } from './types'; +import { getUpdateConnectionInitialValues } from './utils'; + +export const UpdateConnection = ({ connection, group }: UpdateConnectionProps) => { + const navigate = useNavigate(); + + const handleUpdateConnection = ({ name, description, ...values }: UpdateConnectionForm) => { + return connectionService.updateConnection( + Object.assign({ id: connection.id, name, description }, adaptConnectionTypeRequest(values)), + ); + }; + + const onSuccess = (response: Connection) => { + navigate(`/connections/${response.id}`); + }; + + const onCancel = () => { + navigate('/connections'); + }; + + return ( + + mutationFunction={handleUpdateConnection} + initialValues={getUpdateConnectionInitialValues(connection)} + onSuccess={onSuccess} + keysInvalidateQueries={[ + [{ queryKey: [ConnectionQueryKey.GET_CONNECTIONS, group.id] }], + [{ queryKey: [ConnectionQueryKey.GET_CONNECTION, connection.id] }], + ]} + > + + + + + + + + + + + + + + + ); +}; diff --git a/src/features/connection/UpdateConnection/types.ts b/src/features/connection/UpdateConnection/types.ts new file mode 100644 index 00000000..775e0d0a --- /dev/null +++ b/src/features/connection/UpdateConnection/types.ts @@ -0,0 +1,11 @@ +import { Connection, CreateConnectionRequest, UpdateConnectionRequest } from '@entities/connection'; +import { GroupData } from '@entities/group'; + +export type UpdateConnectionForm = Omit & + CreateConnectionRequest['connection_data'] & + CreateConnectionRequest['auth_data']; + +export interface UpdateConnectionProps { + connection: Connection; + group: GroupData; +} diff --git a/src/features/connection/UpdateConnection/utils/getUpdateConnectionInitialValues/index.ts b/src/features/connection/UpdateConnection/utils/getUpdateConnectionInitialValues/index.ts new file mode 100644 index 00000000..e1e5eec2 --- /dev/null +++ b/src/features/connection/UpdateConnection/utils/getUpdateConnectionInitialValues/index.ts @@ -0,0 +1,58 @@ +import { Connection } from '@entities/connection'; +import { ConnectionType } from '@shared/types'; + +import { UpdateConnectionForm } from '../../types'; + +/* Util for preparing initial data for update connection form */ +export const getUpdateConnectionInitialValues = (connection: Connection): UpdateConnectionForm => { + const baseFormData = { + name: connection.name, + description: connection.description, + id: connection.id, + }; + // TODO: [DOP-21832] Temporary solution until backend takes ConnectionType up a level in response. + if (connection.auth_data.type === ConnectionType.HDFS && connection.connection_data.type === ConnectionType.HDFS) { + return Object.assign(baseFormData, { + ...connection.connection_data, + ...connection.auth_data, + }); + } + if (connection.auth_data.type === ConnectionType.HIVE && connection.connection_data.type === ConnectionType.HIVE) { + return Object.assign(baseFormData, { + ...connection.connection_data, + ...connection.auth_data, + }); + } + if ( + connection.auth_data.type === ConnectionType.ORACLE && + connection.connection_data.type === ConnectionType.ORACLE + ) { + return Object.assign(baseFormData, { + ...connection.connection_data, + ...connection.auth_data, + }); + } + if ( + connection.auth_data.type === ConnectionType.POSTGRES && + connection.connection_data.type === ConnectionType.POSTGRES + ) { + return Object.assign(baseFormData, { + ...connection.connection_data, + ...connection.auth_data, + }); + } + if (connection.auth_data.type === ConnectionType.S3 && connection.connection_data.type === ConnectionType.S3) { + return Object.assign(baseFormData, { + ...connection.connection_data, + ...connection.auth_data, + }); + } + return { + name: connection.name, + description: connection.description, + type: ConnectionType.HDFS, + cluster: '', + user: '', + password: '', + }; +}; diff --git a/src/features/connection/UpdateConnection/utils/index.ts b/src/features/connection/UpdateConnection/utils/index.ts new file mode 100644 index 00000000..9b2f85be --- /dev/null +++ b/src/features/connection/UpdateConnection/utils/index.ts @@ -0,0 +1 @@ +export * from './getUpdateConnectionInitialValues'; diff --git a/src/features/connection/index.ts b/src/features/connection/index.ts index 92f2c7c1..51f160df 100644 --- a/src/features/connection/index.ts +++ b/src/features/connection/index.ts @@ -1,2 +1,5 @@ export * from './ConnectionList'; export * from './ConnectionDetailInfo'; +export * from './DeleteConnection'; +export * from './CreateConnection'; +export * from './UpdateConnection'; diff --git a/src/features/connection/utils/adaptConnectionTypeRequest/index.ts b/src/features/connection/utils/adaptConnectionTypeRequest/index.ts new file mode 100644 index 00000000..573a474e --- /dev/null +++ b/src/features/connection/utils/adaptConnectionTypeRequest/index.ts @@ -0,0 +1,27 @@ +import { ConnectionType } from '@shared/types'; +import { + adaptConnectionHdfs, + adaptConnectionHive, + adaptConnectionOracle, + adaptConnectionPostgres, + adaptConnectionS3, + ConnectionData, +} from '@entities/connection'; + +import { AdaptConnectionTypeRequestProps } from './types'; + +/* Util for for creating ConnectionData object for preparing request data for creating/updating connection */ +export const adaptConnectionTypeRequest = (values: AdaptConnectionTypeRequestProps): ConnectionData => { + switch (values.type) { + case ConnectionType.HDFS: + return adaptConnectionHdfs(values); + case ConnectionType.HIVE: + return adaptConnectionHive(values); + case ConnectionType.ORACLE: + return adaptConnectionOracle(values); + case ConnectionType.POSTGRES: + return adaptConnectionPostgres(values); + case ConnectionType.S3: + return adaptConnectionS3(values); + } +}; diff --git a/src/features/connection/utils/adaptConnectionTypeRequest/types.ts b/src/features/connection/utils/adaptConnectionTypeRequest/types.ts new file mode 100644 index 00000000..01f42b7c --- /dev/null +++ b/src/features/connection/utils/adaptConnectionTypeRequest/types.ts @@ -0,0 +1,3 @@ +import { ConnectionData } from '@entities/connection'; + +export type AdaptConnectionTypeRequestProps = ConnectionData['auth_data'] & ConnectionData['connection_data']; diff --git a/src/features/connection/utils/index.ts b/src/features/connection/utils/index.ts new file mode 100644 index 00000000..bca9a6c7 --- /dev/null +++ b/src/features/connection/utils/index.ts @@ -0,0 +1 @@ +export * from './adaptConnectionTypeRequest'; diff --git a/src/features/group/UpdateGroup/utils/getUpdateGroupInitialValues/index.ts b/src/features/group/UpdateGroup/utils/getUpdateGroupInitialValues/index.ts index 0d7a93a6..587a63dc 100644 --- a/src/features/group/UpdateGroup/utils/getUpdateGroupInitialValues/index.ts +++ b/src/features/group/UpdateGroup/utils/getUpdateGroupInitialValues/index.ts @@ -2,6 +2,7 @@ import { GroupData } from '@entities/group'; import { UpdateGroupForm } from '../../types'; +/* Util for preparing initial values for update group form */ export const getUpdateGroupInitialValues = (group: GroupData): UpdateGroupForm => { return { name: group.name, diff --git a/src/features/queue/DeleteQueue/index.tsx b/src/features/queue/DeleteQueue/index.tsx index ffea8116..925074cb 100644 --- a/src/features/queue/DeleteQueue/index.tsx +++ b/src/features/queue/DeleteQueue/index.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { ControlButtons } from '@shared/ui'; import { Typography } from 'antd'; import { WarningOutlined } from '@ant-design/icons'; -import { useDeleteQueue } from '@entities/queue/api/hooks/useDeleteQueue'; +import { useDeleteQueue } from '@entities/queue'; import classes from './styles.module.less'; import { DeleteQueueProps } from './types'; diff --git a/src/features/queue/UpdateQueue/utils/getUpdateQueueInitialValues/index.ts b/src/features/queue/UpdateQueue/utils/getUpdateQueueInitialValues/index.ts index 12f1b0f6..b94f85f5 100644 --- a/src/features/queue/UpdateQueue/utils/getUpdateQueueInitialValues/index.ts +++ b/src/features/queue/UpdateQueue/utils/getUpdateQueueInitialValues/index.ts @@ -2,6 +2,7 @@ import { Queue } from '@entities/queue'; import { UpdateQueueForm } from '../../types'; +/* Util for preparing initial values for update queue form */ export const getUpdateQueueInitialValues = (queue: Queue): UpdateQueueForm => { return { name: queue.name, diff --git a/src/index.html b/src/index.html index e3a6686d..aa4e0767 100644 --- a/src/index.html +++ b/src/index.html @@ -5,7 +5,6 @@ SyncMaster -
diff --git a/src/pages/connection/ConnectionListPage/index.tsx b/src/pages/connection/ConnectionListPage/index.tsx index 98df3433..0067d043 100644 --- a/src/pages/connection/ConnectionListPage/index.tsx +++ b/src/pages/connection/ConnectionListPage/index.tsx @@ -1,8 +1,12 @@ import React from 'react'; -import { PageContentWrapper } from '@shared/ui'; -import { Typography } from 'antd'; +import { AccessWrapper, PageContentWrapper } from '@shared/ui'; +import { Button, Typography } from 'antd'; import { GroupWarningAlert, useSelectedGroup } from '@entities/group'; import { ConnectionListWrapper } from '@widgets/connection'; +import { UserRole } from '@shared/types'; +import { Link } from 'react-router-dom'; + +import classes from './styles.module.less'; const { Title } = Typography; @@ -16,6 +20,11 @@ export const ConnectionListPage = () => { return ( + + + ); diff --git a/src/pages/connection/ConnectionListPage/styles.module.less b/src/pages/connection/ConnectionListPage/styles.module.less new file mode 100644 index 00000000..e2457ca4 --- /dev/null +++ b/src/pages/connection/ConnectionListPage/styles.module.less @@ -0,0 +1,3 @@ +.createButton { + align-self: flex-end; +} diff --git a/src/pages/connection/CreateConnectionPage/components/CreateConnectionPageContent/index.tsx b/src/pages/connection/CreateConnectionPage/components/CreateConnectionPageContent/index.tsx new file mode 100644 index 00000000..56ec23da --- /dev/null +++ b/src/pages/connection/CreateConnectionPage/components/CreateConnectionPageContent/index.tsx @@ -0,0 +1,17 @@ +import { GroupWarningAlert, useSelectedGroup } from '@entities/group'; +import { CreateConnection } from '@features/connection'; +import { UserRole } from '@shared/types'; +import { hasAccessByUserRole } from '@shared/utils'; +import React from 'react'; + +export const CreateConnectionPageContent = () => { + const { group } = useSelectedGroup(); + + if (!group?.data.id) { + return ; + } + if (!hasAccessByUserRole(UserRole.Maintainer, group.role)) { + return ; + } + return ; +}; diff --git a/src/pages/connection/CreateConnectionPage/components/index.ts b/src/pages/connection/CreateConnectionPage/components/index.ts new file mode 100644 index 00000000..e4f6b351 --- /dev/null +++ b/src/pages/connection/CreateConnectionPage/components/index.ts @@ -0,0 +1 @@ +export * from './CreateConnectionPageContent'; diff --git a/src/pages/connection/CreateConnectionPage/index.tsx b/src/pages/connection/CreateConnectionPage/index.tsx new file mode 100644 index 00000000..ec6601a6 --- /dev/null +++ b/src/pages/connection/CreateConnectionPage/index.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { Typography } from 'antd'; +import { PageContentWrapper } from '@shared/ui'; + +import { CreateConnectionPageContent } from './components'; + +const { Title } = Typography; + +export const CreateConnectionPage = () => { + return ( + + Create Connection + + + ); +}; diff --git a/src/pages/connection/UpdateConnectionPage/index.tsx b/src/pages/connection/UpdateConnectionPage/index.tsx new file mode 100644 index 00000000..5b763a65 --- /dev/null +++ b/src/pages/connection/UpdateConnectionPage/index.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { Typography } from 'antd'; +import { PageContentWrapper } from '@shared/ui'; +import { useParams } from 'react-router-dom'; +import { PageDetailParams, UserRole } from '@shared/types'; +import { useGetGroup } from '@entities/group'; +import { hasAccessByUserRole } from '@shared/utils'; +import { AccessError } from '@shared/config'; +import { useGetConnection } from '@entities/connection'; +import { UpdateConnection } from '@features/connection'; + +const { Title } = Typography; + +export const UpdateConnectionPage = () => { + const params = useParams(); + const { data: connection } = useGetConnection({ id: Number(params.id) }); + const { data: group } = useGetGroup({ id: connection.group_id }); + + if (!connection || !group) { + return null; + } + + if (!hasAccessByUserRole(UserRole.Maintainer, group.role)) { + throw new AccessError(); + } + + return ( + + Update Connection + + + ); +}; diff --git a/src/pages/connection/index.ts b/src/pages/connection/index.ts index 010aeb23..83b16ffa 100644 --- a/src/pages/connection/index.ts +++ b/src/pages/connection/index.ts @@ -1,2 +1,4 @@ export * from './ConnectionListPage'; export * from './ConnectionDetailPage'; +export * from './CreateConnectionPage'; +export * from './UpdateConnectionPage'; diff --git a/src/shared/config/axios/instance.ts b/src/shared/config/axios/instance.ts index df87340d..ef804cc9 100644 --- a/src/shared/config/axios/instance.ts +++ b/src/shared/config/axios/instance.ts @@ -3,7 +3,7 @@ import axios from 'axios'; import { requestInterceptor, responseSuccessInterceptor } from './interceptors'; export const axiosInstance = axios.create({ - baseURL: window.env?.API_URL || process.env.API_URL || 'http://localhost:8000/', + baseURL: window.env?.API_URL || process.env.API_URL || 'http://localhost:8000/v1/', headers: { Accept: 'application/json', 'Content-Type': 'application/json', diff --git a/src/shared/ui/FormCurrentGroupDescription/index.tsx b/src/shared/ui/FormCurrentGroupDescription/index.tsx index acc8f4af..4fb31512 100644 --- a/src/shared/ui/FormCurrentGroupDescription/index.tsx +++ b/src/shared/ui/FormCurrentGroupDescription/index.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { memo } from 'react'; import { Typography } from 'antd'; import classes from './styles.module.less'; @@ -6,7 +6,7 @@ import { FormCurrentGroupDescriptionProps } from './types'; const { Text } = Typography; -export const FormCurrentGroupDescription = ({ groupName }: FormCurrentGroupDescriptionProps) => { +export const FormCurrentGroupDescription = memo(({ groupName }: FormCurrentGroupDescriptionProps) => { return (
@@ -14,4 +14,4 @@ export const FormCurrentGroupDescription = ({ groupName }: FormCurrentGroupDescr
); -}; +}); diff --git a/src/widgets/connection/ConnectionDetail/components/DeleteConnectionButton/index.tsx b/src/widgets/connection/ConnectionDetail/components/DeleteConnectionButton/index.tsx new file mode 100644 index 00000000..fb500422 --- /dev/null +++ b/src/widgets/connection/ConnectionDetail/components/DeleteConnectionButton/index.tsx @@ -0,0 +1,40 @@ +import { DEFAULT_MODAL_DELETE_WIDTH } from '@shared/constants'; +import { useModalState } from '@shared/hooks'; +import { ModalWrapper } from '@shared/ui'; +import { Button } from 'antd'; +import React from 'react'; +import { DeleteConnection } from '@features/connection'; +import { useNavigate } from 'react-router-dom'; + +import { DeleteConnectionButtonProps } from './types'; + +export const DeleteConnectionButton = ({ connection }: DeleteConnectionButtonProps) => { + const navigate = useNavigate(); + + const { isOpened: isOpenedModal, handleOpen: handleOpenModal, handleClose: handleCloseModal } = useModalState(); + + const handleSuccessDeleteConnection = () => { + navigate('/connections'); + handleCloseModal(); + }; + + return ( + <> + + + + + + ); +}; diff --git a/src/widgets/connection/ConnectionDetail/components/DeleteConnectionButton/types.ts b/src/widgets/connection/ConnectionDetail/components/DeleteConnectionButton/types.ts new file mode 100644 index 00000000..e39242d3 --- /dev/null +++ b/src/widgets/connection/ConnectionDetail/components/DeleteConnectionButton/types.ts @@ -0,0 +1,5 @@ +import { Connection } from '@entities/connection'; + +export interface DeleteConnectionButtonProps { + connection: Connection; +} diff --git a/src/widgets/connection/ConnectionDetail/components/UpdateConnectionButton/index.tsx b/src/widgets/connection/ConnectionDetail/components/UpdateConnectionButton/index.tsx new file mode 100644 index 00000000..85db89ac --- /dev/null +++ b/src/widgets/connection/ConnectionDetail/components/UpdateConnectionButton/index.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { Button } from 'antd'; +import { Link } from 'react-router-dom'; + +import { UpdateConnectionButtonProps } from './types'; + +export const UpdateConnectionButton = ({ connectionId }: UpdateConnectionButtonProps) => { + return ( + + ); +}; diff --git a/src/widgets/connection/ConnectionDetail/components/UpdateConnectionButton/types.ts b/src/widgets/connection/ConnectionDetail/components/UpdateConnectionButton/types.ts new file mode 100644 index 00000000..dd6b1a63 --- /dev/null +++ b/src/widgets/connection/ConnectionDetail/components/UpdateConnectionButton/types.ts @@ -0,0 +1,3 @@ +export interface UpdateConnectionButtonProps { + connectionId: number; +} diff --git a/src/widgets/connection/ConnectionDetail/components/index.ts b/src/widgets/connection/ConnectionDetail/components/index.ts new file mode 100644 index 00000000..ae69eb85 --- /dev/null +++ b/src/widgets/connection/ConnectionDetail/components/index.ts @@ -0,0 +1,2 @@ +export * from './DeleteConnectionButton'; +export * from './UpdateConnectionButton'; diff --git a/src/widgets/connection/ConnectionDetail/index.tsx b/src/widgets/connection/ConnectionDetail/index.tsx index 0bef5fc3..5ad51366 100644 --- a/src/widgets/connection/ConnectionDetail/index.tsx +++ b/src/widgets/connection/ConnectionDetail/index.tsx @@ -1,14 +1,27 @@ import React from 'react'; -import { PageContentWrapper } from '@shared/ui'; +import { AccessWrapper, PageContentWrapper } from '@shared/ui'; import { ConnectionDetailInfo } from '@features/connection'; +import { UserRole } from '@shared/types'; import { ConnectionDetailProps } from './types'; +import { DeleteConnectionButton, UpdateConnectionButton } from './components'; +import classes from './styles.module.less'; export const ConnectionDetail = ({ connection, group }: ConnectionDetailProps) => { return ( - {/* //TODO: [DOP-20043] add update and delete actions for connection */} - + +
+ + +
+ + } + />
); }; diff --git a/src/widgets/connection/ConnectionDetail/styles.module.less b/src/widgets/connection/ConnectionDetail/styles.module.less new file mode 100644 index 00000000..f0c745a4 --- /dev/null +++ b/src/widgets/connection/ConnectionDetail/styles.module.less @@ -0,0 +1,5 @@ +.actions { + display: flex; + align-items: center; + gap: 16px; +} diff --git a/src/widgets/connection/ConnectionListWrapper/index.tsx b/src/widgets/connection/ConnectionListWrapper/index.tsx index 721234b0..afa78cf5 100644 --- a/src/widgets/connection/ConnectionListWrapper/index.tsx +++ b/src/widgets/connection/ConnectionListWrapper/index.tsx @@ -1,9 +1,56 @@ -import React from 'react'; -import { ConnectionList } from '@features/connection'; +import React, { useCallback, useState } from 'react'; +import { ConnectionList, DeleteConnection } from '@features/connection'; +import { ModalWrapper } from '@shared/ui'; +import { DEFAULT_MODAL_DELETE_WIDTH } from '@shared/constants'; +import { useNavigate } from 'react-router-dom'; +import { Connection } from '@entities/connection'; +import { useModalState } from '@shared/hooks'; import { ConnectionListWrapperProps } from './types'; export const ConnectionListWrapper = ({ group }: ConnectionListWrapperProps) => { - //TODO: [DOP-20043] add update and delete actions for connection - return ; + const navigate = useNavigate(); + const [selectedConnection, setSelectedConnection] = useState(); + + const { + isOpened: isOpenedDeleteConnectionModal, + handleOpen: handleOpenDeleteConnectionModal, + handleClose: handleCloseDeleteConnectionModal, + } = useModalState(); + + const handleDeleteUserClick = useCallback( + (connection: Connection) => { + setSelectedConnection(connection); + handleOpenDeleteConnectionModal(); + }, + [handleOpenDeleteConnectionModal], + ); + + const handleUpdateRowClick = useCallback( + (record: Connection) => { + navigate(`/connections/${record.id}/update`); + }, + [navigate], + ); + + return ( + <> + {selectedConnection && ( + + + + )} + + + + ); }; diff --git a/src/widgets/queue/QueueDetail/components/DeleteQueueButton/index.tsx b/src/widgets/queue/QueueDetail/components/DeleteQueueButton/index.tsx index 3e707ddb..54410e29 100644 --- a/src/widgets/queue/QueueDetail/components/DeleteQueueButton/index.tsx +++ b/src/widgets/queue/QueueDetail/components/DeleteQueueButton/index.tsx @@ -4,16 +4,24 @@ import { useModalState } from '@shared/hooks'; import { ModalWrapper } from '@shared/ui'; import { Button } from 'antd'; import React from 'react'; +import { useNavigate } from 'react-router-dom'; import { DeleteQueueButtonProps } from './types'; export const DeleteQueueButton = ({ queue }: DeleteQueueButtonProps) => { + const navigate = useNavigate(); + const { isOpened: isOpenedDeleteQueueModal, handleOpen: handleOpenDeleteQueueModal, handleClose: handleCloseDeleteQueueModal, } = useModalState(); + const handleSuccessDeleteQueue = () => { + navigate('/queues'); + handleCloseDeleteQueueModal(); + }; + return ( <>