Мемоизированно больше страниц и функций, добавлена переадресация со старницы входа на предыдущую, косметика

This commit is contained in:
Александр Сироткин 2022-02-25 16:57:08 +05:00
parent 8f451035f7
commit dca17e31e8
30 changed files with 201 additions and 196 deletions

View File

@ -15,12 +15,13 @@ import Login from '@pages/Login'
import Register from '@pages/Register' import Register from '@pages/Register'
import '@styles/App.less' import '@styles/App.less'
import { memo } from 'react'
//OpenAPI.BASE = 'http://localhost:3000' //OpenAPI.BASE = 'http://localhost:3000'
OpenAPI.TOKEN = async () => getUserToken() OpenAPI.TOKEN = async () => getUserToken()
OpenAPI.HEADERS = {'Content-Type': 'application/json'} OpenAPI.HEADERS = {'Content-Type': 'application/json'}
export const App = () => ( export const App = memo(() => (
<ConfigProvider locale={locale}> <ConfigProvider locale={locale}>
<Router> <Router>
<Switch> <Switch>
@ -36,6 +37,6 @@ export const App = () => (
</Switch> </Switch>
</Router> </Router>
</ConfigProvider> </ConfigProvider>
) ))
export default App export default App

View File

@ -1,5 +1,5 @@
import { memo, ReactNode } from 'react' import { memo, ReactNode } from 'react'
import { Link } from 'react-router-dom' import { Link, useLocation } from 'react-router-dom'
import { Button, Layout, LayoutProps } from 'antd' import { Button, Layout, LayoutProps } from 'antd'
import PageHeader from '@components/PageHeader' import PageHeader from '@components/PageHeader'
@ -8,17 +8,21 @@ export type AdminLayoutPortalProps = LayoutProps & {
title?: ReactNode title?: ReactNode
} }
export const AdminLayoutPortal = memo<AdminLayoutPortalProps>(({ title, ...props }) => ( export const AdminLayoutPortal = memo<AdminLayoutPortalProps>(({ title, ...props }) => {
const location = useLocation()
return (
<Layout.Content> <Layout.Content>
<PageHeader isAdmin title={title} style={{ backgroundColor: '#900' }}> <PageHeader isAdmin title={title} style={{ backgroundColor: '#900' }}>
<Button size={'large'}> <Button size={'large'}>
<Link to={'/'}>Вернуться на сайт</Link> <Link to={{ pathname: '/', state: { from: location.pathname }}}>Вернуться на сайт</Link>
</Button> </Button>
</PageHeader> </PageHeader>
<Layout> <Layout>
<Layout.Content className={'site-layout-background sheet'} {...props}/> <Layout.Content className={'site-layout-background sheet'} {...props}/>
</Layout> </Layout>
</Layout.Content> </Layout.Content>
)) )
})
export default AdminLayoutPortal export default AdminLayoutPortal

View File

@ -1,17 +1,18 @@
import { HTMLAttributes } from 'react' import { HTMLAttributes } from 'react'
import Loader from './Loader' import { Loader } from '@components/icons'
type LoaderPortalProps = HTMLAttributes<HTMLDivElement> & { type LoaderPortalProps = HTMLAttributes<HTMLDivElement> & {
show?: boolean, show?: boolean,
fade?: boolean, fade?: boolean,
spinnerProps?: HTMLAttributes<HTMLDivElement>,
} }
export const LoaderPortal: React.FC<LoaderPortalProps> = ({ className, show, fade = true, children, ...other }) => ( export const LoaderPortal: React.FC<LoaderPortalProps> = ({ className, show, fade = true, children, spinnerProps, ...other }) => (
<div className={`loader-container ${className}`} {...other}> <div className={`loader-container ${className}`} {...other}>
<div className={'loader-content'}>{children}</div> <div className={'loader-content'}>{children}</div>
{show && fade && <div className={'loader-fade'}/>} {show && fade && <div className={'loader-fade'}/>}
{show && <div className={'loader-overlay'}><Loader/></div>} {show && <div className={'loader-overlay'}><Loader {...spinnerProps} /></div>}
</div> </div>
) )

View File

