forked from ddrilling/asb_cloud_front
* Добавлено управление столбцами таблиц
* Сменён цвет фона страницы входа * Лого вынесено вне формы входа
This commit is contained in:
parent
79ca654d45
commit
0c092aa138
@ -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 { useState, useEffect, memo, useCallback } from 'react'
|
||||
|
||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||
|
||||
import { Table } from '.'
|
||||
import { EditableCell } from './EditableCell'
|
||||
|
||||
const newRowKeyValue = 'newRow'
|
||||
|
57
src/components/Table/Table.tsx
Normal file
57
src/components/Table/Table.tsx
Normal 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
|
96
src/components/Table/TableSettingsChanger.tsx
Normal file
96
src/components/Table/TableSettingsChanger.tsx
Normal 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
|
@ -1,15 +1,18 @@
|
||||
import { memo, useEffect, useState, ReactNode } from 'react'
|
||||
import { InputNumber, Select, Table as RawTable, Tag, SelectProps, TableProps } from 'antd'
|
||||
import { DefaultOptionType, SelectValue } from 'antd/lib/select'
|
||||
import { InputNumber, Select, Tag, SelectProps } from 'antd'
|
||||
import { ColumnProps } from 'antd/lib/table'
|
||||
import { DefaultOptionType, SelectValue } from 'antd/lib/select'
|
||||
import { Rule } from 'rc-field-form/lib/interface'
|
||||
|
||||
import { tryAddKeys } from './EditableTable'
|
||||
import { SimpleTimezoneDto } from '@api'
|
||||
import { makeNumericSorter, makeStringSorter } from './sorters'
|
||||
|
||||
export { makeDateSorter, makeNumericSorter, makeStringSorter } from './sorters'
|
||||
export { EditableTable, makeActionHandler } from './EditableTable'
|
||||
export { DatePickerWrapper } from './DatePickerWrapper'
|
||||
export { Table } from './Table'
|
||||
|
||||
export type { BaseTableColumn, TableColumns, TableContainer } from './Table'
|
||||
|
||||
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))
|
||||
})
|
||||
|
||||
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 = {
|
||||
'Калининград': 2,
|
||||
'Москва': 3,
|
||||
@ -337,11 +331,7 @@ const timezoneOptions = Object
|
||||
value: id,
|
||||
}))
|
||||
|
||||
export type TimezoneSelectProps = SelectProps & {
|
||||
//
|
||||
}
|
||||
|
||||
export const TimezoneSelect = memo<TimezoneSelectProps>(({ onChange, ...other }) => {
|
||||
export const TimezoneSelect = memo<SelectProps>(({ onChange, ...other }) => {
|
||||
const [id, setId] = useState<keyof typeof rawTimezones | null>(null)
|
||||
|
||||
useEffect(() => onChange?.({
|
||||
@ -353,6 +343,12 @@ export const TimezoneSelect = memo<TimezoneSelectProps>(({ onChange, ...other })
|
||||
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 = (
|
||||
title: string = 'Зона',
|
||||
key: string = 'timezone',
|
||||
@ -362,11 +358,7 @@ export const makeTimezoneColumn = (
|
||||
) => makeColumn(title, key, {
|
||||
width: 100,
|
||||
editable: true,
|
||||
render: (timezone) => {
|
||||
if (!timezone) return 'UTC~?? :: Неизвестно'
|
||||
const { hours, timezoneId } = timezone
|
||||
return `UTC${hours > 0 ? '+':''}${hours ? ('0' + hours).slice(-2) : '~??'} :: ${timezoneId ?? 'Неизвестно'}`
|
||||
},
|
||||
render: makeTimezoneRenderer(),
|
||||
input: (
|
||||
<TimezoneSelect
|
||||
allowClear={allowClear}
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { Button, Layout } from 'antd'
|
||||
import {
|
||||
AuditOutlined,
|
||||
CheckOutlined,
|
||||
CloseOutlined,
|
||||
FileWordOutlined,
|
||||
LoadingOutlined,
|
||||
ReloadOutlined,
|
||||
WarningOutlined
|
||||
WarningOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { memo, useCallback, useEffect, useState } from 'react'
|
||||
|
||||
@ -30,7 +31,7 @@ const idStateUnknown = -1
|
||||
|
||||
const stateString = {
|
||||
[idStateNotInitialized]: { icon: CloseOutlined, text: 'Не настроена' },
|
||||
[idStateApproving]: { icon: LoadingOutlined, text: 'Согласовывается' },
|
||||
[idStateApproving]: { icon: AuditOutlined, text: 'Согласовывается' },
|
||||
[idStateCreating]: { icon: LoadingOutlined, text: 'Формируется' },
|
||||
[idStateReady]: { icon: CheckOutlined, text: 'Сформирована' },
|
||||
[idStateError]: { icon: WarningOutlined, text: 'Ошибка формирования' },
|
||||
|
@ -12,8 +12,6 @@ import { AuthService } from '@api'
|
||||
import '@styles/index.css'
|
||||
import Logo from '@images/Logo'
|
||||
|
||||
const logoIcon = <Logo width={130} />
|
||||
|
||||
export const Login = memo(() => {
|
||||
const history = useHistory()
|
||||
const location = useLocation()
|
||||
@ -34,24 +32,27 @@ export const Login = memo(() => {
|
||||
|
||||
return (
|
||||
<LoaderPortal show={showLoader} className={'loader-container login_page shadow'}>
|
||||
<Card title={'Система мониторинга'} className={'shadow'} bordered={true} style={{ width: 350 }} extra={logoIcon}>
|
||||
<Form onFinish={handleLogin}>
|
||||
<Form.Item name={'login'} rules={loginRules}>
|
||||
<Input placeholder={'Пользователь'} prefix={<UserOutlined />} />
|
||||
</Form.Item>
|
||||
<Form.Item name={'password'} rules={passwordRules}>
|
||||
<Input.Password placeholder={'Пароль'} prefix={<LockOutlined />} />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<div className={'login-button'}>
|
||||
<Button type={'primary'} htmlType={'submit'}>Вход</Button>
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Logo style={{ marginBottom: '10px' }}/>
|
||||
<Card title={'Система мониторинга'} className={'shadow'} bordered={true} style={{ width: 350 }}>
|
||||
<Form onFinish={handleLogin}>
|
||||
<Form.Item name={'login'} rules={loginRules}>
|
||||
<Input placeholder={'Пользователь'} prefix={<UserOutlined />} />
|
||||
</Form.Item>
|
||||
<Form.Item name={'password'} rules={passwordRules}>
|
||||
<Input.Password placeholder={'Пароль'} prefix={<LockOutlined />} />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<div className={'login-button'}>
|
||||
<Button type={'primary'} htmlType={'submit'}>Вход</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
<div className={'text-align-center'}>
|
||||
<Link to={`/register`}>Отправить заявку на регистрацию</Link>
|
||||
</div>
|
||||
</Form.Item>
|
||||
<div className={'text-align-center'}>
|
||||
<Link to={`/register`}>Отправить заявку на регистрацию</Link>
|
||||
</div>
|
||||
</Form>
|
||||
</Card>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
</LoaderPortal>
|
||||
)
|
||||
})
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 {
|
||||
BarChartOutlined,
|
||||
|
@ -29,14 +29,15 @@ html {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
.login_page{
|
||||
.login_page {
|
||||
position: absolute;
|
||||
height:100%;
|
||||
width:100%;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 24px;
|
||||
background-color: #9d9d9d;
|
||||
}
|
||||
|
||||
.shadow{
|
||||
@ -107,7 +108,7 @@ html {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.border_small{
|
||||
.border_small{
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
|
@ -9,3 +9,10 @@ export const mainFrameSize = () => ({
|
||||
})
|
||||
|
||||
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>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { OpenAPI, UserTokenDto } from '@api'
|
||||
import { Role, Permission } from './permissions'
|
||||
import { normalizeColumn, optimizeColumn, TableSettings, TableSettingsStore } from './table_settings'
|
||||
|
||||
export enum StorageNames {
|
||||
userId = 'userId',
|
||||
@ -7,6 +8,7 @@ export enum StorageNames {
|
||||
login = 'login',
|
||||
permissions = 'permissions',
|
||||
roles = 'roles',
|
||||
tableSettings = 'tableSettings',
|
||||
}
|
||||
|
||||
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.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
|
||||
}
|
||||
|
57
src/utils/table_settings.ts
Normal file
57
src/utils/table_settings.ts
Normal 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user