На странице мониторинга добавлена "Рекомендация установок"

This commit is contained in:
goodmice 2021-11-29 16:49:22 +05:00
parent 43a01bd57f
commit f60451eb4b
12 changed files with 300 additions and 118 deletions

View File

@ -1,33 +0,0 @@
import { Layout, Button } from 'antd'
import { UserOutlined } from '@ant-design/icons'
import logo from '../images/logo_32.png'
import { Link } from "react-router-dom"
import WellTreeSelector from './WellTreeSelector'
const { Header } = Layout
export default function PageHeader({title='Мониторинг', wellsList}){
const login = localStorage['login']
let handleLogout = () => {
localStorage.removeItem('login')
localStorage.removeItem('token')
}
return(
<Layout>
<Header className="header">
<Link to="/" >
<img src={logo} alt="АСБ" className="logo"/>
</Link>
<WellTreeSelector wellsList={wellsList}/>
<h1 className="title">{title}</h1>
<Link to="/login" onClick={handleLogout}>
<Button icon={<UserOutlined/>}>
({login}) Выход
</Button>
</Link>
</Header>
</Layout>
)
};

View File

@ -0,0 +1,36 @@
import { Layout, Button } from 'antd'
import { UserOutlined } from '@ant-design/icons'
import logo from '../images/logo_32.png'
import { Link } from 'react-router-dom'
import WellTreeSelector from './WellTreeSelector'
import { headerHeight } from '../utils'
const { Header } = Layout
const logoStyle = { height: headerHeight }
const handleLogout = () => {
localStorage.removeItem('login')
localStorage.removeItem('token')
}
type PageHeaderProps = { title?: string }
export const PageHeader = ({ title = 'Мониторинг' }: PageHeaderProps) => (
<Layout>
<Header className={'header'}>
<Link to={'/'} style={logoStyle}>
<img src={logo} alt={'АСБ'} className={'logo'}/>
</Link>
<WellTreeSelector />
<h1 className={'title'}>{title}</h1>
<Link to={'/login'} onClick={handleLogout}>
<Button icon={<UserOutlined/>}>
({localStorage['login']}) Выход
</Button>
</Link>
</Header>
</Layout>
)
export default PageHeader

View File

@ -19,13 +19,11 @@ type PeriodPickerProps = {
}
export const PeriodPicker = ({ defaultValue = defaultPeriod, onChange }: PeriodPickerProps) => (
<Select defaultValue={defaultValue} onChange={(value) => onChange?.(Number(value))}>
{timePeriodCollection.map(period => (
<Select.Option key={period.value} value={period.value}>
{period.label}
</Select.Option>
))}
</Select>
<Select
defaultValue={defaultValue}
onChange={(value) => onChange?.(Number(value))}
options={timePeriodCollection}
/>
)
export default PeriodPicker

View File

@ -1,38 +0,0 @@
import { Form, Input} from "antd"
export const EditableCell = ({
editing,
record,
dataIndex,
input,
isRequired,
title,
formItemClass,
formItemRules,
children,
initialValue,
}) => {
const inputNode = input ?? <Input/>
const rules = formItemRules ?? [{
required: isRequired,
message: `Please Input ${title}!`,
}]
const editor = <Form.Item
name={dataIndex}
style={{margin:0}}
className={formItemClass}
rules={rules}
initialValue={initialValue}>
{inputNode}
</Form.Item>
const tdStyle = editing
? { padding:0 }
: null
return (<td style={tdStyle}>
{editing ? editor: children}
</td>)
}

View File

