• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

CSCfi / metadata-submitter-frontend / 17043462297

18 Aug 2025 02:25PM UTC coverage: 56.325% (+0.8%) from 55.494%
17043462297

push

github

Hang Le
Add API key modal (merge commit)

Merge branch 'feature/api-token-modal' into 'main'
* adjust alert icon hue

* change API key alert text darker

* fix to request APIkeys only when the modal is open

* fix api key modal behaviour

* change copied API key to stay until closing modal

* add translations

* add Snackbar message for copied key value and fix clearing input field

* remove button from Nav API key menuItem

* add clearing of API key input field

* add unit test

* clean up Nav component

* disable api key modal from non login pages

* Move API keys modal to App (working example)

* add API connection failure message

* fix API key modal flaws

* add API key modal

Closes #1038 and #1065
See merge request https://gitlab.ci.csc.fi/sds-dev/sd-submit/metadata-submitter-frontend/-/merge_requests/1134

Reviewed-by: Liisa Lado-Villar <145-lilado@users.noreply.gitlab.ci.csc.fi>
Approved-by: Hang Le <lhang@csc.fi>
Co-authored-by: Liisa Lado-Villar <145-lilado@users.noreply.gitlab.ci.csc.fi>
Co-authored-by: Monika Radaviciute <mradavic@csc.fi>
Merged by Hang Le <lhang@csc.fi>

623 of 866 branches covered (71.94%)

Branch coverage included in aggregate %.

263 of 305 new or added lines in 5 files covered. (86.23%)

1 existing line in 1 file now uncovered.

5744 of 10438 relevant lines covered (55.03%)

4.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.0
/src/components/APIKeysModal.tsx
1
import React, { useEffect, useState } from "react"
1✔
2

3
import CloseIcon from "@mui/icons-material/Close"
1✔
4
import ContentCopyIcon from "@mui/icons-material/ContentCopy"
1✔
5
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"
1✔
6
import WarningIcon from "@mui/icons-material/Warning"
1✔
7
import {
8
  Alert,
9
  CircularProgress,
10
  Table,
11
  TableBody,
12
  TableCell,
13
  TableRow,
14
  Typography,
15
} from "@mui/material"
1✔
16
import Box from "@mui/material/Box"
1✔
17
import Button from "@mui/material/Button"
1✔
18
import Modal from "@mui/material/Modal"
1✔
19
import MuiTextField from "@mui/material/TextField"
1✔
20
import { Container, styled } from "@mui/system"
1✔
21
import { useTranslation } from "react-i18next"
1✔
22

23
import { ResponseStatus } from "constants/responseStatus"
1✔
24
import { updateStatus } from "features/statusMessageSlice"
1✔
25
import { useAppDispatch } from "hooks"
1✔
26
import apiKeysService from "services/apiKeysAPI"
1✔
27

28
type APIKeyModalProps = {
29
  open: boolean
30
  onClose: () => void
31
}
32

33
const StyledContainer = styled(Container)(({ theme }) => ({
1✔
34
  position: "absolute",
10✔
35
  backgroundColor: theme.palette.common.white,
10✔
36
  top: "50%",
10✔
37
  left: "50%",
10✔
38
  width: "70%",
10✔
39
  padding: "3rem !important",
10✔
40
  transform: "translate(-50%, -50%)",
10✔
41
  borderRadius: "0.375rem",
10✔
42
  boxShadow: "0 4px 4px 0 rgba(0,0,0,0.25)",
10✔
43
}))
1✔
44

45
const TextField = styled(MuiTextField)(({ theme }) => ({
1✔
46
  backgroundColor: theme.palette.common.white,
10✔
47
  "& fieldset": {
10✔
48
    border: "1px solid",
10✔
49
    borderColor: theme.palette.secondary.main,
10✔
50
  },
10✔
51
  input: {
10✔
52
    color: theme.palette.common.black,
10✔
53
    "&::placeholder": {
10✔
54
      opacity: 0.8,
10✔
55
    },
10✔
56
  },
10✔
57
  width: "100%",
10✔
58
}))
1✔
59

60
const KeyTable = styled(Table)(({ theme }) => ({
1✔
61
  backgroundColor: theme.palette.background.paper,
12✔
62
  borderLeft: `0.10rem solid ${theme.palette.secondary.light}`,
12✔
63
  borderTop: `0.35rem solid ${theme.palette.secondary.light}`,
12✔
64
  borderRight: `0.10rem solid ${theme.palette.secondary.light}`,
12✔
65
  borderBottom: `0.10rem solid ${theme.palette.secondary.light}`,
12✔
66
  color: theme.palette.secondary.main,
12✔
67
  width: "100%",
12✔
68
  tableLayout: "fixed",
12✔
69
}))
1✔
70

