@red-hat-developer-hub/backstage-plugin-lightspeed 1.4.0 → 2.0.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 (92) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +59 -0
  3. package/dist/alpha/LightspeedFAB.esm.js +69 -0
  4. package/dist/alpha/LightspeedFAB.esm.js.map +1 -0
  5. package/dist/alpha/LightspeedProvider.esm.js +6 -0
  6. package/dist/alpha/LightspeedProvider.esm.js.map +1 -0
  7. package/dist/alpha.d.ts +170 -1
  8. package/dist/alpha.esm.js +97 -1
  9. package/dist/alpha.esm.js.map +1 -1
  10. package/dist/api/LightspeedApiClient.esm.js +19 -0
  11. package/dist/api/LightspeedApiClient.esm.js.map +1 -1
  12. package/dist/api/NotebooksApiClient.esm.js +72 -0
  13. package/dist/api/NotebooksApiClient.esm.js.map +1 -0
  14. package/dist/api/api.esm.js.map +1 -1
  15. package/dist/api/notebooksApi.esm.js +10 -0
  16. package/dist/api/notebooksApi.esm.js.map +1 -0
  17. package/dist/components/DeleteModal.esm.js +2 -0
  18. package/dist/components/DeleteModal.esm.js.map +1 -1
  19. package/dist/components/LightSpeedChat.esm.js +714 -66
  20. package/dist/components/LightSpeedChat.esm.js.map +1 -1
  21. package/dist/components/LightspeedChatBox.esm.js +18 -4
  22. package/dist/components/LightspeedChatBox.esm.js.map +1 -1
  23. package/dist/components/LightspeedChatBoxHeader.esm.js +59 -50
  24. package/dist/components/LightspeedChatBoxHeader.esm.js.map +1 -1
  25. package/dist/components/LightspeedChatContainer.esm.js +26 -2
  26. package/dist/components/LightspeedChatContainer.esm.js.map +1 -1
  27. package/dist/components/LightspeedDrawerProvider.esm.js +12 -157
  28. package/dist/components/LightspeedDrawerProvider.esm.js.map +1 -1
  29. package/dist/components/LightspeedFAB.esm.js +5 -4
  30. package/dist/components/LightspeedFAB.esm.js.map +1 -1
  31. package/dist/components/McpServersSettings.esm.js +932 -0
  32. package/dist/components/McpServersSettings.esm.js.map +1 -0
  33. package/dist/components/McpSettingsIcon.esm.js +26 -0
  34. package/dist/components/McpSettingsIcon.esm.js.map +1 -0
  35. package/dist/components/PermissionRequiredState.esm.js +18 -22
  36. package/dist/components/PermissionRequiredState.esm.js.map +1 -1
  37. package/dist/components/RenameConversationModal.esm.js +2 -2
  38. package/dist/components/RenameConversationModal.esm.js.map +1 -1
  39. package/dist/components/ToolCallContent.esm.js +14 -4
  40. package/dist/components/ToolCallContent.esm.js.map +1 -1
  41. package/dist/components/notebooks/DeleteNotebookModal.esm.js +143 -0
  42. package/dist/components/notebooks/DeleteNotebookModal.esm.js.map +1 -0
  43. package/dist/components/notebooks/NotebookCard.esm.js +96 -0
  44. package/dist/components/notebooks/NotebookCard.esm.js.map +1 -0
  45. package/dist/components/notebooks/NotebooksTab.esm.js +61 -0
  46. package/dist/components/notebooks/NotebooksTab.esm.js.map +1 -0
  47. package/dist/components/notebooks/RenameNotebookModal.esm.js +187 -0
  48. package/dist/components/notebooks/RenameNotebookModal.esm.js.map +1 -0
  49. package/dist/const.esm.js +3 -1
  50. package/dist/const.esm.js.map +1 -1
  51. package/dist/hooks/notebooks/useDeleteNotebook.esm.js +19 -0
  52. package/dist/hooks/notebooks/useDeleteNotebook.esm.js.map +1 -0
  53. package/dist/hooks/notebooks/useLightspeedNotebooksPermission.esm.js +15 -0
  54. package/dist/hooks/notebooks/useLightspeedNotebooksPermission.esm.js.map +1 -0
  55. package/dist/hooks/notebooks/useNotebookSessions.esm.js +18 -0
  56. package/dist/hooks/notebooks/useNotebookSessions.esm.js.map +1 -0
  57. package/dist/hooks/notebooks/useRenameNotebook.esm.js +19 -0
  58. package/dist/hooks/notebooks/useRenameNotebook.esm.js.map +1 -0
  59. package/dist/hooks/useConversationMessages.esm.js +136 -36
  60. package/dist/hooks/useConversationMessages.esm.js.map +1 -1
  61. package/dist/hooks/useConversations.esm.js +6 -0
  62. package/dist/hooks/useConversations.esm.js.map +1 -1
  63. package/dist/hooks/useCreateCoversationMessage.esm.js.map +1 -1
  64. package/dist/hooks/useLightspeedProviderState.esm.js +205 -0
  65. package/dist/hooks/useLightspeedProviderState.esm.js.map +1 -0
  66. package/dist/hooks/useStopConversation.esm.js +18 -0
  67. package/dist/hooks/useStopConversation.esm.js.map +1 -0
  68. package/dist/hooks/useWelcomePrompts.esm.js +2 -2
  69. package/dist/hooks/useWelcomePrompts.esm.js.map +1 -1
  70. package/dist/index.d.ts +31 -31
  71. package/dist/plugin.esm.js +10 -0
  72. package/dist/plugin.esm.js.map +1 -1
  73. package/dist/translations/de.esm.js +78 -0
  74. package/dist/translations/de.esm.js.map +1 -1
  75. package/dist/translations/es.esm.js +78 -0
  76. package/dist/translations/es.esm.js.map +1 -1
  77. package/dist/translations/fr.esm.js +64 -1
  78. package/dist/translations/fr.esm.js.map +1 -1
  79. package/dist/translations/it.esm.js +66 -1
  80. package/dist/translations/it.esm.js.map +1 -1
  81. package/dist/translations/ja.esm.js +66 -1
  82. package/dist/translations/ja.esm.js.map +1 -1
  83. package/dist/translations/ref.esm.js +65 -1
  84. package/dist/translations/ref.esm.js.map +1 -1
  85. package/dist/types.esm.js.map +1 -1
  86. package/dist/utils/formatToolResponseForMarkdown.esm.js +46 -0
  87. package/dist/utils/formatToolResponseForMarkdown.esm.js.map +1 -0
  88. package/dist/utils/lightspeed-chatbox-utils.esm.js +7 -3
  89. package/dist/utils/lightspeed-chatbox-utils.esm.js.map +1 -1
  90. package/dist/utils/notebooks-utils.esm.js +25 -0
  91. package/dist/utils/notebooks-utils.esm.js.map +1 -0
  92. package/package.json +20 -12
