@boarteam/boar-pack-common-frontend 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/package.json +50 -0
  2. package/src/components/Descriptions/Descriptions.tsx +166 -0
  3. package/src/components/Descriptions/DescriptionsCreateModal.tsx +65 -0
  4. package/src/components/Descriptions/descriptionTypes.ts +49 -0
  5. package/src/components/Descriptions/index.ts +5 -0
  6. package/src/components/Descriptions/useDescriptionColumns.ts +37 -0
  7. package/src/components/Inputs/DateRange.tsx +75 -0
  8. package/src/components/Inputs/MultiStringSelect.tsx +20 -0
  9. package/src/components/Inputs/NumberInputHandlingNewRecord.tsx +11 -0
  10. package/src/components/Inputs/NumberSwitcher.tsx +27 -0
  11. package/src/components/Inputs/Password.tsx +33 -0
  12. package/src/components/Inputs/RelationSelect.tsx +72 -0
  13. package/src/components/Inputs/SearchSelect.tsx +93 -0
  14. package/src/components/Inputs/filterDropdowns.tsx +103 -0
  15. package/src/components/Inputs/index.ts +9 -0
  16. package/src/components/Inputs/useCheckConnection.tsx +79 -0
  17. package/src/components/List/List.tsx +266 -0
  18. package/src/components/List/index.ts +3 -0
  19. package/src/components/List/listTypes.ts +31 -0
  20. package/src/components/QuestionMarkHint/QuestionMarkHint.tsx +33 -0
  21. package/src/components/QuestionMarkHint/index.ts +1 -0
  22. package/src/components/Table/BulkDeleteButton.tsx +55 -0
  23. package/src/components/Table/BulkEditButton.tsx +160 -0
  24. package/src/components/Table/Table.tsx +400 -0
  25. package/src/components/Table/index.ts +6 -0
  26. package/src/components/Table/tableTools.ts +168 -0
  27. package/src/components/Table/tableTypes.ts +127 -0
  28. package/src/components/Table/useColumnsSets.tsx +110 -0
  29. package/src/components/index.ts +5 -0
  30. package/src/index.ts +2 -0
  31. package/src/tools/WebsocketClient.ts +138 -0
  32. package/src/tools/index.ts +5 -0
  33. package/src/tools/numberTools.ts +6 -0
  34. package/src/tools/safetyRun.ts +5 -0
  35. package/src/tools/useFullscreen.tsx +62 -0
  36. package/src/tools/useTabs.ts +17 -0
