• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In
You are now the owner of this repo.

CSCfi / metadata-submitter-frontend / 20331691855

18 Dec 2025 09:07AM UTC coverage: 55.065% (-3.3%) from 58.346%
20331691855

push

github

Hang Le
Update dependency @vitest/coverage-v8 to v4 (merge commit)

Merge branch 'renovate/vitest-coverage-v8-4.x' into 'main'
* Update dependency @vitest/coverage-v8 to v4

See merge request https://gitlab.ci.csc.fi/sds-dev/sd-submit/metadata-submitter-frontend/-/merge_requests/1182

Approved-by: Liisa Lado-Villar <145-lilado@users.noreply.gitlab.ci.csc.fi>
Co-authored-by: renovate-bot <group_183_bot_aa67d732ac40e4c253df6728543b928a@noreply.gitlab.ci.csc.fi>
Merged by Hang Le <lhang@csc.fi>

476 of 1055 branches covered (45.12%)

Branch coverage included in aggregate %.

1307 of 2183 relevant lines covered (59.87%)

8.04 hits per line

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

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

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

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

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

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

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

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

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

84
const APIKeysModal = ({ open, onClose }: APIKeyModalProps) => {
5✔
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))
28✔
101
      } else {
102
        dispatch(
×
103
          updateStatus({
104
            status: ResponseStatus.error,
105
            response: response,
106
          })
107
        )
108
      }
109
    }
110
    getApiKeys()
4✔
111
    setIsLoading(false)
4✔
112
  }, [open])
113

114
  const handleClose = () => {
14✔
115
    setNewKey({ keyName: "", keyValue: "" })
×
116
    setApikeys([])
×
117
    setKeyInput("")
×
118
    setIsEmptyName(false)
×
119
    setIsUnique(true)
×
120
    onClose()
×
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
  }
128

129
  const handleCreateKey = async () => {
14✔
130
    if (keyInput === "") setIsEmptyName(true)
2!
131
    else if (!apiKeys.includes(keyInput)) {
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 {
138
        dispatch(
×
139
          updateStatus({
140
            status: ResponseStatus.error,
141
            response: response,
142
          })
143
        )
144
      }
145
    } else setIsUnique(false)
×
146
  }
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))
7✔
152
      if (key === newKey.keyName) setNewKey({ keyName: "", keyValue: "" })
1!
153
    } else {
154
      dispatch(
×
155
        updateStatus({
156
          status: ResponseStatus.error,
157
          response: response,
158
        })
159
      )
160
    }
161
  }
162

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

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

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

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

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

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

296
export default APIKeysModal
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

© 2025 Coveralls, Inc