@stellar-expert/ui-framework 1.16.7 → 1.17.0
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/CLAUDE.md +35 -0
- package/README.md +1125 -3
- package/account/account-address.js +127 -127
- package/account/available-balance.js +22 -22
- package/account/identicon.js +90 -90
- package/account/signer-key.js +65 -64
- package/api/explorer-api-hooks.js +209 -202
- package/api/explorer-api-paginated-list-hooks.js +441 -440
- package/api/explorer-batch-info-loader.js +111 -85
- package/api/explorer-tx-api.js +28 -28
- package/api/ledger-stream.js +15 -0
- package/asset/amount.js +56 -56
- package/asset/asset-icon.js +41 -41
- package/asset/asset-issuer.js +21 -21
- package/asset/asset-link.js +107 -107
- package/asset/asset-list-hooks.js +59 -59
- package/asset/asset-meta-hooks.js +88 -88
- package/asset/asset-selector.js +72 -71
- package/basic-styles/base.scss +50 -50
- package/basic-styles/grid.scss +1 -1
- package/basic-styles/table.scss +1 -1
- package/claimable-balance/claimable-balance-claimants.js +5 -0
- package/contract/contract-api.js +15 -0
- package/contract/invocation-info-view.js +10 -2
- package/contract/sc-val.js +24 -0
- package/controls/button-group.js +25 -19
- package/controls/button-group.scss +46 -46
- package/controls/button.js +93 -78
- package/controls/button.scss +173 -173
- package/controls/code-block.js +42 -34
- package/controls/code-block.scss +71 -71
- package/controls/dropdown.js +318 -287
- package/controls/dropdown.scss +159 -159
- package/controls/external-link.js +10 -4
- package/controls/info-tooltip.js +23 -16
- package/controls/slider.js +29 -19
- package/controls/tabs.js +94 -94
- package/controls/tabs.scss +70 -70
- package/controls/tooltip.js +244 -240
- package/controls/tooltip.scss +116 -116
- package/controls/update-highlighter.js +32 -27
- package/controls/update-highlighter.scss +7 -7
- package/date/date-selector.js +56 -54
- package/date/elapsed-time.js +28 -21
- package/dex/price-dynamic.js +53 -44
- package/dex/price-dynamic.scss +33 -33
- package/directory/directory-hooks.js +109 -97
- package/effect/effect-description.js +5 -3
- package/errors/error-boundary.js +110 -97
- package/horizon/horizon-account-helpers.js +104 -104
- package/horizon/horizon-ledger-helpers.js +35 -35
- package/horizon/horizon-trades-helper.js +88 -88
- package/horizon/horizon-transaction-helpers.js +36 -36
- package/index.d.ts +1241 -0
- package/interaction/accordion.js +43 -35
- package/interaction/autofocus.js +13 -9
- package/interaction/block-select.js +64 -53
- package/interaction/block-select.scss +21 -21
- package/interaction/copy-to-clipboard.js +25 -18
- package/interaction/inline-progress.js +20 -15
- package/interaction/qr-code.js +34 -34
- package/interaction/responsive.js +20 -20
- package/interaction/spoiler.js +52 -39
- package/interaction/spoiler.scss +6 -6
- package/interaction/theme-selector.js +13 -10
- package/ledger/ledger-entry-href-formatter.js +4 -3
- package/ledger/ledger-entry-link.js +93 -58
- package/ledger/ledger-info-parser.js +28 -0
- package/meta/page-meta-tags.js +243 -238
- package/module/dynamic-module.js +47 -47
- package/package.json +3 -2
- package/state/on-screen-hooks.js +22 -22
- package/state/page-visibility-helpers.js +29 -16
- package/state/page-visibility-hooks.js +13 -11
- package/state/screen-orientation-hooks.js +19 -15
- package/state/state-hooks.js +76 -76
- package/state/stellar-network-hooks.js +56 -44
- package/state/theme.js +29 -28
- package/stellar/key-type.js +92 -91
- package/stellar/ledger-generic-id.js +39 -39
- package/stellar/signature-hint-utils.js +65 -65
- package/toast/toast-notifications-block.js +59 -59
- package/toast/toast-notifications.scss +1 -1
- package/tx/op-description-view.js +84 -81
- package/tx/op-icon.js +98 -98
- package/tx/parser/op-balance-changes.js +66 -66
- package/tx/parser/op-descriptor.js +51 -48
- package/tx/parser/tx-details-parser.js +81 -81
- package/tx/parser/tx-matcher.js +371 -371
- package/tx/parser/type-filter-matcher.js +126 -126
- package/tx/tx-list-hooks.js +32 -18
- package/tx/tx-operations-list.js +117 -116
|
@@ -1,203 +1,210 @@
|
|
|
1
|
-
import {useEffect, useState} from 'react'
|
|
2
|
-
import isEqual from 'react-fast-compare'
|
|
3
|
-
import {stringifyQuery} from '@stellar-expert/navigation'
|
|
4
|
-
import {addVisibilityChangeListener, isDocumentVisible} from '../state/page-visibility-helpers'
|
|
5
|
-
import {useStellarNetwork} from '../state/stellar-network-hooks'
|
|
6
|
-
import {fetchExplorerApi} from './explorer-api-call'
|
|
7
|
-
import apiCache from './api-cache'
|
|
8
|
-
|
|
9
|
-
export class ExplorerApiResult {
|
|
10
|
-
constructor(apiEndpoint, data, ts) {
|
|
11
|
-
this.apiEndpoint = apiEndpoint
|
|
12
|
-
this.data = data
|
|
13
|
-
this.fetchedAt = ts
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Relative API URL
|
|
18
|
-
* @type {
|
|
19
|
-
*/
|
|
20
|
-
apiEndpoint
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* API response data
|
|
24
|
-
* @type {Object}
|
|
25
|
-
*/
|
|
26
|
-
data = null
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Response error if any
|
|
30
|
-
* @type {
|
|
31
|
-
*/
|
|
32
|
-
error
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* @type {
|
|
36
|
-
*/
|
|
37
|
-
status = 200
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Response timestamp
|
|
41
|
-
* @type {
|
|
42
|
-
*/
|
|
43
|
-
fetchedAt = 0
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Response result
|
|
47
|
-
* @return {
|
|
48
|
-
*/
|
|
49
|
-
get loaded() {
|
|
50
|
-
return !!this.data || !!this.error
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* @private
|
|
55
|
-
*/
|
|
56
|
-
update(data) {
|
|
57
|
-
this.data = data
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function buildApiResult(apiEndpoint, data, ts) {
|
|
62
|
-
return new ExplorerApiResult(apiEndpoint, data, ts)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function buildApiError(apiEndpoint, error, status) {
|
|
66
|
-
const res = new ExplorerApiResult(apiEndpoint, null, Math.round(new Date().getTime() / 1000))
|
|
67
|
-
res.error = error
|
|
68
|
-
res.status = status
|
|
69
|
-
return res
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function setupAutoRefresh(refreshInterval, fetchData) {
|
|
73
|
-
if (!refreshInterval) return () => null
|
|
74
|
-
|
|
75
|
-
let autoRefreshTimer,
|
|
76
|
-
refreshedAt = 0
|
|
77
|
-
|
|
78
|
-
function scheduleAutoRefresh() {
|
|
79
|
-
autoRefreshTimer = setInterval(() => {
|
|
80
|
-
//refresh only when the tab is active
|
|
81
|
-
if (isDocumentVisible()) {
|
|
82
|
-
const now = new Date().getTime()
|
|
83
|
-
if (refreshedAt + (refreshInterval - 1) * 1000 < now) {
|
|
84
|
-
refreshedAt = now
|
|
85
|
-
fetchData()
|
|
86
|
-
}
|
|
87
|
-
} else {
|
|
88
|
-
stopAutoRefresh()
|
|
89
|
-
}
|
|
90
|
-
}, refreshInterval * 1000)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function stopAutoRefresh() {
|
|
94
|
-
clearInterval(autoRefreshTimer)
|
|
95
|
-
autoRefreshTimer = undefined
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const stopVisibilityTracking = addVisibilityChangeListener(visible => {
|
|
99
|
-
if (visible) {
|
|
100
|
-
scheduleAutoRefresh()
|
|
101
|
-
} else {
|
|
102
|
-
stopAutoRefresh()
|
|
103
|
-
}
|
|
104
|
-
})
|
|
105
|
-
if (isDocumentVisible()) {
|
|
106
|
-
scheduleAutoRefresh()
|
|
107
|
-
}
|
|
108
|
-
//return finalizer
|
|
109
|
-
return function () {
|
|
110
|
-
refreshInterval = 0
|
|
111
|
-
stopVisibilityTracking()
|
|
112
|
-
stopAutoRefresh()
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const currentRequests = {}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
return
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
1
|
+
import {useEffect, useState} from 'react'
|
|
2
|
+
import isEqual from 'react-fast-compare'
|
|
3
|
+
import {stringifyQuery} from '@stellar-expert/navigation'
|
|
4
|
+
import {addVisibilityChangeListener, isDocumentVisible} from '../state/page-visibility-helpers'
|
|
5
|
+
import {useStellarNetwork} from '../state/stellar-network-hooks'
|
|
6
|
+
import {fetchExplorerApi} from './explorer-api-call'
|
|
7
|
+
import apiCache from './api-cache'
|
|
8
|
+
|
|
9
|
+
export class ExplorerApiResult {
|
|
10
|
+
constructor(apiEndpoint, data, ts) {
|
|
11
|
+
this.apiEndpoint = apiEndpoint
|
|
12
|
+
this.data = data
|
|
13
|
+
this.fetchedAt = ts
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Relative API URL
|
|
18
|
+
* @type {string}
|
|
19
|
+
*/
|
|
20
|
+
apiEndpoint
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* API response data
|
|
24
|
+
* @type {Object}
|
|
25
|
+
*/
|
|
26
|
+
data = null
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Response error if any
|
|
30
|
+
* @type {string}
|
|
31
|
+
*/
|
|
32
|
+
error
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @type {number}
|
|
36
|
+
*/
|
|
37
|
+
status = 200
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Response timestamp
|
|
41
|
+
* @type {number}
|
|
42
|
+
*/
|
|
43
|
+
fetchedAt = 0
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Response result
|
|
47
|
+
* @return {boolean}
|
|
48
|
+
*/
|
|
49
|
+
get loaded() {
|
|
50
|
+
return !!this.data || !!this.error
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @private
|
|
55
|
+
*/
|
|
56
|
+
update(data) {
|
|
57
|
+
this.data = data
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function buildApiResult(apiEndpoint, data, ts) {
|
|
62
|
+
return new ExplorerApiResult(apiEndpoint, data, ts)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function buildApiError(apiEndpoint, error, status) {
|
|
66
|
+
const res = new ExplorerApiResult(apiEndpoint, null, Math.round(new Date().getTime() / 1000))
|
|
67
|
+
res.error = error
|
|
68
|
+
res.status = status
|
|
69
|
+
return res
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function setupAutoRefresh(refreshInterval, fetchData) {
|
|
73
|
+
if (!refreshInterval) return () => null
|
|
74
|
+
|
|
75
|
+
let autoRefreshTimer,
|
|
76
|
+
refreshedAt = 0
|
|
77
|
+
|
|
78
|
+
function scheduleAutoRefresh() {
|
|
79
|
+
autoRefreshTimer = setInterval(() => {
|
|
80
|
+
//refresh only when the tab is active
|
|
81
|
+
if (isDocumentVisible()) {
|
|
82
|
+
const now = new Date().getTime()
|
|
83
|
+
if (refreshedAt + (refreshInterval - 1) * 1000 < now) {
|
|
84
|
+
refreshedAt = now
|
|
85
|
+
fetchData()
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
stopAutoRefresh()
|
|
89
|
+
}
|
|
90
|
+
}, refreshInterval * 1000)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function stopAutoRefresh() {
|
|
94
|
+
clearInterval(autoRefreshTimer)
|
|
95
|
+
autoRefreshTimer = undefined
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const stopVisibilityTracking = addVisibilityChangeListener(visible => {
|
|
99
|
+
if (visible) {
|
|
100
|
+
scheduleAutoRefresh()
|
|
101
|
+
} else {
|
|
102
|
+
stopAutoRefresh()
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
if (isDocumentVisible()) {
|
|
106
|
+
scheduleAutoRefresh()
|
|
107
|
+
}
|
|
108
|
+
//return finalizer
|
|
109
|
+
return function () {
|
|
110
|
+
refreshInterval = 0
|
|
111
|
+
stopVisibilityTracking()
|
|
112
|
+
stopAutoRefresh()
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const currentRequests = {}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Fetch data from the Explorer API with caching support
|
|
120
|
+
* @param {string} url - API endpoint URL (with network prefix)
|
|
121
|
+
* @param {number} [ttl] - Cache time-to-live in seconds
|
|
122
|
+
* @param {function(*): *} [processResult] - Optional callback to transform the response data
|
|
123
|
+
* @return {Promise<{data: *, ts: number}>}
|
|
124
|
+
*/
|
|
125
|
+
export function fetchData(url, ttl, processResult) {
|
|
126
|
+
//try to retrieve data from the browser cache
|
|
127
|
+
const fromCache = apiCache.get(url)
|
|
128
|
+
//check if the cache is fresh enough
|
|
129
|
+
if (fromCache && !fromCache.isExpired) {
|
|
130
|
+
//if the cached data is up to date - just proceed with it
|
|
131
|
+
return Promise.resolve({data: fromCache.data, ts: fromCache.ts})
|
|
132
|
+
}
|
|
133
|
+
const existingRequest = currentRequests[url]
|
|
134
|
+
if (existingRequest) return existingRequest
|
|
135
|
+
//fetch from the server only if there is no data or it is expired
|
|
136
|
+
return currentRequests[url] = fetchExplorerApi(url)
|
|
137
|
+
.then(data => {
|
|
138
|
+
if (typeof processResult === 'function') {
|
|
139
|
+
data = processResult(data)
|
|
140
|
+
}
|
|
141
|
+
//in case of error set small ttl in order to try re-fetching in 4 seconds
|
|
142
|
+
const {ts} = apiCache.set(url, data, data.error ? 4 : ttl)
|
|
143
|
+
delete currentRequests[url]
|
|
144
|
+
return {data, ts}
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
*
|
|
150
|
+
* @param {string|APIEndpointParams} apiEndpoint - Server API endpoint to use as a data source.
|
|
151
|
+
* @param {number} [refreshInterval] - Auto-refresh interval in seconds for dynamic data.
|
|
152
|
+
* @param {number} [ttl] - Cache time-to-live in seconds.
|
|
153
|
+
* @param {function} [processResult] - Callback to process a fetch result.
|
|
154
|
+
* @param {boolean} [allowStaleDataTransition] - Allow stale data to be returned when the url changed and the new data has not been loaded yet.
|
|
155
|
+
* @return {ExplorerApiResult}
|
|
156
|
+
*/
|
|
157
|
+
export function useExplorerApi(apiEndpoint, {refreshInterval, ttl = 60, processResult, allowStaleDataTransition = false} = {}) {
|
|
158
|
+
const network = useStellarNetwork()
|
|
159
|
+
const endpointWithQuery = `${network}/${apiEndpoint}`
|
|
160
|
+
const [apiResponseData, updateApiResponseData] = useState(buildApiResult(endpointWithQuery))
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
let componentUnmounted = false
|
|
163
|
+
updateApiResponseData(buildApiResult(endpointWithQuery))
|
|
164
|
+
|
|
165
|
+
if (!apiEndpoint) {
|
|
166
|
+
updateApiResponseData(buildApiError(endpointWithQuery, 'Not found', 404))
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (typeof apiEndpoint !== 'string') {
|
|
171
|
+
apiEndpoint = apiEndpoint.path + stringifyQuery(apiEndpoint.query)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function load() {
|
|
175
|
+
fetchData(endpointWithQuery, ttl, processResult)
|
|
176
|
+
.then(({data, ts}) => {
|
|
177
|
+
if (componentUnmounted) return
|
|
178
|
+
|
|
179
|
+
const newData = buildApiResult(endpointWithQuery, data, ts)
|
|
180
|
+
|
|
181
|
+
if (!isEqual(newData, apiResponseData)) {
|
|
182
|
+
updateApiResponseData(newData)
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!componentUnmounted) {
|
|
188
|
+
load()
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
//set up auto-refresh
|
|
192
|
+
const stopAutoRefresh = setupAutoRefresh(refreshInterval, load)
|
|
193
|
+
|
|
194
|
+
return () => {
|
|
195
|
+
//finalize
|
|
196
|
+
componentUnmounted = true
|
|
197
|
+
stopAutoRefresh()
|
|
198
|
+
}
|
|
199
|
+
}, [apiEndpoint])
|
|
200
|
+
if (apiResponseData.apiEndpoint !== endpointWithQuery && !allowStaleDataTransition)
|
|
201
|
+
return buildApiResult(endpointWithQuery) //return empty result on URL transition if allowStaleDataTransition not set
|
|
202
|
+
|
|
203
|
+
return apiResponseData
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* @typedef {Object} APIEndpointParams
|
|
208
|
+
* @property {string} path
|
|
209
|
+
* @property {{}} query
|
|
203
210
|
*/
|