Merge branch 'dev' of bitbucket.org:autodrilling/asb_cloud_front into dev

This commit is contained in:
goodmice 2022-10-03 15:59:58 +05:00
commit 01f499b85d
No known key found for this signature in database
GPG Key ID: 63EA771203189CF1
10 changed files with 396 additions and 17 deletions

View File

@ -12,7 +12,6 @@ export {
makeNumericColumnPlanFact, makeNumericColumnPlanFact,
makeNumericStartEnd, makeNumericStartEnd,
makeNumericMinMax, makeNumericMinMax,
makeNumericAvgRange
} from './numeric' } from './numeric'
export { makeColumnsPlanFact } from './plan_fact' export { makeColumnsPlanFact } from './plan_fact'
export { makeSelectColumn } from './select' export { makeSelectColumn } from './select'

View File

@ -94,18 +94,4 @@ export const makeNumericMinMax = (
makeNumericColumn('макс', dataIndex + 'Max', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Max')), makeNumericColumn('макс', dataIndex + 'Max', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Max')),
]) ])
export const makeNumericAvgRange = (
title: ReactNode,
dataIndex: string,
fixed: number,
filters: object[],
filterDelegate: (key: string | number) => any,
renderDelegate: (_: any, row: object) => any,
width: string
) => makeGroupColumn(title, [
makeNumericColumn('мин', dataIndex + 'Min', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Min')),
makeNumericColumn('сред', dataIndex + 'Avg', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Avg')),
makeNumericColumn('макс', dataIndex + 'Max', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Max'))
])
export default makeNumericColumn export default makeNumericColumn

View File

@ -20,7 +20,6 @@ export {
makeNumericColumnPlanFact, makeNumericColumnPlanFact,
makeNumericStartEnd, makeNumericStartEnd,
makeNumericMinMax, makeNumericMinMax,
makeNumericAvgRange,
makeSelectColumn, makeSelectColumn,
makeTagColumn, makeTagColumn,
makeTagInput, makeTagInput,

View File

@ -7,6 +7,9 @@ import { DataType } from './Columns'
export const makeNumericSorter = <T extends unknown>(key: keyof DataType<T>) => export const makeNumericSorter = <T extends unknown>(key: keyof DataType<T>) =>
(a: DataType<T>, b: DataType<T>) => Number(a[key]) - Number(b[key]) (a: DataType<T>, b: DataType<T>) => Number(a[key]) - Number(b[key])
export const makeNumericObjSorter = (key: [string, string]) =>
(a: DataType, b: DataType) => Number(a[key[0]][key[1]]) - Number(b[key[0]][key[1]])
export const makeStringSorter = <T extends unknown>(key: keyof DataType<T>) => (a?: DataType<T> | null, b?: DataType<T> | null) => { export const makeStringSorter = <T extends unknown>(key: keyof DataType<T>) => (a?: DataType<T> | null, b?: DataType<T> | null) => {
if (!a && !b) return 0 if (!a && !b) return 0
if (!a) return 1 if (!a) return 1

View File

@ -0,0 +1,149 @@
import React, { memo, useEffect } from 'react'
import * as d3 from 'd3'
import LoaderPortal from '@components/LoaderPortal'
import { useElementSize } from 'usehooks-ts'
import '@styles/d3.less'
type DataType = {
name: string
percent: number
}
type D3HorizontalChartProps = {
width?: string
height?: string
data: DataType[]
colors?: string[]
}
const D3HorizontalChart = memo((
{
width: givenWidth = '100%',
height: givenHeight = '100%',
data,
colors
}: D3HorizontalChartProps) => {
const [rootRef, { width, height }] = useElementSize()
const margin = { top: 50, right: 100, bottom: 50, left: 100 }
useEffect(() => {
if (width < 100 || height < 100) return
const _width = width - margin.left - margin.right
const _height = height - margin.top - margin.bottom
const svg = d3.select('#d3-horizontal-chart')
.attr('width', '100%')
.attr('height', '100%')
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`)
const percents = ['percents']
const names = data.map(d => d.name)
const stackedData = d3.stack()
//@ts-ignore
.keys(percents)(data)
const xMax = 100
// scales
const x = d3.scaleLinear()
.domain([0, xMax])
.range([0, _width])
const y = d3.scaleBand()
.domain(names)
.range([0, _height])
.padding(0.25)
// axes
const xAxisTop = d3.axisTop(x)
.tickValues([0, 25, 50, 75, 100])
.tickFormat(d => d + '%')
const xAxisBottom = d3.axisBottom(x)
.tickValues([0, 25, 50, 75, 100])
.tickFormat(d => d + '%')
const yAxisLeft = d3.axisLeft(y)
const gridlines = d3.axisBottom(x)
.tickValues([0, 25, 50, 75, 100])
.tickFormat(d => '')
.tickSize(_height)
const yAxisRight = d3.axisRight(y)
.ticks(0)
.tickValues([])
.tickFormat(d => '')
svg.append('g')
.attr('transform', `translate(0,0)`)
.attr("class", "grid-line")
.call(g => g.select('.domain').remove())
.call(gridlines)
svg.append('g')
.attr('transform', `translate(0,0)`)
.call(xAxisTop)
svg.append("g")
.call(yAxisLeft)
svg.append('g')
.attr('transform', `translate(0,${_height})`)
.call(xAxisBottom)
svg.append('g')
.attr('transform', `translate(${_width},0)`)
.call(yAxisRight)
const layers = svg.append('g')
.selectAll('g')
.data(stackedData)
.join('g')
// transition for bars
const duration = 1000
const t = d3.transition()
.duration(duration)
.ease(d3.easeLinear)
layers.each(function() {
d3.select(this)
.selectAll('rect')
//@ts-ignore
.data(d => d)
.join('rect')
.attr('fill', (d, i) => colors ? colors[i] : 'black')
//@ts-ignore
.attr('y', d => y(d.data.name))
.attr('height', y.bandwidth())
//@ts-ignore
.transition(t)
//@ts-ignore
.attr('width', d => x(d.data.percent))
})
return () => {
svg.selectAll("g").selectAll("*").remove()
}
}, [width, height, data])
return (
<LoaderPortal show={false} style={{width: givenWidth, height: givenHeight}}>
<div ref={rootRef} style={{width: '100%', height: '100%'}}>
<svg id={'d3-horizontal-chart'}></svg>
</div>
</LoaderPortal>
)
})
export default D3HorizontalChart

View File

@ -0,0 +1,153 @@
import React, { ReactNode, useEffect, useState } from 'react'
import { Col, Row, Select } from 'antd'
import { Moment } from 'moment'
import { DateRangeWrapper, makeColumn, makeNumericRender, Table } from '@components/Table'
import LoaderPortal from '@components/LoaderPortal'
import { arrayOrDefault, wrapPrivateComponent } from '@utils'
import D3HorizontalChart from '@components/d3/D3HorizontalChart'
import { useWell } from '@asb/context'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { SubsystemOperationTimeService } from '@api'
const { Option } = Select;
type DataType = {
idSubsystem: number
subsystemName: string
usedTimeHours: number
kUsage: number
sumDepthInterval: number
operationCount: number
}
const subsystemColors = [
'#1abc9c',
'#16a085',
'#2ecc71',
'#27ae60',
'#3498db',
'#2980b9',
'#9b59b6',
'#8e44ad',
'#34495e',
'#2c3e50',
'#f1c40f',
'#f39c12',
'#e67e22',
'#d35400',
'#e74c3c',
'#c0392b',
'#ecf0f1',
'#bdc3c7',
'#95a5a6',
'#7f8c8d',
]
const tableColumns = [
makeColumn('Цвет', 'color', { width: 50, render: (color) => (
<div style={{ backgroundColor: color, padding: '5px 0' }} />
) }),
makeColumn('Подсистема', 'subsystemName'),
makeColumn('% использования', 'kUsage', { render: val => (+val * 100).toFixed(2) }),
makeColumn('Проходка, м', 'sumDepthInterval', {render: makeNumericRender(2)}),
makeColumn('Время работы, ч', 'usedTimeHours', {render: makeNumericRender(2)}),
makeColumn('Кол-во запусков', 'operationCount'),
]
const OperationTime = () => {
const [showLoader, setShowLoader] = useState(false)
const [data, setData] = useState<DataType[]>([])
const [dateRange, setDateRange] = useState<Moment[]>([])
const [childrenData, setChildrenData] = useState<ReactNode[]>([])
const [well] = useWell()
const errorNotifyText = `Не удалось загрузить данные`
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
if (!well.id) return
try {
setData(arrayOrDefault(await SubsystemOperationTimeService.getStat(
well.id,
undefined,
dateRange[1] ? dateRange[0]?.toISOString() : undefined,
dateRange[1]?.toISOString(),
)))
} catch(e) {
setData([])
throw e
}
},
setShowLoader,
errorNotifyText,
{ actionName: 'Получение данных по скважине', well }
)
}, [dateRange])
useEffect(() => {
setChildrenData(data.map((item) => (
<Option key={item.subsystemName}>
{item.subsystemName}
</Option>
)))
}, [data])
const selectChange = (value: string[]) => {
setData(data.reduce((previousValue: DataType[], currentValue) => {
if (value.includes(currentValue.subsystemName)) {
previousValue.push(currentValue)
}
return previousValue
}, []))
}
return (
<LoaderPortal show={showLoader}>
<h3 className={'filter-group-heading'}>Фильтр подсистем</h3>
<Row align={'middle'} justify={'space-between'} wrap={false} style={{ backgroundColor: 'white' }}>
<Col span={18}>
<Select
mode="multiple"
defaultValue={data.map(d => d.subsystemName)}
onChange={selectChange}
style={{ width: '100%' }}
>
{childrenData}
</Select>
</Col>
<Col span={6}>
<DateRangeWrapper
onCalendarChange={(dateRange: any) => setDateRange(dateRange)}
value={[dateRange[0], dateRange[1]]}
/>
</Col>
</Row>
<div style={{width: '100%', height: '50vh'}}>
<D3HorizontalChart
data={data.map(item => ({name: item.subsystemName, percent: item.kUsage * 100}))}
colors={subsystemColors}
width={'100%'}
height={'50vh'}
/>
</div>
<Table
size={'small'}
columns={tableColumns}
dataSource={data.map((d, i) => ({...d, color: subsystemColors[i]}))}
scroll={{ y: '25vh', x: true }}
pagination={false}
/>
</LoaderPortal>
)
}
export default wrapPrivateComponent(OperationTime, {
requirements: [],
title: 'Наработка',
route: 'operation_time',
})

View File

@ -12,6 +12,7 @@ import Messages from './Messages'
import Operations from './Operations' import Operations from './Operations'
import DashboardNNB from './DashboardNNB' import DashboardNNB from './DashboardNNB'
import TelemetryView from './TelemetryView' import TelemetryView from './TelemetryView'
import OperationTime from './OperationTime'
import '@styles/index.css' import '@styles/index.css'
@ -30,6 +31,7 @@ const Telemetry = memo(() => {
<PrivateMenu.Link content={Archive} icon={<DatabaseOutlined />} /> <PrivateMenu.Link content={Archive} icon={<DatabaseOutlined />} />
<PrivateMenu.Link content={DashboardNNB} /> <PrivateMenu.Link content={DashboardNNB} />
<PrivateMenu.Link content={Operations} /> <PrivateMenu.Link content={Operations} />
<PrivateMenu.Link content={OperationTime} />
</PrivateMenu> </PrivateMenu>
<Layout> <Layout>
@ -43,6 +45,7 @@ const Telemetry = memo(() => {
<Route path={Archive.route} element={<Archive />} /> <Route path={Archive.route} element={<Archive />} />
<Route path={DashboardNNB.route} element={<DashboardNNB />} /> <Route path={DashboardNNB.route} element={<DashboardNNB />} />
<Route path={Operations.route} element={<Operations />} /> <Route path={Operations.route} element={<Operations />} />
<Route path={OperationTime.route} element={<OperationTime />} />
</Routes> </Routes>
</Content> </Content>
</Layout> </Layout>

View File

@ -1,17 +1,87 @@
import { useState, useEffect, useCallback, memo, useMemo } from 'react' import { useState, useEffect, useCallback, memo, useMemo } from 'react'
import { InputNumber } from 'antd'
import { useWell } from '@asb/context' import { useWell } from '@asb/context'
import { import {
EditableTable, EditableTable,
makeSelectColumn, makeSelectColumn,
makeNumericAvgRange, makeGroupColumn,
makeNumericRender,
makeNumericSorter, makeNumericSorter,
RegExpIsFloat,
} from '@components/Table' } from '@components/Table'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { DrillParamsService, WellOperationService } from '@api' import { DrillParamsService, WellOperationService } from '@api'
import { arrayOrDefault } from '@utils' import { arrayOrDefault } from '@utils'
import { makeNumericObjSorter } from '@components/Table/sorters'
const makeNumericObjRender = (fixed, columnKey) => (value, obj) => {
let val = '-'
const isSelected = obj && columnKey && obj[columnKey[0]] ? obj[columnKey[0]][columnKey[1]] : false
if ((value ?? null) !== null && Number.isFinite(+value)) {
val = (fixed ?? null) !== null
? (+value).toFixed(fixed)
: (+value).toPrecision(5)
}
return (
<div className={`text-align-r-container ${isSelected ? 'color-pale-green' : ''}`}>
<span>{val}</span>
</div>
)
}
const makeNumericColumnOptionsWithColor = (fixed, sorterKey, columnKey) => ({
editable: true,
initialValue: 0,
width: 100,
sorter: sorterKey ? makeNumericObjSorter(sorterKey) : undefined,
formItemRules: [{
required: true,
message: 'Введите число',
pattern: RegExpIsFloat,
}],
render: makeNumericObjRender(fixed, columnKey),
})
const makeNumericObjColumn = (
title,
dataIndex,
filters,
filterDelegate,
renderDelegate,
width,
other
) => ({
title: title,
dataIndex: dataIndex,
key: dataIndex,
filters: filters,
onFilter: filterDelegate ? filterDelegate(dataIndex) : null,
sorter: makeNumericObjSorter(dataIndex),
width: width,
input: <InputNumber style={{ width: '100%' }}/>,
render: renderDelegate ?? makeNumericRender(),
align: 'right',
...other
})
const makeNumericAvgRange = (
title,
dataIndex,
fixed,
filters,
filterDelegate,
renderDelegate,
width
) => makeGroupColumn(title, [
makeNumericObjColumn('мин', [dataIndex, 'min'], filters, filterDelegate, renderDelegate, width, makeNumericColumnOptionsWithColor(fixed, [dataIndex, 'min'], [dataIndex, 'isMin'])),
makeNumericObjColumn('сред', [dataIndex, 'avg'], filters, filterDelegate, renderDelegate, width, makeNumericColumnOptionsWithColor(fixed, [dataIndex, 'avg'])),
makeNumericObjColumn('макс', [dataIndex, 'max'], filters, filterDelegate, renderDelegate, width, makeNumericColumnOptionsWithColor(fixed, [dataIndex, 'max'], [dataIndex, 'isMax']))
])
export const getColumns = async (idWell) => { export const getColumns = async (idWell) => {
let sectionTypes = await WellOperationService.getSectionTypes(idWell) let sectionTypes = await WellOperationService.getSectionTypes(idWell)

View File

@ -116,6 +116,11 @@
} }
} }
.grid-line line {
stroke: #ddd;
stroke-dasharray: 4
}
@media (max-width: 1800px) { @media (max-width: 1800px) {
.asb-d3-chart .adaptive-tooltip { .asb-d3-chart .adaptive-tooltip {
font-size: 11px; font-size: 11px;

View File

@ -142,3 +142,15 @@ code {
height: 32px; height: 32px;
padding: 4px 15px; padding: 4px 15px;
} }
.ant-table-cell:has(.color-pale-green) {
background-color: #98fb98;
}
.ant-table-tbody > tr > td.ant-table-cell-row-hover:has( > div.color-pale-green) {
background: #98fb98;
}
.color-pale-green {
background-color: #98fb98;
}