* Добавлено управление столбцами таблиц

* Сменён цвет фона страницы входа
* Лого вынесено вне формы входа
This commit is contained in:
Александр Сироткин 2022-03-02 21:17:27 +05:00
parent 79ca654d45
commit 0c092aa138
11 changed files with 295 additions and 50 deletions

View File

@ -1,9 +1,10 @@
import { Form, Table, Button, Popconfirm } from 'antd' import { memo, useCallback, useState, useEffect } from 'react'
import { Form, 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, memo, useCallback } from 'react'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { Table } from '.'
import { EditableCell } from './EditableCell' import { EditableCell } from './EditableCell'
const newRowKeyValue = 'newRow' const newRowKeyValue = 'newRow'

View File

@ -0,0 +1,57 @@
import { memo, useCallback, useEffect, useState, ReactNode } from 'react'
import { Table as RawTable, TableProps } from 'antd'
import { ColumnGroupType, ColumnType } from 'antd/lib/table'
import { OmitExtends } from '@utils'
import { getTableSettings, setTableSettings } from '@utils/storage'
import { applySettings, ColumnSettings, TableSettings } from '@utils/table_settings'
import TableSettingsChanger from './TableSettingsChanger'
import { tryAddKeys } from './EditableTable'
export type BaseTableColumn<T = any> = ColumnGroupType<T> | ColumnType<T>
export type TableColumns<T = any> = OmitExtends<BaseTableColumn<T>, ColumnSettings>[]
export type TableContainer = TableProps<any> & {
columns: TableColumns
dataSource: any[]
children?: ReactNode
tableName?: string
showSettingsChanger?: boolean
}
export const Table = memo<TableContainer>(({ columns, dataSource, children, tableName, showSettingsChanger, ...other }) => {
const [newColumns, setNewColumns] = useState<TableColumns>([])
const [settings, setSettings] = useState<TableSettings>({})
const onSettingsChanged = useCallback((settings?: TableSettings | null) => {
if (tableName)
setTableSettings(tableName, settings)
setSettings(settings ?? {})
}, [tableName])
useEffect(() => setSettings(tableName ? getTableSettings(tableName) : {}), [tableName])
useEffect(() => setNewColumns(() => {
const newColumns = applySettings(columns, settings)
if (tableName && showSettingsChanger) {
const oldTitle = newColumns[0].title
newColumns[0].title = (props) => (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'stretch', justifyContent: 'space-between', position: 'relative', padding: '16px 0' }}>
<TableSettingsChanger columns={columns} settings={settings} onChange={onSettingsChanged}/>
<div>
{typeof oldTitle === 'function' ? oldTitle(props) : oldTitle}
</div>
</div>
)
}
return newColumns
}), [settings, columns, onSettingsChanged, showSettingsChanger, tableName])
return (
<>
<RawTable columns={newColumns} dataSource={tryAddKeys(dataSource)} {...other}>{children}</RawTable>
</>
)
})
export default Table

View File

@ -0,0 +1,96 @@
import { memo, useCallback, useEffect, useState } from 'react'
import { ColumnsType } from 'antd/lib/table'
import { Button, Modal, Switch, Table } from 'antd'
import { SettingOutlined } from '@ant-design/icons'
import { ColumnSettings, makeSettings, mergeSettings, TableSettings } from '@utils/table_settings'
import { TableColumns } from './Table'
import { makeColumn } from '.'
const parseSettings = (columns?: TableColumns, settings?: TableSettings | null): ColumnSettings[] => {
const newSettings = mergeSettings(makeSettings(columns ?? []), settings ?? {})
return Object.values(newSettings).map((set, i) => ({ ...set, key: i }))
}
const unparseSettings = (columns: ColumnSettings[]): TableSettings =>
Object.fromEntries(columns.map((column) => [column.columnName, column]))
export type TableSettingsChangerProps = {
title?: string
columns?: TableColumns
settings?: TableSettings | null
onChange: (settings: TableSettings | null) => void
}
export const TableSettingsChanger = memo<TableSettingsChangerProps>(({ title, columns, settings, onChange }) => {
const [visible, setVisible] = useState<boolean>(false)
const [newSettings, setNewSettings] = useState<ColumnSettings[]>(parseSettings(columns, settings))
const [tableColumns, setTableColumns] = useState<ColumnsType<ColumnSettings>>([])
const onVisibilityChange = useCallback((index: number, visible: boolean) => {
setNewSettings((oldSettings) => {
const newSettings = [...oldSettings]
newSettings[index].visible = visible
return newSettings
})
}, [])
const toogleAll = useCallback((show: boolean) => {
setNewSettings((oldSettings) => oldSettings.map((column) => {
column.visible = show
return column
}))
}, [])
useEffect(() => {
setTableColumns([
makeColumn('Название', 'title'),
makeColumn(null, 'visible', {
title: () => (
<>
Показать
<Button type={'link'} onClick={() => toogleAll(true)}>Показать все</Button>
</>
),
render: (visible: boolean, _?: ColumnSettings, index: number = NaN) => (
<Switch
checked={visible}
checkedChildren={'Отображён'}
unCheckedChildren={'Скрыт'}
onChange={(visible) => onVisibilityChange(index, visible)}
/>
)
}),
])
}, [toogleAll, onVisibilityChange])
useEffect(() => setNewSettings(parseSettings(columns, settings)), [columns, settings])
const onModalOk = useCallback(() => {
onChange(unparseSettings(newSettings))
setVisible(false)
}, [newSettings, onChange])
const onModalCancel = useCallback(() => {
setNewSettings(parseSettings(columns, settings))
setVisible(false)
}, [columns, settings])
return (
<>
<Modal
centered
visible={visible}
onCancel={onModalCancel}
onOk={onModalOk}
title={title ?? 'Настройка отображения таблицы'}
width={1000}
>
<Table columns={tableColumns} dataSource={newSettings} />
</Modal>
<Button size={'small'} style={{ position: 'absolute', left: 0, top: 0, opacity: .5 }} type={'link'} onClick={() => setVisible(true)} icon={<SettingOutlined />}/>
</>
)
})
export default TableSettingsChanger