@@ -0,0 +1,168 @@
1
+ import { CondOperator, QueryJoin, SCondition } from "@nestjsx/crud-request";
2
+ import { IWithId, TFilters, TSearchableColumn } from "./tableTypes";
3
+ import React, { Key } from "react";
4
+ import { TColumnsStates } from "./useColumnsSets";
5
+
6
+ export function getFiltersSearch({
7
+ baseFilters = {},
8
+ filters = {},
9
+ searchableColumns,
10
+ }: {
11
+ baseFilters?: TFilters,
12
+ filters?: TFilters,
13
+ searchableColumns: TSearchableColumn[],
14
+ }): SCondition {
15
+ const filterKeys = new Set(Object.keys(filters).concat(Object.keys(baseFilters)));
16
+ const search: SCondition = { '$and': [] };
17
+ searchableColumns.forEach((col) => {
18
+ const colDataIndex = Array.isArray(col.field) ? col.field.join('.') : col.field;
19
+ const field = col.filterField || colDataIndex;
20
+ let operator = col.filterOperator || col.operator;
21
+ let value = filters[colDataIndex] || baseFilters[colDataIndex];
22
+ filterKeys.delete(colDataIndex);
23
+ if (!value || col.numeric && !Number.isFinite(Number(value))) {
24
+ return;
25
+ }
26
+
27
+ if (operator === Operators.between) {
28
+ if (value?.[0] === undefined) {
29
+ operator = Operators.lowerOrEquals;
30
+ value = value?.[1];
31
+ }
32
+ else if (value?.[1] === undefined) {
33
+ operator = Operators.greaterOrEquals;
34
+ value = value?.[0];
35
+ }
36
+ }
37
+
38
+ search.$and?.push({ [field]: { [operator]: value } });
39
+ });
40
+
41
+ if (filterKeys.size) {
42
+ throw new Error(`Some filters are not defined in searchableColumns: ${Array.from(filterKeys).join(', ')}`);
43
+ }
44
+
45
+ return search;
46
+ }
47
+
48
+ export const Operators = {
49
+ containsLow: CondOperator.CONTAINS_LOW,
50
+ contains: CondOperator.CONTAINS,
51
+ equals: CondOperator.EQUALS,
52
+ in: CondOperator.IN,
53
+ inLow: CondOperator.IN_LOW,
54
+ between: CondOperator.BETWEEN,
55
+ greaterOrEquals: CondOperator.GREATER_THAN_EQUALS,
56
+ lowerOrEquals: CondOperator.LOWER_THAN_EQUALS,
57
+ } as const;
58
+
59
+ export function applyKeywordToSearch(
60
+ filterSearch: SCondition,
61
+ searchableColumns: TSearchableColumn[],
62
+ columnsState?: TColumnsStates,
63
+ keyword?: string,
64
+ ): SCondition {
65
+ if (!keyword) {
66
+ return filterSearch;
67
+ }
68
+
69
+ const keywordSearches: SCondition[] = [];
70
+ keyword.split(' ').forEach((word) => {
71
+ const keywordSearch: SCondition = { $or: [] };
72
+ searchableColumns!.forEach((col) => {
73
+ if (col.searchField === null) {
74
+ return;
75
+ }
76
+
77
+ const dataIndex = Array.isArray(col.field) ? col.field.join(',') : col.field;
78
+ if (columnsState?.[dataIndex] && !columnsState[dataIndex].show) {
79
+ return;
80
+ }
81
+
82
+ const field = col.searchField || (Array.isArray(col.field) ? col.field.join('.') : col.field);
83
+ const operator = col.operator;
84
+
85
+ if (!col.numeric || Number.isFinite(Number(word))) {
86
+ keywordSearch.$or.push({ [field]: { [operator]: word } });
87
+ }
88
+ });
89
+
90
+ keywordSearches.push(keywordSearch);
91
+ });
92
+
93
+ if (!Array.isArray(filterSearch.$and)) {
94
+ throw new Error('Bad format of filter search');
95
+ }
96
+
97
+ return {
98
+ $and: [...filterSearch.$and, ...keywordSearches]
99
+ }
100
+ }
101
+
102
+ export type TIndexableRecord = {
103
+ dataIndex?: Key | Key[];
104
+ children?: TIndexableRecord[] | React.ReactNode;
105
+ };
106
+
107
+ export function collectFieldsFromColumns<T>(
108
+ columns: TIndexableRecord[] | undefined,
109
+ idColumnName: string | string[],
110
+ joinFields: Set<string> = new Set,
111
+ fields: Set<string> = new Set
112
+ ): string[] {
113
+ return [Array.from(buildFieldsFromColumns<T>(columns, idColumnName, joinFields, fields)).join(',')];
114
+ }
115
+
116
+ export function buildFieldsFromColumns<T>(
117
+ columns: TIndexableRecord[] | undefined,
118
+ idColumnName: string | string[],
119
+ joinFields: Set<string> = new Set,
120
+ fields: Set<string> = new Set
121
+ ): Set<string> {
122
+ columns?.forEach(col => {
123
+ if ('children' in col && Array.isArray(col.children)) {
124
+ buildFieldsFromColumns(col.children, idColumnName, joinFields, fields);
125
+ }
126
+
127
+ // skip id column because it is always included by backend
128
+ // and join fields because they are included by join
129
+
130
+ if (!col.dataIndex || (Array.isArray(idColumnName) ? idColumnName.includes(col.dataIndex) : col.dataIndex === idColumnName) || joinFields.has(col.dataIndex as string)) {
131
+ return;
132
+ }
133
+
134
+ fields.add(String(Array.isArray(col.dataIndex) ? col.dataIndex[0] : col.dataIndex));
135
+ });
136
+
137
+ return fields;
138
+ }
139
+
140
+ export function withNumericId<T extends IWithId>(entity: T): T & { id: number } {
141
+ return {
142
+ ...entity,
143
+ id: Number(entity.id),
144
+ };
145
+ }
146
+
147
+ export function buildJoinFields(join?: QueryJoin | QueryJoin[]) {
148
+ const joinFields = new Set<string>();
149
+ let joinSelect: string[] = [];
150
+ if (join) {
151
+ let joinArr = join;
152
+ if (!Array.isArray(joinArr)) {
153
+ joinArr = [joinArr];
154
+ }
155
+ joinSelect = joinArr.map(relation => {
156
+ joinFields.add(relation.field);
157
+ let res = relation.field;
158
+ if (relation.select) {
159
+ res += `||${relation.select.join(',')}`;
160
+ }
161
+ return res;
162
+ });
163
+ }
164
+ return {
165
+ joinSelect,
166
+ joinFields,
167
+ };
168
+ }
@@ -0,0 +1,127 @@
1
+ import React, { MutableRefObject } from "react";
2
+ import { ActionType, ProTableProps } from "@ant-design/pro-table";
3
+ import { QueryJoin, QuerySortArr } from "@nestjsx/crud-request";
4
+ import { Operators } from "./tableTools";
5
+ import { TColumnsSet } from "./useColumnsSets";
6
+ import { ColumnStateType } from "@ant-design/pro-table/es/typing";
7
+ import { RowEditableConfig } from "@ant-design/pro-utils";
8
+
9
+ export type IWithId = {
10
+ id: string | number,
11
+ }
12
+ export type TGetAllParams = {
13
+ /**
14
+ * Selects resource fields. <a href="https://github.com/nestjsx/crud/wiki/Requests#select" target="_blank">Docs</a>
15
+ */
16
+ fields?: Array<string>,
17
+ /**
18
+ * Adds search condition. <a href="https://github.com/nestjsx/crud/wiki/Requests#search" target="_blank">Docs</a>
19
+ */
20
+ s?: string,
21
+ /**
22
+ * Adds filter condition. <a href="https://github.com/nestjsx/crud/wiki/Requests#filter" target="_blank">Docs</a>
23
+ */
24
+ filter?: Array<string>,
25
+ /**
26
+ * Adds OR condition. <a href="https://github.com/nestjsx/crud/wiki/Requests#or" target="_blank">Docs</a>
27
+ */
28
+ or?: Array<string>,
29
+ /**
30
+ * Adds sort by field. <a href="https://github.com/nestjsx/crud/wiki/Requests#sort" target="_blank">Docs</a>
31
+ */
32
+ sort?: Array<string>,
33
+ /**
34
+ * Adds relational resources. <a href="https://github.com/nestjsx/crud/wiki/Requests#join" target="_blank">Docs</a>
35
+ */
36
+ join?: Array<string>,
37
+ /**
38
+ * Limit amount of resources. <a href="https://github.com/nestjsx/crud/wiki/Requests#limit" target="_blank">Docs</a>
39
+ */
40
+ limit?: number,
41
+ /**
42
+ * Offset amount of resources. <a href="https://github.com/nestjsx/crud/wiki/Requests#offset" target="_blank">Docs</a>
43
+ */
44
+ offset?: number,
45
+ /**
46
+ * Page portion of resources. <a href="https://github.com/nestjsx/crud/wiki/Requests#page" target="_blank">Docs</a>
47
+ */
48
+ page?: number,
49
+ /**
50
+ * Reset cache (if was enabled). <a href="https://github.com/nestjsx/crud/wiki/Requests#cache" target="_blank">Docs</a>
51
+ */
52
+ cache?: number,
53
+ }
54
+ export type TFilters = {
55
+ [key: string]: number | string | boolean | (string | number)[] | null;
56
+ }
57
+
58
+ export type TGetRequestParams = {
59
+ baseFilters?: TFilters;
60
+ join?: QueryJoin | QueryJoin[];
61
+ };
62
+ export type TGetAllRequestParams = TGetRequestParams & {
63
+ keyword?: string;
64
+ }
65
+ export type TFilterParams = {
66
+ current?: number;
67
+ pageSize?: number;
68
+ sortMap?: { [key: string]: string };
69
+ } & TGetAllRequestParams;
70
+ export type TSort = {
71
+ [key: string]: 'ascend' | 'descend' | null,
72
+ };
73
+ export type TSearchableColumn = {
74
+ field: string | string[],
75
+ searchField?: string | null,
76
+ operator: typeof Operators[keyof typeof Operators],
77
+ filterField?: string,
78
+ filterOperator?: typeof Operators[keyof typeof Operators],
79
+ numeric?: boolean,
80
+ }
81
+
82
+ interface BaseProps<Entity,
83
+ CreateDto,
84
+ UpdateDto,
85
+ TEntityParams = {},
86
+ TPathParams = {}> extends ProTableProps<Entity, TEntityParams & TFilterParams> {
87
+ getAll: ({}: TGetAllParams & TPathParams) => Promise<{ data: Entity[] }>;
88
+ pathParams: TPathParams;
89
+ idColumnName?: string & keyof Entity | (string & keyof Entity)[];
90
+ createNewDefaultParams?: Partial<Entity>;
91
+ afterSave?: (record: Entity) => Promise<void>;
92
+ actionRef?: MutableRefObject<ActionType | undefined>;
93
+ editable?: RowEditableConfig<Entity>;
94
+ defaultSort?: QuerySortArr;
95
+ searchableColumns?: TSearchableColumn[];
96
+ viewOnly?: boolean;
97
+ columnsSets?: TColumnsSet<Entity>[];
98
+ popupCreation?: boolean;
99
+ columnsState?: ColumnStateType;
100
+ columnsSetSelect?: () => React.ReactNode;
101
+ }
102
+
103
+ interface EditableProps<Entity, CreateDto, UpdateDto, TPathParams = {}> {
104
+ onCreate?: ({}: { requestBody: CreateDto } & TPathParams) => Promise<Entity>;
105
+ onUpdate: ({}: Record<keyof Entity, string> & { requestBody: UpdateDto } & TPathParams) => Promise<Entity>;
106
+ onDelete: ({}: Record<keyof Entity, string> & TPathParams) => Promise<void>;
107
+ entityToCreateDto: (entity: Entity) => CreateDto;
108
+ entityToUpdateDto: (entity: Entity) => UpdateDto;
109
+ onUpdateMany: ({}: Record<keyof Entity, string> & { requestBody: { updateValues: Partial<UpdateDto>[], records: Entity[] } } & TPathParams) => Promise<void>,
110
+ onDeleteMany: ({}: Record<keyof Entity, string> & { requestBody: { records: Entity[] } } & TPathParams) => Promise<void>,
111
+ }
112
+
113
+ // Conditional type to merge base and editable props conditionally
114
+ type ConditionalProps<Entity,
115
+ CreateDto,
116
+ UpdateDto,
117
+ TEntityParams,
118
+ TPathParams> = { viewOnly: true } extends { viewOnly: boolean }
119
+ ? BaseProps<Entity, CreateDto, UpdateDto, TEntityParams, TPathParams> & Partial<EditableProps<Entity, CreateDto, UpdateDto, TPathParams>>
120
+ : BaseProps<Entity, CreateDto, UpdateDto, TEntityParams, TPathParams> & EditableProps<Entity, CreateDto, UpdateDto, TPathParams>;
121
+
122
+ // Main type
123
+ export type TTableProps<Entity,
124
+ CreateDto,
125
+ UpdateDto,
126
+ TEntityParams = {},
127
+ TPathParams = {}> = ConditionalProps<Entity, CreateDto, UpdateDto, TEntityParams, TPathParams>;
@@ -0,0 +1,110 @@
1
+ import { ColumnsState, ProColumns } from "@ant-design/pro-components";
2
+ import React, { useMemo, useState } from "react";
3
+ import { Select } from "antd";
4
+ import { ColumnStateType } from "@ant-design/pro-table/es/typing";
5
+ import { TIndexableRecord } from "./tableTools";
6
+ import QuestionMarkHint from "../QuestionMarkHint/QuestionMarkHint";
7
+ import { SettingOutlined } from "@ant-design/icons";
8
+
9
+ export type TColumnsSet<Entity> = {
10
+ name: string,
11
+ columns: (keyof Entity)[],
12
+ }
13
+
14
+ type TUseColumnsSetsParams<Entity> = {
15
+ columns: ProColumns<Entity>[],
16
+ columnsSets?: TColumnsSet<Entity>[],
17
+ defaultColumnState?: string;
18
+ }
19
+
20
+ export type TColumnsStates = Record<string, ColumnsState>;
21
+
22
+ type TUseColumnsSetsResult<Entity> = {
23
+ columnsSetSelect: () => React.ReactNode,
24
+ chosenColumnsSet: TColumnsStates | undefined,
25
+ setChosenColumnsSet: React.Dispatch<React.SetStateAction<TColumnsStates | undefined>>,
26
+ setChosenColumnsSetByName: (value: string) => void,
27
+ columnsState: ColumnStateType,
28
+ }
29
+
30
+ type TColumnsState = Partial<Record<string, ColumnsState>>;
31
+
32
+ function getColumnsStates<T>(
33
+ columns: TIndexableRecord[],
34
+ shownCols: Set<keyof T>,
35
+ state: TColumnsState = {},
36
+ ): Record<string, ColumnsState> {
37
+ columns.forEach(col => {
38
+ const idx = Array.isArray(col.dataIndex) ? col.dataIndex.join(',') : col.dataIndex;
39
+ if ('children' in col && Array.isArray(col.children)) {
40
+ getColumnsStates(col.children, shownCols, state);
41
+ }
42
+
43
+ if (idx && !shownCols.has(idx as keyof T)) {
44
+ state[idx as string] = { show: false };
45
+ }
46
+ }, state);
47
+
48
+ return state as Record<string, ColumnsState>;
49
+ }
50
+
51
+ export default function useColumnsSets<Entity>({
52
+ columns,
53
+ columnsSets,
54
+ defaultColumnState,
55
+ }: TUseColumnsSetsParams<Entity>): TUseColumnsSetsResult<Entity> {
56
+ const columnsSetsByName: Map<string, TColumnsStates> = useMemo(
57
+ () => new Map<string, TColumnsStates>(
58
+ columnsSets?.map(({
59
+ name,
60
+ columns: columnsSet
61
+ }) => [
62
+ name,
63
+ getColumnsStates<Entity>(columns, new Set(columnsSet))
64
+ ])
65
+ ), [columns]
66
+ );
67
+
68
+ const [chosenSetName, setChosenSetName] = useState<string | undefined>(
69
+ defaultColumnState || columnsSets?.[0].name || undefined
70
+ );
71
+ const [chosenColumnsSet, setChosenColumnsSet] = useState<TColumnsStates | undefined>(
72
+ columnsSetsByName.get(chosenSetName || '') || undefined
73
+ );
74
+
75
+ const setChosenColumnsSetByName = (value: string) => {
76
+ setChosenSetName(value);
77
+ setChosenColumnsSet(columnsSetsByName.get(value));
78
+ }
79
+
80
+ const options = Array.from(columnsSetsByName.keys()).map(name => ({
81
+ value: name,
82
+ label: name,
83
+ }));
84
+
85
+ const columnsSetSelect = () => columnsSetsByName.size > 1 ? <>
86
+ <Select
87
+ key="columnsSetSelect"
88
+ style={{ width: 200 }}
89
+ value={chosenSetName}
90
+ onChange={(value: string) => setChosenColumnsSetByName(value)}
91
+ options={options}
92
+ />
93
+ <QuestionMarkHint intlPrefix={'tables.columnsSetSelect'} values={{
94
+ gearIcon: <SettingOutlined />,
95
+ }} />
96
+ </> : null;
97
+
98
+ const columnsState = {
99
+ value: chosenColumnsSet,
100
+ onChange: setChosenColumnsSet,
101
+ };
102
+
103
+ return {
104
+ columnsSetSelect,
105
+ chosenColumnsSet,
106
+ setChosenColumnsSet,
107
+ setChosenColumnsSetByName,
108
+ columnsState,
109
+ }
110
+ }
@@ -0,0 +1,5 @@
1
+ export * from './Descriptions';
2
+ export * from './Inputs';
3
+ export * from './QuestionMarkHint';
4
+ export * from './Table';
5
+ export * from './List';
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './components/index';
2
+ export * from './tools/index';
@@ -0,0 +1,138 @@
1
+ import { TIncomeEvent } from "../../../liquidity-manager-frontend/src/components/RealTimeData/RealTimeDataSource";
2
+ import { message } from "antd";
3
+
4
+ export enum WsErrorCodes {
5
+ ConnectionClosed = 1000,
6
+ InvalidJson = 4000,
7
+ ErrorMessage = 4001,
8
+ Unauthorized = 4003,
9
+ }
10
+
11
+ export class WebsocketClient {
12
+ private socket: WebSocket | null;
13
+ private reconnectTimeout: number | undefined;
14
+ private readonly worker: null | string;
15
+ private serverSocketStatus: WebSocket['readyState'] = WebSocket.CLOSED;
16
+
17
+ private readonly closeHandler: (event: CloseEvent) => void;
18
+ private readonly openHandler: () => void;
19
+ private readonly messageHandler: (msg: TIncomeEvent) => void;
20
+
21
+ constructor({
22
+ worker,
23
+ onOpen,
24
+ onMessage,
25
+ onClose,
26
+ }: {
27
+ worker: null | string;
28
+ onOpen: () => void;
29
+ onMessage: (msg: TIncomeEvent) => void;
30
+ onClose?: (event: CloseEvent) => void;
31
+ }) {
32
+ this.worker = worker;
33
+ this.closeHandler = onClose || (() => {});
34
+ this.openHandler = onOpen;
35
+ this.messageHandler = onMessage;
36
+
37
+ this.connect();
38
+ }
39
+
40
+ get status() {
41
+ return this.serverSocketStatus === WebSocket.OPEN ? this.socket?.readyState : this.serverSocketStatus;
42
+ }
43
+
44
+ private connect() {
45
+ const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
46
+ const url = `${wsProtocol}//${location.host}/ws/${this.worker || 'primary'}/ws`;
47
+ console.log(`QuotesDataSocket: connecting to ${url}...`);
48
+ this.serverSocketStatus = WebSocket.CONNECTING;
49
+ this.socket = new WebSocket(url);
50
+ this.socket.addEventListener("open", this.onOpen);
51
+ this.socket.addEventListener("message", this.onMessage);
52
+ this.socket.addEventListener("error", this.onError);
53
+ this.socket.addEventListener("close", this.onClose);
54
+ }
55
+
56
+ private onError = (event: WebSocketEventMap['error']) => {
57
+ console.error(`QuotesDataSocket: error for ${this.worker || 'primary'} socket:`);
58
+ console.error(event);
59
+ this.socket?.close();
60
+ }
61
+
62
+ private onClose = (event: WebSocketEventMap['close']) => {
63
+ console.log(`QuotesDataSocket: ${this.worker || 'primary'} socket closed`);
64
+ this.socket?.removeEventListener("close", this.onClose);
65
+ this.socket?.removeEventListener("error", this.onError);
66
+ this.socket?.removeEventListener("message", this.onMessage);
67
+ this.socket?.removeEventListener("open", this.onOpen);
68
+ this.socket = null;
69
+ this.closeHandler(event);
70
+ }
71
+
72
+ private onOpen = () => {
73
+ this.openHandler();
74
+ }
75
+
76
+ private onMessage = async (event: WebSocketEventMap['message']) => {
77
+ let msg: TIncomeEvent;
78
+
79
+ try {
80
+ msg = JSON.parse(event.data);
81
+ } catch (e) {
82
+ console.error(`Error, while parsing message from WS server ${event.data}`);
83
+ console.error(e);
84
+ this.socket?.close(WsErrorCodes.InvalidJson, "Invalid JSON");
85
+ return;
86
+ }
87
+
88
+ if (msg.event === "error") {
89
+ console.error(`Error from WS server: ${msg.data.message}`);
90
+ this.socket?.close(WsErrorCodes.ErrorMessage, msg.data.message);
91
+ await message.error(`WS server error: ${msg.data.message}`);
92
+ return;
93
+ }
94
+
95
+ if (msg.event === "status") {
96
+ this.serverSocketStatus = msg.data?.status ?? this.serverSocketStatus;
97
+ }
98
+
99
+ this.messageHandler(msg);
100
+ }
101
+
102
+ public close(): Promise<void> {
103
+ clearTimeout(this.reconnectTimeout);
104
+ if (!this.socket || this.socket.readyState === WebSocket.CLOSED) {
105
+ return Promise.resolve();
106
+ }
107
+
108
+ return new Promise((resolve) => {
109
+ this.socket?.addEventListener("close", () => resolve(), { once: true });
110
+ this.socket?.close();
111
+ });
112
+ }
113
+
114
+ public reconnect(timeout: number) {
115
+ this.reconnectTimeout = setTimeout(() => {
116
+ this.connect();
117
+ }, timeout) as unknown as number;
118
+ }
119
+
120
+ public send<T>(data: T) {
121
+ const send = () => {
122
+ this.socket?.send(JSON.stringify(data));
123
+ }
124
+
125
+ if (this.socket?.readyState !== WebSocket.OPEN) {
126
+ console.warn(`QuotesDataSocket: socket is not ready to send data`);
127
+ this.socket.addEventListener("open", send, { once: true });
128
+ return;
129
+ }
130
+
131
+ try {
132
+ send();
133
+ } catch (e) {
134
+ console.error(`QuotesDataSocket: error, while sending data to WS server`);
135
+ console.error(e);
136
+ }
137
+ }
138
+ }
@@ -0,0 +1,5 @@
1
+ export * from './safetyRun';
2
+ export * from './useFullscreen';
3
+ export * from './numberTools';
4
+ export * from './useTabs';
5
+ export * from './WebsocketClient';
@@ -0,0 +1,6 @@
1
+ export function dropTrailZeroes(str: string | undefined) {
2
+ if (typeof str === 'string' && str.includes(".")) {
3
+ return str.replace(/\.?0*$/, "");
4
+ }
5
+ return str;
6
+ }
@@ -0,0 +1,5 @@
1
+ export default function safetyRun<T>(promise?: Promise<T>): void {
2
+ promise?.catch((e) => {
3
+ console.error(e);
4
+ })
5
+ }
@@ -0,0 +1,62 @@
1
+ import { useEffect, useState } from "react";
2
+ import { FullscreenExitOutlined, FullscreenOutlined } from "@ant-design/icons";
3
+ import { Button } from "antd";
4
+ import { createStyles } from "antd-style";
5
+
6
+ function changeFullScreen(fullscreen: boolean) {
7
+ if (fullscreen) {
8
+ document.documentElement.requestFullscreen().catch((e) => {
9
+ console.log(e);
10
+ });
11
+ } else if (document.fullscreenElement) {
12
+ document.exitFullscreen().catch((e) => {
13
+ console.log(e);
14
+ });
15
+ }
16
+ }
17
+
18
+ const useStyles = createStyles(({ token }) => {
19
+ return {
20
+ fullscreen: {
21
+ backgroundColor: token.colorBgLayout,
22
+ position: 'fixed',
23
+ top: 0,
24
+ left: 0,
25
+ zIndex: 100,
26
+ width: '100%',
27
+ height: '100%',
28
+ overflow: 'auto',
29
+ },
30
+ notFullscreen: {},
31
+ };
32
+ });
33
+
34
+ export function useFullscreen() {
35
+ const [isFullscreen, setIsFullscreen] = useState(false);
36
+ const { styles } = useStyles();
37
+
38
+ useEffect(() => {
39
+ const listener = () => {
40
+ setIsFullscreen(!!document.fullscreenElement);
41
+ };
42
+ document.addEventListener('fullscreenchange', listener);
43
+ return () => {
44
+ document.removeEventListener('fullscreenchange', listener);
45
+ }
46
+ }, []);
47
+
48
+ const button = <Button
49
+ key="fullscreen"
50
+ type="text"
51
+ onClick={() => changeFullScreen(!isFullscreen)}
52
+ >
53
+ {isFullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
54
+ </Button>;
55
+
56
+ return {
57
+ isFullscreen,
58
+ setIsFullscreen,
59
+ fullscreenClassName: isFullscreen ? styles.fullscreen : styles.notFullscreen,
60
+ fullscreenButton: button,
61
+ };
62
+ }
@@ -0,0 +1,17 @@
1
+ import { useSearchParams } from "react-router-dom";
2
+ import React, { useEffect, useState } from "react";
3
+
4
+ export function useTabs<T extends string>(defaultTab: T): [T, React.Dispatch<React.SetStateAction<T>>] {
5
+ const [searchParams, setSearchParams] = useSearchParams();
6
+ const [tab, setTab] = useState<T>(() => {
7
+ return searchParams.get('tab') as T ?? defaultTab;
8
+ });
9
+
10
+ useEffect(() => {
11
+ const newSearchParams = new URLSearchParams(searchParams);
12
+ newSearchParams.set('tab', tab);
13
+ setSearchParams(newSearchParams, { replace: true });
14
+ }, [tab]);
15
+
16
+ return [tab, setTab];
17
+ }