71
const APIKeyAlert = styled(Alert)(({ theme }) => ({
1✔
72
  backgroundColor: theme.palette.background.paper,
2✔
73
  borderLeft: `1.25rem solid ${theme.palette.warning.main}`,
2✔
74
  borderTop: `0.15rem solid ${theme.palette.warning.main}`,
2✔
75
  borderRight: `0.15rem solid ${theme.palette.warning.main}`,
2✔
76
  borderBottom: `0.15rem solid ${theme.palette.warning.main}`,
2✔
77
  color: theme.palette.text.primary,
2✔
78
  alignItems: "center",
2✔
79
  "& .MuiAlert-icon": {
2✔
80
    opacity: 1,
2✔
81
  },
2✔
82
}))
1✔
83

84
const APIKeysModal = ({ open, onClose }: APIKeyModalProps) => {
1✔
85
  // Store only the names of the keys
86
  const [apiKeys, setApikeys] = useState<string[]>([])
14✔
87
  const [isLoading, setIsLoading] = useState<boolean>(true)
14✔
88
  const [newKey, setNewKey] = useState({ keyName: "", keyValue: "" })
14✔
89
  const [keyInput, setKeyInput] = useState("")
14✔
90
  const [isEmptyName, setIsEmptyName] = useState(false)
14✔
91
  const [isUnique, setIsUnique] = useState(true)
14✔
92

93
  const { t } = useTranslation()
14✔
94
  const dispatch = useAppDispatch()
14✔
95

96
  useEffect(() => {
14✔
97
    const getApiKeys = async () => {
4✔
98
      const response = await apiKeysService.getAPIKeys()
4✔
99
      if (response.ok) {
4✔
100
        setApikeys(response.data.map(item => item.key_id))
4✔
101
      } else {
4!
NEW
102
        dispatch(
×
NEW
103
          updateStatus({
×
NEW
104
            status: ResponseStatus.error,
×
NEW
105
            response: response,
×
NEW
106
          })
×
NEW
107
        )
×
NEW
108
      }
×
109
    }
4✔
110
    getApiKeys()
4✔
111
    setIsLoading(false)
4✔
112
  }, [open])
14✔
113

114
  const handleClose = () => {
14✔
NEW
115
    setNewKey({ keyName: "", keyValue: "" })
×
NEW
116
    setApikeys([])
×
NEW
117
    setKeyInput("")
×
NEW
118
    setIsEmptyName(false)
×
NEW
119
    setIsUnique(true)
×
NEW
120
    onClose()
×
NEW
121
  }
×
122

123
  const handleGetName = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
14✔
124
    setKeyInput(e.target.value)
1✔
125
    setIsEmptyName(false)
1✔
126
    setIsUnique(true)
1✔
127
  }
1✔
128

129
  const handleCreateKey = async () => {
14✔
130
    if (keyInput === "") setIsEmptyName(true)
2✔
131
    else if (!apiKeys.includes(keyInput)) {
1✔
132
      const response = await apiKeysService.addAPIKey(keyInput)
1✔
133
      if (response.ok) {
1✔
134
        setNewKey({ keyName: keyInput, keyValue: response.data })
1✔
135
        setApikeys(apiKeys.concat([keyInput]))
1✔
136
        setKeyInput("")
1✔
137
      } else {
1!
NEW
138
        dispatch(
×
NEW
139
          updateStatus({
×
NEW
140
            status: ResponseStatus.error,
×
NEW
141
            response: response,
×
NEW
142
          })
×
NEW
143
        )
×
NEW
144
      }
×
145
    } else setIsUnique(false)
1!
146
  }
2✔
147

148
  const handleDelete = async (key: string) => {
14✔
149
    const response = await apiKeysService.deleteAPIKey(key)
1✔
150
    if (response.ok) {
1✔
151
      setApikeys(apiKeys.filter(item => item !== key))
1✔
152
      if (key === newKey.keyName) setNewKey({ keyName: "", keyValue: "" })
1!
153
    } else {
1!
NEW
154
      dispatch(
×
NEW
155
        updateStatus({
×
NEW
156
          status: ResponseStatus.error,
×
NEW
157
          response: response,
×
NEW
158
        })
×
NEW
159
      )
×
NEW
160
    }
×
161
  }
1✔
162

163
  const handleCopy = () => {
14✔
NEW
164
    navigator.clipboard.writeText(newKey.keyValue)
×
NEW
165
    dispatch(
×
NEW
166
      updateStatus({
×
NEW
167
        status: ResponseStatus.success,
×
NEW
168
        helperText: "snackbarMessages.success.apikey.copied",
×
NEW
169
      })
×
NEW
170
    )
×
NEW
171
  }
×
172