@ -0,0 +1,43 @@
import { Form, Input } from 'antd'
import { NamePath, Rule } from 'rc-field-form/lib/interface'
type EditableCellProps = {
editing?: boolean
dataIndex?: NamePath
input?: React.Component
isRequired?: boolean
title: string
formItemClass?: string
formItemRules?: Rule[]
children: React.ReactNode
initialValue: any
}
export const EditableCell = ({
editing,
dataIndex,
input,
isRequired,
title,
formItemClass,
formItemRules,
children,
initialValue,
}: EditableCellProps) => (
<td style={editing ? { padding: 0 } : undefined}>
{!editing ? children : (
<Form.Item
name={dataIndex}
style={{ margin: 0 }}
className={formItemClass}
rules={formItemRules ?? [{
required: isRequired,
message: `Please Input ${title}!`,
}]}
initialValue={initialValue}
>
{input ?? <Input/>}
</Form.Item>
)}
</td>
)

View File

@ -14,13 +14,14 @@ export const tryAddKeys = (items) => {
}
export const EditableTable = ({
columns,
dataSource,
onChange, // Метод вызывается со всем dataSource с измененными элементами после любого действия
onRowAdd, // Метод вызывается с новой добавленной записью. Если метод не определен, то кнопка добавления строки не показывается
onRowEdit,// Метод вызывается с новой отредактированной записью. Если метод не поределен, то кнопка редактирования строки не показывается
onRowDelete,// Метод вызывается с удаленной записью. Если метод не поределен, то кнопка удаления строки не показывается
...otherTableProps}) => {
columns,
dataSource,
onChange, // Метод вызывается со всем dataSource с измененными элементами после любого действия
onRowAdd, // Метод вызывается с новой добавленной записью. Если метод не определен, то кнопка добавления строки не показывается
onRowEdit,// Метод вызывается с новой отредактированной записью. Если метод не поределен, то кнопка редактирования строки не показывается
onRowDelete,// Метод вызывается с удаленной записью. Если метод не поределен, то кнопка удаления строки не показывается
...otherTableProps
}) => {
const [form] = Form.useForm()
const [data, setData] = useState(tryAddKeys(dataSource))

View File

@ -10,21 +10,19 @@ export { SelectFromDictionary } from './SelectFromDictionary'
export const RegExpIsFloat = /^[-+]?\d+\.?\d*$/
export const formatDate='YYYY.MM.DD HH:mm'
export const makeNumericRender = (fixed?:number) => (value: any, row: object): ReactNode => {
const placeholder = '-'
let val = placeholder
if((value !== null) &&
(value !== undefined) &&
!Number.isNaN(value) &&
Number.isFinite(value)){
val = !!fixed
export const makeNumericRender = (fixed?: number) => (value: any, row: object): ReactNode => {
let val = '-'
if (value && Number.isFinite(+value)) {
val = !!fixed
? (+value).toFixed(fixed)
: (+value).toPrecision(5)
}
}
return (<div className='text-align-r-container'>
<span>{val}</span>
</div>)
return (
<div className={'text-align-r-container'}>
<span>{val}</span>
</div>
)
}
export const makeNumericColumnOptions = (fixed?:number, sorterKey?:string ) => ({

38
src/images/logoSmaill.svg Normal file
View File

@ -0,0 +1,38 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="1018.0595 0 8006.9226 3574.0189" version="1.1" fill="#2d2242">
<clipPath id="c">
<path d="m 5189,716 c 587,0 1063,476 1063,1063 0,587 -476,1063 -1063,1063 -588,0 -1064,-476 -1064,-1063 0,-587 476,-1063 1064,-1063 z" />
</clipPath>
<mask id="m" fill="#000">
<rect width="100%" height="100%" fill="#fff"/>
<polygon points="5166,1737 5061,1830 5089,1676"/>
<polygon points="5288,1737 5393,1830 5365,1676"/>
<polygon points="5224,1696 5285,1654 5172,1654"/>
<polygon points="5143,2007 5019,2062 5039,1952"/>
<polygon points="5310,2007 5435,2062 5415,1952"/>
<polygon points="5091,1894 5229,1962 5365,1894 5229,1783"/>
<polygon points="5052,2132 5232,2251 5412,2130 5229,2043"/>
<polygon points="5163,2297 4949,2445 4996,2184"/>
<polygon points="5292,2297 5505,2445 5458,2184"/>
<polygon points="5226,2337 5497,2523 4958,2523"/>
</mask>
<g fill="#e31e21">
<path d="M 1756,3564 H 1018 L 3236,2 3848,3 4452,0 4400,184 C 4637,66 4905,0 5189,0 5751,0 6253,261 6579,669 l -233,810 C 6213,964 5745,584 5189,584 c -528,0 -975,341 -1134,815 l -30,108 c -20,87 -31,178 -31,272 0,660 535,1195 1195,1195 318,0 607,-125 821,-327 l -220,764 c -185,88 -388,147 -601,147 -636,0 -1194,-334 -1508,-836 l -239,842 h -702 l 187,-595 H 2146 Z M 3082,2443 3703,446 2463,2444 Z"/>
<path d="m 7725,3574 c -392,-2 -748,-14 -1152,-2 L 5869,3559 6882,2 l 1790,1 -136,559 -1176,9 -121,462 h 836 c 570,93 953,697 950,1254 -3,656 -585,1291 -1300,1287 z m -995,-606 c 333,0 665,0 998,2 381,2 691,-335 693,-686 1,-291 -206,-632 -510,-673 h -824 z"/>
</g>
<g mask="url(#m)">
<polygon points="5347,1437 5105,1437 5105,1315 5347,1315"/>
<polygon points="5455,1555 4992,1555 4992,1469 5455,1469"/>
<polygon points="5597,2523 4860,2523 5027,1587 5419,1587"/>
</g>
<polygon points="5246,2523 5200,2523 5200,1735 5246,1735"/>
<g clip-path="url(#c)">
<g fill="#9d9e9e">
<path d="m 5136,177 c -688,66 -1152,378 -1415,911 l 1475,-196 783,591 z"/>
<path d="M 6684,1229 C 6401,599 5957,260 5367,182 l 659,1333 -308,931 z"/>
</g>
<path d="m 6189,3044 c 509,-466 692,-994 581,-1579 l -1059,1044 -981,-1 z"/>
<path d="m 4267,3105 c 598,345 1157,360 1681,78 L 4633,2488 4337,1552 Z"/>
<path d="m 3626,1346 c -142,676 17,1212 447,1622 l 253,-1466 798,-571 z"/>
</g>
<path fill="none" d="m 5189,716 c 587,0 1063,476 1063,1063 0,587 -476,1063 -1063,1063 -588,0 -1064,-476 -1064,-1063 0,-587 476,-1063 1064,-1063 z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
import moment from 'moment'
import { DatePicker } from 'antd'
import { useState, useEffect } from 'react'

View File

@ -0,0 +1,127 @@
import { Button, Input, Modal, Select } from 'antd'
import { useState } from 'react'
import { invokeWebApiWrapperAsync } from '../../components/factory'
import LoaderPortal from '../../components/LoaderPortal'
import PeriodPicker, { defaultPeriod } from '../../components/PeriodPicker'
import { EditableTable, makeNumericRender } from '../../components/Table'
import { SetpointsService } from '../../services/api'
export const Setpoints = ({ idWell, ...other }) => {
const [isModalShown, setIsModalShown] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [isUploading, setIsUploading] = useState(false)
const [setpointNames, setSetpointNames] = useState([])
const [setpoints, setSetpoints] = useState([])
const [comment, setComment] = useState('')
const [expirePeriod, setExpirePeriod] = useState(defaultPeriod)
const columns = [
{
title: 'Наименование установки',
dataIndex: 'name',
editable: true,
isRequired: true,
width: 200,
input: <Select options={setpointNames} />,
render: (val) => setpointNames.find((name) => name.value === val)?.label
}, {
title: 'Значение',
dataIndex: 'value',
editable: true,
isRequired: true,
width: 125,
render: makeNumericRender(7),
align: 'right'
}
]
const onOpenClick = () => invokeWebApiWrapperAsync(
async () => {
const names = await SetpointsService.getSetpointsNamesByIdWell(idWell)
if (!names) throw Error('Setpoints not found')
setSetpointNames(names.map(spn => ({
label: spn.displayName,
value: spn.name,
tooltip: spn.comment
})))
setIsModalShown(true)
},
setIsLoading,
`Не удалось загрузить список для скважины "${idWell}"`
)
const onModalOk = () => invokeWebApiWrapperAsync(
async () => {
// eslint-disable-next-line no-sequences
const setpointsObject = setpoints.reduce((obj, sp) => (obj[sp.name] = sp.value, obj), {})
const request = {
uploadDate: new Date(),
obsolescenceSec: expirePeriod,
setpoints: setpointsObject,
comment: comment
}
await SetpointsService.insert(idWell, request)
setIsModalShown(false)
},
setIsUploading,
`Не удалось отправить рекомендации по скважине "${idWell}"`
)
const onAdd = async (setpoint) => setSetpoints((prevSetpoints) => {
setpoint.key = Date.now()
prevSetpoints.push(setpoint)
return prevSetpoints
})
const onEdit = async (setpoint) => setSetpoints((prevSetpoints) => {
const idx = prevSetpoints.findIndex((val) => val.key === setpoint.key)
prevSetpoints[idx] = setpoint
return prevSetpoints
})
const onDelete = async (setpoint) => setSetpoints((prevSetpoints) => {
const idx = prevSetpoints.findIndex((val) => val.key === setpoint.key)
prevSetpoints.splice(idx, 1)
return prevSetpoints
})
return (
<div {...other}>
<Button onClick={onOpenClick} loading={isLoading}>
Рекомендовать установки
</Button>
<Modal
width={800}
title={'Рекомендация установок'}
visible={isModalShown}
onCancel={() => setIsModalShown(false)}
onOk={onModalOk}
okText={'Отправить'}
>
<LoaderPortal show={isUploading}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
Период актуальности рекомендаций:
<PeriodPicker onChange={setExpirePeriod} />
</div>
<EditableTable
bordered
columns={columns}
dataSource={setpoints}
onRowAdd={onAdd}
onRowEdit={onEdit}
onRowDelete={onDelete}
pagination={false}
style={{ margin: '10px 0' }}
/>
Комментарий:
<Input.TextArea
rows={4}
onChange={(e) => setComment(e.value)}
value={comment}
required
/>
</LoaderPortal>
</Modal>
</div>
)
}

View File

@ -6,6 +6,7 @@ import { CustomColumn } from './CustomColumn'
import ActiveMessagesOnline from './ActiveMessagesOnline'
import { ModeDisplay } from './ModeDisplay'
import { UserOfWell } from './UserOfWells'
import { Setpoints } from './Setpoints'
import LoaderPortal from '../../components/LoaderPortal'
import { Grid, GridItem, Flex } from '../../components/Grid'
@ -380,6 +381,7 @@ export default function TelemetryView({ idWell }) {
<Option value={2}>Завершено</Option>
</Select>
</div>
<Setpoints idWell={idWell} style={{ marginLeft: '1rem' }} />
<span style={{ flexGrow: 20 }}>&nbsp;</span>
<img src={isTorqueStabEnabled(dataSpin) ? MomentStabPicEnabled : MomentStabPicDisabled} style={{ marginRight: '15px' }} alt={'TorqueMaster'} />
<img src={isSpinEnabled(dataSpin) ? SpinPicEnabled : SpinPicDisabled} style={{ marginRight: '15px' }} alt={'SpinMaster'} />

9
src/utils/index.ts Normal file
View File

@ -0,0 +1,9 @@
export type { RawDate } from './DateTimeUtils'
export { isRawDate } from './DateTimeUtils'
export const headerHeight = 64
export const mainFrameSize = () => ({
width: window.innerWidth,
height: window.innerHeight - headerHeight
})