@trackunit/react-core-contexts 2.6.17-alpha-62031fc7c43.0 → 2.7.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/index.cjs.js +68 -7
- package/index.esm.js +70 -9
- package/migrations/entry.js.map +1 -0
- package/package.json +8 -8
- package/src/createApolloClient.d.ts +2 -1
- package/src/errorLink/errorLink.d.ts +2 -1
- package/src/errorLink/missingScopeErrors.d.ts +16 -0
- package/src/errorLink/subscriptionErrorLink.d.ts +4 -2
package/index.cjs.js
CHANGED
|
@@ -40,10 +40,25 @@ const FeatureFlagProviderIrisApp = ({ children }) => {
|
|
|
40
40
|
return jsxRuntime.jsx(reactCoreContextsApi.FeatureFlagContextProvider, { value: featureFlags, children: children });
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Picks out the messages that report a missing authorization scope.
|
|
45
|
+
*
|
|
46
|
+
* The server offers no structured signal to key off: `verifyScopes`
|
|
47
|
+
* (libs/graphql/common/src/schemabuilder/AuthorizationDirective.ts) throws a plain
|
|
48
|
+
* `ForbiddenError` whose message is assembled as "Missing " + the allowed scopes joined with
|
|
49
|
+
* " or " + " scope". Matching on that shape is therefore the only option available, and it
|
|
50
|
+
* lives here so the HTTP and subscription links cannot drift apart on what counts as a
|
|
51
|
+
* missing scope.
|
|
52
|
+
*
|
|
53
|
+
* Filtering rather than inspecting the first error matters in both links: a response can
|
|
54
|
+
* carry several errors in any order, since the directive throws per resolved field.
|
|
55
|
+
*/
|
|
56
|
+
const getMissingScopeMessages = (errors) => errors.map(error => error.message).filter(message => message.startsWith("Missing ") && message.endsWith(" scope"));
|
|
57
|
+
|
|
43
58
|
/**
|
|
44
59
|
* This error link is used to capture error information, i. e. traceId, graphQL errors, network errors, etc.
|
|
45
60
|
*/
|
|
46
|
-
const createErrorLink = ({ errorHandler, getToken, }) => {
|
|
61
|
+
const createErrorLink = ({ errorHandler, getToken, showErrorToast, }) => {
|
|
47
62
|
return error.onError(({ graphQLErrors, networkError, operation, response, forward }) => {
|
|
48
63
|
if (networkError) {
|
|
49
64
|
// We skip the error logging if the error is an AbortError
|
|
@@ -70,6 +85,10 @@ const createErrorLink = ({ errorHandler, getToken, }) => {
|
|
|
70
85
|
}
|
|
71
86
|
// eslint-disable-next-line no-console
|
|
72
87
|
console.error(`Error calling: '${operation.getContext().clientAwareness.name}' fetching Data for: ${operation.operationName}`, graphQLErrors);
|
|
88
|
+
const missingScopeMessages = getMissingScopeMessages(graphQLErrors);
|
|
89
|
+
if (missingScopeMessages.length > 0) {
|
|
90
|
+
showErrorToast(new Error(missingScopeMessages.join(", ")));
|
|
91
|
+
}
|
|
73
92
|
/**
|
|
74
93
|
* We want to see the full graphQL error since
|
|
75
94
|
* it contains extra details like the query/mutation
|
|
@@ -120,18 +139,20 @@ const DEFAULT_AUTH_GRACE_MS = 30000;
|
|
|
120
139
|
/**
|
|
121
140
|
* Wraps an SSE subscription link and provides the same error monitoring as the
|
|
122
141
|
* HTTP error link — capturing GraphQL errors, traceIds, FORCE_RELOAD_BROWSER,
|
|
123
|
-
* and UNAUTHENTICATED codes
|
|
142
|
+
* and UNAUTHENTICATED codes, and surfacing missing authorization scopes — for
|
|
143
|
+
* long-lived subscription Observables.
|
|
124
144
|
*
|
|
125
145
|
* UNAUTHENTICATED errors are deferred by a grace period (default 30s) to allow
|
|
126
146
|
* Okta's autoRenew to refresh the token. If a successful response arrives
|
|
127
147
|
* within the window the capture is cancelled; if the timer fires, a single
|
|
128
148
|
* captureException is sent — an actionable signal that token refresh failed.
|
|
129
149
|
*/
|
|
130
|
-
const createSubscriptionErrorLink = ({ errorHandler, getToken, graceMs = DEFAULT_AUTH_GRACE_MS, }) => {
|
|
150
|
+
const createSubscriptionErrorLink = ({ errorHandler, getToken, showErrorToast, graceMs = DEFAULT_AUTH_GRACE_MS, }) => {
|
|
131
151
|
return new client.ApolloLink((operation, forward) => {
|
|
132
152
|
return new client.Observable(observer => {
|
|
133
153
|
let authGraceTimer = null;
|
|
134
154
|
let authErrorReported = false;
|
|
155
|
+
let missingScopeReported = false;
|
|
135
156
|
const clearGraceTimer = () => {
|
|
136
157
|
if (authGraceTimer !== null) {
|
|
137
158
|
clearTimeout(authGraceTimer);
|
|
@@ -161,6 +182,16 @@ const createSubscriptionErrorLink = ({ errorHandler, getToken, graceMs = DEFAULT
|
|
|
161
182
|
level: "error",
|
|
162
183
|
data: { log: JSON.stringify(errors) },
|
|
163
184
|
});
|
|
185
|
+
// A subscription is long-lived and a scope failure is static for the operation, so
|
|
186
|
+
// the same error can arrive on every payload. Report it once per subscription
|
|
187
|
+
// rather than re-raising the toast, which the provider's de-dup would otherwise
|
|
188
|
+
// make visibly disappear and re-enter. Reset when a clean payload arrives, as the
|
|
189
|
+
// auth grace handling below does.
|
|
190
|
+
const missingScopeMessages = getMissingScopeMessages(errors);
|
|
191
|
+
if (missingScopeMessages.length > 0 && !missingScopeReported) {
|
|
192
|
+
missingScopeReported = true;
|
|
193
|
+
showErrorToast(new Error(missingScopeMessages.join(", ")));
|
|
194
|
+
}
|
|
164
195
|
const invalidToken = errors.some(x => x.extensions?.code === "UNAUTHENTICATED" ||
|
|
165
196
|
x.message.includes("Invalid token specified") ||
|
|
166
197
|
x.message.includes("Access denied! You need to be authorized to perform this action!"));
|
|
@@ -180,6 +211,7 @@ const createSubscriptionErrorLink = ({ errorHandler, getToken, graceMs = DEFAULT
|
|
|
180
211
|
else {
|
|
181
212
|
clearGraceTimer();
|
|
182
213
|
authErrorReported = false;
|
|
214
|
+
missingScopeReported = false;
|
|
183
215
|
}
|
|
184
216
|
observer.next(response);
|
|
185
217
|
},
|
|
@@ -281,7 +313,7 @@ const isInternalGqlContext = () => {
|
|
|
281
313
|
/**
|
|
282
314
|
* @internal
|
|
283
315
|
*/
|
|
284
|
-
const createApolloClient = ({ graphqlPublicUrl, graphqlInternalUrl, graphqlReportUrl, isDev, tracingHeaders: initialTracingHeaders, firstToken, errorHandler, }) => {
|
|
316
|
+
const createApolloClient = ({ graphqlPublicUrl, graphqlInternalUrl, graphqlReportUrl, isDev, tracingHeaders: initialTracingHeaders, firstToken, errorHandler, showErrorToast, }) => {
|
|
285
317
|
let token = firstToken;
|
|
286
318
|
let tracingHeaders = initialTracingHeaders;
|
|
287
319
|
const publicGraphQLLink = client.createHttpLink({
|
|
@@ -301,7 +333,7 @@ const createApolloClient = ({ graphqlPublicUrl, graphqlInternalUrl, graphqlRepor
|
|
|
301
333
|
},
|
|
302
334
|
};
|
|
303
335
|
});
|
|
304
|
-
const errorLink = createErrorLink({ errorHandler, getToken: () => token });
|
|
336
|
+
const errorLink = createErrorLink({ errorHandler, getToken: () => token, showErrorToast });
|
|
305
337
|
const defaultOptions = {
|
|
306
338
|
watchQuery: {
|
|
307
339
|
fetchPolicy: "no-cache",
|
|
@@ -339,7 +371,7 @@ const createApolloClient = ({ graphqlPublicUrl, graphqlInternalUrl, graphqlRepor
|
|
|
339
371
|
headers: () => generateHeaders(token, tracingHeaders),
|
|
340
372
|
});
|
|
341
373
|
// Split links based on operation type
|
|
342
|
-
const subscriptionErrorLink = createSubscriptionErrorLink({ errorHandler, getToken: () => token });
|
|
374
|
+
const subscriptionErrorLink = createSubscriptionErrorLink({ errorHandler, getToken: () => token, showErrorToast });
|
|
343
375
|
const splitLink = client.from([
|
|
344
376
|
authLink,
|
|
345
377
|
client.split(({ query }) => {
|
|
@@ -394,6 +426,34 @@ const useApolloClient = () => {
|
|
|
394
426
|
const { graphqlPublicUrl, graphqlInternalUrl, graphqlReportUrl, environment, tracingHeaders } = reactCoreHooks.useEnvironment();
|
|
395
427
|
const { token: currentToken } = reactCoreHooks.useToken();
|
|
396
428
|
const errorHandler = reactCoreHooks.useErrorHandler();
|
|
429
|
+
const { addToast } = reactCoreHooks.useToast();
|
|
430
|
+
const { permissions } = reactCoreHooks.useCurrentUser();
|
|
431
|
+
const showErrorToast = react.useCallback((error) => {
|
|
432
|
+
// Only a developer running an Iris App locally can act on a missing scope, since only
|
|
433
|
+
// they can edit the manifest. The server throws the same message for real customer
|
|
434
|
+
// sessions and for token clients that have no manifest at all, so surfacing it outside
|
|
435
|
+
// local mode tells users to fix something they cannot reach.
|
|
436
|
+
//
|
|
437
|
+
// Read from storage rather than through useIrisAppsSDK().developerMode: that context is
|
|
438
|
+
// created below this provider (it queries GraphQL, so it needs this Apollo client) and
|
|
439
|
+
// lives in a visibility:host library this one cannot import.
|
|
440
|
+
const isDeveloper = permissions?.some(permission => permission === "account.iris.app.developer");
|
|
441
|
+
if (!isDeveloper) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
void addToast({
|
|
445
|
+
// ToastProviderHost de-dups on id *or* title, so both must be constant and
|
|
446
|
+
// specific: a message-derived id churned on every scope combination, and the
|
|
447
|
+
// generic "Error" title evicted unrelated toasts (and was evicted by them).
|
|
448
|
+
id: "apollo-missing-scope",
|
|
449
|
+
intent: "danger",
|
|
450
|
+
title: "Missing scope",
|
|
451
|
+
// A missing scope leaves the view permanently empty, so the only explanation
|
|
452
|
+
// the user gets must not time out.
|
|
453
|
+
behavior: "persistent",
|
|
454
|
+
description: `${error.message} - you must add the scope to your iris app manifest`,
|
|
455
|
+
});
|
|
456
|
+
}, [addToast, permissions]);
|
|
397
457
|
const [client] = react.useState(() => {
|
|
398
458
|
return createApolloClient({
|
|
399
459
|
/* DON'T CHANGE THIS ! its there to ensure we don't recreate apollo client just because the token changes,
|
|
@@ -406,6 +466,7 @@ const useApolloClient = () => {
|
|
|
406
466
|
tracingHeaders,
|
|
407
467
|
firstToken: currentToken,
|
|
408
468
|
errorHandler,
|
|
469
|
+
showErrorToast,
|
|
409
470
|
});
|
|
410
471
|
});
|
|
411
472
|
// Synchronously propagate the token into the Apollo client closure during render
|
|
@@ -973,7 +1034,7 @@ const TrackunitProviders = ({ translations, children, errorHandler }) => {
|
|
|
973
1034
|
i18nLibraryTranslation.registerTranslations(translations); // Register the apps translations if passed.
|
|
974
1035
|
}
|
|
975
1036
|
i18nLibraryTranslation.initializeTranslationsForApp(); // Initialize all registered translations
|
|
976
|
-
return (jsxRuntime.jsx(EnvironmentProviderIrisApp, { children: jsxRuntime.jsx(ErrorHandlingProviderIrisApp, { errorHandler: errorHandler, children: jsxRuntime.jsx(ThemeCssProviderIrisApp, { children: jsxRuntime.jsx(TokenProviderIrisApp, { children: jsxRuntime.jsx(CurrentUserPreferenceProviderIrisApp, { children: jsxRuntime.jsx(CurrentUserProviderIrisApp, { children: jsxRuntime.jsx(UserSubscriptionProviderIrisApp, { children: jsxRuntime.jsx(AnalyticsProviderIrisApp, { children: jsxRuntime.jsx(OemBrandingProviderIrisApp, { children: jsxRuntime.jsx(AssetSortingProviderIrisApp, { children: jsxRuntime.jsx(
|
|
1037
|
+
return (jsxRuntime.jsx(EnvironmentProviderIrisApp, { children: jsxRuntime.jsx(ErrorHandlingProviderIrisApp, { errorHandler: errorHandler, children: jsxRuntime.jsx(ThemeCssProviderIrisApp, { children: jsxRuntime.jsx(TokenProviderIrisApp, { children: jsxRuntime.jsx(CurrentUserPreferenceProviderIrisApp, { children: jsxRuntime.jsx(CurrentUserProviderIrisApp, { children: jsxRuntime.jsx(UserSubscriptionProviderIrisApp, { children: jsxRuntime.jsx(AnalyticsProviderIrisApp, { children: jsxRuntime.jsx(OemBrandingProviderIrisApp, { children: jsxRuntime.jsx(AssetSortingProviderIrisApp, { children: jsxRuntime.jsx(ToastProviderIrisApp, { children: jsxRuntime.jsx(ManagerApolloProvider, { children: jsxRuntime.jsx(NavigationProviderIrisApp, { children: jsxRuntime.jsx(ModalDialogContextProviderIrisApp, { children: jsxRuntime.jsx(ConfirmationDialogProviderIrisApp, { children: jsxRuntime.jsx(FilterBarProviderIrisApp, { children: jsxRuntime.jsx(ExportDataProviderIrisApp, { children: jsxRuntime.jsx(TimeRangeProviderIrisApp, { children: jsxRuntime.jsx(WidgetConfigProviderIrisApp, { children: jsxRuntime.jsx(GeolocationProviderIrisApp, { children: jsxRuntime.jsx(react.Suspense, { fallback: jsxRuntime.jsx(reactComponents.Spinner, { centering: "centered", "data-testid": "trackunit-providers" }), children: jsxRuntime.jsx(FeatureFlagProviderIrisApp, { children: children }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }));
|
|
977
1038
|
};
|
|
978
1039
|
|
|
979
1040
|
exports.FeatureFlagProviderIrisApp = FeatureFlagProviderIrisApp;
|
package/index.esm.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { jsx, Fragment } from 'react/jsx-runtime';
|
|
2
2
|
import { FeatureFlagRuntime, ToastRuntime, AnalyticsRuntime, registerHostChangeHandler, AssetSortingRuntime, ConfirmationDialogRuntime, EnvironmentRuntime, ExportDataRuntime, AssetsFilterBarRuntime, CustomersFilterBarRuntime, SitesFilterBarRuntime, GeolocationRuntime, ModalDialogRuntime, NavigationRuntime, OemBrandingRuntime, ThemeCssRuntime, TimeRangeRuntime, TokenRuntime, CurrentUserRuntime, CurrentUserPreferenceRuntime, UserSubscriptionRuntime, WidgetConfigRuntime } from '@trackunit/iris-app-runtime-core';
|
|
3
3
|
import { FeatureFlagContextProvider, ToastProvider, AnalyticsContextProvider, AssetSortingProvider, ConfirmationDialogProvider, EnvironmentContextProvider, ErrorHandlingContextProvider, ExportDataContext, FilterBarProvider, GeolocationProvider, ModalDialogContextProvider, NavigationContextProvider, OemBrandingContextProvider, TimeRangeProvider, TokenProvider, CurrentUserProvider, CurrentUserPreferenceProvider, UserSubscriptionProvider, WidgetConfigProvider } from '@trackunit/react-core-contexts-api';
|
|
4
|
-
import { useState, useEffect,
|
|
4
|
+
import { useState, useEffect, useCallback, useMemo, useReducer, Suspense } from 'react';
|
|
5
5
|
import { ApolloLink, Observable, createHttpLink, from, split, ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
|
|
6
|
-
import { useEnvironment, useToken, useErrorHandler } from '@trackunit/react-core-hooks';
|
|
6
|
+
import { useEnvironment, useToken, useErrorHandler, useToast, useCurrentUser } from '@trackunit/react-core-hooks';
|
|
7
7
|
import { setContext } from '@apollo/client/link/context';
|
|
8
8
|
import { removeTypenameFromVariables } from '@apollo/client/link/remove-typename';
|
|
9
9
|
import { getMainDefinition, Observable as Observable$1 } from '@apollo/client/utilities';
|
|
@@ -38,10 +38,25 @@ const FeatureFlagProviderIrisApp = ({ children }) => {
|
|
|
38
38
|
return jsx(FeatureFlagContextProvider, { value: featureFlags, children: children });
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Picks out the messages that report a missing authorization scope.
|
|
43
|
+
*
|
|
44
|
+
* The server offers no structured signal to key off: `verifyScopes`
|
|
45
|
+
* (libs/graphql/common/src/schemabuilder/AuthorizationDirective.ts) throws a plain
|
|
46
|
+
* `ForbiddenError` whose message is assembled as "Missing " + the allowed scopes joined with
|
|
47
|
+
* " or " + " scope". Matching on that shape is therefore the only option available, and it
|
|
48
|
+
* lives here so the HTTP and subscription links cannot drift apart on what counts as a
|
|
49
|
+
* missing scope.
|
|
50
|
+
*
|
|
51
|
+
* Filtering rather than inspecting the first error matters in both links: a response can
|
|
52
|
+
* carry several errors in any order, since the directive throws per resolved field.
|
|
53
|
+
*/
|
|
54
|
+
const getMissingScopeMessages = (errors) => errors.map(error => error.message).filter(message => message.startsWith("Missing ") && message.endsWith(" scope"));
|
|
55
|
+
|
|
41
56
|
/**
|
|
42
57
|
* This error link is used to capture error information, i. e. traceId, graphQL errors, network errors, etc.
|
|
43
58
|
*/
|
|
44
|
-
const createErrorLink = ({ errorHandler, getToken, }) => {
|
|
59
|
+
const createErrorLink = ({ errorHandler, getToken, showErrorToast, }) => {
|
|
45
60
|
return onError(({ graphQLErrors, networkError, operation, response, forward }) => {
|
|
46
61
|
if (networkError) {
|
|
47
62
|
// We skip the error logging if the error is an AbortError
|
|
@@ -68,6 +83,10 @@ const createErrorLink = ({ errorHandler, getToken, }) => {
|
|
|
68
83
|
}
|
|
69
84
|
// eslint-disable-next-line no-console
|
|
70
85
|
console.error(`Error calling: '${operation.getContext().clientAwareness.name}' fetching Data for: ${operation.operationName}`, graphQLErrors);
|
|
86
|
+
const missingScopeMessages = getMissingScopeMessages(graphQLErrors);
|
|
87
|
+
if (missingScopeMessages.length > 0) {
|
|
88
|
+
showErrorToast(new Error(missingScopeMessages.join(", ")));
|
|
89
|
+
}
|
|
71
90
|
/**
|
|
72
91
|
* We want to see the full graphQL error since
|
|
73
92
|
* it contains extra details like the query/mutation
|
|
@@ -118,18 +137,20 @@ const DEFAULT_AUTH_GRACE_MS = 30000;
|
|
|
118
137
|
/**
|
|
119
138
|
* Wraps an SSE subscription link and provides the same error monitoring as the
|
|
120
139
|
* HTTP error link — capturing GraphQL errors, traceIds, FORCE_RELOAD_BROWSER,
|
|
121
|
-
* and UNAUTHENTICATED codes
|
|
140
|
+
* and UNAUTHENTICATED codes, and surfacing missing authorization scopes — for
|
|
141
|
+
* long-lived subscription Observables.
|
|
122
142
|
*
|
|
123
143
|
* UNAUTHENTICATED errors are deferred by a grace period (default 30s) to allow
|
|
124
144
|
* Okta's autoRenew to refresh the token. If a successful response arrives
|
|
125
145
|
* within the window the capture is cancelled; if the timer fires, a single
|
|
126
146
|
* captureException is sent — an actionable signal that token refresh failed.
|
|
127
147
|
*/
|
|
128
|
-
const createSubscriptionErrorLink = ({ errorHandler, getToken, graceMs = DEFAULT_AUTH_GRACE_MS, }) => {
|
|
148
|
+
const createSubscriptionErrorLink = ({ errorHandler, getToken, showErrorToast, graceMs = DEFAULT_AUTH_GRACE_MS, }) => {
|
|
129
149
|
return new ApolloLink((operation, forward) => {
|
|
130
150
|
return new Observable(observer => {
|
|
131
151
|
let authGraceTimer = null;
|
|
132
152
|
let authErrorReported = false;
|
|
153
|
+
let missingScopeReported = false;
|
|
133
154
|
const clearGraceTimer = () => {
|
|
134
155
|
if (authGraceTimer !== null) {
|
|
135
156
|
clearTimeout(authGraceTimer);
|
|
@@ -159,6 +180,16 @@ const createSubscriptionErrorLink = ({ errorHandler, getToken, graceMs = DEFAULT
|
|
|
159
180
|
level: "error",
|
|
160
181
|
data: { log: JSON.stringify(errors) },
|
|
161
182
|
});
|
|
183
|
+
// A subscription is long-lived and a scope failure is static for the operation, so
|
|
184
|
+
// the same error can arrive on every payload. Report it once per subscription
|
|
185
|
+
// rather than re-raising the toast, which the provider's de-dup would otherwise
|
|
186
|
+
// make visibly disappear and re-enter. Reset when a clean payload arrives, as the
|
|
187
|
+
// auth grace handling below does.
|
|
188
|
+
const missingScopeMessages = getMissingScopeMessages(errors);
|
|
189
|
+
if (missingScopeMessages.length > 0 && !missingScopeReported) {
|
|
190
|
+
missingScopeReported = true;
|
|
191
|
+
showErrorToast(new Error(missingScopeMessages.join(", ")));
|
|
192
|
+
}
|
|
162
193
|
const invalidToken = errors.some(x => x.extensions?.code === "UNAUTHENTICATED" ||
|
|
163
194
|
x.message.includes("Invalid token specified") ||
|
|
164
195
|
x.message.includes("Access denied! You need to be authorized to perform this action!"));
|
|
@@ -178,6 +209,7 @@ const createSubscriptionErrorLink = ({ errorHandler, getToken, graceMs = DEFAULT
|
|
|
178
209
|
else {
|
|
179
210
|
clearGraceTimer();
|
|
180
211
|
authErrorReported = false;
|
|
212
|
+
missingScopeReported = false;
|
|
181
213
|
}
|
|
182
214
|
observer.next(response);
|
|
183
215
|
},
|
|
@@ -279,7 +311,7 @@ const isInternalGqlContext = () => {
|
|
|
279
311
|
/**
|
|
280
312
|
* @internal
|
|
281
313
|
*/
|
|
282
|
-
const createApolloClient = ({ graphqlPublicUrl, graphqlInternalUrl, graphqlReportUrl, isDev, tracingHeaders: initialTracingHeaders, firstToken, errorHandler, }) => {
|
|
314
|
+
const createApolloClient = ({ graphqlPublicUrl, graphqlInternalUrl, graphqlReportUrl, isDev, tracingHeaders: initialTracingHeaders, firstToken, errorHandler, showErrorToast, }) => {
|
|
283
315
|
let token = firstToken;
|
|
284
316
|
let tracingHeaders = initialTracingHeaders;
|
|
285
317
|
const publicGraphQLLink = createHttpLink({
|
|
@@ -299,7 +331,7 @@ const createApolloClient = ({ graphqlPublicUrl, graphqlInternalUrl, graphqlRepor
|
|
|
299
331
|
},
|
|
300
332
|
};
|
|
301
333
|
});
|
|
302
|
-
const errorLink = createErrorLink({ errorHandler, getToken: () => token });
|
|
334
|
+
const errorLink = createErrorLink({ errorHandler, getToken: () => token, showErrorToast });
|
|
303
335
|
const defaultOptions = {
|
|
304
336
|
watchQuery: {
|
|
305
337
|
fetchPolicy: "no-cache",
|
|
@@ -337,7 +369,7 @@ const createApolloClient = ({ graphqlPublicUrl, graphqlInternalUrl, graphqlRepor
|
|
|
337
369
|
headers: () => generateHeaders(token, tracingHeaders),
|
|
338
370
|
});
|
|
339
371
|
// Split links based on operation type
|
|
340
|
-
const subscriptionErrorLink = createSubscriptionErrorLink({ errorHandler, getToken: () => token });
|
|
372
|
+
const subscriptionErrorLink = createSubscriptionErrorLink({ errorHandler, getToken: () => token, showErrorToast });
|
|
341
373
|
const splitLink = from([
|
|
342
374
|
authLink,
|
|
343
375
|
split(({ query }) => {
|
|
@@ -392,6 +424,34 @@ const useApolloClient = () => {
|
|
|
392
424
|
const { graphqlPublicUrl, graphqlInternalUrl, graphqlReportUrl, environment, tracingHeaders } = useEnvironment();
|
|
393
425
|
const { token: currentToken } = useToken();
|
|
394
426
|
const errorHandler = useErrorHandler();
|
|
427
|
+
const { addToast } = useToast();
|
|
428
|
+
const { permissions } = useCurrentUser();
|
|
429
|
+
const showErrorToast = useCallback((error) => {
|
|
430
|
+
// Only a developer running an Iris App locally can act on a missing scope, since only
|
|
431
|
+
// they can edit the manifest. The server throws the same message for real customer
|
|
432
|
+
// sessions and for token clients that have no manifest at all, so surfacing it outside
|
|
433
|
+
// local mode tells users to fix something they cannot reach.
|
|
434
|
+
//
|
|
435
|
+
// Read from storage rather than through useIrisAppsSDK().developerMode: that context is
|
|
436
|
+
// created below this provider (it queries GraphQL, so it needs this Apollo client) and
|
|
437
|
+
// lives in a visibility:host library this one cannot import.
|
|
438
|
+
const isDeveloper = permissions?.some(permission => permission === "account.iris.app.developer");
|
|
439
|
+
if (!isDeveloper) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
void addToast({
|
|
443
|
+
// ToastProviderHost de-dups on id *or* title, so both must be constant and
|
|
444
|
+
// specific: a message-derived id churned on every scope combination, and the
|
|
445
|
+
// generic "Error" title evicted unrelated toasts (and was evicted by them).
|
|
446
|
+
id: "apollo-missing-scope",
|
|
447
|
+
intent: "danger",
|
|
448
|
+
title: "Missing scope",
|
|
449
|
+
// A missing scope leaves the view permanently empty, so the only explanation
|
|
450
|
+
// the user gets must not time out.
|
|
451
|
+
behavior: "persistent",
|
|
452
|
+
description: `${error.message} - you must add the scope to your iris app manifest`,
|
|
453
|
+
});
|
|
454
|
+
}, [addToast, permissions]);
|
|
395
455
|
const [client] = useState(() => {
|
|
396
456
|
return createApolloClient({
|
|
397
457
|
/* DON'T CHANGE THIS ! its there to ensure we don't recreate apollo client just because the token changes,
|
|
@@ -404,6 +464,7 @@ const useApolloClient = () => {
|
|
|
404
464
|
tracingHeaders,
|
|
405
465
|
firstToken: currentToken,
|
|
406
466
|
errorHandler,
|
|
467
|
+
showErrorToast,
|
|
407
468
|
});
|
|
408
469
|
});
|
|
409
470
|
// Synchronously propagate the token into the Apollo client closure during render
|
|
@@ -971,7 +1032,7 @@ const TrackunitProviders = ({ translations, children, errorHandler }) => {
|
|
|
971
1032
|
registerTranslations(translations); // Register the apps translations if passed.
|
|
972
1033
|
}
|
|
973
1034
|
initializeTranslationsForApp(); // Initialize all registered translations
|
|
974
|
-
return (jsx(EnvironmentProviderIrisApp, { children: jsx(ErrorHandlingProviderIrisApp, { errorHandler: errorHandler, children: jsx(ThemeCssProviderIrisApp, { children: jsx(TokenProviderIrisApp, { children: jsx(CurrentUserPreferenceProviderIrisApp, { children: jsx(CurrentUserProviderIrisApp, { children: jsx(UserSubscriptionProviderIrisApp, { children: jsx(AnalyticsProviderIrisApp, { children: jsx(OemBrandingProviderIrisApp, { children: jsx(AssetSortingProviderIrisApp, { children: jsx(
|
|
1035
|
+
return (jsx(EnvironmentProviderIrisApp, { children: jsx(ErrorHandlingProviderIrisApp, { errorHandler: errorHandler, children: jsx(ThemeCssProviderIrisApp, { children: jsx(TokenProviderIrisApp, { children: jsx(CurrentUserPreferenceProviderIrisApp, { children: jsx(CurrentUserProviderIrisApp, { children: jsx(UserSubscriptionProviderIrisApp, { children: jsx(AnalyticsProviderIrisApp, { children: jsx(OemBrandingProviderIrisApp, { children: jsx(AssetSortingProviderIrisApp, { children: jsx(ToastProviderIrisApp, { children: jsx(ManagerApolloProvider, { children: jsx(NavigationProviderIrisApp, { children: jsx(ModalDialogContextProviderIrisApp, { children: jsx(ConfirmationDialogProviderIrisApp, { children: jsx(FilterBarProviderIrisApp, { children: jsx(ExportDataProviderIrisApp, { children: jsx(TimeRangeProviderIrisApp, { children: jsx(WidgetConfigProviderIrisApp, { children: jsx(GeolocationProviderIrisApp, { children: jsx(Suspense, { fallback: jsx(Spinner, { centering: "centered", "data-testid": "trackunit-providers" }), children: jsx(FeatureFlagProviderIrisApp, { children: children }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }));
|
|
975
1036
|
};
|
|
976
1037
|
|
|
977
1038
|
export { FeatureFlagProviderIrisApp, ManagerApolloProvider, ToastProviderIrisApp, TrackunitProviders };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entry.js","sourceRoot":"","sources":["../../../../../libs/react/core-contexts/migrations/entry.ts"],"names":[],"mappings":"","sourcesContent":["export {};\n"]}
|
package/package.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/react-core-contexts",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"repository": "https://github.com/Trackunit/manager",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=24.x"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@trackunit/iris-app-api": "2.4.12
|
|
11
|
-
"@trackunit/iris-app-runtime-core-api": "1.17.24
|
|
12
|
-
"@trackunit/react-core-hooks": "1.21.18
|
|
13
|
-
"@trackunit/i18n-library-translation": "2.4.18
|
|
14
|
-
"@trackunit/react-components": "2.10.12
|
|
15
|
-
"@trackunit/iris-app-runtime-core": "1.18.24
|
|
10
|
+
"@trackunit/iris-app-api": "2.4.12",
|
|
11
|
+
"@trackunit/iris-app-runtime-core-api": "1.17.24",
|
|
12
|
+
"@trackunit/react-core-hooks": "1.21.18",
|
|
13
|
+
"@trackunit/i18n-library-translation": "2.4.18",
|
|
14
|
+
"@trackunit/react-components": "2.10.12",
|
|
15
|
+
"@trackunit/iris-app-runtime-core": "1.18.24",
|
|
16
16
|
"graphql-sse": "^2.5.4",
|
|
17
|
-
"@trackunit/react-core-contexts-api": "1.18.24
|
|
17
|
+
"@trackunit/react-core-contexts-api": "1.18.24"
|
|
18
18
|
},
|
|
19
19
|
"peerDependencies": {
|
|
20
20
|
"@apollo/client": "^3.13.8",
|
|
@@ -3,7 +3,7 @@ import { ErrorHandlingContextValue, TracingHeaders } from "@trackunit/iris-app-r
|
|
|
3
3
|
/**
|
|
4
4
|
* @internal
|
|
5
5
|
*/
|
|
6
|
-
export declare const createApolloClient: ({ graphqlPublicUrl, graphqlInternalUrl, graphqlReportUrl, isDev, tracingHeaders: initialTracingHeaders, firstToken, errorHandler, }: {
|
|
6
|
+
export declare const createApolloClient: ({ graphqlPublicUrl, graphqlInternalUrl, graphqlReportUrl, isDev, tracingHeaders: initialTracingHeaders, firstToken, errorHandler, showErrorToast, }: {
|
|
7
7
|
graphqlPublicUrl: string;
|
|
8
8
|
graphqlInternalUrl: string;
|
|
9
9
|
graphqlReportUrl: string;
|
|
@@ -11,6 +11,7 @@ export declare const createApolloClient: ({ graphqlPublicUrl, graphqlInternalUrl
|
|
|
11
11
|
isDev: boolean;
|
|
12
12
|
firstToken?: string;
|
|
13
13
|
errorHandler: ErrorHandlingContextValue;
|
|
14
|
+
showErrorToast: (error: Error) => void;
|
|
14
15
|
}) => {
|
|
15
16
|
client: ApolloClient<import("@apollo/client").NormalizedCacheObject>;
|
|
16
17
|
setToken: (newToken: string | undefined) => void;
|
|
@@ -3,7 +3,8 @@ import { ErrorHandlingContextValue } from "@trackunit/iris-app-runtime-core-api"
|
|
|
3
3
|
/**
|
|
4
4
|
* This error link is used to capture error information, i. e. traceId, graphQL errors, network errors, etc.
|
|
5
5
|
*/
|
|
6
|
-
export declare const createErrorLink: ({ errorHandler, getToken, }: {
|
|
6
|
+
export declare const createErrorLink: ({ errorHandler, getToken, showErrorToast, }: {
|
|
7
7
|
errorHandler: ErrorHandlingContextValue;
|
|
8
8
|
getToken: () => string | undefined;
|
|
9
|
+
showErrorToast: (error: Error) => void;
|
|
9
10
|
}) => ApolloLink;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Picks out the messages that report a missing authorization scope.
|
|
3
|
+
*
|
|
4
|
+
* The server offers no structured signal to key off: `verifyScopes`
|
|
5
|
+
* (libs/graphql/common/src/schemabuilder/AuthorizationDirective.ts) throws a plain
|
|
6
|
+
* `ForbiddenError` whose message is assembled as "Missing " + the allowed scopes joined with
|
|
7
|
+
* " or " + " scope". Matching on that shape is therefore the only option available, and it
|
|
8
|
+
* lives here so the HTTP and subscription links cannot drift apart on what counts as a
|
|
9
|
+
* missing scope.
|
|
10
|
+
*
|
|
11
|
+
* Filtering rather than inspecting the first error matters in both links: a response can
|
|
12
|
+
* carry several errors in any order, since the directive throws per resolved field.
|
|
13
|
+
*/
|
|
14
|
+
export declare const getMissingScopeMessages: (errors: ReadonlyArray<{
|
|
15
|
+
message: string;
|
|
16
|
+
}>) => Array<string>;
|
|
@@ -3,15 +3,17 @@ import { ErrorHandlingContextValue } from "@trackunit/iris-app-runtime-core-api"
|
|
|
3
3
|
/**
|
|
4
4
|
* Wraps an SSE subscription link and provides the same error monitoring as the
|
|
5
5
|
* HTTP error link — capturing GraphQL errors, traceIds, FORCE_RELOAD_BROWSER,
|
|
6
|
-
* and UNAUTHENTICATED codes
|
|
6
|
+
* and UNAUTHENTICATED codes, and surfacing missing authorization scopes — for
|
|
7
|
+
* long-lived subscription Observables.
|
|
7
8
|
*
|
|
8
9
|
* UNAUTHENTICATED errors are deferred by a grace period (default 30s) to allow
|
|
9
10
|
* Okta's autoRenew to refresh the token. If a successful response arrives
|
|
10
11
|
* within the window the capture is cancelled; if the timer fires, a single
|
|
11
12
|
* captureException is sent — an actionable signal that token refresh failed.
|
|
12
13
|
*/
|
|
13
|
-
export declare const createSubscriptionErrorLink: ({ errorHandler, getToken, graceMs, }: {
|
|
14
|
+
export declare const createSubscriptionErrorLink: ({ errorHandler, getToken, showErrorToast, graceMs, }: {
|
|
14
15
|
errorHandler: ErrorHandlingContextValue;
|
|
15
16
|
getToken: () => string | undefined;
|
|
17
|
+
showErrorToast: (error: Error) => void;
|
|
16
18
|
graceMs?: number;
|
|
17
19
|
}) => ApolloLink;
|