@j2inn/fin5-ui-utils 0.0.1-alpha.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/README.md +5 -0
- package/dist/fantomProps/fantomPropsToObject.d.ts +8 -0
- package/dist/fantomProps/fantomPropsToObject.js +175 -0
- package/dist/fantomProps/generateJsonFromFantomPropsFile.d.ts +1 -0
- package/dist/fantomProps/generateJsonFromFantomPropsFile.js +8 -0
- package/dist/fantomProps/index.d.ts +3 -0
- package/dist/fantomProps/index.js +3 -0
- package/dist/fantomProps/readFantomPropsFile.d.ts +5 -0
- package/dist/fantomProps/readFantomPropsFile.js +32 -0
- package/dist/fin5Top/fin5Top.d.ts +111 -0
- package/dist/fin5Top/fin5Top.js +52 -0
- package/dist/fin5Top/getFin5BinUrl.d.ts +2 -0
- package/dist/fin5Top/getFin5BinUrl.js +2 -0
- package/dist/fin5Top/index.d.ts +4 -0
- package/dist/fin5Top/index.js +4 -0
- package/dist/fin5Top/openFin5Alarm.d.ts +22 -0
- package/dist/fin5Top/openFin5Alarm.js +17 -0
- package/dist/fin5Top/openFin5Historian.d.ts +3 -0
- package/dist/fin5Top/openFin5Historian.js +12 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/react/components/DefaultErrorBoundary/index.d.ts +19 -0
- package/dist/react/components/DefaultErrorBoundary/index.jsx +35 -0
- package/dist/react/components/LoadingSpinner/index.d.ts +3 -0
- package/dist/react/components/LoadingSpinner/index.jsx +6 -0
- package/dist/react/components/index.d.ts +5 -0
- package/dist/react/components/index.js +5 -0
- package/dist/react/components/navigation/BasicLayout/index.d.ts +11 -0
- package/dist/react/components/navigation/BasicLayout/index.jsx +74 -0
- package/dist/react/components/navigation/MenuPage/index.d.ts +33 -0
- package/dist/react/components/navigation/MenuPage/index.jsx +27 -0
- package/dist/react/components/navigation/Router.d.ts +12 -0
- package/dist/react/components/navigation/Router.jsx +15 -0
- package/package.json +118 -0
package/README.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface Props {
|
|
2
|
+
[key: string]: string;
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Convert the given fantom props as string to JS object (@see https://fantom.org/doc/sys/InStream#readProps)
|
|
6
|
+
*/
|
|
7
|
+
export default function fantomPropsToObject(props: string): Props;
|
|
8
|
+
export declare function parseFantomProps(readChar: () => number, unreadChar: (char: number) => void): Props;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert the given fantom props as string to JS object (@see https://fantom.org/doc/sys/InStream#readProps)
|
|
3
|
+
*/
|
|
4
|
+
export default function fantomPropsToObject(props) {
|
|
5
|
+
let i = 0;
|
|
6
|
+
const readChar = () => {
|
|
7
|
+
if (i < props.length) {
|
|
8
|
+
i++;
|
|
9
|
+
return props.charCodeAt(i - 1);
|
|
10
|
+
}
|
|
11
|
+
else
|
|
12
|
+
return -1;
|
|
13
|
+
};
|
|
14
|
+
const unreadChar = () => {
|
|
15
|
+
if (i > 0) {
|
|
16
|
+
i--;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
return parseFantomProps(readChar, unreadChar);
|
|
20
|
+
}
|
|
21
|
+
var UTF_8;
|
|
22
|
+
(function (UTF_8) {
|
|
23
|
+
UTF_8[UTF_8["tab"] = 9] = "tab";
|
|
24
|
+
UTF_8[UTF_8["lf"] = 10] = "lf";
|
|
25
|
+
UTF_8[UTF_8["cr"] = 13] = "cr";
|
|
26
|
+
UTF_8[UTF_8["space"] = 32] = "space";
|
|
27
|
+
UTF_8[UTF_8["numberSign"] = 35] = "numberSign";
|
|
28
|
+
UTF_8[UTF_8["asterisk"] = 42] = "asterisk";
|
|
29
|
+
UTF_8[UTF_8["solidus"] = 47] = "solidus";
|
|
30
|
+
UTF_8[UTF_8["equalSign"] = 61] = "equalSign";
|
|
31
|
+
UTF_8[UTF_8["reverseSolidus"] = 92] = "reverseSolidus";
|
|
32
|
+
UTF_8[UTF_8["n"] = 110] = "n";
|
|
33
|
+
UTF_8[UTF_8["r"] = 114] = "r";
|
|
34
|
+
UTF_8[UTF_8["t"] = 116] = "t";
|
|
35
|
+
UTF_8[UTF_8["u"] = 117] = "u";
|
|
36
|
+
})(UTF_8 || (UTF_8 = {}));
|
|
37
|
+
const isSpace = (char) => char === UTF_8.space ||
|
|
38
|
+
char === UTF_8.tab ||
|
|
39
|
+
char === UTF_8.lf ||
|
|
40
|
+
char === UTF_8.cr;
|
|
41
|
+
const hex = function (c) {
|
|
42
|
+
if (48 <= c && c <= 57)
|
|
43
|
+
return c - 48;
|
|
44
|
+
if (97 <= c && c <= 102)
|
|
45
|
+
return c - 97 + 10;
|
|
46
|
+
if (65 <= c && c <= 70)
|
|
47
|
+
return c - 65 + 10;
|
|
48
|
+
return -1;
|
|
49
|
+
};
|
|
50
|
+
/*
|
|
51
|
+
* Parse the Fantom props file character by character.
|
|
52
|
+
* Implementation based on https://github.com/fantom-lang/fantom/blob/master/src/sys/js/fan/InStream.js
|
|
53
|
+
*/
|
|
54
|
+
export function parseFantomProps(readChar, unreadChar) {
|
|
55
|
+
const props = {};
|
|
56
|
+
let name = '';
|
|
57
|
+
let value = null;
|
|
58
|
+
let inBlockComment = 0;
|
|
59
|
+
let inEndOfLineComment = false;
|
|
60
|
+
let char = UTF_8.space, lastChar = UTF_8.space;
|
|
61
|
+
let lineNum = 1;
|
|
62
|
+
while (true) {
|
|
63
|
+
lastChar = char;
|
|
64
|
+
char = readChar();
|
|
65
|
+
if (char < 0)
|
|
66
|
+
break;
|
|
67
|
+
// end of line
|
|
68
|
+
if (char == UTF_8.lf || char == UTF_8.cr) {
|
|
69
|
+
inEndOfLineComment = false;
|
|
70
|
+
if (lastChar == UTF_8.cr && char == UTF_8.lf)
|
|
71
|
+
continue;
|
|
72
|
+
const n = name.trim();
|
|
73
|
+
if (value !== null) {
|
|
74
|
+
props[n] = value.trim();
|
|
75
|
+
name = '';
|
|
76
|
+
value = null;
|
|
77
|
+
}
|
|
78
|
+
else if (n.length > 0)
|
|
79
|
+
throw new Error('Invalid name/value pair [Line ' + lineNum + ']');
|
|
80
|
+
lineNum++;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
// if in comment
|
|
84
|
+
if (inEndOfLineComment)
|
|
85
|
+
continue;
|
|
86
|
+
// block comment
|
|
87
|
+
if (inBlockComment > 0) {
|
|
88
|
+
if (lastChar == UTF_8.solidus && char == UTF_8.asterisk)
|
|
89
|
+
inBlockComment++;
|
|
90
|
+
if (lastChar == UTF_8.asterisk && char == UTF_8.solidus)
|
|
91
|
+
inBlockComment--;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
// equal
|
|
95
|
+
if (char == UTF_8.equalSign && value === null) {
|
|
96
|
+
value = '';
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
// line comment
|
|
100
|
+
if (char === UTF_8.numberSign &&
|
|
101
|
+
(lastChar == UTF_8.lf || lastChar == UTF_8.cr)) {
|
|
102
|
+
inEndOfLineComment = true;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
// end of line comment
|
|
106
|
+
if (char == UTF_8.solidus && isSpace(lastChar)) {
|
|
107
|
+
const peek = readChar();
|
|
108
|
+
if (peek < 0)
|
|
109
|
+
break;
|
|
110
|
+
if (peek == UTF_8.solidus) {
|
|
111
|
+
inEndOfLineComment = true;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (peek == UTF_8.asterisk) {
|
|
115
|
+
inBlockComment++;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
unreadChar(peek);
|
|
119
|
+
}
|
|
120
|
+
// escape or line continuation
|
|
121
|
+
if (char == UTF_8.reverseSolidus) {
|
|
122
|
+
let peek = readChar();
|
|
123
|
+
if (peek < 0)
|
|
124
|
+
break;
|
|
125
|
+
else if (peek == UTF_8.n)
|
|
126
|
+
char = UTF_8.lf;
|
|
127
|
+
else if (peek == UTF_8.r)
|
|
128
|
+
char = UTF_8.cr;
|
|
129
|
+
else if (peek == UTF_8.t)
|
|
130
|
+
char = UTF_8.tab;
|
|
131
|
+
else if (peek == UTF_8.reverseSolidus)
|
|
132
|
+
char = UTF_8.reverseSolidus;
|
|
133
|
+
else if (peek == UTF_8.cr || peek == UTF_8.lf) {
|
|
134
|
+
// line continuation
|
|
135
|
+
lineNum++;
|
|
136
|
+
if (peek == UTF_8.cr) {
|
|
137
|
+
peek = readChar();
|
|
138
|
+
if (peek != UTF_8.lf)
|
|
139
|
+
unreadChar(peek);
|
|
140
|
+
}
|
|
141
|
+
while (true) {
|
|
142
|
+
peek = readChar();
|
|
143
|
+
if (peek == UTF_8.space || peek == UTF_8.tab)
|
|
144
|
+
continue;
|
|
145
|
+
unreadChar(peek);
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
else if (peek == UTF_8.u) {
|
|
151
|
+
const n3 = hex(readChar());
|
|
152
|
+
const n2 = hex(readChar());
|
|
153
|
+
const n1 = hex(readChar());
|
|
154
|
+
const n0 = hex(readChar());
|
|
155
|
+
if (n3 < 0 || n2 < 0 || n1 < 0 || n0 < 0)
|
|
156
|
+
throw new Error('Invalid hex value for \\uxxxx [Line ' + lineNum + ']');
|
|
157
|
+
char = (n3 << 12) | (n2 << 8) | (n1 << 4) | n0;
|
|
158
|
+
}
|
|
159
|
+
else
|
|
160
|
+
throw new Error('Invalid escape sequence [Line ' + lineNum + ']');
|
|
161
|
+
}
|
|
162
|
+
// normal character
|
|
163
|
+
if (value === null)
|
|
164
|
+
name += String.fromCharCode(char);
|
|
165
|
+
else
|
|
166
|
+
value += String.fromCharCode(char);
|
|
167
|
+
}
|
|
168
|
+
const n = name.trim();
|
|
169
|
+
if (value !== null) {
|
|
170
|
+
props[n] = value.trim();
|
|
171
|
+
}
|
|
172
|
+
else if (n.length > 0)
|
|
173
|
+
throw new Error('Invalid name/value pair [Line ' + lineNum + ']');
|
|
174
|
+
return props;
|
|
175
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function generateJsonFromFantomPropsFile(inputPath?: string, outputPath?: string): Promise<void>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import readFantomPropsFile from './readFantomPropsFile';
|
|
3
|
+
export default async function generateJsonFromFantomPropsFile(inputPath = '../locale/en.props', outputPath = './src/localeKeys.json') {
|
|
4
|
+
const result = await readFantomPropsFile(inputPath);
|
|
5
|
+
fs.writeFile(outputPath, JSON.stringify(result, null, '\t'), () => {
|
|
6
|
+
return;
|
|
7
|
+
});
|
|
8
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import { parseFantomProps } from './fantomPropsToObject';
|
|
3
|
+
/**
|
|
4
|
+
* Asynchronously read the given Fantom props file to JS object (@see https://fantom.org/doc/sys/InStream#readProps)
|
|
5
|
+
*/
|
|
6
|
+
export default function readFantomPropsFile(path) {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const fileStream = fs.createReadStream(path);
|
|
9
|
+
fileStream.once('readable', function () {
|
|
10
|
+
const readChar = () => {
|
|
11
|
+
return fileStream.read(1)?.[0] ?? -1;
|
|
12
|
+
};
|
|
13
|
+
const unreadChar = (char) => {
|
|
14
|
+
return fileStream.unshift(new Uint8Array([char]));
|
|
15
|
+
};
|
|
16
|
+
try {
|
|
17
|
+
resolve(parseFantomProps(readChar, unreadChar));
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
reject(error);
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
try {
|
|
24
|
+
fileStream.close();
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
console.error(err);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { AlarmFilter } from './openFin5Alarm';
|
|
2
|
+
import { HistorianArgs, HistorianType } from './openFin5Historian';
|
|
3
|
+
export declare const getFin5top: (notifyFailure?: () => void) => Fin5Top | null;
|
|
4
|
+
declare const fin5Top: Fin5Top | null;
|
|
5
|
+
export default fin5Top;
|
|
6
|
+
export declare function isWindowTopFin5(notifyFailure?: () => void): boolean;
|
|
7
|
+
export interface Fin5Top {
|
|
8
|
+
app: Fin5App;
|
|
9
|
+
languageManager: LanguageManager;
|
|
10
|
+
finstack?: {
|
|
11
|
+
projectName?: string;
|
|
12
|
+
device?: {
|
|
13
|
+
currentUser?: {
|
|
14
|
+
toObj: () => unknown;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export interface Fin5App {
|
|
20
|
+
/**
|
|
21
|
+
* Toggles FIN right side menu.
|
|
22
|
+
* The forceTo parameter can be used to enforce a specific status instead of a toggling behaviour,
|
|
23
|
+
* where: true -> menu closed, false -> menu open
|
|
24
|
+
*/
|
|
25
|
+
ToggleCollapse: (forceTo: boolean) => void;
|
|
26
|
+
/**
|
|
27
|
+
* Returns current target record id.
|
|
28
|
+
*/
|
|
29
|
+
TargetRef: () => TargetRef;
|
|
30
|
+
/**
|
|
31
|
+
* Updates the current target record id.
|
|
32
|
+
* Navigates the current app to the specified
|
|
33
|
+
* <code>targetRef</code>. Target Ref is the id of any equip/site etc..
|
|
34
|
+
*/
|
|
35
|
+
NavigateToTargetRef: (targetRef: TargetRef, maintainView: boolean) => void;
|
|
36
|
+
/**
|
|
37
|
+
* Navigates to a particular application (trends, notes .. etc) under a specific target.
|
|
38
|
+
* @param {Fin5AppNames} appName name of the application
|
|
39
|
+
* @param {TargetRef} targetRef id of the target. Is the id of the equip/site.. etc to where the app should navigate
|
|
40
|
+
* @param {boolean} skipRef is the flag indicating where to ignore the target ref if not mentioned. If this flag is true and the target ref is not provided, then the navigation will be made to the root target ref and will not be obtained from where the app already is
|
|
41
|
+
*/
|
|
42
|
+
NavigateToApp: (appName: Fin5AppNames, targetRef?: TargetRef, skipRef?: boolean, params?: string[]) => Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* Method to open a url on the application LHS. The LHS here is the <code>iframe</code> where all
|
|
45
|
+
* the graphics etc are opened.
|
|
46
|
+
*
|
|
47
|
+
* @method LoadApplication
|
|
48
|
+
* @param {string} url - Is the url which is to be loaded
|
|
49
|
+
* @param {string} title - Specifies the title to be displayed on the iframe object
|
|
50
|
+
* @param {string} targetId - Specifies the id of the equip (etc) to which the main navigation update.
|
|
51
|
+
* @param {object} settings - extra settings that can be passed in when calling hte func
|
|
52
|
+
* @param {boolean} settings.collapse - flag indicating the collapse state of the RHS app panel
|
|
53
|
+
*/
|
|
54
|
+
LoadApplication: (url: string, title?: string, targetRef?: TargetRef, settings?: {
|
|
55
|
+
collapse?: boolean;
|
|
56
|
+
}) => Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Get the filters object from the alarms application.
|
|
59
|
+
*/
|
|
60
|
+
GetAlarmFilters: () => AlarmFilter;
|
|
61
|
+
/**
|
|
62
|
+
* Sets the filter settings for the alarms.
|
|
63
|
+
* @param {AlarmFilter} filters Is the object that defines the filter properties.
|
|
64
|
+
* @param {boolean} historical Flag indicating if the filter properties need to be applied to the historical alarms.
|
|
65
|
+
*/
|
|
66
|
+
SetAlarmFilters: (filters: AlarmFilter, historical?: boolean) => void;
|
|
67
|
+
/**
|
|
68
|
+
* Navigates to the historian app and loads the graphs as per the passed arguments. The graph will be drawn for the passed "point" ids or for a "query" id or for a "string" query
|
|
69
|
+
* @param {HistorianType} type is the type of graph arg. Available args are:
|
|
70
|
+
* - point : the args will have point ids in an array
|
|
71
|
+
* - query : the arg will be the id of a saved chart query
|
|
72
|
+
* - string : the arg will be a query string
|
|
73
|
+
* @param {HistorianArgs} args is the array/string for point ids, the query id or the raw query string
|
|
74
|
+
*/
|
|
75
|
+
OpenHistorianWithOptions: (type: HistorianType, args: HistorianArgs) => void;
|
|
76
|
+
findComponent: (name: string) => Fin5AppComponent;
|
|
77
|
+
/**
|
|
78
|
+
* @see https://ractive.js.org/api/#ractiveobserve
|
|
79
|
+
*/
|
|
80
|
+
observe: (keypath: string, callback: (newValue: unknown, oldValue: unknown, keypath: string) => void) => {
|
|
81
|
+
cancel: () => void;
|
|
82
|
+
};
|
|
83
|
+
set: (name: string, value: unknown) => Promise<undefined>;
|
|
84
|
+
get: (name: string) => Promise<unknown>;
|
|
85
|
+
APP_NAMES: typeof Fin5AppNames;
|
|
86
|
+
}
|
|
87
|
+
export declare enum Fin5AppNames {
|
|
88
|
+
EQUIP = "equip",
|
|
89
|
+
GRAPHICS = "graphics",
|
|
90
|
+
POINTS = "points",
|
|
91
|
+
ALARMS = "alarms",
|
|
92
|
+
NOTES = "notes",
|
|
93
|
+
SUMMARY = "summary",
|
|
94
|
+
SUMMARY_TOPICS = "summaryList",
|
|
95
|
+
TOOLS = "tools",
|
|
96
|
+
SCHEDULES = "schedules",
|
|
97
|
+
SCHEDULE_LIST = "scheduleList",
|
|
98
|
+
HISTORIAN = "trends",
|
|
99
|
+
TREND_LIST = "trendList",
|
|
100
|
+
SEARCH = "search",
|
|
101
|
+
LOGOUT = "logout",
|
|
102
|
+
GENERIC = "generic",
|
|
103
|
+
REPORTS = "reports"
|
|
104
|
+
}
|
|
105
|
+
interface LanguageManager {
|
|
106
|
+
currentLang: string;
|
|
107
|
+
}
|
|
108
|
+
interface Fin5AppComponent {
|
|
109
|
+
set: (value: unknown) => Promise<undefined>;
|
|
110
|
+
}
|
|
111
|
+
export declare type TargetRef = string | null | undefined;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
class Fin5TopRetriever {
|
|
2
|
+
static _fin5Top;
|
|
3
|
+
static get fin5Top() {
|
|
4
|
+
return this.getFin5top();
|
|
5
|
+
}
|
|
6
|
+
static getFin5top = (notifyFailure) => {
|
|
7
|
+
if (this._fin5Top === undefined) {
|
|
8
|
+
this._fin5Top = isWindowTopFin5(notifyFailure)
|
|
9
|
+
? window.top // Cast to unknown to obscure properties from Window to simplify usage.
|
|
10
|
+
: null;
|
|
11
|
+
}
|
|
12
|
+
return this._fin5Top;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export const getFin5top = Fin5TopRetriever.getFin5top;
|
|
16
|
+
const fin5Top = Fin5TopRetriever.fin5Top;
|
|
17
|
+
export default fin5Top;
|
|
18
|
+
export function isWindowTopFin5(notifyFailure) {
|
|
19
|
+
try {
|
|
20
|
+
window?.top?.origin;
|
|
21
|
+
if (window.top.finstack) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
throw new Error('FIN 5 top not available');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
console.warn(err);
|
|
30
|
+
notifyFailure?.();
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export var Fin5AppNames;
|
|
35
|
+
(function (Fin5AppNames) {
|
|
36
|
+
Fin5AppNames["EQUIP"] = "equip";
|
|
37
|
+
Fin5AppNames["GRAPHICS"] = "graphics";
|
|
38
|
+
Fin5AppNames["POINTS"] = "points";
|
|
39
|
+
Fin5AppNames["ALARMS"] = "alarms";
|
|
40
|
+
Fin5AppNames["NOTES"] = "notes";
|
|
41
|
+
Fin5AppNames["SUMMARY"] = "summary";
|
|
42
|
+
Fin5AppNames["SUMMARY_TOPICS"] = "summaryList";
|
|
43
|
+
Fin5AppNames["TOOLS"] = "tools";
|
|
44
|
+
Fin5AppNames["SCHEDULES"] = "schedules";
|
|
45
|
+
Fin5AppNames["SCHEDULE_LIST"] = "scheduleList";
|
|
46
|
+
Fin5AppNames["HISTORIAN"] = "trends";
|
|
47
|
+
Fin5AppNames["TREND_LIST"] = "trendList";
|
|
48
|
+
Fin5AppNames["SEARCH"] = "search";
|
|
49
|
+
Fin5AppNames["LOGOUT"] = "logout";
|
|
50
|
+
Fin5AppNames["GENERIC"] = "generic";
|
|
51
|
+
Fin5AppNames["REPORTS"] = "reports";
|
|
52
|
+
})(Fin5AppNames || (Fin5AppNames = {}));
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { TargetRef } from './fin5Top';
|
|
2
|
+
export interface AlarmFilterTypes {
|
|
3
|
+
alarms: boolean;
|
|
4
|
+
events: boolean;
|
|
5
|
+
acks: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface AlarmFilter {
|
|
8
|
+
range?: string;
|
|
9
|
+
priorityThreshold?: string;
|
|
10
|
+
acked?: boolean;
|
|
11
|
+
unacked?: boolean;
|
|
12
|
+
inAlarm?: boolean;
|
|
13
|
+
notInAlarm?: boolean;
|
|
14
|
+
search?: string;
|
|
15
|
+
types?: AlarmFilterTypes;
|
|
16
|
+
}
|
|
17
|
+
export declare type AlarmOptions = {
|
|
18
|
+
targetRef?: TargetRef;
|
|
19
|
+
filter: AlarmFilter;
|
|
20
|
+
historical?: boolean;
|
|
21
|
+
};
|
|
22
|
+
export declare const openFin5AlarmsApp: ({ targetRef, filter, historical, }: AlarmOptions) => Promise<void>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Alarms
|
|
3
|
+
//////////////////////////////////////////////////////////////////////////
|
|
4
|
+
import { getFin5top } from './fin5Top';
|
|
5
|
+
export const openFin5AlarmsApp = async ({ targetRef, filter, historical = false, }) => {
|
|
6
|
+
const fin5top = getFin5top();
|
|
7
|
+
await fin5top?.app.NavigateToApp(fin5top?.app?.APP_NAMES?.ALARMS, targetRef);
|
|
8
|
+
fin5top?.app.ToggleCollapse(false);
|
|
9
|
+
let alarmFilters = fin5top?.app?.GetAlarmFilters() ?? {};
|
|
10
|
+
if (filter) {
|
|
11
|
+
alarmFilters = {
|
|
12
|
+
...alarmFilters,
|
|
13
|
+
...filter,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
fin5top?.app?.SetAlarmFilters(alarmFilters, historical);
|
|
17
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Historian
|
|
3
|
+
//////////////////////////////////////////////////////////////////////////
|
|
4
|
+
import fin5Top from './fin5Top';
|
|
5
|
+
export const openFin5HistorianApp = async (queryType, args) => {
|
|
6
|
+
if (fin5Top) {
|
|
7
|
+
queryType && args
|
|
8
|
+
? fin5Top.app.OpenHistorianWithOptions(queryType, args)
|
|
9
|
+
: fin5Top.app.NavigateToApp(fin5Top.app.APP_NAMES.TREND_LIST);
|
|
10
|
+
fin5Top?.app.ToggleCollapse(false);
|
|
11
|
+
}
|
|
12
|
+
};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React, { ErrorInfo, ReactNode } from 'react';
|
|
2
|
+
interface ErrorBoundaryState {
|
|
3
|
+
error?: Error;
|
|
4
|
+
}
|
|
5
|
+
interface ErrorBoundaryProps {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
export declare class DefaultErrorBoundary extends React.Component<ErrorBoundaryProps> {
|
|
9
|
+
state: ErrorBoundaryState;
|
|
10
|
+
static getDerivedStateFromError(error: Error): ErrorBoundaryState;
|
|
11
|
+
componentDidCatch(error: Error, errorInfo: ErrorInfo): void;
|
|
12
|
+
render(): ReactNode;
|
|
13
|
+
}
|
|
14
|
+
export interface ErrorDisplayerProps {
|
|
15
|
+
error: Error;
|
|
16
|
+
extra: React.ReactNode;
|
|
17
|
+
}
|
|
18
|
+
export declare const ErrorDisplayer: React.FC<ErrorDisplayerProps>;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Button, Collapse, Container, Result, Typography } from '@j2inn/ui';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
export class DefaultErrorBoundary extends React.Component {
|
|
4
|
+
state = {};
|
|
5
|
+
static getDerivedStateFromError(error) {
|
|
6
|
+
// Update state so the next render will show the fallback UI.
|
|
7
|
+
return { error: error };
|
|
8
|
+
}
|
|
9
|
+
componentDidCatch(error, errorInfo) {
|
|
10
|
+
// You can also log the error to an error reporting service
|
|
11
|
+
console.error(error, errorInfo);
|
|
12
|
+
}
|
|
13
|
+
render() {
|
|
14
|
+
if (this.state.error) {
|
|
15
|
+
// You can render any custom fallback UI
|
|
16
|
+
return (<ErrorDisplayer error={this.state.error} extra={[
|
|
17
|
+
<Button type='primary' key='refresh' onClick={() => window.location.reload()}>
|
|
18
|
+
Refresh The Page
|
|
19
|
+
</Button>,
|
|
20
|
+
]}/>);
|
|
21
|
+
}
|
|
22
|
+
return this.props.children;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export const ErrorDisplayer = ({ error, extra, }) => {
|
|
26
|
+
return (<Result status='error' title='Ouch... Something Went Wrong' subTitle={<Container center>
|
|
27
|
+
<Collapse style={{ width: 600, textAlign: 'left' }}>
|
|
28
|
+
<Collapse.Panel header={`${error.toString()}`} key='1'>
|
|
29
|
+
<Typography.Text style={{ width: 400 }}>
|
|
30
|
+
{error.stack}
|
|
31
|
+
</Typography.Text>
|
|
32
|
+
</Collapse.Panel>
|
|
33
|
+
</Collapse>
|
|
34
|
+
</Container>} extra={extra}/>);
|
|
35
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { MenuProps } from 'antd';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { MenuPage } from '../MenuPage';
|
|
4
|
+
export interface BasicLayoutProps {
|
|
5
|
+
pages: MenuPage[] & [MenuPage];
|
|
6
|
+
defaultPage?: string;
|
|
7
|
+
onPageChange?: (page: string) => void;
|
|
8
|
+
uncollapsedMenuWidth?: number;
|
|
9
|
+
menuProps: Omit<MenuProps, 'items' | 'selectedKeys' | 'onSelect'>;
|
|
10
|
+
}
|
|
11
|
+
export declare const BasicLayout: React.FC<BasicLayoutProps>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons';
|
|
2
|
+
import { Button, Layout, Menu } from '@j2inn/ui';
|
|
3
|
+
import React, { useEffect, useState } from 'react';
|
|
4
|
+
import { ErrorDisplayer } from '../../DefaultErrorBoundary';
|
|
5
|
+
import { getPage } from '../MenuPage';
|
|
6
|
+
import { Router } from '../Router';
|
|
7
|
+
// Workaround j2inn/ui exporting problem
|
|
8
|
+
const Header = Layout.Sider;
|
|
9
|
+
const Sider = Layout.Sider;
|
|
10
|
+
const Content = Layout.Sider;
|
|
11
|
+
const DEFAULT_UNCOLLAPSED_MENU_WIDTH = 45;
|
|
12
|
+
const COLLAPSED_MENU_WIDTH = 0;
|
|
13
|
+
export const BasicLayout = ({ pages, defaultPage = pages[0].key, onPageChange, uncollapsedMenuWidth = DEFAULT_UNCOLLAPSED_MENU_WIDTH, menuProps, }) => {
|
|
14
|
+
const [currentPage, setCurrentPage] = useState(defaultPage);
|
|
15
|
+
// Fire onChange
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
onPageChange?.(currentPage);
|
|
18
|
+
}, [currentPage]);
|
|
19
|
+
// manage mobile navigation
|
|
20
|
+
const [isMobile, setIsMobile] = useState(false);
|
|
21
|
+
const [hiddenSider, setHiddenSider] = useState(true);
|
|
22
|
+
const actualMenuWidth = hiddenSider && isMobile ? COLLAPSED_MENU_WIDTH : uncollapsedMenuWidth;
|
|
23
|
+
return (<Layout hasSider>
|
|
24
|
+
{isMobile && (<Header style={{
|
|
25
|
+
position: 'fixed',
|
|
26
|
+
width: '100%',
|
|
27
|
+
zIndex: 1000,
|
|
28
|
+
}}>
|
|
29
|
+
{React.createElement(hiddenSider ? MenuUnfoldOutlined : MenuFoldOutlined, {
|
|
30
|
+
className: 'antd-menu-custom-header-trigger',
|
|
31
|
+
onClick: () => {
|
|
32
|
+
setHiddenSider(!hiddenSider);
|
|
33
|
+
},
|
|
34
|
+
})}
|
|
35
|
+
</Header>)}
|
|
36
|
+
<Sider breakpoint='md' onBreakpoint={(broken) => {
|
|
37
|
+
setIsMobile(broken);
|
|
38
|
+
setHiddenSider(true);
|
|
39
|
+
}} collapsed={true} collapsedWidth={actualMenuWidth} trigger={null} style={{
|
|
40
|
+
overflow: 'auto',
|
|
41
|
+
height: '100vh',
|
|
42
|
+
position: 'fixed',
|
|
43
|
+
left: 0,
|
|
44
|
+
top: 0,
|
|
45
|
+
bottom: 0,
|
|
46
|
+
zIndex: 999,
|
|
47
|
+
paddingTop: isMobile
|
|
48
|
+
? uncollapsedMenuWidth
|
|
49
|
+
: COLLAPSED_MENU_WIDTH,
|
|
50
|
+
}}>
|
|
51
|
+
<Menu mode='inline' {...menuProps} items={pages} selectedKeys={[currentPage]} onSelect={({ key }) => {
|
|
52
|
+
if (!hiddenSider) {
|
|
53
|
+
setHiddenSider(true);
|
|
54
|
+
}
|
|
55
|
+
const page = getPage(pages, key);
|
|
56
|
+
if (page?.component) {
|
|
57
|
+
setCurrentPage(key);
|
|
58
|
+
}
|
|
59
|
+
}}/>
|
|
60
|
+
</Sider>
|
|
61
|
+
<Content style={{
|
|
62
|
+
paddingLeft: actualMenuWidth,
|
|
63
|
+
paddingTop: isMobile
|
|
64
|
+
? uncollapsedMenuWidth
|
|
65
|
+
: COLLAPSED_MENU_WIDTH,
|
|
66
|
+
}}>
|
|
67
|
+
<Router pages={pages} currentPage={currentPage ?? defaultPage} fallbackComponent={<ErrorDisplayer error={new Error('Page Not Found')} extra={[
|
|
68
|
+
<Button type='primary' key='refresh' onClick={() => setCurrentPage(defaultPage)}>
|
|
69
|
+
Go Home
|
|
70
|
+
</Button>,
|
|
71
|
+
]}/>}/>
|
|
72
|
+
</Content>
|
|
73
|
+
</Layout>);
|
|
74
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ItemType } from 'antd/lib/menu/hooks/useItems';
|
|
2
|
+
import React, { PropsWithChildren } from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* Menu page has both the data required by the menu and the data required to actually render the page.
|
|
5
|
+
*/
|
|
6
|
+
export declare type MenuPage<T = Record<string, unknown>> = ItemType & Page<T>;
|
|
7
|
+
/**
|
|
8
|
+
* Menu Item that represents an application page
|
|
9
|
+
*/
|
|
10
|
+
interface Page<T = Record<string, unknown>> {
|
|
11
|
+
key: string;
|
|
12
|
+
component?: React.LazyExoticComponent<React.FC<T>>;
|
|
13
|
+
props?: PropsWithChildren<T>;
|
|
14
|
+
children?: Page<T>[];
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Retrieve a specific page or subPage from a root page list
|
|
19
|
+
* @param name the name of the searched page
|
|
20
|
+
* @param pages the list of root pages
|
|
21
|
+
*/
|
|
22
|
+
export declare function getPage<T extends MenuPage>(pages: T[], name?: string): T | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Expands the list of pages to include all the subpages
|
|
25
|
+
*/
|
|
26
|
+
export declare function pageTreeToPageList<T extends MenuPage>(pages: T[]): T[];
|
|
27
|
+
/**
|
|
28
|
+
* Recursively get all the subPages of a root page
|
|
29
|
+
* @param page root page
|
|
30
|
+
* @returns the whole tree of subPages as a list
|
|
31
|
+
*/
|
|
32
|
+
export declare function getAllSubPages<T extends MenuPage>(page: T): T[];
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retrieve a specific page or subPage from a root page list
|
|
3
|
+
* @param name the name of the searched page
|
|
4
|
+
* @param pages the list of root pages
|
|
5
|
+
*/
|
|
6
|
+
export function getPage(pages, name) {
|
|
7
|
+
return pageTreeToPageList(pages).find((page) => page.key === name && !page.disabled);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Expands the list of pages to include all the subpages
|
|
11
|
+
*/
|
|
12
|
+
export function pageTreeToPageList(pages) {
|
|
13
|
+
return pages.flatMap((page) => getAllSubPages(page));
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Recursively get all the subPages of a root page
|
|
17
|
+
* @param page root page
|
|
18
|
+
* @returns the whole tree of subPages as a list
|
|
19
|
+
*/
|
|
20
|
+
export function getAllSubPages(page) {
|
|
21
|
+
if (page.children) {
|
|
22
|
+
return [page].concat(...page.children.map((subPage) => getAllSubPages(subPage)));
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
return [page];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { PropsWithChildren, ReactElement } from 'react';
|
|
2
|
+
import { MenuPage } from './MenuPage';
|
|
3
|
+
export interface RouterProps<T extends MenuPage> {
|
|
4
|
+
pages: T[];
|
|
5
|
+
currentPage: string;
|
|
6
|
+
onPageChange?: (selectedPage?: T) => void;
|
|
7
|
+
fallbackComponent?: ReactElement;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Renders the current selected page from a tree of pages
|
|
11
|
+
*/
|
|
12
|
+
export declare function Router<T extends MenuPage>({ pages, currentPage, onPageChange, fallbackComponent, }: PropsWithChildren<RouterProps<T>>): JSX.Element;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React, { Suspense, useEffect, useMemo, } from 'react';
|
|
2
|
+
import LoadingSpinner from '../LoadingSpinner';
|
|
3
|
+
import { getPage } from './MenuPage';
|
|
4
|
+
/**
|
|
5
|
+
* Renders the current selected page from a tree of pages
|
|
6
|
+
*/
|
|
7
|
+
export function Router({ pages, currentPage, onPageChange, fallbackComponent, }) {
|
|
8
|
+
const page = useMemo(() => getPage(pages, currentPage), [pages, currentPage]);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
onPageChange?.(page);
|
|
11
|
+
}, [page]);
|
|
12
|
+
return page?.component ? (<Suspense fallback={<LoadingSpinner />}>
|
|
13
|
+
{React.createElement(page.component, page?.props)}
|
|
14
|
+
</Suspense>) : (fallbackComponent ?? <div>{currentPage} page not found</div>);
|
|
15
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@j2inn/fin5-ui-utils",
|
|
3
|
+
"version": "0.0.1-alpha.1",
|
|
4
|
+
"description": "A set of useful client-side utilities useful for creating UI applications on top of FIN 5",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"start": "webpack serve --config node_modules/@j2inn/react-config/webpack.config.js",
|
|
9
|
+
"clean": "rimraf ./dist",
|
|
10
|
+
"doc": "typedoc ./src --excludePrivate",
|
|
11
|
+
"prebuild": "npm run clean",
|
|
12
|
+
"build": "tsc -d --outDir dist",
|
|
13
|
+
"checktypes": "tsc --noEmit",
|
|
14
|
+
"format": "prettier-eslint --list-different --write \"$(pwd)/src/**/*.{ts,tsx,js,jsx}\"",
|
|
15
|
+
"lint": "eslint --fix --ext ts,tsx,js,jsx src/",
|
|
16
|
+
"lint-staged": "lint-staged",
|
|
17
|
+
"analyze": "webpack --analyze --config node_modules/@j2inn/react-config/webpack.config.js",
|
|
18
|
+
"test": "cross-env NODE_ENV=test && npm run test:jest",
|
|
19
|
+
"test:jest": "jest --passWithNoTests",
|
|
20
|
+
"prepack": "npm run lint && npm test && npm run build"
|
|
21
|
+
},
|
|
22
|
+
"author": "",
|
|
23
|
+
"license": "ISC",
|
|
24
|
+
"files": [
|
|
25
|
+
"dist/**/*"
|
|
26
|
+
],
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@babel/cli": "^7.14.3",
|
|
29
|
+
"@babel/core": "^7.14.3",
|
|
30
|
+
"@babel/eslint-parser": "^7.14.4",
|
|
31
|
+
"@babel/plugin-proposal-class-properties": "^7.13.0",
|
|
32
|
+
"@babel/plugin-proposal-decorators": "^7.14.2",
|
|
33
|
+
"@babel/plugin-proposal-export-default-from": "^7.12.13",
|
|
34
|
+
"@babel/plugin-proposal-function-bind": "^7.12.13",
|
|
35
|
+
"@babel/plugin-proposal-object-rest-spread": "^7.14.4",
|
|
36
|
+
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
|
37
|
+
"@babel/plugin-transform-react-inline-elements": "^7.12.13",
|
|
38
|
+
"@babel/plugin-transform-runtime": "^7.14.3",
|
|
39
|
+
"@babel/preset-env": "^7.14.4",
|
|
40
|
+
"@babel/preset-react": "^7.13.13",
|
|
41
|
+
"@babel/preset-typescript": "^7.13.0",
|
|
42
|
+
"@j2inn/react-config": "^4.0.10",
|
|
43
|
+
"@svgr/webpack": "^5.5.0",
|
|
44
|
+
"@types/jest": "^27.4.0",
|
|
45
|
+
"@types/node": "^15.0.2",
|
|
46
|
+
"@types/react": "^17.0.8",
|
|
47
|
+
"@types/react-dom": "^17.0.5",
|
|
48
|
+
"@typescript-eslint/eslint-plugin": "^5.28.0",
|
|
49
|
+
"@typescript-eslint/parser": "^5.28.0",
|
|
50
|
+
"babel-eslint": "^10.1.0",
|
|
51
|
+
"babel-loader": "^8.2.2",
|
|
52
|
+
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
|
|
53
|
+
"babel-plugin-react-html-attrs": "^3.0.5",
|
|
54
|
+
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
|
|
55
|
+
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
|
|
56
|
+
"bundle-loader": "^0.5.6",
|
|
57
|
+
"cpy": "^8.1.2",
|
|
58
|
+
"cpy-cli": "^3.1.1",
|
|
59
|
+
"cross-env": "^7.0.3",
|
|
60
|
+
"css-loader": "^5.2.6",
|
|
61
|
+
"css-modules-typescript-loader": "^4.0.1",
|
|
62
|
+
"cypress": "^7.4.0",
|
|
63
|
+
"eslint": "^7.27.0",
|
|
64
|
+
"eslint-config-prettier": "^8.3.0",
|
|
65
|
+
"eslint-plugin-jest": "^24.3.6",
|
|
66
|
+
"eslint-plugin-prettier": "^4.0.0",
|
|
67
|
+
"eslint-plugin-react": "^7.24.0",
|
|
68
|
+
"extract-css-chunks-webpack-plugin": "^4.9.0",
|
|
69
|
+
"file-loader": "^6.2.0",
|
|
70
|
+
"html-webpack-plugin": "^5.3.1",
|
|
71
|
+
"husky": "^6.0.0",
|
|
72
|
+
"jest": "^27.0.3",
|
|
73
|
+
"jest-chain": "^1.1.5",
|
|
74
|
+
"jest-css-modules": "^2.1.0",
|
|
75
|
+
"jest-css-modules-transform": "^4.2.1",
|
|
76
|
+
"jest-extended": "^0.11.5",
|
|
77
|
+
"less": "^4.1.1",
|
|
78
|
+
"less-loader": "^9.0.0",
|
|
79
|
+
"lint-staged": "^11.0.0",
|
|
80
|
+
"lodash-webpack-plugin": "^0.11.6",
|
|
81
|
+
"loglevel": "^1.7.1",
|
|
82
|
+
"mini-css-extract-plugin": "^1.6.0",
|
|
83
|
+
"moment-locales-webpack-plugin": "^1.2.0",
|
|
84
|
+
"monaco-editor-webpack-plugin": "^3.1.0",
|
|
85
|
+
"nib": "^1.1.2",
|
|
86
|
+
"prettier-eslint": "^12.0.0",
|
|
87
|
+
"prettier-eslint-cli": "^5.0.1",
|
|
88
|
+
"raw-loader": "^4.0.2",
|
|
89
|
+
"rimraf": "^3.0.2",
|
|
90
|
+
"style-loader": "^2.0.0",
|
|
91
|
+
"stylus": "^0.54.8",
|
|
92
|
+
"stylus-loader": "^6.0.0",
|
|
93
|
+
"ts-jest": "^27.1.3",
|
|
94
|
+
"typedoc": "^0.22.5",
|
|
95
|
+
"typescript": "^4.3.2",
|
|
96
|
+
"url-loader": "^4.1.1",
|
|
97
|
+
"webpack": "^5.38.1",
|
|
98
|
+
"webpack-bundle-analyzer": "^4.4.1",
|
|
99
|
+
"webpack-cli": "^4.7.0",
|
|
100
|
+
"webpack-dev-server": "^3.11.2"
|
|
101
|
+
},
|
|
102
|
+
"dependencies": {
|
|
103
|
+
"@j2inn/ui": "^5.0.0-beta.6",
|
|
104
|
+
"@j2inn/utils": "^5.0.6",
|
|
105
|
+
"haystack-core": "^2.0.29",
|
|
106
|
+
"haystack-nclient": "^3.0.12",
|
|
107
|
+
"haystack-react": "^3.0.3",
|
|
108
|
+
"haystack-units": "^1.0.12",
|
|
109
|
+
"react": "^17.0.2",
|
|
110
|
+
"react-dom": "^17.0.2"
|
|
111
|
+
},
|
|
112
|
+
"lint-staged": {
|
|
113
|
+
"**/*.[jt]s?(x)": [
|
|
114
|
+
"prettier-eslint --write",
|
|
115
|
+
"eslint --fix"
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
}
|