@ -1,5 +1,5 @@
import { memo } from 'react' import { memo } from 'react'
import { Link } from 'react-router-dom' import { Link, useLocation } from 'react-router-dom'
import { Layout } from 'antd' import { Layout } from 'antd'
import { BasicProps } from 'antd/lib/layout/layout' import { BasicProps } from 'antd/lib/layout/layout'
@ -14,10 +14,13 @@ export type PageHeaderProps = BasicProps & {
children?: React.ReactNode children?: React.ReactNode
} }
export const PageHeader: React.FC<PageHeaderProps> = memo(({ title = 'Мониторинг', isAdmin, children, ...other }) => ( export const PageHeader: React.FC<PageHeaderProps> = memo(({ title = 'Мониторинг', isAdmin, children, ...other }) => {
const location = useLocation()
return (
<Layout> <Layout>
<Layout.Header className={'header'} {...other}> <Layout.Header className={'header'} {...other}>
<Link to={'/'} style={{ height: headerHeight }}> <Link to={{ pathname: '/', state: { from: location.pathname }}} style={{ height: headerHeight }}>
<Logo /> <Logo />
</Link> </Link>
{children} {children}
@ -25,6 +28,7 @@ export const PageHeader: React.FC<PageHeaderProps> = memo(({ title = 'Монит
<UserMenu isAdmin={isAdmin} /> <UserMenu isAdmin={isAdmin} />
</Layout.Header> </Layout.Header>
</Layout> </Layout>
)) )
})
export default PageHeader export default PageHeader

View File

@ -1,4 +1,4 @@
import { memo, ReactNode, useState } from 'react' import { memo, ReactNode, useCallback, useState } from 'react'
import { Button, ButtonProps, Form, FormProps, Popover, PopoverProps } from 'antd' import { Button, ButtonProps, Form, FormProps, Popover, PopoverProps } from 'antd'
export type PopromptProps = PopoverProps & { export type PopromptProps = PopoverProps & {
@ -13,10 +13,10 @@ export type PopromptProps = PopoverProps & {
export const Poprompt = memo<PopromptProps>(({ formProps, buttonProps, footer, children, onDone, text, ...other }) => { export const Poprompt = memo<PopromptProps>(({ formProps, buttonProps, footer, children, onDone, text, ...other }) => {
const [visible, setVisible] = useState<boolean>(false) const [visible, setVisible] = useState<boolean>(false)
const onFormFinish = (values: any) => { const onFormFinish = useCallback((values: any) => {
setVisible(false) setVisible(false)
onDone?.(values) onDone?.(values)
} }, [onDone])
return ( return (
<Popover <Popover

View File

@ -1,13 +1,14 @@
import React from 'react' import { memo, ReactElement } from 'react'
import { isURLAvailable } from '@utils/permissions' import { isURLAvailable } from '@utils/permissions'
export type PrivateContentProps = { export type PrivateContentProps = {
absolutePath: string absolutePath: string
children?: React.ReactElement<any, any> children?: ReactElement<any, any>
} }
export const PrivateContent: React.FC<PrivateContentProps> = ({ absolutePath, children = null }) => export const PrivateContent = memo<PrivateContentProps>(({ absolutePath, children = null }) =>
isURLAvailable(absolutePath) ? children : null isURLAvailable(absolutePath) ? children : null
)
export default PrivateContent export default PrivateContent

View File

@ -1,5 +1,5 @@
import { memo } from 'react' import { memo } from 'react'
import { Redirect, Route, RouteProps } from 'react-router-dom' import { Redirect, Route, RouteProps, useLocation } from 'react-router-dom'
import { getUserId } from '@utils/storage' import { getUserId } from '@utils/storage'
import { isURLAvailable } from '@utils/permissions' import { isURLAvailable } from '@utils/permissions'
@ -9,10 +9,17 @@ export type PrivateDefaultRouteProps = RouteProps & {
elseRedirect?: string elseRedirect?: string
} }
export const PrivateDefaultRoute = memo<PrivateDefaultRouteProps>(({ elseRedirect, urls, ...other }) => ( export const PrivateDefaultRoute = memo<PrivateDefaultRouteProps>(({ elseRedirect, urls, ...other }) => {
const location = useLocation()
return (
<Route {...other} path={'/'}> <Route {...other} path={'/'}>
<Redirect to={{ pathname: urls.find((url) => isURLAvailable(url)) ?? elseRedirect ?? (getUserId() ? '/access_denied' : '/login') }} /> <Redirect to={{
pathname: urls.find((url) => isURLAvailable(url)) ?? elseRedirect ?? (getUserId() ? '/access_denied' : '/login'),
state: { from: location.pathname },
}} />
</Route> </Route>
)) )
})
export default PrivateDefaultRoute export default PrivateDefaultRoute

View File

@ -3,7 +3,7 @@ import { Menu, MenuItemProps } from 'antd'
import { memo, NamedExoticComponent } from 'react' import { memo, NamedExoticComponent } from 'react'
import { isURLAvailable } from '@utils/permissions' import { isURLAvailable } from '@utils/permissions'
import { Link } from 'react-router-dom' import { Link, useLocation } from 'react-router-dom'
export type PrivateMenuItemProps = MenuItemProps & { export type PrivateMenuItemProps = MenuItemProps & {
root: string root: string
@ -16,11 +16,14 @@ export type PrivateMenuLinkProps = MenuItemProps & {
title: string title: string
} }
export const PrivateMenuItemLink = memo<PrivateMenuLinkProps>(({ root = '', path, title, ...other }) => ( export const PrivateMenuItemLink = memo<PrivateMenuLinkProps>(({ root = '', path, title, ...other }) => {
const location = useLocation()
return (
<PrivateMenuItem key={path} root={root} path={path} {...other}> <PrivateMenuItem key={path} root={root} path={path} {...other}>
<Link to={join(root, path)}>{title}</Link> <Link to={{ pathname: join(root, path), state: { from: location.pathname }}}>{title}</Link>
</PrivateMenuItem> </PrivateMenuItem>
)) )
})
export const PrivateMenuItem: NamedExoticComponent<PrivateMenuItemProps> & { export const PrivateMenuItem: NamedExoticComponent<PrivateMenuItemProps> & {
Link: NamedExoticComponent<PrivateMenuLinkProps> Link: NamedExoticComponent<PrivateMenuLinkProps>

View File

@ -14,7 +14,7 @@ export type PrivateRouteProps = RouteProps & {
} }
export const defaultRedirect = (location?: Location<unknown>) => ( export const defaultRedirect = (location?: Location<unknown>) => (
<Redirect to={{ pathname: getUserId() ? '/access_denied' : '/login', state: { from: location } }} /> <Redirect to={{ pathname: getUserId() ? '/access_denied' : '/login', state: { from: location?.pathname } }} />
) )
export const PrivateRoute = memo<PrivateRouteProps>(({ root = '', path, component, children, redirect = defaultRedirect, ...other }) => { export const PrivateRoute = memo<PrivateRouteProps>(({ root = '', path, component, children, redirect = defaultRedirect, ...other }) => {

View File

@ -1,7 +1,8 @@
import { memo } from 'react'
import { Form, Input } from 'antd' import { Form, Input } from 'antd'
import { NamePath, Rule } from 'rc-field-form/lib/interface' import { NamePath, Rule } from 'rc-field-form/lib/interface'
type EditableCellProps = { type EditableCellProps = React.DetailedHTMLProps<React.TdHTMLAttributes<HTMLTableDataCellElement>, HTMLTableDataCellElement> & {
editing?: boolean editing?: boolean
dataIndex?: NamePath dataIndex?: NamePath
input?: React.Component input?: React.Component
@ -13,7 +14,7 @@ type EditableCellProps = {
initialValue: any initialValue: any
} }
export const EditableCell = ({ export const EditableCell = memo<EditableCellProps>(({
editing, editing,
dataIndex, dataIndex,
input, input,
@ -22,8 +23,9 @@ export const EditableCell = ({
formItemRules, formItemRules,
children, children,
initialValue, initialValue,
}: EditableCellProps) => ( ...other
<td style={editing ? { padding: 0 } : undefined}> }) => (
<td style={editing ? { padding: 0 } : undefined} {...other}>
{!editing ? children : ( {!editing ? children : (
<Form.Item <Form.Item
name={dataIndex} name={dataIndex}
@ -39,4 +41,4 @@ export const EditableCell = ({
</Form.Item> </Form.Item>
)} )}
</td> </td>
) ))

View File

@ -1,6 +1,6 @@
import { Form, Table, Button, Popconfirm } from 'antd' import { Form, Table, Button, Popconfirm } from 'antd'
import { EditOutlined, SaveOutlined, PlusOutlined, CloseCircleOutlined, DeleteOutlined } from '@ant-design/icons' import { EditOutlined, SaveOutlined, PlusOutlined, CloseCircleOutlined, DeleteOutlined } from '@ant-design/icons'
import { useState, useEffect } from 'react' import { useState, useEffect, memo, useCallback } from 'react'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
@ -42,7 +42,7 @@ export const tryAddKeys = (items) => {
return items.map((item, index) => ({...item, key: item.key ?? item.id ?? index })) return items.map((item, index) => ({...item, key: item.key ?? item.id ?? index }))
} }
export const EditableTable = ({ export const EditableTable = memo(({
columns, columns,
dataSource, dataSource,
onChange, // Метод вызывается со всем dataSource с измененными элементами после любого действия onChange, // Метод вызывается со всем dataSource с измененными элементами после любого действия
@ -62,14 +62,14 @@ export const EditableTable = ({
setData(tryAddKeys(dataSource)) setData(tryAddKeys(dataSource))
}, [dataSource]) }, [dataSource])
const isEditing = (record) => record?.key === editingKey const isEditing = useCallback((record) => record?.key === editingKey, [editingKey])
const edit = (record) => { const edit = useCallback((record) => {
form.setFieldsValue({...record}) form.setFieldsValue({...record})
setEditingKey(record.key) setEditingKey(record.key)
} }, [form])
const cancel = () => { const cancel = useCallback(() => {
if (editingKey === newRowKeyValue) { if (editingKey === newRowKeyValue) {
const newData = [...data] const newData = [...data]
const index = newData.findIndex((item) => newRowKeyValue === item.key) const index = newData.findIndex((item) => newRowKeyValue === item.key)
@ -77,9 +77,9 @@ export const EditableTable = ({
setData(newData) setData(newData)
} }
setEditingKey('') setEditingKey('')
} }, [data, editingKey])
const addNewRow = async () => { const addNewRow = useCallback(async () => {
let newRow = { let newRow = {
...form.initialValues, ...form.initialValues,
key:newRowKeyValue key:newRowKeyValue
@ -88,9 +88,9 @@ export const EditableTable = ({
const newData = [newRow, ...data] const newData = [newRow, ...data]
setData(newData) setData(newData)
edit(newRow) edit(newRow)
} }, [data, edit, form.initialValues])
const save = async (record) => { const save = useCallback(async (record) => {
try { try {
const row = await form.validateFields() const row = await form.validateFields()
const newData = [...data] const newData = [...data]
@ -121,8 +121,7 @@ export const EditableTable = ({
} }
try { try {
if (onChange) onChange?.(newData)
onChange(newData)
} catch (err) { } catch (err) {
console.log('callback onChange fault:', err) console.log('callback onChange fault:', err)
} }
@ -130,9 +129,9 @@ export const EditableTable = ({
} catch (errInfo) { } catch (errInfo) {
console.log('Validate Failed:', errInfo) console.log('Validate Failed:', errInfo)
} }
} }, [data, editingKey, form, onChange, onRowAdd, onRowEdit])
const deleteRow = (record) =>{ const deleteRow = useCallback((record) => {
const newData = [...data] const newData = [...data]
const index = newData.findIndex((item) => record.key === item.key) const index = newData.findIndex((item) => record.key === item.key)
@ -140,10 +139,8 @@ export const EditableTable = ({
setData(newData) setData(newData)
onRowDelete(record) onRowDelete(record)
onChange?.(newData)
if (onChange) }, [data, onChange, onRowDelete])
onChange(newData)
}
const operationColumn = { const operationColumn = {
width: buttonsWidth ?? 82, width: buttonsWidth ?? 82,
@ -180,7 +177,7 @@ export const EditableTable = ({
), ),
} }
const handleColumn = (col) => { const handleColumn = useCallback((col) => {
if (col.children) if (col.children)
col.children = col.children.map(handleColumn) col.children = col.children.map(handleColumn)
@ -197,13 +194,13 @@ export const EditableTable = ({
input: col.input, input: col.input,
isRequired: col.isRequired, isRequired: col.isRequired,
title: col.title, title: col.title,
dataType: col.dataType, datatype: col.datatype,
formItemClass: col.formItemClass, formItemClass: col.formItemClass,
formItemRules: col.formItemRules, formItemRules: col.formItemRules,
initialValue: col.initialValue, initialValue: col.initialValue,
}), }),
} }
} }, [isEditing])
const mergedColumns = [...columns.map(handleColumn), operationColumn] const mergedColumns = [...columns.map(handleColumn), operationColumn]
@ -221,4 +218,4 @@ export const EditableTable = ({
/> />
</Form> </Form>
) )
} })

View File

@ -1,7 +1,6 @@
import { memo, useEffect, useState, ReactNode } from 'react' import { memo, useEffect, useState, ReactNode } from 'react'
import { InputNumber, Select, Table as RawTable, Tag, SelectProps, TableProps } from 'antd' import { InputNumber, Select, Table as RawTable, Tag, SelectProps, TableProps } from 'antd'
import { SelectValue } from 'antd/lib/select' import { DefaultOptionType, SelectValue } from 'antd/lib/select'
import { OptionsType } from 'rc-select/lib/interface'
import { Rule } from 'rc-field-form/lib/interface' import { Rule } from 'rc-field-form/lib/interface'
import { tryAddKeys } from './EditableTable' import { tryAddKeys } from './EditableTable'
@ -210,7 +209,7 @@ export const makeNumericAvgRange = (
export const makeSelectColumn = <T extends unknown = string>( export const makeSelectColumn = <T extends unknown = string>(
title: string, title: string,
dataIndex: string, dataIndex: string,
options: OptionsType, options: DefaultOptionType[],
defaultValue?: T, defaultValue?: T,
other?: columnPropsOther, other?: columnPropsOther,
selectOther?: SelectProps<SelectValue> selectOther?: SelectProps<SelectValue>
@ -229,7 +228,7 @@ const makeTagInput = <T extends Record<string, any>>(value_key: string, label_ke
onChange?: (values: T[]) => void, onChange?: (values: T[]) => void,
other?: SelectProps<SelectValue>, other?: SelectProps<SelectValue>,
}>(({ options, value, onChange, other }) => { }>(({ options, value, onChange, other }) => {
const [selectOptions, setSelectOptions] = useState<OptionsType>([]) const [selectOptions, setSelectOptions] = useState<DefaultOptionType[]>([])
const [selectedValue, setSelectedValue] = useState<SelectValue>([]) const [selectedValue, setSelectedValue] = useState<SelectValue>([])
useEffect(() => { useEffect(() => {
@ -307,6 +306,6 @@ export type TableContainer = TableProps<any> & {
children?: ReactNode children?: ReactNode
} }
export const Table = ({dataSource, children, ...other}: TableContainer) => ( export const Table = memo<TableContainer>(({dataSource, children, ...other}) => (
<RawTable dataSource={tryAddKeys(dataSource)} {...other}>{children}</RawTable> <RawTable dataSource={tryAddKeys(dataSource)} {...other}>{children}</RawTable>
) ))

View File

@ -1,6 +1,8 @@
export const makeNumericSorter = (key: string) => (a: any, b: any) => Number(a[key]) - Number(b[key]) import { RawDate } from "@asb/utils"
export const makeStringSorter = (key: string) => (a: any, b: any) => { export const makeNumericSorter = (key: string) => (a: Record<string, number>, b: Record<string, number>) => Number(a[key]) - Number(b[key])
export const makeStringSorter = (key: string) => (a: Record<string, string> | null | undefined, b: Record<string, string> | null | undefined) => {
if (!a && !b) return 0 if (!a && !b) return 0
if (!a) return 1 if (!a) return 1
if (!b) return -1 if (!b) return -1
@ -8,7 +10,7 @@ export const makeStringSorter = (key: string) => (a: any, b: any) => {
return ('' + a[key]).localeCompare(b[key]) return ('' + a[key]).localeCompare(b[key])
} }
export const makeDateSorter = (key: string) => (a: any, b: any) => { export const makeDateSorter = (key: string) => (a: Record<string, RawDate>, b: Record<string, RawDate>) => {
const date = new Date(a[key]) const date = new Date(a[key])
if (Number.isNaN(date.getTime())) if (Number.isNaN(date.getTime()))

View File

@ -1,4 +1,4 @@
import { memo, useState } from 'react' import { memo, useCallback, useState } from 'react'
import { Upload, Button } from 'antd' import { Upload, Button } from 'antd'
import { UploadOutlined } from '@ant-design/icons' import { UploadOutlined } from '@ant-design/icons'
@ -8,7 +8,7 @@ import { ErrorFetch } from './ErrorFetch'
export const UploadForm = memo(({ url, disabled, accept, style, formData, onUploadStart, onUploadSuccess, onUploadComplete, onUploadError }) => { export const UploadForm = memo(({ url, disabled, accept, style, formData, onUploadStart, onUploadSuccess, onUploadComplete, onUploadError }) => {
const [fileList, setfileList] = useState([]) const [fileList, setfileList] = useState([])
const handleFileSend = async () => { const handleFileSend = useCallback(async () => {
onUploadStart?.() onUploadStart?.()
try { try {
const formDataLocal = new FormData() const formDataLocal = new FormData()
@ -34,7 +34,7 @@ export const UploadForm = memo(({ url, disabled, accept, style, formData, onUplo
setfileList([]) setfileList([])
onUploadComplete?.() onUploadComplete?.()
} }
} }, [fileList, formData, onUploadComplete, onUploadError, onUploadStart, onUploadSuccess, url])
const isSendButtonEnabled = fileList.length > 0 const isSendButtonEnabled = fileList.length > 0
return( return(

View File

@ -1,5 +1,5 @@
import { memo, MouseEventHandler, useState } from 'react' import { memo, MouseEventHandler, useCallback, useState } from 'react'
import { Link, useHistory } from 'react-router-dom' import { Link, useHistory, useLocation } from 'react-router-dom'
import { Button, Dropdown, DropDownProps, Menu } from 'antd' import { Button, Dropdown, DropDownProps, Menu } from 'antd'
import { UserOutlined } from '@ant-design/icons' import { UserOutlined } from '@ant-design/icons'
@ -14,16 +14,17 @@ export const UserMenu = memo<UserMenuProps>(({ isAdmin, ...other }) => {
const [isModalVisible, setIsModalVisible] = useState<boolean>(false) const [isModalVisible, setIsModalVisible] = useState<boolean>(false)
const history = useHistory() const history = useHistory()
const location = useLocation()
const onChangePasswordClick: MouseEventHandler = (e) => { const onChangePasswordClick: MouseEventHandler = useCallback((e) => {
setIsModalVisible(true) setIsModalVisible(true)
e.preventDefault() e.preventDefault()
} }, [])
const onChangePasswordOk = () => { const onChangePasswordOk = useCallback(() => {
setIsModalVisible(false) setIsModalVisible(false)
history.push('/login') history.push({ pathname: '/login', state: { from: location.pathname }})
} }, [history, location])
return ( return (
<> <>
@ -33,15 +34,15 @@ export const UserMenu = memo<UserMenuProps>(({ isAdmin, ...other }) => {
overlay={( overlay={(
<Menu style={{ textAlign: 'right' }}> <Menu style={{ textAlign: 'right' }}>
{isAdmin ? ( {isAdmin ? (
<PrivateMenuItemLink path={'/'} title={'Вернуться на сайт'}/> <PrivateMenuItemLink key={''} path={'/'} title={'Вернуться на сайт'}/>
) : ( ) : (
<PrivateMenuItemLink path={'/admin'} title={'Панель администратора'}/> <PrivateMenuItemLink key={'admin'} path={'/admin'} title={'Панель администратора'}/>
)} )}
<Menu.Item> <Menu.Item>
<Link to={'/'} onClick={onChangePasswordClick}>Сменить пароль</Link> <Link to={'/'} onClick={onChangePasswordClick}>Сменить пароль</Link>
</Menu.Item> </Menu.Item>
<Menu.Item> <Menu.Item>
<Link to={'/login'} onClick={removeUser}>Выход</Link> <Link to={{ pathname: '/login', state: { from: location.pathname }}} onClick={removeUser}>Выход</Link>
</Menu.Item> </Menu.Item>
</Menu> </Menu>
)} )}

View File

@ -1,7 +1,10 @@
import { TreeSelect } from 'antd' import { TreeSelect } from 'antd'
import { DefaultValueType } from 'rc-tree-select/lib/interface' import { DefaultValueType } from 'rc-tree-select/lib/interface'
import { useState, useEffect, ReactNode } from 'react' import { useState, useEffect, ReactNode, useCallback, memo } from 'react'
import { useHistory, useRouteMatch } from 'react-router-dom' import { useHistory, useLocation, useRouteMatch } from 'react-router-dom'
import { RawValueType } from 'rc-tree-select/lib/TreeSelect'
import { LabelInValueType } from 'rc-select/lib/Select'
import { isRawDate } from '@utils' import { isRawDate } from '@utils'
import LoaderPortal from './LoaderPortal' import LoaderPortal from './LoaderPortal'
@ -54,11 +57,12 @@ const getLabel = (wellsTree: TreeNodeData[], value?: string): string | undefined
return value return value
} }
export const WellTreeSelector = () => { export const WellTreeSelector = memo(({ ...other }) => {
const [wellsTree, setWellsTree] = useState<any[]>([]) // TODO: Исправить тип (необходимо разобраться с типом value rc-select) const [wellsTree, setWellsTree] = useState<TreeNodeData[]>([])
const [showLoader, setShowLoader] = useState<boolean>(false) const [showLoader, setShowLoader] = useState<boolean>(false)
const [value, setValue] = useState<string>() const [value, setValue] = useState<string>()
const history = useHistory() const history = useHistory()
const location = useLocation()
const routeMatch = useRouteMatch('/:route/:id') const routeMatch = useRouteMatch('/:route/:id')
useEffect(() => { useEffect(() => {
@ -100,14 +104,15 @@ export const WellTreeSelector = () => {
setValue(getLabel(wellsTree, routeMatch?.url)) setValue(getLabel(wellsTree, routeMatch?.url))
}, [wellsTree, routeMatch]) }, [wellsTree, routeMatch])
const onChange = (value: string): void => { const onChange = useCallback((value: string): void => {
if (wellsTree) if (wellsTree)
setValue(getLabel(wellsTree, value)) setValue(getLabel(wellsTree, value))
} }, [wellsTree])
const onSelect = (value: string): void => { const onSelect = useCallback((value: RawValueType | LabelInValueType): void => {
if (value) history.push(value) if (['number', 'string'].includes(typeof value))
} history.push({ pathname: String(value), state: { from: location.pathname }})
}, [history, location])
return ( return (
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader}>
@ -123,9 +128,10 @@ export const WellTreeSelector = () => {
onChange={onChange} onChange={onChange}
value={value} value={value}
style={{ width: '350px' }} style={{ width: '350px' }}
{...other}
/> />
</LoaderPortal> </LoaderPortal>
) )
} })
export default WellTreeSelector export default WellTreeSelector

View File

@ -3,3 +3,4 @@ export type { WellIconColors, WellIconProps, WellIconState } from './WellIcon'
export { PointerIcon } from './PointerIcon' export { PointerIcon } from './PointerIcon'
export { WellIcon } from './WellIcon' export { WellIcon } from './WellIcon'
export { Loader } from './Loader'

View File

@ -1,33 +0,0 @@
import { Tooltip, Tag, Typography, Popconfirm, Button } from 'antd'
import { memo } from 'react'
import { FileMarkDto } from '@api'
import { UserView } from './UserView'
const markTypes: { [id: number]: {color: string, text: string} } = {
0: {color: 'orange', text: 'неизвестно'},
1: {color: 'green', text: 'согласовано'},
}
const { Text } = Typography
export type MarkViewProps = {
mark: FileMarkDto
onDelete?: (e?: React.MouseEvent<HTMLElement, MouseEvent>) => void
}
export const MarkView = memo<MarkViewProps>(({ mark, onDelete }) => {
const markType = markTypes[mark.idMarkType ?? 0] ?? markTypes[0]
return <Tooltip title={<UserView user={mark.user}/>}>
<Tag color={markType.color}>
<Text delete={mark?.isDeleted}>
{`${markType.text} ${new Date(mark.dateCreated ?? 0).toLocaleString()}`}
</Text>
{!mark?.isDeleted && (
<Popconfirm title='Отозвать согласование?' onConfirm={onDelete}>
<Button type='link'>x</Button>
</Popconfirm>
)}
</Tag>
</Tooltip>
})

View File

@ -10,10 +10,12 @@ export type RoleViewProps = {
} }
export const RoleView = memo<RoleViewProps>(({ role }) => { export const RoleView = memo<RoleViewProps>(({ role }) => {
if (!role) return ( <Tooltip title={'нет данных'}>-</Tooltip> )
const hasIncludedRoles = (role?.roles?.length && role.roles.length > 0) ? 1 : 0 const hasIncludedRoles = (role?.roles?.length && role.roles.length > 0) ? 1 : 0
const hasPermissions = (role?.permissions?.length && role.permissions.length > 0) ? 1 : 0 const hasPermissions = (role?.permissions?.length && role.permissions.length > 0) ? 1 : 0
return role ? ( return (
<Tooltip <Tooltip
overlayInnerStyle={{ width: '400px' }} overlayInnerStyle={{ width: '400px' }}
title={ title={
@ -54,7 +56,5 @@ export const RoleView = memo<RoleViewProps>(({ role }) => {
> >
{role.caption} {role.caption}
</Tooltip> </Tooltip>
) : (
<Tooltip title={'нет данных'}>-</Tooltip>
) )
}) })

View File

@ -1,13 +1,11 @@
export type { PermissionViewProps } from './PermissionView' export type { PermissionViewProps } from './PermissionView'
export type { TelemetryViewProps } from './TelemetryView' export type { TelemetryViewProps } from './TelemetryView'
export type { CompanyViewProps } from './CompanyView' export type { CompanyViewProps } from './CompanyView'
export type { MarkViewProps } from './MarkView'
export type { RoleViewProps } from './RoleView' export type { RoleViewProps } from './RoleView'
export type { UserViewProps } from './UserView' export type { UserViewProps } from './UserView'
export { PermissionView } from './PermissionView' export { PermissionView } from './PermissionView'
export { TelemetryView, getTelemetryLabel } from './TelemetryView' export { TelemetryView, getTelemetryLabel } from './TelemetryView'
export { CompanyView } from './CompanyView' export { CompanyView } from './CompanyView'
export { MarkView } from './MarkView'
export { RoleView } from './RoleView' export { RoleView } from './RoleView'
export { UserView } from './UserView' export { UserView } from './UserView'

View File

@ -1,17 +1,17 @@
import React from 'react'; import React from 'react'
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom'
import './styles/index.css'; import App from './App'
import App from './App'; import reportWebVitals from './reportWebVitals'
import reportWebVitals from './reportWebVitals';
ReactDOM.render( import '@styles/index.css'
ReactDOM.render((
<React.StrictMode> <React.StrictMode>
<App /> <App />
</React.StrictMode>, </React.StrictMode>
document.getElementById('root') ), document.getElementById('root'))
);
// If you want to start measuring performance in your app, pass a function // If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log)) // to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals(); reportWebVitals()

View File

@ -1,5 +1,5 @@
import { Link } from 'react-router-dom' import { Link, useLocation } from 'react-router-dom'
import { useState, useEffect } from 'react' import { useState, useEffect, memo } from 'react'
import { Tag, Button, Modal } from 'antd' import { Tag, Button, Modal } from 'antd'
import { LineChartOutlined, ProfileOutlined } from '@ant-design/icons' import { LineChartOutlined, ProfileOutlined } from '@ant-design/icons'
@ -35,7 +35,9 @@ const filtersWellsType = []
const DAY_IN_MS = 86_400_000 const DAY_IN_MS = 86_400_000
const ONLINE_DEADTIME = 600_000 const ONLINE_DEADTIME = 600_000
export const ClusterWells = ({ statsWells }) => { const getDate = (str) => isRawDate(str) ? new Date(str).toLocaleString() : '-'
export const ClusterWells = memo(({ statsWells }) => {
const [selectedWellId, setSelectedWellId] = useState(0) const [selectedWellId, setSelectedWellId] = useState(0)
const [isTVDModalVisible, setIsTVDModalVisible] = useState(false) const [isTVDModalVisible, setIsTVDModalVisible] = useState(false)
const [isOpsModalVisible, setIsOpsModalVisible] = useState(false) const [isOpsModalVisible, setIsOpsModalVisible] = useState(false)
@ -43,6 +45,8 @@ export const ClusterWells = ({ statsWells }) => {
const [tableData, setTableData] = useState([]) const [tableData, setTableData] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const location = useLocation()
useEffect(() => { useEffect(() => {
if (!isOpsModalVisible || selectedWellId <= 0) { if (!isOpsModalVisible || selectedWellId <= 0) {
setWellOperations([]) setWellOperations([])
@ -104,12 +108,10 @@ export const ClusterWells = ({ statsWells }) => {
setTableData(data) setTableData(data)
}, [statsWells]) }, [statsWells])
const getDate = (str) => isRawDate(str) ? new Date(str).toLocaleString() : '-'
const columns = [ const columns = [
makeTextColumn('скв №', 'caption', null, null, makeTextColumn('скв №', 'caption', null, null,
(_, item) => ( (_, item) => (
<Link to={`/well/${item.id}`} style={{display: 'flex', alignItems: 'center'}}> <Link to={{ pathname: `/well/${item.id}`, state: { from: location.pathname }}} style={{display: 'flex', alignItems: 'center'}}>
<PointerIcon <PointerIcon
state={item.idState === 1 ? 'active' : 'unknown'} state={item.idState === 1 ? 'active' : 'unknown'}
width={32} width={32}
@ -195,6 +197,6 @@ export const ClusterWells = ({ statsWells }) => {
</Modal> </Modal>
</> </>
) )
} })
export default ClusterWells export default ClusterWells

View File

@ -1,5 +1,5 @@
import { Map, Overlay } from 'pigeon-maps' import { Map, Overlay } from 'pigeon-maps'
import { Link } from 'react-router-dom' import { Link, useLocation } from 'react-router-dom'
import { useState, useEffect, memo } from 'react' import { useState, useEffect, memo } from 'react'
import { ClusterService } from '@api' import { ClusterService } from '@api'
@ -41,6 +41,8 @@ export const Deposit = memo(() => {
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [viewParams, setViewParams] = useState(defaultViewParams) const [viewParams, setViewParams] = useState(defaultViewParams)
const location = useLocation()
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => invokeWebApiWrapperAsync(
async () => { async () => {
const data = await ClusterService.getClusters() const data = await ClusterService.getClusters()
@ -62,7 +64,7 @@ export const Deposit = memo(() => {
anchor={[cluster.latitude, cluster.longitude]} anchor={[cluster.latitude, cluster.longitude]}
key={`${cluster.latitude} ${cluster.longitude}`} key={`${cluster.latitude} ${cluster.longitude}`}
> >
<Link to={`/cluster/${cluster.id}`}> <Link to={{ pathname: `/cluster/${cluster.id}`, state: { from: location.pathname }}}>
<PointerIcon state={'active'} width={48} height={59} /> <PointerIcon state={'active'} width={48} height={59} />
<span>{cluster.caption}</span> <span>{cluster.caption}</span>
</Link> </Link>

View File

@ -1,4 +1,4 @@
import { Button, Layout, Menu } from 'antd' import { Button, Layout } from 'antd'
import { import {
CheckOutlined, CheckOutlined,
CloseOutlined, CloseOutlined,

View File

@ -1,13 +1,13 @@
import { memo, useState } from 'react' import { memo, useCallback, useState } from 'react'
import { Link, useHistory } from 'react-router-dom' import { Link, useHistory, useLocation } from 'react-router-dom'
import { Card, Form, Input, Button } from 'antd' import { Card, Form, Input, Button } from 'antd'
import { UserOutlined, LockOutlined } from '@ant-design/icons' import { UserOutlined, LockOutlined } from '@ant-design/icons'
import { AuthService } from '@api'
import { setUser } from '@utils/storage'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { loginRules, passwordRules } from '@utils/validationRules' import { loginRules, passwordRules } from '@utils/validationRules'
import { setUser } from '@utils/storage'
import { AuthService } from '@api'
import '@styles/index.css' import '@styles/index.css'
import Logo from '@images/Logo' import Logo from '@images/Logo'
@ -16,19 +16,21 @@ const logoIcon = <Logo width={130} />
export const Login = memo(() => { export const Login = memo(() => {
const history = useHistory() const history = useHistory()
const location = useLocation()
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const handleLogin = (formData) => invokeWebApiWrapperAsync( const handleLogin = useCallback((formData) => invokeWebApiWrapperAsync(
async () => { async () => {
const user = await AuthService.login(formData) const user = await AuthService.login(formData)
if (!user) throw Error('Неправильный логин или пароль') if (!user) throw Error('Неправильный логин или пароль')
setUser(user) setUser(user)
history.push('well') console.log(location.state?.from)
history.push(location.state?.from ?? 'well')
}, },
setShowLoader, setShowLoader,
(ex) => ex?.message ?? 'Ошибка входа', (ex) => ex?.message ?? 'Ошибка входа',
'Вход в систему' 'Вход в систему'
) ), [history, location])
return ( return (
<LoaderPortal show={showLoader} className={'loader-container login_page shadow'}> <LoaderPortal show={showLoader} className={'loader-container login_page shadow'}>

View File

@ -1,4 +1,4 @@
import { memo, useState } from 'react' import { memo, useCallback, useState } from 'react'
import { Link, useHistory } from 'react-router-dom' import { Link, useHistory } from 'react-router-dom'
import { Card, Form, Input, Button } from 'antd' import { Card, Form, Input, Button } from 'antd'
import { import {
@ -54,7 +54,7 @@ export const Register = memo(() => {
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const history = useHistory() const history = useHistory()
const handleRegister = (formData) => invokeWebApiWrapperAsync( const handleRegister = useCallback((formData) => invokeWebApiWrapperAsync(
async () => { async () => {
await AuthService.register(formData) await AuthService.register(formData)
history.push('/login') history.push('/login')
@ -62,7 +62,7 @@ export const Register = memo(() => {
setShowLoader, setShowLoader,
`Ошибка отправки заявки на регистрацию`, `Ошибка отправки заявки на регистрацию`,
'Отправка заявки на регистрацию' 'Отправка заявки на регистрацию'
) ), [history])
return ( return (
<LoaderPortal show={showLoader} className={'loader-container login_page shadow'}> <LoaderPortal show={showLoader} className={'loader-container login_page shadow'}>

View File

@ -1,5 +1,5 @@
import { Link } from 'react-router-dom' import { Link, useLocation } from 'react-router-dom'
import { useState, useEffect, memo } from 'react' import { useState, useEffect, memo, useCallback } from 'react'
import { LineChartOutlined, ProfileOutlined } from '@ant-design/icons' import { LineChartOutlined, ProfileOutlined } from '@ant-design/icons'
import { Table, Tag, Button, Badge, Divider, Modal, Row, Col, Popconfirm } from 'antd' import { Table, Tag, Button, Badge, Divider, Modal, Row, Col, Popconfirm } from 'antd'
@ -42,6 +42,8 @@ export const WellCompositeSections = memo(({ idWell, statsWells, selectedSection
const [isOpsModalVisible, setIsOpsModalVisible] = useState(false) const [isOpsModalVisible, setIsOpsModalVisible] = useState(false)
const [isParamsModalVisible, setIsParamsModalVisible] = useState(false) const [isParamsModalVisible, setIsParamsModalVisible] = useState(false)
const location = useLocation()
useEffect(() => (async() => setParamsColumns(await getColumns(idWell)))(), [idWell]) useEffect(() => (async() => setParamsColumns(await getColumns(idWell)))(), [idWell])
useEffect(() => { useEffect(() => {
@ -125,7 +127,7 @@ export const WellCompositeSections = memo(({ idWell, statsWells, selectedSection
const columns = [ const columns = [
makeTextColumn('скв №', 'caption', null, null, makeTextColumn('скв №', 'caption', null, null,
(text, item) => <Link to={`/well/${item?.id}`}>{text ?? '-'}</Link> (text, item) => <Link to={{ pathname: `/well/${item?.id}`, state: { from: location.pathname }}}>{text ?? '-'}</Link>
), ),
makeTextColumn('Секция', 'sectionType', filtersSectionsType, null, (text) => text ?? '-'), makeTextColumn('Секция', 'sectionType', filtersSectionsType, null, (text) => text ?? '-'),
makeNumericColumnPlanFact('Глубина, м', 'sectionWellDepth', filtersMinMax, makeFilterMinMaxFunction), makeNumericColumnPlanFact('Глубина, м', 'sectionWellDepth', filtersMinMax, makeFilterMinMaxFunction),
@ -187,7 +189,7 @@ export const WellCompositeSections = memo(({ idWell, statsWells, selectedSection
) )
} }
const onParamButtonClick = () => invokeWebApiWrapperAsync( const onParamButtonClick = useCallback(() => invokeWebApiWrapperAsync(
async () => { async () => {
setIsParamsModalVisible(true) setIsParamsModalVisible(true)
const params = await DrillParamsService.getCompositeAll(idWell) const params = await DrillParamsService.getCompositeAll(idWell)
@ -196,9 +198,9 @@ export const WellCompositeSections = memo(({ idWell, statsWells, selectedSection
setShowParamsLoader, setShowParamsLoader,
`Не удалось загрузить список режимов для скважины "${idWell}"`, `Не удалось загрузить список режимов для скважины "${idWell}"`,
'Получение списка режимов скважины' 'Получение списка режимов скважины'
) ), [idWell])
const onParamsAddClick = () => invokeWebApiWrapperAsync( const onParamsAddClick = useCallback(() => invokeWebApiWrapperAsync(
async () => { async () => {
await DrillParamsService.save(idWell, params) await DrillParamsService.save(idWell, params)
setIsParamsModalVisible(false) setIsParamsModalVisible(false)
@ -206,7 +208,7 @@ export const WellCompositeSections = memo(({ idWell, statsWells, selectedSection
setShowLoader, setShowLoader,
`Не удалось добавить режимы в список скважины "${idWell}"`, `Не удалось добавить режимы в список скважины "${idWell}"`,
'Добавление режима скважины' 'Добавление режима скважины'
) ), [idWell, params])
return ( return (
<> <>

View File

@ -1,6 +1,6 @@
import { memo } from 'react' import { memo, useCallback } from 'react'
import { Layout, Menu } from 'antd' import { Layout, Menu } from 'antd'
import { Switch, useParams, useHistory } from 'react-router-dom' import { Switch, useParams, useHistory, useLocation } from 'react-router-dom'
import { import {
BarChartOutlined, BarChartOutlined,
BuildOutlined, BuildOutlined,
@ -23,9 +23,12 @@ const { Content } = Layout
export const WellOperations = memo(({ idWell }) => { export const WellOperations = memo(({ idWell }) => {
const { tab } = useParams() const { tab } = useParams()
const history = useHistory() const history = useHistory()
const location = useLocation()
const rootPath = `/well/${idWell}/operations` const rootPath = `/well/${idWell}/operations`
const onImported = () => history.push(`${rootPath}`) const onImported = useCallback(() =>
history.push({ pathname: `${rootPath}`, state: { from: location.pathname }})
, [history, location, rootPath])
const isIEBarDisabled = !['plan', 'fact'].includes(tab) const isIEBarDisabled = !['plan', 'fact'].includes(tab)

View File

@ -1,13 +1,13 @@
const reportWebVitals = onPerfEntry => { export const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) { if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry); getCLS(onPerfEntry)
getFID(onPerfEntry); getFID(onPerfEntry)
getFCP(onPerfEntry); getFCP(onPerfEntry)
getLCP(onPerfEntry); getLCP(onPerfEntry)
getTTFB(onPerfEntry); getTTFB(onPerfEntry)
}); })
}
} }
};
export default reportWebVitals; export default reportWebVitals