@@ -0,0 +1,932 @@
1
+ import { jsx, jsxs } from 'react/jsx-runtime';
2
+ import { useState, useCallback, useEffect, useMemo } from 'react';
3
+ import { useApi, configApiRef, fetchApiRef } from '@backstage/core-plugin-api';
4
+ import { usePermission } from '@backstage/plugin-permission-react';
5
+ import { makeStyles } from '@material-ui/core';
6
+ import CancelOutlinedIcon from '@mui/icons-material/CancelOutlined';
7
+ import CloseOutlinedIcon from '@mui/icons-material/CloseOutlined';
8
+ import ModeEditOutlineOutlinedIcon from '@mui/icons-material/ModeEditOutlineOutlined';
9
+ import IconButton from '@mui/material/IconButton';
10
+ import InputAdornment from '@mui/material/InputAdornment';
11
+ import TextField from '@mui/material/TextField';
12
+ import Typography from '@mui/material/Typography';
13
+ import { Title, Button, Alert, Switch, Tooltip, Modal } from '@patternfly/react-core';
14
+ import { TimesIcon, SortAmountDownIcon, SortAmountUpIcon, LockIcon, KeyIcon, OffIcon, ExclamationCircleIcon, CheckCircleIcon } from '@patternfly/react-icons';
15
+ import { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table';
16
+ import { lightspeedMcpManagePermission } from '@red-hat-developer-hub/backstage-plugin-lightspeed-common';
17
+ import { useTranslation } from '../hooks/useTranslation.esm.js';
18
+
19
+ const SAVED_TOKEN_MASK = "********************";
20
+ const useStyles = makeStyles((theme) => ({
21
+ "@global": {
22
+ ".pf-v6-c-backdrop": {
23
+ zIndex: "1400 !important"
24
+ },
25
+ ".pf-v5-c-backdrop": {
26
+ zIndex: "1400 !important"
27
+ }
28
+ },
29
+ root: {
30
+ padding: 0,
31
+ height: "100%",
32
+ minHeight: "100%",
33
+ width: "100%",
34
+ overflow: "auto"
35
+ },
36
+ headerRow: {
37
+ display: "flex",
38
+ alignItems: "flex-start",
39
+ justifyContent: "space-between",
40
+ marginBottom: theme.spacing(2),
41
+ marginTop: theme.spacing(2),
42
+ marginLeft: theme.spacing(3),
43
+ marginRight: theme.spacing(2)
44
+ },
45
+ selectedCount: {
46
+ color: theme.palette.text.secondary,
47
+ marginTop: theme.spacing(0.5),
48
+ fontSize: "0.75rem"
49
+ },
50
+ title: {
51
+ fontSize: "1.125rem"
52
+ },
53
+ closeButton: {
54
+ marginTop: -theme.spacing(1),
55
+ marginRight: -theme.spacing(1),
56
+ color: theme.palette.text.secondary
57
+ },
58
+ nameHeaderButton: {
59
+ paddingLeft: 0,
60
+ paddingTop: 0,
61
+ paddingBottom: 0,
62
+ marginLeft: "-0.85rem",
63
+ fontWeight: 600,
64
+ fontSize: "0.75rem",
65
+ lineHeight: "1.25rem",
66
+ minHeight: "auto",
67
+ color: theme.palette.text.primary,
68
+ textDecoration: "none !important",
69
+ display: "inline-flex",
70
+ alignItems: "center"
71
+ },
72
+ nameHeaderText: {
73
+ paddingLeft: "7px",
74
+ fontSize: "0.75rem",
75
+ lineHeight: "1.25rem",
76
+ fontWeight: 600
77
+ },
78
+ nameCell: {
79
+ paddingLeft: "8px !important"
80
+ },
81
+ statusHeader: {
82
+ paddingLeft: "0 !important"
83
+ },
84
+ statusColumnCell: {
85
+ paddingLeft: "0 !important"
86
+ },
87
+ rowName: {
88
+ fontSize: "1rem",
89
+ fontWeight: 500,
90
+ whiteSpace: "nowrap"
91
+ },
92
+ nameValue: {
93
+ fontSize: "0.875rem",
94
+ fontWeight: 500
95
+ },
96
+ statusCell: {
97
+ display: "flex",
98
+ alignItems: "center",
99
+ gap: theme.spacing(1),
100
+ whiteSpace: "nowrap"
101
+ },
102
+ statusValue: {
103
+ fontSize: "0.875rem"
104
+ },
105
+ statusOk: {
106
+ color: "#147878"
107
+ },
108
+ statusToken: {
109
+ color: "#147878"
110
+ },
111
+ statusWarn: {
112
+ color: "#B1380B"
113
+ },
114
+ statusDisabled: {
115
+ color: theme.palette.text.secondary
116
+ },
117
+ actionButton: {
118
+ color: theme.palette.text.secondary
119
+ },
120
+ modalDescription: {
121
+ color: theme.palette.text.secondary,
122
+ fontSize: "0.875rem",
123
+ marginTop: theme.spacing(2),
124
+ marginBottom: theme.spacing(2)
125
+ },
126
+ modalContent: {
127
+ position: "relative",
128
+ padding: theme.spacing(3, 0, 3, 3),
129
+ marginRight: theme.spacing(3)
130
+ },
131
+ modalCustomCloseButton: {
132
+ position: "absolute",
133
+ top: theme.spacing(2),
134
+ right: theme.spacing(-0.5),
135
+ color: "#1F1F1F"
136
+ },
137
+ modalHeading: {
138
+ display: "flex",
139
+ alignItems: "center",
140
+ gap: theme.spacing(0.75),
141
+ marginBottom: theme.spacing(1),
142
+ fontSize: "1.25rem",
143
+ lineHeight: 1.4,
144
+ fontWeight: 500,
145
+ "& .v5-MuiTypography-root": {
146
+ fontSize: "1.25rem",
147
+ lineHeight: 1.4,
148
+ fontWeight: 500
149
+ }
150
+ },
151
+ tokenRow: {
152
+ position: "relative"
153
+ },
154
+ tokenClearButton: {
155
+ position: "absolute",
156
+ right: theme.spacing(1),
157
+ top: "50%",
158
+ transform: "translateY(-50%)",
159
+ zIndex: 1,
160
+ color: theme.palette.text.secondary
161
+ },
162
+ tokenHelper: {
163
+ color: theme.palette.text.secondary,
164
+ fontSize: "0.75rem",
165
+ marginTop: theme.spacing(0.5)
166
+ },
167
+ tokenInput: {
168
+ marginTop: "1rem !important",
169
+ "& .MuiOutlinedInput-root": {
170
+ height: "3.5rem"
171
+ },
172
+ "& .MuiOutlinedInput-input": {
173
+ padding: "0 0.875rem"
174
+ },
175
+ "& .MuiInputLabel-root": {
176
+ fontSize: "0.875rem"
177
+ }
178
+ },
179
+ tokenInputSuccess: {
180
+ "& .MuiOutlinedInput-root .MuiOutlinedInput-notchedOutline": {
181
+ borderColor: "#3E8635",
182
+ borderWidth: 1
183
+ },
184
+ "& .MuiOutlinedInput-root:hover .MuiOutlinedInput-notchedOutline": {
185
+ borderColor: "#3E8635"
186
+ },
187
+ "& .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline": {
188
+ borderColor: "#3E8635"
189
+ },
190
+ "& .MuiInputLabel-root.Mui-focused": {
191
+ color: "#3E8635"
192
+ }
193
+ },
194
+ tokenInputError: {
195
+ "& .MuiOutlinedInput-root .MuiOutlinedInput-notchedOutline": {
196
+ borderColor: "#C9190B",
197
+ borderWidth: 1
198
+ },
199
+ "& .MuiOutlinedInput-root:hover .MuiOutlinedInput-notchedOutline": {
200
+ borderColor: "#C9190B"
201
+ },
202
+ "& .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline": {
203
+ borderColor: "#C9190B"
204
+ },
205
+ "& .MuiInputLabel-root.Mui-focused": {
206
+ color: "#C9190B"
207
+ }
208
+ },
209
+ modalActions: {
210
+ marginTop: theme.spacing(3),
211
+ display: "flex",
212
+ gap: theme.spacing(1)
213
+ },
214
+ modalActionButton: {
215
+ fontSize: "1rem"
216
+ },
217
+ modalCancelButton: {
218
+ fontSize: "1rem"
219
+ },
220
+ forgetTokenButton: {
221
+ fontSize: "1rem",
222
+ border: "1px solid #B1380B",
223
+ borderRadius: "1.25rem",
224
+ padding: "0.375rem 1rem",
225
+ color: "#B1380B",
226
+ backgroundColor: "transparent",
227
+ marginLeft: theme.spacing(1),
228
+ marginRight: theme.spacing(1),
229
+ boxShadow: "none",
230
+ "&:hover": {
231
+ backgroundColor: "rgba(201, 25, 11, 0.08)"
232
+ }
233
+ },
234
+ configureModal: {
235
+ "& .pf-v6-c-modal-box": {
236
+ width: "608px",
237
+ maxWidth: "608px",
238
+ height: "326px",
239
+ minHeight: "326px"
240
+ },
241
+ "& .pf-v6-c-modal-box__title, & .pf-v6-c-modal-box__title-text, & .pf-v5-c-modal-box__title, & .pf-v5-c-modal-box__title-text": {
242
+ fontSize: "1.25rem !important",
243
+ lineHeight: "1.4 !important"
244
+ },
245
+ "& .pf-v6-c-modal-box__close": {
246
+ display: "none"
247
+ },
248
+ "& .pf-v5-c-modal-box__close": {
249
+ display: "none"
250
+ },
251
+ "& .pf-v6-c-button__icon": {
252
+ paddingTop: "5px !important",
253
+ fontSize: "1.25rem !important"
254
+ }
255
+ },
256
+ toggleCell: {
257
+ paddingRight: "0 !important"
258
+ },
259
+ table: {
260
+ width: "100%",
261
+ "& th": {
262
+ borderBottom: 0,
263
+ fontSize: "0.75rem",
264
+ fontWeight: 600,
265
+ color: theme.palette.text.primary,
266
+ whiteSpace: "nowrap",
267
+ textAlign: "left"
268
+ },
269
+ "& td": {
270
+ borderBottom: 0,
271
+ paddingTop: theme.spacing(1.5),
272
+ paddingBottom: theme.spacing(1.5),
273
+ verticalAlign: "middle"
274
+ }
275
+ },
276
+ alert: {
277
+ marginLeft: theme.spacing(3),
278
+ marginRight: theme.spacing(3),
279
+ marginBottom: theme.spacing(2)
280
+ }
281
+ }));
282
+ const getStatusIcon = (status, className) => {
283
+ if (status === "tokenRequired") return /* @__PURE__ */ jsx(KeyIcon, { className });
284
+ if (status === "disabled") return /* @__PURE__ */ jsx(OffIcon, { className });
285
+ if (status === "failed")
286
+ return /* @__PURE__ */ jsx(ExclamationCircleIcon, { className });
287
+ return /* @__PURE__ */ jsx(CheckCircleIcon, { className });
288
+ };
289
+ const getDisplayStatus = (server) => {
290
+ if (!server.hasToken) return "tokenRequired";
291
+ if (!server.enabled) return "disabled";
292
+ if (server.status === "error") return "failed";
293
+ if (server.status === "connected") return "ok";
294
+ return "unknown";
295
+ };
296
+ const toUiServer = (server, validationError) => ({
297
+ id: server.name,
298
+ name: server.name,
299
+ url: server.url,
300
+ enabled: server.enabled,
301
+ status: server.status,
302
+ toolCount: server.toolCount,
303
+ hasToken: server.hasToken,
304
+ hasUserToken: server.hasUserToken,
305
+ validationError: server.status === "error" ? validationError : undefined
306
+ });
307
+ const McpServersSettings = ({
308
+ onClose,
309
+ backgroundColor
310
+ }) => {
311
+ const classes = useStyles();
312
+ const { t } = useTranslation();
313
+ const configApi = useApi(configApiRef);
314
+ const fetchApi = useApi(fetchApiRef);
315
+ const mcpManagePermission = usePermission({
316
+ permission: lightspeedMcpManagePermission
317
+ });
318
+ const canManageMcp = mcpManagePermission.allowed;
319
+ const [servers, setServers] = useState([]);
320
+ const [sortAsc, setSortAsc] = useState(true);
321
+ const [isLoading, setIsLoading] = useState(true);
322
+ const [isSaving, setIsSaving] = useState({});
323
+ const [error, setError] = useState(null);
324
+ const [editingServerId, setEditingServerId] = useState(null);
325
+ const [tokenInputValue, setTokenInputValue] = useState("");
326
+ const [hasSavedTokenInModal, setHasSavedTokenInModal] = useState(false);
327
+ const [canRemovePersonalToken, setCanRemovePersonalToken] = useState(false);
328
+ const [tokenValidationState, setTokenValidationState] = useState("idle");
329
+ const [tokenValidationMessage, setTokenValidationMessage] = useState("");
330
+ const getBaseUrl = useCallback(() => {
331
+ return `${configApi.getString("backend.baseUrl")}/api/lightspeed`;
332
+ }, [configApi]);
333
+ const fetchJson = useCallback(
334
+ async (url, init) => {
335
+ const response = await fetchApi.fetch(url, {
336
+ headers: {
337
+ "Content-Type": "application/json"
338
+ },
339
+ ...init
340
+ });
341
+ if (!response.ok) {
342
+ let message = `${response.status} ${response.statusText}`;
343
+ try {
344
+ const bodyText = await response.text();
345
+ if (bodyText) {
346
+ const parsed = JSON.parse(bodyText);
347
+ if (parsed?.error) {
348
+ message = parsed.error;
349
+ }
350
+ }
351
+ } catch {
352
+ }
353
+ throw new Error(message);
354
+ }
355
+ const text = await response.text();
356
+ return text ? JSON.parse(text) : {};
357
+ },
358
+ [fetchApi]
359
+ );
360
+ const validateServer = useCallback(
361
+ async (serverName) => {
362
+ const baseUrl = getBaseUrl();
363
+ const data = await fetchJson(
364
+ `${baseUrl}/mcp-servers/${encodeURIComponent(serverName)}/validate`,
365
+ {
366
+ method: "POST"
367
+ }
368
+ );
369
+ setServers(
370
+ (prev) => prev.map(
371
+ (server) => server.name === serverName ? {
372
+ ...server,
373
+ status: data.status,
374
+ toolCount: data.toolCount,
375
+ validationError: data.status === "error" ? data.validation?.error ?? "Validation failed" : undefined
376
+ } : server
377
+ )
378
+ );
379
+ return data;
380
+ },
381
+ [fetchJson, getBaseUrl]
382
+ );
383
+ const validateCredentials = useCallback(
384
+ async (url, token) => {
385
+ const baseUrl = getBaseUrl();
386
+ return await fetchJson(
387
+ `${baseUrl}/mcp-servers/validate`,
388
+ {
389
+ method: "POST",
390
+ body: JSON.stringify({ url, token })
391
+ }
392
+ );
393
+ },
394
+ [fetchJson, getBaseUrl]
395
+ );
396
+ const loadServers = useCallback(async () => {
397
+ setIsLoading(true);
398
+ setError(null);
399
+ try {
400
+ const baseUrl = getBaseUrl();
401
+ const data = await fetchJson(
402
+ `${baseUrl}/mcp-servers`
403
+ );
404
+ const uiServers = (data.servers ?? []).map((server) => toUiServer(server));
405
+ setServers(uiServers);
406
+ if (canManageMcp) {
407
+ const serversToValidate = uiServers.filter((server) => server.hasToken);
408
+ void Promise.allSettled(
409
+ serversToValidate.map(async (server) => {
410
+ try {
411
+ await validateServer(server.name);
412
+ } catch (validationError) {
413
+ setError(
414
+ (prev) => prev ?? (validationError instanceof Error ? validationError.message : `Failed to validate ${server.name}`)
415
+ );
416
+ }
417
+ })
418
+ );
419
+ }
420
+ } catch (e) {
421
+ setError(
422
+ e instanceof Error ? e.message : "Failed to load MCP server settings"
423
+ );
424
+ } finally {
425
+ setIsLoading(false);
426
+ }
427
+ }, [canManageMcp, fetchJson, getBaseUrl, validateServer]);
428
+ useEffect(() => {
429
+ loadServers();
430
+ }, [loadServers]);
431
+ const patchServer = useCallback(
432
+ async (serverName, body) => {
433
+ if (!canManageMcp) {
434
+ return;
435
+ }
436
+ setError(null);
437
+ setIsSaving((prev) => ({ ...prev, [serverName]: true }));
438
+ try {
439
+ const baseUrl = getBaseUrl();
440
+ const data = await fetchJson(
441
+ `${baseUrl}/mcp-servers/${encodeURIComponent(serverName)}`,
442
+ {
443
+ method: "PATCH",
444
+ body: JSON.stringify(body)
445
+ }
446
+ );
447
+ if (data.server) {
448
+ setServers(
449
+ (prev) => prev.map(
450
+ (server) => server.name === serverName ? toUiServer(data.server, data.validation?.error) : server
451
+ )
452
+ );
453
+ } else {
454
+ await loadServers();
455
+ }
456
+ } catch (e) {
457
+ setError(
458
+ e instanceof Error ? e.message : `Failed to update ${serverName} settings`
459
+ );
460
+ throw e;
461
+ } finally {
462
+ setIsSaving((prev) => ({ ...prev, [serverName]: false }));
463
+ }
464
+ },
465
+ [canManageMcp, fetchJson, getBaseUrl, loadServers]
466
+ );
467
+ const editingServer = useMemo(
468
+ () => servers.find((server) => server.id === editingServerId),
469
+ [servers, editingServerId]
470
+ );
471
+ const selectedCount = useMemo(
472
+ () => servers.filter((server) => {
473
+ const displayStatus = getDisplayStatus(server);
474
+ const isUnavailable = displayStatus === "failed" || displayStatus === "tokenRequired";
475
+ return server.enabled && !isUnavailable;
476
+ }).length,
477
+ [servers]
478
+ );
479
+ const sortedServers = useMemo(() => {
480
+ const next = [...servers];
481
+ next.sort(
482
+ (a, b) => sortAsc ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name)
483
+ );
484
+ return next;
485
+ }, [servers, sortAsc]);
486
+ const getDisplayDetail = useCallback(
487
+ (server, displayStatus) => {
488
+ if (displayStatus === "disabled")
489
+ return t("mcp.settings.status.disabled");
490
+ if (displayStatus === "tokenRequired")
491
+ return t("mcp.settings.status.tokenRequired");
492
+ if (displayStatus === "failed") return t("mcp.settings.status.failed");
493
+ if (displayStatus === "ok") {
494
+ if (server.toolCount === 1) {
495
+ return t("mcp.settings.status.oneTool", {
496
+ count: String(server.toolCount)
497
+ });
498
+ }
499
+ return t("mcp.settings.status.manyTools", {
500
+ count: String(server.toolCount)
501
+ });
502
+ }
503
+ return t("mcp.settings.status.unknown");
504
+ },
505
+ [t]
506
+ );
507
+ const closeConfigureModal = useCallback(() => {
508
+ setEditingServerId(null);
509
+ setTokenInputValue("");
510
+ setHasSavedTokenInModal(false);
511
+ setCanRemovePersonalToken(false);
512
+ setTokenValidationState("idle");
513
+ setTokenValidationMessage("");
514
+ }, []);
515
+ const openConfigureModal = (server) => {
516
+ setEditingServerId(server.id);
517
+ const hasSavedToken = server.hasToken;
518
+ setHasSavedTokenInModal(hasSavedToken);
519
+ setCanRemovePersonalToken(server.hasUserToken);
520
+ setTokenInputValue(hasSavedToken ? SAVED_TOKEN_MASK : "");
521
+ if (server.status === "error" && server.validationError) {
522
+ setTokenValidationState("error");
523
+ setTokenValidationMessage(server.validationError);
524
+ } else {
525
+ setTokenValidationState("idle");
526
+ setTokenValidationMessage("");
527
+ }
528
+ };
529
+ const onTokenInputChange = (value) => {
530
+ if (hasSavedTokenInModal && value !== SAVED_TOKEN_MASK) {
531
+ setHasSavedTokenInModal(false);
532
+ }
533
+ setTokenInputValue(value);
534
+ setTokenValidationState("idle");
535
+ setTokenValidationMessage("");
536
+ };
537
+ const clearTokenInput = () => {
538
+ if (hasSavedTokenInModal) {
539
+ setHasSavedTokenInModal(false);
540
+ }
541
+ setTokenInputValue("");
542
+ setTokenValidationState("idle");
543
+ setTokenValidationMessage("");
544
+ };
545
+ let tokenInputStateClass = "";
546
+ let tokenHelperColor;
547
+ if (tokenValidationState === "success") {
548
+ tokenInputStateClass = classes.tokenInputSuccess;
549
+ tokenHelperColor = "#3E8635";
550
+ } else if (tokenValidationState === "error") {
551
+ tokenInputStateClass = classes.tokenInputError;
552
+ tokenHelperColor = "#C9190B";
553
+ }
554
+ let tokenInputAdornment = /* @__PURE__ */ jsx(
555
+ IconButton,
556
+ {
557
+ "aria-label": t("mcp.settings.token.clearAriaLabel"),
558
+ size: "small",
559
+ className: classes.tokenClearButton,
560
+ onClick: clearTokenInput,
561
+ children: /* @__PURE__ */ jsx(
562
+ CancelOutlinedIcon,
563
+ {
564
+ style: {
565
+ color: "#6A6E73",
566
+ fontSize: 24,
567
+ width: 24,
568
+ height: 24
569
+ }
570
+ }
571
+ )
572
+ }
573
+ );
574
+ if (tokenValidationState === "success") {
575
+ tokenInputAdornment = /* @__PURE__ */ jsx(
576
+ CheckCircleIcon,
577
+ {
578
+ style: {
579
+ color: "#3E8635",
580
+ fontSize: 20,
581
+ width: 20,
582
+ height: 20,
583
+ marginRight: 3
584
+ }
585
+ }
586
+ );
587
+ } else if (tokenValidationState === "error") {
588
+ tokenInputAdornment = /* @__PURE__ */ jsx(
589
+ ExclamationCircleIcon,
590
+ {
591
+ style: {
592
+ color: "#C9190B",
593
+ fontSize: 20,
594
+ width: 20,
595
+ height: 20,
596
+ marginRight: 3
597
+ }
598
+ }
599
+ );
600
+ }
601
+ const isUsingOrganizationCredentialInModal = hasSavedTokenInModal && !canRemovePersonalToken;
602
+ const saveServerToken = useCallback(async () => {
603
+ if (!editingServer || !canManageMcp) return;
604
+ if (hasSavedTokenInModal && tokenInputValue === SAVED_TOKEN_MASK) {
605
+ closeConfigureModal();
606
+ return;
607
+ }
608
+ const token = tokenInputValue.trim();
609
+ const hasToken = token.length > 0;
610
+ setTokenValidationState("validating");
611
+ setTokenValidationMessage(t("mcp.settings.token.validating"));
612
+ try {
613
+ if (hasToken) {
614
+ if (!editingServer.url) {
615
+ setTokenValidationState("error");
616
+ setTokenValidationMessage(
617
+ t("mcp.settings.token.urlUnavailableForValidation")
618
+ );
619
+ return;
620
+ }
621
+ const credentialValidation = await validateCredentials(
622
+ editingServer.url,
623
+ token
624
+ );
625
+ if (!credentialValidation.valid) {
626
+ setTokenValidationState("error");
627
+ setTokenValidationMessage(
628
+ credentialValidation.error ?? t("mcp.settings.token.invalidCredentials")
629
+ );
630
+ return;
631
+ }
632
+ }
633
+ setTokenValidationMessage(t("mcp.settings.token.savingAndValidating"));
634
+ await patchServer(editingServer.name, {
635
+ enabled: editingServer.enabled,
636
+ token: hasToken ? token : null
637
+ });
638
+ const validationResult = await validateServer(editingServer.name);
639
+ if (validationResult.status === "error") {
640
+ setTokenValidationState("error");
641
+ setTokenValidationMessage(
642
+ validationResult.validation?.error ?? t("mcp.settings.token.validationFailed")
643
+ );
644
+ return;
645
+ }
646
+ setTokenValidationState("success");
647
+ setTokenValidationMessage(t("mcp.settings.token.connectionSuccessful"));
648
+ closeConfigureModal();
649
+ } catch (e) {
650
+ setTokenValidationState("error");
651
+ setTokenValidationMessage(
652
+ e instanceof Error ? e.message : `Failed to update ${editingServer.name} token`
653
+ );
654
+ }
655
+ }, [
656
+ canManageMcp,
657
+ closeConfigureModal,
658
+ editingServer,
659
+ hasSavedTokenInModal,
660
+ patchServer,
661
+ t,
662
+ tokenInputValue,
663
+ validateCredentials,
664
+ validateServer
665
+ ]);
666
+ const removePersonalToken = () => {
667
+ setHasSavedTokenInModal(false);
668
+ setCanRemovePersonalToken(false);
669
+ setTokenInputValue("");
670
+ setTokenValidationState("idle");
671
+ setTokenValidationMessage("");
672
+ };
673
+ return /* @__PURE__ */ jsxs(
674
+ "div",
675
+ {
676
+ className: classes.root,
677
+ style: backgroundColor ? { backgroundColor } : undefined,
678
+ children: [
679
+ /* @__PURE__ */ jsxs("div", { className: classes.headerRow, children: [
680
+ /* @__PURE__ */ jsxs("div", { children: [
681
+ /* @__PURE__ */ jsx(Title, { headingLevel: "h2", size: "xl", className: classes.title, children: t("mcp.settings.title") }),
682
+ /* @__PURE__ */ jsx("div", { className: classes.selectedCount, children: t("mcp.settings.selectedCount", {
683
+ selectedCount: String(selectedCount),
684
+ totalCount: String(servers.length)
685
+ }) })
686
+ ] }),
687
+ /* @__PURE__ */ jsx(
688
+ Button,
689
+ {
690
+ "aria-label": t("mcp.settings.closeAriaLabel"),
691
+ icon: /* @__PURE__ */ jsx(TimesIcon, {}),
692
+ variant: "plain",
693
+ className: classes.closeButton,
694
+ onClick: onClose
695
+ }
696
+ )
697
+ ] }),
698
+ error && /* @__PURE__ */ jsx(
699
+ Alert,
700
+ {
701
+ variant: "danger",
702
+ isInline: true,
703
+ title: error,
704
+ className: classes.alert
705
+ }
706
+ ),
707
+ !mcpManagePermission.loading && !canManageMcp && /* @__PURE__ */ jsx(
708
+ Alert,
709
+ {
710
+ variant: "info",
711
+ isInline: true,
712
+ title: t("mcp.settings.readOnlyAccess"),
713
+ className: classes.alert
714
+ }
715
+ ),
716
+ /* @__PURE__ */ jsxs(
717
+ Table,
718
+ {
719
+ variant: "compact",
720
+ "aria-label": t("mcp.settings.tableAriaLabel"),
721
+ className: classes.table,
722
+ children: [
723
+ /* @__PURE__ */ jsx(Thead, { children: /* @__PURE__ */ jsxs(Tr, { children: [
724
+ /* @__PURE__ */ jsx(Th, { width: 10, screenReaderText: t("mcp.settings.enabled") }),
725
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(
726
+ Button,
727
+ {
728
+ variant: "link",
729
+ className: classes.nameHeaderButton,
730
+ icon: sortAsc ? /* @__PURE__ */ jsx(SortAmountDownIcon, {}) : /* @__PURE__ */ jsx(SortAmountUpIcon, {}),
731
+ iconPosition: "right",
732
+ onClick: () => setSortAsc((prev) => !prev),
733
+ children: /* @__PURE__ */ jsx(Typography, { component: "span", className: classes.nameHeaderText, children: t("mcp.settings.name") })
734
+ }
735
+ ) }),
736
+ /* @__PURE__ */ jsx(Th, { className: classes.statusHeader, children: t("mcp.settings.status") }),
737
+ /* @__PURE__ */ jsx(Th, { screenReaderText: t("mcp.settings.edit") })
738
+ ] }) }),
739
+ /* @__PURE__ */ jsxs(Tbody, { children: [
740
+ isLoading && /* @__PURE__ */ jsx(Tr, { children: /* @__PURE__ */ jsx(Td, { colSpan: 4, children: t("mcp.settings.loading") }) }),
741
+ !isLoading && sortedServers.length === 0 && /* @__PURE__ */ jsx(Tr, { children: /* @__PURE__ */ jsx(Td, { colSpan: 4, children: t("mcp.settings.noneAvailable") }) }),
742
+ sortedServers.map((server) => {
743
+ const displayStatus = getDisplayStatus(server);
744
+ const displayDetail = getDisplayDetail(server, displayStatus);
745
+ let statusClass = classes.statusWarn;
746
+ if (displayStatus === "ok") {
747
+ statusClass = classes.statusOk;
748
+ } else if (displayStatus === "tokenRequired") {
749
+ statusClass = classes.statusToken;
750
+ } else if (displayStatus === "disabled") {
751
+ statusClass = classes.statusDisabled;
752
+ }
753
+ return /* @__PURE__ */ jsxs(Tr, { children: [
754
+ /* @__PURE__ */ jsx(Td, { width: 10, className: classes.toggleCell, children: (() => {
755
+ const isUnavailable = displayStatus === "failed" || displayStatus === "tokenRequired";
756
+ const isChecked = isUnavailable ? false : server.enabled;
757
+ const isRowSaving = Boolean(isSaving[server.name]);
758
+ const isToggleDisabled = isUnavailable || isRowSaving || !canManageMcp;
759
+ const switchControl = /* @__PURE__ */ jsx(
760
+ Switch,
761
+ {
762
+ id: `mcp-switch-${server.id}`,
763
+ "aria-label": t(
764
+ "mcp.settings.toggleServerAriaLabel",
765
+ {
766
+ serverName: server.name
767
+ }
768
+ ),
769
+ isChecked,
770
+ isDisabled: isToggleDisabled,
771
+ onChange: (_event, checked) => {
772
+ void patchServer(server.name, {
773
+ enabled: checked
774
+ }).catch(() => {
775
+ });
776
+ }
777
+ }
778
+ );
779
+ if (!isToggleDisabled) {
780
+ return switchControl;
781
+ }
782
+ return /* @__PURE__ */ jsx(Tooltip, { content: displayDetail, children: /* @__PURE__ */ jsx(Typography, { component: "span", children: switchControl }) });
783
+ })() }),
784
+ /* @__PURE__ */ jsx(
785
+ Td,
786
+ {
787
+ width: 35,
788
+ className: `${classes.rowName} ${classes.nameCell}`,
789
+ children: /* @__PURE__ */ jsx(Typography, { component: "span", className: classes.nameValue, children: server.name })
790
+ }
791
+ ),
792
+ /* @__PURE__ */ jsx(Td, { width: 40, className: classes.statusColumnCell, children: /* @__PURE__ */ jsxs("div", { className: classes.statusCell, children: [
793
+ getStatusIcon(displayStatus, statusClass),
794
+ displayStatus === "failed" ? /* @__PURE__ */ jsx(
795
+ Tooltip,
796
+ {
797
+ content: server.validationError ?? t("mcp.settings.token.validationFailed"),
798
+ children: /* @__PURE__ */ jsx(
799
+ Typography,
800
+ {
801
+ component: "span",
802
+ className: classes.statusValue,
803
+ children: displayDetail
804
+ }
805
+ )
806
+ }
807
+ ) : /* @__PURE__ */ jsx(
808
+ Typography,
809
+ {
810
+ component: "span",
811
+ className: classes.statusValue,
812
+ children: displayDetail
813
+ }
814
+ )
815
+ ] }) }),
816
+ /* @__PURE__ */ jsx(Td, { width: 15, isActionCell: true, style: { textAlign: "right" }, children: /* @__PURE__ */ jsx(
817
+ Button,
818
+ {
819
+ "aria-label": t("mcp.settings.editServerAriaLabel", {
820
+ serverName: server.name
821
+ }),
822
+ icon: /* @__PURE__ */ jsx(ModeEditOutlineOutlinedIcon, { fontSize: "small" }),
823
+ variant: "plain",
824
+ className: classes.actionButton,
825
+ isDisabled: !canManageMcp,
826
+ onClick: () => openConfigureModal(server)
827
+ }
828
+ ) })
829
+ ] }, server.id);
830
+ })
831
+ ] })
832
+ ]
833
+ }
834
+ ),
835
+ /* @__PURE__ */ jsx(
836
+ Modal,
837
+ {
838
+ variant: "small",
839
+ title: t("mcp.settings.configureServerTitle", {
840
+ serverName: editingServer?.name ?? ""
841
+ }),
842
+ isOpen: Boolean(editingServer),
843
+ onClose: closeConfigureModal,
844
+ className: classes.configureModal,
845
+ children: /* @__PURE__ */ jsxs("div", { className: classes.modalContent, children: [
846
+ /* @__PURE__ */ jsx(
847
+ IconButton,
848
+ {
849
+ "aria-label": t("mcp.settings.closeConfigureModalAriaLabel"),
850
+ size: "small",
851
+ className: classes.modalCustomCloseButton,
852
+ onClick: closeConfigureModal,
853
+ children: /* @__PURE__ */ jsx(CloseOutlinedIcon, {})
854
+ }
855
+ ),
856
+ /* @__PURE__ */ jsxs("div", { className: classes.modalHeading, children: [
857
+ /* @__PURE__ */ jsx(LockIcon, {}),
858
+ /* @__PURE__ */ jsx(Typography, { component: "div", children: t("mcp.settings.configureServerTitle", {
859
+ serverName: editingServer?.name ?? ""
860
+ }) })
861
+ ] }),
862
+ /* @__PURE__ */ jsx("div", { className: classes.modalDescription, children: t("mcp.settings.modalDescription") }),
863
+ /* @__PURE__ */ jsxs("div", { className: classes.tokenRow, children: [
864
+ /* @__PURE__ */ jsx(
865
+ TextField,
866
+ {
867
+ id: "mcp-pat-input",
868
+ type: "password",
869
+ variant: "outlined",
870
+ fullWidth: true,
871
+ value: tokenInputValue,
872
+ onChange: (event) => onTokenInputChange(event.target.value),
873
+ className: `${classes.tokenInput} ${tokenInputStateClass}`,
874
+ label: hasSavedTokenInModal ? t("mcp.settings.savedToken") : t("mcp.settings.personalAccessToken"),
875
+ InputProps: {
876
+ endAdornment: /* @__PURE__ */ jsx(InputAdornment, { position: "end", children: tokenInputAdornment })
877
+ }
878
+ }
879
+ ),
880
+ (isUsingOrganizationCredentialInModal || !hasSavedTokenInModal || tokenValidationState !== "idle") && /* @__PURE__ */ jsx(
881
+ "div",
882
+ {
883
+ className: classes.tokenHelper,
884
+ style: { color: tokenHelperColor },
885
+ children: tokenValidationMessage || (isUsingOrganizationCredentialInModal ? t("mcp.settings.usingAdminCredential") : t("mcp.settings.enterToken"))
886
+ }
887
+ )
888
+ ] }),
889
+ /* @__PURE__ */ jsxs("div", { className: classes.modalActions, children: [
890
+ /* @__PURE__ */ jsx(
891
+ Button,
892
+ {
893
+ variant: "primary",
894
+ onClick: () => void saveServerToken(),
895
+ isDisabled: !canManageMcp || Boolean(isSaving[editingServer?.name ?? ""]) || tokenValidationState === "validating",
896
+ className: classes.modalActionButton,
897
+ children: t("modal.save")
898
+ },
899
+ "save"
900
+ ),
901
+ canRemovePersonalToken && hasSavedTokenInModal && /* @__PURE__ */ jsx(
902
+ Button,
903
+ {
904
+ variant: "plain",
905
+ onClick: removePersonalToken,
906
+ isDisabled: !canManageMcp || Boolean(isSaving[editingServer?.name ?? ""]) || tokenValidationState === "validating",
907
+ className: classes.forgetTokenButton,
908
+ children: t("mcp.settings.removePersonalToken")
909
+ },
910
+ "forget-token"
911
+ ),
912
+ /* @__PURE__ */ jsx(
913
+ Button,
914
+ {
915
+ variant: "link",
916
+ onClick: closeConfigureModal,
917
+ className: classes.modalCancelButton,
918
+ children: t("common.cancel")
919
+ },
920
+ "cancel"
921
+ )
922
+ ] })
923
+ ] })
924
+ }
925
+ )
926
+ ]
927
+ }
928
+ );
929
+ };
930
+
931
+ export { McpServersSettings };
932
+ //# sourceMappingURL=McpServersSettings.esm.js.map