View File

@ -1,15 +1,18 @@
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, Tag, SelectProps } from 'antd'
import { DefaultOptionType, SelectValue } from 'antd/lib/select'
import { ColumnProps } from 'antd/lib/table' import { ColumnProps } from 'antd/lib/table'
import { DefaultOptionType, SelectValue } from 'antd/lib/select'
import { Rule } from 'rc-field-form/lib/interface' import { Rule } from 'rc-field-form/lib/interface'
import { tryAddKeys } from './EditableTable' import { SimpleTimezoneDto } from '@api'
import { makeNumericSorter, makeStringSorter } from './sorters' import { makeNumericSorter, makeStringSorter } from './sorters'
export { makeDateSorter, makeNumericSorter, makeStringSorter } from './sorters' export { makeDateSorter, makeNumericSorter, makeStringSorter } from './sorters'
export { EditableTable, makeActionHandler } from './EditableTable' export { EditableTable, makeActionHandler } from './EditableTable'
export { DatePickerWrapper } from './DatePickerWrapper' export { DatePickerWrapper } from './DatePickerWrapper'
export { Table } from './Table'
export type { BaseTableColumn, TableColumns, TableContainer } from './Table'
export const RegExpIsFloat = /^[-+]?\d+\.?\d*$/ export const RegExpIsFloat = /^[-+]?\d+\.?\d*$/
@ -302,15 +305,6 @@ export const makePaginationObject = (сontainer: PaginationContainer, ...other:
current: 1 + Math.floor((сontainer.skip ?? 0) / (сontainer.take ?? 1)) current: 1 + Math.floor((сontainer.skip ?? 0) / (сontainer.take ?? 1))
}) })
export type TableContainer = TableProps<any> & {
dataSource: any[]
children?: ReactNode
}
export const Table = memo<TableContainer>(({dataSource, children, ...other}) => (
<RawTable dataSource={tryAddKeys(dataSource)} {...other}>{children}</RawTable>
))
const rawTimezones = { const rawTimezones = {
'Калининград': 2, 'Калининград': 2,
'Москва': 3, 'Москва': 3,
@ -337,11 +331,7 @@ const timezoneOptions = Object
value: id, value: id,
})) }))
export type TimezoneSelectProps = SelectProps & { export const TimezoneSelect = memo<SelectProps>(({ onChange, ...other }) => {
//
}
export const TimezoneSelect = memo<TimezoneSelectProps>(({ onChange, ...other }) => {
const [id, setId] = useState<keyof typeof rawTimezones | null>(null) const [id, setId] = useState<keyof typeof rawTimezones | null>(null)
useEffect(() => onChange?.({ useEffect(() => onChange?.({
@ -353,6 +343,12 @@ export const TimezoneSelect = memo<TimezoneSelectProps>(({ onChange, ...other })
return (<Select {...other} onChange={setId} value={id} />) return (<Select {...other} onChange={setId} value={id} />)
}) })
export const makeTimezoneRenderer = () => (timezone?: SimpleTimezoneDto) => {
if (!timezone) return 'UTC~?? :: Неизвестно'
const { hours, timezoneId } = timezone
return `UTC${hours && hours > 0 ? '+':''}${hours ? ('0' + hours).slice(-2) : '~??'} :: ${timezoneId ?? 'Неизвестно'}`
}
export const makeTimezoneColumn = ( export const makeTimezoneColumn = (
title: string = 'Зона', title: string = 'Зона',
key: string = 'timezone', key: string = 'timezone',
@ -362,11 +358,7 @@ export const makeTimezoneColumn = (
) => makeColumn(title, key, { ) => makeColumn(title, key, {
width: 100, width: 100,
editable: true, editable: true,
render: (timezone) => { render: makeTimezoneRenderer(),
if (!timezone) return 'UTC~?? :: Неизвестно'
const { hours, timezoneId } = timezone
return `UTC${hours > 0 ? '+':''}${hours ? ('0' + hours).slice(-2) : '~??'} :: ${timezoneId ?? 'Неизвестно'}`
},
input: ( input: (
<TimezoneSelect <TimezoneSelect
allowClear={allowClear} allowClear={allowClear}

View File

@ -1,11 +1,12 @@
import { Button, Layout } from 'antd' import { Button, Layout } from 'antd'
import { import {
AuditOutlined,
CheckOutlined, CheckOutlined,
CloseOutlined, CloseOutlined,
FileWordOutlined, FileWordOutlined,
LoadingOutlined, LoadingOutlined,
ReloadOutlined, ReloadOutlined,
WarningOutlined WarningOutlined,
} from '@ant-design/icons' } from '@ant-design/icons'
import { memo, useCallback, useEffect, useState } from 'react' import { memo, useCallback, useEffect, useState } from 'react'
@ -30,7 +31,7 @@ const idStateUnknown = -1
const stateString = { const stateString = {
[idStateNotInitialized]: { icon: CloseOutlined, text: 'Не настроена' }, [idStateNotInitialized]: { icon: CloseOutlined, text: 'Не настроена' },
[idStateApproving]: { icon: LoadingOutlined, text: 'Согласовывается' }, [idStateApproving]: { icon: AuditOutlined, text: 'Согласовывается' },
[idStateCreating]: { icon: LoadingOutlined, text: 'Формируется' }, [idStateCreating]: { icon: LoadingOutlined, text: 'Формируется' },
[idStateReady]: { icon: CheckOutlined, text: 'Сформирована' }, [idStateReady]: { icon: CheckOutlined, text: 'Сформирована' },
[idStateError]: { icon: WarningOutlined, text: 'Ошибка формирования' }, [idStateError]: { icon: WarningOutlined, text: 'Ошибка формирования' },

View File

@ -12,8 +12,6 @@ import { AuthService } from '@api'
import '@styles/index.css' import '@styles/index.css'
import Logo from '@images/Logo' import Logo from '@images/Logo'
const logoIcon = <Logo width={130} />
export const Login = memo(() => { export const Login = memo(() => {
const history = useHistory() const history = useHistory()
const location = useLocation() const location = useLocation()
@ -34,7 +32,9 @@ export const Login = memo(() => {
return ( return (
<LoaderPortal show={showLoader} className={'loader-container login_page shadow'}> <LoaderPortal show={showLoader} className={'loader-container login_page shadow'}>
<Card title={'Система мониторинга'} className={'shadow'} bordered={true} style={{ width: 350 }} extra={logoIcon}> <div style={{ display: 'flex', flexDirection: 'column' }}>
<Logo style={{ marginBottom: '10px' }}/>
<Card title={'Система мониторинга'} className={'shadow'} bordered={true} style={{ width: 350 }}>
<Form onFinish={handleLogin}> <Form onFinish={handleLogin}>
<Form.Item name={'login'} rules={loginRules}> <Form.Item name={'login'} rules={loginRules}>
<Input placeholder={'Пользователь'} prefix={<UserOutlined />} /> <Input placeholder={'Пользователь'} prefix={<UserOutlined />} />
@ -52,6 +52,7 @@ export const Login = memo(() => {
</div> </div>
</Form> </Form>
</Card> </Card>
</div>
</LoaderPortal> </LoaderPortal>
) )
}) })

View File

@ -1,5 +1,5 @@
import { memo, useCallback } from 'react' import { memo, useCallback } from 'react'
import { Layout, Menu, Popover } from 'antd' import { Layout, Menu } from 'antd'
import { Switch, useParams, useHistory, useLocation } from 'react-router-dom' import { Switch, useParams, useHistory, useLocation } from 'react-router-dom'
import { import {
BarChartOutlined, BarChartOutlined,

View File

@ -29,14 +29,15 @@ html {
margin-left: 30px; margin-left: 30px;
} }
.login_page{ .login_page {
position: absolute; position: absolute;
height:100%; height: 100%;
width:100%; width: 100%;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: 24px; padding: 24px;
background-color: #9d9d9d;
} }
.shadow{ .shadow{

View File

@ -9,3 +9,10 @@ export const mainFrameSize = () => ({
}) })
export const arrayOrDefault = <T extends unknown>(arr?: unknown, def: T[] = []): T[] => Array.isArray(arr) ? arr : def export const arrayOrDefault = <T extends unknown>(arr?: unknown, def: T[] = []): T[] => Array.isArray(arr) ? arr : def
/**
* Объединить типы, исключив совпадающие поля справа
* @param T Тип, передаваемый полностью
* @param R Аддитивный тип
*/
export type OmitExtends<T, R> = T & Omit<R, keyof T>

View File

@ -1,5 +1,6 @@
import { OpenAPI, UserTokenDto } from '@api' import { OpenAPI, UserTokenDto } from '@api'
import { Role, Permission } from './permissions' import { Role, Permission } from './permissions'
import { normalizeColumn, optimizeColumn, TableSettings, TableSettingsStore } from './table_settings'
export enum StorageNames { export enum StorageNames {
userId = 'userId', userId = 'userId',
@ -7,6 +8,7 @@ export enum StorageNames {
login = 'login', login = 'login',
permissions = 'permissions', permissions = 'permissions',
roles = 'roles', roles = 'roles',
tableSettings = 'tableSettings',
} }
export const getArrayFromLocalStorage = <T extends string = string>(name: string, sep: string | RegExp = ','): T[] | null => { export const getArrayFromLocalStorage = <T extends string = string>(name: string, sep: string | RegExp = ','): T[] | null => {
@ -39,3 +41,33 @@ export const removeUser = () => {
localStorage.removeItem(StorageNames.permissions) localStorage.removeItem(StorageNames.permissions)
localStorage.removeItem(StorageNames.roles) localStorage.removeItem(StorageNames.roles)
} }
export const getTableSettings = (tableName: string): TableSettings => {
const tablesJSON = localStorage.getItem(StorageNames.tableSettings)
if (!tablesJSON) return {}
try {
const tables: TableSettingsStore = JSON.parse(tablesJSON)
if (tableName in tables) {
const columns = tables[tableName] ?? {}
for (const [name, column] of Object.entries(columns))
columns[name] = normalizeColumn(column, name)
return columns
}
} catch {}
return {}
}
export const setTableSettings = (tableName: string, settings?: TableSettings | null): boolean => {
const currentJSON = localStorage.getItem(StorageNames.tableSettings)
try {
const currentStore: TableSettingsStore = currentJSON ? JSON.parse(currentJSON) : {}
const newSettings = settings ?? null
if (newSettings)
for (const [name, column] of Object.entries(newSettings))
newSettings[name] = optimizeColumn(column)
currentStore[tableName] = newSettings
localStorage.setItem(StorageNames.tableSettings, JSON.stringify(currentStore))
return true
} catch {}
return false
}

View File

@ -0,0 +1,57 @@
import { TableColumns } from '@components/Table'
export type ColumnSettings = {
columnName?: string
title?: string
visible?: boolean
}
export type TableSettings = Record<string, ColumnSettings>
export type TableSettingsStore = Record<string, TableSettings | null>
export const makeSettings = (columns: TableColumns): TableSettings => {
const settings: TableSettings = {}
columns.forEach((column) => {
if (!column.key) return
const key = String(column.key)
settings[key] = {
columnName: key,
title: typeof column.title === 'string' ? column.title : key,
visible: column.visible ?? true,
}
})
return settings
}
export const mergeSettings = (...settings: TableSettings[]): TableSettings => {
const newSettings: TableSettings = {}
for (const setting of settings) {
for (const [name, column] of Object.entries(setting)) {
newSettings[name] = {
...newSettings[name],
...column,
}
}
}
return newSettings
}
export const normalizeColumn = (column: ColumnSettings, name?: string): ColumnSettings => ({
...column,
columnName: column.columnName ?? name,
visible: column.visible ?? true,
})
export const optimizeColumn = (column: ColumnSettings): ColumnSettings => ({
...column,
visible: column.visible ?? true,
})
export const applySettings = (columns: TableColumns, settings: TableSettings): TableColumns => {
let newColumns: TableColumns = columns.map((column) => ({ ...column }))
newColumns = newColumns.filter((column) => {
const name = String(column.key)
return !(name in settings) || settings[name]?.visible
})
return newColumns
}