mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-24 08:58:06 +00:00
feat(ui): support refresh with multi tabs open (#17782)
* feat(ui): support refresh with multi tabs open * address comments * fix clientType error * fix unit tests * add version miss match alert for seamless experience * update variable values * use custom hook for location * patch DataInsight app before running it * fix addRoldeAndAssignToUser flaky test failure (cherry picked from commit 17744f42dc5e71f8e3ca26e2b076b9d72a11556b)
This commit is contained in:
parent
b72513cf03
commit
f08e643152
@ -33,17 +33,51 @@ setup(
|
|||||||
|
|
||||||
await table.create(apiContext);
|
await table.create(apiContext);
|
||||||
|
|
||||||
apiContext.patch(`/api/v1/tables/${table.entityResponseData?.id ?? ''}`, {
|
await apiContext.patch(
|
||||||
|
`/api/v1/tables/${table.entityResponseData?.id ?? ''}`,
|
||||||
|
{
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
op: 'add',
|
||||||
|
path: '/tags/0',
|
||||||
|
value: {
|
||||||
|
name: 'Tier2',
|
||||||
|
tagFQN: 'Tier.Tier2',
|
||||||
|
labelType: 'Manual',
|
||||||
|
state: 'Confirmed',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json-patch+json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await apiContext.patch(`/api/v1/apps/trigger/DataInsightsApplication`, {
|
||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
op: 'add',
|
op: 'remove',
|
||||||
path: '/tags/0',
|
path: '/appConfiguration/backfillConfiguration/startDate',
|
||||||
value: {
|
},
|
||||||
name: 'Tier2',
|
{
|
||||||
tagFQN: 'Tier.Tier2',
|
op: 'remove',
|
||||||
labelType: 'Manual',
|
path: '/appConfiguration/backfillConfiguration/endDate',
|
||||||
state: 'Confirmed',
|
},
|
||||||
},
|
{
|
||||||
|
op: 'replace',
|
||||||
|
path: '/batchSize',
|
||||||
|
value: 1000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
op: 'replace',
|
||||||
|
path: '/recreateDataAssetsIndex',
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
op: 'replace',
|
||||||
|
path: '/backfillConfiguration/enabled',
|
||||||
|
value: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
<svg width="34" height="34" viewBox="0 0 34 34" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_9647_49527)">
|
||||||
|
<path d="M15.6719 23.4391V13.8251L12.9884 16.5767C12.4763 17.1019 11.6355 17.1124 11.1103 16.6003C10.5852 16.0882 10.5747 15.2473 11.0868 14.7222L16.0493 9.63362C16.2992 9.37736 16.6421 9.23279 17.0001 9.23279C17.3581 9.23279 17.701 9.37736 17.9509 9.63362L22.9133 14.7222C23.4255 15.2473 23.4149 16.0882 22.8898 16.6003C22.6316 16.8521 22.297 16.9776 21.9627 16.9776C21.6172 16.9776 21.272 16.8437 21.0117 16.5767L18.3281 13.8251V23.4391C18.3281 24.1727 17.7335 24.7673 17 24.7673C16.2665 24.7673 15.6719 24.1726 15.6719 23.4391ZM29.0209 4.97914C25.8099 1.76833 21.5409 0 17 0C12.4591 0 8.19008 1.76833 4.97914 4.97914C1.76833 8.19008 0 12.4591 0 17C0 21.5409 1.76833 25.8099 4.97914 29.0208C8.19008 32.2317 12.4591 34 17 34C20.3597 34 23.6088 33.0212 26.3962 31.1693C27.0072 30.7634 27.1734 29.9391 26.7675 29.3281C26.3616 28.7172 25.5373 28.5508 24.9263 28.9568C22.5759 30.5183 19.835 31.3438 17 31.3438C9.09082 31.3438 2.65625 24.9092 2.65625 17C2.65625 9.09082 9.09082 2.65625 17 2.65625C24.9092 2.65625 31.3438 9.09082 31.3438 17C31.3438 19.6974 30.5913 22.3251 29.1677 24.5993C28.7785 25.221 28.9671 26.0405 29.5888 26.4297C30.2102 26.8188 31.03 26.6304 31.4192 26.0086C33.1076 23.3114 34 20.1963 34 17C34 12.4591 32.2317 8.19008 29.0209 4.97914Z" fill="#48CA9E"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_9647_49527">
|
||||||
|
<rect width="34" height="34" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
@ -15,6 +15,7 @@ import React, {
|
|||||||
forwardRef,
|
forwardRef,
|
||||||
Fragment,
|
Fragment,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
|
useCallback,
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -45,34 +46,33 @@ const BasicAuthenticator = forwardRef(
|
|||||||
isApplicationLoading,
|
isApplicationLoading,
|
||||||
} = useApplicationStore();
|
} = useApplicationStore();
|
||||||
|
|
||||||
const handleSilentSignIn = async (): Promise<AccessTokenResponse> => {
|
const handleSilentSignIn =
|
||||||
const refreshToken = getRefreshToken();
|
useCallback(async (): Promise<AccessTokenResponse> => {
|
||||||
|
const refreshToken = getRefreshToken();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
authConfig?.provider !== AuthProvider.Basic &&
|
authConfig?.provider !== AuthProvider.Basic &&
|
||||||
authConfig?.provider !== AuthProvider.LDAP
|
authConfig?.provider !== AuthProvider.LDAP
|
||||||
) {
|
) {
|
||||||
Promise.reject(t('message.authProvider-is-not-basic'));
|
Promise.reject(t('message.authProvider-is-not-basic'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await getAccessTokenOnExpiry({
|
const response = await getAccessTokenOnExpiry({
|
||||||
refreshToken: refreshToken as string,
|
refreshToken: refreshToken as string,
|
||||||
});
|
});
|
||||||
|
|
||||||
setRefreshToken(response.refreshToken);
|
setRefreshToken(response.refreshToken);
|
||||||
setOidcToken(response.accessToken);
|
setOidcToken(response.accessToken);
|
||||||
|
|
||||||
return Promise.resolve(response);
|
return Promise.resolve(response);
|
||||||
};
|
}, [authConfig, getRefreshToken, setOidcToken, setRefreshToken, t]);
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
invokeLogout() {
|
invokeLogout() {
|
||||||
handleLogout();
|
handleLogout();
|
||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
},
|
},
|
||||||
renewIdToken() {
|
renewIdToken: handleSilentSignIn,
|
||||||
return handleSilentSignIn();
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -49,7 +49,7 @@ export const GenericAuthenticator = forwardRef(
|
|||||||
const resp = await renewToken();
|
const resp = await renewToken();
|
||||||
setOidcToken(resp.accessToken);
|
setOidcToken(resp.accessToken);
|
||||||
|
|
||||||
return Promise.resolve(resp);
|
return resp;
|
||||||
};
|
};
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
|
@ -153,6 +153,12 @@ const MsalAuthenticator = forwardRef<AuthenticatorRef, Props>(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renewIdToken = async () => {
|
||||||
|
const user = await fetchIdToken();
|
||||||
|
|
||||||
|
return user.id_token;
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const oidcUserToken = getOidcToken();
|
const oidcUserToken = getOidcToken();
|
||||||
if (
|
if (
|
||||||
@ -177,23 +183,9 @@ const MsalAuthenticator = forwardRef<AuthenticatorRef, Props>(
|
|||||||
}, [inProgress, accounts, instance, account]);
|
}, [inProgress, accounts, instance, account]);
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
invokeLogin() {
|
invokeLogin: login,
|
||||||
login();
|
invokeLogout: logout,
|
||||||
},
|
renewIdToken: renewIdToken,
|
||||||
invokeLogout() {
|
|
||||||
logout();
|
|
||||||
},
|
|
||||||
renewIdToken(): Promise<string> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
fetchIdToken()
|
|
||||||
.then((user) => {
|
|
||||||
resolve(user.id_token);
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
reject(e);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return <Fragment>{children}</Fragment>;
|
return <Fragment>{children}</Fragment>;
|
||||||
|
@ -102,15 +102,9 @@ const OidcAuthenticator = forwardRef<AuthenticatorRef, Props>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
invokeLogin() {
|
invokeLogin: login,
|
||||||
login();
|
invokeLogout: logout,
|
||||||
},
|
renewIdToken: signInSilently,
|
||||||
invokeLogout() {
|
|
||||||
logout();
|
|
||||||
},
|
|
||||||
renewIdToken() {
|
|
||||||
return signInSilently();
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const AppWithAuth = getAuthenticator(childComponentType, userManager);
|
const AppWithAuth = getAuthenticator(childComponentType, userManager);
|
||||||
@ -165,6 +159,7 @@ const OidcAuthenticator = forwardRef<AuthenticatorRef, Props>(
|
|||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
||||||
|
onLogoutSuccess();
|
||||||
history.push(ROUTES.SIGNIN);
|
history.push(ROUTES.SIGNIN);
|
||||||
}}
|
}}
|
||||||
onSuccess={(user) => {
|
onSuccess={(user) => {
|
||||||
|
@ -42,22 +42,20 @@ const OktaAuthenticator = forwardRef<AuthenticatorRef, Props>(
|
|||||||
onLogoutSuccess();
|
onLogoutSuccess();
|
||||||
};
|
};
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
const renewToken = async () => {
|
||||||
invokeLogin() {
|
const renewToken = await oktaAuth.token.renewTokens();
|
||||||
login();
|
oktaAuth.tokenManager.setTokens(renewToken);
|
||||||
},
|
const newToken =
|
||||||
invokeLogout() {
|
renewToken?.idToken?.idToken ?? oktaAuth.getIdToken() ?? '';
|
||||||
logout();
|
setOidcToken(newToken);
|
||||||
},
|
|
||||||
async renewIdToken() {
|
|
||||||
const renewToken = await oktaAuth.token.renewTokens();
|
|
||||||
oktaAuth.tokenManager.setTokens(renewToken);
|
|
||||||
const newToken =
|
|
||||||
renewToken?.idToken?.idToken ?? oktaAuth.getIdToken() ?? '';
|
|
||||||
setOidcToken(newToken);
|
|
||||||
|
|
||||||
return Promise.resolve(newToken);
|
return Promise.resolve(newToken);
|
||||||
},
|
};
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
invokeLogin: login,
|
||||||
|
invokeLogout: logout,
|
||||||
|
renewIdToken: renewToken,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return <Fragment>{children}</Fragment>;
|
return <Fragment>{children}</Fragment>;
|
||||||
|
@ -97,15 +97,9 @@ const SamlAuthenticator = forwardRef<AuthenticatorRef, Props>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
invokeLogin() {
|
invokeLogin: login,
|
||||||
login();
|
invokeLogout: logout,
|
||||||
},
|
renewIdToken: handleSilentSignIn,
|
||||||
invokeLogout() {
|
|
||||||
logout();
|
|
||||||
},
|
|
||||||
async renewIdToken() {
|
|
||||||
return handleSilentSignIn();
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return <Fragment>{children}</Fragment>;
|
return <Fragment>{children}</Fragment>;
|
||||||
|
@ -63,6 +63,7 @@ import {
|
|||||||
fetchAuthorizerConfig,
|
fetchAuthorizerConfig,
|
||||||
} from '../../../rest/miscAPI';
|
} from '../../../rest/miscAPI';
|
||||||
import { getLoggedInUser, updateUserDetail } from '../../../rest/userAPI';
|
import { getLoggedInUser, updateUserDetail } from '../../../rest/userAPI';
|
||||||
|
import TokenService from '../../../utils/Auth/TokenService/TokenServiceUtil';
|
||||||
import {
|
import {
|
||||||
extractDetailsFromToken,
|
extractDetailsFromToken,
|
||||||
getAuthConfig,
|
getAuthConfig,
|
||||||
@ -138,6 +139,7 @@ export const AuthProvider = ({
|
|||||||
setApplicationLoading,
|
setApplicationLoading,
|
||||||
} = useApplicationStore();
|
} = useApplicationStore();
|
||||||
const { updateDomains, updateDomainLoading } = useDomainStore();
|
const { updateDomains, updateDomainLoading } = useDomainStore();
|
||||||
|
const tokenService = useRef<TokenService>();
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
@ -181,9 +183,13 @@ export const AuthProvider = ({
|
|||||||
setApplicationLoading(false);
|
setApplicationLoading(false);
|
||||||
}, [timeoutId]);
|
}, [timeoutId]);
|
||||||
|
|
||||||
const onRenewIdTokenHandler = () => {
|
useEffect(() => {
|
||||||
return authenticatorRef.current?.renewIdToken();
|
if (authenticatorRef.current?.renewIdToken) {
|
||||||
};
|
tokenService.current = new TokenService(
|
||||||
|
authenticatorRef.current?.renewIdToken
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [authenticatorRef.current?.renewIdToken]);
|
||||||
|
|
||||||
const fetchDomainList = useCallback(async () => {
|
const fetchDomainList = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@ -290,8 +296,20 @@ export const AuthProvider = ({
|
|||||||
*/
|
*/
|
||||||
const renewIdToken = async () => {
|
const renewIdToken = async () => {
|
||||||
try {
|
try {
|
||||||
const onRenewIdTokenHandlerPromise = onRenewIdTokenHandler();
|
if (!tokenService.current?.isTokenUpdateInProgress()) {
|
||||||
onRenewIdTokenHandlerPromise && (await onRenewIdTokenHandlerPromise);
|
await tokenService.current?.refreshToken();
|
||||||
|
} else {
|
||||||
|
// wait for renewal to complete
|
||||||
|
const wait = new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
return resolve(true);
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
await wait;
|
||||||
|
|
||||||
|
// should have updated token after renewal
|
||||||
|
return getOidcToken();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(
|
console.error(
|
||||||
@ -351,8 +369,7 @@ export const AuthProvider = ({
|
|||||||
const startTokenExpiryTimer = () => {
|
const startTokenExpiryTimer = () => {
|
||||||
// Extract expiry
|
// Extract expiry
|
||||||
const { isExpired, timeoutExpiry } = extractDetailsFromToken(
|
const { isExpired, timeoutExpiry } = extractDetailsFromToken(
|
||||||
getOidcToken(),
|
getOidcToken()
|
||||||
clientType
|
|
||||||
);
|
);
|
||||||
const refreshToken = getRefreshToken();
|
const refreshToken = getRefreshToken();
|
||||||
|
|
||||||
|
@ -13,7 +13,9 @@
|
|||||||
|
|
||||||
import Icon from '@ant-design/icons';
|
import Icon from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
|
Alert,
|
||||||
Badge,
|
Badge,
|
||||||
|
Button,
|
||||||
Col,
|
Col,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
Input,
|
Input,
|
||||||
@ -45,6 +47,7 @@ import { ReactComponent as DropDownIcon } from '../../assets/svg/drop-down.svg';
|
|||||||
import { ReactComponent as IconBell } from '../../assets/svg/ic-alert-bell.svg';
|
import { ReactComponent as IconBell } from '../../assets/svg/ic-alert-bell.svg';
|
||||||
import { ReactComponent as DomainIcon } from '../../assets/svg/ic-domain.svg';
|
import { ReactComponent as DomainIcon } from '../../assets/svg/ic-domain.svg';
|
||||||
import { ReactComponent as Help } from '../../assets/svg/ic-help.svg';
|
import { ReactComponent as Help } from '../../assets/svg/ic-help.svg';
|
||||||
|
import { ReactComponent as RefreshIcon } from '../../assets/svg/ic-refresh.svg';
|
||||||
import { ReactComponent as IconSearch } from '../../assets/svg/search.svg';
|
import { ReactComponent as IconSearch } from '../../assets/svg/search.svg';
|
||||||
import {
|
import {
|
||||||
NOTIFICATION_READ_TIMER,
|
NOTIFICATION_READ_TIMER,
|
||||||
@ -54,8 +57,10 @@ import { HELP_ITEMS_ENUM } from '../../constants/Navbar.constants';
|
|||||||
import { useWebSocketConnector } from '../../context/WebSocketProvider/WebSocketProvider';
|
import { useWebSocketConnector } from '../../context/WebSocketProvider/WebSocketProvider';
|
||||||
import { EntityTabs, EntityType } from '../../enums/entity.enum';
|
import { EntityTabs, EntityType } from '../../enums/entity.enum';
|
||||||
import { useApplicationStore } from '../../hooks/useApplicationStore';
|
import { useApplicationStore } from '../../hooks/useApplicationStore';
|
||||||
|
import useCustomLocation from '../../hooks/useCustomLocation/useCustomLocation';
|
||||||
import { useDomainStore } from '../../hooks/useDomainStore';
|
import { useDomainStore } from '../../hooks/useDomainStore';
|
||||||
import { getVersion } from '../../rest/miscAPI';
|
import { getVersion } from '../../rest/miscAPI';
|
||||||
|
import { isProtectedRoute } from '../../utils/AuthProvider.util';
|
||||||
import brandImageClassBase from '../../utils/BrandImage/BrandImageClassBase';
|
import brandImageClassBase from '../../utils/BrandImage/BrandImageClassBase';
|
||||||
import {
|
import {
|
||||||
hasNotificationPermission,
|
hasNotificationPermission,
|
||||||
@ -108,7 +113,9 @@ const NavBar = ({
|
|||||||
const { searchCriteria, updateSearchCriteria } = useApplicationStore();
|
const { searchCriteria, updateSearchCriteria } = useApplicationStore();
|
||||||
const searchContainerRef = useRef<HTMLDivElement>(null);
|
const searchContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const Logo = useMemo(() => brandImageClassBase.getMonogram().src, []);
|
const Logo = useMemo(() => brandImageClassBase.getMonogram().src, []);
|
||||||
|
const [showVersionMissMatchAlert, setShowVersionMissMatchAlert] =
|
||||||
|
useState(false);
|
||||||
|
const location = useCustomLocation();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const {
|
const {
|
||||||
domainOptions,
|
domainOptions,
|
||||||
@ -298,7 +305,23 @@ const NavBar = ({
|
|||||||
if (shouldRequestPermission()) {
|
if (shouldRequestPermission()) {
|
||||||
Notification.requestPermission();
|
Notification.requestPermission();
|
||||||
}
|
}
|
||||||
}, []);
|
|
||||||
|
const handleDocumentVisibilityChange = async () => {
|
||||||
|
if (isProtectedRoute(location.pathname) && isTourRoute) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newVersion = await getVersion();
|
||||||
|
if (version !== newVersion.version) {
|
||||||
|
setShowVersionMissMatchAlert(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
addEventListener('focus', handleDocumentVisibilityChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
removeEventListener('focus', handleDocumentVisibilityChange);
|
||||||
|
};
|
||||||
|
}, [isTourRoute, version]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (socket) {
|
if (socket) {
|
||||||
@ -578,6 +601,26 @@ const NavBar = ({
|
|||||||
onCancel={handleModalCancel}
|
onCancel={handleModalCancel}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{showVersionMissMatchAlert && (
|
||||||
|
<Alert
|
||||||
|
showIcon
|
||||||
|
action={
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
onClick={() => {
|
||||||
|
history.go(0);
|
||||||
|
}}>
|
||||||
|
{t('label.refresh')}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
className="refresh-alert slide-in-top"
|
||||||
|
description="For a seamless experience recommend you to refresh the page"
|
||||||
|
icon={<RefreshIcon />}
|
||||||
|
message="A new version is available"
|
||||||
|
type="info"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{renderAlertCards}
|
{renderAlertCards}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -10,6 +10,8 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
@import (reference) '../../styles/variables.less';
|
||||||
|
|
||||||
.global-search-overlay {
|
.global-search-overlay {
|
||||||
.ant-popover-inner-content {
|
.ant-popover-inner-content {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
@ -28,9 +30,53 @@
|
|||||||
line-height: 21px;
|
line-height: 21px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
& + .refresh-alert {
|
||||||
|
width: 470px;
|
||||||
|
bottom: 30px;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 9999;
|
||||||
|
margin: 0 auto;
|
||||||
|
box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.12);
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid @text-color;
|
||||||
|
background: @text-color;
|
||||||
|
color: @white;
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
|
||||||
|
.ant-alert-message {
|
||||||
|
color: @white;
|
||||||
|
}
|
||||||
|
.ant-alert-description {
|
||||||
|
color: @grey-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-alert-content {
|
||||||
|
border-right: 1px solid @white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-btn {
|
||||||
|
font-weight: 700;
|
||||||
|
color: @white;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.domain-dropdown-menu {
|
.domain-dropdown-menu {
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.slide-in-top {
|
||||||
|
animation: slide-in-top 1s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-in-top {
|
||||||
|
0% {
|
||||||
|
transform: translate(-50%, 100%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(-50%, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -55,6 +55,13 @@ window.DOMMatrixReadOnly = jest.fn().mockImplementation(() => ({
|
|||||||
isIdentity: true,
|
isIdentity: true,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
window.BroadcastChannel = jest.fn().mockImplementation(() => ({
|
||||||
|
postMessage: jest.fn(),
|
||||||
|
addEventListener: jest.fn(),
|
||||||
|
removeEventListener: jest.fn(),
|
||||||
|
close: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* mock implementation of ResizeObserver
|
* mock implementation of ResizeObserver
|
||||||
*/
|
*/
|
||||||
|
@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 Collate.
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
import { useApplicationStore } from '../../../hooks/useApplicationStore';
|
||||||
|
import { AccessTokenResponse } from '../../../rest/auth-API';
|
||||||
|
import { extractDetailsFromToken } from '../../AuthProvider.util';
|
||||||
|
import { getOidcToken } from '../../LocalStorageUtils';
|
||||||
|
|
||||||
|
class TokenService {
|
||||||
|
channel: BroadcastChannel;
|
||||||
|
renewToken: () => Promise<string> | Promise<AccessTokenResponse>;
|
||||||
|
tokeUpdateInProgress: boolean;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
renewToken: () => Promise<string> | Promise<AccessTokenResponse>
|
||||||
|
) {
|
||||||
|
this.channel = new BroadcastChannel('auth_channel');
|
||||||
|
this.renewToken = renewToken;
|
||||||
|
this.channel.onmessage = this.handleTokenUpdate.bind(this);
|
||||||
|
this.tokeUpdateInProgress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This method will update token across tabs on receiving message to the channel
|
||||||
|
handleTokenUpdate(event: {
|
||||||
|
data: { type: string; token: string | AccessTokenResponse };
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
data: { type, token },
|
||||||
|
} = event;
|
||||||
|
if (type === 'TOKEN_UPDATE' && token) {
|
||||||
|
if (typeof token !== 'string') {
|
||||||
|
useApplicationStore.getState().setOidcToken(token.accessToken);
|
||||||
|
useApplicationStore.getState().setRefreshToken(token.refreshToken);
|
||||||
|
useApplicationStore.getState().updateAxiosInterceptors();
|
||||||
|
} else {
|
||||||
|
useApplicationStore.getState().setOidcToken(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh the token if it is expired
|
||||||
|
async refreshToken() {
|
||||||
|
const token = getOidcToken();
|
||||||
|
const { isExpired } = extractDetailsFromToken(token);
|
||||||
|
|
||||||
|
if (isExpired) {
|
||||||
|
// Logic to refresh the token
|
||||||
|
const newToken = await this.fetchNewToken();
|
||||||
|
// To update all the tabs on updating channel token
|
||||||
|
this.channel.postMessage({ type: 'TOKEN_UPDATE', token: newToken });
|
||||||
|
|
||||||
|
return newToken;
|
||||||
|
} else {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call renewal method according to the provider
|
||||||
|
async fetchNewToken() {
|
||||||
|
let response: string | AccessTokenResponse | null = null;
|
||||||
|
if (typeof this.renewToken === 'function') {
|
||||||
|
try {
|
||||||
|
this.tokeUpdateInProgress = true;
|
||||||
|
response = await this.renewToken();
|
||||||
|
this.tokeUpdateInProgress = false;
|
||||||
|
} catch (error) {
|
||||||
|
// Do nothing
|
||||||
|
} finally {
|
||||||
|
this.tokeUpdateInProgress = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tracker for any ongoing token update
|
||||||
|
isTokenUpdateInProgress() {
|
||||||
|
return this.tokeUpdateInProgress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TokenService;
|
@ -36,11 +36,8 @@ import { isDev } from './EnvironmentUtils';
|
|||||||
|
|
||||||
const cookieStorage = new CookieStorage();
|
const cookieStorage = new CookieStorage();
|
||||||
|
|
||||||
// 25s for server auth approach
|
// 1 minutes for client auth approach
|
||||||
export const EXPIRY_THRESHOLD_MILLES = 25 * 1000;
|
export const EXPIRY_THRESHOLD_MILLES = 1 * 60 * 1000;
|
||||||
|
|
||||||
// 2 minutes for client auth approach
|
|
||||||
export const EXPIRY_THRESHOLD_MILLES_PUBLIC = 2 * 60 * 1000;
|
|
||||||
|
|
||||||
export const getRedirectUri = (callbackUrl: string) => {
|
export const getRedirectUri = (callbackUrl: string) => {
|
||||||
return isDev()
|
return isDev()
|
||||||
@ -328,10 +325,7 @@ export const getUrlPathnameExpiry = () => {
|
|||||||
* @timeoutExpiry time in ms for try to silent sign-in
|
* @timeoutExpiry time in ms for try to silent sign-in
|
||||||
* @returns exp, isExpired, diff, timeoutExpiry
|
* @returns exp, isExpired, diff, timeoutExpiry
|
||||||
*/
|
*/
|
||||||
export const extractDetailsFromToken = (
|
export const extractDetailsFromToken = (token: string) => {
|
||||||
token: string,
|
|
||||||
clientType = ClientType.Public
|
|
||||||
) => {
|
|
||||||
if (token) {
|
if (token) {
|
||||||
try {
|
try {
|
||||||
const { exp } = jwtDecode<JwtPayload>(token);
|
const { exp } = jwtDecode<JwtPayload>(token);
|
||||||
@ -343,10 +337,7 @@ export const extractDetailsFromToken = (
|
|||||||
isExpired: false,
|
isExpired: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const threshouldMillis =
|
const threshouldMillis = EXPIRY_THRESHOLD_MILLES;
|
||||||
clientType === ClientType.Public
|
|
||||||
? EXPIRY_THRESHOLD_MILLES_PUBLIC
|
|
||||||
: EXPIRY_THRESHOLD_MILLES;
|
|
||||||
|
|
||||||
const diff = exp && exp * 1000 - dateNow;
|
const diff = exp && exp * 1000 - dateNow;
|
||||||
const timeoutExpiry =
|
const timeoutExpiry =
|
||||||
|
Loading…
x
Reference in New Issue
Block a user