Документирована часть комментариев и функций

This commit is contained in:
Александр Сироткин 2022-12-13 15:29:19 +05:00
parent d235b01c80
commit 7af33d702d
32 changed files with 463 additions and 83 deletions

View File

@ -1,6 +1,7 @@
{
"cSpell.words": [
"день"
"день",
"Saub"
],
"liveServer.settings.port": 5501
}

View File

@ -5,6 +5,11 @@ import { DatePickerWrapper, getObjectByDeepKey } from '..'
import { DatePickerWrapperProps } from '../DatePickerWrapper'
import { formatDate, isRawDate } from '@utils'
/**
* Фабрика методов сортировки столбцов для данных типа **Дата**
* @param key Ключ столбца
* @returns Метод сортировки
*/
export const makeDateSorter = <T extends unknown>(key: Key): SorterMethod<T> => (a, b) => {
const vA = a ? getObjectByDeepKey(key, a) : null
const vB = b ? getObjectByDeepKey(key, b) : null
@ -16,6 +21,17 @@ export const makeDateSorter = <T extends unknown>(key: Key): SorterMethod<T> =>
return (new Date(vA)).getTime() - (new Date(vB)).getTime()
}
/**
* Фабрика объектов-столбцов для компонента `Table` для работы с данными типа **Дата**
*
* @param title Название столбца
* @param key Ключ столбца
* @param utc Конвертировать ли дату в UTC
* @param format Формат отображения даты
* @param other Дополнительные опции столбца
* @param pickerOther Опции компонента селектора даты
* @returns Объект-столбец для работы с данными типа **Дата**
*/
export const makeDateColumn = <T extends unknown>(
title: ReactNode,
key: string,

View File

@ -6,8 +6,11 @@ import moment, { Moment } from 'moment'
import { defaultFormat } from '@utils'
export type DatePickerWrapperProps = PickerDateProps<Moment> & {
/** Значение селектора */
value?: Moment,
/** Метод вызывается при изменений даты */
onChange?: (date: Moment | null) => any
/** Конвертировать ли значение в UTC */
isUTC?: boolean
}

View File

@ -9,31 +9,41 @@ import { defaultFormat } from '@utils'
const { RangePicker } = DatePicker
export type DateRangeWrapperProps = RangePickerSharedProps<Moment> & {
value?: RangeValue<Moment>,
/** Значение селектора в виде массива из 2 элементов (от, до) */
value?: RangeValue<Moment>
/** Конвертировать ли значения в UTC */
isUTC?: boolean
/** Разрешить сброс значения селектора */
allowClear?: boolean
}
/**
* Подготавливает значения к передаче в селектор
*
* @param value Массиз из 2 дат
* @param isUTC Конвертировать ли значения в UTC
* @returns Подготовленные даты
*/
const normalizeDates = (value?: RangeValue<Moment>, isUTC?: boolean): RangeValue<Moment> => {
if (!value) return [null, null]
return [
value[0] ? (isUTC ? moment.utc(value[0]).local() : moment(value[0])) : null,
value[1] ? (isUTC ? moment.utc(value[1]).local() : moment(value[1])) : null,
]
if (!value) return [null, null]
return [
value[0] ? (isUTC ? moment.utc(value[0]).local() : moment(value[0])) : null,
value[1] ? (isUTC ? moment.utc(value[1]).local() : moment(value[1])) : null,
]
}
export const DateRangeWrapper = memo<DateRangeWrapperProps>(({ value, isUTC, allowClear = false, ...other }) => (
<RangePicker
showTime
allowClear={allowClear}
format={defaultFormat}
defaultValue={[
moment().subtract(1, 'days').startOf('day'),
moment().startOf('day'),
]}
value={normalizeDates(value)}
{...other}
/>
export const DateRangeWrapper = memo<DateRangeWrapperProps>(({ value, isUTC, allowClear, ...other }) => (
<RangePicker
showTime
allowClear={allowClear}
format={defaultFormat}
defaultValue={[
moment().subtract(1, 'days').startOf('day'),
moment().startOf('day'),
]}
value={normalizeDates(value, isUTC)}
{...other}
/>
))
export default DateRangeWrapper

View File

@ -1,6 +1,6 @@
import { Key, memo, useCallback, useEffect, useState } from 'react'
import { ColumnGroupType, ColumnType } from 'antd/lib/table'
import { Table as RawTable, TableProps } from 'antd'
import { Table as RawTable, TableProps as RawTableProps } from 'antd'
import { RenderMethod } from './Columns'
import { tryAddKeys } from './EditableTable'
@ -14,16 +14,28 @@ export type BaseTableColumn<T> = ColumnGroupType<T> | ColumnType<T>
export type TableColumn<T> = OmitExtends<BaseTableColumn<T>, TableColumnSettings>
export type TableColumns<T> = TableColumn<T>[]
export type TableContainer<T> = TableProps<T> & {
export type TableProps<T> = RawTableProps<T> & {
/** Массив колонок таблицы с настройками (описаны в `TableColumnSettings`) */
columns: TableColumn<T>[]
/** Название таблицы для сохранения настроек */
tableName?: string
/** Отображать ли кнопку настроек */
showSettingsChanger?: boolean
}
export interface DataSet<T, D = any> {
[k: Key]: DataSet<T> | T | D
[k: Key]: DataSet<T, D> | T | D
}
/**
* Получить значение из объекта по составному ключу
*
* Составной ключ имеет вид: `<поле 1>[.<поле 2>...]`
*
* @param key Составной ключ
* @param data Объект из которого будет полученно значение
* @returns Значение, найденное по ключу, либо `undefined`
*/
export const getObjectByDeepKey = <T,>(key: Key | undefined, data: DataSet<T>): T | undefined => {
if (!key) return undefined
const parts = String(key).split('.')
@ -36,36 +48,44 @@ export const getObjectByDeepKey = <T,>(key: Key | undefined, data: DataSet<T>):
return out as T
}
/**
* Фабрика обёрток render-функций ячеек с поддержкой составных ключей
* @param key Составной ключ
* @param render Стандартная render-функция
* @returns Обёрнутая render-функция
*/
export const makeColumnRenderWrapper = <T extends DataSet<any>>(key: Key | undefined, render: RenderMethod<T, T> | undefined): RenderMethod<T, T> =>
(_: any, dataset: T, index: number) => {
const renderFunc: RenderMethod<T, T> = typeof render === 'function' ? render : (record) => String(record)
return renderFunc(getObjectByDeepKey<T>(key, dataset), dataset, index)
}
const applyColumnWrappers = <T extends DataSet<any>>(columns: BaseTableColumn<T>[]): BaseTableColumn<T>[] => {
return columns.map((column) => {
if ('children' in column) {
return {
...column,
children: applyColumnWrappers(column.children),
}
}
/**
* Применяет необходимые обёртки ко всем столбцам таблицы
* @param columns Исходные столбцы
* @returns Обёрнутые столбцы
*/
const applyColumnWrappers = <T extends DataSet<any>>(columns: TableColumns<T>): TableColumns<T> => columns.map((column) => {
if ('children' in column) {
return {
...column,
render: makeColumnRenderWrapper<T>(column.key, column.render),
children: applyColumnWrappers(column.children),
}
})
}
}
return {
...column,
render: makeColumnRenderWrapper<T>(column.key, column.render),
}
})
function _Table<T extends DataSet<any>>({ columns, dataSource, tableName, showSettingsChanger, ...other }: TableContainer<T>) {
function _Table<T extends DataSet<any>>({ columns, dataSource, tableName, showSettingsChanger, ...other }: TableProps<T>) {
const [newColumns, setNewColumns] = useState<TableColumn<T>[]>([])
const [settings, setSettings] = useState<TableSettings>({})
const onSettingsChanged = useCallback((settings?: TableSettings | null) => {
if (tableName)
setTableSettings(tableName, settings)
setSettings(settings ?? {})
setSettings(settings || {})
}, [tableName])
useEffect(() => setSettings(tableName ? getTableSettings(tableName) : {}), [tableName])
@ -92,6 +112,13 @@ function _Table<T extends DataSet<any>>({ columns, dataSource, tableName, showSe
)
}
/**
* Обёртка над компонентом таблицы AntD
*
* Особенности:
* * Поддержка составных ключей столбцов
* * Работа с настройками столбцов таблицы
*/
export const Table = memo(_Table) as typeof _Table
export default Table

View File

@ -6,8 +6,11 @@ import { defaultTimeFormat, momentToTime, timeToMoment } from '@utils'
import { TimeDto } from '@api'
export type TimePickerWrapperProps = Omit<Omit<TimePickerProps, 'value'>, 'onChange'> & {
/** Текущее значение */
value?: TimeDto,
/** Метод вызывается при изменений времени */
onChange?: (date: TimeDto | null) => any
/** Конвертировать ли время в UTC */
isUTC?: boolean
}

View File

@ -17,6 +17,13 @@ export type PaginationContainer<T> = {
items?: T[] | null
}
/**
* Генерирует объект пагинации для компонента `Table` из данных от сервисов
*
* @param сontainer данные от сервиса
* @param other Дополнительные поля (передаются в объект напрямую в приоритете)
* @returns Объект пагинации
*/
export const makePaginationObject = <T, M extends object>(сontainer: PaginationContainer<T>, other: M) => ({
...other,
pageSize: сontainer.take,

View File

@ -9,6 +9,7 @@ export type CompanyViewProps = {
company?: CompanyDto
}
/** Компонент для отображения информации о компании */
export const CompanyView = memo<CompanyViewProps>(({ company }) => company ? (
<Tooltip title={
<Grid style={{ columnGap: '8px' }}>

View File

@ -8,6 +8,7 @@ export type PermissionViewProps = {
info?: PermissionDto
}
/** Компонент для отображения информации о разрешении */
export const PermissionView = memo<PermissionViewProps>(({ info }) => info ? (
<Tooltip overlayInnerStyle={{ width: '400px' }} title={
<Grid>

View File

@ -9,6 +9,7 @@ export type RoleViewProps = {
role?: UserRoleDto
}
/** Компонент для отображения информации о роли */
export const RoleView = memo<RoleViewProps>(({ role }) => {
if (!role) return ( <Tooltip title={'нет данных'}>-</Tooltip> )

View File

@ -19,6 +19,12 @@ export const lables: Record<string, string> = {
spinPlcVersion: 'Версия Спин Мастер',
}
/**
* Строит название для телеметрии
*
* @param telemetry Объект телеметрии
* @returns Название
*/
export const getTelemetryLabel = (telemetry?: TelemetryDto) =>
`${telemetry?.id ?? '-'} / ${telemetry?.info?.deposit ?? '-'} / ${telemetry?.info?.cluster ?? '-'} / ${telemetry?.info?.well ?? '-'}`
@ -26,6 +32,7 @@ export type TelemetryViewProps = {
telemetry?: TelemetryDto
}
/** Компонент для отображения информации о телеметрии */
export const TelemetryView = memo<TelemetryViewProps>(({ telemetry }) => telemetry?.info ? (
<Tooltip
overlayInnerStyle={{ width: '400px' }}

View File

@ -10,6 +10,7 @@ export type UserViewProps = HTMLProps<HTMLSpanElement> & {
user?: UserDto
}
/** Компонент для отображения информации о пользователе */
export const UserView = memo<UserViewProps>(({ user, ...other }) =>
user ? (
<Tooltip

View File

@ -19,8 +19,14 @@ export type WellViewProps = TooltipProps & {
labelProps?: DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>
}
/**
* Получить название скважины
* @param well Объект с данными скважины
* @returns Название скважины
*/
export const getWellTitle = (well: WellDto) => `${well.deposit || '-'} / ${well.cluster || '-'} / ${well.caption || '-'}`
/** Компонент для отображения информации о скважине */
export const WellView = memo<WellViewProps>(({ well, iconProps, labelProps, ...other }) => well ? (
<Tooltip {...other} title={(
<Grid style={{ columnGap: '8px' }}>

View File

@ -21,6 +21,7 @@ export type WirelineViewProps = TooltipProps & {
buttonProps?: ButtonProps
}
/** Компонент для отображения информации о талевом канате */
export const WirelineView = memo<WirelineViewProps>(({ wireline, buttonProps, ...other }) => (
<Tooltip
{...other}

View File

@ -7,7 +7,7 @@ import LoaderPortal from '@components/LoaderPortal'
import { DateRangeWrapper } from '@components/Table'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { unique } from '@utils/filters'
import { getPermissions, arrayOrDefault, range, withPermissions, pretify } from '@utils'
import { getPermissions, arrayOrDefault, range, withPermissions, prettify } from '@utils'
import { DetectedOperationService, DrillerService, TelemetryDataSaubService } from '@api'
import DrillerList from './DrillerList'
@ -67,7 +67,7 @@ const Operations = memo(() => {
const maxTarget = Math.max(...data.operations?.map((op) => op.operationValue?.targetValue || 0))
const uniqueOps = data.operations?.map((op) => op.value || 0).filter(unique)
const value = uniqueOps.reduce((out, op) => out + op, 0) / uniqueOps.length * 3 / 2
setYDomain(pretify(Math.max(maxTarget, value)))
setYDomain(prettify(Math.max(maxTarget, value)))
}, [data])
useEffect(() => {

View File

@ -8,7 +8,7 @@ import { useTopRightBlock, useWell } from '@asb/context'
import { D3Chart } from '@components/d3'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { formatDate, fractionalSum, withPermissions, getOperations, pretify } from '@utils'
import { formatDate, fractionalSum, withPermissions, getOperations, prettify } from '@utils'
import TLPie from './TLPie'
import TLChart from './TLChart'
@ -180,7 +180,7 @@ const Tvd = memo(({ well: givenWell, title, ...other }) => {
.map(([_, ops]) => Math.max(...ops.map((op) => op.depth).filter(Boolean)))
.filter(Boolean)
)
const minValue = pretify(maxValue)
const minValue = prettify(maxValue)
return {
date: {

View File

@ -1,12 +1,30 @@
import { getObjectByDeepKey } from "@asb/components/Table"
/**
* Фабрика методов фильтрации строк таблицы по столбцу с текстом
* @param key Составной ключ столбца
* @returns Метод фильтрации
*/
export const makeTextOnFilter = (key: string) =>
(value: string, record?: Record<string, unknown>) => String(record?.[key]).startsWith(value)
(value: string, record?: Record<string, unknown>) => record && String(getObjectByDeepKey(key, record)).startsWith(value)
/**
* Фабрика методов фильтрации строк таблицы по столбцу с массивами
* @param key Составной ключ столбца
* @returns Метод фильтрации
*/
export const makeArrayOnFilter = (key: string) =>
(value: string, record?: Record<string, string[]>) => (!value && (record?.[key]?.length ?? 0) <= 0) || record?.[key]?.includes(value)
export const makeObjectOnFilter = (field: string, key: string) =>
(value: string, record?: Record<string, Record<string, unknown>>) => String(record?.[field]?.[key]).startsWith(value)
(value: string, record?: Record<string, string[]>) => record && (
(!value && (getObjectByDeepKey<any[]>(key, record)?.length ?? 0) <= 0) ||
getObjectByDeepKey<any[]>(key, record)?.includes(value)
)
/**
* Создаёт список значений для фильтрации текстовых столбцов
* @param array Массив значений
* @param keys Массив ключей
* @returns Список значений
*/
export const makeTextFilters = (array: Record<string, unknown>[], keys: string[]) => {
const filters: string[][] = Array(keys.length)

View File

@ -1,3 +1,11 @@
export * from './columnFilters'
/**
* Проверяет значение на уникальность в массиве
*
* @param value Проверяемое значение
* @param index Индекс проверяемого значения в массиве
* @param self Массив, содержащий значение
* @returns Является ли значение уникальным или первым
*/
export const unique = <T,>(value: T, index: number, self: T[]) => self.indexOf(value) === index

View File

@ -1,10 +1,10 @@
/**
* Возвращает
* Гарантированно возвращает массив нужного типа
*
* @param arr Входящие данные
* @param def Значение по-умолчанию
*
* @returns Если `arr` - массив будет возвращено оно, иначе `def`
* @returns Если входящие данные - массив будут возвращены они, иначе значение из `def`
*/
export const arrayOrDefault = <T,>(arr: unknown, def: T[] = []): T[] => arr instanceof Array ? arr : def

View File

@ -3,6 +3,12 @@ import { AntdIconProps } from '@ant-design/icons/lib/components/AntdIcon'
import { BaseDataType, ChartDataset } from '@components/d3'
/**
* Фабрика методов-оптимизаторов для массивов точек перед выводом на график
*
* @param isEquals Метод сравнения элементов на равенство
* @returns Метод вырезки идентичных элементов (по результатам работы переданного метода)
*/
export const makePointsOptimizator = <DataType extends BaseDataType>(isEquals: (a: DataType, b: DataType) => boolean) => (points: DataType[]) => {
if (!Array.isArray(points) || points.length < 3) return points
@ -16,6 +22,22 @@ export const makePointsOptimizator = <DataType extends BaseDataType>(isEquals: (
export type TouchType = 'all' | 'x' | 'y'
/**
* Получает расстояние между точками в зависимости от типа касания
*
* Если тип касания 'x', то вернёт модуль разницы абсцисс
*
* Если тип касания 'y', то вернёт модуль разницы ординат
*
* Иначе вернёт корень суммы квадратов разностей координат точек
*
* @param x1 Абсцисса первой точки
* @param y1 Ордината первой точки
* @param x2 Абсцисса второй точки
* @param y2 Ордината второй точки
* @param type Тип касания
* @returns Расстояние между точками
*/
export const getDistance = (x1: number, y1: number, x2: number, y2: number, type: TouchType = 'all') => {
if (type === 'x') return Math.abs(x1 - x2)
if (type === 'y') return Math.abs(y1 - y2)
@ -23,6 +45,13 @@ export const getDistance = (x1: number, y1: number, x2: number, y2: number, type
return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2))
}
/**
* Возвращает иконку графика в зависимости от типа
*
* @param chart График, у которого будет проверен тип
* @param options Дополнительные опции иконки
* @returns Элемент иконки
*/
export const getChartIcon = <DataType extends BaseDataType>(chart: ChartDataset<DataType>, options?: Omit<AntdIconProps, 'ref'>) => {
let Icon
switch (chart.type) {

View File

@ -16,10 +16,22 @@ export enum timeInS {
week = day * 7,
}
/**
* Проверка значения на возможность преобразования к дате
*
* @param value Проверяемое значение
* @returns Является ли значение потенциальной датой
*/
export function isRawDate(value: unknown): value is RawDate {
return !isNaN(Date.parse(String(value)))
}
/**
* Проверка значения на возможность преобразования к `TimeDto`
*
* @param value Проверяемое значение
* @returns Является ли значение потенциальным объектом `TimeDto`
*/
export function isTime(value: unknown): value is TimeDto {
if (!value || typeof value !== 'object')
return false
@ -27,18 +39,40 @@ export function isTime(value: unknown): value is TimeDto {
return ['hour', 'minute', 'second'].every((key) => keys.includes(key))
}
/**
* Форматировать значение как дату в строку
*
* @param date Форматируемое значение
* @param utc Преобразовывать ли к UTC
* @param format Формат вывода
* @returns Форматированная дата в строке
*/
export const formatDate = (date: unknown, utc: boolean = false, format: string = defaultFormat) => {
if (!isRawDate(date)) return null
const out = utc ? moment.utc(date).local() : moment(date)
return out.format(format)
}
/**
* Форматировать значение как время в строку
*
* @param time Форматируемое значение
* @param utc Преобразовывать ли к UTC
* @param format Формат вывода
* @returns Форматированное время в строке
*/
export const formatTime = (time: unknown, utc: boolean = false, format: string = defaultTimeFormat) => {
if(!isTime(time)) return
const out = timeToMoment(time, utc, format)
return out.format(format)
}
/**
* Привести секунды к строке периоду
*
* @param time Указанное кол-во секунд
* @returns Форматированная строка период
*/
export const periodToString = (time?: number) => {
if (!time || time <= 0) return '00:00:00'
const days = Math.floor(time / timeInS.day)
@ -54,11 +88,26 @@ export const periodToString = (time?: number) => {
return `${days > 0 ? days : ''} ${toFixed(hours)}:${toFixed(minutes)}:${toFixed(seconds)}`
}
/**
* Вычислить кол-во дней между двумя датами
*
* @param start Левая дата
* @param end Правая дата
* @returns если оба аргумента потенциальны даты, то кол-во дней между ними, иначе `undefined`
*/
export const calcDuration = (start: unknown, end: unknown): number | undefined => {
if (!isRawDate(start) || !isRawDate(end)) return undefined
return (+new Date(end) - +new Date(start)) * timeInS.millisecond / timeInS.day
}
/**
* Сдвинуть дату на указанное время
*
* @param date Исходная дата
* @param value Коэффициент сдвига
* @param type Тип сдвига (день, час, минут и т.д.)
* @returns Смещённая дата
*/
export const fractionalSum = (date: unknown, value: number, type: keyof typeof timeInS): RawDate | null => {
if (!isRawDate(date) || !timeInS[type] || isNaN(value ?? NaN)) return null
const d = new Date(date)
@ -86,17 +135,41 @@ export const rawTimezones = Object.freeze({
export type TimezoneId = keyof typeof rawTimezones
/**
* Проверяет, является ли переданное значение корректным ID часовой зоны
*
* @param value Проверяемое значение
* @returns Является ли переданное значение корректным ID часовой зоны
*/
export const isTimezoneId = (value: unknown): value is TimezoneId => !!value && String(value) in rawTimezones
/**
* Ищет часовую зону для переданной телеметрии
* @param value Данные телеметрии
* @returns Название часовой зоны
*/
export const findTimezoneId = (value: SimpleTimezoneDto): TimezoneId =>
(isTimezoneId(value.timezoneId) && value.timezoneId) ||
(Object.keys(rawTimezones) as TimezoneId[]).find(id => rawTimezones[id] === value.hours) as TimezoneId
/**
* Приводит `TimeDto`-объект к `Moment`-объекту
*
* @param time `TimeDto`-объект
* @param isUtc Приводить ли к UTC
* @param format Формат для обработки
* @returns `Moment`-объект
*/
export const timeToMoment = (time?: TimeDto | null, isUtc?: boolean, format: string = defaultTimeFormat): Moment => {
const input = `${time?.hour ?? 0}:${time?.minute ?? 0}:${time?.second ?? 0}`
return isUtc ? moment.utc(input, format).local() : moment(input, format)
}
/**
* Приводит `Moment`-объект к `TimeDto`-объекту
* @param time `Moment`-объект
* @returns `TimeDto`-объекту
*/
export const momentToTime = (time?: Moment | null): TimeDto => ({
hour: time?.hour() ?? 0,
minute: time?.minute() ?? 0,

View File

@ -1,5 +1,13 @@
import { ReactNode } from 'react'
/**
* Форматирует число по заданным параметрам
*
* @param number Форматируемое число
* @param def Вывод по-умолчанию
* @param fixed Длина числа после форматирования
* @returns Строка - форматированное число
*/
export const getPrecision = (number: number, def: string = '-', fixed: number = 2): string => Number.isFinite(number) ? number.toFixed(fixed) : def
/**
@ -25,7 +33,12 @@ export const limitValue = <T,>(min: T, max: T) => (value: T) => {
*/
export const range = (end: number, start: number = 0) => Array(end - start).fill(undefined).map((_, i) => start + i)
export const pretify = (n: number): number | null => {
/**
* Округляет число до ближайшего красивого
* @param n Округляемое число
* @returns Округлённое число
*/
export const prettify = (n: number): number | null => {
if (!Number.isFinite(n)) return null
let i = 0
for (; Math.abs(n) >= 100; i++) n /= 10

View File

@ -1,20 +1,20 @@
/**
* Копирует данные в глубину.
* Копирует данные с максимальной глубиной
*
* @remarks
* При копированиий объектов, содержащих функций может возникнуть исключение.
* При копирований объектов, содержащих функций может возникнуть исключение.
* Не предназначено для копирования функций.
*
* @param data Копируемые данные
* @returns Полная копия `data`
* @returns Полная копия исходных данных
*/
export const deepCopy = <T,>(data: T): T => JSON.parse(JSON.stringify(data ?? null))
/**
* Маппинг полей объекта
* Аналог функции `Array.prototype.map`, но для работы с объектами
*
* @param data Входящие данные
* @param handler Обработчик
* @param data Исходный объект
* @param handler Метод-обработчик
*
* @returns Объект с обработанными полями
*/

View File

@ -12,10 +12,22 @@ export type ServiceName = string
export type ServiceRequestType = 'get' | 'edit' | 'delete'
export type PermissionRequest = `${ServiceName}.${ServiceRequestType}`
/**
* Проверка соответствует ли значение типу `ServiceRequestType`
*
* @param value Проверяемое значение
* @returns Является ли значение объектом типа `ServiceRequestType`
*/
export function isRequestType(value: string): value is ServiceRequestType {
return ['get', 'edit', 'delete'].includes(value)
}
/**
* Генерация объекта, содержащего информацию о наличии или отсутствия перечисленных разрешений
*
* @param values Список разрешений
* @returns Объект с информацией о разрешениях
*/
export const getPermissions = (...values: PermissionRequest[]) => {
const permissions: Record<string, Partial<Record<ServiceRequestType, boolean>>> = {}
values.forEach((key) => {
@ -27,6 +39,13 @@ export const getPermissions = (...values: PermissionRequest[]) => {
return permissions
}
/**
* Проверка наличия у пользователя разрешения или списка разрешений
*
* @param permission Разрешение или список разрешений
* @param userPermissions Список разрешений пользователя (если не указано, будут получены из локального хранилища)
* @returns `true` если все разрешения присутствуют, иначе `false`
*/
export const hasPermission = (permission?: Permission | Permission[], userPermissions?: Permission[]): boolean => {
if (!Array.isArray(permission) && typeof permission !== 'string')
return true
@ -36,6 +55,13 @@ export const hasPermission = (permission?: Permission | Permission[], userPermis
return permission.every((perm) => userPerms.includes(perm))
}
/**
* Проверка доступности секции сайта для посещения пользователем
*
* @param section Секция сайта
* @param userPermission Разрешения пользователя
* @returns `true` если все разрешения присутствуют, иначе `false`
*/
const sectionAvailable = (section: PermissionRecord, userPermission: Permission[]) => {
for (const child of Object.values(section)) {
if (!child) continue
@ -48,6 +74,12 @@ const sectionAvailable = (section: PermissionRecord, userPermission: Permission[
return false
}
/**
* Проверка доступности URL для посещения пользователем
* @param path URL
* @param userPermissions Разрешения пользователя (если не заданы, будут получены из локального хранилища)
* @returns `true` если все разрешения присутствуют, иначе `false`
*/
export const isURLAvailable = (path: string, userPermissions?: Permission[]) => {
if (publicPages.includes(path)) return true
@ -95,14 +127,26 @@ export const NoAccessComponent = memo(() => getUser().login ? (
<Navigate to={'/login'} replace />
))
/**
* HOC добавляющий проверку на наличие разрешений для отображения компонента
* @param Component Исходных компонент
* @param requirements Необходимые разрешения
* @param elseNode Компонент по-умолчанию
* @returns Обёрнутый компонент с проверкой разрешений
*/
export const withPermissions = <P extends object>(
Component: NamedExoticComponent<P> | ((props: P) => ReactElement),
requirements: Permission[] = [],
elseNode: JSX.Element = <NoAccessComponent />
): PrivateComponent<P> => Object.assign(memo<P>(function PrivateWrapper(props) {
): PrivateComponent<P> => memo<P>(function PrivateWrapper(props) {
return hasPermission(requirements) ? <Component {...props} /> : elseNode
}))
})
/**
* Получить url текущей выбранной вкладки
*
* @returns url текущей выбранной вкладки
*/
export const getTabname = () => {
const params = useParams()
const attr = useMemo(() => params['*']?.split('/').filter(s => s)[0] ?? null, [params])

View File

@ -11,10 +11,15 @@ export type SaubData = WellOperationDto & {
depth?: number
/** Дата */
date?: string
/** Колличество часов НПВ с начала бурения до текущего момента */
/** Количество часов НПВ с начала бурения до текущего момента */
nptHours?: number
}
/**
* Получить списки операций для конкретной скважины
* @param idWell ID скважины
* @returns Списки операций
*/
export const getOperations = async (idWell: number): Promise<{
operations: WellOperationDtoPlanFactPredictBase[],
plan: SaubData[]

View File

@ -16,12 +16,25 @@ export enum StorageNames {
witsInfo = 'witsInfo'
}
/**
* Получить массив значений из локального хранилища
*
* @param name Имя массива
* @param sep Разделитель элементов
* @returns Массив значений или `null`
*/
export const getArrayFromLocalStorage = <T extends string = string>(name: string, sep: string | RegExp = ','): T[] | null => {
const raw = localStorage.getItem(name)
if (!raw) return null
return raw.split(sep).map<T>(elm => elm as T)
return raw.split(sep) as T[]
}
/**
* Получить объект из JSON строки в локальном хранилище
*
* @param name Имя строки
* @returns Прочитанный объект или `null`
*/
export const getJSON = <T,>(name: StorageNames): T | null => {
try {
const raw = localStorage.getItem(name)
@ -32,6 +45,13 @@ export const getJSON = <T,>(name: StorageNames): T | null => {
return null
}
/**
* Записать объект в локальное хранилище, как JSON строка
*
* @param name Имя строки
* @param data Сохраняемый объект
* @returns `true` если сохранение успешно, иначе `false`
*/
export const setJSON = <T,>(name: StorageNames, data: T | null): boolean => {
try {
localStorage.setItem(name, JSON.stringify(data))
@ -42,8 +62,17 @@ export const setJSON = <T,>(name: StorageNames, data: T | null): boolean => {
return false
}
/**
* Получить информацию о пользователе из локального хранилища
*
* @returns Объект данных о пользователе
*/
export const getUser = (): UserTokenDto => getJSON(StorageNames.user) || {}
/**
* Получить разрешения пользователя из локального хранилища
* @returns Список разрешений или `null`
*/
export const getUserPermissions = (): Permission[] | null => {
let permissions = getUser()?.permissions?.map((perm) => perm.name as string)
if (!permissions) // TODO: Удалить в следующем релизе, вставлено для совместимости
@ -51,11 +80,20 @@ export const getUserPermissions = (): Permission[] | null => {
return permissions || null
}
/**
* Сохранить данные пользователя в локальное хранилище
*
* @param user Данные пользователя
* @returns `true` если сохранение успешно, иначе `false`
*/
export const setUser = (user: UserTokenDto) => {
OpenAPI.TOKEN = user.token ?? undefined
localStorage.setItem(StorageNames.user, JSON.stringify(user))
return setJSON(StorageNames.user, user)
}
/**
* Очистить данные о пользователе в локальном хранилище
*/
export const removeUser = () => {
localStorage.removeItem(StorageNames.userId)
localStorage.removeItem(StorageNames.login)
@ -65,13 +103,26 @@ export const removeUser = () => {
localStorage.removeItem(StorageNames.user)
}
/**
* Получить объект настроек таблицы
*
* @param tableName Имя таблицы
* @returns Объект настроек таблицы
*/
export const getTableSettings = (tableName: string): TableSettings => {
const tables = getJSON<TableSettingsStore>(StorageNames.tableSettings) ?? {}
if (!(tableName in tables)) return {}
return wrapValues(tables[tableName] ?? {}, normalizeTableColumn)
}
export const setTableSettings = (tableName: string, settings?: TableSettings | null): boolean => {
/**
* Сохранить объект настроек таблицы
*
* @param tableName Имя таблицы
* @param settings Объект настроек
* @returns `true` если сохранение успешно, иначе `false`
*/
export const setTableSettings = (tableName: string, settings?: TableSettings | null) => {
const currentStore = getJSON<TableSettingsStore>(StorageNames.tableSettings) ?? {}
currentStore[tableName] = wrapValues(settings ?? {}, optimizeTableColumn)
return setJSON(StorageNames.tableSettings, currentStore)
@ -81,4 +132,9 @@ export type DataDashboardNNB = {
}
/**
* Получить настройки панели ННБ
*
* @returns Объект настроек панели ННБ
*/
export const getDashboardNNB = () => getJSON<DataDashboardNNB>(StorageNames.dashboardNNB)

View File

@ -1,10 +1,18 @@
/**
* Фабрика методов обрезки строк по словам с добавлением суффикса
*
* @param maxLength Максимальная длинна строки
* @param separator Разделитель слов в строке
* @param suffix Суффикс строки, отображаемый в случае обрезки
* @returns Метод обрезки строки
*/
export const makeStringCutter = (maxLength: number = 100, separator: string = ' ', suffix: string = '...') => <T,>(comment: T): T | string => {
if (!comment || typeof comment !== 'string' || comment.length <= maxLength)
return comment
if (maxLength <= suffix.length)
return comment // Обрабатываются только строки с длинной выше максимальной
if (maxLength <= suffix.length) // Если максимальная длина меньше длины суффикса вывести начало суффикса длинной `maxLength`
return suffix.substring(0, maxLength)
const lastSep = comment.lastIndexOf(separator, maxLength - suffix.length)
if (lastSep < 0)
const lastSep = comment.lastIndexOf(separator, maxLength - suffix.length) // Ищем последнее разделение слов перед максимальной длинной с вычетом длины суффикса
if (lastSep < 0) // Если разделитель не найден обрезаем само слово и добавляем суффикс
return comment.substring(0, maxLength - suffix.length) + suffix
return comment.substring(0, lastSep) + suffix
return comment.substring(0, lastSep) + suffix // Иначе обрезаем до разделителя и добавляем суффикс
}

View File

@ -1,4 +1,8 @@
/**
* Превращает SVG-элемент в `BLOB`
* @param svg SVG-элемент
* @returns `BLOB` строка
*/
export const svgToDataURL = (svg: SVGSVGElement) => {
const serializer = new XMLSerializer()
let source = serializer.serializeToString(svg)

View File

@ -9,20 +9,32 @@ export type TableColumnSettings = {
export type TableSettings = Record<string, TableColumnSettings>
export type TableSettingsStore = Record<string, TableSettings | null>
/**
* Создаёт объект настроек таблицы исходя из массива столбцов
* @param columns Массив столбцов таблицы
* @returns Объект настроек таблицы
*/
export const makeTableSettings = <T extends object>(columns: TableColumns<T>): TableSettings => {
const settings: TableSettings = {}
columns.forEach((column) => {
if (!column.key) return
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,
title: typeof column.title === 'string' ? column.title : key, // В качестве заголовка невозможно использовать `ReactNode`
visible: column.visible ?? true, // По-умолчанию все столбцы видимые
}
})
return settings
}
/**
* Объединяет несколько объектов настроек таблицы в один
*
* Приоритет объектом снижается с последнего влево
* @param settings Список объектов настроек
* @returns Совмещённый объект настроек
*/
export const mergeTableSettings = (...settings: TableSettings[]): TableSettings => {
const newSettings: TableSettings = {}
for (const setting of settings) {
@ -36,17 +48,36 @@ export const mergeTableSettings = (...settings: TableSettings[]): TableSettings
return newSettings
}
/**
* Расширяет настройки столбца, полученные из хранилища
* @param column Настройки столбца
* @param name Имя столбца в случае его отсутствия в настройках
* @returns Расширенные настройки столбца
*/
export const normalizeTableColumn = (column: TableColumnSettings, name?: string): TableColumnSettings => ({
...column,
columnName: column.columnName ?? name,
visible: column.visible ?? true,
})
/**
* Подготавливает настройки столбца к записи в хранилище
*
* @param column Настройки столбца
* @returns Подготовленные настройки столбца
*/
export const optimizeTableColumn = (column: TableColumnSettings): TableColumnSettings => ({
...column,
visible: column.visible ?? true,
})
/**
* Применяет настройки таблицы к списку столбцов
*
* @param columns Список столбцов таблицы
* @param settings Объект настройки таблицы
* @returns Список столбцов с настройками
*/
export const applyTableSettings = <T extends object>(columns: TableColumns<T>, settings: TableSettings): TableColumns<T> => {
let newColumns: TableColumns<T> = columns.map((column) => ({ ...column }))
newColumns = newColumns.filter((column) => {

View File

@ -3,7 +3,7 @@ import { useMemo } from 'react'
import { ArgumentTypes } from '@utils/types'
/**
* Значение типа может быть представлено непосредственно значением либо функцией его возвращаюшей
* Значение типа может быть представлено непосредственно значением либо функцией его возвращающей
*/
export type ReturnType<T> = T extends (...args: any) => infer R ? R : any
export type FunctionalValue<F extends Function> = ReturnType<F> | F
@ -11,7 +11,7 @@ export type FunctionalValue<F extends Function> = ReturnType<F> | F
export const getFunctionalValue = <F extends Function>(value: FunctionalValue<F>) => value instanceof Function ? value : ((...args: ArgumentTypes<F>) => value) as unknown as F
/**
* Облегчает работу со значениями, которые могут быть представлены функциям.
* Облегчает работу со значениями, которые могут быть представлены функциям
*
* @param value Значение или функция его возвращающая
* @returns Функция, вызов которой вернёт искомое значение

View File

@ -1,10 +1,16 @@
export type TaskHandler<T> = () => T | PromiseLike<T>
export type Queue<T> = {
/** Метод добавления задачи в очередь */
push: (task: TaskHandler<T>) => Promise<T>
/** Длина очереди */
readonly length: number
}
/**
* Фабрика очередей задач
* @returns Очередь задач
*/
export const makeTaskQueue = <T,>(): Queue<T> => {
let pending: Promise<T | void> = Promise.resolve()
let count: number = 0