@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.
- package/package.json +50 -0
- package/src/components/Descriptions/Descriptions.tsx +166 -0
- package/src/components/Descriptions/DescriptionsCreateModal.tsx +65 -0
- package/src/components/Descriptions/descriptionTypes.ts +49 -0
- package/src/components/Descriptions/index.ts +5 -0
- package/src/components/Descriptions/useDescriptionColumns.ts +37 -0
- package/src/components/Inputs/DateRange.tsx +75 -0
- package/src/components/Inputs/MultiStringSelect.tsx +20 -0
- package/src/components/Inputs/NumberInputHandlingNewRecord.tsx +11 -0
- package/src/components/Inputs/NumberSwitcher.tsx +27 -0
- package/src/components/Inputs/Password.tsx +33 -0
- package/src/components/Inputs/RelationSelect.tsx +72 -0
- package/src/components/Inputs/SearchSelect.tsx +93 -0
- package/src/components/Inputs/filterDropdowns.tsx +103 -0
- package/src/components/Inputs/index.ts +9 -0
- package/src/components/Inputs/useCheckConnection.tsx +79 -0
- package/src/components/List/List.tsx +266 -0
- package/src/components/List/index.ts +3 -0
- package/src/components/List/listTypes.ts +31 -0
- package/src/components/QuestionMarkHint/QuestionMarkHint.tsx +33 -0
- package/src/components/QuestionMarkHint/index.ts +1 -0
- package/src/components/Table/BulkDeleteButton.tsx +55 -0
- package/src/components/Table/BulkEditButton.tsx +160 -0
- package/src/components/Table/Table.tsx +400 -0
- package/src/components/Table/index.ts +6 -0
- package/src/components/Table/tableTools.ts +168 -0
- package/src/components/Table/tableTypes.ts +127 -0
- package/src/components/Table/useColumnsSets.tsx +110 -0
- package/src/components/index.ts +5 -0
- package/src/index.ts +2 -0
- package/src/tools/WebsocketClient.ts +138 -0
- package/src/tools/index.ts +5 -0
- package/src/tools/numberTools.ts +6 -0
- package/src/tools/safetyRun.ts +5 -0
- package/src/tools/useFullscreen.tsx +62 -0
- 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
|
+
}
|
package/src/index.ts
ADDED
|
@@ -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,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
|
+
}
|