173
  return (
14✔
174
    <Modal open={open} onClose={handleClose} aria-labelledby="API-key-modal">
14✔
175
      <StyledContainer>
14✔
176
        <Box sx={{ display: "flex", justifyContent: "space-between" }}>
14✔
177
          <Typography
14✔
178
            variant="h5"
14✔
179
            role="heading"
14✔
180
            color="secondary"
14✔
181
            sx={{ mb: "2rem", fontWeight: 700 }}
14✔
182
          >
183
            {t("apiKeys.createAPIKeysTitle", { serviceTitle: t("serviceTitle") })}
14✔
184
          </Typography>
14✔
185

186
          <Button onClick={handleClose} startIcon={<CloseIcon />} sx={{ pt: 0, mt: 0 }}>
14✔
187
            <Typography variant={"subtitle1"}>{t("close")}</Typography>
14✔
188
          </Button>
14✔
189
        </Box>
14✔
190
        <Typography variant="subtitle1">{t("apiKeys.nameAPIKey")}</Typography>
14✔
191

192
        <TextField
14✔
193
          error={!isUnique || isEmptyName}
14✔
194
          size="small"
14✔
195
          placeholder={t("apiKeys.keyName")}
14✔
196
          required={true}
14✔
197
          margin="dense"
14✔
198
          onChange={e => handleGetName(e)}
14✔
199
          value={keyInput}
14✔
200
          helperText={!isUnique && t("apiKeys.keyMustBeUnique")}
14!
201
        />
202

203
        <Button
14✔
204
          sx={{ my: "1.5rem" }}
14✔
205
          size="medium"
14✔
206
          variant="contained"
14✔
207
          type="submit"
14✔
208
          aria-label="Create API key"
14✔
209
          data-testid="api-key-create-button"
14✔
210
          onClick={() => handleCreateKey()}
14✔
211
        >
212
          {t("apiKeys.createKey")}
14✔
213
        </Button>
14✔
214

215
        {newKey.keyValue !== "" && (
14✔
216
          <>
2✔
217
            <Typography variant="subtitle2" sx={{ px: "2rem", my: "1rem" }}>
2✔
218
              {t("apiKeys.latestKey")}
2✔
219
            </Typography>
2✔
220
            <KeyTable size="small">
2✔
221
              <TableBody>
2✔
222
                <TableRow>
2✔
223
                  <TableCell width="25%">{newKey.keyName}</TableCell>
2✔
224
                  <TableCell
2✔
225
                    width="60%"
2✔
226
                    data-testid="new-key-value"
2✔
227
                    sx={{ wordWrap: "break-word" }}
2✔
228
                  >
229
                    {newKey.keyValue}
2✔
230
                  </TableCell>
2✔
231
                  <TableCell align="right" sx={{ p: 0 }}>
2✔
232
                    <Button
2✔
233
                      sx={{ m: 0 }}
2✔
234
                      size="small"
2✔
235
                      type="submit"
2✔
236
                      aria-label="Create API key"
2✔
237
                      data-testid="api-key-copy"
2✔
238
                      onClick={() => handleCopy()}
2✔
239
                      startIcon={<ContentCopyIcon fontSize="large" />}
2✔
240
                    >
241
                      <Typography variant={"subtitle1"}>{t("copy")}</Typography>
2✔
242
                    </Button>
2✔
243
                  </TableCell>
2✔
244
                </TableRow>
2✔
245
              </TableBody>
2✔
246
            </KeyTable>
2✔
247
            <APIKeyAlert
2✔
248
              icon={<WarningIcon fontSize="large" />}
2✔
249
              variant="outlined"
2✔
250
              severity="warning"
2✔
251
              sx={{ my: "1.5rem", py: "0.5rem", pl: "2rem", pr: "4rem" }}
2✔
252
            >
253
              {t("apiKeys.keyStoreWarning")}
2✔
254
            </APIKeyAlert>
2✔
255
          </>
2✔
256
        )}
257
        <Typography variant="subtitle2" sx={{ px: "2rem", pt: "1.5rem", my: "1rem" }}>
14✔
258
          {t("apiKeys.activeKeys")}
14✔
259
        </Typography>
14✔
260
        {isLoading ? (
14✔
261
          <CircularProgress color="primary" />
4✔
262
        ) : apiKeys?.length > 0 ? (
10✔
263
          <KeyTable size="small" aria-label="key table" data-testid="api-key-table">
5✔
264
            <TableBody>
5✔
265
              {apiKeys.map(apikey => (
5✔
266
                <TableRow key={apikey}>
22✔
267
                  <TableCell>{apikey}</TableCell>
22✔
268
                  <TableCell align="right">
22✔
269
                    <Button
22✔
270
                      data-testid="api-key-delete"
22✔
271
                      sx={{ pt: 0, mt: 0 }}
22✔
272
                      onClick={() => handleDelete(apikey)}
22✔
273
                    >
274
                      <DeleteOutlineIcon fontSize="large" sx={{ mr: "1rem" }} />
22✔
275
                      <Typography variant={"subtitle1"}>{t("delete")}</Typography>
22✔
276
                    </Button>
22✔
277
                  </TableCell>
22✔
278
                </TableRow>
22✔
279
              ))}
5✔
280
            </TableBody>
5✔
281
          </KeyTable>
5✔
282
        ) : (
283
          <KeyTable aria-label="no keys table" data-testid="no-api-keys">
5✔
284
            <TableBody>
5✔
285
              <TableRow sx={{ bgcolor: theme => theme.palette.primary.lightest }}>
5✔
286
                <TableCell>{t("apiKeys.noAPIKeys")}</TableCell>
5✔
287
              </TableRow>
5✔
288
            </TableBody>
5✔
289
          </KeyTable>
5✔
290
        )}
291
      </StyledContainer>
14✔
292
    </Modal>
14✔
293
  )
294
}
14✔
295

296
export default APIKeysModal
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc