@databiosphere/findable-ui 49.4.1 → 49.6.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.
Files changed (20) hide show
  1. package/.release-please-manifest.json +1 -1
  2. package/CHANGELOG.md +15 -0
  3. package/lib/components/Export/components/ExportToTerra/components/TerraSetUpForm/components/FormStep/components/ConnectTerraToNIHAccount/connectTerraToNIHAccount.js +1 -1
  4. package/lib/components/Export/components/ExportToTerra/components/TerraSetUpForm/components/NIHAccountExpiryWarning/nihAccountExpiryWarning.js +9 -9
  5. package/lib/hooks/authentication/terra/useAuthenticationNIHExpiry.d.ts +3 -3
  6. package/lib/hooks/authentication/terra/useAuthenticationNIHExpiry.js +15 -15
  7. package/lib/providers/authentication/terra/hooks/useFetchTerraNIHProfile.d.ts +3 -7
  8. package/lib/providers/authentication/terra/hooks/useFetchTerraNIHProfile.js +22 -18
  9. package/lib/views/ResearchView/assistant/components/Messages/components/AssistantMessage/assistantMessage.d.ts +2 -2
  10. package/lib/views/ResearchView/assistant/components/Messages/components/AssistantMessage/assistantMessage.js +6 -7
  11. package/package.json +1 -1
  12. package/src/components/Export/components/ExportToTerra/components/TerraSetUpForm/components/FormStep/components/ConnectTerraToNIHAccount/connectTerraToNIHAccount.tsx +1 -1
  13. package/src/components/Export/components/ExportToTerra/components/TerraSetUpForm/components/NIHAccountExpiryWarning/nihAccountExpiryWarning.tsx +16 -10
  14. package/src/hooks/authentication/terra/useAuthenticationNIHExpiry.ts +18 -16
  15. package/src/providers/authentication/terra/hooks/useFetchTerraNIHProfile.ts +25 -33
  16. package/src/views/ResearchView/assistant/components/Messages/components/AssistantMessage/assistantMessage.tsx +7 -35
  17. package/lib/views/ResearchView/assistant/components/Messages/components/AssistantMessage/utils.d.ts +0 -13
  18. package/lib/views/ResearchView/assistant/components/Messages/components/AssistantMessage/utils.js +0 -25
  19. package/src/views/ResearchView/assistant/components/Messages/components/AssistantMessage/utils.ts +0 -31
  20. package/tests/research.assistantMessageUtils.test.ts +0 -149
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "49.4.1"
2
+ ".": "49.6.0"
3
3
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [49.6.0](https://github.com/DataBiosphere/findable-ui/compare/v49.5.0...v49.6.0) (2026-03-12)
4
+
5
+
6
+ ### Features
7
+
8
+ * drop extracted mentions and mappings from chat response display ([#822](https://github.com/DataBiosphere/findable-ui/issues/822)) ([#823](https://github.com/DataBiosphere/findable-ui/issues/823)) ([a8a33e8](https://github.com/DataBiosphere/findable-ui/commit/a8a33e86a44f5707b15da60e585b8e25010e6d94))
9
+
10
+ ## [49.5.0](https://github.com/DataBiosphere/findable-ui/compare/v49.4.1...v49.5.0) (2026-03-12)
11
+
12
+
13
+ ### Features
14
+
15
+ * migrate NIH account link check to ECM API [#819](https://github.com/DataBiosphere/findable-ui/issues/819) ([#820](https://github.com/DataBiosphere/findable-ui/issues/820)) ([34e5c2f](https://github.com/DataBiosphere/findable-ui/commit/34e5c2fcae9dfddb102ba03dce60658da7a4d4c7))
16
+ * migrate NIH account link check to External Credentials Manager API [#819](https://github.com/DataBiosphere/findable-ui/issues/819) ([34e5c2f](https://github.com/DataBiosphere/findable-ui/commit/34e5c2fcae9dfddb102ba03dce60658da7a4d4c7))
17
+
3
18
  ## [49.4.1](https://github.com/DataBiosphere/findable-ui/compare/v49.4.0...v49.4.1) (2026-03-10)
4
19
 
5
20
 
@@ -4,7 +4,7 @@ import { ANCHOR_TARGET, REL_ATTRIBUTE, } from "../../../../../../../../../Links/
4
4
  import { FormStep } from "../../formStep";
5
5
  export const ConnectTerraToNIHAccount = ({ active, completed, step, }) => {
6
6
  const onGotoTutorial = () => {
7
- window.open("https://support.terra.bio/hc/en-us/articles/19124069598235-Access-controlled-data-files-by-linking-your-NIH-account-in-Terra", ANCHOR_TARGET.BLANK, REL_ATTRIBUTE.NO_OPENER_NO_REFERRER);
7
+ window.open("https://support.terra.bio/hc/en-us/articles/32634034451099-RAS-Integration-for-AnVIL-Data-Launching-3-25-26", ANCHOR_TARGET.BLANK, REL_ATTRIBUTE.NO_OPENER_NO_REFERRER);
8
8
  };
9
9
  return (_jsx(FormStep, { action: _jsx(ButtonPrimary, { onClick: onGotoTutorial, children: "Go to Tutorial" }), active: active, completed: completed, step: step, text: _jsx("p", { children: "Next, connect your Terra account to your NIH account by following the tutorial below." }), title: "Connect Terra to your NIH account" }));
10
10
  };
@@ -6,31 +6,31 @@ import { FluidPaper } from "../../../../../../../common/Paper/paper.styles";
6
6
  import { Link } from "../../../../../../../Links/components/Link/link";
7
7
  export const NIHAccountExpiryWarning = () => {
8
8
  const expiryStatus = useAuthenticationNIHExpiry();
9
- const { isReady, linkExpired, linkExpireTime, linkWillExpire } = expiryStatus || {};
9
+ const { expirationTimestamp, isReady, linkExpired, linkWillExpire } = expiryStatus || {};
10
10
  if (!isReady)
11
11
  return null;
12
- return linkWillExpire || linkExpired ? (_jsx(Alert, { ...ALERT_PROPS.STANDARD_WARNING, component: FluidPaper, children: _jsxs("span", { children: [_jsx("span", { children: getExpiryMessage(linkExpired, linkExpireTime) }), " ", _jsxs("span", { children: ["Please", " ", _jsx(Link, { label: "renew your account", url: "https://support.terra.bio/hc/en-us/articles/19124069598235" }), " ", "link."] })] }) })) : null;
12
+ return linkWillExpire || linkExpired ? (_jsx(Alert, { ...ALERT_PROPS.STANDARD_WARNING, component: FluidPaper, children: _jsxs("span", { children: [_jsx("span", { children: getExpiryMessage(linkExpired, expirationTimestamp) }), " ", _jsxs("span", { children: ["Please", " ", _jsx(Link, { label: "renew your account", url: "https://support.terra.bio/hc/en-us/articles/32634034451099-RAS-Integration-for-AnVIL-Data-Launching-3-25-26" }), " ", "link."] })] }) })) : null;
13
13
  };
14
14
  /**
15
15
  * Calculates the remaining days until the link expires.
16
- * @param expireTime - Link expiration time in seconds.
16
+ * @param expirationTimestamp - Expiration timestamp as an ISO 8601 date-time string.
17
17
  * @returns remaining days until the link expires.
18
18
  */
19
- function getExpireTimeInDays(expireTime) {
20
- if (!expireTime) {
19
+ function getExpireTimeInDays(expirationTimestamp) {
20
+ if (!expirationTimestamp) {
21
21
  return 0;
22
22
  }
23
- return Math.max(Math.ceil(expireTimeInSeconds(expireTime) / 60 / 60 / 24), 0);
23
+ return Math.max(Math.ceil(expireTimeInSeconds(expirationTimestamp) / 60 / 60 / 24), 0);
24
24
  }
25
25
  /**
26
26
  * Returns an expiration message indicating whether the provided link has already expired or is set to expire.
27
27
  * @param linkExpired - Link expired flag.
28
- * @param expireTime - Link expiration time in seconds.
28
+ * @param expirationTimestamp - Expiration timestamp as an ISO 8601 date-time string.
29
29
  * @returns expiration message.
30
30
  */
31
- function getExpiryMessage(linkExpired, expireTime) {
31
+ function getExpiryMessage(linkExpired, expirationTimestamp) {
32
32
  if (linkExpired) {
33
33
  return "Your NIH account link has expired.";
34
34
  }
35
- return `Your NIH account link will expire in ${getExpireTimeInDays(expireTime)} days.`;
35
+ return `Your NIH account link will expire in ${getExpireTimeInDays(expirationTimestamp)} days.`;
36
36
  }
@@ -1,7 +1,7 @@
1
1
  interface UseAuthenticationNIHExpiry {
2
+ expirationTimestamp?: string;
2
3
  isReady: boolean;
3
4
  linkExpired?: boolean;
4
- linkExpireTime?: number;
5
5
  linkWillExpire?: boolean;
6
6
  }
7
7
  /**
@@ -11,8 +11,8 @@ interface UseAuthenticationNIHExpiry {
11
11
  export declare const useAuthenticationNIHExpiry: () => UseAuthenticationNIHExpiry;
12
12
  /**
13
13
  * Calculates the remaining time in seconds until the given expiration time.
14
- * @param expireTime - Expire time in seconds.
14
+ * @param expirationTimestamp - Expiration timestamp as an ISO 8601 date-time string.
15
15
  * @returns remaining time in seconds.
16
16
  */
17
- export declare function expireTimeInSeconds(expireTime: number): number;
17
+ export declare function expireTimeInSeconds(expirationTimestamp: string): number;
18
18
  export {};
@@ -8,42 +8,42 @@ const WARNING_WINDOW_SECONDS = 60 * 60 * 24 * 5; // 5 days.
8
8
  export const useAuthenticationNIHExpiry = () => {
9
9
  const { terraNIHProfileLoginStatus } = useTerraProfile();
10
10
  const { requestStatus, response } = terraNIHProfileLoginStatus;
11
- const { linkExpireTime } = response || {};
11
+ const { expirationTimestamp } = response || {};
12
12
  const isReady = requestStatus === REQUEST_STATUS.COMPLETED;
13
- const linkExpired = hasLinkedNIHAccountExpired(linkExpireTime);
14
- const linkWillExpire = isLinkedNIHAccountWillExpire(linkExpireTime);
13
+ const linkExpired = hasLinkedNIHAccountExpired(expirationTimestamp);
14
+ const linkWillExpire = isLinkedNIHAccountWillExpire(expirationTimestamp);
15
15
  return {
16
+ expirationTimestamp,
16
17
  isReady,
17
- linkExpireTime,
18
18
  linkExpired,
19
19
  linkWillExpire,
20
20
  };
21
21
  };
22
22
  /**
23
23
  * Calculates the remaining time in seconds until the given expiration time.
24
- * @param expireTime - Expire time in seconds.
24
+ * @param expirationTimestamp - Expiration timestamp as an ISO 8601 date-time string.
25
25
  * @returns remaining time in seconds.
26
26
  */
27
- export function expireTimeInSeconds(expireTime) {
28
- return expireTime - Date.now() / 1000;
27
+ export function expireTimeInSeconds(expirationTimestamp) {
28
+ return new Date(expirationTimestamp).getTime() / 1000 - Date.now() / 1000;
29
29
  }
30
30
  /**
31
31
  * Returns true if the linked NIH account has expired.
32
- * @param expireTime - Expire time in seconds.
32
+ * @param expirationTimestamp - Expiration timestamp as an ISO 8601 date-time string.
33
33
  * @returns true if the linked NIH account has expired.
34
34
  */
35
- function hasLinkedNIHAccountExpired(expireTime) {
36
- if (!expireTime)
35
+ function hasLinkedNIHAccountExpired(expirationTimestamp) {
36
+ if (!expirationTimestamp)
37
37
  return;
38
- return expireTimeInSeconds(expireTime) < 0;
38
+ return expireTimeInSeconds(expirationTimestamp) < 0;
39
39
  }
40
40
  /**
41
41
  * Returns true if the linked NIH account will expire in less than a week.
42
- * @param expireTime - Expire time in seconds.
42
+ * @param expirationTimestamp - Expiration timestamp as an ISO 8601 date-time string.
43
43
  * @returns true if the linked NIH account will expire in less than a week.
44
44
  */
45
- function isLinkedNIHAccountWillExpire(expireTime) {
46
- if (!expireTime)
45
+ function isLinkedNIHAccountWillExpire(expirationTimestamp) {
46
+ if (!expirationTimestamp)
47
47
  return;
48
- return expireTimeInSeconds(expireTime) < WARNING_WINDOW_SECONDS;
48
+ return expireTimeInSeconds(expirationTimestamp) < WARNING_WINDOW_SECONDS;
49
49
  }
@@ -1,13 +1,9 @@
1
1
  import { LoginStatus } from "./common/entities";
2
- interface DatasetPermission {
3
- authorized: boolean;
4
- name: string;
5
- }
6
2
  type Status = LoginStatus<TerraNIHResponse>;
7
3
  export interface TerraNIHResponse {
8
- datasetPermissions: DatasetPermission[];
9
- linkedNihUsername: string;
10
- linkExpireTime: number;
4
+ authenticated?: boolean;
5
+ expirationTimestamp: string;
6
+ externalUserId: string;
11
7
  }
12
8
  /**
13
9
  * Returns Terra NIH login status from configured endpoint.
@@ -1,7 +1,7 @@
1
1
  import { useCallback, useEffect, useState } from "react";
2
2
  import { useAuthenticationConfig } from "../../../../hooks/authentication/config/useAuthenticationConfig";
3
3
  import { LOGIN_STATUS_FAILED, LOGIN_STATUS_NOT_STARTED, LOGIN_STATUS_PENDING, TERRA_SERVICE_ID, } from "./common/constants";
4
- import { REQUEST_STATUS, } from "./common/entities";
4
+ import { REQUEST_STATUS } from "./common/entities";
5
5
  import { getAuthenticationRequestOptions, initLoginStatus, } from "./common/utils";
6
6
  import { getServiceEndpoint } from "./utils";
7
7
  const ENDPOINT_ID = "nihStatus";
@@ -22,19 +22,31 @@ export const useFetchTerraNIHProfile = (token) => {
22
22
  }
23
23
  setLoginStatus(LOGIN_STATUS_PENDING);
24
24
  fetch(endpoint, getAuthenticationRequestOptions(accessToken))
25
- .then((response) => response.json())
26
25
  .then((response) => {
27
- if (isResponseError(response)) {
28
- setLoginStatus(LOGIN_STATUS_FAILED);
29
- }
30
- else {
26
+ if (response.status === 404) {
31
27
  setLoginStatus((prevStatus) => ({
32
28
  ...prevStatus,
33
- isSuccess: isResponseSuccess(response),
29
+ isSuccess: false,
34
30
  requestStatus: REQUEST_STATUS.COMPLETED,
35
- response,
31
+ response: undefined,
36
32
  }));
33
+ return;
37
34
  }
35
+ if (!response.ok) {
36
+ setLoginStatus(LOGIN_STATUS_FAILED);
37
+ return;
38
+ }
39
+ return response.json();
40
+ })
41
+ .then((response) => {
42
+ if (!response)
43
+ return;
44
+ setLoginStatus((prevStatus) => ({
45
+ ...prevStatus,
46
+ isSuccess: isResponseSuccess(response),
47
+ requestStatus: REQUEST_STATUS.COMPLETED,
48
+ response,
49
+ }));
38
50
  })
39
51
  .catch((err) => {
40
52
  console.log(err); // TODO handle error.
@@ -50,18 +62,10 @@ export const useFetchTerraNIHProfile = (token) => {
50
62
  return loginStatus;
51
63
  };
52
64
  /**
53
- * Returns true if response is an error response.
54
- * @param response - Response.
55
- * @returns true if response is an error response.
56
- */
57
- function isResponseError(response) {
58
- return Boolean(response.statusCode);
59
- }
60
- /**
61
- * Returns true if the user accepted terms of service version is current.
65
+ * Returns true if the user has a linked external account.
62
66
  * @param response - Response.
63
67
  * @returns true if response is successful.
64
68
  */
65
69
  function isResponseSuccess(response) {
66
- return Boolean(response.linkedNihUsername);
70
+ return Boolean(response.externalUserId);
67
71
  }
@@ -4,6 +4,6 @@ import { AssistantMessageProps } from "./types";
4
4
  * Renders an assistant message.
5
5
  * @param props - Component props.
6
6
  * @param props.message - Assistant message.
7
- * @returns The assistant message element.
7
+ * @returns The assistant message element, or null if there is no message.
8
8
  */
9
- export declare const AssistantMessage: ({ message, }: AssistantMessageProps) => JSX.Element;
9
+ export declare const AssistantMessage: ({ message, }: AssistantMessageProps) => JSX.Element | null;
@@ -1,15 +1,14 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Stack, Typography } from "@mui/material";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Typography } from "@mui/material";
3
3
  import { TYPOGRAPHY_PROPS } from "../../../../../../../styles/common/mui/typography";
4
- import { getMappings, getMentions } from "./utils";
5
4
  /**
6
5
  * Renders an assistant message.
7
6
  * @param props - Component props.
8
7
  * @param props.message - Assistant message.
9
- * @returns The assistant message element.
8
+ * @returns The assistant message element, or null if there is no message.
10
9
  */
11
10
  export const AssistantMessage = ({ message, }) => {
12
- const mentions = getMentions(message);
13
- const mappings = getMappings(message);
14
- return (_jsxs(Stack, { gap: 2, useFlexGap: true, children: [message.response.message && (_jsx(Typography, { variant: TYPOGRAPHY_PROPS.VARIANT.BODY_400, children: message.response.message })), mentions && (_jsxs(Typography, { variant: TYPOGRAPHY_PROPS.VARIANT.BODY_SMALL_400, children: [_jsx(Typography, { color: TYPOGRAPHY_PROPS.COLOR.INK_LIGHT, variant: TYPOGRAPHY_PROPS.VARIANT.BODY_SMALL_500, children: "Extracted mentions:" }), " ", mentions] })), mappings && (_jsxs(Typography, { variant: TYPOGRAPHY_PROPS.VARIANT.BODY_SMALL_400, children: [_jsx(Typography, { color: TYPOGRAPHY_PROPS.COLOR.INK_LIGHT, variant: TYPOGRAPHY_PROPS.VARIANT.BODY_SMALL_500, children: "Extracted mappings:" }), " ", mappings] }))] }));
11
+ if (!message.response.message)
12
+ return null;
13
+ return (_jsx(Typography, { variant: TYPOGRAPHY_PROPS.VARIANT.BODY_400, children: message.response.message }));
15
14
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@databiosphere/findable-ui",
3
- "version": "49.4.1",
3
+ "version": "49.6.0",
4
4
  "description": "",
5
5
  "scripts": {
6
6
  "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
@@ -19,7 +19,7 @@ export const ConnectTerraToNIHAccount = ({
19
19
  }: ConnectTerraToNIHAccountProps): JSX.Element | null => {
20
20
  const onGotoTutorial = (): void => {
21
21
  window.open(
22
- "https://support.terra.bio/hc/en-us/articles/19124069598235-Access-controlled-data-files-by-linking-your-NIH-account-in-Terra",
22
+ "https://support.terra.bio/hc/en-us/articles/32634034451099-RAS-Integration-for-AnVIL-Data-Launching-3-25-26",
23
23
  ANCHOR_TARGET.BLANK,
24
24
  REL_ATTRIBUTE.NO_OPENER_NO_REFERRER,
25
25
  );
@@ -10,7 +10,7 @@ import { Link } from "../../../../../../../Links/components/Link/link";
10
10
 
11
11
  export const NIHAccountExpiryWarning = (): JSX.Element | null => {
12
12
  const expiryStatus = useAuthenticationNIHExpiry();
13
- const { isReady, linkExpired, linkExpireTime, linkWillExpire } =
13
+ const { expirationTimestamp, isReady, linkExpired, linkWillExpire } =
14
14
  expiryStatus || {};
15
15
 
16
16
  if (!isReady) return null;
@@ -18,12 +18,12 @@ export const NIHAccountExpiryWarning = (): JSX.Element | null => {
18
18
  return linkWillExpire || linkExpired ? (
19
19
  <Alert {...ALERT_PROPS.STANDARD_WARNING} component={FluidPaper}>
20
20
  <span>
21
- <span>{getExpiryMessage(linkExpired, linkExpireTime)}</span>{" "}
21
+ <span>{getExpiryMessage(linkExpired, expirationTimestamp)}</span>{" "}
22
22
  <span>
23
23
  Please{" "}
24
24
  <Link
25
25
  label="renew your account"
26
- url="https://support.terra.bio/hc/en-us/articles/19124069598235"
26
+ url="https://support.terra.bio/hc/en-us/articles/32634034451099-RAS-Integration-for-AnVIL-Data-Launching-3-25-26"
27
27
  />{" "}
28
28
  link.
29
29
  </span>
@@ -34,27 +34,33 @@ export const NIHAccountExpiryWarning = (): JSX.Element | null => {
34
34
 
35
35
  /**
36
36
  * Calculates the remaining days until the link expires.
37
- * @param expireTime - Link expiration time in seconds.
37
+ * @param expirationTimestamp - Expiration timestamp as an ISO 8601 date-time string.
38
38
  * @returns remaining days until the link expires.
39
39
  */
40
- function getExpireTimeInDays(expireTime?: number): number {
41
- if (!expireTime) {
40
+ function getExpireTimeInDays(expirationTimestamp?: string): number {
41
+ if (!expirationTimestamp) {
42
42
  return 0;
43
43
  }
44
- return Math.max(Math.ceil(expireTimeInSeconds(expireTime) / 60 / 60 / 24), 0);
44
+ return Math.max(
45
+ Math.ceil(expireTimeInSeconds(expirationTimestamp) / 60 / 60 / 24),
46
+ 0,
47
+ );
45
48
  }
46
49
 
47
50
  /**
48
51
  * Returns an expiration message indicating whether the provided link has already expired or is set to expire.
49
52
  * @param linkExpired - Link expired flag.
50
- * @param expireTime - Link expiration time in seconds.
53
+ * @param expirationTimestamp - Expiration timestamp as an ISO 8601 date-time string.
51
54
  * @returns expiration message.
52
55
  */
53
- function getExpiryMessage(linkExpired?: boolean, expireTime?: number): string {
56
+ function getExpiryMessage(
57
+ linkExpired?: boolean,
58
+ expirationTimestamp?: string,
59
+ ): string {
54
60
  if (linkExpired) {
55
61
  return "Your NIH account link has expired.";
56
62
  }
57
63
  return `Your NIH account link will expire in ${getExpireTimeInDays(
58
- expireTime,
64
+ expirationTimestamp,
59
65
  )} days.`;
60
66
  }
@@ -4,9 +4,9 @@ import { REQUEST_STATUS } from "../../../providers/authentication/terra/hooks/co
4
4
  const WARNING_WINDOW_SECONDS = 60 * 60 * 24 * 5; // 5 days.
5
5
 
6
6
  interface UseAuthenticationNIHExpiry {
7
+ expirationTimestamp?: string;
7
8
  isReady: boolean;
8
9
  linkExpired?: boolean;
9
- linkExpireTime?: number;
10
10
  linkWillExpire?: boolean;
11
11
  }
12
12
 
@@ -17,13 +17,13 @@ interface UseAuthenticationNIHExpiry {
17
17
  export const useAuthenticationNIHExpiry = (): UseAuthenticationNIHExpiry => {
18
18
  const { terraNIHProfileLoginStatus } = useTerraProfile();
19
19
  const { requestStatus, response } = terraNIHProfileLoginStatus;
20
- const { linkExpireTime } = response || {};
20
+ const { expirationTimestamp } = response || {};
21
21
  const isReady = requestStatus === REQUEST_STATUS.COMPLETED;
22
- const linkExpired = hasLinkedNIHAccountExpired(linkExpireTime);
23
- const linkWillExpire = isLinkedNIHAccountWillExpire(linkExpireTime);
22
+ const linkExpired = hasLinkedNIHAccountExpired(expirationTimestamp);
23
+ const linkWillExpire = isLinkedNIHAccountWillExpire(expirationTimestamp);
24
24
  return {
25
+ expirationTimestamp,
25
26
  isReady,
26
- linkExpireTime,
27
27
  linkExpired,
28
28
  linkWillExpire,
29
29
  };
@@ -31,31 +31,33 @@ export const useAuthenticationNIHExpiry = (): UseAuthenticationNIHExpiry => {
31
31
 
32
32
  /**
33
33
  * Calculates the remaining time in seconds until the given expiration time.
34
- * @param expireTime - Expire time in seconds.
34
+ * @param expirationTimestamp - Expiration timestamp as an ISO 8601 date-time string.
35
35
  * @returns remaining time in seconds.
36
36
  */
37
- export function expireTimeInSeconds(expireTime: number): number {
38
- return expireTime - Date.now() / 1000;
37
+ export function expireTimeInSeconds(expirationTimestamp: string): number {
38
+ return new Date(expirationTimestamp).getTime() / 1000 - Date.now() / 1000;
39
39
  }
40
40
 
41
41
  /**
42
42
  * Returns true if the linked NIH account has expired.
43
- * @param expireTime - Expire time in seconds.
43
+ * @param expirationTimestamp - Expiration timestamp as an ISO 8601 date-time string.
44
44
  * @returns true if the linked NIH account has expired.
45
45
  */
46
- function hasLinkedNIHAccountExpired(expireTime?: number): boolean | undefined {
47
- if (!expireTime) return;
48
- return expireTimeInSeconds(expireTime) < 0;
46
+ function hasLinkedNIHAccountExpired(
47
+ expirationTimestamp?: string,
48
+ ): boolean | undefined {
49
+ if (!expirationTimestamp) return;
50
+ return expireTimeInSeconds(expirationTimestamp) < 0;
49
51
  }
50
52
 
51
53
  /**
52
54
  * Returns true if the linked NIH account will expire in less than a week.
53
- * @param expireTime - Expire time in seconds.
55
+ * @param expirationTimestamp - Expiration timestamp as an ISO 8601 date-time string.
54
56
  * @returns true if the linked NIH account will expire in less than a week.
55
57
  */
56
58
  function isLinkedNIHAccountWillExpire(
57
- expireTime?: number,
59
+ expirationTimestamp?: string,
58
60
  ): boolean | undefined {
59
- if (!expireTime) return;
60
- return expireTimeInSeconds(expireTime) < WARNING_WINDOW_SECONDS;
61
+ if (!expirationTimestamp) return;
62
+ return expireTimeInSeconds(expirationTimestamp) < WARNING_WINDOW_SECONDS;
61
63
  }
@@ -6,11 +6,7 @@ import {
6
6
  LOGIN_STATUS_PENDING,
7
7
  TERRA_SERVICE_ID,
8
8
  } from "./common/constants";
9
- import {
10
- LoginResponseError,
11
- LoginStatus,
12
- REQUEST_STATUS,
13
- } from "./common/entities";
9
+ import { LoginStatus, REQUEST_STATUS } from "./common/entities";
14
10
  import {
15
11
  getAuthenticationRequestOptions,
16
12
  initLoginStatus,
@@ -19,17 +15,12 @@ import { getServiceEndpoint } from "./utils";
19
15
 
20
16
  const ENDPOINT_ID = "nihStatus";
21
17
 
22
- interface DatasetPermission {
23
- authorized: boolean;
24
- name: string;
25
- }
26
-
27
18
  type Status = LoginStatus<TerraNIHResponse>;
28
19
 
29
20
  export interface TerraNIHResponse {
30
- datasetPermissions: DatasetPermission[];
31
- linkedNihUsername: string;
32
- linkExpireTime: number;
21
+ authenticated?: boolean;
22
+ expirationTimestamp: string;
23
+ externalUserId: string;
33
24
  }
34
25
 
35
26
  /**
@@ -53,18 +44,30 @@ export const useFetchTerraNIHProfile = (token?: string): Status => {
53
44
  }
54
45
  setLoginStatus(LOGIN_STATUS_PENDING as Status);
55
46
  fetch(endpoint, getAuthenticationRequestOptions(accessToken))
56
- .then((response) => response.json())
57
- .then((response: LoginResponseError | TerraNIHResponse) => {
58
- if (isResponseError(response)) {
59
- setLoginStatus(LOGIN_STATUS_FAILED as Status);
60
- } else {
47
+ .then((response) => {
48
+ if (response.status === 404) {
61
49
  setLoginStatus((prevStatus) => ({
62
50
  ...prevStatus,
63
- isSuccess: isResponseSuccess(response),
51
+ isSuccess: false,
64
52
  requestStatus: REQUEST_STATUS.COMPLETED,
65
- response,
53
+ response: undefined,
66
54
  }));
55
+ return;
56
+ }
57
+ if (!response.ok) {
58
+ setLoginStatus(LOGIN_STATUS_FAILED as Status);
59
+ return;
67
60
  }
61
+ return response.json();
62
+ })
63
+ .then((response?: TerraNIHResponse) => {
64
+ if (!response) return;
65
+ setLoginStatus((prevStatus) => ({
66
+ ...prevStatus,
67
+ isSuccess: isResponseSuccess(response),
68
+ requestStatus: REQUEST_STATUS.COMPLETED,
69
+ response,
70
+ }));
68
71
  })
69
72
  .catch((err) => {
70
73
  console.log(err); // TODO handle error.
@@ -84,21 +87,10 @@ export const useFetchTerraNIHProfile = (token?: string): Status => {
84
87
  };
85
88
 
86
89
  /**
87
- * Returns true if response is an error response.
88
- * @param response - Response.
89
- * @returns true if response is an error response.
90
- */
91
- function isResponseError(
92
- response: TerraNIHResponse | LoginResponseError,
93
- ): response is LoginResponseError {
94
- return Boolean((response as LoginResponseError).statusCode);
95
- }
96
-
97
- /**
98
- * Returns true if the user accepted terms of service version is current.
90
+ * Returns true if the user has a linked external account.
99
91
  * @param response - Response.
100
92
  * @returns true if response is successful.
101
93
  */
102
94
  function isResponseSuccess(response: TerraNIHResponse): boolean {
103
- return Boolean(response.linkedNihUsername);
95
+ return Boolean(response.externalUserId);
104
96
  }
@@ -1,49 +1,21 @@
1
- import { Stack, Typography } from "@mui/material";
1
+ import { Typography } from "@mui/material";
2
2
  import { JSX } from "react";
3
3
  import { AssistantMessageProps } from "./types";
4
4
  import { TYPOGRAPHY_PROPS } from "../../../../../../../styles/common/mui/typography";
5
- import { getMappings, getMentions } from "./utils";
6
5
 
7
6
  /**
8
7
  * Renders an assistant message.
9
8
  * @param props - Component props.
10
9
  * @param props.message - Assistant message.
11
- * @returns The assistant message element.
10
+ * @returns The assistant message element, or null if there is no message.
12
11
  */
13
12
  export const AssistantMessage = ({
14
13
  message,
15
- }: AssistantMessageProps): JSX.Element => {
16
- const mentions = getMentions(message);
17
- const mappings = getMappings(message);
14
+ }: AssistantMessageProps): JSX.Element | null => {
15
+ if (!message.response.message) return null;
18
16
  return (
19
- <Stack gap={2} useFlexGap>
20
- {message.response.message && (
21
- <Typography variant={TYPOGRAPHY_PROPS.VARIANT.BODY_400}>
22
- {message.response.message}
23
- </Typography>
24
- )}
25
- {mentions && (
26
- <Typography variant={TYPOGRAPHY_PROPS.VARIANT.BODY_SMALL_400}>
27
- <Typography
28
- color={TYPOGRAPHY_PROPS.COLOR.INK_LIGHT}
29
- variant={TYPOGRAPHY_PROPS.VARIANT.BODY_SMALL_500}
30
- >
31
- Extracted mentions:
32
- </Typography>{" "}
33
- {mentions}
34
- </Typography>
35
- )}
36
- {mappings && (
37
- <Typography variant={TYPOGRAPHY_PROPS.VARIANT.BODY_SMALL_400}>
38
- <Typography
39
- color={TYPOGRAPHY_PROPS.COLOR.INK_LIGHT}
40
- variant={TYPOGRAPHY_PROPS.VARIANT.BODY_SMALL_500}
41
- >
42
- Extracted mappings:
43
- </Typography>{" "}
44
- {mappings}
45
- </Typography>
46
- )}
47
- </Stack>
17
+ <Typography variant={TYPOGRAPHY_PROPS.VARIANT.BODY_400}>
18
+ {message.response.message}
19
+ </Typography>
48
20
  );
49
21
  };
@@ -1,13 +0,0 @@
1
- import { AssistantMessage } from "../../../../../state/types";
2
- /**
3
- * Formats mention mappings from an assistant message as a readable string.
4
- * @param message - Assistant message containing response mentions.
5
- * @returns Formatted facet-value mappings separated by slashes, or empty string if no mentions.
6
- */
7
- export declare function getMappings(message: AssistantMessage): string;
8
- /**
9
- * Extracts original mention text from an assistant message.
10
- * @param message - Assistant message containing response mentions.
11
- * @returns Comma-separated original mention text, or empty string if no mentions.
12
- */
13
- export declare function getMentions(message: AssistantMessage): string;
@@ -1,25 +0,0 @@
1
- /**
2
- * Formats mention mappings from an assistant message as a readable string.
3
- * @param message - Assistant message containing response mentions.
4
- * @returns Formatted facet-value mappings separated by slashes, or empty string if no mentions.
5
- */
6
- export function getMappings(message) {
7
- const mappings = message.response.query.mentions.reduce((acc, m) => {
8
- const key = m.exclude ? `${m.facet} (exclude)` : m.facet;
9
- acc[key] = [...(acc[key] || []), ...m.values];
10
- return acc;
11
- }, {});
12
- return Object.entries(mappings)
13
- .map(([facet, values]) => `${facet}: ${values.join(", ")}`)
14
- .join(" / ");
15
- }
16
- /**
17
- * Extracts original mention text from an assistant message.
18
- * @param message - Assistant message containing response mentions.
19
- * @returns Comma-separated original mention text, or empty string if no mentions.
20
- */
21
- export function getMentions(message) {
22
- return message.response.query.mentions
23
- .map((mention) => mention.originalText)
24
- .join(", ");
25
- }
@@ -1,31 +0,0 @@
1
- import { AssistantMessage } from "../../../../../state/types";
2
-
3
- /**
4
- * Formats mention mappings from an assistant message as a readable string.
5
- * @param message - Assistant message containing response mentions.
6
- * @returns Formatted facet-value mappings separated by slashes, or empty string if no mentions.
7
- */
8
- export function getMappings(message: AssistantMessage): string {
9
- const mappings = message.response.query.mentions.reduce<
10
- Record<string, string[]>
11
- >((acc, m) => {
12
- const key = m.exclude ? `${m.facet} (exclude)` : m.facet;
13
- acc[key] = [...(acc[key] || []), ...m.values];
14
- return acc;
15
- }, {});
16
-
17
- return Object.entries(mappings)
18
- .map(([facet, values]) => `${facet}: ${values.join(", ")}`)
19
- .join(" / ");
20
- }
21
-
22
- /**
23
- * Extracts original mention text from an assistant message.
24
- * @param message - Assistant message containing response mentions.
25
- * @returns Comma-separated original mention text, or empty string if no mentions.
26
- */
27
- export function getMentions(message: AssistantMessage): string {
28
- return message.response.query.mentions
29
- .map((mention) => mention.originalText)
30
- .join(", ");
31
- }
@@ -1,149 +0,0 @@
1
- import {
2
- getMappings,
3
- getMentions,
4
- } from "../src/views/ResearchView/assistant/components/Messages/components/AssistantMessage/utils";
5
- import {
6
- AssistantMessage,
7
- MESSAGE_TYPE,
8
- } from "../src/views/ResearchView/state/types";
9
-
10
- /**
11
- * Creates a mock AssistantMessage with the given mentions.
12
- * @param mentions - Array of mention objects.
13
- * @returns A mock AssistantMessage.
14
- */
15
- function mockMessage(
16
- mentions: AssistantMessage["response"]["query"]["mentions"],
17
- ): AssistantMessage {
18
- return {
19
- createdAt: Date.now(),
20
- response: {
21
- intent: "auto",
22
- message: null,
23
- query: {
24
- mentions,
25
- message: null,
26
- },
27
- timing: {
28
- lookupMs: 0,
29
- pipelineMs: 0,
30
- totalMs: 0,
31
- },
32
- },
33
- type: MESSAGE_TYPE.ASSISTANT,
34
- };
35
- }
36
-
37
- describe("getMappings", () => {
38
- it("returns empty string for empty mentions", () => {
39
- expect(getMappings(mockMessage([]))).toBe("");
40
- });
41
-
42
- it("formats a single mention", () => {
43
- const message = mockMessage([
44
- {
45
- exclude: false,
46
- facet: "disease",
47
- originalText: "T2D",
48
- values: ["T2D"],
49
- },
50
- ]);
51
- expect(getMappings(message)).toBe("disease: T2D");
52
- });
53
-
54
- it("formats a mention with multiple values", () => {
55
- const message = mockMessage([
56
- {
57
- exclude: false,
58
- facet: "disease",
59
- originalText: "cancer",
60
- values: ["lung cancer", "breast cancer"],
61
- },
62
- ]);
63
- expect(getMappings(message)).toBe("disease: lung cancer, breast cancer");
64
- });
65
-
66
- it("groups mentions by facet", () => {
67
- const message = mockMessage([
68
- {
69
- exclude: false,
70
- facet: "disease",
71
- originalText: "T2D",
72
- values: ["T2D"],
73
- },
74
- {
75
- exclude: false,
76
- facet: "disease",
77
- originalText: "T1D",
78
- values: ["T1D"],
79
- },
80
- ]);
81
- expect(getMappings(message)).toBe("disease: T2D, T1D");
82
- });
83
-
84
- it("separates different facets with slashes", () => {
85
- const message = mockMessage([
86
- {
87
- exclude: false,
88
- facet: "disease",
89
- originalText: "T2D",
90
- values: ["T2D"],
91
- },
92
- {
93
- exclude: false,
94
- facet: "data_type",
95
- originalText: "WGS",
96
- values: ["WGS"],
97
- },
98
- ]);
99
- expect(getMappings(message)).toBe("disease: T2D / data_type: WGS");
100
- });
101
-
102
- it("marks excluded facets", () => {
103
- const message = mockMessage([
104
- {
105
- exclude: true,
106
- facet: "disease",
107
- originalText: "T2D",
108
- values: ["T2D"],
109
- },
110
- ]);
111
- expect(getMappings(message)).toBe("disease (exclude): T2D");
112
- });
113
- });
114
-
115
- describe("getMentions", () => {
116
- it("returns empty string for empty mentions", () => {
117
- expect(getMentions(mockMessage([]))).toBe("");
118
- });
119
-
120
- it("returns original text for a single mention", () => {
121
- const message = mockMessage([
122
- {
123
- exclude: false,
124
- facet: "disease",
125
- originalText: "T2D",
126
- values: ["T2D"],
127
- },
128
- ]);
129
- expect(getMentions(message)).toBe("T2D");
130
- });
131
-
132
- it("joins multiple mentions with commas", () => {
133
- const message = mockMessage([
134
- {
135
- exclude: false,
136
- facet: "disease",
137
- originalText: "T2D",
138
- values: ["T2D"],
139
- },
140
- {
141
- exclude: false,
142
- facet: "data_type",
143
- originalText: "WGS",
144
- values: ["WGS"],
145
- },
146
- ]);
147
- expect(getMentions(message)).toBe("T2D, WGS");
148
- });
149
- });