Skip to content

Commit 30eac3c

Browse files
committed
Add option to generate token for the montandon
1 parent 939956e commit 30eac3c

File tree

11 files changed

+413
-3
lines changed

11 files changed

+413
-3
lines changed

.changeset/strong-singers-think.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"go-web-app": patch
3+
---
4+
5+
Add option to generate API token for Montandon in the user profile

app/env.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { defineConfig, Schema } from '@julr/vite-plugin-validate-env';
33
export default defineConfig({
44
APP_TITLE: Schema.string(),
55
APP_ENVIRONMENT: Schema.enum(['development', 'testing', 'staging', 'production'] as const),
6-
APP_API_ENDPOINT: Schema.string({ format: 'url', protocol: true }),
6+
APP_API_ENDPOINT: Schema.string({ format: 'url', protocol: true, tld: false }),
77
APP_ADMIN_URL: Schema.string.optional({ format: 'url', protocol: true }),
88
APP_SHOW_ENV_BANNER: Schema.boolean.optional(),
99
APP_MAPBOX_ACCESS_TOKEN: Schema.string(),
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"namespace": "generateMontandonTokenModal",
3+
"strings": {
4+
"title": "Generate token for Montandon",
5+
"description": "By clicking 'Accept & Generate' button, you'll accept the {termsLink} of the Montandon API. Please make sure to review it.",
6+
"termsAndConditionLabel": "Terms & Condition",
7+
"cancelButtonLabel": "Cancel",
8+
"acceptButtonLabel": "Accept & Generate",
9+
"doneButtonLabel": "Done",
10+
"titleInputLabel": "Title for token",
11+
"titleInputPlaceholder": "Enter a descriptive title for the token",
12+
"titleInputHint": "Note: title should contain at least {minCharacters} characters",
13+
"tokenNote": "Please make sure to copy and store the token safely. Once you close this modal, you'll not be able to see the token again!"
14+
}
15+
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import { useCallback } from 'react';
2+
import {
3+
Button,
4+
Modal,
5+
TextInput,
6+
} from '@ifrc-go/ui';
7+
import {
8+
useBooleanState,
9+
useTranslation,
10+
} from '@ifrc-go/ui/hooks';
11+
import {
12+
resolveToComponent,
13+
resolveToString,
14+
} from '@ifrc-go/ui/utils';
15+
import {
16+
isDefined,
17+
isNotDefined,
18+
} from '@togglecorp/fujs';
19+
20+
import Link from '#components/Link';
21+
import useInputState from '#hooks/useInputState';
22+
import { useLazyRequest } from '#utils/restRequest';
23+
24+
import TokenDetails from '../TokenDetails';
25+
26+
import i18n from './i18n.json';
27+
import styles from './styles.module.css';
28+
29+
const MIN_TITLE_LENGTH = 3;
30+
31+
interface Props {
32+
onClose: () => void;
33+
}
34+
35+
function GenerateMontandonTokenModal(props: Props) {
36+
const { onClose } = props;
37+
const [accepted, { setTrue: setAcceptedTrue }] = useBooleanState(false);
38+
const strings = useTranslation(i18n);
39+
40+
const [tokenTitle, setTokenTitle] = useInputState<string | undefined>(undefined);
41+
42+
const {
43+
pending: tokenPending,
44+
response: tokenResponse,
45+
error: tokenError,
46+
trigger: triggerGenerateTokenRequest,
47+
} = useLazyRequest({
48+
method: 'POST',
49+
url: '/api/v2/external-token/',
50+
body: (context: string) => ({
51+
title: context,
52+
expire_timestamp: undefined,
53+
}),
54+
});
55+
56+
const handleAcceptAndGenerateClick = useCallback(
57+
(title: string | undefined) => {
58+
if (isDefined(title) && title.length >= MIN_TITLE_LENGTH) {
59+
setAcceptedTrue();
60+
triggerGenerateTokenRequest(title);
61+
}
62+
},
63+
[setAcceptedTrue, triggerGenerateTokenRequest],
64+
);
65+
66+
return (
67+
<Modal
68+
className={styles.generateMontandonTokenModal}
69+
heading={strings.title}
70+
onClose={onClose}
71+
headerDescription={!accepted && resolveToComponent(
72+
strings.description,
73+
{
74+
termsLink: (
75+
<Link
76+
external
77+
// TODO: use actual link
78+
href="https://github.com/IFRCGo"
79+
withLinkIcon
80+
withUnderline
81+
>
82+
{strings.termsAndConditionLabel}
83+
</Link>
84+
),
85+
},
86+
)}
87+
size="sm"
88+
contentViewType="vertical"
89+
spacing="comfortable"
90+
footerActions={(
91+
<>
92+
{!accepted && (
93+
<>
94+
<Button
95+
name={undefined}
96+
onClick={onClose}
97+
variant="secondary"
98+
>
99+
{strings.cancelButtonLabel}
100+
</Button>
101+
<Button
102+
name={tokenTitle}
103+
onClick={handleAcceptAndGenerateClick}
104+
disabled={isNotDefined(tokenTitle)
105+
|| tokenTitle.trim().length < MIN_TITLE_LENGTH}
106+
>
107+
{strings.acceptButtonLabel}
108+
</Button>
109+
</>
110+
)}
111+
{accepted && (
112+
<Button
113+
name={undefined}
114+
onClick={onClose}
115+
>
116+
{strings.doneButtonLabel}
117+
</Button>
118+
)}
119+
</>
120+
)}
121+
pending={tokenPending}
122+
errored={isDefined(tokenError)}
123+
errorMessage={tokenError?.value.messageForNotification}
124+
>
125+
{!accepted && (
126+
<TextInput
127+
name="tokenTitle"
128+
label={strings.titleInputLabel}
129+
value={tokenTitle}
130+
onChange={setTokenTitle}
131+
hint={
132+
resolveToString(
133+
strings.titleInputHint,
134+
{ minCharacters: MIN_TITLE_LENGTH },
135+
)
136+
}
137+
placeholder={strings.titleInputPlaceholder}
138+
/>
139+
)}
140+
{accepted && (
141+
<>
142+
<TokenDetails
143+
data={tokenResponse}
144+
/>
145+
<div className={styles.note}>
146+
{strings.tokenNote}
147+
</div>
148+
</>
149+
)}
150+
</Modal>
151+
);
152+
}
153+
154+
export default GenerateMontandonTokenModal;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.generate-montandon-token-modal {
2+
.note {
3+
color: var(--go-ui-color-primary-red);
4+
font-weight: var(--go-ui-font-weight-medium);
5+
}
6+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"namespace": "tokenDetails",
3+
"strings": {
4+
"copyButtonLabel": "Copy to clipboard",
5+
"createdAtLabel": "Created at",
6+
"expiresOnLabel": "Expires on",
7+
"copySuccessMessage": "Token copied to the clipboard!"
8+
}
9+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { useCallback } from 'react';
2+
import { CopyLineIcon } from '@ifrc-go/icons';
3+
import {
4+
Button,
5+
Container,
6+
TextOutput,
7+
} from '@ifrc-go/ui';
8+
import { useTranslation } from '@ifrc-go/ui/hooks';
9+
import {
10+
_cs,
11+
isDefined,
12+
} from '@togglecorp/fujs';
13+
14+
import useAlert from '#hooks/useAlert';
15+
import { GoApiResponse } from '#utils/restRequest';
16+
17+
import i18n from './i18n.json';
18+
import styles from './styles.module.css';
19+
20+
interface Props {
21+
className?: string;
22+
data: GoApiResponse<'/api/v2/external-token/{id}/'> | undefined;
23+
}
24+
25+
function TokenDetails(props: Props) {
26+
const {
27+
data,
28+
className,
29+
} = props;
30+
31+
const alert = useAlert();
32+
const strings = useTranslation(i18n);
33+
34+
const handleCopyButtonClick = useCallback(
35+
(token: string | undefined) => {
36+
if (isDefined(token)) {
37+
window.navigator.clipboard.writeText(token);
38+
alert.show(strings.copySuccessMessage);
39+
}
40+
},
41+
[alert, strings],
42+
);
43+
44+
return (
45+
<Container
46+
className={_cs(styles.tokenDetails, className)}
47+
heading={data?.title}
48+
headingLevel={5}
49+
actions={isDefined(data?.token) && (
50+
<Button
51+
name={data?.token}
52+
variant="tertiary"
53+
title={strings.copyButtonLabel}
54+
onClick={handleCopyButtonClick}
55+
>
56+
<CopyLineIcon />
57+
</Button>
58+
)}
59+
headerDescription={(
60+
<>
61+
<TextOutput
62+
label={strings.createdAtLabel}
63+
value={data?.created_at}
64+
valueType="date"
65+
className={styles.createdAt}
66+
/>
67+
<TextOutput
68+
label={strings.expiresOnLabel}
69+
value={data?.expire_timestamp}
70+
valueType="date"
71+
className={styles.expiresOn}
72+
/>
73+
</>
74+
)}
75+
>
76+
{isDefined(data?.token) && (
77+
<div className={styles.token}>
78+
{data?.token}
79+
</div>
80+
)}
81+
</Container>
82+
);
83+
}
84+
85+
export default TokenDetails;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
.token-details {
2+
.token {
3+
flex-grow: 1;
4+
background-color: var(--go-ui-color-background);
5+
padding: var(--go-ui-spacing-md);
6+
overflow: auto;
7+
word-break: break-word;
8+
font-family: monospace;
9+
}
10+
11+
.created-at,
12+
.expires-on {
13+
color: var(--go-ui-color-text-light);
14+
font-size: var(--go-ui-font-size-sm);
15+
}
16+
}

app/src/views/AccountDetails/i18n.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@
1212
"departmentLabel": "Department",
1313
"positionLabel": "Position",
1414
"changePasswordButtonLabel": "Change Password",
15-
"editProfileButtonLabel": "Edit Profile"
15+
"editProfileButtonLabel": "Edit Profile",
16+
"externalConnectionsTitle": "External Connections",
17+
"externalConnectionsDescription": "You can connect and recieve the data to use it in the external platform or dashboard using the GO APIs. You'll need an access token to make the connection.",
18+
"externalConnectionMontandonTitle": "Montandon - Global Crisis Data Bank",
19+
"externalConnectionMontandonDescription": "The Montandon is the world’s largest disaster database: a public good with applications beyond IFRC. It contains information about natural hazards (both forecasted and observed), their impacts (also modeled and observed) as well as what actions were taken when, where, by whom and to what effect.",
20+
"viewMoreDetailsLabel": "View more details",
21+
"generateNewTokenLabel": "Generate new token"
1622
}
1723
}

0 commit comments

Comments
 (0)